diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index c3b3ebd53d..771c9e1472 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/devcontainers/python:1-3.13 +FROM mcr.microsoft.com/devcontainers/python:3.14 ENV \ DEBIAN_FRONTEND=noninteractive \ diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 2326565499..2895f5c51a 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -67,7 +67,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w ```yaml diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index df08cb58ae..08fc7bb9c9 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -251,7 +251,6 @@ For browser support, API details, and current specifications, refer to these aut **Available Dialog Types:** - `ha-wa-dialog` - Preferred for new dialogs (Web Awesome based) -- `ha-md-dialog` - Material Design 3 dialog component - `ha-dialog` - Legacy component (still widely used) **Opening Dialogs (Fire Event Pattern - Recommended):** @@ -621,7 +620,6 @@ this.hass.localize("ui.panel.config.updates.update_available", { #### Key Terminology -- **"add-on"** (hyphenated, not "addon") - **"integration"** (preferred over "component") - **Technical terms**: Use lowercase (automation, entity, device, service) @@ -713,7 +711,7 @@ this.hass.localize("ui.panel.config.automation.delete_confirm", { - [ ] American English spelling - [ ] Friendly, informational tone - [ ] Avoids abbreviations and jargon -- [ ] Correct terminology (add-on not addon, integration not component) +- [ ] Correct terminology (integration not component) ### Component-Specific Checks diff --git a/.github/labeler.yml b/.github/labeler.yml index e772cc81a5..244a249378 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -44,8 +44,3 @@ GitHub Actions: - any-glob-to-any-file: - .github/workflows/** - .github/*.yml - -Supervisor: - - changed-files: - - any-glob-to-any-file: - - hassio/src/** diff --git a/.github/workflows/cast_deployment.yaml b/.github/workflows/cast_deployment.yaml index fc57d26236..21ddb5f05e 100644 --- a/.github/workflows/cast_deployment.yaml +++ b/.github/workflows/cast_deployment.yaml @@ -21,12 +21,12 @@ jobs: url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} steps: - name: Check out files from GitHub - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: dev - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: ".nvmrc" cache: yarn @@ -56,12 +56,12 @@ jobs: url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} steps: - name: Check out files from GitHub - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: master - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: ".nvmrc" cache: yarn diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 714ab47c27..b1d789d001 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,9 +24,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out files from GitHub - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: ".nvmrc" cache: yarn @@ -37,7 +37,7 @@ jobs: - name: Build resources run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages - name: Setup lint cache - uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: | node_modules/.cache/prettier @@ -58,9 +58,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out files from GitHub - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: ".nvmrc" cache: yarn @@ -76,9 +76,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out files from GitHub - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: ".nvmrc" cache: yarn @@ -94,27 +94,10 @@ jobs: name: frontend-bundle-stats path: build/stats/*.json if-no-files-found: error - supervisor: - name: Build supervisor - needs: [lint, test] - runs-on: ubuntu-latest - steps: - - name: Check out files from GitHub - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 - with: - node-version-file: ".nvmrc" - cache: yarn - - name: Install dependencies - run: yarn install --immutable - - name: Build Application - run: ./node_modules/.bin/gulp build-hassio - env: - IS_TEST: "true" - - name: Upload bundle stats + - name: Upload frontend build uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: - name: supervisor-bundle-stats - path: build/stats/*.json + name: frontend-build + path: hass_frontend/ if-no-files-found: error + retention-days: 7 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a371224428..0c796a63ee 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. @@ -36,14 +36,14 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 with: languages: ${{ matrix.language }} # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/autobuild@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -57,4 +57,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 + uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 diff --git a/.github/workflows/demo_deployment.yaml b/.github/workflows/demo_deployment.yaml index 38671a636a..9ed52e8171 100644 --- a/.github/workflows/demo_deployment.yaml +++ b/.github/workflows/demo_deployment.yaml @@ -22,12 +22,12 @@ jobs: url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} steps: - name: Check out files from GitHub - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: dev - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: ".nvmrc" cache: yarn @@ -57,12 +57,12 @@ jobs: url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} steps: - name: Check out files from GitHub - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: master - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: ".nvmrc" cache: yarn diff --git a/.github/workflows/design_deployment.yaml b/.github/workflows/design_deployment.yaml index eb8a73ea08..8f82fb2825 100644 --- a/.github/workflows/design_deployment.yaml +++ b/.github/workflows/design_deployment.yaml @@ -16,10 +16,10 @@ jobs: url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} steps: - name: Check out files from GitHub - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: ".nvmrc" cache: yarn diff --git a/.github/workflows/design_preview.yaml b/.github/workflows/design_preview.yaml index 8cb75a67a1..5daade844b 100644 --- a/.github/workflows/design_preview.yaml +++ b/.github/workflows/design_preview.yaml @@ -21,10 +21,10 @@ jobs: if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview') steps: - name: Check out files from GitHub - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: ".nvmrc" cache: yarn diff --git a/.github/workflows/nightly.yaml b/.github/workflows/nightly.yaml index 6c2646cf97..de82272ad1 100644 --- a/.github/workflows/nightly.yaml +++ b/.github/workflows/nightly.yaml @@ -6,7 +6,7 @@ on: - cron: "0 1 * * *" env: - PYTHON_VERSION: "3.13" + PYTHON_VERSION: "3.14" NODE_OPTIONS: --max_old_space_size=6144 permissions: @@ -20,15 +20,15 @@ jobs: contents: write steps: - name: Checkout the repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: python-version: ${{ env.PYTHON_VERSION }} - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: ".nvmrc" cache: yarn diff --git a/.github/workflows/relative-ci.yaml b/.github/workflows/relative-ci.yaml index ec9f79d2ad..22bb409b8f 100644 --- a/.github/workflows/relative-ci.yaml +++ b/.github/workflows/relative-ci.yaml @@ -12,7 +12,7 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'success' }} strategy: matrix: - bundle: [frontend, supervisor] + bundle: [frontend] build: [modern, legacy] runs-on: ubuntu-latest steps: diff --git a/.github/workflows/release-drafter.yaml b/.github/workflows/release-drafter.yaml index 950067534b..af08380321 100644 --- a/.github/workflows/release-drafter.yaml +++ b/.github/workflows/release-drafter.yaml @@ -18,6 +18,6 @@ jobs: pull-requests: read runs-on: ubuntu-latest steps: - - uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6.1.0 + - uses: release-drafter/release-drafter@6db134d15f3909ccc9eefd369f02bd1e9cffdf97 # v6.2.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b8d28b553f..2adf51c41f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -6,7 +6,7 @@ on: - published env: - PYTHON_VERSION: "3.13" + PYTHON_VERSION: "3.14" NODE_OPTIONS: --max_old_space_size=6144 # Set default workflow permissions @@ -26,10 +26,10 @@ jobs: if: github.repository_owner == 'home-assistant' steps: - name: Checkout the repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ env.PYTHON_VERSION }} @@ -37,7 +37,7 @@ jobs: uses: home-assistant/actions/helpers/verify-version@master - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: ".nvmrc" cache: yarn @@ -84,7 +84,7 @@ jobs: - name: Build wheels uses: home-assistant/wheels@2025.12.0 with: - abi: cp313 + abi: cp314 tag: musllinux_1_2 arch: amd64 wheels-key: ${{ secrets.WHEELS_KEY }} @@ -98,9 +98,9 @@ jobs: contents: write # Required to upload release assets steps: - name: Checkout the repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version-file: ".nvmrc" cache: yarn @@ -118,32 +118,3 @@ jobs: uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 with: files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz - - release-supervisor: - name: Release supervisor frontend - if: github.event.release.prerelease == false - runs-on: ubuntu-latest - permissions: - contents: write # Required to upload release assets - steps: - - name: Checkout the repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - name: Setup Node - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 - with: - node-version-file: ".nvmrc" - cache: yarn - - name: Install dependencies - run: yarn install - - name: Download Translations - run: ./script/translations_download - env: - LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} - - name: Build supervisor - run: hassio/script/build_hassio - - name: Tar folder - run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build . - - name: Upload release asset - uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 - with: - files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 9739516249..e2b3495157 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Upload Translations run: | diff --git a/.nvmrc b/.nvmrc index cf2efde811..f94d3c2ea9 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.13.0 \ No newline at end of file +24.13.1 \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 401f183930..49291d22ba 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -73,37 +73,6 @@ "instanceLimit": 1 } }, - { - "label": "Develop Supervisor panel", - "type": "gulp", - "task": "develop-hassio", - "problemMatcher": { - "owner": "ha-build", - "source": "ha-build", - "fileLocation": "absolute", - "severity": "error", - "pattern": [ - { - "regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)", - "severity": 1, - "file": 2, - "message": 3, - "line": 4, - "column": 5 - } - ], - "background": { - "activeOnStart": true, - "beginsPattern": "Changes detected. Starting compilation", - "endsPattern": "Build done @" - } - }, - "isBackground": true, - "group": "build", - "runOptions": { - "instanceLimit": 1 - } - }, { "label": "Develop Gallery", "type": "gulp", @@ -246,20 +215,6 @@ "instanceLimit": 1 } }, - { - "label": "Run HA Core for Supervisor in devcontainer", - "type": "shell", - "command": "SUPERVISOR=${input:supervisorHost} SUPERVISOR_TOKEN=${input:supervisorToken} script/core", - "isBackground": true, - "group": { - "kind": "build", - "isDefault": true - }, - "problemMatcher": [], - "runOptions": { - "instanceLimit": 1 - } - }, { "label": "Setup and fetch nightly translations", "type": "gulp", @@ -268,16 +223,6 @@ } ], "inputs": [ - { - "id": "supervisorHost", - "type": "promptString", - "description": "The IP of the Supervisor host running the Remote API proxy add-on" - }, - { - "id": "supervisorToken", - "type": "promptString", - "description": "The token for the Remote API proxy add-on" - }, { "id": "coreUrl", "type": "promptString", diff --git a/README.md b/README.md index 0e6f470a01..4c3d20c4b8 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ This is the repository for the official [Home Assistant](https://home-assistant. - Development: [Instructions](https://developers.home-assistant.io/docs/frontend/development/) - Production build: `script/build_frontend` - Gallery: `cd gallery && script/develop_gallery` -- Supervisor: [Instructions](https://developers.home-assistant.io/docs/supervisor/developing) ## Frontend development diff --git a/build-scripts/bundle.cjs b/build-scripts/bundle.cjs index 5381e40730..2778a32306 100644 --- a/build-scripts/bundle.cjs +++ b/build-scripts/bundle.cjs @@ -18,14 +18,14 @@ module.exports.sourceMapURL = () => { module.exports.ignorePackages = () => []; // Files from NPM packages that we should replace with empty file -module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) => +module.exports.emptyPackages = ({ isLandingPageBuild }) => [ - // Icons in supervisor conflict with icons in HA so we don't load. - (isHassioBuild || isLandingPageBuild) && + // Icons in landingpage conflict with icons in HA so we don't load. + isLandingPageBuild && require.resolve( path.resolve(paths.root_dir, "src/components/ha-icon.ts") ), - (isHassioBuild || isLandingPageBuild) && + isLandingPageBuild && require.resolve( path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts") ), @@ -36,7 +36,6 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({ __BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"), __VERSION__: JSON.stringify(env.version()), __DEMO__: false, - __SUPERVISOR__: false, __BACKWARDS_COMPAT__: false, __STATIC_PATH__: "/static/", __HASS_URL__: `\`${ @@ -289,26 +288,6 @@ module.exports.config = { }; }, - hassio({ isProdBuild, latestBuild, isStatsBuild, isTestBuild }) { - return { - name: "supervisor" + nameSuffix(latestBuild), - entry: { - entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"), - }, - outputPath: outputPath(paths.hassio_output_root, latestBuild), - publicPath: publicPath(latestBuild, paths.hassio_publicPath), - isProdBuild, - latestBuild, - isStatsBuild, - isTestBuild, - isHassioBuild: true, - defineOverlay: { - __SUPERVISOR__: true, - __STATIC_PATH__: `"${paths.hassio_publicPath}/static/"`, - }, - }; - }, - gallery({ isProdBuild, latestBuild }) { return { name: "gallery" + nameSuffix(latestBuild), diff --git a/build-scripts/gulp/clean.js b/build-scripts/gulp/clean.js index b7c570c9d4..0c4cc0a76b 100644 --- a/build-scripts/gulp/clean.js +++ b/build-scripts/gulp/clean.js @@ -24,10 +24,6 @@ gulp.task( ) ); -gulp.task("clean-hassio", async () => - deleteSync([paths.hassio_output_root, paths.build_dir]) -); - gulp.task( "clean-gallery", gulp.parallel("clean-translations", async () => diff --git a/build-scripts/gulp/compress.js b/build-scripts/gulp/compress.js index 81e1c87abe..62df03ed54 100644 --- a/build-scripts/gulp/compress.js +++ b/build-scripts/gulp/compress.js @@ -43,29 +43,11 @@ const compressAppModernBrotli = () => const compressAppModernZopfli = () => compressModern(paths.app_output_root, paths.app_output_latest, "zopfli"); -const compressHassioModernBrotli = () => - compressModern( - paths.hassio_output_root, - paths.hassio_output_latest, - "brotli" - ); -const compressHassioModernZopfli = () => - compressModern( - paths.hassio_output_root, - paths.hassio_output_latest, - "zopfli" - ); - const compressAppOtherBrotli = () => compressOther(paths.app_output_root, paths.app_output_latest, "brotli"); const compressAppOtherZopfli = () => compressOther(paths.app_output_root, paths.app_output_latest, "zopfli"); -const compressHassioOtherBrotli = () => - compressOther(paths.hassio_output_root, paths.hassio_output_latest, "brotli"); -const compressHassioOtherZopfli = () => - compressOther(paths.hassio_output_root, paths.hassio_output_latest, "zopfli"); - gulp.task( "compress-app", gulp.parallel( @@ -75,12 +57,3 @@ gulp.task( compressAppOtherZopfli ) ); -gulp.task( - "compress-hassio", - gulp.parallel( - compressHassioModernBrotli, - compressHassioOtherBrotli, - compressHassioModernZopfli, - compressHassioOtherZopfli - ) -); diff --git a/build-scripts/gulp/entry-html.js b/build-scripts/gulp/entry-html.js index d41dd8f457..4eb8e91e88 100644 --- a/build-scripts/gulp/entry-html.js +++ b/build-scripts/gulp/entry-html.js @@ -266,28 +266,3 @@ gulp.task( paths.landingPage_output_es5 ) ); - -const HASSIO_PAGE_ENTRIES = { "entrypoint.js": ["entrypoint"] }; - -gulp.task( - "gen-pages-hassio-dev", - genPagesDevTask( - HASSIO_PAGE_ENTRIES, - paths.hassio_dir, - paths.hassio_output_root, - "src", - paths.hassio_publicPath - ) -); - -gulp.task( - "gen-pages-hassio-prod", - genPagesProdTask( - HASSIO_PAGE_ENTRIES, - paths.hassio_dir, - paths.hassio_output_root, - paths.hassio_output_latest, - paths.hassio_output_es5, - "src" - ) -); diff --git a/build-scripts/gulp/gather-static.js b/build-scripts/gulp/gather-static.js index 0c8637c21c..88b1f4d3cc 100644 --- a/build-scripts/gulp/gather-static.js +++ b/build-scripts/gulp/gather-static.js @@ -123,22 +123,11 @@ gulp.task("copy-translations-app", async () => { copyTranslations(staticDir); }); -gulp.task("copy-translations-supervisor", async () => { - const staticDir = paths.hassio_output_static; - copyTranslations(staticDir); -}); - gulp.task("copy-translations-landing-page", async () => { const staticDir = paths.landingPage_output_static; copyTranslations(staticDir); }); -gulp.task("copy-static-supervisor", async () => { - const staticDir = paths.hassio_output_static; - copyLocaleData(staticDir); - copyFonts(staticDir); -}); - gulp.task("copy-static-app", async () => { const staticDir = paths.app_output_static; // Basic static files diff --git a/build-scripts/gulp/hassio.js b/build-scripts/gulp/hassio.js deleted file mode 100644 index a09c0d1ea4..0000000000 --- a/build-scripts/gulp/hassio.js +++ /dev/null @@ -1,45 +0,0 @@ -import gulp from "gulp"; -import env from "../env.cjs"; -import "./clean.js"; -import "./compress.js"; -import "./entry-html.js"; -import "./gather-static.js"; -import "./gen-icons-json.js"; -import "./translations.js"; -import "./rspack.js"; - -gulp.task( - "develop-hassio", - gulp.series( - async function setEnv() { - process.env.NODE_ENV = "development"; - }, - "clean-hassio", - "gen-dummy-icons-json", - "gen-pages-hassio-dev", - "build-supervisor-translations", - "copy-translations-supervisor", - "build-locale-data", - "copy-static-supervisor", - "rspack-watch-hassio" - ) -); - -gulp.task( - "build-hassio", - gulp.series( - async function setEnv() { - process.env.NODE_ENV = "production"; - }, - "clean-hassio", - "gen-dummy-icons-json", - "build-supervisor-translations", - "copy-translations-supervisor", - "build-locale-data", - "copy-static-supervisor", - "rspack-prod-hassio", - "gen-pages-hassio-prod", - ...// Don't compress running tests - (env.isTestBuild() ? [] : ["compress-hassio"]) - ) -); diff --git a/build-scripts/gulp/index.mjs b/build-scripts/gulp/index.mjs index 8ae6311ff2..e7cdcc3368 100644 --- a/build-scripts/gulp/index.mjs +++ b/build-scripts/gulp/index.mjs @@ -9,7 +9,6 @@ import "./fetch-nightly-translations.js"; import "./gallery.js"; import "./gather-static.js"; import "./gen-icons-json.js"; -import "./hassio.js"; import "./landing-page.js"; import "./locale-data.js"; import "./rspack.js"; diff --git a/build-scripts/gulp/rspack.js b/build-scripts/gulp/rspack.js index 84f9d71079..2080c84a87 100644 --- a/build-scripts/gulp/rspack.js +++ b/build-scripts/gulp/rspack.js @@ -13,7 +13,6 @@ import { createCastConfig, createDemoConfig, createGalleryConfig, - createHassioConfig, createLandingPageConfig, } from "../rspack.cjs"; @@ -159,31 +158,6 @@ gulp.task("rspack-prod-cast", () => ) ); -gulp.task("rspack-watch-hassio", () => { - // This command will run forever because we don't close compiler - rspack( - createHassioConfig({ - isProdBuild: false, - latestBuild: true, - }) - ).watch({ ignored: /build/, poll: isWsl }, doneHandler()); - - gulp.watch( - path.join(paths.translations_src, "en.json"), - gulp.series("build-supervisor-translations", "copy-translations-supervisor") - ); -}); - -gulp.task("rspack-prod-hassio", () => - prodBuild( - bothBuilds(createHassioConfig, { - isProdBuild: true, - isStatsBuild: env.isStatsBuild(), - isTestBuild: env.isTestBuild(), - }) - ) -); - gulp.task("rspack-dev-server-gallery", () => runDevServer({ compiler: rspack( diff --git a/build-scripts/gulp/translations.js b/build-scripts/gulp/translations.js index 76af601e89..c5d4275dbf 100755 --- a/build-scripts/gulp/translations.js +++ b/build-scripts/gulp/translations.js @@ -170,9 +170,7 @@ const setFragment = (fragment) => async () => { }; const panelFragment = (fragment) => - fragment !== "base" && - fragment !== "supervisor" && - fragment !== "landing-page"; + fragment !== "base" && fragment !== "landing-page"; const HASHES = new Map(); @@ -207,18 +205,15 @@ const createTranslations = async () => { FRAGMENTS.map((fragment) => { switch (fragment) { case "base": - // Remove the panels and supervisor to create the base translations + // Remove the panels and landing-page to create the base translations return [ flatten({ ...data, ui: { ...data.ui, panel: undefined }, - supervisor: undefined, + "landing-page": undefined, }), "", ]; - case "supervisor": - // Supervisor key is at the top level - return [flatten(data.supervisor), ""]; case "landing-page": // landing-page key is at the top level return [flatten(data["landing-page"]), ""]; @@ -318,11 +313,6 @@ gulp.task( ) ); -gulp.task( - "build-supervisor-translations", - gulp.series(setFragment("supervisor"), "build-translations") -); - gulp.task( "build-landing-page-translations", gulp.series(setFragment("landing-page"), "build-translations") diff --git a/build-scripts/paths.cjs b/build-scripts/paths.cjs index b181ee7c00..43a784ab35 100644 --- a/build-scripts/paths.cjs +++ b/build-scripts/paths.cjs @@ -49,15 +49,5 @@ module.exports = { "../landing-page/dist/static" ), - hassio_dir: path.resolve(__dirname, "../hassio"), - hassio_output_root: path.resolve(__dirname, "../hassio/build"), - hassio_output_static: path.resolve(__dirname, "../hassio/build/static"), - hassio_output_latest: path.resolve( - __dirname, - "../hassio/build/frontend_latest" - ), - hassio_output_es5: path.resolve(__dirname, "../hassio/build/frontend_es5"), - hassio_publicPath: "/api/hassio/app", - translations_src: path.resolve(__dirname, "../src/translations"), }; diff --git a/build-scripts/rspack.cjs b/build-scripts/rspack.cjs index 7fe506c9d7..207cd10131 100644 --- a/build-scripts/rspack.cjs +++ b/build-scripts/rspack.cjs @@ -40,7 +40,6 @@ const createRspackConfig = ({ latestBuild, isStatsBuild, isTestBuild, - isHassioBuild, isLandingPageBuild, dontHash, }) => { @@ -168,13 +167,9 @@ const createRspackConfig = ({ ); }, }), - bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).length + bundle.emptyPackages({ isLandingPageBuild }).length ? new rspack.NormalModuleReplacementPlugin( - new RegExp( - bundle - .emptyPackages({ isHassioBuild, isLandingPageBuild }) - .join("|") - ), + new RegExp(bundle.emptyPackages({ isLandingPageBuild }).join("|")), path.resolve(paths.root_dir, "src/util/empty.js") ) : false, @@ -205,6 +200,7 @@ const createRspackConfig = ({ "lit/decorators$": "lit/decorators.js", "lit/directive$": "lit/directive.js", "lit/directives/until$": "lit/directives/until.js", + "lit/directives/ref$": "lit/directives/ref.js", "lit/directives/class-map$": "lit/directives/class-map.js", "lit/directives/style-map$": "lit/directives/style-map.js", "lit/directives/if-defined$": "lit/directives/if-defined.js", @@ -325,21 +321,6 @@ const createDemoConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => const createCastConfig = ({ isProdBuild, latestBuild }) => createRspackConfig(bundle.config.cast({ isProdBuild, latestBuild })); -const createHassioConfig = ({ - isProdBuild, - latestBuild, - isStatsBuild, - isTestBuild, -}) => - createRspackConfig( - bundle.config.hassio({ - isProdBuild, - latestBuild, - isStatsBuild, - isTestBuild, - }) - ); - const createGalleryConfig = ({ isProdBuild, latestBuild }) => createRspackConfig(bundle.config.gallery({ isProdBuild, latestBuild })); @@ -350,7 +331,6 @@ module.exports = { createAppConfig, createDemoConfig, createCastConfig, - createHassioConfig, createGalleryConfig, createRspackConfig, createLandingPageConfig, diff --git a/cast/src/launcher/layout/hc-cast.ts b/cast/src/launcher/layout/hc-cast.ts index 2af8599fa2..708f76ebd1 100644 --- a/cast/src/launcher/layout/hc-cast.ts +++ b/cast/src/launcher/layout/hc-cast.ts @@ -206,7 +206,7 @@ class HcCast extends LitElement { } private async _handlePickView(ev: CustomEvent) { - const path = this.lovelaceViews![ev.detail.index].path ?? ev.detail.index; + const path = this.lovelaceViews?.[ev.detail.index]?.path ?? ev.detail.index; await ensureConnectedCastSession(this.castManager!, this.auth!); castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path); } diff --git a/demo/src/ha-demo.ts b/demo/src/ha-demo.ts index 3406e0270d..1d2a735279 100644 --- a/demo/src/ha-demo.ts +++ b/demo/src/ha-demo.ts @@ -9,11 +9,14 @@ import { selectedDemoConfig } from "./configs/demo-configs"; import { mockAreaRegistry } from "./stubs/area_registry"; import { mockAuth } from "./stubs/auth"; import { mockConfigEntries } from "./stubs/config_entries"; +import { mockDeviceRegistry } from "./stubs/device_registry"; import { mockEnergy } from "./stubs/energy"; import { energyEntities } from "./stubs/entities"; import { mockEntityRegistry } from "./stubs/entity_registry"; import { mockEvents } from "./stubs/events"; +import { mockFloorRegistry } from "./stubs/floor_registry"; import { mockFrontend } from "./stubs/frontend"; +import { mockLabelRegistry } from "./stubs/label_registry"; import { mockIcons } from "./stubs/icons"; import { mockHistory } from "./stubs/history"; import { mockLovelace } from "./stubs/lovelace"; @@ -60,6 +63,9 @@ export class HaDemo extends HomeAssistantAppEl { mockPersistentNotification(hass); mockConfigEntries(hass); mockAreaRegistry(hass); + mockDeviceRegistry(hass); + mockFloorRegistry(hass); + mockLabelRegistry(hass); mockEntityRegistry(hass, [ { config_entry_id: "co2signal", diff --git a/demo/src/stubs/frontend.ts b/demo/src/stubs/frontend.ts index 70a4d5a0d2..9cdfadaff6 100644 --- a/demo/src/stubs/frontend.ts +++ b/demo/src/stubs/frontend.ts @@ -1,14 +1,12 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; -let changeFunction; +let sidebarChangeCallback; export const mockFrontend = (hass: MockHomeAssistant) => { - hass.mockWS("frontend/get_user_data", () => ({ - value: null, - })); + hass.mockWS("frontend/get_user_data", () => ({ value: null })); hass.mockWS("frontend/set_user_data", ({ key, value }) => { if (key === "sidebar") { - changeFunction?.({ + sidebarChangeCallback?.({ value: { panelOrder: value.panelOrder || [], hiddenPanels: value.hiddenPanels || [], @@ -16,15 +14,34 @@ export const mockFrontend = (hass: MockHomeAssistant) => { }); } }); - hass.mockWS("frontend/subscribe_user_data", (_msg, _hass, onChange) => { - changeFunction = onChange; + hass.mockWS("frontend/subscribe_user_data", (msg, _hass, onChange) => { + if (msg.key === "sidebar") { + sidebarChangeCallback = onChange; + } + onChange?.({ value: null }); + // eslint-disable-next-line @typescript-eslint/no-empty-function + return () => {}; + }); + hass.mockWS( + "frontend/subscribe_system_data", + (_msg, currentHass, onChange) => { + onChange?.({ + value: currentHass.systemData, + }); + // eslint-disable-next-line @typescript-eslint/no-empty-function + return () => {}; + } + ); + hass.mockWS("labs/subscribe", (_msg, _currentHass, onChange) => { onChange?.({ - value: { - panelOrder: [], - hiddenPanels: [], - }, + preview_feature: _msg.preview_feature, + domain: _msg.domain, + enabled: false, + is_built_in: true, }); // eslint-disable-next-line @typescript-eslint/no-empty-function return () => {}; }); + hass.mockWS("repairs/list_issues", () => ({ issues: [] })); + hass.mockWS("frontend/get_themes", (_msg, currentHass) => currentHass.themes); }; diff --git a/demo/src/stubs/lovelace.ts b/demo/src/stubs/lovelace.ts index e4544a10ef..06d6871450 100644 --- a/demo/src/stubs/lovelace.ts +++ b/demo/src/stubs/lovelace.ts @@ -29,6 +29,7 @@ export const mockLovelace = ( hass.mockWS("lovelace/config/save", () => Promise.resolve()); hass.mockWS("lovelace/resources", () => Promise.resolve([])); + hass.mockWS("lovelace/dashboards/list", () => Promise.resolve([])); }; customElements.whenDefined("hui-root").then(() => { diff --git a/demo/src/stubs/template.ts b/demo/src/stubs/template.ts index ca77017c1a..82608cae4e 100644 --- a/demo/src/stubs/template.ts +++ b/demo/src/stubs/template.ts @@ -7,8 +7,18 @@ export const mockTemplate = (hass: MockHomeAssistant) => { }) ); hass.mockWS("render_template", (msg, _hass, onChange) => { + let result = msg.template; + // Simple variable substitution for demo purposes + if (msg.variables) { + for (const [key, value] of Object.entries(msg.variables)) { + result = result.replace( + new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, "g"), + String(value) + ); + } + } onChange!({ - result: msg.template, + result, listeners: { all: false, domains: [], entities: [], time: false }, }); // eslint-disable-next-line @typescript-eslint/no-empty-function diff --git a/eslint.config.mjs b/eslint.config.mjs index 401abeb4ec..a1aa9e96fa 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -43,7 +43,6 @@ export default tseslint.config( __BUILD__: false, __VERSION__: false, __STATIC_PATH__: false, - __SUPERVISOR__: false, }, parser: tseslint.parser, diff --git a/gallery/src/pages/brand/logo.markdown b/gallery/src/pages/brand/logo.markdown index e66cd398ef..7e1b3526d2 100644 --- a/gallery/src/pages/brand/logo.markdown +++ b/gallery/src/pages/brand/logo.markdown @@ -10,7 +10,9 @@ As a community, we are proud of our logo. Follow these guidelines to ensure it a ![Logo](/images/brand/logo.png) -Please note that this logo is not released under the CC license. All rights reserved. + +This logo is trademarked and the property of the Open Home Foundation. This means it is not available for commercial use without express written permission from the foundation. We regard commercial use as anything designed to market or promote a product, software or service that is for sale. Please contact partner@openhomefoundation.org for further information + # Design diff --git a/gallery/src/pages/brand/logo.ts b/gallery/src/pages/brand/logo.ts new file mode 100644 index 0000000000..5d7552808c --- /dev/null +++ b/gallery/src/pages/brand/logo.ts @@ -0,0 +1 @@ +import "../../../../src/components/ha-alert"; diff --git a/gallery/src/pages/components/ha-form.ts b/gallery/src/pages/components/ha-form.ts index dc2dc211ad..78550489f0 100644 --- a/gallery/src/pages/components/ha-form.ts +++ b/gallery/src/pages/components/ha-form.ts @@ -169,7 +169,7 @@ const SCHEMAS: { { title: "Selectors", translations: { - addon: "Addon", + app: "App", entity: "Entity", device: "Device", area: "Area", @@ -188,7 +188,7 @@ const SCHEMAS: { entities: "Entities", }, schema: [ - { name: "addon", selector: { addon: {} } }, + { name: "app", selector: { app: {} } }, { name: "entity", selector: { entity: {} } }, { name: "Attribute", diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index debcb1bcd8..038c425439 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -239,7 +239,7 @@ const SCHEMAS: { selector: { config_entry: {} }, }, duration: { name: "Duration", selector: { duration: {} } }, - addon: { name: "Addon", selector: { addon: {} } }, + app: { name: "App", selector: { app: {} } }, number_box: { name: "Number Box", selector: { diff --git a/gallery/src/pages/concepts/home.markdown b/gallery/src/pages/concepts/home.markdown index 3094cd36dc..901f393f3c 100644 --- a/gallery/src/pages/concepts/home.markdown +++ b/gallery/src/pages/concepts/home.markdown @@ -18,7 +18,7 @@ The Home Assistant interface is based on Material Design. It's a design system c We want to make it as easy for designers to contribute as it is for developers. There’s a lot a designer can contribute to: -- Meet us at devs_ux Discord. Feel free to share your designs, user test or strategic ideas. +- Meet us at Discord #designers channel. If you can't see the channel, make sure you set the correct role in Channels & Roles. - Start designing with our Figma DesignKit. - Find the latest UX discussions and issues on GitHub. Everyone can start a new issue or discussion! diff --git a/hassio/config.cjs b/hassio/config.cjs deleted file mode 100644 index 732fb98c68..0000000000 --- a/hassio/config.cjs +++ /dev/null @@ -1,9 +0,0 @@ -const path = require("path"); - -module.exports = { - // Target directory for the build. - buildDir: path.resolve(__dirname, "build"), - nodeDir: path.resolve(__dirname, "../node_modules"), - // Path where the Hass.io frontend will be publicly available. - publicPath: "/api/hassio/app", -}; diff --git a/hassio/script/build_hassio b/hassio/script/build_hassio deleted file mode 100755 index 193cbb0687..0000000000 --- a/hassio/script/build_hassio +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -# Builds the Hass.io app for production - -# Stop on errors -set -e - -cd "$(dirname "$0")/../.." - -./node_modules/.bin/gulp build-hassio diff --git a/hassio/script/develop b/hassio/script/develop deleted file mode 100755 index 0b62666b10..0000000000 --- a/hassio/script/develop +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -# Run the Hass.io development server - -# Stop on errors -set -e - -cd "$(dirname "$0")/../.." - -./node_modules/.bin/gulp develop-hassio diff --git a/hassio/src/addon-store/hassio-addon-store.ts b/hassio/src/addon-store/hassio-addon-store.ts deleted file mode 100644 index 867df14ac3..0000000000 --- a/hassio/src/addon-store/hassio-addon-store.ts +++ /dev/null @@ -1,248 +0,0 @@ -import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; - -import { mdiDotsVertical } from "@mdi/js"; -import type { PropertyValues, TemplateResult } from "lit"; -import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import { atLeastVersion } from "../../../src/common/config/version"; -import { fireEvent } from "../../../src/common/dom/fire_event"; -import { navigate } from "../../../src/common/navigate"; -import { extractSearchParam } from "../../../src/common/url/search-params"; -import "../../../src/components/ha-button-menu"; -import "../../../src/components/ha-icon-button"; -import "../../../src/components/ha-list-item"; -import "../../../src/components/search-input"; -import type { HassioAddonRepository } from "../../../src/data/hassio/addon"; -import { reloadHassioAddons } from "../../../src/data/hassio/addon"; -import { extractApiErrorMessage } from "../../../src/data/hassio/common"; -import type { StoreAddon } from "../../../src/data/supervisor/store"; -import type { Supervisor } from "../../../src/data/supervisor/supervisor"; -import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; -import "../../../src/layouts/hass-loading-screen"; -import "../../../src/layouts/hass-subpage"; -import type { HomeAssistant, Route } from "../../../src/types"; -import { showRegistriesDialog } from "../dialogs/registries/show-dialog-registries"; -import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories"; -import "./hassio-addon-repository"; - -const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => { - if (a.slug === "local") { - return -1; - } - if (b.slug === "local") { - return 1; - } - if (a.slug === "core") { - return -1; - } - if (b.slug === "core") { - return 1; - } - return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1; -}; - -@customElement("hassio-addon-store") -export class HassioAddonStore extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - @property({ type: Boolean }) public narrow = false; - - @property({ attribute: false }) public route!: Route; - - @state() private _filter?: string; - - public async refreshData() { - try { - await reloadHassioAddons(this.hass); - } catch (err) { - showAlertDialog(this, { - text: extractApiErrorMessage(err), - }); - } finally { - this._loadData(); - } - } - - protected render() { - let repos: (TemplateResult | typeof nothing)[] = []; - - if (this.supervisor.store.repositories) { - repos = this.addonRepositories( - this.supervisor.store.repositories, - this.supervisor.store.addons, - this._filter - ); - } - - return html` - - - - - ${this.supervisor.localize("store.check_updates")} - - - ${this.supervisor.localize("store.repositories")} - - ${this.hass.userData?.showAdvanced && - atLeastVersion(this.hass.config.version, 0, 117) - ? html` - ${this.supervisor.localize("store.registries")} - ` - : ""} - - ${repos.length === 0 - ? html`` - : html` - - - ${repos} - `} - ${!this.hass.userData?.showAdvanced - ? html` - - ` - : ""} - - `; - } - - protected firstUpdated(changedProps: PropertyValues) { - super.firstUpdated(changedProps); - const repositoryUrl = extractSearchParam("repository_url"); - navigate("/hassio/store", { replace: true }); - if (repositoryUrl) { - this._manageRepositories(repositoryUrl); - } - - this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev)); - this._loadData(); - } - - private addonRepositories = memoizeOne( - ( - repositories: HassioAddonRepository[], - addons: StoreAddon[], - filter?: string - ) => - repositories.sort(sortRepos).map((repo) => { - const filteredAddons = addons.filter( - (addon) => addon.repository === repo.slug - ); - - return filteredAddons.length !== 0 - ? html` - - ` - : nothing; - }) - ); - - private _handleAction(ev: CustomEvent) { - switch (ev.detail.index) { - case 0: - this.refreshData(); - break; - case 1: - this._manageRepositoriesClicked(); - break; - case 2: - this._manageRegistries(); - break; - } - } - - private _apiCalled(ev) { - if (ev.detail.success) { - this._loadData(); - } - } - - private _manageRepositoriesClicked() { - this._manageRepositories(); - } - - private _manageRepositories(url?: string) { - showRepositoriesDialog(this, { - supervisor: this.supervisor, - url, - }); - } - - private _manageRegistries() { - showRegistriesDialog(this, { supervisor: this.supervisor }); - } - - private _loadData() { - fireEvent(this, "supervisor-collection-refresh", { collection: "addon" }); - fireEvent(this, "supervisor-collection-refresh", { - collection: "supervisor", - }); - } - - private _filterChanged(e) { - this._filter = e.detail.value; - } - - static styles = css` - hassio-addon-repository { - margin-top: 24px; - } - .search { - position: sticky; - top: 0; - z-index: 2; - } - search-input { - display: block; - --mdc-text-field-fill-color: var(--sidebar-background-color); - --mdc-text-field-idle-line-color: var(--divider-color); - } - .advanced { - padding: 12px; - display: flex; - flex-wrap: wrap; - color: var(--primary-text-color); - } - .advanced a { - margin-left: 0.5em; - margin-inline-start: 0.5em; - margin-inline-end: initial; - color: var(--primary-color); - } - `; -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-addon-store": HassioAddonStore; - } -} diff --git a/hassio/src/addon-view/hassio-addon-dashboard.ts b/hassio/src/addon-view/hassio-addon-dashboard.ts deleted file mode 100644 index 9d663659ff..0000000000 --- a/hassio/src/addon-view/hassio-addon-dashboard.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { - mdiCogs, - mdiFileDocument, - mdiInformationVariant, - mdiMathLog, -} from "@mdi/js"; -import type { CSSResultGroup, TemplateResult } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import { fireEvent } from "../../../src/common/dom/fire_event"; -import { navigate } from "../../../src/common/navigate"; -import { extractSearchParam } from "../../../src/common/url/search-params"; -import type { HassioAddonDetails } from "../../../src/data/hassio/addon"; -import { - fetchAddonInfo, - fetchHassioAddonInfo, - fetchHassioAddonsInfo, -} from "../../../src/data/hassio/addon"; -import { extractApiErrorMessage } from "../../../src/data/hassio/common"; -import type { StoreAddonDetails } from "../../../src/data/supervisor/store"; -import { - addStoreRepository, - fetchSupervisorStore, -} from "../../../src/data/supervisor/store"; -import type { Supervisor } from "../../../src/data/supervisor/supervisor"; -import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box"; -import "../../../src/layouts/hass-error-screen"; -import "../../../src/layouts/hass-loading-screen"; -import "../../../src/layouts/hass-tabs-subpage"; -import type { PageNavigation } from "../../../src/layouts/hass-tabs-subpage"; -import { haStyle } from "../../../src/resources/styles"; -import type { HomeAssistant, Route } from "../../../src/types"; -import { hassioStyle } from "../resources/hassio-style"; -import "./config/hassio-addon-audio"; -import "./config/hassio-addon-config"; -import "./config/hassio-addon-network"; -import "./hassio-addon-router"; -import "./info/hassio-addon-info"; - -@customElement("hassio-addon-dashboard") -class HassioAddonDashboard extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - @property({ attribute: false }) public route!: Route; - - @property({ attribute: false }) public addon?: - | HassioAddonDetails - | StoreAddonDetails; - - @property({ type: Boolean }) public narrow = false; - - @state() - private _controlEnabled = false; - - @state() private _error?: string; - - private _backPath = new URLSearchParams(window.parent.location.search).get( - "store" - ) - ? "/hassio/store" - : "/hassio/dashboard"; - - private _computeTail = memoizeOne((route: Route) => { - const dividerPos = route.path.indexOf("/", 1); - return dividerPos === -1 - ? { - prefix: route.prefix + route.path, - path: "", - } - : { - prefix: route.prefix + route.path.substr(0, dividerPos), - path: route.path.substr(dividerPos), - }; - }); - - protected render(): TemplateResult { - if (this._error) { - return html``; - } - - if (!this.addon || !this.supervisor?.addon) { - return html``; - } - - const addonTabs: PageNavigation[] = [ - { - translationKey: "addon.panel.info", - path: `/hassio/addon/${this.addon.slug}/info`, - iconPath: mdiInformationVariant, - }, - ]; - - if (this.addon.documentation) { - addonTabs.push({ - translationKey: "addon.panel.documentation", - path: `/hassio/addon/${this.addon.slug}/documentation`, - iconPath: mdiFileDocument, - }); - } - - if (this.addon.version) { - addonTabs.push( - { - translationKey: "addon.panel.configuration", - path: `/hassio/addon/${this.addon.slug}/config`, - iconPath: mdiCogs, - }, - { - translationKey: "addon.panel.log", - path: `/hassio/addon/${this.addon.slug}/logs`, - iconPath: mdiMathLog, - } - ); - } - - const route = this._computeTail(this.route); - - return html` - - ${this.addon.name} - - - `; - } - - private _enableControl() { - this._controlEnabled = true; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - hassioStyle, - css` - :host { - color: var(--primary-text-color); - } - .content { - padding: 24px 0 32px; - display: flex; - flex-direction: column; - align-items: center; - } - hassio-addon-info, - hassio-addon-network, - hassio-addon-audio, - hassio-addon-config { - margin-bottom: 24px; - width: 600px; - } - @media only screen and (max-width: 600px) { - hassio-addon-info, - hassio-addon-network, - hassio-addon-audio, - hassio-addon-config { - max-width: 100%; - min-width: 100%; - } - } - `, - ]; - } - - protected async firstUpdated(): Promise { - if (this.route.path === "") { - const requestedAddon = extractSearchParam("addon"); - const requestedAddonRepository = extractSearchParam("repository_url"); - if (requestedAddonRepository) { - const storeInfo = await fetchSupervisorStore(this.hass); - if ( - !storeInfo.repositories.find( - (repo) => repo.source === requestedAddonRepository - ) - ) { - if ( - !(await showConfirmationDialog(this, { - title: this.supervisor.localize("my.add_addon_repository_title"), - text: this.supervisor.localize( - "my.add_addon_repository_description", - { addon: requestedAddon, repository: requestedAddonRepository } - ), - confirmText: this.supervisor.localize("common.add"), - dismissText: this.supervisor.localize("common.cancel"), - })) - ) { - this._error = this.supervisor.localize( - "my.error_repository_not_found" - ); - return; - } - - try { - await addStoreRepository(this.hass, requestedAddonRepository); - } catch (err: any) { - this._error = extractApiErrorMessage(err); - } - } - } - - if (requestedAddon) { - const store = await fetchSupervisorStore(this.hass); - const validAddon = store.addons.some( - (addon) => addon.slug === requestedAddon - ); - if (!validAddon) { - this._error = this.supervisor.localize("my.error_addon_not_found"); - } else { - navigate(`/hassio/addon/${requestedAddon}`, { replace: true }); - } - } - } - this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev)); - } - - private async _apiCalled(ev): Promise { - if (!ev.detail.success) { - return; - } - - const pathSplit: string[] = ev.detail.path?.split("/"); - - if (!pathSplit || pathSplit.length === 0) { - return; - } - - const path: string = pathSplit[pathSplit.length - 1]; - - if (["uninstall", "install", "update", "start", "stop"].includes(path)) { - fireEvent(this, "supervisor-collection-refresh", { - collection: "addon", - }); - } - - if (path === "uninstall") { - if (this.isConnected) { - navigate(this._backPath); - } - } else if (path === "install") { - this.addon = await fetchHassioAddonInfo(this.hass, this.addon!.slug); - } else { - await this._routeDataChanged(); - } - } - - protected updated(changedProperties) { - if (changedProperties.has("route") && !this.addon) { - this._routeDataChanged(); - } - } - - private async _routeDataChanged(): Promise { - const addon = this.route.path.split("/")[1]; - if (!addon) { - return; - } - try { - if (!this.supervisor.addon) { - const addonsInfo = await fetchHassioAddonsInfo(this.hass); - fireEvent(this, "supervisor-update", { addon: addonsInfo }); - } - this.addon = await fetchAddonInfo(this.hass, this.supervisor, addon); - } catch (err: any) { - this._error = `Error fetching addon info: ${extractApiErrorMessage(err)}`; - this.addon = undefined; - } - } -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-addon-dashboard": HassioAddonDashboard; - } -} diff --git a/hassio/src/addon-view/hassio-addon-router.ts b/hassio/src/addon-view/hassio-addon-router.ts deleted file mode 100644 index 1213ff9c12..0000000000 --- a/hassio/src/addon-view/hassio-addon-router.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { customElement, property } from "lit/decorators"; -import type { HassioAddonDetails } from "../../../src/data/hassio/addon"; -import type { StoreAddonDetails } from "../../../src/data/supervisor/store"; -import type { Supervisor } from "../../../src/data/supervisor/supervisor"; -import type { RouterOptions } from "../../../src/layouts/hass-router-page"; -import { HassRouterPage } from "../../../src/layouts/hass-router-page"; -import type { HomeAssistant } from "../../../src/types"; -import "./config/hassio-addon-config-tab"; -import "./documentation/hassio-addon-documentation-tab"; -// Don't codesplit the others, because it breaks the UI when pushed to a Pi -import "./info/hassio-addon-info-tab"; -import "./log/hassio-addon-log-tab"; - -@customElement("hassio-addon-router") -class HassioAddonRouter extends HassRouterPage { - @property({ type: Boolean }) public narrow = false; - - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - @property({ attribute: false }) public addon!: - | HassioAddonDetails - | StoreAddonDetails; - - @property({ type: Boolean, attribute: "control-enabled" }) - public controlEnabled = false; - - protected routerOptions: RouterOptions = { - defaultPage: "info", - showLoading: true, - routes: { - info: { - tag: "hassio-addon-info-tab", - }, - documentation: { - tag: "hassio-addon-documentation-tab", - }, - config: { - tag: "hassio-addon-config-tab", - }, - logs: { - tag: "hassio-addon-log-tab", - }, - }, - }; - - protected updatePageEl(el) { - el.route = this.routeTail; - el.hass = this.hass; - el.supervisor = this.supervisor; - el.addon = this.addon; - el.narrow = this.narrow; - el.controlEnabled = this.controlEnabled; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-addon-router": HassioAddonRouter; - } -} diff --git a/hassio/src/backups/hassio-backups.ts b/hassio/src/backups/hassio-backups.ts deleted file mode 100644 index cd73cd3790..0000000000 --- a/hassio/src/backups/hassio-backups.ts +++ /dev/null @@ -1,425 +0,0 @@ -import type { ActionDetail } from "@material/mwc-list"; - -import { mdiBackupRestore, mdiDelete, mdiDotsVertical, mdiPlus } from "@mdi/js"; -import type { CSSResultGroup, PropertyValues } from "lit"; -import { LitElement, css, html, nothing } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; -import { classMap } from "lit/directives/class-map"; -import memoizeOne from "memoize-one"; -import { atLeastVersion } from "../../../src/common/config/version"; -import { relativeTime } from "../../../src/common/datetime/relative_time"; -import type { HASSDomEvent } from "../../../src/common/dom/fire_event"; -import type { - DataTableColumnContainer, - RowClickedEvent, - SelectionChangedEvent, -} from "../../../src/components/data-table/ha-data-table"; -import "../../../src/components/ha-button-menu"; -import "../../../src/components/ha-fab"; -import "../../../src/components/ha-button"; -import "../../../src/components/ha-icon-button"; -import "../../../src/components/ha-list-item"; -import "../../../src/components/ha-svg-icon"; -import type { HassioBackup } from "../../../src/data/hassio/backup"; -import { - fetchHassioBackups, - friendlyFolderName, - reloadHassioBackups, - removeBackup, -} from "../../../src/data/hassio/backup"; -import { extractApiErrorMessage } from "../../../src/data/hassio/common"; -import type { Supervisor } from "../../../src/data/supervisor/supervisor"; -import { - showAlertDialog, - showConfirmationDialog, -} from "../../../src/dialogs/generic/show-dialog-box"; -import "../../../src/layouts/hass-loading-screen"; -import "../../../src/layouts/hass-tabs-subpage-data-table"; -import type { HaTabsSubpageDataTable } from "../../../src/layouts/hass-tabs-subpage-data-table"; -import { haStyle } from "../../../src/resources/styles"; -import type { HomeAssistant, Route } from "../../../src/types"; -import { showBackupUploadDialog } from "../dialogs/backup/show-dialog-backup-upload"; -import { showHassioBackupLocationDialog } from "../dialogs/backup/show-dialog-hassio-backu-location"; -import { showHassioBackupDialog } from "../dialogs/backup/show-dialog-hassio-backup"; -import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup"; -import { supervisorTabs } from "../hassio-tabs"; -import { hassioStyle } from "../resources/hassio-style"; - -type BackupItem = HassioBackup & { - secondary: string; -}; - -@customElement("hassio-backups") -export class HassioBackups extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - @property({ attribute: false }) public route!: Route; - - @property({ type: Boolean }) public narrow = false; - - @property({ attribute: "is-wide", type: Boolean }) public isWide = false; - - @state() private _selectedBackups: string[] = []; - - @state() private _backups?: HassioBackup[] = []; - - @state() private _isLoading = false; - - @query("hass-tabs-subpage-data-table", true) - private _dataTable!: HaTabsSubpageDataTable; - - private _firstUpdatedCalled = false; - - public connectedCallback(): void { - super.connectedCallback(); - if (this.hass && this._firstUpdatedCalled) { - this._fetchBackups(); - } - } - - private _computeBackupContent = (backup: HassioBackup): string => { - if (backup.type === "full") { - return this.supervisor.localize("backup.full_backup"); - } - const content: string[] = []; - if (backup.content.homeassistant) { - content.push("Home Assistant"); - } - if (backup.content.folders.length !== 0) { - for (const folder of backup.content.folders) { - content.push(friendlyFolderName[folder] || folder); - } - } - - if (backup.content.addons.length !== 0) { - for (const addon of backup.content.addons) { - content.push( - this.supervisor.addon.addons.find((entry) => entry.slug === addon) - ?.name || addon - ); - } - } - - return content.join(", "); - }; - - protected firstUpdated(changedProperties: PropertyValues): void { - super.firstUpdated(changedProperties); - if (this.hass && this.isConnected) { - this._fetchBackups(); - } - this._firstUpdatedCalled = true; - } - - private _columns = memoizeOne( - (narrow: boolean): DataTableColumnContainer => ({ - name: { - title: this.supervisor.localize("backup.name"), - main: true, - sortable: true, - filterable: true, - flex: 2, - template: (backup) => - html`${backup.name || backup.slug} -
${backup.secondary}
`, - }, - size: { - title: this.supervisor.localize("backup.size"), - hidden: narrow, - filterable: true, - sortable: true, - template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB", - }, - location: { - title: this.supervisor.localize("backup.location"), - hidden: narrow, - filterable: true, - sortable: true, - template: (backup) => - backup.location || this.supervisor.localize("backup.data_disk"), - }, - date: { - title: this.supervisor.localize("backup.created"), - direction: "desc", - hidden: narrow, - filterable: true, - sortable: true, - template: (backup) => - relativeTime(new Date(backup.date), this.hass.locale), - }, - secondary: { - title: "", - hidden: true, - filterable: true, - }, - }) - ); - - private _backupData = memoizeOne((backups: HassioBackup[]): BackupItem[] => - backups.map((backup) => ({ - ...backup, - secondary: this._computeBackupContent(backup), - })) - ); - - protected render() { - if (!this.supervisor) { - return nothing; - } - - if (this._isLoading) { - return html``; - } - - return html` - - - - - ${this.supervisor.localize("common.reload")} - - - ${this.supervisor.localize("dialog.backup_location.title")} - - ${atLeastVersion(this.hass.config.version, 0, 116) - ? html` - ${this.supervisor.localize("backup.upload_backup")} - ` - : ""} - - - ${this._selectedBackups.length - ? html`
-

- ${this.supervisor.localize("backup.selected", { - number: this._selectedBackups.length, - })} -

-
- ${!this.narrow - ? html` - - ${this.supervisor.localize("backup.delete_selected")} - - ` - : html` - - `} -
-
` - : ""} - - - - -
- `; - } - - private _handleAction(ev: CustomEvent) { - switch (ev.detail.index) { - case 0: - this._fetchBackups(); - break; - case 1: - showHassioBackupLocationDialog(this, { supervisor: this.supervisor }); - break; - case 2: - this._showUploadBackupDialog(); - break; - } - } - - private _handleSelectionChanged( - ev: HASSDomEvent - ): void { - this._selectedBackups = ev.detail.value; - } - - private _showUploadBackupDialog() { - showBackupUploadDialog(this, { - showBackup: (slug: string) => - showHassioBackupDialog(this, { - slug, - supervisor: this.supervisor, - onDelete: () => this._fetchBackups(), - }), - reloadBackup: () => this._fetchBackups(), - }); - } - - private async _fetchBackups() { - this._isLoading = true; - await reloadHassioBackups(this.hass); - this._backups = await fetchHassioBackups(this.hass); - this._isLoading = false; - } - - private async _deleteSelected() { - const confirm = await showConfirmationDialog(this, { - title: this.supervisor.localize("backup.delete_backup_title"), - text: this.supervisor.localize("backup.delete_backup_text", { - number: this._selectedBackups.length, - }), - confirmText: this.supervisor.localize("backup.delete_backup_confirm"), - destructive: true, - }); - - if (!confirm) { - return; - } - - try { - await Promise.all( - this._selectedBackups.map((slug) => removeBackup(this.hass, slug)) - ); - } catch (err: any) { - showAlertDialog(this, { - title: this.supervisor.localize("backup.failed_to_delete"), - text: extractApiErrorMessage(err), - }); - return; - } - await this._fetchBackups(); - this._dataTable.clearSelection(); - } - - private _handleRowClicked(ev: HASSDomEvent) { - const slug = ev.detail.id; - showHassioBackupDialog(this, { - slug, - supervisor: this.supervisor, - onDelete: () => this._fetchBackups(), - }); - } - - private _createBackup() { - if (this.supervisor!.info.state !== "running") { - showAlertDialog(this, { - title: this.supervisor!.localize("backup.could_not_create"), - text: this.supervisor!.localize("backup.create_blocked_not_running", { - state: this.supervisor!.info.state, - }), - }); - return; - } - showHassioCreateBackupDialog(this, { - supervisor: this.supervisor!, - onCreate: () => this._fetchBackups(), - }); - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - hassioStyle, - css` - :host { - color: var(--primary-text-color); - } - .table-header { - display: flex; - justify-content: space-between; - align-items: center; - height: 58px; - border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12); - } - .header-toolbar { - display: flex; - justify-content: space-between; - align-items: center; - color: var(--secondary-text-color); - position: relative; - top: -4px; - } - .selected-txt { - font-weight: var(--ha-font-weight-bold); - padding-left: 16px; - padding-inline-start: 16px; - padding-inline-end: initial; - color: var(--primary-text-color); - } - .table-header .selected-txt { - margin-top: 20px; - } - .header-toolbar .selected-txt { - font-size: var(--ha-font-size-l); - } - .header-toolbar .header-btns { - margin-right: -12px; - margin-inline-end: -12px; - margin-inline-start: initial; - } - .header-btns > ha-button, - .header-btns > ha-icon-button { - margin: 8px; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-backups": HassioBackups; - } -} diff --git a/hassio/src/components/hassio-upload-backup.ts b/hassio/src/components/hassio-upload-backup.ts deleted file mode 100644 index fee23e10a9..0000000000 --- a/hassio/src/components/hassio-upload-backup.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { mdiFolderUpload } from "@mdi/js"; -import type { TemplateResult } from "lit"; -import { html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { fireEvent } from "../../../src/common/dom/fire_event"; -import "../../../src/components/ha-file-upload"; -import type { HassioBackup } from "../../../src/data/hassio/backup"; -import { uploadBackup } from "../../../src/data/hassio/backup"; -import { extractApiErrorMessage } from "../../../src/data/hassio/common"; -import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; -import type { HomeAssistant } from "../../../src/types"; -import type { LocalizeFunc } from "../../../src/common/translations/localize"; - -declare global { - interface HASSDomEvents { - "hassio-backup-uploaded": { backup: HassioBackup }; - "backup-cleared": undefined; - } -} - -@customElement("hassio-upload-backup") -export class HassioUploadBackup extends LitElement { - public hass?: HomeAssistant; - - @property({ attribute: false }) public localize?: LocalizeFunc; - - @state() public value: string | null = null; - - @state() private _uploading = false; - - public render(): TemplateResult { - return html` - - `; - } - - private _clear() { - this.value = null; - fireEvent(this, "backup-cleared"); - } - - private async _uploadFile(ev) { - const file = ev.detail.files[0]; - - if (!["application/x-tar"].includes(file.type)) { - showAlertDialog(this, { - title: "Unsupported file format", - text: "Please choose a Home Assistant backup file (.tar)", - confirmText: "ok", - }); - return; - } - this._uploading = true; - try { - const backup = await uploadBackup(this.hass, file); - fireEvent(this, "hassio-backup-uploaded", { backup: backup.data }); - } catch (err: any) { - showAlertDialog(this, { - title: "Upload failed", - text: extractApiErrorMessage(err), - confirmText: "ok", - }); - } finally { - this._uploading = false; - } - } -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-upload-backup": HassioUploadBackup; - } -} diff --git a/hassio/src/components/supervisor-backup-content.ts b/hassio/src/components/supervisor-backup-content.ts deleted file mode 100644 index 422d8452eb..0000000000 --- a/hassio/src/components/supervisor-backup-content.ts +++ /dev/null @@ -1,460 +0,0 @@ -import { mdiFolder, mdiPuzzle } from "@mdi/js"; -import type { TemplateResult } from "lit"; -import { LitElement, css, html, nothing } from "lit"; -import { customElement, property, query } from "lit/decorators"; -import { atLeastVersion } from "../../../src/common/config/version"; -import { formatDate } from "../../../src/common/datetime/format_date"; -import { formatDateTime } from "../../../src/common/datetime/format_date_time"; -import "../../../src/components/ha-checkbox"; -import "../../../src/components/ha-formfield"; -import "../../../src/components/ha-textfield"; -import "../../../src/components/ha-password-field"; -import "../../../src/components/ha-radio"; -import type { HaRadio } from "../../../src/components/ha-radio"; -import type { - HassioBackupDetail, - HassioFullBackupCreateParams, - HassioPartialBackupCreateParams, -} from "../../../src/data/hassio/backup"; -import type { Supervisor } from "../../../src/data/supervisor/supervisor"; -import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg"; -import type { HomeAssistant } from "../../../src/types"; -import "./supervisor-formfield-label"; -import type { HaTextField } from "../../../src/components/ha-textfield"; - -interface CheckboxItem { - slug: string; - checked: boolean; - name: string; -} - -interface AddonCheckboxItem extends CheckboxItem { - version: string; -} - -const _computeFolders = (folders): CheckboxItem[] => { - const list: CheckboxItem[] = []; - if (folders.includes("ssl")) { - list.push({ slug: "ssl", name: "SSL", checked: false }); - } - if (folders.includes("share")) { - list.push({ slug: "share", name: "Share", checked: false }); - } - if (folders.includes("media")) { - list.push({ slug: "media", name: "Media", checked: false }); - } - if (folders.includes("addons/local")) { - list.push({ slug: "addons/local", name: "Local add-ons", checked: false }); - } - return list.sort((a, b) => (a.name > b.name ? 1 : -1)); -}; - -const _computeAddons = (addons): AddonCheckboxItem[] => - addons - .map((addon) => ({ - slug: addon.slug, - name: addon.name, - version: addon.version, - checked: false, - })) - .sort((a, b) => (a.name > b.name ? 1 : -1)); - -@customElement("supervisor-backup-content") -export class SupervisorBackupContent extends LitElement { - @property({ attribute: false }) public hass?: HomeAssistant; - - @property({ attribute: false }) public supervisor?: Supervisor; - - @property({ attribute: false }) public backup?: HassioBackupDetail; - - @property({ attribute: false }) - public backupType: HassioBackupDetail["type"] = "full"; - - @property({ attribute: false }) public folders?: CheckboxItem[]; - - @property({ attribute: false }) public addons?: AddonCheckboxItem[]; - - @property({ attribute: false }) public homeAssistant = false; - - @property({ attribute: false }) public backupHasPassword = false; - - @property({ type: Boolean }) public onboarding = false; - - @property({ attribute: false }) public backupName = ""; - - @property({ attribute: false }) public backupPassword = ""; - - @property({ attribute: false }) public confirmBackupPassword = ""; - - @query("ha-textfield, ha-radio, ha-checkbox", true) private _focusTarget; - - public willUpdate(changedProps) { - super.willUpdate(changedProps); - if (!this.hasUpdated) { - this.folders = _computeFolders( - this.backup - ? this.backup.folders - : ["ssl", "share", "media", "addons/local"] - ); - this.addons = _computeAddons( - this.backup ? this.backup.addons : this.supervisor?.addon.addons - ); - this.backupType = this.backup?.type || "full"; - this.backupName = this.backup?.name || ""; - this.backupHasPassword = this.backup?.protected || false; - } - } - - public override focus() { - this._focusTarget?.focus(); - } - - protected render() { - if (!this.onboarding && !this.supervisor) { - return nothing; - } - const foldersSection = - this.backupType === "partial" ? this._getSection("folders") : undefined; - const addonsSection = - this.backupType === "partial" ? this._getSection("addons") : undefined; - - return html` - ${this.backup - ? html`
- ${this.backup.type === "full" - ? this.supervisor?.localize("backup.full_backup") - : this.supervisor?.localize("backup.partial_backup")} - (${Math.ceil(this.backup.size * 10) / 10 + " MB"})
- ${this.hass - ? formatDateTime( - new Date(this.backup.date), - this.hass.locale, - this.hass.config - ) - : this.backup.date} -
` - : html` - `} - ${!this.backup || this.backup.type === "full" - ? html`
- ${!this.backup - ? this.supervisor?.localize("backup.type") - : this.supervisor?.localize("backup.select_type")} -
-
- - - - - - - - -
` - : ""} - ${this.backupType === "partial" - ? html`
- ${!this.backup || this.backup.homeassistant - ? html` - `} - > - - - ` - : ""} - ${foldersSection?.templates.length - ? html` - - `} - > - - - -
${foldersSection.templates}
- ` - : ""} - ${addonsSection?.templates.length - ? html` - - `} - > - - - -
${addonsSection.templates}
- ` - : ""} -
` - : ""} - ${this.backupType === "partial" && - (!this.backup || this.backupHasPassword) - ? html`
` - : ""} - ${!this.backup - ? html` - - - ` - : ""} - ${this.backupHasPassword - ? html` - - - ${!this.backup - ? html` - ` - : ""} - ` - : ""} - `; - } - - private _toggleHomeAssistant() { - this.homeAssistant = !this.homeAssistant; - } - - static styles = css` - .partial-picker ha-formfield { - display: block; - } - .partial-picker ha-checkbox { - --mdc-checkbox-touch-target-size: 32px; - } - .partial-picker { - display: block; - margin: 0px -6px; - } - supervisor-formfield-label { - display: inline-flex; - align-items: center; - } - hr { - border-color: var(--divider-color); - border-bottom: none; - margin: 16px 0; - } - .details { - color: var(--secondary-text-color); - } - .section-content { - display: flex; - flex-direction: column; - margin-left: 30px; - margin-inline-start: 30px; - margin-inline-end: initial; - } - ha-formfield.password { - display: block; - margin: 0 -14px -16px; - } - .backup-types { - display: flex; - margin-left: -13px; - margin-inline-start: -13px; - margin-inline-end: initial; - } - .sub-header { - margin-top: 8px; - } - `; - - public backupDetails(): - | HassioPartialBackupCreateParams - | HassioFullBackupCreateParams { - const data: any = {}; - - if (!this.backup && this.hass) { - data.name = - this.backupName || - formatDate(new Date(), this.hass.locale, this.hass.config); - } - - if (this.backupHasPassword) { - data.password = this.backupPassword; - if (!this.backup) { - data.confirm_password = this.confirmBackupPassword; - } - } - - if (this.backupType === "full") { - return data; - } - - const addons = this.addons - ?.filter((addon) => addon.checked) - .map((addon) => addon.slug); - const folders = this.folders - ?.filter((folder) => folder.checked) - .map((folder) => folder.slug); - - if (addons?.length) { - data.addons = addons; - } - if (folders?.length) { - data.folders = folders; - } - - // onboarding needs at least homeassistant to restore - data.homeassistant = this.onboarding || this.homeAssistant; - - return data; - } - - private _getSection(section: string) { - const templates: TemplateResult[] = []; - const addons = - section === "addons" - ? new Map( - this.supervisor?.addon.addons.map((item) => [item.slug, item]) - ) - : undefined; - let checkedItems = 0; - this[section].forEach((item) => { - templates.push( - html` - `} - > - - - ` - ); - - if (item.checked) { - checkedItems++; - } - }); - - const checked = checkedItems === this[section].length; - - return { - templates, - checked, - indeterminate: !checked && checkedItems !== 0, - }; - } - - private _handleRadioValueChanged(ev: CustomEvent) { - const input = ev.currentTarget as HaRadio; - this[input.name] = input.value; - } - - private _handleTextValueChanged(ev: InputEvent) { - const input = ev.currentTarget as HaTextField; - this[input.name!] = input.value; - } - - private _toggleHasPassword(): void { - this.backupHasPassword = !this.backupHasPassword; - } - - private _toggleSection(ev): void { - const section = ev.currentTarget.section; - - this[section] = (section === "addons" ? this.addons : this.folders)!.map( - (item) => ({ - ...item, - checked: ev.currentTarget.checked, - }) - ); - } - - private _updateSectionEntry(ev): void { - const item = ev.currentTarget.item; - const section = ev.currentTarget.section; - this[section] = this[section].map((entry) => - entry.slug === item.slug - ? { - ...entry, - checked: ev.currentTarget.checked, - } - : entry - ); - } -} - -declare global { - interface HTMLElementTagNameMap { - "supervisor-backup-content": SupervisorBackupContent; - } -} diff --git a/hassio/src/components/supervisor-formfield-label.ts b/hassio/src/components/supervisor-formfield-label.ts deleted file mode 100644 index ce04c6d8d6..0000000000 --- a/hassio/src/components/supervisor-formfield-label.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { TemplateResult } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators"; -import "../../../src/components/ha-svg-icon"; - -@customElement("supervisor-formfield-label") -class SupervisorFormfieldLabel extends LitElement { - @property({ type: String }) public label!: string; - - @property({ attribute: false }) public imageUrl?: string; - - @property({ attribute: false }) public iconPath?: string; - - @property({ type: String }) public version?: string; - - protected render(): TemplateResult { - return html` - ${this.imageUrl - ? html`` - : this.iconPath - ? html`` - : ""} - ${this.label} - ${this.version - ? html`(${this.version})` - : ""} - `; - } - - static styles = css` - :host { - display: flex; - align-items: center; - } - .label { - margin-right: 4px; - margin-inline-end: 4px; - margin-inline-start: initial; - } - .version { - color: var(--secondary-text-color); - } - .icon { - max-height: 22px; - max-width: 22px; - margin-right: 8px; - margin-inline-end: 8px; - margin-inline-start: initial; - } - `; -} - -declare global { - interface HTMLElementTagNameMap { - "supervisor-formfield-label": SupervisorFormfieldLabel; - } -} diff --git a/hassio/src/dashboard/hassio-addons.ts b/hassio/src/dashboard/hassio-addons.ts deleted file mode 100644 index d83c8a0f0b..0000000000 --- a/hassio/src/dashboard/hassio-addons.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { mdiArrowUpBoldCircle, mdiPuzzle } from "@mdi/js"; -import type { CSSResultGroup, TemplateResult } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import { atLeastVersion } from "../../../src/common/config/version"; -import { navigate } from "../../../src/common/navigate"; -import { caseInsensitiveStringCompare } from "../../../src/common/string/compare"; -import "../../../src/components/ha-card"; -import "../../../src/components/search-input"; -import type { HassioAddonInfo } from "../../../src/data/hassio/addon"; -import type { Supervisor } from "../../../src/data/supervisor/supervisor"; -import { haStyle } from "../../../src/resources/styles"; -import type { HomeAssistant } from "../../../src/types"; -import "../components/hassio-card-content"; -import { hassioStyle } from "../resources/hassio-style"; - -@customElement("hassio-addons") -class HassioAddons extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - @property({ type: Boolean }) public narrow = false; - - @state() private _filter?: string; - - protected render(): TemplateResult { - return html` - -
- ${!atLeastVersion(this.hass.config.version, 2021, 12) - ? html`

${this.supervisor.localize("dashboard.addons")}

` - : ""} -
- ${!this.supervisor.addon.addons.length - ? html` - -
- -
-
- ` - : this._getAddons(this.supervisor.addon.addons, this._filter).map( - (addon) => html` - -
- -
-
- ` - )} -
-
- `; - } - - private _getAddons = memoizeOne( - (addons: HassioAddonInfo[], filter?: string) => { - if (filter) { - addons = addons.filter((addon) => { - const lowerCaseFilter = filter.toLowerCase(); - return ( - addon.name.toLowerCase().includes(lowerCaseFilter) || - addon.description.toLowerCase().includes(lowerCaseFilter) || - addon.slug.toLowerCase().includes(lowerCaseFilter) - ); - }); - } - return addons.sort((a, b) => - caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language) - ); - } - ); - - private _handleSearchChange(ev: CustomEvent) { - this._filter = ev.detail.value; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - hassioStyle, - css` - ha-card { - cursor: pointer; - overflow: hidden; - direction: ltr; - } - .search { - position: sticky; - top: 0; - z-index: 2; - } - search-input { - display: block; - --mdc-text-field-fill-color: var(--sidebar-background-color); - --mdc-text-field-idle-line-color: var(--divider-color); - } - .content { - margin-bottom: 72px; - } - `, - ]; - } - - private _addonTapped(ev: any): void { - navigate(`/hassio/addon/${ev.currentTarget.addon.slug}/info`); - } - - private _openStore(): void { - navigate("/hassio/store"); - } -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-addons": HassioAddons; - } -} diff --git a/hassio/src/dashboard/hassio-dashboard.ts b/hassio/src/dashboard/hassio-dashboard.ts deleted file mode 100644 index 4cbf3c97b4..0000000000 --- a/hassio/src/dashboard/hassio-dashboard.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { mdiRefresh, mdiStorePlus } from "@mdi/js"; -import type { CSSResultGroup, TemplateResult } from "lit"; -import { LitElement, css, html } from "lit"; -import { customElement, property } from "lit/decorators"; -import { atLeastVersion } from "../../../src/common/config/version"; -import { fireEvent } from "../../../src/common/dom/fire_event"; -import "../../../src/components/ha-fab"; -import { reloadHassioAddons } from "../../../src/data/hassio/addon"; -import { extractApiErrorMessage } from "../../../src/data/hassio/common"; -import type { Supervisor } from "../../../src/data/supervisor/supervisor"; -import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; -import "../../../src/layouts/hass-subpage"; -import "../../../src/layouts/hass-tabs-subpage"; -import { haStyle } from "../../../src/resources/styles"; -import type { HomeAssistant, Route } from "../../../src/types"; -import { supervisorTabs } from "../hassio-tabs"; -import "./hassio-addons"; - -@customElement("hassio-dashboard") -class HassioDashboard extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - @property({ type: Boolean }) public narrow = false; - - @property({ attribute: false }) public route!: Route; - - firstUpdated() { - if (!atLeastVersion(this.hass.config.version, 2022, 5)) { - import("./hassio-update"); - } - } - - protected render(): TemplateResult { - if (atLeastVersion(this.hass.config.version, 2022, 5)) { - return html` - - - - - - `; - } - - return html` - - - ${this.supervisor.localize( - atLeastVersion(this.hass.config.version, 2021, 12) - ? "panel.addons" - : "panel.dashboard" - )} - -
- ${!atLeastVersion(this.hass.config.version, 2021, 12) - ? html` - - ` - : ""} - -
- - - - -
- `; - } - - private async _handleCheckUpdates() { - try { - await reloadHassioAddons(this.hass); - } catch (err) { - showAlertDialog(this, { - text: extractApiErrorMessage(err), - }); - } finally { - fireEvent(this, "supervisor-collection-refresh", { collection: "addon" }); - } - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - .content { - margin: 0 auto; - } - ha-fab.non-tabs { - position: fixed; - right: calc(16px + var(--safe-area-inset-right)); - bottom: calc(16px + var(--safe-area-inset-bottom)); - inset-inline-end: calc(16px + var(--safe-area-inset-right)); - inset-inline-start: initial; - z-index: 1; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-dashboard": HassioDashboard; - } -} diff --git a/hassio/src/dashboard/hassio-update.ts b/hassio/src/dashboard/hassio-update.ts deleted file mode 100644 index e62e4ea327..0000000000 --- a/hassio/src/dashboard/hassio-update.ts +++ /dev/null @@ -1,158 +0,0 @@ -import type { CSSResultGroup } from "lit"; -import { css, html, LitElement, nothing } from "lit"; -import { customElement, property } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import "../../../src/components/ha-card"; -import "../../../src/components/ha-button"; -import "../../../src/components/ha-settings-row"; -import "../../../src/components/ha-svg-icon"; -import type { HassioHassOSInfo } from "../../../src/data/hassio/host"; -import type { - HassioHomeAssistantInfo, - HassioSupervisorInfo, -} from "../../../src/data/hassio/supervisor"; -import type { Supervisor } from "../../../src/data/supervisor/supervisor"; -import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg"; -import { haStyle } from "../../../src/resources/styles"; -import type { HomeAssistant } from "../../../src/types"; -import { hassioStyle } from "../resources/hassio-style"; - -const computeVersion = (key: string, version: string): string => - key === "os" ? version : `${key}-${version}`; - -@customElement("hassio-update") -export class HassioUpdate extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - private _pendingUpdates = memoizeOne( - (supervisor: Supervisor): number => - Object.keys(supervisor).filter( - (value) => supervisor[value].update_available - ).length - ); - - protected render() { - if (!this.supervisor) { - return nothing; - } - - const updatesAvailable = this._pendingUpdates(this.supervisor); - if (!updatesAvailable) { - return nothing; - } - - return html` -
-

- ${this.supervisor.localize("common.update_available", { - count: updatesAvailable, - })} - 🎉 -

-
- ${this._renderUpdateCard( - "Home Assistant Core", - "core", - this.supervisor.core - )} - ${this._renderUpdateCard( - "Supervisor", - "supervisor", - this.supervisor.supervisor - )} - ${this.supervisor.host.features.includes("haos") - ? this._renderUpdateCard( - "Operating System", - "os", - this.supervisor.os - ) - : ""} -
-
- `; - } - - private _renderUpdateCard( - name: string, - key: string, - object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo - ) { - if (!object.update_available) { - return nothing; - } - return html` - -
-
- -
-
${name}
- - - ${this.supervisor.localize("common.version")} - - - ${computeVersion(key, object.version!)} - - - - - - ${this.supervisor.localize("common.newest_version")} - - - ${computeVersion(key, object.version_latest!)} - - -
-
- - ${this.supervisor.localize("common.show")} - -
-
- `; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - hassioStyle, - css` - .icon { - --mdc-icon-size: 48px; - float: right; - margin: 0 0 2px 10px; - color: var(--primary-text-color); - } - .update-heading { - font-size: var(--ha-font-size-l); - font-weight: var(--ha-font-weight-medium); - margin-bottom: 0.5em; - color: var(--primary-text-color); - } - .card-content { - height: calc(100% - 47px); - box-sizing: border-box; - } - .card-actions { - text-align: right; - } - a { - text-decoration: none; - } - ha-settings-row { - padding: 0; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-update": HassioUpdate; - } -} diff --git a/hassio/src/dialogs/backup/dialog-hassio-backup-location.ts b/hassio/src/dialogs/backup/dialog-hassio-backup-location.ts deleted file mode 100644 index 5d5ec48370..0000000000 --- a/hassio/src/dialogs/backup/dialog-hassio-backup-location.ts +++ /dev/null @@ -1,155 +0,0 @@ -import type { CSSResultGroup } from "lit"; -import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import "../../../../src/components/ha-dialog"; -import "../../../../src/components/ha-button"; -import "../../../../src/components/ha-form/ha-form"; -import type { SchemaUnion } from "../../../../src/components/ha-form/types"; -import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; -import { changeMountOptions } from "../../../../src/data/supervisor/mounts"; -import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; -import type { HomeAssistant } from "../../../../src/types"; -import type { HassioBackupLocationDialogParams } from "./show-dialog-hassio-backu-location"; - -const SCHEMA = memoizeOne( - () => - [ - { - name: "default_backup_mount", - required: true, - selector: { backup_location: {} }, - }, - ] as const -); - -@customElement("dialog-hassio-backup-location") -class HassioBackupLocationDialog extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private _dialogParams?: HassioBackupLocationDialogParams; - - @state() private _data?: { default_backup_mount: string | null }; - - @state() private _waiting?: boolean; - - @state() private _error?: string; - - public async showDialog( - dialogParams: HassioBackupLocationDialogParams - ): Promise { - this._dialogParams = dialogParams; - } - - public closeDialog(): void { - this._data = undefined; - this._error = undefined; - this._waiting = undefined; - this._dialogParams = undefined; - fireEvent(this, "dialog-closed", { dialog: this.localName }); - } - - protected render() { - if (!this._dialogParams) { - return nothing; - } - return html` - - ${this._error - ? html`${this._error}` - : nothing} - - - - ${this._dialogParams.supervisor.localize("common.cancel")} - - - ${this._dialogParams.supervisor.localize("common.save")} - - - `; - } - - private _computeLabelCallback = ( - // @ts-ignore - schema: SchemaUnion> - ): string => - this._dialogParams!.supervisor.localize( - `dialog.backup_location.options.${schema.name}.name` - ) || schema.name; - - private _computeHelperCallback = ( - // @ts-ignore - schema: SchemaUnion> - ): string => - this._dialogParams!.supervisor.localize( - `dialog.backup_location.options.${schema.name}.description` - ); - - private _valueChanged(ev: CustomEvent) { - const newLocation = ev.detail.value.default_backup_mount; - this._data = { - default_backup_mount: newLocation === "/backup" ? null : newLocation, - }; - } - - private async _changeMount() { - if (!this._data) { - return; - } - this._error = undefined; - this._waiting = true; - try { - await changeMountOptions(this.hass, this._data); - } catch (err: any) { - this._error = extractApiErrorMessage(err); - this._waiting = false; - return; - } - this.closeDialog(); - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - haStyleDialog, - css` - .delete-btn { - --mdc-theme-primary: var(--error-color); - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-hassio-backup-location": HassioBackupLocationDialog; - } -} diff --git a/hassio/src/dialogs/backup/dialog-hassio-backup-upload.ts b/hassio/src/dialogs/backup/dialog-hassio-backup-upload.ts deleted file mode 100644 index f96ac681f7..0000000000 --- a/hassio/src/dialogs/backup/dialog-hassio-backup-upload.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { mdiClose } from "@mdi/js"; -import type { CSSResultGroup } from "lit"; -import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import "../../../../src/components/ha-header-bar"; -import "../../../../src/components/ha-icon-button"; -import "../../../../src/components/ha-dialog"; -import type { HassDialog } from "../../../../src/dialogs/make-dialog-manager"; -import { haStyleDialog } from "../../../../src/resources/styles"; -import type { HomeAssistant } from "../../../../src/types"; -import "../../components/hassio-upload-backup"; -import type { HassioBackupUploadDialogParams } from "./show-dialog-backup-upload"; - -@customElement("dialog-hassio-backup-upload") -export class DialogHassioBackupUpload - extends LitElement - implements HassDialog -{ - @property({ attribute: false }) public hass?: HomeAssistant; - - @state() private _dialogParams?: HassioBackupUploadDialogParams; - - public async showDialog( - dialogParams: HassioBackupUploadDialogParams - ): Promise { - this._dialogParams = dialogParams; - await this.updateComplete; - } - - public closeDialog() { - if (this._dialogParams && !this._dialogParams.onboarding) { - if (this._dialogParams.reloadBackup) { - this._dialogParams.reloadBackup(); - } - } - this._dialogParams = undefined; - fireEvent(this, "dialog-closed", { dialog: this.localName }); - return true; - } - - protected render() { - if (!this._dialogParams) { - return nothing; - } - - return html` - -
- - ${this.hass?.localize( - "ui.panel.page-onboarding.restore.upload_backup" - ) || "Upload backup"} - - -
- -
- `; - } - - private _backupUploaded(ev) { - const backup = ev.detail.backup; - this._dialogParams?.showBackup(backup.slug); - this.closeDialog(); - } - - static get styles(): CSSResultGroup { - return [ - haStyleDialog, - css` - ha-header-bar { - --mdc-theme-on-primary: var(--primary-text-color); - --mdc-theme-primary: var(--mdc-theme-surface); - flex-shrink: 0; - } - /* overrule the ha-style-dialog max-height on small screens */ - @media all and (max-width: 450px), all and (max-height: 500px) { - ha-header-bar { - --mdc-theme-primary: var(--app-header-background-color); - --mdc-theme-on-primary: var(--app-header-text-color, white); - } - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-hassio-backup-upload": DialogHassioBackupUpload; - } -} diff --git a/hassio/src/dialogs/backup/dialog-hassio-backup.ts b/hassio/src/dialogs/backup/dialog-hassio-backup.ts deleted file mode 100644 index c3df55c919..0000000000 --- a/hassio/src/dialogs/backup/dialog-hassio-backup.ts +++ /dev/null @@ -1,339 +0,0 @@ -import type { ActionDetail } from "@material/mwc-list"; - -import { mdiClose, mdiDotsVertical } from "@mdi/js"; -import type { CSSResultGroup } from "lit"; -import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; -import { atLeastVersion } from "../../../../src/common/config/version"; -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import { stopPropagation } from "../../../../src/common/dom/stop_propagation"; -import { slugify } from "../../../../src/common/string/slugify"; -import "../../../../src/components/ha-alert"; -import "../../../../src/components/ha-button"; -import "../../../../src/components/ha-button-menu"; -import "../../../../src/components/ha-dialog-header"; -import "../../../../src/components/ha-header-bar"; -import "../../../../src/components/ha-icon-button"; -import "../../../../src/components/ha-list-item"; -import "../../../../src/components/ha-md-dialog"; -import type { HaMdDialog } from "../../../../src/components/ha-md-dialog"; -import "../../../../src/components/ha-spinner"; -import { getSignedPath } from "../../../../src/data/auth"; -import type { HassioBackupDetail } from "../../../../src/data/hassio/backup"; -import { - fetchHassioBackupInfo, - removeBackup, - restoreBackup, -} from "../../../../src/data/hassio/backup"; -import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; -import { - showAlertDialog, - showConfirmationDialog, -} from "../../../../src/dialogs/generic/show-dialog-box"; -import type { HassDialog } from "../../../../src/dialogs/make-dialog-manager"; -import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; -import type { HomeAssistant } from "../../../../src/types"; -import { fileDownload } from "../../../../src/util/file_download"; -import "../../components/supervisor-backup-content"; -import type { SupervisorBackupContent } from "../../components/supervisor-backup-content"; -import type { HassioBackupDialogParams } from "./show-dialog-hassio-backup"; - -@customElement("dialog-hassio-backup") -class HassioBackupDialog - extends LitElement - implements HassDialog -{ - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private _error?: string; - - @state() private _backup?: HassioBackupDetail; - - @state() private _dialogParams?: HassioBackupDialogParams; - - @state() private _restoringBackup = false; - - @query("supervisor-backup-content") - private _backupContent!: SupervisorBackupContent; - - @query("ha-md-dialog") private _dialog?: HaMdDialog; - - public async showDialog(dialogParams: HassioBackupDialogParams) { - this._dialogParams = dialogParams; - this._backup = await fetchHassioBackupInfo(this.hass, dialogParams.slug); - if (!this._backup) { - this._error = this._dialogParams.supervisor?.localize( - "backup.no_backup_found" - ); - } else if (this._dialogParams.onboarding && !this._backup.homeassistant) { - this._error = this._dialogParams.supervisor?.localize( - "backup.restore_no_home_assistant" - ); - } - this._restoringBackup = false; - } - - private _dialogClosed(): void { - this._backup = undefined; - this._dialogParams = undefined; - this._restoringBackup = false; - this._error = undefined; - fireEvent(this, "dialog-closed", { dialog: this.localName }); - } - - public closeDialog() { - this._dialog?.close(); - return true; - } - - protected render() { - if (!this._dialogParams || !this._backup) { - return nothing; - } - return html` - - - - ${this._backup.name} - ${!this._dialogParams.onboarding && this._dialogParams.supervisor - ? html` - - ${this._dialogParams.supervisor.localize( - "backup.download_backup" - )} - ${this._dialogParams.supervisor.localize( - "backup.delete_backup_title" - )} - ` - : nothing} - -
- ${this._error - ? html`${this._error}` - : this._restoringBackup - ? html`
- -
` - : html` - - - `} -
-
- - ${this._dialogParams.supervisor?.localize("backup.restore")} - -
-
- `; - } - - private _handleMenuAction(ev: CustomEvent) { - switch (ev.detail.index) { - case 0: - this._downloadClicked(); - break; - case 1: - this._deleteClicked(); - break; - } - } - - private async _restoreClicked() { - const backupDetails = this._backupContent.backupDetails(); - this._restoringBackup = true; - - const supervisor = this._dialogParams?.supervisor; - if (supervisor !== undefined && supervisor.info.state !== "running") { - await showAlertDialog(this, { - title: supervisor.localize("backup.could_not_restore"), - text: supervisor.localize("backup.restore_blocked_not_running", { - state: supervisor.info.state, - }), - }); - this._restoringBackup = false; - return; - } - if ( - !(await showConfirmationDialog(this, { - title: supervisor?.localize( - `backup.${ - this._backup!.type === "full" - ? "confirm_restore_full_backup_title" - : "confirm_restore_partial_backup_title" - }` - ), - text: supervisor?.localize( - `backup.${ - this._backup!.type === "full" - ? "confirm_restore_full_backup_text" - : "confirm_restore_partial_backup_text" - }` - ), - confirmText: supervisor?.localize("backup.restore"), - dismissText: supervisor?.localize("backup.cancel"), - })) - ) { - this._restoringBackup = false; - return; - } - - try { - await restoreBackup( - this.hass, - this._backup!.type, - this._backup!.slug, - { ...backupDetails, background: this._dialogParams?.onboarding }, - !!this.hass && atLeastVersion(this.hass.config.version, 2021, 9) - ); - - this._dialogParams?.onRestoring?.(); - this.closeDialog(); - } catch (error: any) { - this._error = - error?.body?.message || - supervisor?.localize("backup.restore_start_failed"); - } finally { - this._restoringBackup = false; - } - } - - private async _deleteClicked() { - const supervisor = this._dialogParams?.supervisor; - if (!supervisor) return; - - if ( - !(await showConfirmationDialog(this, { - title: supervisor!.localize("backup.confirm_delete_title"), - text: supervisor!.localize("backup.confirm_delete_text"), - confirmText: supervisor!.localize("backup.delete"), - dismissText: supervisor!.localize("backup.cancel"), - destructive: true, - })) - ) { - return; - } - - try { - await removeBackup(this.hass!, this._backup!.slug); - if (this._dialogParams!.onDelete) { - this._dialogParams!.onDelete(); - } - this.closeDialog(); - } catch (err: any) { - this._error = err.body.message; - } - } - - private async _downloadClicked() { - const supervisor = this._dialogParams?.supervisor; - if (!supervisor) return; - - let signedPath: { path: string }; - try { - signedPath = await getSignedPath( - this.hass!, - `/api/hassio/${ - atLeastVersion(this.hass!.config.version, 2021, 9) - ? "backups" - : "snapshots" - }/${this._backup!.slug}/download` - ); - } catch (err: any) { - await showAlertDialog(this, { - text: extractApiErrorMessage(err), - }); - return; - } - - if (window.location.href.includes("ui.nabu.casa")) { - const confirm = await showConfirmationDialog(this, { - title: supervisor.localize("backup.remote_download_title"), - text: supervisor.localize("backup.remote_download_text"), - confirmText: supervisor.localize("backup.download"), - dismissText: supervisor?.localize("backup.cancel"), - }); - if (!confirm) { - return; - } - } - - fileDownload( - signedPath.path, - `home_assistant_backup_${slugify(this._computeName)}.tar` - ); - } - - private get _computeName() { - return this._backup - ? this._backup.name || this._backup.slug - : this._dialogParams!.supervisor?.localize("backup.unnamed_backup") || ""; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - haStyleDialog, - css` - ha-header-bar { - --mdc-theme-on-primary: var(--primary-text-color); - --mdc-theme-primary: var(--mdc-theme-surface); - flex-shrink: 0; - display: block; - } - ha-icon-button { - color: var(--secondary-text-color); - } - .loading { - width: 100%; - display: flex; - height: 100%; - justify-content: center; - align-items: center; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-hassio-backup": HassioBackupDialog; - } -} diff --git a/hassio/src/dialogs/backup/dialog-hassio-create-backup.ts b/hassio/src/dialogs/backup/dialog-hassio-create-backup.ts deleted file mode 100644 index 8c2c05d946..0000000000 --- a/hassio/src/dialogs/backup/dialog-hassio-create-backup.ts +++ /dev/null @@ -1,158 +0,0 @@ -import type { CSSResultGroup } from "lit"; -import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import "../../../../src/components/ha-alert"; -import "../../../../src/components/ha-button"; -import "../../../../src/components/ha-spinner"; -import { createCloseHeading } from "../../../../src/components/ha-dialog"; -import { - createHassioFullBackup, - createHassioPartialBackup, -} from "../../../../src/data/hassio/backup"; -import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; -import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box"; -import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; -import type { HomeAssistant } from "../../../../src/types"; -import "../../components/supervisor-backup-content"; -import type { SupervisorBackupContent } from "../../components/supervisor-backup-content"; -import type { HassioCreateBackupDialogParams } from "./show-dialog-hassio-create-backup"; - -@customElement("dialog-hassio-create-backup") -class HassioCreateBackupDialog extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private _dialogParams?: HassioCreateBackupDialogParams; - - @state() private _error?: string; - - @state() private _creatingBackup = false; - - @query("supervisor-backup-content") - private _backupContent!: SupervisorBackupContent; - - public showDialog(dialogParams: HassioCreateBackupDialogParams) { - this._dialogParams = dialogParams; - this._creatingBackup = false; - } - - public closeDialog() { - this._dialogParams = undefined; - this._creatingBackup = false; - this._error = undefined; - fireEvent(this, "dialog-closed", { dialog: this.localName }); - } - - protected render() { - if (!this._dialogParams) { - return nothing; - } - return html` - - ${this._creatingBackup - ? html`` - : html` - `} - ${this._error - ? html`${this._error}` - : ""} - - ${this._dialogParams.supervisor.localize("common.close")} - - - ${this._dialogParams.supervisor.localize("backup.create")} - - - `; - } - - private async _createBackup(): Promise { - if (this._dialogParams!.supervisor.info.state !== "running") { - showAlertDialog(this, { - title: this._dialogParams!.supervisor.localize( - "backup.could_not_create" - ), - text: this._dialogParams!.supervisor.localize( - "backup.create_blocked_not_running", - { state: this._dialogParams!.supervisor.info.state } - ), - }); - return; - } - const backupDetails = this._backupContent.backupDetails(); - this._creatingBackup = true; - - this._error = ""; - if (backupDetails.password && !backupDetails.password.length) { - this._error = this._dialogParams!.supervisor.localize( - "backup.enter_password" - ); - this._creatingBackup = false; - return; - } - if ( - backupDetails.password && - backupDetails.password !== backupDetails.confirm_password - ) { - this._error = this._dialogParams!.supervisor.localize( - "backup.passwords_not_matching" - ); - this._creatingBackup = false; - return; - } - - delete backupDetails.confirm_password; - - try { - if (this._backupContent.backupType === "full") { - await createHassioFullBackup(this.hass, backupDetails); - } else { - await createHassioPartialBackup(this.hass, backupDetails); - } - - this._dialogParams!.onCreate(); - this.closeDialog(); - } catch (err: any) { - this._error = extractApiErrorMessage(err); - } - this._creatingBackup = false; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - haStyleDialog, - css` - :host { - direction: var(--direction); - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-hassio-create-backup": HassioCreateBackupDialog; - } -} diff --git a/hassio/src/dialogs/backup/show-dialog-backup-upload.ts b/hassio/src/dialogs/backup/show-dialog-backup-upload.ts deleted file mode 100644 index 861f192595..0000000000 --- a/hassio/src/dialogs/backup/show-dialog-backup-upload.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import "./dialog-hassio-backup-upload"; - -export interface HassioBackupUploadDialogParams { - showBackup: (slug: string) => void; - reloadBackup?: () => Promise; - onboarding?: boolean; -} - -export const showBackupUploadDialog = ( - element: HTMLElement, - dialogParams: HassioBackupUploadDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-hassio-backup-upload", - dialogImport: () => import("./dialog-hassio-backup-upload"), - dialogParams, - }); -}; diff --git a/hassio/src/dialogs/backup/show-dialog-hassio-backu-location.ts b/hassio/src/dialogs/backup/show-dialog-hassio-backu-location.ts deleted file mode 100644 index 12ac69bc82..0000000000 --- a/hassio/src/dialogs/backup/show-dialog-hassio-backu-location.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import type { Supervisor } from "../../../../src/data/supervisor/supervisor"; - -export interface HassioBackupLocationDialogParams { - supervisor: Supervisor; -} - -export const showHassioBackupLocationDialog = ( - element: HTMLElement, - dialogParams: HassioBackupLocationDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-hassio-backup-location", - dialogImport: () => import("./dialog-hassio-backup-location"), - dialogParams, - }); -}; diff --git a/hassio/src/dialogs/backup/show-dialog-hassio-backup.ts b/hassio/src/dialogs/backup/show-dialog-hassio-backup.ts deleted file mode 100644 index 57b354f087..0000000000 --- a/hassio/src/dialogs/backup/show-dialog-hassio-backup.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import type { Supervisor } from "../../../../src/data/supervisor/supervisor"; - -export interface HassioBackupDialogParams { - slug: string; - onDelete?: () => void; - onRestoring?: () => void; - onboarding?: boolean; - supervisor?: Supervisor; -} - -export const showHassioBackupDialog = ( - element: HTMLElement, - dialogParams: HassioBackupDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-hassio-backup", - dialogImport: () => import("./dialog-hassio-backup"), - dialogParams, - }); -}; diff --git a/hassio/src/dialogs/backup/show-dialog-hassio-create-backup.ts b/hassio/src/dialogs/backup/show-dialog-hassio-create-backup.ts deleted file mode 100644 index aada29d196..0000000000 --- a/hassio/src/dialogs/backup/show-dialog-hassio-create-backup.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import type { Supervisor } from "../../../../src/data/supervisor/supervisor"; - -export interface HassioCreateBackupDialogParams { - supervisor: Supervisor; - onCreate: () => void; -} - -export const showHassioCreateBackupDialog = ( - element: HTMLElement, - dialogParams: HassioCreateBackupDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-hassio-create-backup", - dialogImport: () => import("./dialog-hassio-create-backup"), - dialogParams, - }); -}; diff --git a/hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts b/hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts deleted file mode 100644 index 9cd6c4d087..0000000000 --- a/hassio/src/dialogs/datadisk/dialog-hassio-datadisk.ts +++ /dev/null @@ -1,184 +0,0 @@ -import type { CSSResultGroup } from "lit"; -import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import "../../../../src/components/ha-dialog"; -import "../../../../src/components/ha-button"; -import "../../../../src/components/ha-list-item"; -import "../../../../src/components/ha-select"; -import "../../../../src/components/ha-spinner"; -import { - extractApiErrorMessage, - ignoreSupervisorError, -} from "../../../../src/data/hassio/common"; -import type { DatadiskList } from "../../../../src/data/hassio/host"; -import { listDatadisks, moveDatadisk } from "../../../../src/data/hassio/host"; -import type { Supervisor } from "../../../../src/data/supervisor/supervisor"; -import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box"; -import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; -import type { HomeAssistant } from "../../../../src/types"; -import type { HassioDatatiskDialogParams } from "./show-dialog-hassio-datadisk"; - -const calculateMoveTime = memoizeOne((supervisor: Supervisor): number => { - // Assume a speed of 30 MB/s. - const moveTime = (supervisor.host.disk_used * 1000) / 60 / 30; - const rebootTime = (supervisor.host.startup_time * 4) / 60; - return Math.ceil((moveTime + rebootTime) / 10) * 10; -}); - -@customElement("dialog-hassio-datadisk") -class HassioDatadiskDialog extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private dialogParams?: HassioDatatiskDialogParams; - - @state() private selectedDevice?: string; - - @state() private devices?: DatadiskList["devices"]; - - @state() private moving = false; - - public showDialog(params: HassioDatatiskDialogParams) { - this.dialogParams = params; - listDatadisks(this.hass).then((data) => { - this.devices = data.devices; - }); - } - - public closeDialog(): void { - this.dialogParams = undefined; - this.selectedDevice = undefined; - this.devices = undefined; - this.moving = false; - fireEvent(this, "dialog-closed", { dialog: this.localName }); - } - - protected render() { - if (!this.dialogParams) { - return nothing; - } - return html` - - ${this.moving - ? html` -

- ${this.dialogParams.supervisor.localize( - "dialog.datadisk_move.moving_desc" - )} -

` - : html` ${this.devices?.length - ? html` - ${this.dialogParams.supervisor.localize( - "dialog.datadisk_move.description", - { - current_path: this.dialogParams.supervisor.os.data_disk, - time: calculateMoveTime(this.dialogParams.supervisor), - } - )} -

- - - ${this.devices.map( - (device) => - html`${device}` - )} - - ` - : this.devices === undefined - ? this.dialogParams.supervisor.localize( - "dialog.datadisk_move.loading_devices" - ) - : this.dialogParams.supervisor.localize( - "dialog.datadisk_move.no_devices" - )} - - - ${this.dialogParams.supervisor.localize( - "dialog.datadisk_move.cancel" - )} - - - - ${this.dialogParams.supervisor.localize( - "dialog.datadisk_move.move" - )} - `} -
- `; - } - - private _selectDevice(ev) { - this.selectedDevice = ev.target.value; - } - - private async _moveDatadisk() { - this.moving = true; - try { - await moveDatadisk(this.hass, this.selectedDevice!); - } catch (err: any) { - if (this.hass.connection.connected && !ignoreSupervisorError(err)) { - showAlertDialog(this, { - title: this.dialogParams!.supervisor.localize( - "system.host.failed_to_move" - ), - text: extractApiErrorMessage(err), - }); - this.closeDialog(); - } - } - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - haStyleDialog, - css` - ha-select { - width: 100%; - } - ha-spinner { - display: block; - margin: 32px; - text-align: center; - } - - .progress-text { - text-align: center; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-hassio-datadisk": HassioDatadiskDialog; - } -} diff --git a/hassio/src/dialogs/datadisk/show-dialog-hassio-datadisk.ts b/hassio/src/dialogs/datadisk/show-dialog-hassio-datadisk.ts deleted file mode 100644 index bef5dd64c8..0000000000 --- a/hassio/src/dialogs/datadisk/show-dialog-hassio-datadisk.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import type { Supervisor } from "../../../../src/data/supervisor/supervisor"; - -export interface HassioDatatiskDialogParams { - supervisor: Supervisor; -} - -export const showHassioDatadiskDialog = ( - element: HTMLElement, - dialogParams: HassioDatatiskDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-hassio-datadisk", - dialogImport: () => import("./dialog-hassio-datadisk"), - dialogParams, - }); -}; diff --git a/hassio/src/dialogs/hardware/dialog-hassio-hardware.ts b/hassio/src/dialogs/hardware/dialog-hassio-hardware.ts deleted file mode 100644 index 440a6c44c1..0000000000 --- a/hassio/src/dialogs/hardware/dialog-hassio-hardware.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { mdiClose } from "@mdi/js"; -import { dump } from "js-yaml"; -import type { CSSResultGroup } from "lit"; -import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import { stringCompare } from "../../../../src/common/string/compare"; -import "../../../../src/components/ha-dialog"; -import "../../../../src/components/ha-expansion-panel"; -import "../../../../src/components/ha-icon-button"; -import "../../../../src/components/search-input"; -import type { HassioHardwareInfo } from "../../../../src/data/hassio/hardware"; -import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; -import type { HomeAssistant } from "../../../../src/types"; -import type { HassioHardwareDialogParams } from "./show-dialog-hassio-hardware"; - -const _filterDevices = memoizeOne( - (hardware: HassioHardwareInfo, filter: string, language: string) => - hardware.devices - .filter( - (device) => - device.by_id?.toLowerCase().includes(filter) || - device.name.toLowerCase().includes(filter) || - device.dev_path.toLocaleLowerCase().includes(filter) || - JSON.stringify(device.attributes).toLocaleLowerCase().includes(filter) - ) - .sort((a, b) => stringCompare(a.name, b.name, language)) -); - -@customElement("dialog-hassio-hardware") -class HassioHardwareDialog extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private _dialogParams?: HassioHardwareDialogParams; - - @state() private _filter?: string; - - public showDialog(dialogParams: HassioHardwareDialogParams) { - this._dialogParams = dialogParams; - } - - public closeDialog() { - this._dialogParams = undefined; - fireEvent(this, "dialog-closed", { dialog: this.localName }); - } - - protected render() { - if (!this._dialogParams) { - return nothing; - } - - const devices = _filterDevices( - this._dialogParams.hardware, - (this._filter || "").toLowerCase(), - this.hass.locale.language - ); - - return html` - -
-

- ${this._dialogParams.supervisor.localize("dialog.hardware.title")} -

- - - -
- - ${devices.map( - (device) => - html` -
- - ${this._dialogParams!.supervisor.localize( - "dialog.hardware.subsystem" - )}: - - ${device.subsystem} -
-
- - ${this._dialogParams!.supervisor.localize( - "dialog.hardware.device_path" - )}: - - ${device.dev_path} -
- ${device.by_id - ? html`
- - ${this._dialogParams!.supervisor.localize( - "dialog.hardware.id" - )}: - - ${device.by_id} -
` - : ""} -
- - ${this._dialogParams!.supervisor.localize( - "dialog.hardware.attributes" - )}: - -
${dump(device.attributes, { indent: 2 })}
-
-
` - )} -
- `; - } - - private _handleSearchChange(ev: CustomEvent) { - this._filter = ev.detail.value; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - haStyleDialog, - css` - ha-icon-button { - position: absolute; - right: 16px; - inset-inline-end: 16px; - inset-inline-start: initial; - top: 10px; - text-decoration: none; - color: var(--primary-text-color); - } - h2 { - margin: 18px 42px 0 18px; - margin-inline-start: 18px; - margin-inline-end: 42px; - color: var(--primary-text-color); - } - - ha-expansion-panel { - margin: 4px 0; - } - pre, - code { - background-color: var(--markdown-code-background-color, none); - border-radius: var(--ha-border-radius-sm); - } - pre { - padding: 16px; - overflow: auto; - line-height: 1.45; - font-family: var(--ha-font-family-code); - } - code { - font-size: var(--ha-font-size-s); - padding: 0.2em 0.4em; - } - search-input { - margin: 8px 16px 0; - display: block; - } - .device-property { - display: flex; - justify-content: space-between; - } - .attributes { - margin-top: 12px; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-hassio-hardware": HassioHardwareDialog; - } -} diff --git a/hassio/src/dialogs/hardware/show-dialog-hassio-hardware.ts b/hassio/src/dialogs/hardware/show-dialog-hassio-hardware.ts deleted file mode 100644 index b43410bb65..0000000000 --- a/hassio/src/dialogs/hardware/show-dialog-hassio-hardware.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import type { HassioHardwareInfo } from "../../../../src/data/hassio/hardware"; -import type { Supervisor } from "../../../../src/data/supervisor/supervisor"; - -export interface HassioHardwareDialogParams { - supervisor: Supervisor; - hardware: HassioHardwareInfo; -} - -export const showHassioHardwareDialog = ( - element: HTMLElement, - dialogParams: HassioHardwareDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-hassio-hardware", - dialogImport: () => import("./dialog-hassio-hardware"), - dialogParams, - }); -}; diff --git a/hassio/src/dialogs/markdown/dialog-hassio-markdown.ts b/hassio/src/dialogs/markdown/dialog-hassio-markdown.ts deleted file mode 100644 index 740ee1dcfc..0000000000 --- a/hassio/src/dialogs/markdown/dialog-hassio-markdown.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { CSSResultGroup } from "lit"; -import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { createCloseHeading } from "../../../../src/components/ha-dialog"; -import "../../../../src/components/ha-markdown"; -import { haStyleDialog } from "../../../../src/resources/styles"; -import type { HomeAssistant } from "../../../../src/types"; -import { hassioStyle } from "../../resources/hassio-style"; -import type { HassioMarkdownDialogParams } from "./show-dialog-hassio-markdown"; - -@customElement("dialog-hassio-markdown") -class HassioMarkdownDialog extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - // eslint-disable-next-line lit/no-native-attributes - @property() public title!: string; - - @property() public content!: string; - - @state() private _opened = false; - - public showDialog(params: HassioMarkdownDialogParams) { - this.title = params.title; - this.content = params.content; - this._opened = true; - } - - public closeDialog() { - this._opened = false; - } - - protected render() { - if (!this._opened) { - return nothing; - } - return html` - - - - `; - } - - static get styles(): CSSResultGroup { - return [ - haStyleDialog, - hassioStyle, - css` - @media all and (max-width: 450px), all and (max-height: 500px) { - ha-markdown { - padding: 16px; - } - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-hassio-markdown": HassioMarkdownDialog; - } -} diff --git a/hassio/src/dialogs/markdown/show-dialog-hassio-markdown.ts b/hassio/src/dialogs/markdown/show-dialog-hassio-markdown.ts deleted file mode 100644 index 0d758a54af..0000000000 --- a/hassio/src/dialogs/markdown/show-dialog-hassio-markdown.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { fireEvent } from "../../../../src/common/dom/fire_event"; - -export interface HassioMarkdownDialogParams { - title: string; - content: string; -} - -export const showHassioMarkdownDialog = ( - element: HTMLElement, - dialogParams: HassioMarkdownDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-hassio-markdown", - dialogImport: () => import("./dialog-hassio-markdown"), - dialogParams, - }); -}; diff --git a/hassio/src/dialogs/network/dialog-hassio-network.ts b/hassio/src/dialogs/network/dialog-hassio-network.ts deleted file mode 100644 index a0797018dc..0000000000 --- a/hassio/src/dialogs/network/dialog-hassio-network.ts +++ /dev/null @@ -1,647 +0,0 @@ -import { mdiClose } from "@mdi/js"; -import type { CSSResultGroup } from "lit"; -import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { cache } from "lit/directives/cache"; -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import "../../../../src/components/ha-alert"; -import "../../../../src/components/ha-button"; -import "../../../../src/components/ha-dialog"; -import "../../../../src/components/ha-expansion-panel"; -import "../../../../src/components/ha-formfield"; -import "../../../../src/components/ha-header-bar"; -import "../../../../src/components/ha-icon-button"; -import "../../../../src/components/ha-list"; -import "../../../../src/components/ha-list-item"; -import "../../../../src/components/ha-password-field"; -import "../../../../src/components/ha-radio"; -import "../../../../src/components/ha-tab-group"; -import "../../../../src/components/ha-tab-group-tab"; -import "../../../../src/components/ha-textfield"; -import type { HaTextField } from "../../../../src/components/ha-textfield"; -import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; -import type { - AccessPoints, - NetworkInterface, - WifiConfiguration, -} from "../../../../src/data/hassio/network"; -import { - accesspointScan, - updateNetworkInterface, -} from "../../../../src/data/hassio/network"; -import type { Supervisor } from "../../../../src/data/supervisor/supervisor"; -import { - showAlertDialog, - showConfirmationDialog, -} from "../../../../src/dialogs/generic/show-dialog-box"; -import type { HassDialog } from "../../../../src/dialogs/make-dialog-manager"; -import { haStyleDialog } from "../../../../src/resources/styles"; -import type { HomeAssistant } from "../../../../src/types"; -import type { HassioNetworkDialogParams } from "./show-dialog-network"; - -const IP_VERSIONS = ["ipv4", "ipv6"]; - -@customElement("dialog-hassio-network") -export class DialogHassioNetwork - extends LitElement - implements HassDialog -{ - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - @state() private _accessPoints?: AccessPoints; - - @state() private _curTabIndex = 0; - - @state() private _dirty = false; - - @state() private _interface?: NetworkInterface; - - @state() private _interfaces!: NetworkInterface[]; - - @state() private _params?: HassioNetworkDialogParams; - - @state() private _processing = false; - - @state() private _scanning = false; - - @state() private _wifiConfiguration?: WifiConfiguration; - - public async showDialog(params: HassioNetworkDialogParams): Promise { - this._params = params; - this._dirty = false; - this._curTabIndex = 0; - this.supervisor = params.supervisor; - this._interfaces = params.supervisor.network.interfaces.sort((a, b) => - a.primary > b.primary ? -1 : 1 - ); - this._interface = { ...this._interfaces[this._curTabIndex] }; - - await this.updateComplete; - } - - public closeDialog() { - this._params = undefined; - this._processing = false; - fireEvent(this, "dialog-closed", { dialog: this.localName }); - return true; - } - - protected render() { - if (!this._params || !this._interface) { - return nothing; - } - - return html` - -
- - - ${this.supervisor.localize("dialog.network.title")} - - - - ${this._interfaces.length > 1 - ? html`${this._interfaces.map( - (device, index) => - html` - ${device.interface} - ` - )} - ` - : ""} -
- ${cache(this._renderTab())} -
- `; - } - - private _renderTab() { - return html`
- ${IP_VERSIONS.map((version) => - this._interface![version] ? this._renderIPConfiguration(version) : "" - )} - ${this._interface?.type === "wireless" - ? html` - - ${this._interface?.wifi?.ssid - ? html`

- ${this.supervisor.localize( - "dialog.network.connected_to", - { ssid: this._interface?.wifi?.ssid } - )} -

` - : ""} - - ${this.supervisor.localize("dialog.network.scan_ap")} - - ${this._accessPoints && - this._accessPoints.accesspoints && - this._accessPoints.accesspoints.length !== 0 - ? html` - - ${this._accessPoints.accesspoints - .filter((ap) => ap.ssid) - .map( - (ap) => html` - - ${ap.ssid} - - ${ap.mac} - - ${this.supervisor.localize( - "dialog.network.signal_strength" - )}: - ${ap.signal} - - - ` - )} - - ` - : ""} - ${this._wifiConfiguration - ? html` -
- - - - - - - - - - - - -
- ${this._wifiConfiguration.auth === "wpa-psk" || - this._wifiConfiguration.auth === "wep" - ? html` - - - ` - : ""} - ` - : ""} -
- ` - : ""} - ${this._dirty - ? html` - ${this.supervisor.localize("dialog.network.warning")} - ` - : ""} -
-
- - ${this.supervisor.localize("common.cancel")} - - - ${this.supervisor.localize("common.save")} - -
`; - } - - private _selectAP(event) { - this._wifiConfiguration = event.currentTarget.ap; - this._dirty = true; - } - - private async _scanForAP() { - if (!this._interface) { - return; - } - this._scanning = true; - try { - this._accessPoints = await accesspointScan( - this.hass, - this._interface.interface - ); - } catch (err: any) { - showAlertDialog(this, { - title: "Failed to scan for accesspoints", - text: extractApiErrorMessage(err), - }); - } finally { - this._scanning = false; - } - } - - private _renderIPConfiguration(version: string) { - return html` - -
- - - - - - - - - - - - -
- ${this._interface![version].method === "static" - ? html` - - - - - - - ` - : ""} -
- `; - } - - private _toArray(data: string | string[]): string[] { - if (Array.isArray(data)) { - if (data && typeof data[0] === "string") { - data = data[0]; - } - } - if (!data) { - return []; - } - if (typeof data === "string") { - return data.replace(/ /g, "").split(","); - } - return data; - } - - private _toString(data: string | string[]): string { - if (!data) { - return ""; - } - if (Array.isArray(data)) { - return data.join(", "); - } - return data; - } - - private async _updateNetwork() { - this._processing = true; - let interfaceOptions: Partial = {}; - - IP_VERSIONS.forEach((version) => { - interfaceOptions[version] = { - method: this._interface![version]?.method || "auto", - }; - if (this._interface![version]?.method === "static") { - interfaceOptions[version] = { - ...interfaceOptions[version], - address: this._toArray(this._interface![version]?.address), - gateway: this._interface![version]?.gateway, - nameservers: this._toArray(this._interface![version]?.nameservers), - }; - } - }); - - if (this._wifiConfiguration) { - interfaceOptions = { - ...interfaceOptions, - wifi: { - ssid: this._wifiConfiguration.ssid, - mode: this._wifiConfiguration.mode, - auth: this._wifiConfiguration.auth || "open", - }, - }; - if (interfaceOptions.wifi!.auth !== "open") { - interfaceOptions.wifi = { - ...interfaceOptions.wifi, - psk: this._wifiConfiguration.psk, - }; - } - } - - interfaceOptions.enabled = - this._wifiConfiguration !== undefined || - interfaceOptions.ipv4?.method !== "disabled" || - interfaceOptions.ipv6?.method !== "disabled"; - - try { - await updateNetworkInterface( - this.hass, - this._interface!.interface, - interfaceOptions - ); - } catch (err: any) { - showAlertDialog(this, { - title: this.supervisor.localize("dialog.network.failed_to_change"), - text: extractApiErrorMessage(err), - }); - this._processing = false; - return; - } - this._params?.loadData(); - this.closeDialog(); - } - - private async _handleTabActivated(ev: CustomEvent): Promise { - if (this._dirty) { - const confirm = await showConfirmationDialog(this, { - text: this.supervisor.localize("dialog.network.unsaved"), - confirmText: this.supervisor.localize("common.yes"), - dismissText: this.supervisor.localize("common.no"), - }); - if (!confirm) { - this.requestUpdate("_interface"); - return; - } - } - this._curTabIndex = Number(ev.detail.name); - this._interface = { ...this._interfaces[this._curTabIndex] }; - } - - private _handleRadioValueChanged(ev: CustomEvent): void { - const value = (ev.target as any).value as "disabled" | "auto" | "static"; - const version = (ev.target as any).version as "ipv4" | "ipv6"; - - if ( - !value || - !this._interface || - this._interface[version]!.method === value - ) { - return; - } - this._dirty = true; - - this._interface[version]!.method = value; - this.requestUpdate("_interface"); - } - - private _handleRadioValueChangedAp(ev: CustomEvent): void { - const value = (ev.target as any).value as string as - | "open" - | "wep" - | "wpa-psk"; - this._wifiConfiguration!.auth = value; - this._dirty = true; - this.requestUpdate("_wifiConfiguration"); - } - - private _handleInputValueChanged(ev: Event): void { - const source = ev.target as HaTextField; - const value = source.value; - const version = (ev.target as any).version as "ipv4" | "ipv6"; - const id = source.id; - - if ( - !value || - !this._interface || - this._toString(this._interface[version]![id]) === this._toString(value) - ) { - return; - } - - this._dirty = true; - this._interface[version]![id] = value; - } - - private _handleInputValueChangedWifi(ev: Event): void { - const source = ev.target as HaTextField; - const value = source.value; - const id = source.id; - - if ( - !value || - !this._wifiConfiguration || - this._wifiConfiguration![id] === value - ) { - return; - } - this._dirty = true; - this._wifiConfiguration![id] = value; - } - - static get styles(): CSSResultGroup { - return [ - haStyleDialog, - css` - ha-header-bar { - --mdc-theme-on-primary: var(--primary-text-color); - --mdc-theme-primary: var(--mdc-theme-surface); - flex-shrink: 0; - } - - ha-dialog { - --dialog-content-position: static; - --dialog-content-padding: 0; - --dialog-z-index: 6; - } - - @media all and (min-width: 451px) and (min-height: 501px) { - .container { - width: 400px; - } - } - - .content { - display: block; - padding: 20px 24px; - } - - /* overrule the ha-style-dialog max-height on small screens */ - @media all and (max-width: 450px), all and (max-height: 500px) { - ha-header-bar { - --mdc-theme-primary: var(--app-header-background-color); - --mdc-theme-on-primary: var(--app-header-text-color, white); - } - } - - ha-button.scan { - margin-left: 8px; - margin-inline-start: 8px; - margin-inline-end: initial; - } - - .container { - padding: 0 8px 4px; - } - .form { - margin-bottom: 53px; - } - .buttons { - position: absolute; - bottom: 0; - width: 100%; - box-sizing: border-box; - border-top: 1px solid - var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12)); - display: flex; - justify-content: space-between; - padding: 16px; - padding-bottom: max(var(--safe-area-inset-bottom), 16px); - background-color: var(--mdc-theme-surface, #fff); - } - .warning { - color: var(--error-color); - --primary-color: var(--error-color); - } - div.warning { - margin: 12px 4px -12px; - } - - ha-expansion-panel { - --expansion-panel-summary-padding: 0 16px; - margin: 4px 0; - } - ha-textfield { - padding: 0 14px; - } - ha-list-item { - --mdc-list-side-padding: 10px; - } - - ha-tab-group-tab { - flex: 1; - } - ha-tab-group-tab::part(base) { - width: 100%; - justify-content: center; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-hassio-network": DialogHassioNetwork; - } -} diff --git a/hassio/src/dialogs/network/show-dialog-network.ts b/hassio/src/dialogs/network/show-dialog-network.ts deleted file mode 100644 index 507b4938d3..0000000000 --- a/hassio/src/dialogs/network/show-dialog-network.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import type { Supervisor } from "../../../../src/data/supervisor/supervisor"; -import "./dialog-hassio-network"; - -export interface HassioNetworkDialogParams { - supervisor: Supervisor; - loadData: () => Promise; -} - -export const showNetworkDialog = ( - element: HTMLElement, - dialogParams: HassioNetworkDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-hassio-network", - dialogImport: () => import("./dialog-hassio-network"), - dialogParams, - }); -}; diff --git a/hassio/src/dialogs/registries/show-dialog-registries.ts b/hassio/src/dialogs/registries/show-dialog-registries.ts deleted file mode 100644 index 0ca9bf67dc..0000000000 --- a/hassio/src/dialogs/registries/show-dialog-registries.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import type { Supervisor } from "../../../../src/data/supervisor/supervisor"; -import "./dialog-hassio-registries"; - -export interface RegistriesDialogParams { - supervisor: Supervisor; -} - -export const showRegistriesDialog = ( - element: HTMLElement, - dialogParams: RegistriesDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-hassio-registries", - dialogImport: () => import("./dialog-hassio-registries"), - dialogParams, - }); -}; diff --git a/hassio/src/dialogs/repositories/show-dialog-repositories.ts b/hassio/src/dialogs/repositories/show-dialog-repositories.ts deleted file mode 100644 index 1b540c883c..0000000000 --- a/hassio/src/dialogs/repositories/show-dialog-repositories.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import type { Supervisor } from "../../../../src/data/supervisor/supervisor"; -import "./dialog-hassio-repositories"; - -export interface HassioRepositoryDialogParams { - supervisor: Supervisor; - url?: string; -} - -export const showRepositoriesDialog = ( - element: HTMLElement, - dialogParams: HassioRepositoryDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-hassio-repositories", - dialogImport: () => import("./dialog-hassio-repositories"), - dialogParams, - }); -}; diff --git a/hassio/src/dialogs/suggestAddonRestart.ts b/hassio/src/dialogs/suggestAddonRestart.ts deleted file mode 100644 index 74165ced94..0000000000 --- a/hassio/src/dialogs/suggestAddonRestart.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { LitElement } from "lit"; -import type { HassioAddonDetails } from "../../../src/data/hassio/addon"; -import { restartHassioAddon } from "../../../src/data/hassio/addon"; -import { extractApiErrorMessage } from "../../../src/data/hassio/common"; -import type { Supervisor } from "../../../src/data/supervisor/supervisor"; -import { - showAlertDialog, - showConfirmationDialog, -} from "../../../src/dialogs/generic/show-dialog-box"; -import type { HomeAssistant } from "../../../src/types"; - -export const suggestAddonRestart = async ( - element: LitElement, - hass: HomeAssistant, - supervisor: Supervisor, - addon: HassioAddonDetails -): Promise => { - const confirmed = await showConfirmationDialog(element, { - title: supervisor.localize("dialog.restart_addon.title", { - name: addon.name, - }), - text: supervisor.localize("dialog.restart_addon.text"), - confirmText: supervisor.localize("dialog.restart_addon.restart"), - dismissText: supervisor.localize("common.cancel"), - }); - if (confirmed) { - try { - await restartHassioAddon(hass, addon.slug); - } catch (err: any) { - showAlertDialog(element, { - title: supervisor.localize("common.failed_to_restart_name", { - name: addon.name, - }), - text: extractApiErrorMessage(err), - }); - } - } -}; diff --git a/hassio/src/dialogs/system-managed/dialog-system-managed.ts b/hassio/src/dialogs/system-managed/dialog-system-managed.ts deleted file mode 100644 index 7a41da8cd6..0000000000 --- a/hassio/src/dialogs/system-managed/dialog-system-managed.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { mdiClose, mdiPuzzle, mdiSwapHorizontal } from "@mdi/js"; -import type { CSSResultGroup } from "lit"; -import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; -import { atLeastVersion } from "../../../../src/common/config/version"; -import "../../../../src/components/ha-dialog-header"; -import "../../../../src/components/ha-icon-button"; -import "../../../../src/components/ha-icon-next"; -import "../../../../src/components/ha-md-dialog"; -import type { HaMdDialog } from "../../../../src/components/ha-md-dialog"; -import "../../../../src/components/ha-md-list"; -import "../../../../src/components/ha-md-list-item"; -import "../../../../src/components/ha-svg-icon"; -import { - getConfigEntry, - type ConfigEntry, -} from "../../../../src/data/config_entries"; -import type { HassioAddonDetails } from "../../../../src/data/hassio/addon"; -import type { Supervisor } from "../../../../src/data/supervisor/supervisor"; -import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg"; -import { haStyle } from "../../../../src/resources/styles"; -import type { HomeAssistant } from "../../../../src/types"; -import { brandsUrl } from "../../../../src/util/brands-url"; -import type { SystemManagedDialogParams } from "./show-dialog-system-managed"; - -@customElement("dialog-system-managed") -class HassioSystemManagedDialog extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private _supervisor?: Supervisor; - - @state() private _addon?: HassioAddonDetails; - - @state() private _open = false; - - @state() private _configEntry?: ConfigEntry; - - @query("ha-md-dialog") private _dialog?: HaMdDialog; - - public async showDialog( - dialogParams: SystemManagedDialogParams - ): Promise { - this._addon = dialogParams.addon; - this._supervisor = dialogParams.supervisor; - this._open = true; - this._loadConfigEntry(); - } - - private _dialogClosed() { - this._addon = undefined; - this._supervisor = undefined; - this._configEntry = undefined; - this._open = false; - } - - public closeDialog() { - this._dialog?.close(); - return true; - } - - protected render() { - if (!this._addon || !this._open || !this._supervisor) { - return nothing; - } - - const addonImage = - atLeastVersion(this.hass.config.version, 0, 105) && this._addon.icon - ? `/api/hassio/addons/${this._addon.slug}/icon` - : undefined; - - return html` - - - - ${this._addon?.name} - -
-
- - - ${addonImage - ? html`${this._addon.name}` - : html``} -
- ${this._supervisor.localize("addon.system_managed.title")}.
- ${this._supervisor.localize("addon.system_managed.description")} - ${this._configEntry - ? html` -

- ${this._supervisor.localize( - "addon.system_managed.managed_by" - )}: -

- - - ${this._configEntry.title} - ${this._configEntry.title} - - - - ` - : nothing} -
-
- `; - } - - private _onImageLoad(ev) { - ev.target.style.visibility = "initial"; - } - - private _onImageError(ev) { - ev.target.style.visibility = "hidden"; - } - - private async _loadConfigEntry() { - if (this._addon?.system_managed_config_entry) { - try { - const { config_entry } = await getConfigEntry( - this.hass, - this._addon.system_managed_config_entry - ); - this._configEntry = config_entry; - } catch (err) { - // eslint-disable-next-line no-console - console.error(err); - } - } - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - .icons { - display: flex; - justify-content: center; - align-items: center; - gap: var(--ha-space-4); - --mdc-icon-size: 48px; - margin-bottom: 32px; - } - .icons img { - width: 48px; - } - .icons .primary { - color: var(--primary-color); - } - .actions { - display: flex; - justify-content: space-between; - } - .integration-icon { - width: 24px; - } - ha-md-list-item { - --md-list-item-leading-space: 4px; - --md-list-item-trailing-space: 4px; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "dialog-system-managed": HassioSystemManagedDialog; - } -} diff --git a/hassio/src/dialogs/system-managed/show-dialog-system-managed.ts b/hassio/src/dialogs/system-managed/show-dialog-system-managed.ts deleted file mode 100644 index 3e198d4afb..0000000000 --- a/hassio/src/dialogs/system-managed/show-dialog-system-managed.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import type { HassioAddonDetails } from "../../../../src/data/hassio/addon"; -import type { Supervisor } from "../../../../src/data/supervisor/supervisor"; - -export interface SystemManagedDialogParams { - addon: HassioAddonDetails; - supervisor: Supervisor; -} - -export const showSystemManagedDialog = ( - element: HTMLElement, - dialogParams: SystemManagedDialogParams -): void => { - fireEvent(element, "show-dialog", { - dialogTag: "dialog-system-managed", - dialogImport: () => import("./dialog-system-managed"), - dialogParams, - }); -}; diff --git a/hassio/src/entrypoint.js.template b/hassio/src/entrypoint.js.template deleted file mode 100644 index db0cf25251..0000000000 --- a/hassio/src/entrypoint.js.template +++ /dev/null @@ -1,23 +0,0 @@ -(function () { - function loadES5(src) { - var el = document.createElement("script"); - el.src = src; - document.body.appendChild(el); - } - if (<%= modernRegex %>.test(navigator.userAgent)) { - try { - <% for (const entry of latestEntryJS) { %> - new Function("import('<%= entry %>')")(); - <% } %> - } catch (err) { - <% for (const entry of es5EntryJS) { %> - loadES5("<%= entry %>"); - <% } %> - } - } else { - <% for (const entry of es5EntryJS) { %> - loadES5("<%= entry %>"); - <% } %> - } -})(); - \ No newline at end of file diff --git a/hassio/src/entrypoint.ts b/hassio/src/entrypoint.ts deleted file mode 100644 index 73fa378323..0000000000 --- a/hassio/src/entrypoint.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { - haFontFamilyBody, - haFontSmoothing, - haMozOsxFontSmoothing, -} from "../../src/resources/theme/typography.globals"; -import "./hassio-main"; - -import("../../src/resources/append-ha-style"); - -const styleEl = document.createElement("style"); -styleEl.textContent = ` -body { - font-family: ${haFontFamilyBody}; - -moz-osx-font-smoothing: ${haMozOsxFontSmoothing}; - -webkit-font-smoothing: ${haFontSmoothing}; - font-weight: var(--ha-font-weight-normal); - margin: 0; - padding: 0; - height: 100vh; -} -@media (prefers-color-scheme: dark) { - body { - background-color: #111111; - color: #e1e1e1; - } -} -`; -document.head.appendChild(styleEl); diff --git a/hassio/src/hassio-main.ts b/hassio/src/hassio-main.ts deleted file mode 100644 index 7afbf5d333..0000000000 --- a/hassio/src/hassio-main.ts +++ /dev/null @@ -1,146 +0,0 @@ -import type { PropertyValues } from "lit"; -import { html } from "lit"; -import { customElement, property } from "lit/decorators"; -import { atLeastVersion } from "../../src/common/config/version"; -import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element"; -import { fireEvent } from "../../src/common/dom/fire_event"; -import { mainWindow } from "../../src/common/dom/get_main_window"; -import { isNavigationClick } from "../../src/common/dom/is-navigation-click"; -import { navigate } from "../../src/common/navigate"; -import type { HassioPanelInfo } from "../../src/data/hassio/supervisor"; -import type { Supervisor } from "../../src/data/supervisor/supervisor"; -import { makeDialogManager } from "../../src/dialogs/make-dialog-manager"; -import type { HomeAssistant } from "../../src/types"; -import "./hassio-router"; -import { SupervisorBaseElement } from "./supervisor-base-element"; - -@customElement("hassio-main") -export class HassioMain extends SupervisorBaseElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - @property({ attribute: false }) public panel!: HassioPanelInfo; - - @property({ type: Boolean }) public narrow = false; - - protected firstUpdated(changedProps: PropertyValues) { - super.firstUpdated(changedProps); - - this._applyTheme(); - - // Paulus - March 17, 2019 - // We went to a single hass-toggle-menu event in HA 0.90. However, the - // supervisor UI can also run under older versions of Home Assistant. - // So here we are going to translate toggle events into the appropriate - // open and close events. These events are a no-op in newer versions of - // Home Assistant. - this.addEventListener("hass-toggle-menu", () => { - fireEvent( - (window.parent as any).customPanel, - // @ts-ignore - this.hass.dockedSidebar ? "hass-close-menu" : "hass-open-menu" - ); - }); - // Paulus - March 19, 2019 - // We changed the navigate event to fire directly on the window, as that's - // where we are listening for it. However, the older panel_custom will - // listen on this element for navigation events, so we need to forward them. - - // Joakim - April 26, 2021 - // Due to changes in behavior in Google Chrome, we changed navigate to listen on the top element - mainWindow.addEventListener("location-changed", (ev) => - // @ts-ignore - fireEvent(this, ev.type, ev.detail, { - bubbles: false, - }) - ); - - // Paulus - May 17, 2021 - // Convert the tags to native nav in Home Assistant < 2021.6 - document.body.addEventListener("click", (ev) => { - const href = isNavigationClick(ev); - if (href) { - navigate(href); - } - }); - - // Forward haptic events to parent window. - window.addEventListener("haptic", (ev) => { - // @ts-ignore - fireEvent(window.parent, ev.type, ev.detail, { - bubbles: false, - }); - }); - - // Forward keydown events to the main window for quickbar access - document.body.addEventListener("keydown", (ev: KeyboardEvent) => { - if (ev.altKey || ev.ctrlKey || ev.shiftKey || ev.metaKey) { - // Ignore if modifier keys are pressed - return; - } - // @ts-ignore - fireEvent(mainWindow, "hass-quick-bar-trigger", ev, { - bubbles: false, - }); - }); - - makeDialogManager(this, this.shadowRoot!); - } - - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if (!oldHass) { - return; - } - if (oldHass.themes !== this.hass.themes) { - this._applyTheme(); - } - } - - protected render() { - return html` - - `; - } - - private _applyTheme() { - let themeName: string; - let themeSettings: Partial | undefined; - - if (atLeastVersion(this.hass.config.version, 0, 114)) { - themeName = - this.hass.selectedTheme?.theme || - (this.hass.themes.darkMode && this.hass.themes.default_dark_theme - ? this.hass.themes.default_dark_theme! - : this.hass.themes.default_theme); - - themeSettings = this.hass.selectedTheme; - } else { - themeName = - (this.hass.selectedTheme as unknown as string) || - this.hass.themes.default_theme; - } - - applyThemesOnElement( - this.parentElement, - this.hass.themes, - themeName, - themeSettings, - true - ); - } -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-main": HassioMain; - } -} diff --git a/hassio/src/hassio-my-redirect.ts b/hassio/src/hassio-my-redirect.ts deleted file mode 100644 index 12f311038e..0000000000 --- a/hassio/src/hassio-my-redirect.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { sanitizeUrl } from "@braintree/sanitize-url"; -import type { TemplateResult } from "lit"; -import { html, LitElement, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { navigate } from "../../src/common/navigate"; -import { - createSearchParam, - extractSearchParamsObject, -} from "../../src/common/url/search-params"; -import type { Supervisor } from "../../src/data/supervisor/supervisor"; -import "../../src/layouts/hass-error-screen"; -import type { - ParamType, - Redirect, - Redirects, -} from "../../src/panels/my/ha-panel-my"; -import type { HomeAssistant, Route } from "../../src/types"; - -export const REDIRECTS: Redirects = { - supervisor: { - redirect: "/hassio/dashboard", - }, - supervisor_logs: { - redirect: "/hassio/system", - }, - supervisor_info: { - redirect: "/hassio/system", - }, - supervisor_snapshots: { - redirect: "/hassio/backups", - }, - supervisor_backups: { - redirect: "/hassio/backups", - }, - supervisor_store: { - redirect: "/hassio/store", - }, - supervisor_addons: { - redirect: "/hassio/dashboard", - }, - supervisor_addon: { - redirect: "/hassio/addon", - params: { - addon: "string", - }, - optional_params: { - repository_url: "url", - }, - }, - supervisor_ingress: { - redirect: "/hassio/ingress", - params: { - addon: "string", - }, - }, - supervisor_add_addon_repository: { - redirect: "/hassio/store", - params: { - repository_url: "url", - }, - }, -}; - -@customElement("hassio-my-redirect") -class HassioMyRedirect extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - @property({ attribute: false }) public route!: Route; - - @state() public _error?: TemplateResult | string; - - connectedCallback() { - super.connectedCallback(); - const path = this.route.path.substr(1); - const redirect = REDIRECTS[path]; - - if (!redirect) { - this._error = this.supervisor.localize("my.not_supported", { - link: html` - ${this.supervisor.localize("my.faq_link")} - `, - }); - return; - } - - let url: string; - try { - url = this._createRedirectUrl(redirect); - } catch (_err: any) { - this._error = this.supervisor.localize("my.error"); - return; - } - - navigate(url, { replace: true }); - } - - protected render() { - if (this._error) { - return html``; - } - return nothing; - } - - private _createRedirectUrl(redirect: Redirect): string { - const params = this._createRedirectParams(redirect); - return `${redirect.redirect}${params}`; - } - - private _createRedirectParams(redirect: Redirect): string { - const params = extractSearchParamsObject(); - if (!redirect.params && !Object.keys(params).length) { - return ""; - } - const resultParams = {}; - Object.entries(redirect.params || {}).forEach(([key, type]) => { - if (!params[key] || !this._checkParamType(type, params[key])) { - throw Error(); - } - resultParams[key] = params[key]; - }); - Object.entries(redirect.optional_params || {}).forEach(([key, type]) => { - if (params[key]) { - if (!this._checkParamType(type, params[key])) { - throw Error(); - } - resultParams[key] = params[key]; - } - }); - return `?${createSearchParam(resultParams)}`; - } - - private _checkParamType(type: ParamType, value: string) { - if (type === "string") { - return true; - } - if (type === "url") { - return value && value === sanitizeUrl(value); - } - return false; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-my-redirect": HassioMyRedirect; - } -} diff --git a/hassio/src/hassio-panel-router.ts b/hassio/src/hassio-panel-router.ts deleted file mode 100644 index 42eff379f5..0000000000 --- a/hassio/src/hassio-panel-router.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { customElement, property } from "lit/decorators"; -import type { Supervisor } from "../../src/data/supervisor/supervisor"; -import type { RouterOptions } from "../../src/layouts/hass-router-page"; -import { HassRouterPage } from "../../src/layouts/hass-router-page"; -import type { HomeAssistant, Route } from "../../src/types"; -// Don't codesplit it, that way the dashboard always loads fast. -import "./dashboard/hassio-dashboard"; - -@customElement("hassio-panel-router") -class HassioPanelRouter extends HassRouterPage { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - @property({ attribute: false }) public route!: Route; - - @property({ type: Boolean }) public narrow = false; - - protected routerOptions: RouterOptions = { - beforeRender: (page: string) => - page === "snapshots" ? "backups" : undefined, - routes: { - dashboard: { - tag: "hassio-dashboard", - }, - store: { - tag: "hassio-addon-store", - load: () => import("./addon-store/hassio-addon-store"), - }, - backups: { - tag: "hassio-backups", - load: () => import("./backups/hassio-backups"), - }, - system: { - tag: "hassio-system", - load: () => import("./system/hassio-system"), - }, - }, - }; - - protected updatePageEl(el) { - el.hass = this.hass; - el.supervisor = this.supervisor; - el.route = this.route; - el.narrow = this.narrow; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-panel-router": HassioPanelRouter; - } -} diff --git a/hassio/src/hassio-panel.ts b/hassio/src/hassio-panel.ts deleted file mode 100644 index c0922f463d..0000000000 --- a/hassio/src/hassio-panel.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { TemplateResult } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators"; -import type { Supervisor } from "../../src/data/supervisor/supervisor"; -import { supervisorCollection } from "../../src/data/supervisor/supervisor"; -import "../../src/layouts/hass-loading-screen"; -import type { HomeAssistant, Route } from "../../src/types"; -import "./hassio-panel-router"; - -@customElement("hassio-panel") -class HassioPanel extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - @property({ type: Boolean }) public narrow = false; - - @property({ attribute: false }) public route!: Route; - - protected render(): TemplateResult { - if (!this.hass) { - return html``; - } - - if ( - Object.keys(supervisorCollection).some( - (collection) => !this.supervisor[collection] - ) - ) { - return html``; - } - return html` - - `; - } - - static styles = css` - :host { - --app-header-background-color: var(--sidebar-background-color); - --app-header-text-color: var(--sidebar-text-color); - --app-header-border-bottom: 1px solid var(--divider-color); - } - `; -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-panel": HassioPanel; - } -} diff --git a/hassio/src/hassio-router.ts b/hassio/src/hassio-router.ts deleted file mode 100644 index a886fe2ae4..0000000000 --- a/hassio/src/hassio-router.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { customElement, property } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import type { HassioPanelInfo } from "../../src/data/hassio/supervisor"; -import type { Supervisor } from "../../src/data/supervisor/supervisor"; -import type { RouterOptions } from "../../src/layouts/hass-router-page"; -import { HassRouterPage } from "../../src/layouts/hass-router-page"; -import type { HomeAssistant } from "../../src/types"; -// Don't codesplit it, that way the dashboard always loads fast. -import "./hassio-panel"; - -@customElement("hassio-router") -class HassioRouter extends HassRouterPage { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - @property({ attribute: false }) public panel!: HassioPanelInfo; - - @property({ type: Boolean }) public narrow = false; - - protected routerOptions: RouterOptions = { - // Hass.io has a page with tabs, so we route all non-matching routes to it. - defaultPage: "dashboard", - beforeRender: (page: string) => { - if (page === "snapshots") { - return "backups"; - } - if (page === "dashboard" && this.panel.config?.ingress) { - return "ingress"; - } - return undefined; - }, - showLoading: true, - routes: { - dashboard: { - tag: "hassio-panel", - cache: true, - }, - backups: "dashboard", - store: "dashboard", - system: "dashboard", - "update-available": { - tag: "update-available-dashboard", - load: () => import("./update-available/update-available-dashboard"), - }, - addon: { - tag: "hassio-addon-dashboard", - load: () => import("./addon-view/hassio-addon-dashboard"), - }, - ingress: { - tag: "hassio-ingress-view", - load: () => import("./ingress-view/hassio-ingress-view"), - }, - _my_redirect: { - tag: "hassio-my-redirect", - load: () => import("./hassio-my-redirect"), - }, - }, - }; - - protected updatePageEl(el) { - // the tabs page does its own routing so needs full route. - const hassioPanel = el.localName === "hassio-panel"; - const ingressPanel = el.localName === "hassio-ingress-view"; - const route = hassioPanel - ? this.route - : ingressPanel && this.panel.config?.ingress - ? this._ingressRoute(this.panel.config?.ingress) - : this.routeTail; - - el.hass = this.hass; - el.narrow = this.narrow; - el.route = route; - el.supervisor = this.supervisor; - - if (ingressPanel) { - el.ingressPanel = Boolean(this.panel.config?.ingress); - } - } - - private _ingressRoute = memoizeOne((ingress: string) => ({ - prefix: "/hassio/ingress", - path: `/${ingress}`, - })); -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-router": HassioRouter; - } -} diff --git a/hassio/src/hassio-tabs.ts b/hassio/src/hassio-tabs.ts deleted file mode 100644 index a66646ee17..0000000000 --- a/hassio/src/hassio-tabs.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - mdiBackupRestore, - mdiCogs, - mdiPuzzle, - mdiViewDashboard, -} from "@mdi/js"; -import { atLeastVersion } from "../../src/common/config/version"; -import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage"; -import type { HomeAssistant } from "../../src/types"; - -export const supervisorTabs = (hass: HomeAssistant): PageNavigation[] => - atLeastVersion(hass.config.version, 2022, 5) - ? [] - : [ - { - translationKey: atLeastVersion(hass.config.version, 2021, 12) - ? "panel.addons" - : "panel.dashboard", - path: `/hassio/dashboard`, - iconPath: atLeastVersion(hass.config.version, 2021, 12) - ? mdiPuzzle - : mdiViewDashboard, - }, - { - translationKey: "panel.backups", - path: `/hassio/backups`, - iconPath: mdiBackupRestore, - }, - { - translationKey: "panel.system", - path: `/hassio/system`, - iconPath: mdiCogs, - }, - ]; diff --git a/hassio/src/ingress-view/hassio-ingress-view.ts b/hassio/src/ingress-view/hassio-ingress-view.ts deleted file mode 100644 index 417954398f..0000000000 --- a/hassio/src/ingress-view/hassio-ingress-view.ts +++ /dev/null @@ -1,377 +0,0 @@ -import { mdiMenu } from "@mdi/js"; -import type { PropertyValues, TemplateResult } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { fireEvent } from "../../../src/common/dom/fire_event"; -import { goBack, navigate } from "../../../src/common/navigate"; -import { extractSearchParam } from "../../../src/common/url/search-params"; -import { nextRender } from "../../../src/common/util/render-status"; -import "../../../src/components/ha-icon-button"; -import type { HassioAddonDetails } from "../../../src/data/hassio/addon"; -import { - fetchHassioAddonInfo, - startHassioAddon, -} from "../../../src/data/hassio/addon"; -import { extractApiErrorMessage } from "../../../src/data/hassio/common"; -import { - createHassioSession, - validateHassioSession, -} from "../../../src/data/hassio/ingress"; -import type { Supervisor } from "../../../src/data/supervisor/supervisor"; -import { - showAlertDialog, - showConfirmationDialog, -} from "../../../src/dialogs/generic/show-dialog-box"; -import "../../../src/layouts/hass-loading-screen"; -import "../../../src/layouts/hass-subpage"; -import type { HomeAssistant, Route } from "../../../src/types"; - -@customElement("hassio-ingress-view") -class HassioIngressView extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - @property({ attribute: false }) public route!: Route; - - @property({ attribute: false }) public ingressPanel = false; - - @property({ type: Boolean }) public narrow = false; - - @state() private _addon?: HassioAddonDetails; - - @state() private _loadingMessage?: string; - - private _sessionKeepAlive?: number; - - private _fetchDataTimeout?: number; - - public disconnectedCallback() { - super.disconnectedCallback(); - - if (this._sessionKeepAlive) { - clearInterval(this._sessionKeepAlive); - this._sessionKeepAlive = undefined; - } - if (this._fetchDataTimeout) { - clearInterval(this._fetchDataTimeout); - this._fetchDataTimeout = undefined; - } - } - - protected render(): TemplateResult { - if (!this._addon) { - return html``; - } - - const iframe = html``; - - if (!this.ingressPanel) { - return html` - ${iframe} - `; - } - - return html`${this.narrow || this.hass.dockedSidebar === "always_hidden" - ? html`
- -
${this._addon.name}
-
- ${iframe}` - : iframe}`; - } - - protected async firstUpdated(): Promise { - if (this.route.path === "") { - const requestedAddon = extractSearchParam("addon"); - let addonInfo: HassioAddonDetails; - if (requestedAddon) { - try { - addonInfo = await fetchHassioAddonInfo(this.hass, requestedAddon); - } catch (err: any) { - await showAlertDialog(this, { - text: extractApiErrorMessage(err), - title: requestedAddon, - }); - await nextRender(); - navigate("/hassio/store", { replace: true }); - return; - } - if (!addonInfo.version) { - await showAlertDialog(this, { - text: this.supervisor.localize("my.error_addon_not_installed"), - title: addonInfo.name, - }); - await nextRender(); - navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true }); - } else if (!addonInfo.ingress) { - await showAlertDialog(this, { - text: this.supervisor.localize("my.error_addon_no_ingress"), - title: addonInfo.name, - }); - await nextRender(); - navigate(`/hassio/addon/${addonInfo.slug}/info`, { replace: true }); - } else { - navigate(`/hassio/ingress/${addonInfo.slug}`, { replace: true }); - } - } - } - } - - protected willUpdate(changedProps: PropertyValues) { - super.willUpdate(changedProps); - - if (!changedProps.has("route")) { - return; - } - - const addon = this.route.path.substring(1); - - const oldRoute = changedProps.get("route") as this["route"] | undefined; - const oldAddon = oldRoute ? oldRoute.path.substring(1) : undefined; - - if (addon && addon !== oldAddon) { - this._loadingMessage = undefined; - this._fetchData(addon); - } - } - - private async _fetchData(addonSlug: string) { - const createSessionPromise = createHassioSession(this.hass); - - let addon: HassioAddonDetails; - - try { - addon = await fetchHassioAddonInfo(this.hass, addonSlug); - } catch (_err: any) { - await this.updateComplete; - await showAlertDialog(this, { - text: - this.supervisor.localize("ingress.error_addon_info") || - "Unable to fetch add-on info to start Ingress", - title: "Supervisor", - }); - await nextRender(); - navigate("/hassio/store", { replace: true }); - return; - } - - if (!addon.version) { - await this.updateComplete; - await showAlertDialog(this, { - text: - this.supervisor.localize("ingress.error_addon_not_installed") || - "The add-on is not installed. Please install it first", - title: addon.name, - }); - await nextRender(); - navigate(`/hassio/addon/${addon.slug}/info`, { replace: true }); - return; - } - - if (!addon.ingress_url) { - await this.updateComplete; - await showAlertDialog(this, { - text: - this.supervisor.localize("ingress.error_addon_not_supported") || - "This add-on does not support Ingress", - title: addon.name, - }); - await nextRender(); - goBack(); - return; - } - - if (!addon.state || !["startup", "started"].includes(addon.state)) { - await this.updateComplete; - const confirm = await showConfirmationDialog(this, { - text: - this.supervisor.localize("ingress.error_addon_not_running") || - "The add-on is not running. Do you want to start it now?", - title: addon.name, - confirmText: - this.supervisor.localize("ingress.start_addon") || "Start add-on", - dismissText: this.supervisor.localize("common.no") || "No", - }); - if (confirm) { - try { - this._loadingMessage = - this.supervisor.localize("ingress.addon_starting") || - "The add-on is starting, this can take some time..."; - await startHassioAddon(this.hass, addonSlug); - fireEvent(this, "supervisor-collection-refresh", { - collection: "addon", - }); - this._fetchData(addonSlug); - return; - } catch (_err) { - await showAlertDialog(this, { - text: - this.supervisor.localize("ingress.error_starting_addon") || - "Error starting the add-on", - title: addon.name, - }); - await nextRender(); - navigate(`/hassio/addon/${addon.slug}/logs`, { replace: true }); - return; - } - } else { - await nextRender(); - navigate(`/hassio/addon/${addon.slug}/info`, { replace: true }); - return; - } - } - - if (addon.state === "startup") { - // Addon is starting up, wait for it to start - this._loadingMessage = - this.supervisor.localize("ingress.addon_starting") || - "The add-on is starting, this can take some time..."; - - this._fetchDataTimeout = window.setTimeout(() => { - this._fetchData(addonSlug); - }, 500); - return; - } - - if (addon.state !== "started") { - return; - } - - this._loadingMessage = undefined; - - if (this._fetchDataTimeout) { - clearInterval(this._fetchDataTimeout); - this._fetchDataTimeout = undefined; - } - - let session: string; - - try { - session = await createSessionPromise; - } catch (_err: any) { - if (this._sessionKeepAlive) { - clearInterval(this._sessionKeepAlive); - } - await showAlertDialog(this, { - text: - this.supervisor.localize("ingress.error_creating_session") || - "Unable to create an Ingress session", - title: addon.name, - }); - await nextRender(); - goBack(); - return; - } - - if (this._sessionKeepAlive) { - clearInterval(this._sessionKeepAlive); - } - this._sessionKeepAlive = window.setInterval(async () => { - try { - await validateHassioSession(this.hass, session); - } catch (_err: any) { - session = await createHassioSession(this.hass); - } - }, 60000); - - this._addon = addon; - } - - private async _checkLoaded(ev): Promise { - if (!this._addon) { - return; - } - if (ev.target.contentDocument.body.textContent === "502: Bad Gateway") { - await this.updateComplete; - showConfirmationDialog(this, { - text: - this.supervisor.localize("ingress.error_addon_not_ready") || - "The add-on seems to not be ready, it might still be starting. Do you want to try again?", - title: this._addon.name, - confirmText: this.supervisor.localize("ingress.retry") || "Retry", - dismissText: this.supervisor.localize("common.no") || "No", - confirm: async () => { - const addon = this._addon; - this._addon = undefined; - await Promise.all([ - this.updateComplete, - new Promise((resolve) => { - setTimeout(resolve, 500); - }), - ]); - this._addon = addon; - }, - }); - } - } - - private _toggleMenu(): void { - fireEvent(this, "hass-toggle-menu"); - } - - static styles = css` - iframe { - display: block; - width: 100%; - height: 100%; - border: 0; - } - - .header + iframe { - height: calc(100% - 40px); - } - - .header { - display: flex; - align-items: center; - font-size: var(--ha-font-size-l); - height: 40px; - padding: 0 16px; - pointer-events: none; - background-color: var(--app-header-background-color); - font-weight: var(--ha-font-weight-normal); - color: var(--app-header-text-color, white); - border-bottom: var(--app-header-border-bottom, none); - box-sizing: border-box; - --mdc-icon-size: 20px; - } - - .main-title { - margin: var(--margin-title); - line-height: var(--ha-line-height-condensed); - flex-grow: 1; - } - - ha-icon-button { - pointer-events: auto; - } - - hass-subpage { - --app-header-background-color: var(--sidebar-background-color); - --app-header-text-color: var(--sidebar-text-color); - --app-header-border-bottom: 1px solid var(--divider-color); - } - `; -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-ingress-view": HassioIngressView; - } -} diff --git a/hassio/src/supervisor-base-element.ts b/hassio/src/supervisor-base-element.ts deleted file mode 100644 index c1b826762c..0000000000 --- a/hassio/src/supervisor-base-element.ts +++ /dev/null @@ -1,231 +0,0 @@ -import type { Collection, UnsubscribeFunc } from "home-assistant-js-websocket"; -import type { PropertyValues } from "lit"; -import { LitElement } from "lit"; -import { property, state } from "lit/decorators"; -import { atLeastVersion } from "../../src/common/config/version"; -import { computeLocalize } from "../../src/common/translations/localize"; -import { fetchHassioAddonsInfo } from "../../src/data/hassio/addon"; -import type { HassioResponse } from "../../src/data/hassio/common"; -import { - fetchHassioHassOsInfo, - fetchHassioHostInfo, -} from "../../src/data/hassio/host"; -import { fetchNetworkInfo } from "../../src/data/hassio/network"; -import { fetchHassioResolution } from "../../src/data/hassio/resolution"; -import { - fetchHassioHomeAssistantInfo, - fetchHassioInfo, - fetchHassioSupervisorInfo, -} from "../../src/data/hassio/supervisor"; -import { fetchSupervisorStore } from "../../src/data/supervisor/store"; -import type { - Supervisor, - SupervisorObject, - SupervisorKeys, -} from "../../src/data/supervisor/supervisor"; -import { - getSupervisorEventCollection, - supervisorCollection, - cleanupSupervisorCollection, -} from "../../src/data/supervisor/supervisor"; -import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin"; -import { urlSyncMixin } from "../../src/state/url-sync-mixin"; -import type { HomeAssistant, Route } from "../../src/types"; -import { getTranslation } from "../../src/util/common-translation"; -import { - computeRTLDirection, - setDirectionStyles, -} from "../../src/common/util/compute_rtl"; - -declare global { - interface HASSDomEvents { - "supervisor-update": Partial; - "supervisor-collection-refresh": { collection: SupervisorObject }; - } -} - -export class SupervisorBaseElement extends urlSyncMixin( - ProvideHassLitMixin(LitElement) -) { - @property({ attribute: false }) public route?: Route; - - @property({ attribute: false }) public supervisor: Partial = { - localize: () => "", - }; - - @state() private _unsubs: Record = {}; - - @state() private _collections: Record> = {}; - - @state() private _language = "en"; - - public connectedCallback(): void { - super.connectedCallback(); - if (!this.hasUpdated) { - return; - } - if (this.route?.prefix === "/hassio") { - this._initSupervisor(); - } - } - - public disconnectedCallback() { - super.disconnectedCallback(); - Object.keys(this._unsubs).forEach((unsub) => { - this._unsubs[unsub](); - delete this._unsubs[unsub]; - }); - Object.keys(this._collections).forEach((collection) => { - cleanupSupervisorCollection(this.hass.connection, collection); - }); - this._collections = {}; - this.removeEventListener( - "supervisor-collection-refresh", - this._handleSupervisorStoreRefreshEvent - ); - } - - protected willUpdate(changedProperties: PropertyValues) { - if (!this.hasUpdated) { - if (this.route?.prefix === "/hassio") { - this._initSupervisor(); - } - } - if (changedProperties.has("hass")) { - const oldHass = changedProperties.get("hass") as - | HomeAssistant - | undefined; - if (oldHass?.language !== this.hass.language) { - this._language = this.hass.language; - } - } - - if (changedProperties.has("_language") || !this.hasUpdated) { - this._initializeLocalize(); - this._applyDirection(this.hass); - } - } - - protected _updateSupervisor(update: Partial): void { - this.supervisor = { ...this.supervisor, ...update }; - } - - private async _initializeLocalize() { - const { language, data } = await getTranslation(null, this._language); - - this._updateSupervisor({ - localize: await computeLocalize( - this.constructor.prototype, - language, - { - [language]: data, - } - ), - }); - } - - private async _handleSupervisorStoreRefreshEvent(ev) { - const collection = ev.detail.collection; - if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) { - if (collection in this._collections) { - this._collections[collection].refresh(); - } - return; - } - - const response = await this.hass.callApi>( - "GET", - `hassio${supervisorCollection[collection]}` - ); - this._updateSupervisor({ [collection]: response.data }); - } - - private _subscribeCollection(collection: string) { - if (this._unsubs[collection]) { - this._unsubs[collection](); - } - try { - this._unsubs[collection] = this._collections[collection].subscribe( - (data) => - this._updateSupervisor({ - [collection]: data, - }) - ); - } catch (e) { - // eslint-disable-next-line no-console - console.error(e); - } - } - - private async _initSupervisor(): Promise { - this.addEventListener( - "supervisor-collection-refresh", - this._handleSupervisorStoreRefreshEvent - ); - - if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) { - Object.keys(supervisorCollection).forEach((collection) => { - if (collection in this._collections) { - this._subscribeCollection(collection); - this._collections[collection].refresh(); - } else { - this._collections[collection] = getSupervisorEventCollection( - this.hass.connection, - collection, - supervisorCollection[collection] - ); - if (this._collections[collection].state) { - // happens when the grace period of the collection unsubscribe has not passed yet - this._updateSupervisor({ - [collection]: this._collections[collection].state, - }); - } - this._subscribeCollection(collection); - } - }); - } else { - const [ - addon, - supervisor, - host, - core, - info, - os, - network, - resolution, - store, - ] = await Promise.all([ - fetchHassioAddonsInfo(this.hass), - fetchHassioSupervisorInfo(this.hass), - fetchHassioHostInfo(this.hass), - fetchHassioHomeAssistantInfo(this.hass), - fetchHassioInfo(this.hass), - fetchHassioHassOsInfo(this.hass), - fetchNetworkInfo(this.hass), - fetchHassioResolution(this.hass), - fetchSupervisorStore(this.hass), - ]); - - this._updateSupervisor({ - addon, - supervisor, - host, - core, - info, - os, - network, - resolution, - store, - }); - - this.addEventListener("supervisor-update", (ev) => - this._updateSupervisor(ev.detail) - ); - } - } - - private _applyDirection(hass: HomeAssistant) { - const direction = computeRTLDirection(hass); - setDirectionStyles(direction, this); - } -} diff --git a/hassio/src/system/hassio-core-info.ts b/hassio/src/system/hassio-core-info.ts deleted file mode 100644 index 7206c13859..0000000000 --- a/hassio/src/system/hassio-core-info.ts +++ /dev/null @@ -1,206 +0,0 @@ -import type { CSSResultGroup, TemplateResult } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { atLeastVersion } from "../../../src/common/config/version"; -import "../../../src/components/buttons/ha-progress-button"; -import "../../../src/components/ha-button"; -import "../../../src/components/ha-button-menu"; -import "../../../src/components/ha-card"; -import "../../../src/components/ha-settings-row"; -import type { HassioStats } from "../../../src/data/hassio/common"; -import { - extractApiErrorMessage, - fetchHassioStats, -} from "../../../src/data/hassio/common"; -import { restartCore } from "../../../src/data/supervisor/core"; -import type { Supervisor } from "../../../src/data/supervisor/supervisor"; -import { - showAlertDialog, - showConfirmationDialog, -} from "../../../src/dialogs/generic/show-dialog-box"; -import { haStyle } from "../../../src/resources/styles"; -import type { HomeAssistant } from "../../../src/types"; -import { bytesToString } from "../../../src/util/bytes-to-string"; -import "../components/supervisor-metric"; -import { hassioStyle } from "../resources/hassio-style"; - -@customElement("hassio-core-info") -class HassioCoreInfo extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - @state() private _metrics?: HassioStats; - - protected render(): TemplateResult | undefined { - const metrics = [ - { - description: this.supervisor.localize("system.core.cpu_usage"), - value: this._metrics?.cpu_percent, - }, - { - description: this.supervisor.localize("system.core.ram_usage"), - value: this._metrics?.memory_percent, - tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString( - this._metrics?.memory_limit - )}`, - }, - ]; - - return html` - -
-
- - - ${this.supervisor.localize("common.version")} - - - core-${this.supervisor.core.version} - - - - - ${this.supervisor.localize("common.newest_version")} - - - core-${this.supervisor.core.version_latest} - - ${!atLeastVersion(this.hass.config.version, 2021, 12) && - this.supervisor.core.update_available - ? html` - - ${this.supervisor.localize("common.show")} - - ` - : ""} - -
-
- ${metrics.map( - (metric) => html` - - ` - )} -
-
-
- - ${this.supervisor.localize("common.restart_name", { name: "Core" })} - -
-
- `; - } - - protected firstUpdated(): void { - this._loadData(); - } - - private async _loadData(): Promise { - this._metrics = await fetchHassioStats(this.hass, "core"); - } - - private async _coreRestart(ev: CustomEvent): Promise { - const button = ev.currentTarget as any; - button.progress = true; - - const confirmed = await showConfirmationDialog(this, { - title: this.supervisor.localize("confirm.restart.title", { - name: "Home Assistant Core", - }), - text: this.supervisor.localize("confirm.restart.text", { - name: "Home Assistant Core", - }), - confirmText: this.supervisor.localize("common.restart"), - dismissText: this.supervisor.localize("common.cancel"), - }); - - if (!confirmed) { - button.progress = false; - return; - } - - try { - await restartCore(this.hass); - } catch (err: any) { - if (this.hass.connection.connected) { - showAlertDialog(this, { - title: this.supervisor.localize("common.failed_to_restart_name", { - name: "Home Assistant Core", - }), - text: extractApiErrorMessage(err), - }); - } - } finally { - button.progress = false; - } - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - hassioStyle, - css` - ha-card { - height: 100%; - justify-content: space-between; - flex-direction: column; - display: flex; - } - .card-actions { - height: 48px; - border-top: none; - display: flex; - justify-content: flex-end; - align-items: center; - } - .card-content { - display: flex; - flex-direction: column; - height: calc(100% - 124px); - justify-content: space-between; - } - ha-settings-row { - padding: 0; - height: 54px; - width: 100%; - } - ha-settings-row[three-line] { - height: 74px; - } - ha-settings-row > span[slot="description"] { - white-space: normal; - color: var(--secondary-text-color); - } - ha-button-menu { - color: var(--secondary-text-color); - --mdc-menu-min-width: 200px; - } - a { - text-decoration: none; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-core-info": HassioCoreInfo; - } -} diff --git a/hassio/src/system/hassio-host-info.ts b/hassio/src/system/hassio-host-info.ts deleted file mode 100644 index 69c935701f..0000000000 --- a/hassio/src/system/hassio-host-info.ts +++ /dev/null @@ -1,453 +0,0 @@ -import { mdiDotsVertical } from "@mdi/js"; -import type { CSSResultGroup, TemplateResult } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import { atLeastVersion } from "../../../src/common/config/version"; -import { fireEvent } from "../../../src/common/dom/fire_event"; -import "../../../src/components/buttons/ha-progress-button"; -import "../../../src/components/ha-button"; -import "../../../src/components/ha-button-menu"; -import "../../../src/components/ha-card"; -import "../../../src/components/ha-icon-button"; -import "../../../src/components/ha-list-item"; -import "../../../src/components/ha-settings-row"; -import { - extractApiErrorMessage, - ignoreSupervisorError, -} from "../../../src/data/hassio/common"; -import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware"; -import { - changeHostOptions, - configSyncOS, - rebootHost, - shutdownHost, -} from "../../../src/data/hassio/host"; -import type { NetworkInfo } from "../../../src/data/hassio/network"; -import { fetchNetworkInfo } from "../../../src/data/hassio/network"; -import type { Supervisor } from "../../../src/data/supervisor/supervisor"; -import { - showAlertDialog, - showConfirmationDialog, - showPromptDialog, -} from "../../../src/dialogs/generic/show-dialog-box"; -import { haStyle } from "../../../src/resources/styles"; -import type { HomeAssistant } from "../../../src/types"; -import { - getValueInPercentage, - roundWithOneDecimal, -} from "../../../src/util/calculate"; -import "../components/supervisor-metric"; -import { showHassioDatadiskDialog } from "../dialogs/datadisk/show-dialog-hassio-datadisk"; -import { showHassioHardwareDialog } from "../dialogs/hardware/show-dialog-hassio-hardware"; -import { showNetworkDialog } from "../dialogs/network/show-dialog-network"; -import { hassioStyle } from "../resources/hassio-style"; - -@customElement("hassio-host-info") -class HassioHostInfo extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - protected render(): TemplateResult | undefined { - const primaryIpAddress = this.supervisor.host.features.includes("network") - ? this._primaryIpAddress(this.supervisor.network!) - : ""; - - const metrics = [ - { - description: this.supervisor.localize("system.host.used_space"), - value: this._getUsedSpace( - this.supervisor.host.disk_used, - this.supervisor.host.disk_total - ), - tooltip: `${this.supervisor.host.disk_used} GB/${this.supervisor.host.disk_total} GB`, - }, - ]; - return html` - -
-
- ${this.supervisor.host.features.includes("hostname") - ? html` - - ${this.supervisor.localize("system.host.hostname")} - - - ${this.supervisor.host.hostname} - - - ${this.supervisor.localize("system.host.change")} - - ` - : ""} - ${this.supervisor.host.features.includes("network") - ? html` - - ${this.supervisor.localize("system.host.ip_address")} - - ${primaryIpAddress} - - ${this.supervisor.localize("system.host.change")} - - ` - : ""} - - - - ${this.supervisor.localize("system.host.operating_system")} - - - ${this.supervisor.host.operating_system} - - ${!atLeastVersion(this.hass.config.version, 2021, 12) && - this.supervisor.os.update_available - ? html` - - ${this.supervisor.localize("common.show")} - - ` - : ""} - - ${!this.supervisor.host.features.includes("haos") - ? html` - - ${this.supervisor.localize("system.host.docker_version")} - - - ${this.supervisor.info.docker} - - ` - : ""} - ${this.supervisor.host.deployment - ? html` - - ${this.supervisor.localize("system.host.deployment")} - - - ${this.supervisor.host.deployment} - - ` - : ""} -
-
- ${this.supervisor.host.disk_life_time !== null - ? html` - - ${this.supervisor.localize("system.host.lifetime_used")} - - - ${this.supervisor.host.disk_life_time} % - - ` - : ""} - ${metrics.map( - (metric) => html` - - ` - )} -
-
-
- ${this.supervisor.host.features.includes("reboot") - ? html` - - ${this.supervisor.localize("system.host.reboot_host")} - - ` - : ""} - ${this.supervisor.host.features.includes("shutdown") - ? html` - - ${this.supervisor.localize("system.host.shutdown_host")} - - ` - : ""} - - - - - ${this.supervisor.localize("system.host.hardware")} - - ${this.supervisor.host.features.includes("haos") - ? html` - - ${this.supervisor.localize("system.host.import_from_usb")} - - ${this.supervisor.host.features.includes("os_agent") && - atLeastVersion(this.supervisor.host.agent_version, 1, 2, 0) - ? html` - - ${this.supervisor.localize( - "system.host.move_datadisk" - )} - - ` - : ""} - ` - : ""} - -
-
- `; - } - - protected firstUpdated(): void { - this._loadData(); - } - - private _getUsedSpace = memoizeOne((used: number, total: number) => - roundWithOneDecimal(getValueInPercentage(used, 0, total)) - ); - - private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => { - if (!network_info || !network_info.interfaces) { - return ""; - } - return network_info.interfaces.find((a) => a.primary)?.ipv4?.address![0]; - }); - - private async _handleMenuAction(ev) { - switch ((ev.target as any).action) { - case "hardware": - await this._showHardware(); - break; - case "import_from_usb": - await this._importFromUSB(); - break; - case "move_datadisk": - await this._moveDatadisk(); - break; - } - } - - private _moveDatadisk(): void { - showHassioDatadiskDialog(this, { - supervisor: this.supervisor, - }); - } - - private async _showHardware(): Promise { - let hardware; - try { - hardware = await fetchHassioHardwareInfo(this.hass); - } catch (err: any) { - await showAlertDialog(this, { - title: this.supervisor.localize( - "system.host.failed_to_get_hardware_list" - ), - text: extractApiErrorMessage(err), - }); - return; - } - showHassioHardwareDialog(this, { supervisor: this.supervisor, hardware }); - } - - private async _hostReboot(ev: CustomEvent): Promise { - const button = ev.currentTarget as any; - button.progress = true; - - const confirmed = await showConfirmationDialog(this, { - title: this.supervisor.localize("system.host.reboot_host"), - text: this.supervisor.localize("system.host.confirm_reboot"), - confirmText: this.supervisor.localize("system.host.reboot_host"), - dismissText: this.supervisor.localize("common.cancel"), - }); - - if (!confirmed) { - button.progress = false; - return; - } - - try { - await rebootHost(this.hass); - } catch (err: any) { - // Ignore connection errors, these are all expected - if (this.hass.connection.connected && !ignoreSupervisorError(err)) { - showAlertDialog(this, { - title: this.supervisor.localize("system.host.failed_to_reboot"), - text: extractApiErrorMessage(err), - }); - } - } - button.progress = false; - } - - private async _hostShutdown(ev: CustomEvent): Promise { - const button = ev.currentTarget as any; - button.progress = true; - - const confirmed = await showConfirmationDialog(this, { - title: this.supervisor.localize("system.host.shutdown_host"), - text: this.supervisor.localize("system.host.confirm_shutdown"), - confirmText: this.supervisor.localize("system.host.shutdown_host"), - dismissText: this.supervisor.localize("common.cancel"), - }); - - if (!confirmed) { - button.progress = false; - return; - } - - try { - await shutdownHost(this.hass); - } catch (err: any) { - // Ignore connection errors, these are all expected - if (this.hass.connection.connected && !ignoreSupervisorError(err)) { - showAlertDialog(this, { - title: this.supervisor.localize("system.host.failed_to_shutdown"), - text: extractApiErrorMessage(err), - }); - } - } - button.progress = false; - } - - private async _changeNetworkClicked(): Promise { - showNetworkDialog(this, { - supervisor: this.supervisor, - loadData: () => this._loadData(), - }); - } - - private async _changeHostnameClicked(): Promise { - const curHostname: string = this.supervisor.host.hostname; - const hostname = await showPromptDialog(this, { - title: this.supervisor.localize("system.host.change_hostname"), - inputLabel: this.supervisor.localize("system.host.new_hostname"), - inputType: "string", - defaultValue: curHostname, - confirmText: this.supervisor.localize("common.update"), - }); - - if (hostname && hostname !== curHostname) { - try { - await changeHostOptions(this.hass, { hostname }); - fireEvent(this, "supervisor-collection-refresh", { - collection: "host", - }); - } catch (err: any) { - showAlertDialog(this, { - title: this.supervisor.localize("system.host.failed_to_set_hostname"), - text: extractApiErrorMessage(err), - }); - } - } - } - - private async _importFromUSB(): Promise { - try { - await configSyncOS(this.hass); - fireEvent(this, "supervisor-collection-refresh", { - collection: "host", - }); - } catch (err: any) { - showAlertDialog(this, { - title: this.supervisor.localize( - "system.host.failed_to_import_from_usb" - ), - text: extractApiErrorMessage(err), - }); - } - } - - private async _loadData(): Promise { - if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) { - fireEvent(this, "supervisor-collection-refresh", { - collection: "network", - }); - } else { - const network = await fetchNetworkInfo(this.hass); - fireEvent(this, "supervisor-update", { network }); - } - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - hassioStyle, - css` - ha-card { - height: 100%; - justify-content: space-between; - flex-direction: column; - display: flex; - } - .card-actions { - height: 48px; - border-top: none; - display: flex; - justify-content: space-between; - align-items: center; - } - .card-content { - display: flex; - flex-direction: column; - height: calc(100% - 124px); - justify-content: space-between; - } - ha-settings-row { - padding: 0; - height: 54px; - width: 100%; - } - ha-settings-row[three-line] { - height: 74px; - } - ha-settings-row > span[slot="description"] { - white-space: normal; - color: var(--secondary-text-color); - } - - ha-button-menu { - color: var(--secondary-text-color); - --mdc-menu-min-width: 200px; - } - ha-list-item ha-svg-icon { - color: var(--secondary-text-color); - } - a { - text-decoration: none; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-host-info": HassioHostInfo; - } -} diff --git a/hassio/src/system/hassio-supervisor-info.ts b/hassio/src/system/hassio-supervisor-info.ts deleted file mode 100644 index e47bfe42df..0000000000 --- a/hassio/src/system/hassio-supervisor-info.ts +++ /dev/null @@ -1,469 +0,0 @@ -import type { CSSResultGroup, TemplateResult } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { atLeastVersion } from "../../../src/common/config/version"; -import { fireEvent } from "../../../src/common/dom/fire_event"; -import "../../../src/components/buttons/ha-progress-button"; -import "../../../src/components/ha-alert"; -import "../../../src/components/ha-button"; -import "../../../src/components/ha-card"; -import "../../../src/components/ha-settings-row"; -import "../../../src/components/ha-switch"; -import type { HassioStats } from "../../../src/data/hassio/common"; -import { - extractApiErrorMessage, - fetchHassioStats, -} from "../../../src/data/hassio/common"; -import type { SupervisorOptions } from "../../../src/data/hassio/supervisor"; -import { - reloadSupervisor, - restartSupervisor, - setSupervisorOption, -} from "../../../src/data/hassio/supervisor"; -import type { Supervisor } from "../../../src/data/supervisor/supervisor"; -import { - showAlertDialog, - showConfirmationDialog, -} from "../../../src/dialogs/generic/show-dialog-box"; -import { showJoinBetaDialog } from "../../../src/panels/config/core/updates/show-dialog-join-beta"; -import { - UNHEALTHY_REASON_URL, - UNSUPPORTED_REASON_URL, -} from "../../../src/panels/config/repairs/dialog-system-information"; -import { haStyle } from "../../../src/resources/styles"; -import type { HomeAssistant } from "../../../src/types"; -import { bytesToString } from "../../../src/util/bytes-to-string"; -import { documentationUrl } from "../../../src/util/documentation-url"; -import "../components/supervisor-metric"; -import { hassioStyle } from "../resources/hassio-style"; - -@customElement("hassio-supervisor-info") -class HassioSupervisorInfo extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - @state() private _metrics?: HassioStats; - - protected render(): TemplateResult | undefined { - const metrics = [ - { - description: this.supervisor.localize("system.supervisor.cpu_usage"), - value: this._metrics?.cpu_percent, - }, - { - description: this.supervisor.localize("system.supervisor.ram_usage"), - value: this._metrics?.memory_percent, - tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString( - this._metrics?.memory_limit - )}`, - }, - ]; - return html` - -
-
- - - ${this.supervisor.localize("common.version")} - - - supervisor-${this.supervisor.supervisor.version} - - - - - ${this.supervisor.localize("common.newest_version")} - - - supervisor-${this.supervisor.supervisor.version_latest} - - ${!atLeastVersion(this.hass.config.version, 2021, 12) && - this.supervisor.supervisor.update_available - ? html` - - ${this.supervisor.localize("common.show")} - - ` - : ""} - - - - ${this.supervisor.localize("system.supervisor.channel")} - - - ${this.supervisor.supervisor.channel} - - ${this.supervisor.supervisor.channel === "beta" - ? html` - - ${this.supervisor.localize( - "system.supervisor.leave_beta_action" - )} - - ` - : this.supervisor.supervisor.channel === "stable" - ? html` - - ${this.supervisor.localize( - "system.supervisor.join_beta_action" - )} - - ` - : ""} - - - ${this.supervisor.supervisor.supported - ? !atLeastVersion(this.hass.config.version, 2021, 4) - ? html` - - ${this.supervisor.localize( - "system.supervisor.share_diagnostics" - )} - -
- ${this.supervisor.localize( - "system.supervisor.share_diagnostics_description" - )} - -
- -
` - : "" - : html` - ${this.supervisor.localize( - "system.supervisor.unsupported_title" - )} - - ${this.supervisor.localize("common.learn_more")} - - `} - ${!this.supervisor.supervisor.healthy - ? html` - ${this.supervisor.localize( - "system.supervisor.unhealthy_title" - )} - - ${this.supervisor.localize("common.learn_more")} - - ` - : ""} -
-
- ${metrics.map( - (metric) => html` - - ` - )} -
-
-
- - ${this.supervisor.localize("system.supervisor.reload_supervisor")} - - - ${this.supervisor.localize("common.restart_name", { - name: "Supervisor", - })} - -
-
- `; - } - - protected firstUpdated(): void { - this._loadData(); - } - - private async _loadData(): Promise { - this._metrics = await fetchHassioStats(this.hass, "supervisor"); - } - - private async _toggleBeta(ev: CustomEvent): Promise { - const button = ev.currentTarget as any; - button.progress = true; - - if (this.supervisor.supervisor.channel === "stable") { - showJoinBetaDialog(this, { - join: async () => { - await this._setChannel("beta"); - button.progress = false; - }, - cancel: () => { - button.progress = false; - }, - }); - } else { - await this._setChannel("stable"); - button.progress = false; - } - } - - private async _setChannel( - channel: SupervisorOptions["channel"] - ): Promise { - try { - const data: Partial = { - channel, - }; - await setSupervisorOption(this.hass, data); - await this._reloadSupervisor(); - } catch (err: any) { - showAlertDialog(this, { - title: this.supervisor.localize( - "system.supervisor.failed_to_set_option" - ), - text: extractApiErrorMessage(err), - }); - } - } - - private async _supervisorReload(ev: CustomEvent): Promise { - const button = ev.currentTarget as any; - button.progress = true; - - try { - await this._reloadSupervisor(); - } catch (err: any) { - showAlertDialog(this, { - title: this.supervisor.localize("system.supervisor.failed_to_reload"), - text: extractApiErrorMessage(err), - }); - } finally { - button.progress = false; - } - } - - private async _reloadSupervisor(): Promise { - await reloadSupervisor(this.hass); - fireEvent(this, "supervisor-collection-refresh", { - collection: "supervisor", - }); - } - - private async _supervisorRestart(ev: CustomEvent): Promise { - const button = ev.currentTarget as any; - button.progress = true; - - const confirmed = await showConfirmationDialog(this, { - title: this.supervisor.localize("confirm.restart.title", { - name: "Supervisor", - }), - text: this.supervisor.localize("confirm.restart.text", { - name: "Supervisor", - }), - confirmText: this.supervisor.localize("common.restart"), - dismissText: this.supervisor.localize("common.cancel"), - }); - - if (!confirmed) { - button.progress = false; - return; - } - - try { - await restartSupervisor(this.hass); - } catch (err: any) { - showAlertDialog(this, { - title: this.supervisor.localize("common.failed_to_restart_name", { - name: "Supervisor", - }), - text: extractApiErrorMessage(err), - }); - } finally { - button.progress = false; - } - } - - private async _diagnosticsInformationDialog(): Promise { - await showAlertDialog(this, { - title: this.supervisor.localize( - "system.supervisor.share_diagonstics_title" - ), - text: this.supervisor.localize( - "system.supervisor.share_diagonstics_description", - { line_break: html`

` } - ), - }); - } - - private async _unsupportedDialog(): Promise { - await showAlertDialog(this, { - title: this.supervisor.localize("system.supervisor.unsupported_title"), - text: html`${this.supervisor.localize( - "system.supervisor.unsupported_description" - )}

- `, - }); - } - - private async _unhealthyDialog(): Promise { - await showAlertDialog(this, { - title: this.supervisor.localize("system.supervisor.unhealthy_title"), - text: html`${this.supervisor.localize( - "system.supervisor.unhealthy_description" - )}

- `, - }); - } - - private async _toggleDiagnostics(): Promise { - try { - const data: SupervisorOptions = { - diagnostics: !this.supervisor.supervisor?.diagnostics, - }; - await setSupervisorOption(this.hass, data); - } catch (err: any) { - showAlertDialog(this, { - title: this.supervisor.localize( - "system.supervisor.failed_to_set_option" - ), - text: extractApiErrorMessage(err), - }); - } - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - hassioStyle, - css` - ha-card { - height: 100%; - justify-content: space-between; - flex-direction: column; - display: flex; - } - .card-actions { - height: 48px; - border-top: none; - display: flex; - justify-content: space-between; - align-items: center; - } - .card-content { - display: flex; - flex-direction: column; - height: calc(100% - 124px); - justify-content: space-between; - } - .metrics-block { - margin-top: 16px; - } - button.link { - color: var(--primary-color); - } - ha-settings-row { - padding: 0; - height: 54px; - width: 100%; - } - ha-settings-row[three-line] { - height: 74px; - } - ha-settings-row > div[slot="description"] { - white-space: normal; - color: var(--secondary-text-color); - } - a { - text-decoration: none; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-supervisor-info": HassioSupervisorInfo; - } -} diff --git a/hassio/src/system/hassio-supervisor-log.ts b/hassio/src/system/hassio-supervisor-log.ts deleted file mode 100644 index 13f2e6540f..0000000000 --- a/hassio/src/system/hassio-supervisor-log.ts +++ /dev/null @@ -1,162 +0,0 @@ -import type { CSSResultGroup, TemplateResult } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import "../../../src/components/buttons/ha-progress-button"; -import "../../../src/components/ha-alert"; -import "../../../src/components/ha-ansi-to-html"; -import "../../../src/components/ha-card"; -import "../../../src/components/ha-select"; -import "../../../src/components/ha-list-item"; -import { extractApiErrorMessage } from "../../../src/data/hassio/common"; -import { fetchHassioLogs } from "../../../src/data/hassio/supervisor"; -import type { Supervisor } from "../../../src/data/supervisor/supervisor"; -import "../../../src/layouts/hass-loading-screen"; -import { haStyle } from "../../../src/resources/styles"; -import type { HomeAssistant } from "../../../src/types"; -import { hassioStyle } from "../resources/hassio-style"; - -interface LogProvider { - key: string; - name: string; -} - -const logProviders: LogProvider[] = [ - { - key: "supervisor", - name: "Supervisor", - }, - { - key: "core", - name: "Core", - }, - { - key: "host", - name: "Host", - }, - { - key: "dns", - name: "DNS", - }, - { - key: "audio", - name: "Audio", - }, - { - key: "multicast", - name: "Multicast", - }, -]; - -@customElement("hassio-supervisor-log") -class HassioSupervisorLog extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - @state() private _error?: string; - - @state() private _selectedLogProvider = "supervisor"; - - @state() private _content?: string; - - public async connectedCallback(): Promise { - super.connectedCallback(); - await this._loadData(); - } - - protected render(): TemplateResult | undefined { - return html` - - ${this._error - ? html`${this._error}` - : ""} - ${this.hass.userData?.showAdvanced - ? html` - - ${logProviders.map( - (provider) => html` - - ${provider.name} - - ` - )} - - ` - : ""} - -
- ${this._content - ? html` - ` - : html``} -
-
- - ${this.supervisor.localize("common.refresh")} - -
-
- `; - } - - private async _setLogProvider(ev): Promise { - const provider = ev.target.value; - this._selectedLogProvider = provider; - this._loadData(); - } - - private async _refresh(ev: CustomEvent): Promise { - const button = ev.currentTarget as any; - button.progress = true; - await this._loadData(); - button.progress = false; - } - - private async _loadData(): Promise { - this._error = undefined; - - try { - const response = await fetchHassioLogs( - this.hass, - this._selectedLogProvider - ); - - this._content = await response.text(); - } catch (err: any) { - this._error = this.supervisor.localize("system.log.get_logs", { - provider: this._selectedLogProvider, - error: extractApiErrorMessage(err), - }); - } - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - hassioStyle, - css` - ha-card { - margin-top: 8px; - width: 100%; - } - pre { - white-space: pre-wrap; - } - ha-select { - width: 100%; - margin-bottom: 4px; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-supervisor-log": HassioSupervisorLog; - } -} diff --git a/hassio/src/system/hassio-system.ts b/hassio/src/system/hassio-system.ts deleted file mode 100644 index 643b9f3530..0000000000 --- a/hassio/src/system/hassio-system.ts +++ /dev/null @@ -1,93 +0,0 @@ -import type { CSSResultGroup, TemplateResult } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators"; -import { atLeastVersion } from "../../../src/common/config/version"; -import type { Supervisor } from "../../../src/data/supervisor/supervisor"; -import "../../../src/layouts/hass-tabs-subpage"; -import { haStyle } from "../../../src/resources/styles"; -import type { HomeAssistant, Route } from "../../../src/types"; -import { supervisorTabs } from "../hassio-tabs"; -import { hassioStyle } from "../resources/hassio-style"; -import "./hassio-core-info"; -import "./hassio-host-info"; -import "./hassio-supervisor-info"; -import "./hassio-supervisor-log"; - -@customElement("hassio-system") -class HassioSystem extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - @property({ type: Boolean }) public narrow = false; - - @property({ attribute: false }) public route!: Route; - - protected render(): TemplateResult | undefined { - return html` - - ${this.supervisor.localize("panel.system")} -
-
- - - -
- -
-
- `; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - hassioStyle, - css` - .content { - margin: 8px; - color: var(--primary-text-color); - } - .title { - margin-top: 24px; - color: var(--primary-text-color); - font-size: 2em; - padding-left: 8px; - padding-inline-start: 8px; - padding-inline-end: initial; - margin-bottom: 8px; - } - hassio-supervisor-log { - width: 100%; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hassio-system": HassioSystem; - } -} diff --git a/hassio/src/update-available/update-available-card.ts b/hassio/src/update-available/update-available-card.ts deleted file mode 100644 index dd40d6e886..0000000000 --- a/hassio/src/update-available/update-available-card.ts +++ /dev/null @@ -1,507 +0,0 @@ -import { - css, - type CSSResultGroup, - html, - LitElement, - nothing, - type PropertyValues, -} from "lit"; -import { customElement, property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; -import { atLeastVersion } from "../../../src/common/config/version"; -import { fireEvent } from "../../../src/common/dom/fire_event"; -import "../../../src/components/buttons/ha-progress-button"; -import "../../../src/components/ha-alert"; -import "../../../src/components/ha-button-menu"; -import "../../../src/components/ha-card"; -import "../../../src/components/ha-spinner"; -import "../../../src/components/ha-checkbox"; -import "../../../src/components/ha-faded"; -import "../../../src/components/ha-icon-button"; -import "../../../src/components/ha-markdown"; -import "../../../src/components/ha-md-list"; -import "../../../src/components/ha-md-list-item"; -import "../../../src/components/ha-svg-icon"; -import "../../../src/components/ha-switch"; -import type { HaSwitch } from "../../../src/components/ha-switch"; -import type { HassioAddonDetails } from "../../../src/data/hassio/addon"; -import { - fetchHassioAddonChangelog, - fetchHassioAddonInfo, - updateHassioAddon, -} from "../../../src/data/hassio/addon"; -import { - extractApiErrorMessage, - ignoreSupervisorError, -} from "../../../src/data/hassio/common"; -import { fetchHassioHassOsInfo, updateOS } from "../../../src/data/hassio/host"; -import { - fetchHassioHomeAssistantInfo, - fetchHassioSupervisorInfo, - updateSupervisor, -} from "../../../src/data/hassio/supervisor"; -import { updateCore } from "../../../src/data/supervisor/core"; -import type { StoreAddon } from "../../../src/data/supervisor/store"; -import type { Supervisor } from "../../../src/data/supervisor/supervisor"; -import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; -import { haStyle } from "../../../src/resources/styles"; -import type { HomeAssistant, Route } from "../../../src/types"; -import { addonArchIsSupported, extractChangelog } from "../util/addon"; - -declare global { - interface HASSDomEvents { - "update-complete": undefined; - } -} - -const SUPERVISOR_UPDATE_NAMES = { - core: "Home Assistant Core", - os: "Home Assistant Operating System", - supervisor: "Home Assistant Supervisor", -}; - -type UpdateType = "os" | "supervisor" | "core" | "addon"; - -const changelogUrl = ( - entry: UpdateType, - version: string -): string | undefined => { - if (entry === "addon") { - return undefined; - } - if (entry === "core") { - return version.includes("dev") - ? "https://github.com/home-assistant/core/commits/dev" - : version.includes("b") - ? "https://next.home-assistant.io/latest-release-notes/" - : "https://www.home-assistant.io/latest-release-notes/"; - } - if (entry === "os") { - return version.includes("dev") - ? "https://github.com/home-assistant/operating-system/commits/dev" - : `https://github.com/home-assistant/operating-system/releases/tag/${version}`; - } - if (entry === "supervisor") { - return version.includes("dev") - ? "https://github.com/home-assistant/supervisor/commits/main" - : `https://github.com/home-assistant/supervisor/releases/tag/${version}`; - } - return undefined; -}; - -@customElement("update-available-card") -class UpdateAvailableCard extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - @property({ attribute: false }) public route!: Route; - - @property({ type: Boolean }) public narrow = false; - - @property({ attribute: false }) public addonSlug?: string; - - @state() private _updateType?: UpdateType; - - @state() private _changelogContent?: string; - - @state() private _addonInfo?: HassioAddonDetails; - - @state() private _updating = false; - - @state() private _error?: string; - - private _addonStoreInfo = memoizeOne( - (slug: string, storeAddons: StoreAddon[]) => - storeAddons.find((addon) => addon.slug === slug) - ); - - protected render() { - if ( - !this._updateType || - (this._updateType === "addon" && !this._addonInfo) - ) { - return nothing; - } - - const changelog = changelogUrl(this._updateType, this._version_latest); - - const createBackupTexts = this._computeCreateBackupTexts(); - - return html` - -
- ${this._error - ? html`${this._error}` - : ""} - ${this._version === this._version_latest - ? html`

- ${this.supervisor.localize("update_available.no_update", { - name: this._name, - })} -

` - : !this._updating - ? html` - ${this._changelogContent - ? html` - - - - - ` - : nothing} -
-

- ${this.supervisor.localize( - "update_available.description", - { - name: this._name, - version: this._version, - newest_version: this._version_latest, - } - )} -

-
- ${createBackupTexts - ? html` -
- - - - ${createBackupTexts.title} - - - ${createBackupTexts.description - ? html` - - ${createBackupTexts.description} - - ` - : nothing} - - - - ` - : nothing} - ` - : html` -

- ${this.supervisor.localize("update_available.updating", { - name: this._name, - version: this._version_latest, - })} -

`} -
- ${this._version !== this._version_latest && !this._updating - ? html` -
- ${changelog - ? html` - - ${this.supervisor.localize( - "update_available.open_release_notes" - )} - - ` - : nothing} - - - ${this.supervisor.localize("common.update")} - -
- ` - : nothing} -
- `; - } - - protected firstUpdated(changedProps: PropertyValues) { - super.firstUpdated(changedProps); - const pathPart = this.route?.path.substring(1, this.route.path.length); - const updateType = ["core", "os", "supervisor"].includes(pathPart) - ? pathPart - : "addon"; - this._updateType = updateType as UpdateType; - - switch (updateType) { - case "addon": - if (!this.addonSlug) { - this.addonSlug = pathPart; - } - this._loadAddonData(); - break; - case "core": - this._loadCoreData(); - break; - case "supervisor": - this._loadSupervisorData(); - break; - case "os": - this._loadOsData(); - break; - } - } - - private _computeCreateBackupTexts(): - | { title: string; description?: string } - | undefined { - // Addon backup - if ( - this._updateType === "addon" && - atLeastVersion(this.hass.config.version, 2025, 2, 0) - ) { - const version = this._version; - return { - title: this.supervisor.localize("update_available.create_backup.addon"), - description: this.supervisor.localize( - "update_available.create_backup.addon_description", - { version: version } - ), - }; - } - - // Old behavior - if (this._updateType && ["core", "addon"].includes(this._updateType)) { - return { - title: this.supervisor.localize( - "update_available.create_backup.generic" - ), - }; - } - return undefined; - } - - get _shouldCreateBackup(): boolean { - if (this._updateType && !["core", "addon"].includes(this._updateType)) { - return false; - } - const createBackupSwitch = this.shadowRoot?.getElementById( - "create-backup" - ) as HaSwitch; - if (createBackupSwitch) { - return createBackupSwitch.checked; - } - return true; - } - - get _version(): string { - return this._updateType - ? this._updateType === "addon" - ? this._addonInfo!.version - : this.supervisor[this._updateType]?.version || "" - : ""; - } - - get _version_latest(): string { - return this._updateType - ? this._updateType === "addon" - ? this._addonInfo!.version_latest - : this.supervisor[this._updateType]?.version_latest || "" - : ""; - } - - get _name(): string { - return this._updateType - ? this._updateType === "addon" - ? this._addonInfo!.name - : SUPERVISOR_UPDATE_NAMES[this._updateType] - : ""; - } - - private async _loadAddonData() { - try { - this._addonInfo = await fetchHassioAddonInfo(this.hass, this.addonSlug!); - } catch (err) { - showAlertDialog(this, { - title: this._updateType, - text: extractApiErrorMessage(err), - }); - return; - } - const addonStoreInfo = - !this._addonInfo.detached && !this._addonInfo.available - ? this._addonStoreInfo( - this._addonInfo.slug, - this.supervisor.store.addons - ) - : undefined; - - if (this._addonInfo.changelog) { - try { - const content = await fetchHassioAddonChangelog( - this.hass, - this.addonSlug! - ); - this._changelogContent = extractChangelog(this._addonInfo, content); - } catch (err) { - this._error = extractApiErrorMessage(err); - return; - } - } - - if (!this._addonInfo.available && addonStoreInfo) { - if ( - !addonArchIsSupported( - this.supervisor.info.supported_arch, - this._addonInfo.arch - ) - ) { - this._error = this.supervisor.localize( - "addon.dashboard.not_available_arch" - ); - } else { - this._error = this.supervisor.localize( - "addon.dashboard.not_available_version", - { - core_version_installed: this.supervisor.core.version, - core_version_needed: addonStoreInfo.homeassistant, - } - ); - } - } - } - - private async _loadSupervisorData() { - try { - const supervisor = await fetchHassioSupervisorInfo(this.hass); - fireEvent(this, "supervisor-update", { supervisor }); - } catch (err) { - showAlertDialog(this, { - title: this._updateType, - text: extractApiErrorMessage(err), - }); - } - } - - private async _loadCoreData() { - try { - const core = await fetchHassioHomeAssistantInfo(this.hass); - fireEvent(this, "supervisor-update", { core }); - } catch (err) { - showAlertDialog(this, { - title: this._updateType, - text: extractApiErrorMessage(err), - }); - } - } - - private async _loadOsData() { - try { - const os = await fetchHassioHassOsInfo(this.hass); - fireEvent(this, "supervisor-update", { os }); - } catch (err) { - showAlertDialog(this, { - title: this._updateType, - text: extractApiErrorMessage(err), - }); - } - } - - private async _update() { - if (this._shouldCreateBackup && this.supervisor.info.state === "freeze") { - this._error = this.supervisor.localize("backup.backup_already_running"); - return; - } - this._error = undefined; - this._updating = true; - - try { - if (this._updateType === "addon") { - await updateHassioAddon( - this.hass, - this.addonSlug!, - this._shouldCreateBackup - ); - } else if (this._updateType === "core") { - await updateCore(this.hass, this._shouldCreateBackup); - } else if (this._updateType === "os") { - await updateOS(this.hass); - } else if (this._updateType === "supervisor") { - await updateSupervisor(this.hass); - } - } catch (err: any) { - if (this.hass.connection.connected && !ignoreSupervisorError(err)) { - this._error = extractApiErrorMessage(err); - this._updating = false; - return; - } - } - fireEvent(this, "update-complete"); - this._updating = false; - } - - static get styles(): CSSResultGroup { - return [ - haStyle, - css` - :host { - display: block; - } - ha-card { - margin: auto; - } - a { - text-decoration: none; - color: var(--primary-text-color); - } - .card-actions { - display: flex; - justify-content: space-between; - } - - ha-spinner { - display: block; - margin: 32px; - text-align: center; - } - - .progress-text { - text-align: center; - } - - ha-markdown { - padding-bottom: 8px; - } - - hr { - border-color: var(--divider-color); - border-bottom: none; - margin: 16px 0 0 0; - } - - ha-md-list { - padding: 0; - margin-bottom: -16px; - } - - ha-md-list-item { - --md-list-item-leading-space: 0; - --md-list-item-trailing-space: 0; - --md-item-overflow: visible; - } - `, - ]; - } -} - -declare global { - interface HTMLElementTagNameMap { - "update-available-card": UpdateAvailableCard; - } -} diff --git a/hassio/src/update-available/update-available-dashboard.ts b/hassio/src/update-available/update-available-dashboard.ts deleted file mode 100644 index d53820dd4d..0000000000 --- a/hassio/src/update-available/update-available-dashboard.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { TemplateResult } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators"; -import type { Supervisor } from "../../../src/data/supervisor/supervisor"; -import { goBack } from "../../../src/common/navigate"; -import "../../../src/layouts/hass-subpage"; -import type { HomeAssistant, Route } from "../../../src/types"; -import "./update-available-card"; - -@customElement("update-available-dashboard") -class UpdateAvailableDashboard extends LitElement { - @property({ attribute: false }) public hass!: HomeAssistant; - - @property({ attribute: false }) public supervisor!: Supervisor; - - @property({ type: Boolean }) public narrow = false; - - @property({ attribute: false }) public route!: Route; - - protected render(): TemplateResult { - return html` - - - - `; - } - - private _updateComplete() { - goBack(); - } - - static styles = css` - hass-subpage { - --app-header-background-color: var(--primary-background-color); - --app-header-text-color: var(--sidebar-text-color); - } - update-available-card { - margin: auto; - margin-top: 16px; - margin-bottom: 24px; - max-width: 600px; - } - `; -} - -declare global { - interface HTMLElementTagNameMap { - "update-available-dashboard": UpdateAvailableDashboard; - } -} diff --git a/landing-page/src/ha-landing-page.ts b/landing-page/src/ha-landing-page.ts index b1d64b02e5..d7a6434e16 100644 --- a/landing-page/src/ha-landing-page.ts +++ b/landing-page/src/ha-landing-page.ts @@ -100,7 +100,6 @@ class HaLandingPage extends LandingPageBaseElement { button-style native-name @value-changed=${this._languageChanged} - inline-arrow > /dev/null; then + npm install -g corepack + yes | yarn + fi fi if ! command -v yarn &> /dev/null; then diff --git a/script/core b/script/core index c9848c4625..0f83afbdf1 100755 --- a/script/core +++ b/script/core @@ -53,23 +53,9 @@ logger: default: info logs: homeassistant.components.frontend: debug -" >> "${WD}/config/configuration.yaml" - if [ -n "${HASSIO}" ]; then - echo " -# frontend: -# development_repo: ${WD} - -hassio: - development_repo: ${WD}" >> "${WD}/config/configuration.yaml" - else - echo " frontend: - development_repo: ${WD} - -# hassio: -# development_repo: ${WD}" >> "${WD}/config/configuration.yaml" - fi + development_repo: ${WD}" >> "${WD}/config/configuration.yaml" >> "${WD}/config/configuration.yaml" if [ -n "${CODESPACES}" ]; then echo " diff --git a/src/auth/ha-authorize.ts b/src/auth/ha-authorize.ts index 294c0201bd..6a5fd3fd50 100644 --- a/src/auth/ha-authorize.ts +++ b/src/auth/ha-authorize.ts @@ -194,7 +194,6 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) { button-style native-name @value-changed=${this._languageChanged} - inline-arrow > + pages.filter((page) => { + if (page.path === "#external-app-configuration") { + return hass.auth.external?.config.hasSettingsScreen; + } + // Only show Bluetooth page if there are Bluetooth config entries + if (page.component === "bluetooth") { + return options.hasBluetoothConfigEntries ?? false; + } + return canShowPage(hass, page); + }); diff --git a/src/common/const.ts b/src/common/const.ts index 061adf875d..243bb61ec1 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -116,3 +116,6 @@ export const UNIT_F = "°F"; /** Entity ID of the default view. */ export const DEFAULT_VIEW_ENTITY_ID = "group.default_view"; + +/** String to visually separate labels on UI */ +export const STRINGS_SEPARATOR_DOT = " · "; diff --git a/src/common/entity/compute_attribute_display.ts b/src/common/entity/compute_attribute_display.ts index 951b25b960..ba7b05fb40 100644 --- a/src/common/entity/compute_attribute_display.ts +++ b/src/common/entity/compute_attribute_display.ts @@ -8,7 +8,7 @@ import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_regist import type { FrontendLocaleData } from "../../data/translation"; import type { WeatherEntity } from "../../data/weather"; import { getWeatherUnit } from "../../data/weather"; -import type { HomeAssistant } from "../../types"; +import type { HomeAssistant, ValuePart } from "../../types"; import checkValidDate from "../datetime/check_valid_date"; import { formatDate } from "../datetime/format_date"; import { formatDateTimeWithSeconds } from "../datetime/format_date_time"; @@ -30,12 +30,34 @@ export const computeAttributeValueDisplay = ( attribute: string, value?: any ): string => { + // Number value, return formatted number + const parts = computeAttributeValueToParts( + localize, + stateObj, + locale, + config, + entities, + attribute, + value + ); + return parts.map((p) => p.value).join(""); +}; + +export const computeAttributeValueToParts = ( + localize: LocalizeFunc, + stateObj: HassEntity, + locale: FrontendLocaleData, + config: HassConfig, + entities: HomeAssistant["entities"], + attribute: string, + value?: any +): ValuePart[] => { const attributeValue = value !== undefined ? value : stateObj.attributes[attribute]; // Null value, the state is unknown if (attributeValue === null || attributeValue === undefined) { - return localize("state.default.unknown"); + return [{ type: "value", value: localize("state.default.unknown") }]; } // Number value, return formatted number @@ -58,11 +80,16 @@ export const computeAttributeValueDisplay = ( unit = config.unit_system.temperature; } - if (unit) { - return `${formattedValue}${blankBeforeUnit(unit, locale)}${unit}`; - } + const parts: ValuePart[] = [{ type: "value", value: formattedValue }]; - return formattedValue; + if (unit) { + const literal = blankBeforeUnit(unit, locale); + if (literal) { + parts.push({ type: "literal", value: literal }); + } + parts.push({ type: "unit", value: unit }); + } + return parts; } // Special handling in case this is a string with a known format @@ -73,14 +100,15 @@ export const computeAttributeValueDisplay = ( if (isTimestamp(attributeValue)) { const date = new Date(attributeValue); if (checkValidDate(date)) { - return formatDateTimeWithSeconds(date, locale, config); + const formattedDate = formatDateTimeWithSeconds(date, locale, config); + return [{ type: "value", value: formattedDate }]; } } // Value was not a timestamp, so only do date formatting const date = new Date(attributeValue); if (checkValidDate(date)) { - return formatDate(date, locale, config); + return [{ type: "value", value: formatDate(date, locale, config) }]; } } } @@ -91,11 +119,11 @@ export const computeAttributeValueDisplay = ( attributeValue.some((val) => val instanceof Object)) || (!Array.isArray(attributeValue) && attributeValue instanceof Object) ) { - return JSON.stringify(attributeValue); + return [{ type: "value", value: JSON.stringify(attributeValue) }]; } // If this is an array, try to determine the display value for each item if (Array.isArray(attributeValue)) { - return attributeValue + const formattedValue = attributeValue .map((item) => computeAttributeValueDisplay( localize, @@ -108,6 +136,7 @@ export const computeAttributeValueDisplay = ( ) ) .join(", "); + return [{ type: "value", value: formattedValue }]; } // We've explored all known value handling, so now we'll try to find a @@ -120,7 +149,7 @@ export const computeAttributeValueDisplay = ( | undefined; const translationKey = registryEntry?.translation_key; - return ( + const formattedValue = (translationKey && localize( `component.${registryEntry.platform}.entity.${domain}.${translationKey}.state_attributes.${attribute}.state.${attributeValue}` @@ -132,8 +161,8 @@ export const computeAttributeValueDisplay = ( localize( `component.${domain}.entity_component._.state_attributes.${attribute}.state.${attributeValue}` ) || - attributeValue - ); + attributeValue; + return [{ type: "value", value: formattedValue }]; }; export const computeAttributeNameDisplay = ( diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index 7574d1c6ab..16ab18ff4a 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -3,13 +3,14 @@ import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity"; import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry"; import type { FrontendLocaleData } from "../../data/translation"; import { TimeZone } from "../../data/translation"; -import type { HomeAssistant } from "../../types"; +import type { HomeAssistant, ValuePart } from "../../types"; import { formatDate } from "../datetime/format_date"; import { formatDateTime } from "../datetime/format_date_time"; import { DURATION_UNITS, formatDuration } from "../datetime/format_duration"; import { formatTime } from "../datetime/format_time"; import { formatNumber, + formatNumberToParts, getNumberFormatOptions, isNumericFromAttributes, } from "../number/format_number"; @@ -51,8 +52,36 @@ export const computeStateDisplayFromEntityAttributes = ( attributes: any, state: string ): string => { + const parts = computeStateToPartsFromEntityAttributes( + localize, + locale, + sensorNumericDeviceClasses, + config, + entity, + entityId, + attributes, + state + ); + return parts.map((part) => part.value).join(""); +}; + +const computeStateToPartsFromEntityAttributes = ( + localize: LocalizeFunc, + locale: FrontendLocaleData, + sensorNumericDeviceClasses: string[], + config: HassConfig, + entity: EntityRegistryDisplayEntry | undefined, + entityId: string, + attributes: any, + state: string +): ValuePart[] => { if (state === UNKNOWN || state === UNAVAILABLE) { - return localize(`state.default.${state}`); + return [ + { + type: "value", + value: localize(`state.default.${state}`), + }, + ]; } const domain = computeDomain(entityId); @@ -73,19 +102,27 @@ export const computeStateDisplayFromEntityAttributes = ( DURATION_UNITS.includes(attributes.unit_of_measurement) ) { try { - return formatDuration( - locale, - state, - attributes.unit_of_measurement, - entity?.display_precision - ); + return [ + { + type: "value", + value: formatDuration( + locale, + state, + attributes.unit_of_measurement, + entity?.display_precision + ), + }, + ]; } catch (_err) { // fallback to default } } + + // state is monetary if (attributes.device_class === "monetary") { + let parts: Record[] = []; try { - return formatNumber(state, locale, { + parts = formatNumberToParts(state, locale, { style: "currency", currency: attributes.unit_of_measurement, minimumFractionDigits: 2, @@ -98,8 +135,34 @@ export const computeStateDisplayFromEntityAttributes = ( } catch (_err) { // fallback to default } + + const TYPE_MAP: Record = { + integer: "value", + group: "value", + decimal: "value", + fraction: "value", + literal: "literal", + currency: "unit", + }; + + const valueParts: ValuePart[] = []; + + for (const part of parts) { + const type = TYPE_MAP[part.type]; + if (!type) continue; + const last = valueParts[valueParts.length - 1]; + // Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56") + if (type === "value" && last?.type === "value") { + last.value += part.value; + } else { + valueParts.push({ type, value: part.value }); + } + } + + return valueParts; } + // default processing of numeric values const value = formatNumber( state, locale, @@ -114,10 +177,14 @@ export const computeStateDisplayFromEntityAttributes = ( attributes.unit_of_measurement; if (unit) { - return `${value}${blankBeforeUnit(unit, locale)}${unit}`; + return [ + { type: "value", value: value }, + { type: "literal", value: blankBeforeUnit(unit, locale) }, + { type: "unit", value: unit }, + ]; } - return value; + return [{ type: "value", value: value }]; } if (["date", "input_datetime", "time"].includes(domain)) { @@ -129,36 +196,51 @@ export const computeStateDisplayFromEntityAttributes = ( const components = state.split(" "); if (components.length === 2) { // Date and time. - return formatDateTime( - new Date(components.join("T")), - { ...locale, time_zone: TimeZone.local }, - config - ); + return [ + { + type: "value", + value: formatDateTime( + new Date(components.join("T")), + { ...locale, time_zone: TimeZone.local }, + config + ), + }, + ]; } if (components.length === 1) { if (state.includes("-")) { // Date only. - return formatDate( - new Date(`${state}T00:00`), - { ...locale, time_zone: TimeZone.local }, - config - ); + return [ + { + type: "value", + value: formatDate( + new Date(`${state}T00:00`), + { ...locale, time_zone: TimeZone.local }, + config + ), + }, + ]; } if (state.includes(":")) { // Time only. const now = new Date(); - return formatTime( - new Date(`${now.toISOString().split("T")[0]}T${state}`), - { ...locale, time_zone: TimeZone.local }, - config - ); + return [ + { + type: "value", + value: formatTime( + new Date(`${now.toISOString().split("T")[0]}T${state}`), + { ...locale, time_zone: TimeZone.local }, + config + ), + }, + ]; } } - return state; + return [{ type: "value", value: state }]; } catch (_e) { // Formatting methods may throw error if date parsing doesn't go well, // just return the state string in that case. - return state; + return [{ type: "value", value: state }]; } } @@ -182,25 +264,58 @@ export const computeStateDisplayFromEntityAttributes = ( (domain === "sensor" && attributes.device_class === "timestamp") ) { try { - return formatDateTime(new Date(state), locale, config); + return [ + { + type: "value", + value: formatDateTime(new Date(state), locale, config), + }, + ]; } catch (_err) { - return state; + return [{ type: "value", value: state }]; } } - return ( - (entity?.translation_key && - localize( - `component.${entity.platform}.entity.${domain}.${entity.translation_key}.state.${state}` - )) || - // Return device class translation - (attributes.device_class && - localize( - `component.${domain}.entity_component.${attributes.device_class}.state.${state}` - )) || - // Return default translation - localize(`component.${domain}.entity_component._.state.${state}`) || - // We don't know! Return the raw state. - state + return [ + { + type: "value", + value: + (entity?.translation_key && + localize( + `component.${entity.platform}.entity.${domain}.${entity.translation_key}.state.${state}` + )) || + // Return device class translation + (attributes.device_class && + localize( + `component.${domain}.entity_component.${attributes.device_class}.state.${state}` + )) || + // Return default translation + localize(`component.${domain}.entity_component._.state.${state}`) || + // We don't know! Return the raw state. + state, + }, + ]; +}; + +export const computeStateToParts = ( + localize: LocalizeFunc, + stateObj: HassEntity, + locale: FrontendLocaleData, + sensorNumericDeviceClasses: string[], + config: HassConfig, + entities: HomeAssistant["entities"], + state?: string +): ValuePart[] => { + const entity = entities?.[stateObj.entity_id] as + | EntityRegistryDisplayEntry + | undefined; + return computeStateToPartsFromEntityAttributes( + localize, + locale, + sensorNumericDeviceClasses, + config, + entity, + stateObj.entity_id, + stateObj.attributes, + state !== undefined ? state : stateObj.state ); }; diff --git a/src/common/entity/group_entities.ts b/src/common/entity/group_entities.ts index 1b22b42cc8..76560e9b19 100644 --- a/src/common/entity/group_entities.ts +++ b/src/common/entity/group_entities.ts @@ -8,7 +8,9 @@ export const computeGroupEntitiesState = (states: HassEntity[]): string => { return UNAVAILABLE; } - const validState = states.filter((stateObj) => isUnavailableState(stateObj)); + const validState = states.some( + (stateObj) => !isUnavailableState(stateObj.state) + ); if (!validState) { return UNAVAILABLE; diff --git a/src/common/keyboard/shortcuts.ts b/src/common/keyboard/shortcuts.ts index 80edc335e6..b67544e282 100644 --- a/src/common/keyboard/shortcuts.ts +++ b/src/common/keyboard/shortcuts.ts @@ -16,6 +16,7 @@ export interface ShortcutConfig { * Default is false to avoid interrupting copy/paste. */ allowWhenTextSelected?: boolean; + allowInInput?: boolean; } /** @@ -29,7 +30,10 @@ function registerShortcuts( Object.entries(shortcuts).forEach(([key, config]) => { wrappedShortcuts[key] = (event: KeyboardEvent) => { - if (!canOverrideAlphanumericInput(event.composedPath())) { + if ( + !config.allowInInput && + !canOverrideAlphanumericInput(event.composedPath()) + ) { return; } if (!config.allowWhenTextSelected && window.getSelection()?.toString()) { diff --git a/src/common/number/format_number.ts b/src/common/number/format_number.ts index 0d97bc6e48..b5494c701f 100644 --- a/src/common/number/format_number.ts +++ b/src/common/number/format_number.ts @@ -5,7 +5,6 @@ import type { import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry"; import type { FrontendLocaleData } from "../../data/translation"; import { NumberFormat } from "../../data/translation"; -import { round } from "./round"; /** * Returns true if the entity is considered numeric based on the attributes it has @@ -52,7 +51,22 @@ export const formatNumber = ( num: string | number, localeOptions?: FrontendLocaleData, options?: Intl.NumberFormatOptions -): string => { +): string => + formatNumberToParts(num, localeOptions, options) + .map((part) => part.value) + .join(""); + +/** + * Returns an array of objects containing the formatted number in parts + * Similar to Intl.NumberFormat.prototype.formatToParts() + * + * Input params - same as for formatNumber() + */ +export const formatNumberToParts = ( + num: string | number, + localeOptions?: FrontendLocaleData, + options?: Intl.NumberFormatOptions +): any[] => { const locale = localeOptions ? numberFormatToLocale(localeOptions) : undefined; @@ -71,7 +85,7 @@ export const formatNumber = ( return new Intl.NumberFormat( locale, getDefaultFormatOptions(num, options) - ).format(Number(num)); + ).formatToParts(Number(num)); } if ( @@ -86,15 +100,10 @@ export const formatNumber = ( ...options, useGrouping: false, }) - ).format(Number(num)); + ).formatToParts(Number(num)); } - if (typeof num === "string") { - return num; - } - return `${round(num, options?.maximumFractionDigits).toString()}${ - options?.style === "currency" ? ` ${options.currency}` : "" - }`; + return [{ type: "literal", value: num }]; }; /** diff --git a/src/common/number/normalize-by-si-prefix.ts b/src/common/number/normalize-by-si-prefix.ts new file mode 100644 index 0000000000..ccac81b952 --- /dev/null +++ b/src/common/number/normalize-by-si-prefix.ts @@ -0,0 +1,28 @@ +const SI_PREFIX_MULTIPLIERS: Record = { + T: 1e12, + G: 1e9, + M: 1e6, + k: 1e3, + m: 1e-3, + "\u00B5": 1e-6, // µ (micro sign) + "\u03BC": 1e-6, // μ (greek small letter mu) +}; + +/** + * Normalize a numeric value by detecting SI unit prefixes (T, G, M, k, m, µ). + * Only applies when the unit is longer than 1 character and starts with a + * recognized prefix, avoiding false positives on standalone units like "m" (meters). + */ +export const normalizeValueBySIPrefix = ( + value: number, + unit: string | undefined +): number => { + if (!unit || unit.length <= 1) { + return value; + } + const prefix = unit[0]; + if (prefix in SI_PREFIX_MULTIPLIERS) { + return value * SI_PREFIX_MULTIPLIERS[prefix]; + } + return value; +}; diff --git a/src/common/translations/entity-state.ts b/src/common/translations/entity-state.ts index 3a6ccfcbe8..cd511e451f 100644 --- a/src/common/translations/entity-state.ts +++ b/src/common/translations/entity-state.ts @@ -1,6 +1,6 @@ import type { HassConfig, HassEntity } from "home-assistant-js-websocket"; import type { FrontendLocaleData } from "../../data/translation"; -import type { HomeAssistant } from "../../types"; +import type { HomeAssistant, ValuePart } from "../../types"; import { computeEntityNameDisplay, type EntityNameItem, @@ -12,11 +12,20 @@ export type FormatEntityStateFunc = ( stateObj: HassEntity, state?: string ) => string; +export type FormatEntityStateToPartsFunc = ( + stateObj: HassEntity, + state?: string +) => ValuePart[]; export type FormatEntityAttributeValueFunc = ( stateObj: HassEntity, attribute: string, value?: any ) => string; +export type FormatEntityAttributeValueToPartsFunc = ( + stateObj: HassEntity, + attribute: string, + value?: any +) => ValuePart[]; export type FormatEntityAttributeNameFunc = ( stateObj: HassEntity, attribute: string @@ -41,14 +50,19 @@ export const computeFormatFunctions = async ( sensorNumericDeviceClasses: string[] ): Promise<{ formatEntityState: FormatEntityStateFunc; + formatEntityStateToParts: FormatEntityStateToPartsFunc; formatEntityAttributeValue: FormatEntityAttributeValueFunc; + formatEntityAttributeValueToParts: FormatEntityAttributeValueToPartsFunc; formatEntityAttributeName: FormatEntityAttributeNameFunc; formatEntityName: FormatEntityNameFunc; }> => { - const { computeStateDisplay } = + const { computeStateDisplay, computeStateToParts } = await import("../entity/compute_state_display"); - const { computeAttributeValueDisplay, computeAttributeNameDisplay } = - await import("../entity/compute_attribute_display"); + const { + computeAttributeValueDisplay, + computeAttributeValueToParts, + computeAttributeNameDisplay, + } = await import("../entity/compute_attribute_display"); return { formatEntityState: (stateObj, state) => @@ -61,6 +75,16 @@ export const computeFormatFunctions = async ( entities, state ), + formatEntityStateToParts: (stateObj, state) => + computeStateToParts( + localize, + stateObj, + locale, + sensorNumericDeviceClasses, + config, + entities, + state + ), formatEntityAttributeValue: (stateObj, attribute, value) => computeAttributeValueDisplay( localize, @@ -71,6 +95,16 @@ export const computeFormatFunctions = async ( attribute, value ), + formatEntityAttributeValueToParts: (stateObj, attribute, value) => + computeAttributeValueToParts( + localize, + stateObj, + locale, + config, + entities, + attribute, + value + ), formatEntityAttributeName: (stateObj, attribute) => computeAttributeNameDisplay(localize, stateObj, entities, attribute), formatEntityName: (stateObj, name, options) => diff --git a/src/common/url/route.ts b/src/common/url/route.ts new file mode 100644 index 0000000000..ddc7fb858e --- /dev/null +++ b/src/common/url/route.ts @@ -0,0 +1,14 @@ +import type { Route } from "../../types"; + +export const computeRouteTail = (route: Route) => { + const dividerPos = route.path.indexOf("/", 1); + return dividerPos === -1 + ? { + prefix: route.prefix + route.path, + path: "", + } + : { + prefix: route.prefix + route.path.substring(0, dividerPos), + path: route.path.substring(dividerPos), + }; +}; diff --git a/src/common/util/compute_rtl.ts b/src/common/util/compute_rtl.ts index b5bebfbc7d..723878e7ed 100644 --- a/src/common/util/compute_rtl.ts +++ b/src/common/util/compute_rtl.ts @@ -34,10 +34,6 @@ export function setDirectionStyles(direction: string, element: LitElement) { "--float-end", direction === "ltr" ? "right" : "left" ); - element.style.setProperty( - "--margin-title", - direction === "ltr" ? "var(--margin-title-ltr)" : "var(--margin-title-rtl)" - ); element.style.setProperty( "--scale-direction", direction === "ltr" ? "1" : "-1" diff --git a/src/common/util/media-progress.ts b/src/common/util/media-progress.ts new file mode 100644 index 0000000000..c44428c0b8 --- /dev/null +++ b/src/common/util/media-progress.ts @@ -0,0 +1,19 @@ +export const startMediaProgressInterval = ( + interval: number | undefined, + callback: () => void, + intervalMs = 1000 +): number => { + if (interval) { + return interval; + } + return window.setInterval(callback, intervalMs); +}; + +export const stopMediaProgressInterval = ( + interval: number | undefined +): number | undefined => { + if (interval) { + clearInterval(interval); + } + return undefined; +}; diff --git a/src/common/util/view-transition.ts b/src/common/util/view-transition.ts index a53ab4715c..235e8cf374 100644 --- a/src/common/util/view-transition.ts +++ b/src/common/util/view-transition.ts @@ -1,3 +1,15 @@ +let isViewTransitionDisabled = false; +try { + isViewTransitionDisabled = + window.localStorage.getItem("disableViewTransition") === "true"; +} catch { + // ignore +} + +export const setViewTransitionDisabled = (disabled: boolean): void => { + isViewTransitionDisabled = disabled; +}; + /** * Executes a synchronous callback within a View Transition if supported, otherwise runs it directly. * @@ -14,7 +26,7 @@ export const withViewTransition = ( callback: (viewTransitionAvailable: boolean) => void ): Promise => { - if (!document.startViewTransition) { + if (!document.startViewTransition || isViewTransitionDisabled) { callback(false); return Promise.resolve(); } diff --git a/src/common/util/volume-slider.ts b/src/common/util/volume-slider.ts new file mode 100644 index 0000000000..84921cbd9c --- /dev/null +++ b/src/common/util/volume-slider.ts @@ -0,0 +1,186 @@ +import type { HaSlider } from "../../components/ha-slider"; + +interface VolumeSliderControllerOptions { + getSlider: () => HaSlider | undefined; + step: number; + onSetVolume: (value: number) => void; + onSetVolumeDebounced?: (value: number) => void; + onValueUpdated?: (value: number) => void; +} + +export class VolumeSliderController { + private _touchStartX = 0; + + private _touchStartY = 0; + + private _touchStartValue = 0; + + private _touchDragging = false; + + private _touchScrolling = false; + + private _dragging = false; + + private _lastValue = 0; + + private _options: VolumeSliderControllerOptions; + + constructor(options: VolumeSliderControllerOptions) { + this._options = options; + } + + public get isInteracting(): boolean { + return this._touchDragging || this._dragging; + } + + public setStep(step: number): void { + this._options.step = step; + } + + public handleInput = (ev: Event): void => { + ev.stopPropagation(); + const value = Number((ev.target as HaSlider).value); + this._dragging = true; + this._updateValue(value); + this._options.onSetVolumeDebounced?.(value); + }; + + public handleChange = (ev: Event): void => { + ev.stopPropagation(); + const value = Number((ev.target as HaSlider).value); + this._dragging = false; + this._updateValue(value); + this._options.onSetVolume(value); + }; + + public handleTouchStart = (ev: TouchEvent): void => { + ev.stopPropagation(); + const touch = ev.touches[0]; + this._touchStartX = touch.clientX; + this._touchStartY = touch.clientY; + this._touchStartValue = this._getSliderValue(); + this._touchDragging = false; + this._touchScrolling = false; + this._showTooltip(); + }; + + public handleTouchMove = (ev: TouchEvent): void => { + if (this._touchScrolling) { + return; + } + const touch = ev.touches[0]; + const deltaX = touch.clientX - this._touchStartX; + const deltaY = touch.clientY - this._touchStartY; + const absDeltaX = Math.abs(deltaX); + const absDeltaY = Math.abs(deltaY); + + if (!this._touchDragging) { + if (absDeltaY > 10 && absDeltaY > absDeltaX * 2) { + this._touchScrolling = true; + return; + } + if (absDeltaX > 8) { + this._touchDragging = true; + } + } + + if (this._touchDragging) { + ev.preventDefault(); + const newValue = this._getVolumeFromTouch(touch.clientX); + this._updateValue(newValue); + } + }; + + public handleTouchEnd = (ev: TouchEvent): void => { + if (this._touchScrolling) { + this._touchScrolling = false; + this._hideTooltip(); + return; + } + + const touch = ev.changedTouches[0]; + if (!this._touchDragging) { + const tapValue = this._getVolumeFromTouch(touch.clientX); + const delta = + tapValue > this._touchStartValue + ? this._options.step + : -this._options.step; + const newValue = this._roundVolumeValue(this._touchStartValue + delta); + this._updateValue(newValue); + this._options.onSetVolume(newValue); + } else { + const finalValue = this._getVolumeFromTouch(touch.clientX); + this._updateValue(finalValue); + this._options.onSetVolume(finalValue); + } + + this._touchDragging = false; + this._dragging = false; + this._hideTooltip(); + }; + + public handleTouchCancel = (): void => { + this._touchDragging = false; + this._touchScrolling = false; + this._dragging = false; + this._updateValue(this._touchStartValue); + this._hideTooltip(); + }; + + public handleWheel = (ev: WheelEvent): void => { + ev.preventDefault(); + ev.stopPropagation(); + const direction = ev.deltaY > 0 ? -1 : 1; + const currentValue = this._getSliderValue(); + const newValue = this._roundVolumeValue( + currentValue + direction * this._options.step + ); + this._updateValue(newValue); + this._options.onSetVolume(newValue); + }; + + private _getVolumeFromTouch(clientX: number): number { + const slider = this._options.getSlider(); + if (!slider) { + return 0; + } + const rect = slider.getBoundingClientRect(); + const x = Math.min(Math.max(clientX - rect.left, 0), rect.width); + const percentage = (x / rect.width) * 100; + return this._roundVolumeValue(percentage); + } + + private _roundVolumeValue(value: number): number { + return Math.min( + Math.max(Math.round(value / this._options.step) * this._options.step, 0), + 100 + ); + } + + private _getSliderValue(): number { + const slider = this._options.getSlider(); + if (slider) { + return Number(slider.value); + } + return this._lastValue; + } + + private _updateValue(value: number): void { + this._lastValue = value; + this._options.onValueUpdated?.(value); + const slider = this._options.getSlider(); + if (slider) { + slider.value = value; + } + } + + private _showTooltip(): void { + const slider = this._options.getSlider() as any; + slider?.showTooltip?.(); + } + + private _hideTooltip(): void { + const slider = this._options.getSlider() as any; + slider?.hideTooltip?.(); + } +} diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 555ed1ad23..6eaf002077 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -19,6 +19,7 @@ import { styleMap } from "lit/directives/style-map"; import { ensureArray } from "../../common/array/ensure-array"; import { getAllGraphColors } from "../../common/color/colors"; import { fireEvent } from "../../common/dom/fire_event"; +import type { HASSDomEvent } from "../../common/dom/fire_event"; import { listenMediaQuery } from "../../common/dom/media_query"; import { themesContext } from "../../data/context"; import type { Themes } from "../../data/ws-themes"; @@ -27,6 +28,7 @@ import type { HomeAssistant } from "../../types"; import { isMac } from "../../util/is_mac"; import "../chips/ha-assist-chip"; import "../ha-icon-button"; +import { afterNextRender } from "../../common/util/render-status"; import { filterXSS } from "../../common/util/xss"; import { formatTimeLabel } from "./axis-label"; import { downSampleLineData } from "./down-sample"; @@ -92,10 +94,18 @@ export class HaChartBase extends LitElement { private _resizeAnimationDuration?: number; + private _suspendResize = false; + + private _layoutTransitionActive = false; + // @ts-ignore private _resizeController = new ResizeController(this, { callback: () => { if (this.chart) { + if (this._suspendResize) { + this._shouldResizeChart = true; + return; + } if (!this.chart.getZr().animation.isFinished()) { this._shouldResizeChart = true; } else { @@ -113,8 +123,11 @@ export class HaChartBase extends LitElement { private _originalZrFlush?: () => void; + private _pendingSetup = false; + public disconnectedCallback() { super.disconnectedCallback(); + this._pendingSetup = false; while (this._listeners.length) { this._listeners.pop()!(); } @@ -126,7 +139,13 @@ export class HaChartBase extends LitElement { public connectedCallback() { super.connectedCallback(); if (this.hasUpdated) { - this._setupChart(); + this._pendingSetup = true; + afterNextRender(() => { + if (this.isConnected && this._pendingSetup) { + this._pendingSetup = false; + this._setupChart(); + } + }); } this._listeners.push( @@ -181,6 +200,26 @@ export class HaChartBase extends LitElement { () => window.removeEventListener("keyup", handleKeyUp) ); } + + const handleLayoutTransition: EventListener = (ev) => { + const event = ev as HASSDomEvent; + this._layoutTransitionActive = Boolean(event.detail?.active); + this.toggleAttribute( + "layout-transition-active", + this._layoutTransitionActive + ); + this._suspendResize = this._layoutTransitionActive; + if (!this._suspendResize) { + this._resizeChartIfNeeded(); + } + }; + window.addEventListener("hass-layout-transition", handleLayoutTransition); + this._listeners.push(() => + window.removeEventListener( + "hass-layout-transition", + handleLayoutTransition + ) + ); } protected firstUpdated() { @@ -988,19 +1027,29 @@ export class HaChartBase extends LitElement { } private _handleChartRenderFinished = () => { - if (this._shouldResizeChart) { - this.chart?.resize({ - animation: - this._reducedMotion || - typeof this._resizeAnimationDuration !== "number" - ? undefined - : { duration: this._resizeAnimationDuration }, - }); - this._shouldResizeChart = false; - this._resizeAnimationDuration = undefined; - } + this._resizeChartIfNeeded(); }; + private _resizeChartIfNeeded() { + if (!this.chart || !this._shouldResizeChart) { + return; + } + if (this._suspendResize) { + return; + } + if (!this.chart.getZr().animation.isFinished()) { + return; + } + this.chart.resize({ + animation: + this._reducedMotion || typeof this._resizeAnimationDuration !== "number" + ? undefined + : { duration: this._resizeAnimationDuration }, + }); + this._shouldResizeChart = false; + this._resizeAnimationDuration = undefined; + } + private _compareCustomLegendOptions( oldOptions: ECOption | undefined, newOptions: ECOption | undefined @@ -1022,11 +1071,18 @@ export class HaChartBase extends LitElement { display: block; position: relative; letter-spacing: normal; + overflow: visible; + } + :host([layout-transition-active]), + :host([layout-transition-active]) .container, + :host([layout-transition-active]) .chart-container { + overflow: hidden; } .container { display: flex; flex-direction: column; position: relative; + overflow: visible; } .container.has-height { max-height: var(--chart-max-height, 350px); @@ -1034,6 +1090,7 @@ export class HaChartBase extends LitElement { .chart-container { width: 100%; max-height: var(--chart-max-height, 350px); + overflow: visible; } .has-height .chart-container { flex: 1; diff --git a/src/components/chart/ha-sankey-chart.ts b/src/components/chart/ha-sankey-chart.ts index e9f9e79712..a9fe6ccee7 100644 --- a/src/components/chart/ha-sankey-chart.ts +++ b/src/components/chart/ha-sankey-chart.ts @@ -58,7 +58,7 @@ export class HaSankeyChart extends LitElement { @property({ type: Boolean }) public vertical = false; - @property({ type: String, attribute: false }) public valueFormatter?: ( + @property({ attribute: false }) public valueFormatter?: ( value: number ) => string; diff --git a/src/components/chart/ha-sunburst-chart.ts b/src/components/chart/ha-sunburst-chart.ts index bfb35da27a..921048abb9 100644 --- a/src/components/chart/ha-sunburst-chart.ts +++ b/src/components/chart/ha-sunburst-chart.ts @@ -29,7 +29,7 @@ export class HaSunburstChart extends LitElement { @property({ attribute: false }) public data?: SunburstNode; - @property({ type: String, attribute: false }) public valueFormatter?: ( + @property({ attribute: false }) public valueFormatter?: ( value: number ) => string; diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index fffcf372ba..96fa3807ce 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -50,16 +50,16 @@ export class StateHistoryChartLine extends LitElement { @property({ attribute: false }) public endTime!: Date; - @property({ attribute: false, type: Number }) public paddingYAxis = 0; + @property({ attribute: false }) public paddingYAxis = 0; - @property({ attribute: false, type: Number }) public chartIndex?; + @property({ attribute: false }) public chartIndex?; @property({ attribute: "logarithmic-scale", type: Boolean }) public logarithmicScale = false; - @property({ attribute: false, type: Number }) public minYAxis?: number; + @property({ attribute: false }) public minYAxis?: number; - @property({ attribute: false, type: Number }) public maxYAxis?: number; + @property({ attribute: false }) public maxYAxis?: number; @property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false; @@ -716,6 +716,18 @@ export class StateHistoryChartLine extends LitElement { // Add an entry for final values pushData(endTime, prevValues); + // For sensors, append current state if viewing recent data + const now = new Date(); + // allow 1s of leeway for "now" + const isUpToNow = now.getTime() - endTime.getTime() <= 1000; + if (domain === "sensor" && isUpToNow && data.length === 1) { + const stateObj = this.hass.states[states.entity_id]; + const currentValue = stateObj ? safeParseFloat(stateObj.state) : null; + if (currentValue !== null) { + data[0].data!.push([now, currentValue]); + } + } + // Concat two arrays Array.prototype.push.apply(datasets, data); }); diff --git a/src/components/chart/state-history-chart-timeline.ts b/src/components/chart/state-history-chart-timeline.ts index 6287ac6eb2..325cb314cf 100644 --- a/src/components/chart/state-history-chart-timeline.ts +++ b/src/components/chart/state-history-chart-timeline.ts @@ -47,9 +47,9 @@ export class StateHistoryChartTimeline extends LitElement { @property({ attribute: false }) public endTime!: Date; - @property({ attribute: false, type: Number }) public paddingYAxis = 0; + @property({ attribute: false }) public paddingYAxis = 0; - @property({ attribute: false, type: Number }) public chartIndex?; + @property({ attribute: false }) public chartIndex?; @property({ attribute: "hide-reset-button", type: Boolean }) public hideResetButton?: boolean; diff --git a/src/components/chart/state-history-charts.ts b/src/components/chart/state-history-charts.ts index a51f52a175..58480d9519 100644 --- a/src/components/chart/state-history-charts.ts +++ b/src/components/chart/state-history-charts.ts @@ -60,7 +60,7 @@ export class StateHistoryCharts extends LitElement { @property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false; - @property({ attribute: false, type: Number }) public hoursToShow?: number; + @property({ attribute: false }) public hoursToShow?: number; @property({ attribute: "show-names", type: Boolean }) public showNames = true; @@ -73,9 +73,9 @@ export class StateHistoryCharts extends LitElement { @property({ attribute: "logarithmic-scale", type: Boolean }) public logarithmicScale = false; - @property({ attribute: false, type: Number }) public minYAxis?: number; + @property({ attribute: false }) public minYAxis?: number; - @property({ attribute: false, type: Number }) public maxYAxis?: number; + @property({ attribute: false }) public maxYAxis?: number; @property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false; diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index a536e26e78..e8903dd6c9 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -62,14 +62,14 @@ export class StatisticsChart extends LitElement { @property({ attribute: false }) public endTime?: Date; - @property({ attribute: false, type: Array }) + @property({ attribute: false }) public statTypes: StatisticType[] = ["sum", "min", "mean", "max"]; @property({ attribute: false }) public chartType: "line" | "bar" = "line"; - @property({ attribute: false, type: Number }) public minYAxis?: number; + @property({ attribute: false }) public minYAxis?: number; - @property({ attribute: false, type: Number }) public maxYAxis?: number; + @property({ attribute: false }) public maxYAxis?: number; @property({ attribute: "fit-y-data", type: Boolean }) public fitYData = false; @@ -572,6 +572,7 @@ export class StatisticsChart extends LitElement { let firstSum: number | null | undefined = null; stats.forEach((stat) => { const startDate = new Date(stat.start); + const endDate = new Date(stat.end); if (prevDate === startDate) { return; } @@ -601,10 +602,66 @@ export class StatisticsChart extends LitElement { dataValues.push(val); }); if (!this._hiddenStats.has(statistic_id)) { - pushData(startDate, new Date(stat.end), dataValues); + pushData( + startDate, + endDate.getTime() < endTime.getTime() ? endDate : endTime, + dataValues + ); } }); + // Close out the last stat segment at prevEndTime + const lastEndTime = prevEndTime; + const lastValues = prevValues; + if (lastEndTime && lastValues) { + statDataSets.forEach((d, i) => { + d.data!.push( + this._transformDataValue([lastEndTime, ...lastValues[i]!]) + ); + }); + } + + // Append current state if viewing recent data + const now = new Date(); + // allow 10m of leeway for "now", because stats are 5 minute aggregated + const isUpToNow = now.getTime() - endTime.getTime() <= 600000; + if (isUpToNow) { + // Skip external statistics (they have ":" in the ID) + if (!statistic_id.includes(":")) { + const stateObj = this.hass.states[statistic_id]; + if (stateObj) { + const currentValue = parseFloat(stateObj.state); + if ( + isFinite(currentValue) && + !this._hiddenStats.has(statistic_id) + ) { + // Then push the current state at now + statTypes.forEach((type, i) => { + const val: (number | null)[] = []; + if (type === "sum" || type === "change") { + // Skip cumulative types - need special calculation + val.push(null); + } else if ( + type === bandTop && + this.chartType === "line" && + drawBands && + !this._hiddenStats.has(`${statistic_id}-${bandBottom}`) + ) { + // For band chart, current value is both min and max, so diff is 0 + val.push(0); + val.push(currentValue); + } else { + val.push(currentValue); + } + statDataSets[i].data!.push( + this._transformDataValue([now, ...val]) + ); + }); + } + } + } + } + // Concat two arrays Array.prototype.push.apply(totalDataSets, statDataSets); Array.prototype.push.apply(legendData, statLegendData); diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index f4d1b092f9..6799a0ce9b 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -20,6 +20,7 @@ import type { LocalizeFunc } from "../../common/translations/localize"; import { debounce } from "../../common/util/debounce"; import { groupBy } from "../../common/util/group-by"; import { nextRender } from "../../common/util/render-status"; +import { STRINGS_SEPARATOR_DOT } from "../../common/const"; import { haStyleScrollbar } from "../../resources/styles"; import { loadVirtualizer } from "../../resources/virtualizer"; import type { HomeAssistant } from "../../types"; @@ -130,9 +131,9 @@ export class HaDataTable extends LitElement { // eslint-disable-next-line lit/no-native-attributes @property({ type: String }) public id = "id"; - @property({ attribute: false, type: String }) public noDataText?: string; + @property({ attribute: false }) public noDataText?: string; - @property({ attribute: false, type: String }) public searchLabel?: string; + @property({ attribute: false }) public searchLabel?: string; @property({ type: Boolean, attribute: "no-label-float" }) public noLabelFloat? = false; @@ -636,7 +637,7 @@ export class HaDataTable extends LitElement { .map( ([key2, column2], i) => html`${i !== 0 - ? " · " + ? STRINGS_SEPARATOR_DOT : nothing}${column2.template ? column2.template(row) : row[key2]}` @@ -1192,6 +1193,7 @@ export class HaDataTable extends LitElement { .mdc-data-table__cell--numeric { text-align: var(--float-end); + direction: ltr; } .mdc-data-table__cell--icon { diff --git a/src/components/device/ha-device-automation-picker.ts b/src/components/device/ha-device-automation-picker.ts index f72400f249..67819e8d8f 100644 --- a/src/components/device/ha-device-automation-picker.ts +++ b/src/components/device/ha-device-automation-picker.ts @@ -11,7 +11,7 @@ import { sortDeviceAutomations, } from "../../data/device/device_automation"; import type { EntityRegistryEntry } from "../../data/entity/entity_registry"; -import type { HomeAssistant } from "../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../types"; import "../ha-generic-picker"; import "../ha-md-select"; import "../ha-md-select-option"; @@ -192,7 +192,7 @@ export abstract class HaDeviceAutomationPicker< this._renderEmpty = false; } - private _automationChanged(ev: CustomEvent<{ value: string }>) { + private _automationChanged(ev: ValueChangedEvent) { ev.stopPropagation(); const value = ev.detail.value; if (!value || NO_AUTOMATION_KEY === value) { diff --git a/src/components/device/ha-device-picker.ts b/src/components/device/ha-device-picker.ts index 55737be97a..a68d73b165 100644 --- a/src/components/device/ha-device-picker.ts +++ b/src/components/device/ha-device-picker.ts @@ -48,7 +48,7 @@ export class HaDevicePicker extends LitElement { @property({ type: String, attribute: "search-label" }) public searchLabel?: string; - @property({ attribute: false, type: Array }) public createDomains?: string[]; + @property({ attribute: false }) public createDomains?: string[]; /** * Show only devices with entities from specific domains. diff --git a/src/components/entity/ha-entities-picker.ts b/src/components/entity/ha-entities-picker.ts index b2d3d75de0..d87d4f7df8 100644 --- a/src/components/entity/ha-entities-picker.ts +++ b/src/components/entity/ha-entities-picker.ts @@ -76,7 +76,7 @@ class HaEntitiesPicker extends LitElement { @property({ attribute: false }) public entityFilter?: HaEntityPickerEntityFilterFunc; - @property({ attribute: false, type: Array }) public createDomains?: string[]; + @property({ attribute: false }) public createDomains?: string[]; @property({ type: Boolean }) public reorder = false; diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index 9009e02b7d..959f0fe83f 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -58,7 +58,7 @@ export class HaEntityPicker extends LitElement { @property({ type: String, attribute: "search-label" }) public searchLabel?: string; - @property({ attribute: false, type: Array }) public createDomains?: string[]; + @property({ attribute: false }) public createDomains?: string[]; /** * Show entities from specific domains. diff --git a/src/components/entity/ha-state-label-badge.ts b/src/components/entity/ha-state-label-badge.ts index 3e23fe4242..b11617184f 100644 --- a/src/components/entity/ha-state-label-badge.ts +++ b/src/components/entity/ha-state-label-badge.ts @@ -9,16 +9,7 @@ import secondsToDuration from "../../common/datetime/seconds_to_duration"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateName } from "../../common/entity/compute_state_name"; import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states"; -import { - formatNumber, - getNumberFormatOptions, - isNumericState, -} from "../../common/number/format_number"; -import { - isUnavailableState, - UNAVAILABLE, - UNKNOWN, -} from "../../data/entity/entity"; +import { isUnavailableState, UNAVAILABLE } from "../../data/entity/entity"; import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry"; import { timerTimeRemaining } from "../../data/timer"; import type { HomeAssistant } from "../../types"; @@ -180,16 +171,11 @@ export class HaStateLabelBadge extends LitElement { } // eslint-disable-next-line: disable=no-fallthrough default: - return entityState.state === UNKNOWN || - entityState.state === UNAVAILABLE + return isUnavailableState(entityState.state) ? "—" - : isNumericState(entityState) - ? formatNumber( - entityState.state, - this.hass!.locale, - getNumberFormatOptions(entityState, entry) - ) - : this.hass!.formatEntityState(entityState); + : this.hass!.formatEntityStateToParts(entityState).find( + (part) => part.type === "value" + )?.value; } } @@ -238,7 +224,11 @@ export class HaStateLabelBadge extends LitElement { if (domain === "timer") { return secondsToDuration(_timerTimeRemaining); } - return entityState.attributes.unit_of_measurement || null; + return ( + this.hass!.formatEntityStateToParts(entityState).find( + (part) => part.type === "unit" + )?.value || null + ); } private _clearInterval() { diff --git a/src/components/entity/ha-statistic-picker.ts b/src/components/entity/ha-statistic-picker.ts index 37d1ac178a..5ee97e5222 100644 --- a/src/components/entity/ha-statistic-picker.ts +++ b/src/components/entity/ha-statistic-picker.ts @@ -78,7 +78,7 @@ export class HaStatisticPicker extends LitElement { @property({ type: Boolean, attribute: "allow-custom-entity" }) public allowCustomEntity; - @property({ attribute: false, type: Array }) + @property({ attribute: false }) public statisticIds?: StatisticsMetaData[]; @property({ attribute: false }) public helpMissingEntityUrl = diff --git a/src/components/entity/ha-statistics-picker.ts b/src/components/entity/ha-statistics-picker.ts index 96eab269ee..d8dedfa9e7 100644 --- a/src/components/entity/ha-statistics-picker.ts +++ b/src/components/entity/ha-statistics-picker.ts @@ -11,7 +11,7 @@ class HaStatisticsPicker extends LitElement { @property({ type: Array }) public value?: string[]; - @property({ attribute: false, type: Array }) public statisticIds?: string[]; + @property({ attribute: false }) public statisticIds?: string[]; @property({ attribute: "statistic-types" }) public statisticTypes?: "mean" | "sum"; diff --git a/src/components/ha-addon-picker.ts b/src/components/ha-addon-picker.ts index ea3db8f559..4a7fe6c7a7 100644 --- a/src/components/ha-addon-picker.ts +++ b/src/components/ha-addon-picker.ts @@ -57,13 +57,13 @@ class HaAddonPicker extends LitElement { } protected firstUpdated() { - this._getAddons(); + this._getApps(); } protected render() { const label = this.label === undefined && this.hass - ? this.hass.localize("ui.components.addon-picker.addon") + ? this.hass.localize("ui.components.app-picker.app") : this.label; if (this._error) { @@ -92,7 +92,7 @@ class HaAddonPicker extends LitElement { `; } - private async _getAddons() { + private async _getApps() { try { if (isComponentLoaded(this.hass, "hassio")) { const addonsInfo = await fetchHassioAddonsInfo(this.hass); @@ -113,12 +113,12 @@ class HaAddonPicker extends LitElement { })); } else { this._error = this.hass.localize( - "ui.components.addon-picker.error.no_supervisor" + "ui.components.app-picker.error.no_supervisor" ); } } catch (_err: any) { this._error = this.hass.localize( - "ui.components.addon-picker.error.fetch_addons" + "ui.components.app-picker.error.fetch_apps" ); } } diff --git a/src/components/ha-ansi-to-html.ts b/src/components/ha-ansi-to-html.ts index 224a2cb57b..72b2ebed11 100644 --- a/src/components/ha-ansi-to-html.ts +++ b/src/components/ha-ansi-to-html.ts @@ -48,7 +48,6 @@ export class HaAnsiToHtml extends LitElement { static styles = css` pre { - overflow-x: auto; margin: 0; } pre.wrap { diff --git a/src/components/ha-area-controls-picker.ts b/src/components/ha-area-controls-picker.ts new file mode 100644 index 0000000000..3611556780 --- /dev/null +++ b/src/components/ha-area-controls-picker.ts @@ -0,0 +1,342 @@ +import type { HassEntity } from "home-assistant-js-websocket"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property } from "lit/decorators"; +import Fuse from "fuse.js"; +import memoizeOne from "memoize-one"; +import { computeEntityNameList } from "../common/entity/compute_entity_name_display"; +import { computeRTL } from "../common/util/compute_rtl"; +import type { LocalizeFunc } from "../common/translations/localize"; +import { + multiTermSortedSearch, + type FuseWeightedKey, +} from "../resources/fuseMultiTerm"; +import { + AREA_CONTROLS_BUTTONS, + getAreaControlEntities, + type AreaControlDomain, +} from "../data/area/area_controls"; +import type { HomeAssistant } from "../types"; +import type { PickerComboBoxItem } from "./ha-picker-combo-box"; +import "./ha-combo-box-item"; +import "./ha-domain-icon"; +import "./ha-generic-picker"; +import "./ha-state-icon"; + +export interface AreaControlPickerItem extends PickerComboBoxItem { + type?: "domain" | "entity"; + stateObj?: HassEntity; + domain?: string; + deviceClass?: string; +} + +const AREA_CONTROL_DOMAINS: readonly AreaControlDomain[] = [ + "light", + "fan", + "switch", + "cover-shutter", + "cover-blind", + "cover-curtain", + "cover-shade", + "cover-awning", + "cover-garage", + "cover-gate", + "cover-door", + "cover-window", + "cover-damper", +] as const; + +@customElement("ha-area-controls-picker") +export class HaAreaControlsPicker extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: "area-id" }) public areaId!: string; + + @property({ type: Array, attribute: "exclude-entities" }) + public excludeEntities?: string[]; + + @property() public value?: string; + + @property({ type: Array, attribute: "exclude-values" }) + public excludeValues?: string[]; + + @property() public label?: string; + + @property() public placeholder?: string; + + @property() public helper?: string; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = false; + + @property({ attribute: "add-button-label" }) public addButtonLabel?: string; + + private _domainSearchKeys: FuseWeightedKey[] = [ + { + name: "primary", + weight: 10, + }, + ]; + + private _entitySearchKeys: FuseWeightedKey[] = [ + { + name: "primary", + weight: 10, + }, + { + name: "secondary", + weight: 5, + }, + { + name: "id", + weight: 3, + }, + ]; + + private _createFuseIndex = ( + items: AreaControlPickerItem[], + keys: FuseWeightedKey[] + ) => Fuse.createIndex(keys, items); + + private _domainFuseIndex = memoizeOne((items: AreaControlPickerItem[]) => + this._createFuseIndex(items, this._domainSearchKeys) + ); + + private _entityFuseIndex = memoizeOne((items: AreaControlPickerItem[]) => + this._createFuseIndex(items, this._entitySearchKeys) + ); + + private _getItems = memoizeOne( + ( + areaId: string, + excludeEntities: string[] | undefined, + currentValue: string | undefined, + excludeValues: string[] | undefined, + localize: LocalizeFunc, + _entities: HomeAssistant["entities"], + _devices: HomeAssistant["devices"], + _areas: HomeAssistant["areas"] + ): (( + searchString?: string, + section?: string + ) => (AreaControlPickerItem | string)[]) => + (searchString?: string, section?: string) => { + if (!this.hass) { + return []; + } + + const isSelected = (id: string): boolean => + currentValue === id || + (excludeValues !== undefined && excludeValues.includes(id)); + + const controlEntities = getAreaControlEntities( + AREA_CONTROL_DOMAINS as unknown as AreaControlDomain[], + areaId, + excludeEntities, + this.hass + ); + + const items: (AreaControlPickerItem | string)[] = []; + let domainItems: AreaControlPickerItem[] = []; + let entityItems: AreaControlPickerItem[] = []; + + if (!section || section === "domain") { + const supportedControls = ( + Object.keys(controlEntities) as (keyof typeof controlEntities)[] + ).filter((control) => controlEntities[control].length > 0); + + supportedControls.forEach((control) => { + if (isSelected(control)) { + return; + } + const label = localize( + `ui.panel.lovelace.editor.features.types.area-controls.controls_options.${control}` + ); + const button = AREA_CONTROLS_BUTTONS[control]; + const deviceClass = button.filter.device_class + ? Array.isArray(button.filter.device_class) + ? button.filter.device_class[0] + : button.filter.device_class + : undefined; + + domainItems.push({ + type: "domain", + id: control, + primary: label, + domain: button.filter.domain, + deviceClass, + }); + }); + + if (searchString) { + const fuseIndex = this._domainFuseIndex(domainItems); + domainItems = multiTermSortedSearch( + domainItems, + searchString, + this._domainSearchKeys, + (item) => item.id, + fuseIndex + ); + } + } + + if (!section || section === "entity") { + const allEntityIds = Object.values(controlEntities).flat(); + const uniqueEntityIds = Array.from(new Set(allEntityIds)); + + const isRTL = computeRTL(this.hass); + + uniqueEntityIds.forEach((entityId) => { + if (isSelected(entityId)) { + return; + } + const stateObj = this.hass!.states[entityId]; + if (!stateObj) { + return; + } + + const [entityName, deviceName, areaName] = computeEntityNameList( + stateObj, + [{ type: "entity" }, { type: "device" }, { type: "area" }], + this.hass!.entities, + this.hass!.devices, + this.hass!.areas, + this.hass!.floors + ); + + const primary = entityName || deviceName || entityId; + const secondary = [areaName, entityName ? deviceName : undefined] + .filter(Boolean) + .join(isRTL ? " ◂ " : " ▸ "); + + entityItems.push({ + type: "entity", + id: entityId, + primary, + secondary, + stateObj, + }); + }); + + if (searchString) { + const fuseIndex = this._entityFuseIndex(entityItems); + entityItems = multiTermSortedSearch( + entityItems, + searchString, + this._entitySearchKeys, + (item) => item.id, + fuseIndex + ); + } + } + + // Only add section headers if there are items in that section + if (!section) { + if (domainItems.length > 0) { + items.push( + localize( + "ui.panel.lovelace.editor.features.types.area-controls.sections.domain" + ) + ); + items.push(...domainItems); + } + if (entityItems.length > 0) { + items.push( + localize( + "ui.panel.lovelace.editor.features.types.area-controls.sections.entity" + ) + ); + items.push(...entityItems); + } + } else { + items.push(...domainItems, ...entityItems); + } + + return items; + } + ); + + private _rowRenderer = (item: AreaControlPickerItem) => html` + + ${item.type === "entity" && item.stateObj + ? html`` + : item.domain + ? html`` + : nothing} + ${item.primary} + ${item.secondary + ? html`${item.secondary}` + : nothing} + ${item.type === "entity" && item.stateObj + ? html` + ${item.stateObj.entity_id} + ` + : nothing} + + `; + + protected render() { + if (!this.hass) { + return nothing; + } + + return html` + + `; + } + + static styles = css` + .code { + font-family: var(--ha-font-family-code); + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-area-controls-picker": HaAreaControlsPicker; + } +} diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index beb29826be..3d7d54c257 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -163,7 +163,7 @@ export class HaAreaPicker extends LitElement { { id: ADD_NEW_ID + searchString, primary: this.hass.localize( - "ui.components.area-picker.add_new_sugestion", + "ui.components.area-picker.add_new_suggestion", { name: searchString, } diff --git a/src/components/ha-areas-floors-display-editor.ts b/src/components/ha-areas-floors-display-editor.ts index 29b6034346..57662905c1 100644 --- a/src/components/ha-areas-floors-display-editor.ts +++ b/src/components/ha-areas-floors-display-editor.ts @@ -9,7 +9,7 @@ import { computeFloorName } from "../common/entity/compute_floor_name"; import { getAreaContext } from "../common/entity/context/get_area_context"; import type { FloorRegistryEntry } from "../data/floor_registry"; import { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper"; -import type { HomeAssistant } from "../types"; +import type { HomeAssistant, ValueChangedEvent } from "../types"; import "./ha-expansion-panel"; import "./ha-floor-icon"; import "./ha-items-display-editor"; @@ -200,7 +200,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement { fireEvent(this, "value-changed", { value: newValue }); } - private async _areaDisplayChanged(ev: CustomEvent<{ value: DisplayValue }>) { + private async _areaDisplayChanged(ev: ValueChangedEvent) { ev.stopPropagation(); const value = ev.detail.value; const currentFloorId = (ev.currentTarget as any).floorId; diff --git a/src/components/ha-assist-chat.ts b/src/components/ha-assist-chat.ts index b920767823..ba05e7ed99 100644 --- a/src/components/ha-assist-chat.ts +++ b/src/components/ha-assist-chat.ts @@ -36,7 +36,7 @@ export class HaAssistChat extends LitElement { @property({ type: Boolean, attribute: "disable-speech" }) public disableSpeech = false; - @property({ type: Boolean, attribute: false }) + @property({ attribute: false }) public startListening?: boolean; @query("#message-input") private _messageInput!: HaTextField; diff --git a/src/components/ha-assist-pipeline-picker.ts b/src/components/ha-assist-pipeline-picker.ts index 97013bc507..7ebc6391f1 100644 --- a/src/components/ha-assist-pipeline-picker.ts +++ b/src/components/ha-assist-pipeline-picker.ts @@ -2,14 +2,12 @@ import type { PropertyValueMap } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; -import { stopPropagation } from "../common/dom/stop_propagation"; import { formatLanguageCode } from "../common/language/format_language"; import type { AssistPipeline } from "../data/assist_pipeline"; import { listAssistPipelines } from "../data/assist_pipeline"; import type { HomeAssistant } from "../types"; -import "./ha-list-item"; +import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select"; import "./ha-select"; -import type { HaSelect } from "./ha-select"; const PREFERRED = "preferred"; const LAST_USED = "last_used"; @@ -41,6 +39,31 @@ export class HaAssistPipelinePicker extends LitElement { return nothing; } const value = this.value ?? this._default; + const options: HaSelectOption[] = [ + { + value: PREFERRED, + label: this.hass.localize("ui.components.pipeline-picker.preferred", { + preferred: this._pipelines.find( + (pipeline) => pipeline.id === this._preferredPipeline + )?.name, + }), + }, + ]; + + if (this.includeLastUsed) { + options.unshift({ + value: LAST_USED, + label: this.hass.localize("ui.components.pipeline-picker.last_used"), + }); + } + + options.push( + ...this._pipelines.map((pipeline) => ({ + value: pipeline.id, + label: `${pipeline.name} (${formatLanguageCode(pipeline.language, this.hass.locale)})`, + })) + ); + return html` - ${this.includeLastUsed - ? html` - - ${this.hass!.localize( - "ui.components.pipeline-picker.last_used" - )} - - ` - : null} - - ${this.hass!.localize("ui.components.pipeline-picker.preferred", { - preferred: this._pipelines.find( - (pipeline) => pipeline.id === this._preferredPipeline - )?.name, - })} - - ${this._pipelines.map( - (pipeline) => - html` - ${pipeline.name} - (${formatLanguageCode(pipeline.language, this.hass.locale)}) - ` - )} `; } @@ -96,17 +94,17 @@ export class HaAssistPipelinePicker extends LitElement { } `; - private _changed(ev): void { - const target = ev.target as HaSelect; + private _changed(ev: HaSelectSelectEvent): void { + const value = ev.detail.value; if ( !this.hass || - target.value === "" || - target.value === this.value || - (this.value === undefined && target.value === this._default) + value === "" || + value === this.value || + (this.value === undefined && value === this._default) ) { return; } - this.value = target.value === this._default ? undefined : target.value; + this.value = value === this._default ? undefined : value; fireEvent(this, "value-changed", { value: this.value }); } } diff --git a/src/components/ha-attribute-value.ts b/src/components/ha-attribute-value.ts index a419c6b99e..d586ed5707 100644 --- a/src/components/ha-attribute-value.ts +++ b/src/components/ha-attribute-value.ts @@ -2,7 +2,6 @@ import type { HassEntity } from "home-assistant-js-websocket"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import { until } from "lit/directives/until"; -import { formatNumber } from "../common/number/format_number"; import type { HomeAssistant } from "../types"; @customElement("ha-attribute-value") @@ -21,10 +20,6 @@ class HaAttributeValue extends LitElement { } const attributeValue = this.stateObj.attributes[this.attribute]; - if (typeof attributeValue === "number" && this.hideUnit) { - return formatNumber(attributeValue, this.hass.locale); - } - if (typeof attributeValue === "string") { // URL handling if (attributeValue.startsWith("http")) { @@ -56,6 +51,14 @@ class HaAttributeValue extends LitElement { return html`
${until(yaml, "")}
`; } + if (this.hideUnit) { + const parts = this.hass.formatEntityAttributeValueToParts( + this.stateObj!, + this.attribute + ); + return parts.find((part) => part.type === "value")?.value; + } + return this.hass.formatEntityAttributeValue(this.stateObj!, this.attribute); } diff --git a/src/components/ha-base-time-input.ts b/src/components/ha-base-time-input.ts index 7c12390d92..3d7eb98638 100644 --- a/src/components/ha-base-time-input.ts +++ b/src/components/ha-base-time-input.ts @@ -4,10 +4,9 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import { fireEvent } from "../common/dom/fire_event"; -import { stopPropagation } from "../common/dom/stop_propagation"; +import type { HaSelectSelectEvent } from "./ha-select"; import "./ha-icon-button"; import "./ha-input-helper-text"; -import "./ha-list-item"; import "./ha-select"; import "./ha-textfield"; import type { HaTextField } from "./ha-textfield"; @@ -208,7 +207,8 @@ export class HaBaseTimeInput extends LitElement { ? html` - AM - PM `} ${this.helper @@ -282,10 +277,12 @@ export class HaBaseTimeInput extends LitElement { fireEvent(this, "value-changed"); } - private _valueChanged(ev: InputEvent) { + private _valueChanged(ev: InputEvent | HaSelectSelectEvent): void { const textField = ev.currentTarget as HaTextField; this[textField.name] = - textField.name === "amPm" ? textField.value : Number(textField.value); + textField.name === "amPm" + ? (ev as HaSelectSelectEvent).detail.value + : Number(textField.value); const value: TimeChangedEvent = { hours: this.hours, minutes: this.minutes, @@ -311,7 +308,8 @@ export class HaBaseTimeInput extends LitElement { * Format time fragments */ private _formatValue(value: number, padding = 2) { - return value.toString().padStart(padding, "0"); + const str = value.toString(); + return str.includes(".") ? str : str.padStart(padding, "0"); } /** @@ -365,10 +363,6 @@ export class HaBaseTimeInput extends LitElement { ha-textfield:last-child { --text-field-border-top-right-radius: var(--mdc-shape-medium); } - ha-select { - --mdc-shape-small: 0; - width: 85px; - } :host([clearable]) .mdc-select__anchor { padding-inline-end: var(--select-selected-text-padding-end, 12px); } diff --git a/src/components/ha-blueprint-picker.ts b/src/components/ha-blueprint-picker.ts index 5c13ef894e..dc8971fe35 100644 --- a/src/components/ha-blueprint-picker.ts +++ b/src/components/ha-blueprint-picker.ts @@ -2,12 +2,11 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; -import { stopPropagation } from "../common/dom/stop_propagation"; import { stringCompare } from "../common/string/compare"; import type { Blueprint, BlueprintDomain, Blueprints } from "../data/blueprint"; import { fetchBlueprints } from "../data/blueprint"; import type { HomeAssistant } from "../types"; -import "./ha-list-item"; +import type { HaSelectSelectEvent } from "./ha-select"; import "./ha-select"; @customElement("ha-blueprint-picker") @@ -55,20 +54,16 @@ class HaBluePrintPicker extends LitElement { - ${this._processedBlueprints(this.blueprints).map( - (blueprint) => html` - - ${blueprint.name} - - ` + .options=${this._processedBlueprints(this.blueprints).map( + (blueprint) => ({ + value: blueprint.path, + label: blueprint.name, + }) )} + > `; } @@ -82,8 +77,8 @@ class HaBluePrintPicker extends LitElement { } } - private _blueprintChanged(ev) { - const newValue = ev.target.value; + private _blueprintChanged(ev: HaSelectSelectEvent) { + const newValue = ev.detail.value; if (newValue !== this.value) { this.value = newValue; diff --git a/src/components/ha-button-menu.ts b/src/components/ha-button-menu.ts deleted file mode 100644 index 9562339013..0000000000 --- a/src/components/ha-button-menu.ts +++ /dev/null @@ -1,121 +0,0 @@ -import type { Corner, Menu, MenuCorner } from "@material/mwc-menu"; -import type { TemplateResult } from "lit"; -import { css, html, LitElement } from "lit"; -import { customElement, property, query } from "lit/decorators"; -import { mainWindow } from "../common/dom/get_main_window"; -import { FOCUS_TARGET } from "../dialogs/make-dialog-manager"; -import type { HaIconButton } from "./ha-icon-button"; -import type { HaButton } from "./ha-button"; -import "./ha-menu"; - -@customElement("ha-button-menu") -export class HaButtonMenu extends LitElement { - protected readonly [FOCUS_TARGET]; - - @property() public corner: Corner = "BOTTOM_START"; - - @property({ attribute: "menu-corner" }) public menuCorner: MenuCorner = - "START"; - - @property({ type: Number }) public x: number | null = null; - - @property({ type: Number }) public y: number | null = null; - - @property({ type: Boolean }) public multi = false; - - @property({ type: Boolean }) public activatable = false; - - @property({ type: Boolean }) public disabled = false; - - @property({ type: Boolean }) public fixed = false; - - @property({ type: Boolean, attribute: "no-anchor" }) public noAnchor = false; - - @query("ha-menu", true) private _menu?: Menu; - - public get items() { - return this._menu?.items; - } - - public get selected() { - return this._menu?.selected; - } - - public override focus() { - if (this._menu?.open) { - this._menu.focusItemAtIndex(0); - } else { - this._triggerButton?.focus(); - } - } - - protected render(): TemplateResult { - return html` -
- -
- - - - `; - } - - protected firstUpdated(changedProps): void { - super.firstUpdated(changedProps); - - if (mainWindow.document.dir === "rtl") { - this.updateComplete.then(() => { - this.querySelectorAll("ha-list-item").forEach((item) => { - const style = document.createElement("style"); - style.innerHTML = - "span.material-icons:first-of-type { margin-left: var(--mdc-list-item-graphic-margin, 32px) !important; margin-right: 0px !important;}"; - item!.shadowRoot!.appendChild(style); - }); - }); - } - } - - private _handleClick(): void { - if (this.disabled) { - return; - } - this._menu!.anchor = this.noAnchor ? null : this; - this._menu!.show(); - } - - private get _triggerButton() { - return this.querySelector( - 'ha-icon-button[slot="trigger"], ha-button[slot="trigger"]' - ) as HaIconButton | HaButton | null; - } - - private _setTriggerAria() { - if (this._triggerButton) { - this._triggerButton.ariaHasPopup = "menu"; - } - } - - static styles = css` - :host { - display: inline-block; - position: relative; - } - ::slotted([disabled]) { - color: var(--disabled-text-color); - } - `; -} - -declare global { - interface HTMLElementTagNameMap { - "ha-button-menu": HaButtonMenu; - } -} diff --git a/src/components/ha-card.ts b/src/components/ha-card.ts index 7900bd215d..bcf7b3ccf6 100644 --- a/src/components/ha-card.ts +++ b/src/components/ha-card.ts @@ -51,6 +51,7 @@ export class HaCard extends LitElement { font-weight: var(--ha-font-weight-normal); } + /* clean-css ignore:start */ :host ::slotted( .card-content:not(:nth-child(1 of .card-content, .card-header)) @@ -59,6 +60,7 @@ export class HaCard extends LitElement { padding-top: 0; margin-top: calc(var(--ha-space-2) * -1); } + /* clean-css ignore:end */ :host ::slotted(.card-content) { padding: var(--ha-space-4); diff --git a/src/components/ha-code-editor.ts b/src/components/ha-code-editor.ts index b3085aa775..87feadec13 100644 --- a/src/components/ha-code-editor.ts +++ b/src/components/ha-code-editor.ts @@ -13,6 +13,9 @@ import { mdiArrowCollapse, mdiArrowExpand, mdiContentCopy, + mdiBug, + mdiBugOutline, + mdiFindReplace, mdiRedo, mdiUndo, } from "@mdi/js"; @@ -36,6 +39,7 @@ import type { HaIconButtonToolbar } from "./ha-icon-button-toolbar"; declare global { interface HASSDomEvents { "editor-save": undefined; + "test-toggle": undefined; } } @@ -82,6 +86,11 @@ export class HaCodeEditor extends ReactiveElement { @property({ type: Boolean, attribute: "has-toolbar" }) public hasToolbar = true; + @property({ type: Boolean, attribute: "has-test" }) + public hasTest = false; + + @property({ attribute: false }) public testing = false; + @property({ type: String }) public placeholder?: string; @state() private _value = ""; @@ -213,7 +222,8 @@ export class HaCodeEditor extends ReactiveElement { if ( changedProps.has("_canCopy") || changedProps.has("_canUndo") || - changedProps.has("_canRedo") + changedProps.has("_canRedo") || + changedProps.has("testing") ) { this._updateToolbarButtons(); } @@ -361,6 +371,19 @@ export class HaCodeEditor extends ReactiveElement { } this._editorToolbar.items = [ + ...(this.hasTest && !this._isFullscreen + ? [ + { + id: "test", + label: + this.hass?.localize( + `ui.components.yaml-editor.test_${this.testing ? "off" : "on"}` + ) || "Test", + path: this.testing ? mdiBugOutline : mdiBug, + action: (e: Event) => this._handleTestClick(e), + }, + ] + : []), { id: "undo", disabled: !this._canUndo, @@ -384,6 +407,14 @@ export class HaCodeEditor extends ReactiveElement { path: mdiContentCopy, action: (e: Event) => this._handleClipboardClick(e), }, + { + id: "find-replace", + label: + this.hass?.localize("ui.components.yaml-editor.find_and_replace") || + "Find and replace", + path: mdiFindReplace, + action: (e: Event) => this._handleFindReplaceClick(e), + }, { id: "fullscreen", disabled: this.disableFullscreen, @@ -418,6 +449,15 @@ export class HaCodeEditor extends ReactiveElement { } }; + private _handleTestClick = (e: Event) => { + e.preventDefault(); + e.stopPropagation(); + if (!this.codemirror) { + return; + } + fireEvent(this, "test-toggle"); + }; + private _handleUndoClick = (e: Event) => { e.preventDefault(); e.stopPropagation(); @@ -442,6 +482,21 @@ export class HaCodeEditor extends ReactiveElement { this._updateFullscreenState(!this._isFullscreen); }; + private _handleFindReplaceClick = (e: Event) => { + e.preventDefault(); + e.stopPropagation(); + if (!this.codemirror || !this._loadedCodeMirror) { + return; + } + // Toggle search panel: close if open, open if closed + const searchPanel = this.codemirror.dom.querySelector(".cm-search"); + if (searchPanel) { + this._loadedCodeMirror.closeSearchPanel(this.codemirror); + } else { + this._loadedCodeMirror.openSearchPanel(this.codemirror); + } + }; + private _handleKeyDown = (e: KeyboardEvent) => { if ( (e.key === "Escape" && @@ -691,15 +746,10 @@ export class HaCodeEditor extends ReactiveElement { private _getIconItems = async (): Promise => { if (!this._iconList) { - let iconList: { + const iconList: { name: string; keywords: string[]; - }[]; - if (__SUPERVISOR__) { - iconList = []; - } else { - iconList = (await import("../../build/mdi/iconList.json")).default; - } + }[] = (await import("../../build/mdi/iconList.json")).default; this._iconList = iconList.map((icon) => ({ type: "variable", diff --git a/src/components/ha-color-picker.ts b/src/components/ha-color-picker.ts index f523c98468..1c8902c60c 100644 --- a/src/components/ha-color-picker.ts +++ b/src/components/ha-color-picker.ts @@ -6,7 +6,7 @@ import memoizeOne from "memoize-one"; import { computeCssColor, THEME_COLORS } from "../common/color/compute-color"; import { fireEvent } from "../common/dom/fire_event"; import type { LocalizeKeys } from "../common/translations/localize"; -import type { HomeAssistant } from "../types"; +import type { HomeAssistant, ValueChangedEvent } from "../types"; import "./ha-generic-picker"; import type { PickerComboBoxItem } from "./ha-picker-combo-box"; import type { PickerValueRenderer } from "./ha-picker-field"; @@ -224,7 +224,7 @@ export class HaColorPicker extends LitElement { `; } - private _valueChanged(ev: CustomEvent<{ value?: string }>) { + private _valueChanged(ev: ValueChangedEvent) { ev.stopPropagation(); const selected = ev.detail.value; const normalized = diff --git a/src/components/ha-control-select-menu.ts b/src/components/ha-control-select-menu.ts index d771f9a479..3dc78ef957 100644 --- a/src/components/ha-control-select-menu.ts +++ b/src/components/ha-control-select-menu.ts @@ -1,24 +1,31 @@ -import { SelectBase } from "@material/mwc-select/mwc-select-base"; import { mdiMenuDown } from "@mdi/js"; -import type { PropertyValues } from "lit"; -import { css, html, nothing } from "lit"; +import type { HassEntity } from "home-assistant-js-websocket"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, property, query } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; -import { ifDefined } from "lit/directives/if-defined"; -import { debounce } from "../common/util/debounce"; -import { nextRender } from "../common/util/render-status"; +import memoizeOne from "memoize-one"; +import type { HomeAssistant } from "../types"; +import "./ha-attribute-icon"; +import "./ha-dropdown"; +import "./ha-dropdown-item"; import "./ha-icon"; -import type { HaIcon } from "./ha-icon"; -import "./ha-ripple"; import "./ha-svg-icon"; -import type { HaSvgIcon } from "./ha-svg-icon"; -import "./ha-menu"; + +export interface SelectOption { + label: string; + value: string; + iconPath?: string; + icon?: string; + attributeIcon?: { + stateObj: HassEntity; + attribute: string; + attributeValue?: string; + }; +} @customElement("ha-control-select-menu") -export class HaControlSelectMenu extends SelectBase { - @query(".select") protected mdcRoot!: HTMLElement; - - @query(".select-anchor") protected anchorElement!: HTMLDivElement | null; +export class HaControlSelectMenu extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; @property({ type: Boolean, attribute: "show-arrow" }) public showArrow = false; @@ -26,95 +33,83 @@ export class HaControlSelectMenu extends SelectBase { @property({ type: Boolean, attribute: "hide-label" }) public hideLabel = false; - @property() public options; + @property({ type: Boolean }) + public disabled = false; - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - if (changedProps.get("options")) { - this.layoutOptions(); - this.selectByValue(this.value); - } - } + @property({ type: Boolean }) + public required = false; + + @property() + public label?: string; + + @property() + public value?: string; + + @property({ attribute: false }) public options: SelectOption[] = []; + + @query("button") private _triggerButton!: HTMLButtonElement; public override render() { - const classes = { - "select-disabled": this.disabled, - "select-required": this.required, - "select-invalid": !this.isUiValid, - "select-no-value": !this.selectedText, - }; - - const labelledby = this.label && !this.hideLabel ? "label" : undefined; - const labelAttribute = - this.label && this.hideLabel ? this.label : undefined; + if (this.disabled) { + return this._renderTrigger(); + } return html` -
- - -
- ${this._renderIcon()} -
- ${this.hideLabel - ? nothing - : html`

${this.label}

`} - ${this.selectedText - ? html`

${this.selectedText}

` - : nothing} -
- ${this._renderArrow()} - -
- ${this.renderMenu()} -
+ + ${this._renderTrigger()} ${this.options.map(this._renderOption)} + `; } - protected override renderMenu() { - const classes = this.getMenuClasses(); - return html` - ${this.renderMenuContent()} - `; + ${this._renderIcon()} +
+ ${this.hideLabel + ? nothing + : html`

${this.label}

`} + ${selectedText ? html`

${selectedText}

` : nothing} +
+ ${this._renderArrow()} + `; } + private _renderOption = (option: SelectOption) => + html`${option.iconPath + ? html`` + : option.icon + ? html`` + : option.attributeIcon + ? html`` + : nothing} + ${option.label}`; + private _renderArrow() { - if (!this.showArrow) return nothing; + if (!this.showArrow) { + return nothing; + } return html`
@@ -124,47 +119,42 @@ export class HaControlSelectMenu extends SelectBase { } private _renderIcon() { - const index = this.mdcFoundation?.getSelectedIndex(); - const items = this.menuElement?.items ?? []; - const item = index != null ? items[index] : undefined; + const { iconPath, icon, attributeIcon } = + this.getValueObject(this.options, this.value) ?? {}; const defaultIcon = this.querySelector("[slot='icon']"); - const icon = (item?.querySelector("[slot='graphic']") ?? null) as - | HaSvgIcon - | HaIcon - | null; - - if (!defaultIcon && !icon) { - return null; - } return html`
- ${icon && icon.localName === "ha-svg-icon" && "path" in icon - ? html`` - : icon && icon.localName === "ha-icon" && "icon" in icon - ? html`` - : html``} + ${iconPath + ? html`` + : icon + ? html`` + : attributeIcon + ? html`` + : defaultIcon + ? html`` + : nothing}
`; } - connectedCallback() { - super.connectedCallback(); - window.addEventListener("translations-updated", this._translationsUpdated); - } - - disconnectedCallback() { - super.disconnectedCallback(); - window.removeEventListener( - "translations-updated", - this._translationsUpdated + private _showDropdown() { + this.style.setProperty( + "--control-select-menu-width", + `${this._triggerButton.offsetWidth}px` ); } - private _translationsUpdated = debounce(async () => { - await nextRender(); - this.layoutOptions(); - }, 500); + private getValueObject = memoizeOne( + (options: SelectOption[], value?: string) => + options.find((option) => option.value === value) + ); static override styles = [ css` @@ -186,6 +176,8 @@ export class HaControlSelectMenu extends SelectBase { -webkit-tap-highlight-color: transparent; } .select-anchor { + border: none; + text-align: left; height: var(--control-select-menu-height); padding: var(--control-select-menu-padding); overflow: hidden; @@ -211,6 +203,12 @@ export class HaControlSelectMenu extends SelectBase { font-weight: var(--ha-font-weight-normal); letter-spacing: 0.25px; } + .select-anchor:hover { + --control-select-menu-background-color: var( + --ha-color-on-neutral-quiet + ); + } + .content { display: flex; flex-direction: column; @@ -230,16 +228,19 @@ export class HaControlSelectMenu extends SelectBase { } .label { - font-size: 0.85em; + font-size: var(--ha-font-size-s); letter-spacing: 0.4px; } - .select-no-value .label { font-size: inherit; line-height: inherit; letter-spacing: inherit; } + .content .value { + font-size: var(--ha-font-size-m); + } + .select-anchor:focus-visible { box-shadow: 0 0 0 2px var(--control-select-menu-focus-color); } @@ -258,10 +259,14 @@ export class HaControlSelectMenu extends SelectBase { opacity: var(--control-select-menu-background-opacity); } - .select-disabled .select-anchor { + .select-disabled.select-anchor { cursor: not-allowed; color: var(--disabled-color); } + + ha-dropdown::part(menu) { + min-width: var(--control-select-menu-width); + } `, ]; } diff --git a/src/components/ha-control-switch.ts b/src/components/ha-control-switch.ts index d220852fa4..fa6891bce8 100644 --- a/src/components/ha-control-switch.ts +++ b/src/components/ha-control-switch.ts @@ -24,10 +24,10 @@ export class HaControlSwitch extends LitElement { @property({ type: Boolean }) public checked = false; // SVG icon path (if you need a non SVG icon instead, use the provided on icon slot to pass an in) - @property({ attribute: false, type: String }) pathOn?: string; + @property({ attribute: false }) pathOn?: string; // SVG icon path (if you need a non SVG icon instead, use the provided off icon slot to pass an in) - @property({ attribute: false, type: String }) pathOff?: string; + @property({ attribute: false }) pathOff?: string; @property({ type: String }) public label?: string; diff --git a/src/components/ha-conversation-agent-picker.ts b/src/components/ha-conversation-agent-picker.ts index e576549b90..4013016883 100644 --- a/src/components/ha-conversation-agent-picker.ts +++ b/src/components/ha-conversation-agent-picker.ts @@ -3,7 +3,6 @@ import type { PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; -import { stopPropagation } from "../common/dom/stop_propagation"; import { debounce } from "../common/util/debounce"; import type { ConfigEntry, SubEntry } from "../data/config_entries"; import { getConfigEntry, getSubEntries } from "../data/config_entries"; @@ -14,9 +13,8 @@ import { fetchIntegrationManifest } from "../data/integration"; import { showOptionsFlowDialog } from "../dialogs/config-flow/show-dialog-options-flow"; import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow"; import type { HomeAssistant } from "../types"; -import "./ha-list-item"; import "./ha-select"; -import type { HaSelect } from "./ha-select"; +import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select"; const NONE = "__NONE_OPTION__"; @@ -73,37 +71,35 @@ export class HaConversationAgentPicker extends LitElement { value = NONE; } + const options: HaSelectOption[] = this._agents.map((agent) => ({ + value: agent.id, + label: agent.name, + disabled: + agent.supported_languages !== "*" && + agent.supported_languages.length === 0, + })); + + if (!this.required) { + options.unshift({ + value: NONE, + label: this.hass.localize( + "ui.components.conversation-agent-picker.none" + ), + }); + } + return html` - ${!this.required - ? html` - ${this.hass!.localize( - "ui.components.coversation-agent-picker.none" - )} - ` - : nothing} - ${this._agents.map( - (agent) => - html` - ${agent.name} - ` - )}${(this._subConfigEntry && this._configEntry?.supported_subentry_types[ this._subConfigEntry.subentry_type @@ -238,17 +234,17 @@ export class HaConversationAgentPicker extends LitElement { } `; - private _changed(ev): void { - const target = ev.target as HaSelect; + private _changed(ev: HaSelectSelectEvent): void { + const value = ev.detail.value; if ( !this.hass || - target.value === "" || - target.value === this.value || - (this.value === undefined && target.value === NONE) + value === "" || + value === this.value || + (this.value === undefined && value === NONE) ) { return; } - this.value = target.value === NONE ? undefined : target.value; + this.value = value === NONE ? undefined : value; fireEvent(this, "value-changed", { value: this.value }); fireEvent(this, "supported-languages-changed", { value: this._agents!.find((agent) => agent.id === this.value) diff --git a/src/components/ha-country-picker.ts b/src/components/ha-country-picker.ts index b2f4b8a6e3..9719da5415 100644 --- a/src/components/ha-country-picker.ts +++ b/src/components/ha-country-picker.ts @@ -2,11 +2,17 @@ import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; -import { stopPropagation } from "../common/dom/stop_propagation"; import { caseInsensitiveStringCompare } from "../common/string/compare"; -import "./ha-list-item"; -import "./ha-select"; -import type { HaSelect } from "./ha-select"; +import type { FrontendLocaleData } from "../data/translation"; +import type { HomeAssistant, ValueChangedEvent } from "../types"; +import "./ha-generic-picker"; +import type { PickerComboBoxItem } from "./ha-picker-combo-box"; + +const SEARCH_KEYS = [ + { name: "primary", weight: 10 }, + { name: "secondary", weight: 8 }, + { name: "search_labels.english", weight: 5 }, +]; export const COUNTRIES = [ "AD", @@ -260,9 +266,45 @@ export const COUNTRIES = [ "ZW", ]; +export const getCountryOptions = ( + countries: string[], + noSort: boolean, + locale?: FrontendLocaleData +): PickerComboBoxItem[] => { + const language = locale?.language ?? "en"; + const countryDisplayNames = new Intl.DisplayNames(language, { + type: "region", + fallback: "code", + }); + const englishDisplayNames = new Intl.DisplayNames("en", { + type: "region", + fallback: "code", + }); + + const options: PickerComboBoxItem[] = countries.map((country) => { + const primary = countryDisplayNames.of(country) ?? country; + const englishName = englishDisplayNames.of(country) ?? country; + return { + id: country, + primary, + secondary: country, + search_labels: { + english: englishName !== primary ? englishName : null, + }, + }; + }); + + if (!noSort && locale) { + options.sort((a, b) => + caseInsensitiveStringCompare(a.primary, b.primary, locale.language) + ); + } + return options; +}; + @customElement("ha-country-picker") export class HaCountryPicker extends LitElement { - @property() public language = "en"; + @property({ attribute: false }) public hass?: HomeAssistant; @property() public value?: string; @@ -278,76 +320,72 @@ export class HaCountryPicker extends LitElement { @property({ attribute: "no-sort", type: Boolean }) public noSort = false; - private _getOptions = memoizeOne( - (language?: string, countries?: string[]) => { - let options: { label: string; value: string }[] = []; - const countryDisplayNames = new Intl.DisplayNames(language, { - type: "region", - fallback: "code", - }); - if (countries) { - options = countries.map((country) => ({ - value: country, - label: countryDisplayNames - ? countryDisplayNames.of(country)! - : country, - })); - } else { - options = COUNTRIES.map((country) => ({ - value: country, - label: countryDisplayNames - ? countryDisplayNames.of(country)! - : country, - })); - } + private _getCountryOptions = memoizeOne(getCountryOptions); - if (!this.noSort) { - options.sort((a, b) => - caseInsensitiveStringCompare(a.label, b.label, language) - ); - } - return options; - } - ); + private _getItems = () => + this._getCountryOptions( + this.countries ?? COUNTRIES, + this.noSort, + this.hass?.locale + ); + + private _getCountryName = (country?: string) => + this._getItems().find((c) => c.id === country)?.primary; + + private _valueRenderer = (value: string) => + html`${this._getCountryName(value) ?? value}`; protected render() { - const options = this._getOptions(this.language, this.countries); + const label = + this.label ?? + (this.hass?.localize("ui.components.country-picker.country") || + "Country"); + + const value = + this.value ?? + (this.required && !this.disabled ? this._getItems()[0]?.id : this.value); return html` - - ${options.map( - (option) => html` - ${option.label} - ` - )} - + .getItems=${this._getItems} + .searchKeys=${SEARCH_KEYS} + @value-changed=${this._changed} + hide-clear-icon + > `; } static styles = css` - ha-select { + ha-generic-picker { width: 100%; + min-width: 200px; + display: block; } `; - private _changed(ev): void { - const target = ev.target as HaSelect; - if (target.value === "" || target.value === this.value) { - return; - } - this.value = target.value; + private _changed(ev: ValueChangedEvent): void { + ev.stopPropagation(); + this.value = ev.detail.value; fireEvent(this, "value-changed", { value: this.value }); } + + private _notFoundLabel = (search: string) => { + const term = html`'${search}'`; + return this.hass + ? this.hass.localize("ui.components.country-picker.no_match", { term }) + : html`No countries found for ${term}`; + }; } declare global { diff --git a/src/components/ha-currency-picker.ts b/src/components/ha-currency-picker.ts index e5b05a0a75..1fba44b68e 100644 --- a/src/components/ha-currency-picker.ts +++ b/src/components/ha-currency-picker.ts @@ -2,11 +2,16 @@ import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; -import { stopPropagation } from "../common/dom/stop_propagation"; import { caseInsensitiveStringCompare } from "../common/string/compare"; -import "./ha-list-item"; -import "./ha-select"; -import type { HaSelect } from "./ha-select"; +import type { FrontendLocaleData } from "../data/translation"; +import type { HomeAssistant, ValueChangedEvent } from "../types"; +import "./ha-generic-picker"; +import type { PickerComboBoxItem } from "./ha-picker-combo-box"; + +const SEARCH_KEYS = [ + { name: "primary", weight: 10 }, + { name: "secondary", weight: 8 }, +]; const CURRENCIES = [ "AED", @@ -172,9 +177,31 @@ const curSymbol = (currency: string, locale?: string) => new Intl.NumberFormat(locale, { style: "currency", currency }) .formatToParts(1) .find((x) => x.type === "currency")?.value; + +export const getCurrencyOptions = ( + locale?: FrontendLocaleData +): PickerComboBoxItem[] => { + const language = locale?.language ?? "en"; + const currencyDisplayNames = new Intl.DisplayNames(language, { + type: "currency", + fallback: "code", + }); + + const options: PickerComboBoxItem[] = CURRENCIES.map((currency) => ({ + id: currency, + primary: `${currencyDisplayNames.of(currency)} (${curSymbol(currency, language)})`, + secondary: currency, + })); + + options.sort((a, b) => + caseInsensitiveStringCompare(a.primary, b.primary, language) + ); + return options; +}; + @customElement("ha-currency-picker") export class HaCurrencyPicker extends LitElement { - @property() public language = "en"; + @property({ attribute: false }) public hass?: HomeAssistant; @property() public value?: string; @@ -184,60 +211,62 @@ export class HaCurrencyPicker extends LitElement { @property({ type: Boolean, reflect: true }) public disabled = false; - private _getOptions = memoizeOne((language?: string) => { - const currencyDisplayNames = new Intl.DisplayNames(language, { - type: "currency", - fallback: "code", - }); - const options = CURRENCIES.map((currency) => ({ - value: currency, - label: `${ - currencyDisplayNames ? currencyDisplayNames.of(currency)! : currency - } (${curSymbol(currency, language)})`, - })); - options.sort((a, b) => - caseInsensitiveStringCompare(a.label, b.label, language) - ); - return options; - }); + private _getCurrencyOptions = memoizeOne(getCurrencyOptions); + + private _getItems = () => this._getCurrencyOptions(this.hass?.locale); + + private _getCurrencyName = (currency?: string) => + this._getItems().find((c) => c.id === currency)?.primary; + + private _valueRenderer = (value: string) => + html`${this._getCurrencyName(value) ?? value}`; protected render() { - const options = this._getOptions(this.language); + const label = + this.label ?? + (this.hass?.localize("ui.components.currency-picker.currency") || + "Currency"); return html` - - ${options.map( - (option) => html` - ${option.label} - ` - )} - + .required=${this.required} + .getItems=${this._getItems} + .searchKeys=${SEARCH_KEYS} + @value-changed=${this._changed} + hide-clear-icon + > `; } static styles = css` - ha-select { + ha-generic-picker { width: 100%; + min-width: 200px; + display: block; } `; - private _changed(ev): void { - const target = ev.target as HaSelect; - if (target.value === "" || target.value === this.value) { - return; - } - this.value = target.value; + private _changed(ev: ValueChangedEvent): void { + ev.stopPropagation(); + this.value = ev.detail.value; fireEvent(this, "value-changed", { value: this.value }); } + + private _notFoundLabel = (search: string) => { + const term = html`'${search}'`; + return this.hass + ? this.hass.localize("ui.components.currency-picker.no_match", { term }) + : html`No currencies found for ${term}`; + }; } declare global { diff --git a/src/components/ha-date-range-picker.ts b/src/components/ha-date-range-picker.ts index 7b3d609bc2..edf9f018ca 100644 --- a/src/components/ha-date-range-picker.ts +++ b/src/components/ha-date-range-picker.ts @@ -65,6 +65,10 @@ export class HaDateRangePicker extends LitElement { @property({ attribute: "time-picker", type: Boolean }) public timePicker = false; + public open(): void { + this._openPicker(); + } + @property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public minimal = false; @@ -306,6 +310,15 @@ export class HaDateRangePicker extends LitElement { return dateRangePicker.vueComponent.$children[0]; } + private _openPicker() { + if (!this._dateRangePicker.open) { + const datePicker = this.shadowRoot!.querySelector( + "date-range-picker div.date-range-inputs" + ) as any; + datePicker?.click(); + } + } + private _handleInputClick() { // close the date picker, so it will open again on the click event if (this._dateRangePicker.open) { diff --git a/src/components/ha-dialog-date-picker.ts b/src/components/ha-dialog-date-picker.ts index 54a8cdda0c..090046c7ff 100644 --- a/src/components/ha-dialog-date-picker.ts +++ b/src/components/ha-dialog-date-picker.ts @@ -7,8 +7,9 @@ import { nextRender } from "../common/util/render-status"; import { haStyleDialog } from "../resources/styles"; import type { HomeAssistant } from "../types"; import type { DatePickerDialogParams } from "./ha-date-input"; -import "./ha-dialog"; import "./ha-button"; +import "./ha-dialog-footer"; +import "./ha-wa-dialog"; @customElement("ha-dialog-date-picker") export class HaDialogDatePicker extends LitElement { @@ -22,6 +23,8 @@ export class HaDialogDatePicker extends LitElement { @state() private _params?: DatePickerDialogParams; + @state() private _open = false; + @state() private _value?: string; public async showDialog(params: DatePickerDialogParams): Promise { @@ -30,9 +33,14 @@ export class HaDialogDatePicker extends LitElement { await nextRender(); this._params = params; this._value = params.value; + this._open = true; } public closeDialog() { + this._open = false; + } + + private _dialogClosed() { this._params = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -41,7 +49,13 @@ export class HaDialogDatePicker extends LitElement { if (!this._params) { return nothing; } - return html` + return html` - ${this._params.canClear - ? html` - ${this.hass.localize("ui.dialogs.date-picker.clear")} - ` - : nothing} - - ${this.hass.localize("ui.dialogs.date-picker.today")} - - - ${this.hass.localize("ui.common.cancel")} - - - ${this.hass.localize("ui.common.ok")} - - `; + +
+ ${this._params.canClear + ? html` + ${this.hass.localize("ui.dialogs.date-picker.clear")} + ` + : nothing} + + ${this.hass.localize("ui.dialogs.date-picker.today")} + +
+ + + + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize("ui.common.ok")} + + + `; } private _valueChanged(ev: CustomEvent) { @@ -108,11 +127,20 @@ export class HaDialogDatePicker extends LitElement { static styles = [ haStyleDialog, css` - ha-dialog { + ha-wa-dialog { --dialog-content-padding: 0; - --justify-action-buttons: space-between; + } + .bottom-actions { + display: flex; + gap: var(--ha-space-4); + justify-content: center; + align-items: center; + width: 100%; + margin-bottom: var(--ha-space-1); } app-datepicker { + display: block; + margin-inline: auto; --app-datepicker-accent-color: var(--primary-color); --app-datepicker-bg-color: transparent; --app-datepicker-color: var(--primary-text-color); @@ -129,11 +157,6 @@ export class HaDialogDatePicker extends LitElement { app-datepicker::part(body) { direction: ltr; } - @media all and (min-width: 450px) { - ha-dialog { - --mdc-dialog-min-width: 300px; - } - } @media all and (max-width: 450px), all and (max-height: 500px) { app-datepicker { width: 100%; diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index a1db1c8b8f..ca8a00c5e7 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -76,6 +76,18 @@ export class HaDialog extends DialogBase { var(--divider-color) ); z-index: var(--dialog-z-index, 8); + --mdc-dialog-box-shadow: var(--dialog-box-shadow, none); + --mdc-typography-headline6-font-weight: var(--ha-font-weight-normal); + --mdc-typography-headline6-font-size: 1.574rem; + } + .mdc-dialog::before { + content: ""; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + pointer-events: none; -webkit-backdrop-filter: var( --ha-dialog-scrim-backdrop-filter, var(--dialog-backdrop-filter, none) @@ -84,9 +96,9 @@ export class HaDialog extends DialogBase { --ha-dialog-scrim-backdrop-filter, var(--dialog-backdrop-filter, none) ); - --mdc-dialog-box-shadow: var(--dialog-box-shadow, none); - --mdc-typography-headline6-font-weight: var(--ha-font-weight-normal); - --mdc-typography-headline6-font-size: 1.574rem; + } + .mdc-dialog .mdc-dialog__scrim { + background-color: var(--mdc-dialog-scrim-color, none); } .mdc-dialog__actions { justify-content: var(--justify-action-buttons, flex-end); diff --git a/src/components/ha-drawer.ts b/src/components/ha-drawer.ts index 3e563cd60b..72b69ca8b5 100644 --- a/src/components/ha-drawer.ts +++ b/src/components/ha-drawer.ts @@ -4,6 +4,18 @@ import type { PropertyValues } from "lit"; import { css } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; +import type { HASSDomEvent } from "../common/dom/fire_event"; + +declare global { + interface HASSDomEvents { + "hass-layout-transition": { active: boolean; reason?: string }; + } + interface HTMLElementEventMap { + "hass-layout-transition": HASSDomEvent< + HASSDomEvents["hass-layout-transition"] + >; + } +} const blockingElements = (document as any).$blockingElements; @@ -15,6 +27,30 @@ export class HaDrawer extends DrawerBase { private _rtlStyle?: HTMLElement; + private _sidebarTransitionActive = false; + + private _handleDrawerTransitionStart = (ev: TransitionEvent) => { + if (ev.propertyName !== "width" || this._sidebarTransitionActive) { + return; + } + this._sidebarTransitionActive = true; + fireEvent(window, "hass-layout-transition", { + active: true, + reason: "sidebar", + }); + }; + + private _handleDrawerTransitionEnd = (ev: TransitionEvent) => { + if (ev.propertyName !== "width" || !this._sidebarTransitionActive) { + return; + } + this._sidebarTransitionActive = false; + fireEvent(window, "hass-layout-transition", { + active: false, + reason: "sidebar", + }); + }; + protected createAdapter() { return { ...super.createAdapter(), @@ -63,6 +99,38 @@ export class HaDrawer extends DrawerBase { } } + protected firstUpdated() { + super.firstUpdated(); + this.mdcRoot?.addEventListener( + "transitionstart", + this._handleDrawerTransitionStart + ); + this.mdcRoot?.addEventListener( + "transitionend", + this._handleDrawerTransitionEnd + ); + this.mdcRoot?.addEventListener( + "transitioncancel", + this._handleDrawerTransitionEnd + ); + } + + public disconnectedCallback() { + super.disconnectedCallback(); + this.mdcRoot?.removeEventListener( + "transitionstart", + this._handleDrawerTransitionStart + ); + this.mdcRoot?.removeEventListener( + "transitionend", + this._handleDrawerTransitionEnd + ); + this.mdcRoot?.removeEventListener( + "transitioncancel", + this._handleDrawerTransitionEnd + ); + } + private async _setupSwipe() { const hammer = await import("../resources/hammer"); this._mc = new hammer.Manager(document, { @@ -90,6 +158,16 @@ export class HaDrawer extends DrawerBase { border-color: var(--divider-color, rgba(0, 0, 0, 0.12)); inset-inline-start: 0 !important; inset-inline-end: initial !important; + transition-property: transform, width; + transition-duration: + var(--mdc-drawer-transition-duration, 0.2s), + var(--ha-animation-duration-normal); + transition-timing-function: + var( + --mdc-drawer-transition-timing-function, + cubic-bezier(0.4, 0, 0.2, 1) + ), + ease; } .mdc-drawer.mdc-drawer--modal.mdc-drawer--open { z-index: 200; @@ -103,6 +181,15 @@ export class HaDrawer extends DrawerBase { direction: var(--direction); width: 100%; box-sizing: border-box; + transition: + padding-left var(--ha-animation-duration-normal) ease, + padding-inline-start var(--ha-animation-duration-normal) ease; + } + @media (prefers-reduced-motion: reduce) { + .mdc-drawer, + .mdc-drawer-app-content { + transition: none; + } } `, ]; diff --git a/src/components/ha-dropdown-item.ts b/src/components/ha-dropdown-item.ts index 75c9457742..46c906ce41 100644 --- a/src/components/ha-dropdown-item.ts +++ b/src/components/ha-dropdown-item.ts @@ -1,9 +1,9 @@ import DropdownItem from "@home-assistant/webawesome/dist/components/dropdown-item/dropdown-item"; import "@home-assistant/webawesome/dist/components/icon/icon"; -import { css, type CSSResultGroup, html } from "lit"; -import { customElement } from "lit/decorators"; -import "./ha-svg-icon"; import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js"; +import { css, type CSSResultGroup, html } from "lit"; +import { customElement, property } from "lit/decorators"; +import "./ha-svg-icon"; /** * Home Assistant dropdown item component @@ -17,6 +17,8 @@ import { mdiCheckboxBlankOutline, mdiCheckboxMarked } from "@mdi/js"; */ @customElement("ha-dropdown-item") export class HaDropdownItem extends DropdownItem { + @property({ type: Boolean, reflect: true }) selected = false; + protected renderCheckboxIcon() { return html` = CustomEvent<{ + item: Omit & { value: T }; +}>; /** * Home Assistant dropdown component @@ -13,11 +23,37 @@ import { customElement, property } from "lit/decorators"; * */ @customElement("ha-dropdown") +// @ts-ignore Allow to set an alternative anchor element export class HaDropdown extends Dropdown { @property({ attribute: false }) dropdownTag = "ha-dropdown"; @property({ attribute: false }) dropdownItemTag = "ha-dropdown-item"; + public get anchorElement(): HTMLButtonElement | WaButton | undefined { + // @ts-ignore Allow to set an anchor element on popup + return this.popup?.anchor as HTMLButtonElement | WaButton | undefined; + } + + public set anchorElement(element: HTMLButtonElement | WaButton | undefined) { + // @ts-ignore Allow to get the current anchor element from popup + if (!this.popup) { + return; + } + // @ts-ignore Allow to get the current anchor element from popup + this.popup.anchor = element; + } + + /** Get the slotted trigger button, a or
` @@ -531,6 +579,16 @@ class MoreInfoMediaPlayer extends LitElement { margin-left: var(--ha-space-2); } + .volume-slider-container { + width: 100%; + } + + @media (pointer: coarse) { + .volume-slider { + pointer-events: none; + } + } + .volume ha-svg-icon { padding: var(--ha-space-1); height: 16px; @@ -568,6 +626,10 @@ class MoreInfoMediaPlayer extends LitElement { color: var(--secondary-text-color); } + .position-time { + margin-top: var(--ha-space-2); + } + .media-info-row { display: flex; flex-direction: column; @@ -622,6 +684,39 @@ class MoreInfoMediaPlayer extends LitElement { ); } + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if (changedProps.has("stateObj")) { + this._syncProgressInterval(); + } + } + + private _syncProgressInterval(): void { + if (this._shouldUpdateProgress()) { + this._progressInterval = startMediaProgressInterval( + this._progressInterval, + () => this.requestUpdate() + ); + return; + } + this._clearProgressInterval(); + } + + private _clearProgressInterval(): void { + this._progressInterval = stopMediaProgressInterval(this._progressInterval); + } + + private _shouldUpdateProgress(): boolean { + const stateObj = this.stateObj; + return ( + !!stateObj && + stateObj.state === "playing" && + Number(stateObj.attributes.media_duration) > 0 && + "media_position" in stateObj.attributes && + "media_position_updated_at" in stateObj.attributes + ); + } + private _toggleMute() { this.hass!.callService("media_player", "volume_mute", { entity_id: this.stateObj!.entity_id, @@ -629,15 +724,15 @@ class MoreInfoMediaPlayer extends LitElement { }); } - private _selectedValueChanged(e: Event): void { + private _setVolume(value: number) { this.hass!.callService("media_player", "volume_set", { entity_id: this.stateObj!.entity_id, - volume_level: (e.target as any).value / 100, + volume_level: value / 100, }); } - private _handleSourceClick(e: Event) { - const source = (e.currentTarget as HTMLElement).getAttribute("data-source"); + private _handleSourceChange(e: HaDropdownSelectEvent) { + const source = e.detail.item.value; if (!source || this.stateObj!.attributes.source === source) { return; } @@ -648,10 +743,8 @@ class MoreInfoMediaPlayer extends LitElement { }); } - private _handleSoundModeClick(e: Event) { - const soundMode = (e.currentTarget as HTMLElement).getAttribute( - "data-sound-mode" - ); + private _handleSoundModeChange(ev: HaDropdownSelectEvent) { + const soundMode = ev.detail.item.value; if (!soundMode || this.stateObj!.attributes.sound_mode === soundMode) { return; } diff --git a/src/dialogs/more-info/controls/more-info-person.ts b/src/dialogs/more-info/controls/more-info-person.ts index 826d2a4a9c..4191993b01 100644 --- a/src/dialogs/more-info/controls/more-info-person.ts +++ b/src/dialogs/more-info/controls/more-info-person.ts @@ -3,7 +3,6 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; -import "../../../components/ha-attributes"; import "../../../components/ha-button"; import "../../../components/map/ha-map"; import { showZoneEditor } from "../../../data/zone"; @@ -50,11 +49,6 @@ class MoreInfoPerson extends LitElement { ` : ""} - `; } diff --git a/src/dialogs/more-info/controls/more-info-remote.ts b/src/dialogs/more-info/controls/more-info-remote.ts index 0990ffd5c3..ed56e053fc 100644 --- a/src/dialogs/more-info/controls/more-info-remote.ts +++ b/src/dialogs/more-info/controls/more-info-remote.ts @@ -1,15 +1,11 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; import { supportsFeature } from "../../../common/entity/supports-feature"; -import "../../../components/ha-attributes"; +import type { HaSelectSelectEvent } from "../../../components/ha-select"; +import "../../../components/ha-select"; import type { RemoteEntity } from "../../../data/remote"; import { REMOTE_SUPPORT_ACTIVITY } from "../../../data/remote"; import type { HomeAssistant } from "../../../types"; -import "../../../components/ha-select"; -import "../../../components/ha-list-item"; - -const filterExtraAttributes = "activity_list,current_activity"; @customElement("more-info-remote") class MoreInfoRemote extends LitElement { @@ -33,36 +29,24 @@ class MoreInfoRemote extends LitElement { )} .value=${stateObj.attributes.current_activity || ""} @selected=${this._handleActivityChanged} - fixedMenuPosition - naturalMenuWidth - @closed=${stopPropagation} + .options=${stateObj.attributes.activity_list?.map((activity) => ({ + value: activity, + label: this.hass!.formatEntityAttributeValue( + stateObj, + "activity", + activity + ), + }))} > - ${stateObj.attributes.activity_list?.map( - (activity) => html` - - ${this.hass.formatEntityAttributeValue( - stateObj, - "activity", - activity - )} - - ` - )} ` : nothing} - - `; } - private _handleActivityChanged(ev) { + private _handleActivityChanged(ev: HaSelectSelectEvent) { const oldVal = this.stateObj!.attributes.current_activity; - const newVal = ev.target.value; + const newVal = ev.detail.value; if (!newVal || oldVal === newVal) { return; diff --git a/src/dialogs/more-info/controls/more-info-siren.ts b/src/dialogs/more-info/controls/more-info-siren.ts index c5edf272e4..2737f2b50e 100644 --- a/src/dialogs/more-info/controls/more-info-siren.ts +++ b/src/dialogs/more-info/controls/more-info-siren.ts @@ -3,7 +3,6 @@ import type { HassEntity } from "home-assistant-js-websocket"; import type { CSSResultGroup } from "lit"; import { LitElement, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; -import "../../../components/ha-attributes"; import "../../../state-control/ha-state-control-toggle"; import "../../../components/ha-button"; import type { HomeAssistant } from "../../../types"; @@ -60,10 +59,6 @@ class MoreInfoSiren extends LitElement {
` : nothing} - `; } diff --git a/src/dialogs/more-info/controls/more-info-switch.ts b/src/dialogs/more-info/controls/more-info-switch.ts index 9eb677643e..e51c30a76e 100644 --- a/src/dialogs/more-info/controls/more-info-switch.ts +++ b/src/dialogs/more-info/controls/more-info-switch.ts @@ -3,7 +3,6 @@ import type { HassEntity } from "home-assistant-js-websocket"; import type { CSSResultGroup } from "lit"; import { LitElement, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; -import "../../../components/ha-attributes"; import "../../../state-control/ha-state-control-toggle"; import type { HomeAssistant } from "../../../types"; import "../components/ha-more-info-state-header"; @@ -33,10 +32,6 @@ class MoreInfoSwitch extends LitElement { .iconPathOff=${mdiPowerOff} > - `; } diff --git a/src/dialogs/more-info/controls/more-info-time.ts b/src/dialogs/more-info/controls/more-info-time.ts index f60512f32f..9157cf06fb 100644 --- a/src/dialogs/more-info/controls/more-info-time.ts +++ b/src/dialogs/more-info/controls/more-info-time.ts @@ -5,7 +5,7 @@ import "../../../components/ha-date-input"; import "../../../components/ha-time-input"; import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity"; import { setTimeValue } from "../../../data/time"; -import type { HomeAssistant } from "../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../types"; @customElement("more-info-time") class MoreInfoTime extends LitElement { @@ -35,7 +35,7 @@ class MoreInfoTime extends LitElement { ev.stopPropagation(); } - private _timeChanged(ev: CustomEvent<{ value: string }>): void { + private _timeChanged(ev: ValueChangedEvent): void { if (ev.detail.value) { setTimeValue(this.hass!, this.stateObj!.entity_id, ev.detail.value); } diff --git a/src/dialogs/more-info/controls/more-info-timer.ts b/src/dialogs/more-info/controls/more-info-timer.ts index 3c92fd82dc..29f31dfc0c 100644 --- a/src/dialogs/more-info/controls/more-info-timer.ts +++ b/src/dialogs/more-info/controls/more-info-timer.ts @@ -1,6 +1,5 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; -import "../../../components/ha-attributes"; import "../../../components/ha-button"; import type { TimerEntity } from "../../../data/timer"; import type { HomeAssistant } from "../../../types"; @@ -63,11 +62,6 @@ class MoreInfoTimer extends LitElement { ` : ""} - `; } diff --git a/src/dialogs/more-info/controls/more-info-update.ts b/src/dialogs/more-info/controls/more-info-update.ts index f537708f5b..bdb1596b3e 100644 --- a/src/dialogs/more-info/controls/more-info-update.ts +++ b/src/dialogs/more-info/controls/more-info-update.ts @@ -150,16 +150,16 @@ class MoreInfoUpdate extends LitElement { }; } - // Addon backup + // App backup if (updateType === "addon") { const version = this.stateObj.attributes.installed_version; return { title: this.hass.localize( - "ui.dialogs.more_info_control.update.create_backup.addon" + "ui.dialogs.more_info_control.update.create_backup.app" ), description: version ? this.hass.localize( - "ui.dialogs.more_info_control.update.create_backup.addon_description", + "ui.dialogs.more_info_control.update.create_backup.app_description", { version: version } ) : undefined, diff --git a/src/dialogs/more-info/controls/more-info-vacuum.ts b/src/dialogs/more-info/controls/more-info-vacuum.ts index aec1c6f5b0..33efc4745a 100644 --- a/src/dialogs/more-info/controls/more-info-vacuum.ts +++ b/src/dialogs/more-info/controls/more-info-vacuum.ts @@ -11,14 +11,12 @@ import { import { LitElement, css, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/entity/ha-battery-icon"; -import "../../../components/ha-attributes"; +import type { HaSelectSelectEvent } from "../../../components/ha-select"; import "../../../components/ha-icon"; import "../../../components/ha-icon-button"; -import "../../../components/ha-list-item"; import "../../../components/ha-select"; import { UNAVAILABLE } from "../../../data/entity/entity"; import type { EntityRegistryDisplayEntry } from "../../../data/entity/entity_registry"; @@ -110,9 +108,6 @@ class MoreInfoVacuum extends LitElement { const stateObj = this.stateObj; - const filterExtraAttributes = - "fan_speed,fan_speed_list,status,battery_level,battery_icon"; - return html` ${stateObj.state !== UNAVAILABLE ? html`
@@ -176,21 +171,17 @@ class MoreInfoVacuum extends LitElement { .disabled=${stateObj.state === UNAVAILABLE} .value=${stateObj.attributes.fan_speed} @selected=${this._handleFanSpeedChanged} - fixedMenuPosition - naturalMenuWidth - @closed=${stopPropagation} - > - ${stateObj.attributes.fan_speed_list!.map( - (mode) => html` - - ${this.hass.formatEntityAttributeValue( - stateObj, - "fan_speed", - mode - )} - - ` + .options=${stateObj.attributes.fan_speed_list!.map( + (mode) => ({ + value: mode, + label: this.hass!.formatEntityAttributeValue( + stateObj, + "fan_speed", + mode + ), + }) )} + >
` : ""} - - `; } @@ -301,9 +286,9 @@ class MoreInfoVacuum extends LitElement { }); } - private _handleFanSpeedChanged(ev) { + private _handleFanSpeedChanged(ev: HaSelectSelectEvent) { const oldVal = this.stateObj!.attributes.fan_speed; - const newVal = ev.target.value; + const newVal = ev.detail.value; if (!newVal || oldVal === newVal) { return; diff --git a/src/dialogs/more-info/controls/more-info-valve.ts b/src/dialogs/more-info/controls/more-info-valve.ts index 873a00efe5..49684b57f7 100644 --- a/src/dialogs/more-info/controls/more-info-valve.ts +++ b/src/dialogs/more-info/controls/more-info-valve.ts @@ -3,7 +3,6 @@ import type { CSSResultGroup, PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { supportsFeature } from "../../../common/entity/supports-feature"; -import "../../../components/ha-attributes"; import "../../../components/ha-icon-button-group"; import "../../../components/ha-icon-button-toggle"; import type { ValveEntity } from "../../../data/valve"; @@ -155,11 +154,6 @@ class MoreInfoValve extends LitElement { }
- `; } diff --git a/src/dialogs/more-info/controls/more-info-water_heater.ts b/src/dialogs/more-info/controls/more-info-water_heater.ts index f2c4fc6c96..d9d1a0543e 100644 --- a/src/dialogs/more-info/controls/more-info-water_heater.ts +++ b/src/dialogs/more-info/controls/more-info-water_heater.ts @@ -2,16 +2,16 @@ import { mdiAccount, mdiAccountArrowRight, mdiWaterBoiler } from "@mdi/js"; import type { CSSResultGroup } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; import { supportsFeature } from "../../../common/entity/supports-feature"; +import "../../../components/ha-attribute-icon"; import "../../../components/ha-control-select-menu"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; import "../../../components/ha-list-item"; import { UNAVAILABLE } from "../../../data/entity/entity"; import type { WaterHeaterEntity } from "../../../data/water_heater"; import { WaterHeaterEntityFeature, compareWaterHeaterOperationMode, - computeOperationModeIcon, } from "../../../data/water_heater"; import "../../../state-control/water_heater/ha-state-control-water_heater-temperature"; import type { HomeAssistant } from "../../../types"; @@ -74,29 +74,25 @@ class MoreInfoWaterHeater extends LitElement { ${supportOperationMode && stateObj.attributes.operation_list ? html` - - ${stateObj.attributes.operation_list + @wa-select=${this._handleOperationModeChanged} + .options=${stateObj.attributes.operation_list .concat() .sort(compareWaterHeaterOperationMode) - .map( - (mode) => html` - - - ${this.hass.formatEntityState(stateObj, mode)} - - ` - )} + .map((mode) => ({ + value: mode, + label: this.hass.formatEntityState(stateObj, mode), + attributeIcon: { + stateObj, + attribute: "operation_mode", + attributeValue: mode, + }, + }))} + > + ` : nothing} @@ -109,31 +105,18 @@ class MoreInfoWaterHeater extends LitElement { )} .value=${stateObj.attributes.away_mode} .disabled=${stateObj.state === UNAVAILABLE} - fixedMenuPosition - naturalMenuWidth - @selected=${this._handleAwayModeChanged} - @closed=${stopPropagation} + @wa-select=${this._handleAwayModeChanged} + .options=${["on", "off"].map((mode) => ({ + value: mode, + label: this.hass.formatEntityAttributeValue( + stateObj, + "away_mode", + mode + ), + iconPath: mode === "on" ? mdiAccountArrowRight : mdiAccount, + }))} > - - - ${this.hass.formatEntityAttributeValue( - stateObj, - "away_mode", - "on" - )} - - - - ${this.hass.formatEntityAttributeValue( - stateObj, - "away_mode", - "off" - )} - ` : nothing} @@ -141,8 +124,8 @@ class MoreInfoWaterHeater extends LitElement { `; } - private _handleOperationModeChanged(ev) { - const newVal = ev.target.value; + private _handleOperationModeChanged(ev: HaDropdownSelectEvent) { + const newVal = ev.detail.item.value; this._callServiceHelper( this.stateObj!.state, newVal, @@ -153,8 +136,8 @@ class MoreInfoWaterHeater extends LitElement { ); } - private _handleAwayModeChanged(ev) { - const newVal = ev.target.value === "on"; + private _handleAwayModeChanged(ev: HaDropdownSelectEvent) { + const newVal = ev.detail.item.value === "on"; const oldVal = this.stateObj!.attributes.away_mode === "on"; this._callServiceHelper(oldVal, newVal, "set_away_mode", { diff --git a/src/dialogs/more-info/controls/more-info-weather.ts b/src/dialogs/more-info/controls/more-info-weather.ts index ebf0b91f4b..3116a4274e 100644 --- a/src/dialogs/more-info/controls/more-info-weather.ts +++ b/src/dialogs/more-info/controls/more-info-weather.ts @@ -313,113 +313,119 @@ class MoreInfoWeather extends LitElement { ` : nothing} - -
- ${this.hass.localize("ui.card.weather.forecast")}: -
- ${supportedForecasts?.length > 1 - ? html` - ${supportedForecasts.map( - (forecastType) => - html` + ${this.hass.localize("ui.card.weather.forecast")}: + + ${supportedForecasts?.length > 1 + ? html` - ${this.hass!.localize(`ui.card.weather.${forecastType}`)} - ` - )} - ` - : nothing} -
- ${forecast?.length - ? this._groupForecastByDay(forecast).map((dayForecast) => { - const showDayHeader = hourly || dayNight; - return html` -
- ${showDayHeader - ? html`
- ${formatDateWeekdayShort( - new Date(dayForecast[0].datetime), - this.hass!.locale, - this.hass!.config + ${supportedForecasts.map( + (forecastType) => + html` + ${this.hass!.localize( + `ui.card.weather.${forecastType}` )} -
` - : nothing} -
- ${dayForecast.map((item) => - this._showValue(item.templow) || - this._showValue(item.temperature) - ? html` -
-
- ${hourly - ? formatTime( - new Date(item.datetime), - this.hass!.locale, - this.hass!.config - ) - : dayNight - ? html`
- ${item.is_daytime !== false - ? this.hass!.localize( - "ui.card.weather.day" - ) - : this.hass!.localize( - "ui.card.weather.night" + ` + )} + ` + : nothing} +
+ ${forecast?.length + ? this._groupForecastByDay(forecast).map((dayForecast) => { + const showDayHeader = hourly || dayNight; + return html` +
+ ${showDayHeader + ? html`
+ ${formatDateWeekdayShort( + new Date(dayForecast[0].datetime), + this.hass!.locale, + this.hass!.config + )} +
` + : nothing} +
+ ${dayForecast.map((item) => + this._showValue(item.templow) || + this._showValue(item.temperature) + ? html` +
+
+ ${hourly + ? formatTime( + new Date(item.datetime), + this.hass!.locale, + this.hass!.config + ) + : dayNight + ? html`
+ ${item.is_daytime !== false + ? this.hass!.localize( + "ui.card.weather.day" + ) + : this.hass!.localize( + "ui.card.weather.night" + )} +
` + : formatDateWeekdayShort( + new Date(item.datetime), + this.hass!.locale, + this.hass!.config )} -
` - : formatDateWeekdayShort( - new Date(item.datetime), - this.hass!.locale, - this.hass!.config - )} -
- ${this._showValue(item.condition) - ? html` -
- ${getWeatherStateIcon( - item.condition!, - this, - !( - item.is_daytime || - item.is_daytime === undefined - ) - )}
- ` - : nothing} -
- ${this._showValue(item.temperature) - ? html`${formatNumber( - item.temperature, - this.hass!.locale - )}°` - : "—"} -
-
- ${this._showValue(item.templow) - ? html`${formatNumber( - item.templow!, - this.hass!.locale - )}°` - : nothing} -
-
- ` - : nothing - )} -
-
- `; - }) - : html``} -
- + ${this._showValue(item.condition) + ? html` +
+ ${getWeatherStateIcon( + item.condition!, + this, + !( + item.is_daytime || + item.is_daytime === undefined + ) + )} +
+ ` + : nothing} +
+ ${this._showValue(item.temperature) + ? html`${formatNumber( + item.temperature, + this.hass!.locale + )}°` + : "—"} +
+
+ ${this._showValue(item.templow) + ? html`${formatNumber( + item.templow!, + this.hass!.locale + )}°` + : nothing} +
+
+ ` + : nothing + )} +
+
+ `; + }) + : html``} +
+ ` + : nothing} ${this.stateObj.attributes.attribution ? html`
diff --git a/src/dialogs/more-info/ha-more-info-attributes.ts b/src/dialogs/more-info/ha-more-info-attributes.ts new file mode 100644 index 0000000000..952eea45cc --- /dev/null +++ b/src/dialogs/more-info/ha-more-info-attributes.ts @@ -0,0 +1,152 @@ +import type { HassEntity } from "home-assistant-js-websocket"; +import type { CSSResultGroup, PropertyValues } from "lit"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { computeAttributeNameDisplay } from "../../common/entity/compute_attribute_display"; +import { computeStateDomain } from "../../common/entity/compute_state_domain"; +import "../../components/ha-attribute-value"; +import "../../components/ha-card"; +import { + STATE_ATTRIBUTES, + STATE_ATTRIBUTES_DOMAIN_CLASS, +} from "../../data/entity/entity_attributes"; +import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry"; +import type { HomeAssistant } from "../../types"; + +interface AttributesViewParams { + entityId: string; +} + +@customElement("ha-more-info-attributes") +class HaMoreInfoAttributes extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public entry?: ExtEntityRegistryEntry | null; + + @property({ attribute: false }) public params?: AttributesViewParams; + + @state() private _stateObj?: HassEntity; + + protected willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + if (changedProps.has("params") || changedProps.has("hass")) { + if (this.params?.entityId && this.hass) { + this._stateObj = this.hass.states[this.params.entityId]; + } + } + } + + private _computeDisplayAttributes(stateObj: HassEntity): string[] { + const domain = computeStateDomain(stateObj); + const filtersArray = STATE_ATTRIBUTES.concat( + (STATE_ATTRIBUTES_DOMAIN_CLASS[domain]?.[ + stateObj.attributes?.device_class + ] || []) as string[] + ); + return Object.keys(stateObj.attributes).filter( + (key) => filtersArray.indexOf(key) === -1 + ); + } + + protected render() { + if (!this.params || !this._stateObj) { + return nothing; + } + + const attributes = this._computeDisplayAttributes(this._stateObj); + + return html` +
+ +
+ ${attributes.map( + (attribute) => html` +
+
+ ${computeAttributeNameDisplay( + this.hass.localize, + this._stateObj!, + this.hass.entities, + attribute + )} +
+
+ +
+
+ ` + )} +
+
+ ${this._stateObj.attributes.attribution + ? html` +
+ ${this._stateObj.attributes.attribution} +
+ ` + : nothing} +
+ `; + } + + static styles: CSSResultGroup = css` + :host { + display: flex; + flex-direction: column; + flex: 1; + } + + .content { + padding: var(--ha-space-6); + padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-6)); + } + + ha-card { + direction: ltr; + } + + .card-content { + padding: var(--ha-space-2) var(--ha-space-4); + } + + .data-entry { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: var(--ha-space-2) 0; + border-bottom: 1px solid var(--divider-color); + } + + .data-entry:last-of-type { + border-bottom: none; + } + + .data-entry .value { + max-width: 60%; + overflow-wrap: break-word; + text-align: right; + } + + .key { + flex-grow: 1; + color: var(--secondary-text-color); + } + + .attribution { + color: var(--secondary-text-color); + text-align: center; + margin-top: var(--ha-space-4); + font-size: var(--ha-font-size-s); + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-more-info-attributes": HaMoreInfoAttributes; + } +} diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index 5b17615e01..81d5777a45 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -4,6 +4,7 @@ import { mdiCogOutline, mdiDevices, mdiDotsVertical, + mdiFormatListBulletedSquare, mdiInformationOutline, mdiPencil, mdiPencilOff, @@ -28,6 +29,7 @@ import { computeEntityEntryName, computeEntityName, } from "../../common/entity/compute_entity_name"; +import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { getEntityContext, getEntityEntryContext, @@ -35,13 +37,19 @@ import { import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event"; import { navigate } from "../../common/navigate"; import { computeRTL } from "../../common/util/compute_rtl"; -import "../../components/ha-button-menu"; +import { withViewTransition } from "../../common/util/view-transition"; import "../../components/ha-dialog"; import "../../components/ha-dialog-header"; +import "../../components/ha-dropdown"; +import type { HaDropdownSelectEvent } from "../../components/ha-dropdown"; +import "../../components/ha-dropdown-item"; import "../../components/ha-icon-button"; import "../../components/ha-icon-button-prev"; -import "../../components/ha-list-item"; import "../../components/ha-related-items"; +import { + STATE_ATTRIBUTES, + STATE_ATTRIBUTES_DOMAIN_CLASS, +} from "../../data/entity/entity_attributes"; import type { EntityRegistryEntry, ExtEntityRegistryEntry, @@ -88,6 +96,7 @@ interface ChildView { viewTitle?: string; viewImport?: () => Promise; viewParams?: any; + keepHeader?: boolean; } declare global { @@ -272,18 +281,14 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { this._childView = view; } - private _goToDevice(ev): void { - if (!shouldHandleRequestSelectedEvent(ev)) return; + private _goToDevice(): void { const deviceId = this._getDeviceId(); - if (!deviceId) return; - navigate(`/config/devices/device/${deviceId}`); this.closeDialog(); } - private _goToEdit(ev) { - if (!shouldHandleRequestSelectedEvent(ev)) return; + private _goToEdit() { const stateObj = this.hass.states[this._entityId!]; const domain = computeDomain(this._entityId!); let idToPassThroughUrl = stateObj.entity_id; @@ -301,8 +306,7 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { this.closeDialog(); } - private _toggleInfoEditMode(ev) { - if (!shouldHandleRequestSelectedEvent(ev)) return; + private _toggleInfoEditMode() { this._infoEditMode = !this._infoEditMode; } @@ -310,11 +314,66 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { this._infoEditMode = ev.detail; } - private _goToRelated(ev): void { - if (!shouldHandleRequestSelectedEvent(ev)) return; + private _goToRelated(): void { this._setView("related"); } + private _handleMenuAction(ev: HaDropdownSelectEvent) { + const action = ev.detail?.item?.value; + switch (action) { + case "device": + this._goToDevice(); + break; + case "edit": + this._goToEdit(); + break; + case "toggle_edit": + this._toggleInfoEditMode(); + break; + case "related": + this._goToRelated(); + break; + case "add_to": + this._setView("add_to"); + break; + case "info": + this._resetInitialView(); + break; + case "attributes": + this._showAttributes(); + break; + } + } + + private _showAttributes(): void { + import("./ha-more-info-attributes"); + this._childView = { + viewTag: "ha-more-info-attributes", + viewParams: { entityId: this._entityId }, + keepHeader: true, + }; + } + + private _hasDisplayableAttributes(): boolean { + if (!this._entityId) { + return false; + } + const stateObj = this.hass.states[this._entityId]; + if (!stateObj) { + return false; + } + const domain = computeStateDomain(stateObj); + const filtersArray = STATE_ATTRIBUTES.concat( + (STATE_ATTRIBUTES_DOMAIN_CLASS[domain]?.[ + stateObj.attributes?.device_class + ] || []) as string[] + ); + const displayAttributes = Object.keys(stateObj.attributes).filter( + (key) => filtersArray.indexOf(key) === -1 + ); + return displayAttributes.length > 0; + } + private _goToAddEntityTo(ev) { // Only check for request-selected events (from menu items), not regular clicks (from icon button) if (ev.type === "request-selected" && !shouldHandleRequestSelectedEvent(ev)) @@ -347,12 +406,17 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { const deviceType = (deviceId && this.hass.devices[deviceId].entry_type) || "device"; - const isDefaultView = this._currView === DEFAULT_VIEW && !this._childView; + const isDefaultView = + this._currView === DEFAULT_VIEW && + (!this._childView || this._childView.keepHeader); const isSpecificInitialView = - this._initialView !== DEFAULT_VIEW && !this._childView; + this._initialView !== DEFAULT_VIEW && + (!this._childView || this._childView.keepHeader); const showCloseIcon = - (isDefaultView && this._parentEntityIds.length === 0) || - isSpecificInitialView; + (isDefaultView && + this._parentEntityIds.length === 0 && + !this._childView) || + (isSpecificInitialView && !this._childView); const context = stateObj ? getEntityContext( @@ -386,7 +450,12 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { const breadcrumb = [areaName, deviceName, entityName].filter( (v): v is string => Boolean(v) ); - const title = this._childView?.viewTitle || breadcrumb.pop() || entityId; + const title = + (this._childView && !this._childView.keepHeader + ? this._childView.viewTitle + : undefined) || + breadcrumb.pop() || + entityId; const isRTL = computeRTL(this.hass); @@ -459,12 +528,11 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { .path=${mdiCogOutline} @click=${this._goToSettings} > - + + ${this.hass.localize( "ui.dialogs.more_info_control.device_or_service_info", { @@ -486,29 +557,20 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { ), } )} - - + ` : nothing} ${this._shouldShowEditIcon(domain, stateObj) ? html` - + + ${this.hass.localize( "ui.dialogs.more_info_control.edit" )} - - + ` : nothing} ${this._entry && @@ -516,10 +578,13 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { domain === "light" && lightSupportsFavoriteColors(stateObj) ? html` - + + ${this._infoEditMode ? this.hass.localize( `ui.dialogs.more_info_control.exit_edit_mode` @@ -527,44 +592,45 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { : this.hass.localize( `ui.dialogs.more_info_control.${domain}.edit_mode` )} - - + ` : nothing} - + + ${this.hass.localize( "ui.dialogs.more_info_control.related" )} - - + + ${this._hasDisplayableAttributes() + ? html` + + + ${this.hass.localize( + "ui.dialogs.more_info_control.attributes" + )} + + ` + : nothing} ${this._shouldShowAddEntityTo() ? html` - + + ${this.hass.localize( "ui.dialogs.more_info_control.add_entity_to" )} - - + ` : nothing} - + ` : !__DEMO__ && this._shouldShowAddEntityTo() ? html` @@ -581,12 +647,11 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { ` : isSpecificInitialView ? html` - - + + + ${this.hass.localize("ui.dialogs.more_info_control.info")} - - - + + ` : nothing} @@ -711,7 +771,9 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) { } private _enlarge() { - this.large = !this.large; + withViewTransition(() => { + this.large = !this.large; + }); } private _handleOpened() { diff --git a/src/dialogs/more-info/more-info-content.ts b/src/dialogs/more-info/more-info-content.ts index c2297a0607..b1a45f1349 100644 --- a/src/dialogs/more-info/more-info-content.ts +++ b/src/dialogs/more-info/more-info-content.ts @@ -3,7 +3,10 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; import { dynamicElement } from "../../common/dom/dynamic-element-directive"; +import { computeEntityName } from "../../common/entity/compute_entity_name"; +import type { EntityNameItem } from "../../common/entity/compute_entity_name_display"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; +import { getEntityContext } from "../../common/entity/context/get_entity_context"; import "../../components/ha-badge"; import type { ExtEntityRegistryEntry } from "../../data/entity/entity_registry"; import { supportsCoverPositionCardFeature } from "../../panels/lovelace/card-features/hui-cover-position-card-feature"; @@ -15,6 +18,12 @@ import "../../panels/lovelace/sections/hui-section"; import type { HomeAssistant } from "../../types"; import { stateMoreInfoType } from "./state_more_info_control"; +interface EntityInfo { + entityId: string; + entityName: string | undefined; + areaId: string | undefined; +} + @customElement("more-info-content") class MoreInfoContent extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; @@ -83,32 +92,78 @@ class MoreInfoContent extends LitElement { } private _entitiesSectionConfig = memoizeOne((entityIds: string[]) => { - const cards = entityIds - .map((entityId) => { - const entity = this.hass!.entities[entityId]; - if (entity?.hidden) { + const hass = this.hass!; + + // Get entity names and areas for all visible entities + const entityInfos = entityIds + .map((entityId) => { + const entry = hass.entities[entityId]; + if (entry?.hidden) { return null; } - const features: LovelaceCardFeatureConfig[] = []; - const context = { entity_id: entityId }; - if (supportsCoverPositionCardFeature(this.hass!, context)) { - features.push({ - type: "cover-position", - }); - } else if (supportsLightBrightnessCardFeature(this.hass!, context)) { - features.push({ - type: "light-brightness", - }); + const stateObj = hass.states[entityId]; + if (!stateObj) { + return null; } - return { - type: "tile", - entity: entityId, - features_position: "inline", - features, - grid_options: { columns: 12 }, - } as TileCardConfig; + const entityName = entry + ? computeEntityName(stateObj, hass.entities, hass.devices) + : undefined; + const { area } = getEntityContext( + stateObj, + hass.entities, + hass.devices, + hass.areas, + hass.floors + ); + const areaId = area?.area_id; + return { entityId, entityName, areaId }; }) - .filter(Boolean); + .filter(Boolean) as EntityInfo[]; + + // Check if all entities have the same entity name + const entityNames = new Set(entityInfos.map((info) => info.entityName)); + const allSameEntityName = entityNames.size === 1; + + // Check if all entities have the same area + const areaIds = new Set(entityInfos.map((info) => info.areaId)); + const allSameArea = areaIds.size === 1; + + // Build name and state content config based on conditions + const name: EntityNameItem[] = [{ type: "device" }]; + + if (!allSameEntityName) { + name.push({ type: "entity" }); + } + + const stateContent = ["state"]; + if (!allSameArea) { + stateContent.push("area_name"); + } + + const cards = entityInfos.map(({ entityId }) => { + const features: LovelaceCardFeatureConfig[] = []; + const context = { entity_id: entityId }; + if (supportsCoverPositionCardFeature(hass, context)) { + features.push({ + type: "cover-position", + }); + } else if (supportsLightBrightnessCardFeature(hass, context)) { + features.push({ + type: "light-brightness", + }); + } + + return { + type: "tile", + entity: entityId, + name, + state_content: stateContent, + features_position: "inline", + features, + grid_options: { columns: 12 }, + } as TileCardConfig; + }); + return { type: "grid", cards, diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index 77298fe1b5..71b12b4bc7 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -1,8 +1,10 @@ import { mdiDevices } from "@mdi/js"; import Fuse from "fuse.js"; +import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; +import type { NavigationFilterOptions } from "../../common/config/filter_navigation_pages"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { fireEvent } from "../../common/dom/fire_event"; import { navigate } from "../../common/navigate"; @@ -15,6 +17,7 @@ import "../../components/ha-icon"; import "../../components/ha-picker-combo-box"; import type { HaPickerComboBox, + PickerComboBoxIndexSelectedDetail, PickerComboBoxItem, } from "../../components/ha-picker-combo-box"; import "../../components/ha-spinner"; @@ -44,12 +47,15 @@ import { type ActionCommandComboBoxItem, type NavigationComboBoxItem, } from "../../data/quick_bar"; +import type { RelatedResult } from "../../data/search"; import { multiTermSortedSearch, type FuseWeightedKey, } from "../../resources/fuseMultiTerm"; +import { buttonLinkStyle } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; import { isIosApp } from "../../util/is_ios"; +import { isMac } from "../../util/is_mac"; import { showConfirmationDialog } from "../generic/show-dialog-box"; import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog"; import type { QuickBarParams, QuickBarSection } from "./show-dialog-quick-bar"; @@ -64,12 +70,14 @@ export class QuickBar extends LitElement { @state() private _loading = true; - @state() private _hint?: string; + @state() private _showHint = false; @state() private _selectedSection?: QuickBarSection; @state() private _opened = false; + @state() private _relatedResult?: RelatedResult; + @query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox; private get _showEntityId() { @@ -80,8 +88,12 @@ export class QuickBar extends LitElement { private _addons?: HassioAddonInfo[]; + private _navigationFilterOptions: NavigationFilterOptions = {}; + private _translationsLoaded = false; + private _itemSelected = false; + // #region lifecycle public async showDialog(params: QuickBarParams) { if (!this._translationsLoaded) { @@ -90,7 +102,10 @@ export class QuickBar extends LitElement { } this._initialize(); this._selectedSection = params.mode; - this._hint = params.hint; + this._showHint = params.showHint ?? false; + + this._relatedResult = params.contextItem ? params.related : undefined; + this._open = true; } @@ -104,6 +119,12 @@ export class QuickBar extends LitElement { this._configEntryLookup = Object.fromEntries( configEntries.map((entry) => [entry.entry_id, entry]) ); + // Derive Bluetooth config entries status for navigation filtering + this._navigationFilterOptions = { + hasBluetoothConfigEntries: configEntries.some( + (entry) => entry.domain === "bluetooth" + ), + }; } catch (err) { // eslint-disable-next-line no-console console.error("Error fetching config entries for quick bar", err); @@ -152,15 +173,28 @@ export class QuickBar extends LitElement { this._selectedSection = undefined; this._opened = false; this._open = false; + this._itemSelected = false; fireEvent(this, "dialog-closed", { dialog: this.localName }); }; + // fallback in case the closed event is not fired + private _dialogCloseStarted = () => { + setTimeout( + () => { + if (this._opened) { + this._dialogClosed(); + } + }, + 350 // close animation timeout is 300ms + ); + }; + // #endregion lifecycle // #region render protected render() { - if (!this._open) { + if (!this._open && !this._opened) { return nothing; } @@ -210,6 +244,7 @@ export class QuickBar extends LitElement { hideActions @wa-show=${this._showTriggered} @wa-after-show=${this._dialogOpened} + @wa-hide=${this._dialogCloseStarted} @closed=${this._dialogClosed} > ${!this._loading && this._opened @@ -230,9 +265,17 @@ export class QuickBar extends LitElement { clearable >` : nothing} - ${this._hint + ${this._showHint ? html`${this._hint}${this.hass.localize("ui.tips.key_shortcut_quick_search", { + keyboard_shortcut: html``, + modifier: isMac ? "⌘" : "Ctrl", + })}` : nothing} @@ -281,6 +324,9 @@ export class QuickBar extends LitElement { slot="start" alt=${item.primary ?? "Unknown"} .src=${item.image} + style=${"iconColor" in item && item.iconColor + ? `background-color: ${item.iconColor}; padding: 4px; border-radius: var(--ha-border-radius-circle); width: 24px; height: 24px` + : ""} /> ` : item.icon @@ -377,6 +423,7 @@ export class QuickBar extends LitElement { this._selectedSection = section as QuickBarSection | undefined; return this._getItemsMemoized( this._configEntryLookup, + this._relatedResult, searchString, this._selectedSection ); @@ -385,15 +432,18 @@ export class QuickBar extends LitElement { private _getItemsMemoized = memoizeOne( ( configEntryLookup: Record, + relatedResult: RelatedResult | undefined, filter?: string, section?: QuickBarSection ) => { const items: (string | PickerComboBoxItem)[] = []; + const relatedIdSets = this._getRelatedIdSets(relatedResult); if (!section || section === "navigate") { let navigateItems = this._generateNavigationCommandsMemoized( this.hass, - this._addons + this._addons, + this._navigationFilterOptions ).sort(this._sortBySortingLabel); if (filter) { @@ -436,17 +486,29 @@ export class QuickBar extends LitElement { } if (!section || section === "entity") { - let entityItems = this._getEntitiesMemoized(this.hass).sort( - this._sortBySortingLabel - ); + let entityItems = this._getEntitiesMemoized(this.hass); + + // Mark related items + if (relatedIdSets.entities.size > 0) { + entityItems = entityItems.map((item) => ({ + ...item, + isRelated: relatedIdSets.entities.has( + (item as EntityComboBoxItem).stateObj?.entity_id || "" + ), + })); + } if (filter) { - entityItems = this._filterGroup( - "entity", - entityItems, - filter, - entityComboBoxKeys - ) as EntityComboBoxItem[]; + entityItems = this._sortRelatedFirst( + this._filterGroup( + "entity", + entityItems, + filter, + entityComboBoxKeys + ) as EntityComboBoxItem[] + ); + } else { + entityItems = this._sortRelatedByLabel(entityItems); } if (!section && entityItems.length) { @@ -463,15 +525,25 @@ export class QuickBar extends LitElement { let deviceItems = this._getDevicesMemoized( this.hass, configEntryLookup - ).sort(this._sortBySortingLabel); + ); + + // Mark related items + if (relatedIdSets.devices.size > 0) { + deviceItems = deviceItems.map((item) => { + const deviceId = item.id.split(SEPARATOR)[1]; + return { + ...item, + isRelated: relatedIdSets.devices.has(deviceId || ""), + }; + }); + } if (filter) { - deviceItems = this._filterGroup( - "device", - deviceItems, - filter, - deviceComboBoxKeys + deviceItems = this._sortRelatedFirst( + this._filterGroup("device", deviceItems, filter, deviceComboBoxKeys) ); + } else { + deviceItems = this._sortRelatedByLabel(deviceItems); } if (!section && deviceItems.length) { @@ -487,13 +559,23 @@ export class QuickBar extends LitElement { if (this.hass.user?.is_admin && (!section || section === "area")) { let areaItems = this._getAreasMemoized(this.hass); + // Mark related items + if (relatedIdSets.areas.size > 0) { + areaItems = areaItems.map((item) => { + const areaId = item.id.split(SEPARATOR)[1]; + return { + ...item, + isRelated: relatedIdSets.areas.has(areaId || ""), + }; + }); + } + if (filter) { - areaItems = this._filterGroup( - "area", - areaItems, - filter, - areaComboBoxKeys + areaItems = this._sortRelatedFirst( + this._filterGroup("area", areaItems, filter, areaComboBoxKeys) ); + } else { + areaItems = this._sortRelatedByLabel(areaItems); } if (!section && areaItems.length) { @@ -510,6 +592,12 @@ export class QuickBar extends LitElement { } ); + private _getRelatedIdSets = memoizeOne((related?: RelatedResult) => ({ + entities: new Set(related?.entity || []), + devices: new Set(related?.device || []), + areas: new Set(related?.area || []), + })); + private _getEntitiesMemoized = memoizeOne((hass: HomeAssistant) => getEntities( hass, @@ -559,7 +647,11 @@ export class QuickBar extends LitElement { ); private _generateNavigationCommandsMemoized = memoizeOne( - generateNavigationCommands + ( + hass: HomeAssistant, + apps: HassioAddonInfo[] | undefined, + filterOptions: NavigationFilterOptions + ) => generateNavigationCommands(hass, apps, filterOptions) ); private _generateActionCommandsMemoized = memoizeOne(generateActionCommands); @@ -609,17 +701,50 @@ export class QuickBar extends LitElement { this.hass.locale.language ); + private _sortRelatedByLabel = (items: PickerComboBoxItem[]) => + [...items].sort((a, b) => { + if (a.isRelated && !b.isRelated) return -1; + if (!a.isRelated && b.isRelated) return 1; + return this._sortBySortingLabel(a, b); + }); + + private _sortRelatedFirst = (items: PickerComboBoxItem[]) => + [...items].sort((a, b) => { + const aRelated = Boolean(a.isRelated); + const bRelated = Boolean(b.isRelated); + if (aRelated === bRelated) { + return 0; + } + return aRelated ? -1 : 1; + }); + // #endregion data // #region interaction - private async _handleItemSelected(ev: CustomEvent<{ index: number }>) { - if (this._comboBox && this._comboBox.virtualizerElement) { - const index = ev.detail.index; + private _navigate(path: string, newTab = false) { + if (newTab) { + window.open(path, "_blank", "noreferrer"); + } else { + navigate(path); + } + } + + private async _handleItemSelected( + ev: CustomEvent + ) { + if ( + !this._itemSelected && + this._comboBox && + this._comboBox.virtualizerElement + ) { + const { index, newTab } = ev.detail; const item = this._comboBox.virtualizerElement.items[ index ] as PickerComboBoxItem; + this._itemSelected = true; + // entity selected if (item && "stateObj" in item) { this.closeDialog(); @@ -631,15 +756,17 @@ export class QuickBar extends LitElement { // device selected if (item && item.id.startsWith(`device${SEPARATOR}`)) { + const path = `/config/devices/device/${item.id.split(SEPARATOR)[1]}`; this.closeDialog(); - navigate(`/config/devices/device/${item.id.split(SEPARATOR)[1]}`); + this._navigate(path, newTab); return; } // area selected if (item && item.id.startsWith(`area${SEPARATOR}`)) { + const path = `/config/areas/area/${item.id.split(SEPARATOR)[1]}`; this.closeDialog(); - navigate(`/config/areas/area/${item.id.split(SEPARATOR)[1]}`); + this._navigate(path, newTab); return; } @@ -693,53 +820,65 @@ export class QuickBar extends LitElement { return; } - navigate((item as NavigationComboBoxItem).path); + const path = (item as NavigationComboBoxItem).path; + this._navigate(path, newTab); } } } + private _openShortcutDialog(ev: Event): void { + ev.preventDefault(); + showShortcutsDialog(this); + this.closeDialog(); + } + // #endregion interaction // #region styles - static styles = css` - :host { - --dialog-surface-margin-top: var(--ha-space-10); - --ha-dialog-min-height: 620px; - --ha-bottom-sheet-height: calc( - 100vh - max(var(--safe-area-inset-top), 48px) - ); - --ha-bottom-sheet-height: calc( - 100dvh - max(var(--safe-area-inset-top), 48px) - ); - --ha-bottom-sheet-max-height: calc( - 100vh - max(var(--safe-area-inset-top), 48px) - ); - --ha-bottom-sheet-max-height: calc( - 100dvh - max(var(--safe-area-inset-top), 48px) - ); - --dialog-content-padding: 0; - --safe-area-inset-bottom: 0px; - } + static get styles(): CSSResultGroup { + return [ + buttonLinkStyle, + css` + :host { + --dialog-surface-margin-top: var(--ha-space-10); + --ha-dialog-min-height: 620px; + --ha-bottom-sheet-height: calc( + 100vh - max(var(--safe-area-inset-top), 48px) + ); + --ha-bottom-sheet-height: calc( + 100dvh - max(var(--safe-area-inset-top), 48px) + ); + --ha-bottom-sheet-max-height: calc( + 100vh - max(var(--safe-area-inset-top), 48px) + ); + --ha-bottom-sheet-max-height: calc( + 100dvh - max(var(--safe-area-inset-top), 48px) + ); + --dialog-content-padding: 0; + --safe-area-inset-bottom: 0px; + } - ha-tip { - display: flex; - justify-content: center; - align-items: center; - color: var(--secondary-text-color); - gap: var(--ha-space-1); - } + ha-tip { + display: flex; + justify-content: center; + align-items: center; + color: var(--secondary-text-color); + gap: var(--ha-space-1); + } - ha-tip a { - color: var(--primary-color); - } + ha-tip a { + color: var(--primary-color); + } - @media all and (max-width: 450px), all and (max-height: 690px) { - ha-tip { - display: none; - } - } - `; + @media all and (max-width: 450px), all and (max-height: 690px) { + ha-tip { + display: none; + } + } + `, + ]; + } // #endregion styles } diff --git a/src/dialogs/quick-bar/show-dialog-quick-bar.ts b/src/dialogs/quick-bar/show-dialog-quick-bar.ts index b83818ad9d..55036a57f9 100644 --- a/src/dialogs/quick-bar/show-dialog-quick-bar.ts +++ b/src/dialogs/quick-bar/show-dialog-quick-bar.ts @@ -1,4 +1,6 @@ import { fireEvent } from "../../common/dom/fire_event"; +import type { ItemType, RelatedResult } from "../../data/search"; +import { closeDialog } from "../make-dialog-manager"; export type QuickBarSection = | "entity" @@ -7,10 +9,17 @@ export type QuickBarSection = | "navigate" | "command"; +export interface QuickBarContextItem { + itemType: ItemType; + itemId: string; +} + export interface QuickBarParams { entityFilter?: string; mode?: QuickBarSection; - hint?: string; + showHint?: boolean; + contextItem?: QuickBarContextItem; + related?: RelatedResult; } export const loadQuickBar = () => import("./ha-quick-bar"); @@ -26,3 +35,7 @@ export const showQuickBar = ( addHistory: false, }); }; + +export const closeQuickBar = (): void => { + closeDialog("ha-quick-bar"); +}; diff --git a/src/dialogs/sidebar/dialog-edit-sidebar.ts b/src/dialogs/sidebar/dialog-edit-sidebar.ts index daba7c07ae..66593670bb 100644 --- a/src/dialogs/sidebar/dialog-edit-sidebar.ts +++ b/src/dialogs/sidebar/dialog-edit-sidebar.ts @@ -7,6 +7,8 @@ import { fireEvent } from "../../common/dom/fire_event"; import "../../components/ha-alert"; import "../../components/ha-button"; import "../../components/ha-dialog-footer"; +import "../../components/ha-dropdown"; +import "../../components/ha-dropdown-item"; import "../../components/ha-fade-in"; import "../../components/ha-icon-button"; import "../../components/ha-items-display-editor"; @@ -14,12 +16,10 @@ import type { DisplayItem, DisplayValue, } from "../../components/ha-items-display-editor"; -import "../../components/ha-md-button-menu"; -import "../../components/ha-md-menu-item"; -import "../../components/ha-wa-dialog"; import { computePanels } from "../../components/ha-sidebar"; import "../../components/ha-spinner"; import "../../components/ha-svg-icon"; +import "../../components/ha-wa-dialog"; import { fetchFrontendUserData, saveFrontendUserData, @@ -29,9 +29,8 @@ import { getPanelIcon, getPanelIconPath, getPanelTitle, - SHOW_AFTER_SPACER_PANELS, } from "../../data/panel"; -import type { HomeAssistant } from "../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../types"; import { showConfirmationDialog } from "../generic/show-dialog-box"; @customElement("dialog-edit-sidebar") @@ -144,7 +143,6 @@ class DialogEditSidebar extends LitElement { `${defaultPanel === panel.url_path ? " (default)" : ""}`, icon: getPanelIcon(panel), iconPath: getPanelIconPath(panel), - disableSorting: SHOW_AFTER_SPACER_PANELS.includes(panel.url_path), disableHiding: panel.url_path === defaultPanel, })); @@ -176,22 +174,17 @@ class DialogEditSidebar extends LitElement { : ""} @closed=${this._dialogClosed} > - + - - + + ${this.hass.localize("ui.sidebar.reset_to_defaults")} - - + +
${this._renderContent()}
): void { + private _changed(ev: ValueChangedEvent): void { const { order = [], hidden = [] } = ev.detail.value; this._order = [...order]; this._hidden = [...hidden]; diff --git a/src/dialogs/voice-assistant-setup/cloud/cloud-step-intro.ts b/src/dialogs/voice-assistant-setup/cloud/cloud-step-intro.ts index 564d330ed9..ec38da63e6 100644 --- a/src/dialogs/voice-assistant-setup/cloud/cloud-step-intro.ts +++ b/src/dialogs/voice-assistant-setup/cloud/cloud-step-intro.ts @@ -5,8 +5,8 @@ import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-button"; import "../../../components/ha-svg-icon"; import type { HomeAssistant } from "../../../types"; -import { brandsUrl } from "../../../util/brands-url"; import { AssistantSetupStyles } from "../styles"; +import "../../../components/voice-assistant-brand-icon"; @customElement("cloud-step-intro") export class CloudStepIntro extends LitElement { @@ -62,26 +62,16 @@ export class CloudStepIntro extends LitElement {
- Google Assistant - Amazon Alexa + + + +

${this.hass.localize( @@ -139,11 +129,12 @@ export class CloudStepIntro extends LitElement { } .feature .logos { margin-bottom: 16px; + display: flex; + gap: var(--ha-space-4); } .feature .logos > * { width: 40px; height: 40px; - margin: 0 4px; } .round-icon { border-radius: var(--ha-border-radius-circle); diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-dialog.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-dialog.ts index 1cc04ab578..047e44c85b 100644 --- a/src/dialogs/voice-assistant-setup/voice-assistant-setup-dialog.ts +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-dialog.ts @@ -8,8 +8,10 @@ import { computeDomain } from "../../common/entity/compute_domain"; import { formatLanguageCode } from "../../common/language/format_language"; import "../../components/chips/ha-assist-chip"; import "../../components/ha-dialog"; +import "../../components/ha-dropdown"; +import type { HaDropdownSelectEvent } from "../../components/ha-dropdown"; +import "../../components/ha-dropdown-item"; import { getLanguageOptions } from "../../components/ha-language-picker"; -import "../../components/ha-md-button-menu"; import type { AssistSatelliteConfiguration } from "../../data/assist_satellite"; import { fetchAssistSatelliteConfiguration } from "../../data/assist_satellite"; import { getLanguageScores } from "../../data/conversation"; @@ -169,9 +171,9 @@ export class HaVoiceAssistantSetupDialog extends LitElement { >` : this._step === STEP.PIPELINE ? this._language - ? html` - html` ${lang.primary} - ` + ` )} - ` + ` : nothing : nothing} @@ -328,10 +328,8 @@ export class HaVoiceAssistantSetupDialog extends LitElement { } } - private _handlePickLanguage(ev) { - if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") return; - - this._language = ev.target.value; + private _handlePickLanguage(ev: HaDropdownSelectEvent) { + this._language = ev.detail.item.value; } private _languageChanged(ev: CustomEvent) { @@ -401,7 +399,7 @@ export class HaVoiceAssistantSetupDialog extends LitElement { margin: 24px; display: block; } - ha-md-button-menu { + ha-dropdown { height: 48px; display: flex; align-items: center; diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-check.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-check.ts index e3dc60e5c3..07760dd358 100644 --- a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-check.ts +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-check.ts @@ -5,8 +5,8 @@ import "../../components/ha-button"; import "../../components/ha-spinner"; import { testAssistSatelliteConnection } from "../../data/assist_satellite"; import type { HomeAssistant } from "../../types"; -import { AssistantSetupStyles } from "./styles"; import { documentationUrl } from "../../util/documentation-url"; +import { AssistantSetupStyles } from "./styles"; @customElement("ha-voice-assistant-setup-step-check") export class HaVoiceAssistantSetupStepCheck extends LitElement { @@ -58,7 +58,7 @@ export class HaVoiceAssistantSetupStepCheck extends LitElement { "/voice_control/troubleshooting/#i-dont-get-a-voice-response" )} > - >${this.hass.localize( + ${this.hass.localize( "ui.panel.config.voice_assistants.satellite_wizard.check.help" )} diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-local.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-local.ts index 4490d98657..ad3013b90f 100644 --- a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-local.ts +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-local.ts @@ -349,7 +349,7 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement { }); if (step.type !== "create_entry") { throw new Error( - `${this.hass.localize("ui.panel.config.voice_assistants.satellite_wizard.local.errors.failed_create_entry", { addon: type === "tts" ? this._ttsProviderName : this._sttProviderName })}${"errors" in step ? `: ${step.errors.base}` : ""}` + `${this.hass.localize("ui.panel.config.voice_assistants.satellite_wizard.local.errors.failed_create_entry", { app: type === "tts" ? this._ttsProviderName : this._sttProviderName })}${"errors" in step ? `: ${step.errors.base}` : ""}` ); } } diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-success.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-success.ts index c636ff916f..1f8313012c 100644 --- a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-success.ts +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-success.ts @@ -4,11 +4,11 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import { stopPropagation } from "../../common/dom/stop_propagation"; +import type { HaSelectSelectEvent } from "../../components/ha-select"; import { computeDeviceName, computeDeviceNameDisplay, } from "../../common/entity/compute_device_name"; -import "../../components/ha-list-item"; import "../../components/ha-select"; import "../../components/ha-tts-voice-picker"; import type { AssistPipeline } from "../../data/assist_pipeline"; @@ -115,19 +115,15 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement { .label=${this.hass.localize( "ui.panel.config.voice_assistants.assistants.pipeline.detail.form.wake_word_id" )} - @closed=${stopPropagation} - fixedMenuPosition - naturalMenuWidth .value=${this.assistConfiguration.active_wake_words[0]} @selected=${this._wakeWordPicked} - > - ${this.assistConfiguration.available_wake_words.map( - (wakeword) => - html` - ${wakeword.wake_word} - ` + .options=${this.assistConfiguration.available_wake_words.map( + (wakeword) => ({ + value: wakeword.id, + label: wakeword.wake_word, + }) )} - + > - ${pipelineEntity?.attributes.options.map( - (pipeline) => - html` - ${this.hass.formatEntityState(pipelineEntity, pipeline)} - ` + .options=${pipelineEntity?.attributes.options.map( + (pipeline) => ({ + value: pipeline, + label: this.hass.formatEntityState( + pipelineEntity, + pipeline + ), + }) )} + >
${this.hass.localize("ui.dialogs.voice_command.title")} - ${this._pipeline?.name} ${!this._pipelines - ? html`
- -
` + ? nothing : this._pipelines?.map( (pipeline) => - html` ${pipeline.name}${pipeline.id === this._preferredPipeline ? html` ` : nothing} - ` + ` )} ${this.hass.user?.is_admin - ? html`
  • + ? html` ${this.hass.localize( "ui.dialogs.voice_command.manage_assistants" - )}` + )} + + ` : nothing} -
    +
    | null); formatEntityState(stateObj: HassEntity, state?: string): string; + formatEntityStateToParts(stateObj: HassEntity, state?: string): ValuePart[]; formatEntityAttributeValue( stateObj: HassEntity, attribute: string, value?: any ): string; + formatEntityAttributeValueToParts( + stateObj: HassEntity, + attribute: string, + value?: any + ): ValuePart[]; formatEntityAttributeName(stateObj: HassEntity, attribute: string): string; } @@ -113,8 +118,10 @@ export const provideHass = ( async function updateFormatFunctions() { const { formatEntityState, + formatEntityStateToParts, formatEntityAttributeName, formatEntityAttributeValue, + formatEntityAttributeValueToParts, formatEntityName, } = await computeFormatFunctions( hass().localize, @@ -128,8 +135,10 @@ export const provideHass = ( ); hass().updateHass({ formatEntityState, + formatEntityStateToParts, formatEntityAttributeName, formatEntityAttributeValue, + formatEntityAttributeValueToParts, formatEntityName, }); } @@ -250,6 +259,10 @@ export const provideHass = ( darkMode: false, theme: "default", }, + selectedTheme: { + theme: "default", + dark: false, + }, panels: demoPanels, services: demoServices, user: { @@ -261,7 +274,9 @@ export const provideHass = ( name: "Demo User", }, panelUrl: "lovelace", - defaultPanel: DEFAULT_PANEL, + systemData: { + default_panel: "lovelace", + }, language: localLanguage, selectedLanguage: localLanguage, locale: { @@ -340,7 +355,7 @@ export const provideHass = ( mockTheme(theme) { invalidateThemeCache(); hass().updateHass({ - selectedTheme: { theme: theme ? "mock" : "default" }, + selectedTheme: { theme: theme ? "mock" : "default", dark: false }, themes: { ...hass().themes, themes: { @@ -353,18 +368,31 @@ export const provideHass = ( document.documentElement, themes, selectedTheme!.theme, - undefined, + { dark: false }, true ); }, areas: {}, devices: {}, entities: {}, + floors: {}, formatEntityState: (stateObj, state) => (state !== null ? state : stateObj.state) ?? "", + formatEntityStateToParts: (stateObj, state) => [ + { + type: "value", + value: (state !== null ? state : stateObj.state) ?? "", + }, + ], formatEntityAttributeName: (_stateObj, attribute) => attribute, formatEntityAttributeValue: (stateObj, attribute, value) => value !== null ? value : (stateObj.attributes[attribute] ?? ""), + formatEntityAttributeValueToParts: (stateObj, attribute, value) => [ + { + type: "value", + value: value !== null ? value : (stateObj.attributes[attribute] ?? ""), + }, + ], ...overrideData, }; diff --git a/src/html/index.html.template b/src/html/index.html.template index 76198db310..029f8c7047 100644 --- a/src/html/index.html.template +++ b/src/html/index.html.template @@ -29,11 +29,11 @@ } } ::view-transition-group(launch-screen) { - animation-duration: var(--ha-animation-base-duration, 350ms); + animation-duration: var(--ha-animation-duration-slow, 350ms); animation-timing-function: ease-out; } ::view-transition-old(launch-screen) { - animation: fade-out var(--ha-animation-base-duration, 350ms) ease-out; + animation: fade-out var(--ha-animation-duration-slow, 350ms) ease-out; } html { background-color: var(--primary-background-color, #fafafa); diff --git a/src/layouts/hass-router-page.ts b/src/layouts/hass-router-page.ts index 0b3eee8ab8..e6388bc717 100644 --- a/src/layouts/hass-router-page.ts +++ b/src/layouts/hass-router-page.ts @@ -3,6 +3,7 @@ import { ReactiveElement } from "lit"; import { property } from "lit/decorators"; import memoizeOne from "memoize-one"; import { navigate } from "../common/navigate"; +import { computeRouteTail } from "../common/url/route"; import type { Route } from "../types"; const extractPage = (path: string, defaultPage: string) => { @@ -56,18 +57,9 @@ export class HassRouterPage extends ReactiveElement { private _initialLoadDone = false; - private _computeTail = memoizeOne((route: Route) => { - const dividerPos = route.path.indexOf("/", 1); - return dividerPos === -1 - ? { - prefix: route.prefix + route.path, - path: "", - } - : { - prefix: route.prefix + route.path.substr(0, dividerPos), - path: route.path.substr(dividerPos), - }; - }); + private _showLoadingScreenTimeout?: number; + + private _computeTail = memoizeOne(computeRouteTail); protected createRenderRoot() { return this; @@ -153,7 +145,11 @@ export class HassRouterPage extends ReactiveElement { ? routeOptions.load() : Promise.resolve(); - let showLoadingScreenTimeout: undefined | number; + // Clear any existing loading screen timeout from previous navigation + if (this._showLoadingScreenTimeout) { + clearTimeout(this._showLoadingScreenTimeout); + this._showLoadingScreenTimeout = undefined; + } // Check when loading the page source failed. loadProm.catch((err) => { @@ -170,8 +166,9 @@ export class HassRouterPage extends ReactiveElement { this.removeChild(this.lastChild!); } - if (showLoadingScreenTimeout) { - clearTimeout(showLoadingScreenTimeout); + if (this._showLoadingScreenTimeout) { + clearTimeout(this._showLoadingScreenTimeout); + this._showLoadingScreenTimeout = undefined; } // Show error screen @@ -191,7 +188,7 @@ export class HassRouterPage extends ReactiveElement { // That way we won't have a double fast flash on fast connections. let created = false; - showLoadingScreenTimeout = window.setTimeout(() => { + this._showLoadingScreenTimeout = window.setTimeout(() => { if (created || this._currentPage !== newPage) { return; } diff --git a/src/layouts/hass-subpage.ts b/src/layouts/hass-subpage.ts index 1a9394284b..d70abda6f4 100644 --- a/src/layouts/hass-subpage.ts +++ b/src/layouts/hass-subpage.ts @@ -1,6 +1,7 @@ import type { CSSResultGroup, TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, eventOptions, property } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; import { restoreScroll } from "../common/decorators/restore-scroll"; import { goBack } from "../common/navigate"; import "../components/ha-icon-button-arrow-prev"; @@ -22,19 +23,16 @@ class HassSubpage extends LitElement { @property({ type: Boolean, reflect: true }) public narrow = false; - @property({ type: Boolean }) public supervisor = false; - // @ts-ignore @restoreScroll(".content") private _savedScrollPos?: number; protected render(): TemplateResult { return html` -
    +
    ${this.mainPage || history.state?.root ? html` @@ -135,7 +133,7 @@ class HassSubpage extends LitElement { } .main-title { - margin: var(--margin-title); + margin-inline-start: var(--ha-space-6); line-height: var(--ha-line-height-normal); min-width: 0; flex-grow: 1; @@ -146,6 +144,9 @@ class HassSubpage extends LitElement { overflow: hidden; text-overflow: ellipsis; } + .narrow .main-title { + margin-inline-start: var(--ha-space-2); + } .content { position: relative; diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts index d2f193fad1..fed17911c6 100644 --- a/src/layouts/hass-tabs-subpage-data-table.ts +++ b/src/layouts/hass-tabs-subpage-data-table.ts @@ -1,3 +1,4 @@ +import "@home-assistant/webawesome/dist/components/divider/divider"; import { ResizeController } from "@lit-labs/observers/resize-controller"; import { mdiArrowDown, @@ -28,9 +29,9 @@ import type { import { showDataTableSettingsDialog } from "../components/data-table/show-dialog-data-table-settings"; import "../components/ha-dialog"; import "../components/ha-dialog-header"; -import "../components/ha-md-button-menu"; -import "../components/ha-md-divider"; -import "../components/ha-md-menu-item"; +import "../components/ha-dropdown"; +import type { HaDropdownSelectEvent } from "../components/ha-dropdown"; +import "../components/ha-dropdown-item"; import "../components/search-input-outlined"; import { KeyboardShortcutMixin } from "../mixins/keyboard-shortcut-mixin"; import type { HomeAssistant, Route } from "../types"; @@ -133,7 +134,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) { * String to show when there are no records in the data table. * @type {String} */ - @property({ attribute: false, type: String }) public noDataText?: string; + @property({ attribute: false }) public noDataText?: string; /** * Hides the data table and show an empty message. @@ -254,7 +255,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) { const sortByMenu = Object.values(this.columns).find((col) => col.sortable) ? html` - + column.sortable ? html` - ${this._sortColumn === id @@ -292,17 +289,17 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) { ` : nothing} ${column.title || column.label} - + ` : nothing )} - + ` : nothing; const groupByMenu = Object.values(this.columns).find((col) => col.groupable) ? html` - + column.groupable ? html` - ${column.title || column.label} - + ` : nothing )} - ${localize("ui.components.subpage-data-table.dont_group_by")} - - - + + ${localize( "ui.components.subpage-data-table.collapse_all_groups" )} - - + ${localize("ui.components.subpage-data-table.expand_all_groups")} - - + + ` : nothing; @@ -399,7 +394,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) { "ui.components.subpage-data-table.exit_selection_mode" )} > - + - -
    - ${localize("ui.components.subpage-data-table.select_all")} -
    -
    - -
    - ${localize( - "ui.components.subpage-data-table.select_none" - )} -
    -
    - - -
    - ${localize( - "ui.components.subpage-data-table.exit_selection_mode" - )} -
    -
    -
    + + ${localize("ui.components.subpage-data-table.select_all")} + + + ${localize("ui.components.subpage-data-table.select_none")} + + + + ${localize( + "ui.components.subpage-data-table.exit_selection_mode" + )} + + ${this.selected !== undefined ? html`

    ${localize("ui.components.subpage-data-table.selected", { @@ -612,10 +590,10 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) { this._sortColumn = this._sortDirection ? ev.detail.column : undefined; } - private _handleSortBy(ev) { - if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") return; + private _handleSortBy(ev: HaDropdownSelectEvent) { + ev.preventDefault(); // keep the dropdown open - const columnId = ev.currentTarget.value; + const columnId = ev.detail.item.value; if (!this._sortDirection || this._sortColumn !== columnId) { this._sortDirection = "asc"; } else if (this._sortDirection === "asc") { @@ -631,9 +609,24 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) { }); } - private _handleGroupBy = (item) => { - this._setGroupColumn(item.value); - }; + private _handleGroupBy(ev: HaDropdownSelectEvent) { + const group = ev.detail.item.value; + + if (group === "reset") { + this._setGroupColumn(""); + return; + } + if (group === "collapse_all") { + this._collapseAllGroups(); + return; + } + if (group === "expand_all") { + this._expandAllGroups(); + return; + } + + this._setGroupColumn(group); + } private _setGroupColumn(columnId: string) { this._groupColumn = columnId; @@ -669,6 +662,26 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) { this._selectMode = true; } + private _handleSelect(ev: HaDropdownSelectEvent) { + const action = ev.detail.item.value; + + if (!action) { + return; + } + + switch (action) { + case "all": + this._selectAll(); + break; + case "none": + this._selectNone(); + break; + case "disable_select_mode": + this._disableSelectMode(); + break; + } + } + private _disableSelectMode = () => { this._selectMode = false; this._dataTable.clearSelection(); @@ -902,9 +915,17 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) { flex-direction: column; } - ha-md-button-menu ha-assist-chip { + ha-dropdown ha-assist-chip { --md-assist-chip-trailing-space: 8px; } + + ha-dropdown-item.selected { + border: 1px solid var(--primary-color); + font-weight: var(--ha-font-weight-medium); + color: var(--primary-color); + background-color: var(--ha-color-fill-primary-quiet-resting); + --icon-primary-color: var(--primary-color); + } `; } diff --git a/src/layouts/hass-tabs-subpage.ts b/src/layouts/hass-tabs-subpage.ts index ee17076a6a..9c6a913353 100644 --- a/src/layouts/hass-tabs-subpage.ts +++ b/src/layouts/hass-tabs-subpage.ts @@ -34,8 +34,6 @@ export interface PageNavigation { class HassTabsSubpage extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ type: Boolean }) public supervisor = false; - @property({ attribute: false }) public localizeFunc?: LocalizeFunc; @property({ type: String, attribute: "back-path" }) public backPath?: string; @@ -133,13 +131,12 @@ class HassTabsSubpage extends LitElement { ); const showTabs = tabs.length > 1; return html` -

    +
    ${this.mainPage || (!this.backPath && history.state?.root) ? html` @@ -323,7 +320,10 @@ class HassTabsSubpage extends LitElement { max-height: var(--header-height); line-height: var(--ha-line-height-normal); color: var(--sidebar-text-color); - margin: var(--main-title-margin, var(--margin-title)); + margin-inline-start: var(--main-title-margin, var(--ha-space-6)); + } + .narrow .main-title { + margin-inline-start: var(--main-title-margin, var(--ha-space-2)); } .content { diff --git a/src/layouts/home-assistant.ts b/src/layouts/home-assistant.ts index 7b55a17709..47e8ab481c 100644 --- a/src/layouts/home-assistant.ts +++ b/src/layouts/home-assistant.ts @@ -106,6 +106,10 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) { // Navigation const updateRoute = (path = curPath()) => { + // Developer tools panel was moved to config in 2026.2 + if (path.startsWith("/developer-tools")) { + path = path.replace("/developer-tools", "/config/developer-tools"); + } if (this._route && path === this._route.path) { return; } diff --git a/src/layouts/partial-panel-resolver.ts b/src/layouts/partial-panel-resolver.ts index 671867750c..dc102b67b0 100644 --- a/src/layouts/partial-panel-resolver.ts +++ b/src/layouts/partial-panel-resolver.ts @@ -14,14 +14,13 @@ import { removeLaunchScreen } from "../util/launch-screen"; import type { RouteOptions, RouterOptions } from "./hass-router-page"; import { HassRouterPage } from "./hass-router-page"; -const CACHE_URL_PATHS = ["lovelace", "developer-tools"]; +const CACHE_URL_PATHS = ["lovelace", "home", "config"]; const COMPONENTS = { + app: () => import("../panels/app/ha-panel-app"), energy: () => import("../panels/energy/ha-panel-energy"), calendar: () => import("../panels/calendar/ha-panel-calendar"), config: () => import("../panels/config/ha-panel-config"), custom: () => import("../panels/custom/ha-panel-custom"), - "developer-tools": () => - import("../panels/developer-tools/ha-panel-developer-tools"), lovelace: () => import("../panels/lovelace/ha-panel-lovelace"), history: () => import("../panels/history/ha-panel-history"), iframe: () => import("../panels/iframe/ha-panel-iframe"), @@ -155,6 +154,7 @@ class PartialPanelResolver extends HassRouterPage { // iFrames will lose their state when disconnected // Do not disconnect any iframe panel curPanel.component_name !== "iframe" && + curPanel.component_name !== "app" && // Do not disconnect any custom panel that embeds into iframe (ie hassio) (curPanel.component_name !== "custom" || !(curPanel as CustomPanelInfo).config._panel_custom.embed_iframe) diff --git a/src/onboarding/ha-onboarding.ts b/src/onboarding/ha-onboarding.ts index c3fed7f574..65f0167a6f 100644 --- a/src/onboarding/ha-onboarding.ts +++ b/src/onboarding/ha-onboarding.ts @@ -25,6 +25,7 @@ import { subscribeOne } from "../common/util/subscribe-one"; import "../components/ha-card"; import type { AuthUrlSearchParams } from "../data/auth"; import { hassUrl } from "../data/auth"; +import { saveFrontendSystemData } from "../data/frontend"; import type { OnboardingResponses, OnboardingStep } from "../data/onboarding"; import { fetchInstallationType, @@ -143,7 +144,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) { .label=${""} native-name @value-changed=${this._languageChanged} - inline-arrow > - + >
    @@ -526,13 +516,13 @@ class HassioAddonInfo extends LitElement { > - ${this.supervisor.localize( - "addon.dashboard.option.boot.title" + ${this.hass.localize( + "ui.panel.config.apps.dashboard.option.boot.title" )} - ${this.supervisor.localize( - "addon.dashboard.option.boot.description" + ${this.hass.localize( + "ui.panel.config.apps.dashboard.option.boot.description" )} - ${this.supervisor.localize( - "addon.dashboard.option.watchdog.title" + ${this.hass.localize( + "ui.panel.config.apps.dashboard.option.watchdog.title" )} - ${this.supervisor.localize( - "addon.dashboard.option.watchdog.description" + ${this.hass.localize( + "ui.panel.config.apps.dashboard.option.watchdog.description" )} - ${this.supervisor.localize( - "addon.dashboard.option.auto_update.title" + ${this.hass.localize( + "ui.panel.config.apps.dashboard.option.auto_update.title" )} - ${this.supervisor.localize( - "addon.dashboard.option.auto_update.description" + ${this.hass.localize( + "ui.panel.config.apps.dashboard.option.auto_update.description" )} - ${this.supervisor.localize( - "addon.dashboard.option.ingress_panel.title" + ${this.hass.localize( + "ui.panel.config.apps.dashboard.option.ingress_panel.title" )} - ${this.supervisor.localize( - "addon.dashboard.option.ingress_panel.description" + ${this.hass.localize( + "ui.panel.config.apps.dashboard.option.ingress_panel.description" )} - ${this.supervisor.localize( - "addon.dashboard.option.protected.title" + ${this.hass.localize( + "ui.panel.config.apps.dashboard.option.protected.title" )} - ${this.supervisor.localize( - "addon.dashboard.option.protected.description" + ${this.hass.localize( + "ui.panel.config.apps.dashboard.option.protected.description" )} - ${this.supervisor.localize("addon.dashboard.hostname")} + ${this.hass.localize( + "ui.panel.config.apps.dashboard.hostname" + )} ${this.addon.hostname} ${metrics.map( (metric) => html` - + > ` )}` : nothing} @@ -663,30 +655,6 @@ class HassioAddonInfo extends LitElement { ${this._error ? html`${this._error}` : nothing} - ${!this.addon.version && addonStoreInfo && !this.addon.available - ? !addonArchIsSupported( - this.supervisor.info.supported_arch, - this.addon.arch - ) - ? html` - - ${this.supervisor.localize( - "addon.dashboard.not_available_arch" - )} - - ` - : html` - - ${this.supervisor.localize( - "addon.dashboard.not_available_version", - { - core_version_installed: this.supervisor.core.version, - core_version_needed: addonStoreInfo!.homeassistant, - } - )} - - ` - : nothing}
    @@ -699,14 +667,19 @@ class HassioAddonInfo extends LitElement { @click=${this._stopClicked} .disabled=${systemManaged && !this.controlEnabled} > - ${this.supervisor.localize("addon.dashboard.stop")} + ${this.hass.localize( + "ui.panel.config.apps.dashboard.stop" + )} - ${this.supervisor.localize("addon.dashboard.restart")} + ${this.hass.localize( + "ui.panel.config.apps.dashboard.restart" + )} ` : html` @@ -715,7 +688,9 @@ class HassioAddonInfo extends LitElement { .progress=${this.addon.state === "startup"} appearance="plain" > - ${this.supervisor.localize("addon.dashboard.start")} + ${this.hass.localize( + "ui.panel.config.apps.dashboard.start" + )} ` : nothing} @@ -729,7 +704,9 @@ class HassioAddonInfo extends LitElement { @click=${this._uninstallClicked} .disabled=${systemManaged && !this.controlEnabled} > - ${this.supervisor.localize("addon.dashboard.uninstall")} + ${this.hass.localize( + "ui.panel.config.apps.dashboard.uninstall" + )} ${this.addon.build ? html` @@ -738,7 +715,9 @@ class HassioAddonInfo extends LitElement { appearance="plain" @click=${this._rebuildClicked} > - ${this.supervisor.localize("addon.dashboard.rebuild")} + ${this.hass.localize( + "ui.panel.config.apps.dashboard.rebuild" + )} ` : nothing} @@ -760,8 +739,8 @@ class HassioAddonInfo extends LitElement { ? this._openIngress : undefined} > - ${this.supervisor.localize( - "addon.dashboard.open_web_ui" + ${this.hass.localize( + "ui.panel.config.apps.dashboard.open_web_ui" )} ` @@ -772,7 +751,9 @@ class HassioAddonInfo extends LitElement { .disabled=${!this.addon.available} @click=${this._installClicked} > - ${this.supervisor.localize("addon.dashboard.install")} + ${this.hass.localize( + "ui.panel.config.apps.dashboard.install" + )} `}
    @@ -804,7 +785,7 @@ class HassioAddonInfo extends LitElement { "state" in this.addon && this.addon.state === "startup" ) { - // Addon is starting up, wait for it to start + // App is starting up, wait for it to start this._scheduleDataUpdate(); } } @@ -857,28 +838,24 @@ class HassioAddonInfo extends LitElement { private _showMoreInfo(ev): void { const id = ev.currentTarget.id as AddonCapability; - showHassioMarkdownDialog(this, { - title: this.supervisor.localize(`addon.dashboard.capability.${id}.title`), - content: - id === "stage" - ? this.supervisor.localize( - `addon.dashboard.capability.${id}.description`, - { - icon_stable: ``, - icon_experimental: ``, - icon_deprecated: ``, - } - ) - : this.supervisor.localize( - `addon.dashboard.capability.${id}.description` - ), + showAlertDialog(this, { + title: this.hass.localize( + `ui.panel.config.apps.dashboard.capability.${id}.title` + ), + text: this.hass.localize( + `ui.panel.config.apps.dashboard.capability.${id}.description` + ), }); } - private _showSystemManagedDialog() { - showSystemManagedDialog(this, { - addon: this.addon as HassioAddonDetails, - supervisor: this.supervisor, + private _showSystemManagedInfo() { + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.apps.dashboard.system_managed.title" + ), + text: this.hass.localize( + "ui.panel.config.apps.dashboard.system_managed.description" + ), }); } @@ -902,7 +879,7 @@ class HassioAddonInfo extends LitElement { } private _openIngress(): void { - navigate(`/hassio/ingress/${this.addon.slug}`); + navigate(`/app/${this.addon.slug}`); } private get _computeShowIngressUI(): boolean { @@ -936,9 +913,12 @@ class HassioAddonInfo extends LitElement { }; fireEvent(this, "hass-api-called", eventdata); } catch (err: any) { - this._error = this.supervisor.localize("addon.failed_to_save", { - error: extractApiErrorMessage(err), - }); + this._error = this.hass.localize( + "ui.panel.config.apps.dashboard.failed_to_save", + { + error: extractApiErrorMessage(err), + } + ); } } @@ -956,9 +936,12 @@ class HassioAddonInfo extends LitElement { }; fireEvent(this, "hass-api-called", eventdata); } catch (err: any) { - this._error = this.supervisor.localize("addon.failed_to_save", { - error: extractApiErrorMessage(err), - }); + this._error = this.hass.localize( + "ui.panel.config.apps.dashboard.failed_to_save", + { + error: extractApiErrorMessage(err), + } + ); } } @@ -976,9 +959,12 @@ class HassioAddonInfo extends LitElement { }; fireEvent(this, "hass-api-called", eventdata); } catch (err: any) { - this._error = this.supervisor.localize("addon.failed_to_save", { - error: extractApiErrorMessage(err), - }); + this._error = this.hass.localize( + "ui.panel.config.apps.dashboard.failed_to_save", + { + error: extractApiErrorMessage(err), + } + ); } } @@ -996,9 +982,12 @@ class HassioAddonInfo extends LitElement { }; fireEvent(this, "hass-api-called", eventdata); } catch (err: any) { - this._error = this.supervisor.localize("addon.failed_to_save", { - error: extractApiErrorMessage(err), - }); + this._error = this.hass.localize( + "ui.panel.config.apps.dashboard.failed_to_save", + { + error: extractApiErrorMessage(err), + } + ); } } @@ -1016,9 +1005,12 @@ class HassioAddonInfo extends LitElement { }; fireEvent(this, "hass-api-called", eventdata); } catch (err: any) { - this._error = this.supervisor.localize("addon.failed_to_save", { - error: extractApiErrorMessage(err), - }); + this._error = this.hass.localize( + "ui.panel.config.apps.dashboard.failed_to_save", + { + error: extractApiErrorMessage(err), + } + ); } } @@ -1029,14 +1021,19 @@ class HassioAddonInfo extends LitElement { this.addon.slug ); - showHassioMarkdownDialog(this, { - title: this.supervisor.localize("addon.dashboard.changelog"), - content: extractChangelog(this.addon as HassioAddonDetails, content), + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.apps.dashboard.changelog"), + text: html``, }); } catch (err: any) { showAlertDialog(this, { - title: this.supervisor.localize( - "addon.dashboard.action_error.get_changelog" + title: this.hass.localize( + "ui.panel.config.apps.dashboard.action_error.get_changelog" ), text: extractApiErrorMessage(err), }); @@ -1066,7 +1063,9 @@ class HassioAddonInfo extends LitElement { fireEvent(this, "hass-api-called", eventdata); } catch (err: any) { showAlertDialog(this, { - title: this.supervisor.localize("addon.dashboard.action_error.install"), + title: this.hass.localize( + "ui.panel.config.apps.dashboard.action_error.install" + ), text: extractApiErrorMessage(err), }); } @@ -1091,7 +1090,9 @@ class HassioAddonInfo extends LitElement { fireEvent(this, "hass-api-called", eventdata); } catch (err: any) { showAlertDialog(this, { - title: this.supervisor.localize("addon.dashboard.action_error.stop"), + title: this.hass.localize( + "ui.panel.config.apps.dashboard.action_error.stop" + ), text: extractApiErrorMessage(err), }); } @@ -1099,6 +1100,10 @@ class HassioAddonInfo extends LitElement { } private async _restartClicked(ev: CustomEvent): Promise { + if (this._isSystemManaged(this.addon) && !this.controlEnabled) { + return; + } + const button = ev.currentTarget as any; button.progress = true; @@ -1107,12 +1112,14 @@ class HassioAddonInfo extends LitElement { const eventdata = { success: true, response: undefined, - path: "stop", + path: "restart", }; fireEvent(this, "hass-api-called", eventdata); } catch (err: any) { showAlertDialog(this, { - title: this.supervisor.localize("addon.dashboard.action_error.restart"), + title: this.hass.localize( + "ui.panel.config.apps.dashboard.action_error.restart" + ), text: extractApiErrorMessage(err), }); } @@ -1127,7 +1134,9 @@ class HassioAddonInfo extends LitElement { await rebuildLocalAddon(this.hass, this.addon.slug); } catch (err: any) { showAlertDialog(this, { - title: this.supervisor.localize("addon.dashboard.action_error.rebuild"), + title: this.hass.localize( + "ui.panel.config.apps.dashboard.action_error.rebuild" + ), text: extractApiErrorMessage(err), }); } @@ -1144,15 +1153,15 @@ class HassioAddonInfo extends LitElement { ); if (!validate.valid) { await showConfirmationDialog(this, { - title: this.supervisor.localize( - "addon.dashboard.action_error.start_invalid_config" + title: this.hass.localize( + "ui.panel.config.apps.dashboard.action_error.start_invalid_config" ), text: validate.message.split(" Got ")[0], confirm: () => this._openConfiguration(), - confirmText: this.supervisor.localize( - "addon.dashboard.action_error.go_to_config" + confirmText: this.hass.localize( + "ui.panel.config.apps.dashboard.action_error.go_to_config" ), - dismissText: this.supervisor.localize("common.cancel"), + dismissText: this.hass.localize("ui.common.cancel"), }); button.actionError(); button.progress = false; @@ -1162,7 +1171,9 @@ class HassioAddonInfo extends LitElement { button.actionError(); button.progress = false; showAlertDialog(this, { - title: "Failed to validate addon configuration", + title: this.hass.localize( + "ui.panel.config.apps.dashboard.action_error.validate_config" + ), text: extractApiErrorMessage(err), }); return; @@ -1181,7 +1192,9 @@ class HassioAddonInfo extends LitElement { button.actionError(); button.progress = false; showAlertDialog(this, { - title: this.supervisor.localize("addon.dashboard.action_error.start"), + title: this.hass.localize( + "ui.panel.config.apps.dashboard.action_error.start" + ), text: extractApiErrorMessage(err), }); return; @@ -1191,7 +1204,7 @@ class HassioAddonInfo extends LitElement { } private _openConfiguration(): void { - navigate(`/hassio/addon/${this.addon.slug}/config`); + navigate(`/config/app/${this.addon.slug}/config`); } private async _uninstallClicked(ev: CustomEvent): Promise { @@ -1207,13 +1220,18 @@ class HassioAddonInfo extends LitElement { }; const confirmed = await showConfirmationDialog(this, { - title: this.supervisor.localize("dialog.uninstall_addon.title", { - name: this.addon.name, - }), + title: this.hass.localize( + "ui.panel.config.apps.dashboard.uninstall_dialog.title", + { + name: this.addon.name, + } + ), text: html` - ${this.supervisor.localize("dialog.uninstall_addon.remove_data")} + ${this.hass.localize( + "ui.panel.config.apps.dashboard.uninstall_dialog.remove_data" + )}

    `} >
    `, - confirmText: this.supervisor.localize("dialog.uninstall_addon.uninstall"), - dismissText: this.supervisor.localize("common.cancel"), + confirmText: this.hass.localize( + "ui.panel.config.apps.dashboard.uninstall_dialog.uninstall" + ), + dismissText: this.hass.localize("ui.common.cancel"), destructive: true, }); @@ -1245,8 +1265,8 @@ class HassioAddonInfo extends LitElement { button.actionSuccess(); } catch (err: any) { showAlertDialog(this, { - title: this.supervisor.localize( - "addon.dashboard.action_error.uninstall" + title: this.hass.localize( + "ui.panel.config.apps.dashboard.action_error.uninstall" ), text: extractApiErrorMessage(err), }); @@ -1263,7 +1283,6 @@ class HassioAddonInfo extends LitElement { static get styles(): CSSResultGroup { return [ haStyle, - hassioStyle, css` :host { display: block; @@ -1293,8 +1312,8 @@ class HassioAddonInfo extends LitElement { color: var(--secondary-text-color); } .addon-header { - padding-left: 8px; - padding-inline-start: 8px; + padding-left: var(--ha-space-2); + padding-inline-start: var(--ha-space-2); padding-inline-end: initial; font-size: var(--ha-font-size-2xl); color: var(--ha-card-header-color, var(--primary-text-color)); @@ -1384,6 +1403,11 @@ class HassioAddonInfo extends LitElement { } ha-markdown { padding: 16px; + --markdown-image-background-color: transparent; + --markdown-image-border-radius: 0; + --markdown-image-min-height: auto; + --markdown-image-text-indent: 0; + --markdown-image-transition: none; } ha-settings-row { padding: 0; @@ -1425,7 +1449,7 @@ class HassioAddonInfo extends LitElement { text-decoration: none; } - update-available-card { + supervisor-app-update-available-card { padding-bottom: 16px; } @@ -1443,6 +1467,6 @@ class HassioAddonInfo extends LitElement { } declare global { interface HTMLElementTagNameMap { - "hassio-addon-info": HassioAddonInfo; + "supervisor-app-info": SupervisorAppInfo; } } diff --git a/hassio/src/addon-view/info/hassio-addon-system-managed.ts b/src/panels/config/apps/app-view/info/supervisor-app-system-managed.ts similarity index 55% rename from hassio/src/addon-view/info/hassio-addon-system-managed.ts rename to src/panels/config/apps/app-view/info/supervisor-app-system-managed.ts index f4eb7e10b0..f6e3fb75e0 100644 --- a/hassio/src/addon-view/info/hassio-addon-system-managed.ts +++ b/src/panels/config/apps/app-view/info/supervisor-app-system-managed.ts @@ -1,16 +1,16 @@ import type { TemplateResult } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import "../../../../src/components/ha-alert"; -import "../../../../src/components/ha-button"; -import type { Supervisor } from "../../../../src/data/supervisor/supervisor"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import "../../../../../components/ha-alert"; +import "../../../../../components/ha-button"; +import type { HomeAssistant } from "../../../../../types"; -@customElement("hassio-addon-system-managed") -class HassioAddonSystemManaged extends LitElement { +@customElement("supervisor-app-system-managed") +class SupervisorAppSystemManaged extends LitElement { @property({ type: Boolean }) public narrow = false; - @property({ attribute: false }) public supervisor!: Supervisor; + @property({ attribute: false }) public hass!: HomeAssistant; @property({ type: Boolean, attribute: "hide-button" }) public hideButton = false; @@ -19,14 +19,20 @@ class HassioAddonSystemManaged extends LitElement { return html` - ${this.supervisor.localize("addon.system_managed.description")} + ${this.hass.localize( + "ui.panel.config.apps.dashboard.system_managed.description" + )} ${!this.hideButton ? html` - ${this.supervisor.localize("addon.system_managed.take_control")} + ${this.hass.localize( + "ui.panel.config.apps.dashboard.system_managed.take_control" + )} ` : nothing} @@ -50,7 +56,7 @@ class HassioAddonSystemManaged extends LitElement { } declare global { interface HTMLElementTagNameMap { - "hassio-addon-system-managed": HassioAddonSystemManaged; + "supervisor-app-system-managed": SupervisorAppSystemManaged; } interface HASSDomEvents { diff --git a/hassio/src/addon-view/log/hassio-addon-log-tab.ts b/src/panels/config/apps/app-view/log/supervisor-app-log-tab.ts similarity index 63% rename from hassio/src/addon-view/log/hassio-addon-log-tab.ts rename to src/panels/config/apps/app-view/log/supervisor-app-log-tab.ts index 2a2e05da05..c6ae73a26a 100644 --- a/hassio/src/addon-view/log/hassio-addon-log-tab.ts +++ b/src/panels/config/apps/app-view/log/supervisor-app-log-tab.ts @@ -6,22 +6,19 @@ import { type TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; -import "../../../../src/components/ha-spinner"; -import type { HassioAddonDetails } from "../../../../src/data/hassio/addon"; -import type { Supervisor } from "../../../../src/data/supervisor/supervisor"; -import { haStyle } from "../../../../src/resources/styles"; -import type { HomeAssistant } from "../../../../src/types"; -import { hassioStyle } from "../../resources/hassio-style"; -import "../../../../src/panels/config/logs/error-log-card"; -import "../../../../src/components/search-input"; -import { extractSearchParam } from "../../../../src/common/url/search-params"; +import "../../../../../components/ha-spinner"; +import type { HassioAddonDetails } from "../../../../../data/hassio/addon"; +import { haStyle } from "../../../../../resources/styles"; +import type { HomeAssistant } from "../../../../../types"; +import { supervisorAppsStyle } from "../../resources/supervisor-apps-style"; +import "../../../logs/error-log-card"; +import "../../../../../components/search-input"; +import { extractSearchParam } from "../../../../../common/url/search-params"; -@customElement("hassio-addon-log-tab") -class HassioAddonLogDashboard extends LitElement { +@customElement("supervisor-app-log-tab") +class SupervisorAppLogDashboard extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public supervisor!: Supervisor; - @property({ attribute: false }) public addon?: HassioAddonDetails; @state() private _filter = extractSearchParam("filter") || ""; @@ -36,13 +33,12 @@ class HassioAddonLogDashboard extends LitElement { @value-changed=${this._filterChanged} .hass=${this.hass} .filter=${this._filter} - .label=${this.supervisor.localize("ui.panel.config.logs.search")} + .label=${this.hass.localize("ui.panel.config.logs.search")} >
    @@ -12,7 +12,7 @@ export const extractChangelog = ( content: string ): string => { if (content.startsWith("# Changelog")) { - content = content.substr(12, content.length); + content = content.substring(12); } if ( content.includes(`# ${addon.version}`) && diff --git a/hassio/src/components/hassio-card-content.ts b/src/panels/config/apps/components/supervisor-apps-card-content.ts similarity index 86% rename from hassio/src/components/hassio-card-content.ts rename to src/panels/config/apps/components/supervisor-apps-card-content.ts index 9343022093..7a3afd7a4c 100644 --- a/hassio/src/components/hassio-card-content.ts +++ b/src/panels/config/apps/components/supervisor-apps-card-content.ts @@ -2,11 +2,11 @@ import { mdiHelpCircle } from "@mdi/js"; import type { TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import "../../../src/components/ha-svg-icon"; -import type { HomeAssistant } from "../../../src/types"; +import "../../../../components/ha-svg-icon"; +import type { HomeAssistant } from "../../../../types"; -@customElement("hassio-card-content") -class HassioCardContent extends LitElement { +@customElement("supervisor-apps-card-content") +class SupervisorAppsCardContent extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; // eslint-disable-next-line lit/no-native-attributes @@ -70,9 +70,9 @@ class HassioCardContent extends LitElement { } ha-svg-icon { - margin-right: 24px; - margin-left: 8px; - margin-top: 12px; + margin-right: var(--ha-space-6); + margin-left: var(--ha-space-2); + margin-top: var(--ha-space-3); float: left; color: var(--secondary-text-color); } @@ -106,8 +106,8 @@ class HassioCardContent extends LitElement { .icon_image img { max-height: 40px; max-width: 40px; - margin-top: 4px; - margin-right: 16px; + margin-top: var(--ha-space-1); + margin-right: var(--ha-space-4); float: left; } .icon_image.stopped, @@ -119,8 +119,8 @@ class HassioCardContent extends LitElement { background-color: var(--warning-color); width: 12px; height: 12px; - top: 8px; - right: 8px; + top: var(--ha-space-2); + right: var(--ha-space-2); border-radius: var(--ha-border-radius-circle); } .topbar { @@ -146,6 +146,6 @@ class HassioCardContent extends LitElement { declare global { interface HTMLElementTagNameMap { - "hassio-card-content": HassioCardContent; + "supervisor-apps-card-content": SupervisorAppsCardContent; } } diff --git a/hassio/src/components/hassio-filter-addons.ts b/src/panels/config/apps/components/supervisor-apps-filter.ts similarity index 87% rename from hassio/src/components/hassio-filter-addons.ts rename to src/panels/config/apps/components/supervisor-apps-filter.ts index 21e5d0e29f..690ef8ce1e 100644 --- a/hassio/src/components/hassio-filter-addons.ts +++ b/src/panels/config/apps/components/supervisor-apps-filter.ts @@ -1,6 +1,6 @@ import type { IFuseOptions } from "fuse.js"; import Fuse from "fuse.js"; -import type { StoreAddon } from "../../../src/data/supervisor/store"; +import type { StoreAddon } from "../../../../data/supervisor/store"; export function filterAndSort(addons: StoreAddon[], filter: string) { const options: IFuseOptions = { diff --git a/hassio/src/dialogs/registries/dialog-hassio-registries.ts b/src/panels/config/apps/dialogs/registries/dialog-registries.ts similarity index 73% rename from hassio/src/dialogs/registries/dialog-hassio-registries.ts rename to src/panels/config/apps/dialogs/registries/dialog-registries.ts index b5e4225d20..0d3ab0bd84 100644 --- a/hassio/src/dialogs/registries/dialog-hassio-registries.ts +++ b/src/panels/config/apps/dialogs/registries/dialog-registries.ts @@ -2,24 +2,22 @@ import { mdiDelete, mdiPlus } from "@mdi/js"; import type { CSSResultGroup, TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; -import "../../../../src/components/ha-button"; -import { createCloseHeading } from "../../../../src/components/ha-dialog"; -import "../../../../src/components/ha-form/ha-form"; -import type { SchemaUnion } from "../../../../src/components/ha-form/types"; -import "../../../../src/components/ha-icon-button"; -import "../../../../src/components/ha-settings-row"; -import "../../../../src/components/ha-svg-icon"; -import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; +import "../../../../../components/ha-button"; +import { createCloseHeading } from "../../../../../components/ha-dialog"; +import "../../../../../components/ha-form/ha-form"; +import type { SchemaUnion } from "../../../../../components/ha-form/types"; +import "../../../../../components/ha-icon-button"; +import "../../../../../components/ha-settings-row"; +import "../../../../../components/ha-svg-icon"; +import { extractApiErrorMessage } from "../../../../../data/hassio/common"; import { addHassioDockerRegistry, fetchHassioDockerRegistries, removeHassioDockerRegistry, -} from "../../../../src/data/hassio/docker"; -import type { Supervisor } from "../../../../src/data/supervisor/supervisor"; -import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box"; -import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; -import type { HomeAssistant } from "../../../../src/types"; -import type { RegistriesDialogParams } from "./show-dialog-registries"; +} from "../../../../../data/hassio/docker"; +import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box"; +import { haStyle, haStyleDialog } from "../../../../../resources/styles"; +import type { HomeAssistant } from "../../../../../types"; const SCHEMA = [ { @@ -39,12 +37,10 @@ const SCHEMA = [ }, ] as const; -@customElement("dialog-hassio-registries") -class HassioRegistriesDialog extends LitElement { +@customElement("dialog-apps-registries") +class AppsRegistriesDialog extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public supervisor!: Supervisor; - @state() private _registries?: { registry: string; username: string; @@ -71,8 +67,12 @@ class HassioRegistriesDialog extends LitElement { .heading=${createCloseHeading( this.hass, this._addingRegistry - ? this.supervisor.localize("dialog.registries.title_add") - : this.supervisor.localize("dialog.registries.title_manage") + ? this.hass.localize( + "ui.panel.config.apps.dialog.registries.title_add" + ) + : this.hass.localize( + "ui.panel.config.apps.dialog.registries.title_manage" + ) )} > ${this._addingRegistry @@ -96,7 +96,9 @@ class HassioRegistriesDialog extends LitElement { size="small" > - ${this.supervisor.localize("dialog.registries.add_registry")} + ${this.hass.localize( + "ui.panel.config.apps.dialog.registries.add_registry" + )}
    ` @@ -106,15 +108,15 @@ class HassioRegistriesDialog extends LitElement { ${entry.registry} - ${this.supervisor.localize( - "dialog.registries.username" + ${this.hass.localize( + "ui.panel.config.apps.dialog.registries.username" )}: ${entry.username} - ${this.supervisor.localize( - "dialog.registries.no_registries" + ${this.hass.localize( + "ui.panel.config.apps.dialog.registries.no_registries" )} `} @@ -137,8 +139,8 @@ class HassioRegistriesDialog extends LitElement { size="small" > - ${this.supervisor.localize( - "dialog.registries.add_new_registry" + ${this.hass.localize( + "ui.panel.config.apps.dialog.registries.add_new_registry" )}
    `} @@ -147,16 +149,17 @@ class HassioRegistriesDialog extends LitElement { } private _computeLabel = (schema: SchemaUnion) => - this.supervisor.localize(`dialog.registries.${schema.name}`); + this.hass.localize( + `ui.panel.config.apps.dialog.registries.${schema.name}` as any + ); private _valueChanged(ev: CustomEvent) { this._input = ev.detail.value; } - public async showDialog(dialogParams: RegistriesDialogParams): Promise { + public async showDialog(): Promise { this._opened = true; this._input = {}; - this.supervisor = dialogParams.supervisor; await this._loadRegistries(); await this.updateComplete; } @@ -201,7 +204,9 @@ class HassioRegistriesDialog extends LitElement { this._input = {}; } catch (err: any) { showAlertDialog(this, { - title: this.supervisor.localize("dialog.registries.failed_to_add"), + title: this.hass.localize( + "ui.panel.config.apps.dialog.registries.failed_to_add" + ), text: extractApiErrorMessage(err), }); } @@ -215,7 +220,9 @@ class HassioRegistriesDialog extends LitElement { await this._loadRegistries(); } catch (err: any) { showAlertDialog(this, { - title: this.supervisor.localize("dialog.registries.failed_to_remove"), + title: this.hass.localize( + "ui.panel.config.apps.dialog.registries.failed_to_remove" + ), text: extractApiErrorMessage(err), }); } @@ -250,6 +257,6 @@ class HassioRegistriesDialog extends LitElement { declare global { interface HTMLElementTagNameMap { - "dialog-hassio-registries": HassioRegistriesDialog; + "dialog-apps-registries": AppsRegistriesDialog; } } diff --git a/src/panels/config/apps/dialogs/registries/show-dialog-registries.ts b/src/panels/config/apps/dialogs/registries/show-dialog-registries.ts new file mode 100644 index 0000000000..4ebba836d6 --- /dev/null +++ b/src/panels/config/apps/dialogs/registries/show-dialog-registries.ts @@ -0,0 +1,10 @@ +import { fireEvent } from "../../../../../common/dom/fire_event"; +import "./dialog-registries"; + +export const showRegistriesDialog = (element: HTMLElement): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-apps-registries", + dialogImport: () => import("./dialog-registries"), + dialogParams: {}, + }); +}; diff --git a/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts b/src/panels/config/apps/dialogs/repositories/dialog-repositories.ts similarity index 70% rename from hassio/src/dialogs/repositories/dialog-hassio-repositories.ts rename to src/panels/config/apps/dialogs/repositories/dialog-repositories.ts index a0ab5f35f6..d4e9343820 100644 --- a/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts +++ b/src/panels/config/apps/dialogs/repositories/dialog-repositories.ts @@ -3,41 +3,44 @@ import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { fireEvent } from "../../../../src/common/dom/fire_event"; -import { caseInsensitiveStringCompare } from "../../../../src/common/string/compare"; -import "../../../../src/components/ha-alert"; -import "../../../../src/components/ha-button"; -import { createCloseHeading } from "../../../../src/components/ha-dialog"; -import "../../../../src/components/ha-icon-button"; -import "../../../../src/components/ha-md-list"; -import "../../../../src/components/ha-md-list-item"; -import "../../../../src/components/ha-svg-icon"; -import "../../../../src/components/ha-textfield"; -import type { HaTextField } from "../../../../src/components/ha-textfield"; -import "../../../../src/components/ha-tooltip"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { caseInsensitiveStringCompare } from "../../../../../common/string/compare"; +import "../../../../../components/ha-alert"; +import "../../../../../components/ha-button"; +import { createCloseHeading } from "../../../../../components/ha-dialog"; +import "../../../../../components/ha-icon-button"; +import "../../../../../components/ha-md-list"; +import "../../../../../components/ha-md-list-item"; +import "../../../../../components/ha-svg-icon"; +import "../../../../../components/ha-textfield"; +import type { HaTextField } from "../../../../../components/ha-textfield"; +import "../../../../../components/ha-tooltip"; import type { HassioAddonInfo, + HassioAddonsInfo, HassioAddonRepository, -} from "../../../../src/data/hassio/addon"; -import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; +} from "../../../../../data/hassio/addon"; +import { extractApiErrorMessage } from "../../../../../data/hassio/common"; import { addStoreRepository, fetchStoreRepositories, removeStoreRepository, -} from "../../../../src/data/supervisor/store"; -import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; -import type { HomeAssistant } from "../../../../src/types"; -import type { HassioRepositoryDialogParams } from "./show-dialog-repositories"; +} from "../../../../../data/supervisor/store"; +import { haStyle, haStyleDialog } from "../../../../../resources/styles"; +import type { HomeAssistant } from "../../../../../types"; +import type { RepositoryDialogParams } from "./show-dialog-repositories"; -@customElement("dialog-hassio-repositories") -class HassioRepositoriesDialog extends LitElement { +@customElement("dialog-apps-repositories") +class AppsRepositoriesDialog extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @query("#repository_input", true) private _optionInput?: HaTextField; @state() private _repositories?: HassioAddonRepository[]; - @state() private _dialogParams?: HassioRepositoryDialogParams; + @state() private _dialogParams?: RepositoryDialogParams; + + @state() private _addon?: HassioAddonsInfo; @state() private _opened = false; @@ -45,16 +48,16 @@ class HassioRepositoriesDialog extends LitElement { @state() private _error?: string; - public async showDialog( - dialogParams: HassioRepositoryDialogParams - ): Promise { + public async showDialog(dialogParams: RepositoryDialogParams): Promise { this._dialogParams = dialogParams; + this._addon = dialogParams.addon; this._opened = true; await this._loadData(); await this.updateComplete; } public closeDialog(): void { + this._dialogParams?.closeCallback?.(); this._dialogParams = undefined; this._opened = false; this._error = ""; @@ -64,9 +67,9 @@ class HassioRepositoriesDialog extends LitElement { repos .filter( (repo) => - repo.slug !== "core" && // The core add-ons repository - repo.slug !== "local" && // Locally managed add-ons - repo.slug !== "a0d7b954" && // Home Assistant Community Add-ons + repo.slug !== "core" && // The core apps repository + repo.slug !== "local" && // Locally managed apps + repo.slug !== "a0d7b954" && // Home Assistant Community Apps repo.slug !== "5c53de3b" && // The ESPHome repository repo.slug !== "d5369777" // Music Assistant repository ) @@ -85,13 +88,13 @@ class HassioRepositoriesDialog extends LitElement { ); protected render() { - if (!this._dialogParams?.supervisor || this._repositories === undefined) { + if (!this._addon || this._repositories === undefined) { return nothing; } const repositories = this._filteredRepositories(this._repositories); const usedRepositories = this._filteredUsedRepositories( repositories, - this._dialogParams.supervisor.addon.addons + this._addon.addons ); return html` ${this._error @@ -123,10 +126,10 @@ class HassioRepositoriesDialog extends LitElement { class="delete" slot="end" > - ${this._dialogParams!.supervisor.localize( + ${this.hass.localize( usedRepositories.includes(repo.slug) - ? "dialog.repositories.used" - : "dialog.repositories.remove" + ? "ui.panel.config.apps.dialog.repositories.used" + : "ui.panel.config.apps.dialog.repositories.remove" )}
    @@ -144,8 +147,8 @@ class HassioRepositoriesDialog extends LitElement { ` ) : html`${this._dialogParams!.supervisor.localize( - "dialog.repositories.no_repositories" + >${this.hass.localize( + "ui.panel.config.apps.dialog.repositories.no_repositories" )}`} @@ -153,9 +156,9 @@ class HassioRepositoriesDialog extends LitElement { - ${this._dialogParams!.supervisor.localize( - "dialog.repositories.add" + ${this.hass.localize( + "ui.panel.config.apps.dialog.repositories.add" )}
    - ${this._dialogParams?.supervisor.localize("common.close")} + ${this.hass.localize("ui.common.close")} `; @@ -197,8 +200,8 @@ class HassioRepositoriesDialog extends LitElement { margin-top: 4px; } ha-button { - margin-left: 8px; - margin-inline-start: 8px; + margin-left: var(--ha-space-2); + margin-inline-start: var(--ha-space-2); margin-inline-end: initial; } div.delete ha-icon-button { @@ -232,7 +235,7 @@ class HassioRepositoriesDialog extends LitElement { try { this._repositories = await fetchStoreRepositories(this.hass); - fireEvent(this, "supervisor-collection-refresh", { collection: "addon" }); + fireEvent(this, "apps-collection-refresh", { collection: "addon" }); } catch (err: any) { this._error = extractApiErrorMessage(err); } @@ -249,7 +252,7 @@ class HassioRepositoriesDialog extends LitElement { await addStoreRepository(this.hass, input.value); await this._loadData(); - fireEvent(this, "supervisor-collection-refresh", { collection: "store" }); + fireEvent(this, "apps-collection-refresh", { collection: "store" }); input.value = ""; } catch (err: any) { @@ -264,7 +267,7 @@ class HassioRepositoriesDialog extends LitElement { await removeStoreRepository(this.hass, slug); await this._loadData(); - fireEvent(this, "supervisor-collection-refresh", { collection: "store" }); + fireEvent(this, "apps-collection-refresh", { collection: "store" }); } catch (err: any) { this._error = extractApiErrorMessage(err); } @@ -273,6 +276,6 @@ class HassioRepositoriesDialog extends LitElement { declare global { interface HTMLElementTagNameMap { - "dialog-hassio-repositories": HassioRepositoriesDialog; + "dialog-apps-repositories": AppsRepositoriesDialog; } } diff --git a/src/panels/config/apps/dialogs/repositories/show-dialog-repositories.ts b/src/panels/config/apps/dialogs/repositories/show-dialog-repositories.ts new file mode 100644 index 0000000000..c91d149ba5 --- /dev/null +++ b/src/panels/config/apps/dialogs/repositories/show-dialog-repositories.ts @@ -0,0 +1,20 @@ +import { fireEvent } from "../../../../../common/dom/fire_event"; +import type { HassioAddonsInfo } from "../../../../../data/hassio/addon"; +import "./dialog-repositories"; + +export interface RepositoryDialogParams { + addon: HassioAddonsInfo; + url?: string; + closeCallback?: () => void; +} + +export const showRepositoriesDialog = ( + element: HTMLElement, + dialogParams: RepositoryDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-apps-repositories", + dialogImport: () => import("./dialog-repositories"), + dialogParams, + }); +}; diff --git a/src/panels/config/apps/ha-config-app-dashboard.ts b/src/panels/config/apps/ha-config-app-dashboard.ts new file mode 100644 index 0000000000..ea8bac2e5a --- /dev/null +++ b/src/panels/config/apps/ha-config-app-dashboard.ts @@ -0,0 +1,209 @@ +import { + mdiCogs, + mdiFileDocument, + mdiInformationVariant, + mdiTextBoxOutline, +} from "@mdi/js"; +import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; +import { css, html, LitElement } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { extractSearchParam } from "../../../common/url/search-params"; +import type { HassioAddonDetails } from "../../../data/hassio/addon"; +import { fetchHassioAddonInfo } from "../../../data/hassio/addon"; +import { extractApiErrorMessage } from "../../../data/hassio/common"; +import "../../../layouts/hass-error-screen"; +import "../../../layouts/hass-loading-screen"; +import "../../../layouts/hass-tabs-subpage"; +import type { PageNavigation } from "../../../layouts/hass-tabs-subpage"; +import { haStyle } from "../../../resources/styles"; +import type { HomeAssistant, Route } from "../../../types"; + +// Import app-view components +import "./app-view/supervisor-app-router"; + +@customElement("ha-config-app-dashboard") +class HaConfigAppDashboard extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public route!: Route; + + @property({ type: Boolean }) public narrow = false; + + @state() private _addon?: HassioAddonDetails; + + @state() private _error?: string; + + @state() private _controlEnabled = false; + + @state() private _fromStore = false; + + private _computeTail = memoizeOne((route: Route) => { + const pathParts = route.path.split("/").filter(Boolean); + // Path is like //info or //config + const slug = pathParts[0] || ""; + const subPath = pathParts.slice(1).join("/"); + + return { + prefix: route.prefix + "/" + slug, + path: subPath ? "/" + subPath : "", + }; + }); + + protected async firstUpdated(): Promise { + this._fromStore = extractSearchParam("store") === "true"; + await this._loadAddon(); + this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev)); + } + + protected updated(changedProperties: PropertyValues) { + if (changedProperties.has("route") && this.route) { + const oldRoute = changedProperties.get("route") as Route | undefined; + const oldSlug = oldRoute?.path.split("/")[1]; + const newSlug = this.route.path.split("/")[1]; + + if (oldSlug !== newSlug && newSlug) { + this._loadAddon(); + } + } + } + + protected render(): TemplateResult { + if (this._error) { + return html``; + } + + if (!this._addon) { + return html``; + } + + const addonTabs: PageNavigation[] = [ + { + translationKey: "ui.panel.config.apps.panel.info", + path: `/config/app/${this._addon.slug}/info`, + iconPath: mdiInformationVariant, + }, + ]; + + if (this._addon.documentation) { + addonTabs.push({ + translationKey: "ui.panel.config.apps.panel.documentation", + path: `/config/app/${this._addon.slug}/documentation`, + iconPath: mdiFileDocument, + }); + } + + if (this._addon.version) { + addonTabs.push( + { + translationKey: "ui.panel.config.apps.panel.configuration", + path: `/config/app/${this._addon.slug}/config`, + iconPath: mdiCogs, + }, + { + translationKey: "ui.panel.config.apps.panel.log", + path: `/config/app/${this._addon.slug}/logs`, + iconPath: mdiTextBoxOutline, + } + ); + } + + const route = this._computeTail(this.route); + + return html` + + ${this._addon.name} + + + `; + } + + private async _loadAddon(): Promise { + const slug = this.route.path.split("/")[1]; + if (!slug) { + this._error = "No addon specified"; + return; + } + + try { + this._addon = await fetchHassioAddonInfo(this.hass, slug); + } catch (err: any) { + this._error = `Error loading addon: ${extractApiErrorMessage(err)}`; + } + } + + private async _apiCalled(ev): Promise { + if (!ev.detail.success) { + return; + } + + const pathSplit: string[] = ev.detail.path?.split("/"); + + if (!pathSplit || pathSplit.length === 0) { + return; + } + + const path: string = pathSplit[pathSplit.length - 1]; + + if (["uninstall", "install", "update", "start", "stop"].includes(path)) { + fireEvent(this, "apps-collection-refresh", { + collection: "addon", + }); + } + + if (path === "uninstall") { + // Navigate back to installed apps after uninstall + window.history.back(); + } else { + // Reload app info + await this._loadAddon(); + } + } + + private _enableControl() { + this._controlEnabled = true; + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + :host { + color: var(--primary-text-color); + } + .content { + padding: var(--ha-space-6) 0 var(--ha-space-8); + display: flex; + flex-direction: column; + align-items: center; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-config-app-dashboard": HaConfigAppDashboard; + } +} diff --git a/src/panels/config/apps/ha-config-apps-available.ts b/src/panels/config/apps/ha-config-apps-available.ts new file mode 100644 index 0000000000..a0ef9fe4b2 --- /dev/null +++ b/src/panels/config/apps/ha-config-apps-available.ts @@ -0,0 +1,338 @@ +import { mdiDotsVertical } from "@mdi/js"; +import type { PropertyValues, TemplateResult } from "lit"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import type { HASSDomEvent } from "../../../common/dom/fire_event"; +import { navigate } from "../../../common/navigate"; +import { extractSearchParam } from "../../../common/url/search-params"; +import "../../../components/ha-dropdown"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; +import "../../../components/ha-icon-button"; +import "../../../components/search-input"; +import type { + HassioAddonRepository, + HassioAddonsInfo, +} from "../../../data/hassio/addon"; +import { + fetchHassioAddonsInfo, + reloadHassioAddons, +} from "../../../data/hassio/addon"; +import { extractApiErrorMessage } from "../../../data/hassio/common"; +import type { + StoreAddon, + SupervisorStore, +} from "../../../data/supervisor/store"; +import { fetchSupervisorStore } from "../../../data/supervisor/store"; +import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; +import "../../../layouts/hass-error-screen"; +import "../../../layouts/hass-loading-screen"; +import "../../../layouts/hass-subpage"; +import type { HomeAssistant, Route } from "../../../types"; +import { showRegistriesDialog } from "./dialogs/registries/show-dialog-registries"; +import { showRepositoriesDialog } from "./dialogs/repositories/show-dialog-repositories"; +import "./supervisor-apps-repository"; + +const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => { + if (a.slug === "local") { + return -1; + } + if (b.slug === "local") { + return 1; + } + if (a.slug === "core") { + return -1; + } + if (b.slug === "core") { + return 1; + } + return a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1; +}; + +@customElement("ha-config-apps-available") +export class HaConfigAppsAvailable extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public narrow = false; + + @property({ attribute: false }) public route!: Route; + + @state() private _store?: SupervisorStore; + + @state() private _addon?: HassioAddonsInfo; + + @state() private _error?: string; + + @state() private _filter?: string; + + public connectedCallback(): void { + super.connectedCallback(); + this.addEventListener( + "apps-collection-refresh", + this._handleCollectionRefresh as unknown as EventListener + ); + } + + public disconnectedCallback(): void { + super.disconnectedCallback(); + this.removeEventListener( + "apps-collection-refresh", + this._handleCollectionRefresh as unknown as EventListener + ); + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + const repositoryUrl = extractSearchParam("repository_url"); + navigate("/config/apps/available", { replace: true }); + this._loadData().then(() => { + if (repositoryUrl) { + this._manageRepositories(repositoryUrl); + } + }); + + this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev)); + } + + protected render() { + if (this._error) { + return html` + + `; + } + + if (!this._store || !this._addon) { + return html` + + `; + } + + let repos: (TemplateResult | typeof nothing)[] = []; + + if (this._store.repositories) { + repos = this._addonRepositories( + this._store.repositories, + this._store.addons, + this._filter + ); + } + + return html` + + + + + ${this.hass.localize("ui.panel.config.apps.store.check_updates")} + + + ${this.hass.localize("ui.panel.config.apps.store.repositories")} + + ${this.hass.userData?.showAdvanced + ? html` + ${this.hass.localize("ui.panel.config.apps.store.registries")} + ` + : nothing} + + ${repos.length === 0 + ? html`` + : html` + + + ${repos} + `} + ${!this.hass.userData?.showAdvanced + ? html` + + ` + : ""} + + `; + } + + private _addonRepositories = memoizeOne( + ( + repositories: HassioAddonRepository[], + addons: StoreAddon[], + filter?: string + ) => + repositories.sort(sortRepos).map((repo) => { + const filteredAddons = addons.filter( + (addon) => addon.repository === repo.slug + ); + + return filteredAddons.length !== 0 + ? html` + + ` + : nothing; + }) + ); + + private _handleAction(ev: HaDropdownSelectEvent) { + const action = ev.detail.item.value; + + if (!action) { + return; + } + + switch (action) { + case "check_updates": + this._refreshData(); + break; + case "repositories": + this._manageRepositoriesClicked(); + break; + case "registries": + this._manageRegistries(); + break; + } + } + + private async _refreshData() { + try { + await reloadHassioAddons(this.hass); + } catch (err) { + showAlertDialog(this, { + text: extractApiErrorMessage(err), + }); + } finally { + this._loadData(); + } + } + + private _apiCalled(ev) { + if (ev.detail.success) { + this._loadData(); + } + } + + private _manageRepositoriesClicked() { + this._manageRepositories(); + } + + private _manageRepositories(url?: string) { + showRepositoriesDialog(this, { + addon: this._addon!, + url, + closeCallback: () => this._loadData(), + }); + } + + private _manageRegistries() { + showRegistriesDialog(this); + } + + private async _loadData(): Promise { + try { + const [addon, store] = await Promise.all([ + fetchHassioAddonsInfo(this.hass), + fetchSupervisorStore(this.hass), + ]); + + this._addon = addon; + this._store = store; + } catch (err: any) { + this._error = + err.message || this.hass.localize("ui.panel.config.apps.error_loading"); + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.apps.error_loading"), + text: this._error, + }); + } + } + + private _handleCollectionRefresh = async ( + ev: HASSDomEvent<{ collection: "addon" | "store" }> + ): Promise => { + const { collection } = ev.detail; + try { + if (collection === "addon") { + this._addon = await fetchHassioAddonsInfo(this.hass); + } else if (collection === "store") { + this._store = await fetchSupervisorStore(this.hass); + } + } catch (_err: any) { + // Silently fail on refresh errors + } + }; + + private _filterChanged(e) { + this._filter = e.detail.value; + } + + static styles = css` + :host { + display: block; + height: 100%; + background-color: var(--primary-background-color); + } + supervisor-apps-repository { + margin-top: 24px; + } + .search { + position: sticky; + top: 0; + z-index: 2; + } + search-input { + display: block; + --mdc-text-field-fill-color: var(--sidebar-background-color); + --mdc-text-field-idle-line-color: var(--divider-color); + } + .advanced { + padding: 12px; + display: flex; + flex-wrap: wrap; + color: var(--primary-text-color); + } + .advanced a { + margin-left: 0.5em; + margin-inline-start: 0.5em; + margin-inline-end: initial; + color: var(--primary-color); + } + `; +} + +declare global { + interface HASSDomEvents { + "apps-collection-refresh": { collection: "addon" | "store" }; + } + interface HTMLElementTagNameMap { + "ha-config-apps-available": HaConfigAppsAvailable; + } +} diff --git a/src/panels/config/apps/ha-config-apps-installed.ts b/src/panels/config/apps/ha-config-apps-installed.ts new file mode 100644 index 0000000000..1bb13069bd --- /dev/null +++ b/src/panels/config/apps/ha-config-apps-installed.ts @@ -0,0 +1,296 @@ +import { + mdiArrowUpBoldCircle, + mdiPuzzle, + mdiRefresh, + mdiStorePlus, +} from "@mdi/js"; +import type { CSSResultGroup, TemplateResult } from "lit"; +import { css, html, LitElement } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { navigate } from "../../../common/navigate"; +import { caseInsensitiveStringCompare } from "../../../common/string/compare"; +import "../../../components/ha-card"; +import "../../../components/ha-fab"; +import "../../../components/ha-icon-button"; +import "../../../components/ha-svg-icon"; +import "../../../components/search-input"; +import type { + HassioAddonInfo, + HassioAddonsInfo, +} from "../../../data/hassio/addon"; +import { + fetchHassioAddonsInfo, + reloadHassioAddons, +} from "../../../data/hassio/addon"; +import { extractApiErrorMessage } from "../../../data/hassio/common"; +import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; +import "../../../layouts/hass-error-screen"; +import "../../../layouts/hass-loading-screen"; +import "../../../layouts/hass-subpage"; +import type { HomeAssistant, Route } from "../../../types"; +import "./components/supervisor-apps-card-content"; + +@customElement("ha-config-apps-installed") +export class HaConfigAppsInstalled extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public narrow = false; + + @property({ attribute: false }) public route!: Route; + + @state() private _addonInfo?: HassioAddonsInfo; + + @state() private _filter?: string; + + @state() private _error?: string; + + protected firstUpdated() { + this._loadData(); + } + + protected render(): TemplateResult { + if (this._error) { + return html` + + `; + } + + if (!this._addonInfo) { + return html` + + `; + } + + const addons = this._getAddons(this._addonInfo.addons, this._filter); + + return html` + + + +
    +
    + ${addons.length === 0 + ? html` + +
    + +
    +
    + ` + : addons.map( + (addon) => html` + +
    + +
    +
    + ` + )} +
    +
    + + + + + + +
    + `; + } + + private _getAddons = memoizeOne( + (addons: HassioAddonInfo[], filter?: string) => { + let filteredAddons = addons; + if (filter) { + const lowerCaseFilter = filter.toLowerCase(); + filteredAddons = addons.filter( + (addon) => + addon.name.toLowerCase().includes(lowerCaseFilter) || + addon.description.toLowerCase().includes(lowerCaseFilter) || + addon.slug.toLowerCase().includes(lowerCaseFilter) + ); + } + return filteredAddons.sort((a, b) => + caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language) + ); + } + ); + + private _handleSearchChange(ev: CustomEvent) { + this._filter = ev.detail.value; + } + + private async _loadData(): Promise { + try { + this._addonInfo = await fetchHassioAddonsInfo(this.hass); + } catch (err: any) { + this._error = + err.message || this.hass.localize("ui.panel.config.apps.error_loading"); + } + } + + private async _handleCheckUpdates() { + try { + await reloadHassioAddons(this.hass); + } catch (err) { + showAlertDialog(this, { + text: extractApiErrorMessage(err), + }); + } finally { + this._loadData(); + } + } + + private _addonTapped(ev: Event): void { + const addon = (ev.currentTarget as any).addon as HassioAddonInfo; + navigate(`/config/app/${addon.slug}/info`); + } + + private _openStore(): void { + navigate("/config/apps/available"); + } + + static styles: CSSResultGroup = css` + :host { + display: block; + height: 100%; + background-color: var(--primary-background-color); + } + + ha-card { + cursor: pointer; + overflow: hidden; + direction: ltr; + } + + .search { + position: sticky; + top: 0; + z-index: 2; + } + + search-input { + display: block; + --mdc-text-field-fill-color: var(--sidebar-background-color); + --mdc-text-field-idle-line-color: var(--divider-color); + } + + .content { + padding: var(--ha-space-4); + margin-bottom: var(--ha-space-18); + } + + .card-group { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + grid-gap: var(--ha-space-2); + } + + .card-content { + display: flex; + justify-content: space-between; + padding: var(--ha-space-4); + } + + button.link { + color: var(--primary-color); + background: none; + border: none; + padding: 0; + font: inherit; + text-align: left; + text-decoration: underline; + cursor: pointer; + } + + ha-fab { + position: fixed; + right: calc(var(--ha-space-4) + var(--safe-area-inset-right)); + bottom: calc(var(--ha-space-4) + var(--safe-area-inset-bottom)); + inset-inline-end: calc(var(--ha-space-4) + var(--safe-area-inset-right)); + inset-inline-start: initial; + z-index: 1; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-config-apps-installed": HaConfigAppsInstalled; + } +} diff --git a/src/panels/config/apps/ha-config-apps.ts b/src/panels/config/apps/ha-config-apps.ts new file mode 100644 index 0000000000..95f8a8003c --- /dev/null +++ b/src/panels/config/apps/ha-config-apps.ts @@ -0,0 +1,45 @@ +import { customElement, property } from "lit/decorators"; +import type { RouterOptions } from "../../../layouts/hass-router-page"; +import { HassRouterPage } from "../../../layouts/hass-router-page"; +import type { HomeAssistant, Route } from "../../../types"; + +@customElement("ha-config-apps") +class HaConfigApps extends HassRouterPage { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public narrow = false; + + @property({ attribute: "is-wide", type: Boolean }) public isWide = false; + + @property({ attribute: false }) public showAdvanced = false; + + @property({ attribute: false }) public route!: Route; + + protected routerOptions: RouterOptions = { + defaultPage: "installed", + routes: { + installed: { + tag: "ha-config-apps-installed", + load: () => import("./ha-config-apps-installed"), + }, + available: { + tag: "ha-config-apps-available", + load: () => import("./ha-config-apps-available"), + }, + }, + }; + + protected updatePageEl(pageEl) { + pageEl.hass = this.hass; + pageEl.narrow = this.narrow; + pageEl.isWide = this.isWide; + pageEl.showAdvanced = this.showAdvanced; + pageEl.route = this.routeTail; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-config-apps": HaConfigApps; + } +} diff --git a/hassio/src/resources/hassio-style.ts b/src/panels/config/apps/resources/supervisor-apps-style.ts similarity index 77% rename from hassio/src/resources/hassio-style.ts rename to src/panels/config/apps/resources/supervisor-apps-style.ts index e1872faabb..9e3d149bf5 100644 --- a/hassio/src/resources/hassio-style.ts +++ b/src/panels/config/apps/resources/supervisor-apps-style.ts @@ -1,8 +1,8 @@ import { css } from "lit"; -export const hassioStyle = css` +export const supervisorAppsStyle = css` .content { - margin: 8px; + margin: var(--ha-space-2); } h1, .description, @@ -11,21 +11,21 @@ export const hassioStyle = css` } h1 { font-size: 2em; - margin-bottom: 8px; + margin-bottom: var(--ha-space-2); font-family: var(--ha-font-family-body); -webkit-font-smoothing: var(--ha-font-smoothing); -moz-osx-font-smoothing: var(--ha-moz-osx-font-smoothing); font-size: var(--ha-font-size-2xl); font-weight: var(--ha-font-weight-normal); line-height: var(--ha-line-height-condensed); - padding-left: 8px; - padding-inline-start: 8px; + padding-left: var(--ha-space-2); + padding-inline-start: var(--ha-space-2); padding-inline-end: initial; } .description { - margin-top: 4px; - padding-left: 8px; - padding-inline-start: 8px; + margin-top: var(--ha-space-1); + padding-left: var(--ha-space-2); + padding-inline-start: var(--ha-space-2); padding-inline-end: initial; } .card-group { @@ -50,6 +50,6 @@ export const hassioStyle = css` } .error { color: var(--error-color); - margin-top: 16px; + margin-top: var(--ha-space-4); } `; diff --git a/hassio/src/addon-store/hassio-addon-repository.ts b/src/panels/config/apps/supervisor-apps-repository.ts similarity index 66% rename from hassio/src/addon-store/hassio-addon-repository.ts rename to src/panels/config/apps/supervisor-apps-repository.ts index 59ceab9990..474a7a40c2 100644 --- a/hassio/src/addon-store/hassio-addon-repository.ts +++ b/src/panels/config/apps/supervisor-apps-repository.ts @@ -3,24 +3,20 @@ import type { CSSResultGroup, TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { atLeastVersion } from "../../../src/common/config/version"; -import { navigate } from "../../../src/common/navigate"; -import { caseInsensitiveStringCompare } from "../../../src/common/string/compare"; -import "../../../src/components/ha-card"; -import type { HassioAddonRepository } from "../../../src/data/hassio/addon"; -import type { StoreAddon } from "../../../src/data/supervisor/store"; -import type { Supervisor } from "../../../src/data/supervisor/supervisor"; -import type { HomeAssistant } from "../../../src/types"; -import "../components/hassio-card-content"; -import { filterAndSort } from "../components/hassio-filter-addons"; -import { hassioStyle } from "../resources/hassio-style"; +import { navigate } from "../../../common/navigate"; +import { caseInsensitiveStringCompare } from "../../../common/string/compare"; +import "../../../components/ha-card"; +import type { HassioAddonRepository } from "../../../data/hassio/addon"; +import type { StoreAddon } from "../../../data/supervisor/store"; +import type { HomeAssistant } from "../../../types"; +import "./components/supervisor-apps-card-content"; +import { filterAndSort } from "./components/supervisor-apps-filter"; +import { supervisorAppsStyle } from "./resources/supervisor-apps-style"; -@customElement("hassio-addon-repository") -export class HassioAddonRepositoryEl extends LitElement { +@customElement("supervisor-apps-repository") +export class SupervisorAppsRepositoryEl extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public supervisor!: Supervisor; - @property({ attribute: false }) public repo!: HassioAddonRepository; @property({ attribute: false }) public addons!: StoreAddon[]; @@ -50,9 +46,12 @@ export class HassioAddonRepositoryEl extends LitElement { return html`

    - ${this.supervisor.localize("store.no_results_found", { - repository: repo.name, - })} + ${this.hass.localize( + "ui.panel.config.apps.store.no_results_found", + { + repository: repo.name, + } + )}

    `; @@ -70,7 +69,7 @@ export class HassioAddonRepositoryEl extends LitElement { @click=${this._addonTapped} >
    - + >
    ` @@ -120,12 +121,12 @@ export class HassioAddonRepositoryEl extends LitElement { } private _addonTapped(ev) { - navigate(`/hassio/addon/${ev.currentTarget.addon.slug}?store=true`); + navigate(`/config/app/${ev.currentTarget.addon.slug}/info?store=true`); } static get styles(): CSSResultGroup { return [ - hassioStyle, + supervisorAppsStyle, css` ha-card { cursor: pointer; @@ -144,6 +145,6 @@ export class HassioAddonRepositoryEl extends LitElement { declare global { interface HTMLElementTagNameMap { - "hassio-addon-repository": HassioAddonRepositoryEl; + "supervisor-apps-repository": SupervisorAppsRepositoryEl; } } diff --git a/src/panels/config/areas/dialog-area-registry-detail.ts b/src/panels/config/areas/dialog-area-registry-detail.ts index 60ff671fe7..4223185d5a 100644 --- a/src/panels/config/areas/dialog-area-registry-detail.ts +++ b/src/panels/config/areas/dialog-area-registry-detail.ts @@ -16,8 +16,11 @@ import "../../../components/ha-labels-picker"; import "../../../components/ha-picture-upload"; import type { HaPictureUpload } from "../../../components/ha-picture-upload"; import "../../../components/ha-settings-row"; +import "../../../components/ha-suggest-with-ai-button"; +import type { SuggestWithAIGenerateTask } from "../../../components/ha-suggest-with-ai-button"; import "../../../components/ha-textfield"; import "../../../components/ha-wa-dialog"; +import type { GenDataTaskResult } from "../../../data/ai_task"; import type { AreaRegistryEntry, AreaRegistryEntryMutableParams, @@ -32,6 +35,14 @@ import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box import type { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog"; import { haStyleDialog } from "../../../resources/styles"; import type { HomeAssistant, ValueChangedEvent } from "../../../types"; +import { + type MetadataSuggestionInclude, + type MetadataSuggestionResult, + generateMetadataSuggestionTask, + processMetadataSuggestion, +} from "../common/suggest-metadata-ai"; +import { fetchLabels } from "../common/suggest-metadata-helpers"; +import { buildAreaMetadataInspirations } from "../common/suggest-metadata-inspirations"; import type { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail"; const cropOptions: CropOptions = { @@ -71,10 +82,16 @@ class DialogAreaDetail @state() private _params?: AreaRegistryDetailDialogParams; - @state() private _submitting?: boolean; + @state() private _submitting = false; @state() private _open = false; + @state() private _suggestionInclude: MetadataSuggestionInclude = { + name: true, + labels: true, + floor: true, + }; + public async showDialog( params: AreaRegistryDetailDialogParams ): Promise { @@ -242,6 +259,76 @@ class DialogAreaDetail `; } + private async _getLabelNames(): Promise { + if (!this._labels.length) { + return []; + } + const labels = await fetchLabels(this.hass.connection); + return this._labels + .map((labelId) => labels[labelId]) + .filter((name): name is string => Boolean(name)); + } + + private _generateTask = async (): Promise => { + this._suggestionInclude = { + ...this._suggestionInclude, + name: this._name.trim() === "", + }; + + return generateMetadataSuggestionTask<{ + name: string; + aliases: string[]; + labels: string[]; + floor: string | null; + temperature_entity: string | null; + humidity_entity: string | null; + }>( + this.hass.connection, + this.hass.language, + "area", + { + name: this._name, + aliases: this._aliases, + labels: await this._getLabelNames(), + floor: this._floor ? this.hass.floors?.[this._floor]?.name : null, + temperature_entity: this._temperatureEntity + ? (this.hass.states[this._temperatureEntity]?.attributes + ?.friendly_name ?? null) + : null, + humidity_entity: this._humidityEntity + ? (this.hass.states[this._humidityEntity]?.attributes + ?.friendly_name ?? null) + : null, + }, + await buildAreaMetadataInspirations(this.hass.connection), + this._suggestionInclude + ); + }; + + private async _handleSuggestion( + event: CustomEvent> + ) { + const result = event.detail; + const processed = await processMetadataSuggestion( + this.hass.connection, + "area", + result, + this._suggestionInclude + ); + + if (processed.name) { + this._name = processed.name; + } + + if (processed.labels?.length) { + this._labels = processed.labels; + } + + if (processed.floor) { + this._floor = processed.floor; + } + } + protected render() { if (!this._params) { return nothing; @@ -259,6 +346,12 @@ class DialogAreaDetail : this.hass.localize("ui.panel.config.areas.editor.create_area")} @closed=${this._dialogClosed} > +
    ${this._error ? html`${this._error}` @@ -285,7 +378,7 @@ class DialogAreaDetail ${entry ? this.hass.localize("ui.common.save") @@ -423,13 +516,16 @@ class DialogAreaDetail ha-picture-upload, ha-expansion-panel { display: block; - margin-bottom: 16px; + margin-bottom: var(--ha-space-4); } .content { - padding: 12px; + padding: var(--ha-space-3); } .description { - margin: 0 0 16px 0; + margin: 0 0 var(--ha-space-4) 0; + } + ha-suggest-with-ai-button { + margin: var(--ha-space-2) var(--ha-space-4); } `, ]; diff --git a/src/panels/config/areas/ha-config-area-page.ts b/src/panels/config/areas/ha-config-area-page.ts index 3b20d744ef..e6ed3a10c8 100644 --- a/src/panels/config/areas/ha-config-area-page.ts +++ b/src/panels/config/areas/ha-config-area-page.ts @@ -16,12 +16,12 @@ import { slugify } from "../../../common/string/slugify"; import { groupBy } from "../../../common/util/group-by"; import { afterNextRender } from "../../../common/util/render-status"; import "../../../components/ha-button"; -import "../../../components/ha-button-menu"; import "../../../components/ha-card"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-next"; import "../../../components/ha-list"; -import "../../../components/ha-list-item"; import "../../../components/ha-tooltip"; import type { AreaRegistryEntry } from "../../../data/area/area_registry"; import { @@ -52,6 +52,7 @@ import { loadAreaRegistryDetailDialog, showAreaRegistryDetailDialog, } from "./show-dialog-area-registry-detail"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; declare interface NameAndEntity { name: string; @@ -226,32 +227,23 @@ class HaConfigAreaPage extends LitElement { >` : nothing}${area.name}`} > - + - + + ${this.hass.localize("ui.panel.config.areas.edit_settings")} - - + - + + ${this.hass.localize("ui.panel.config.areas.editor.delete")} - - - - + +
    @@ -613,6 +605,19 @@ class HaConfigAreaPage extends LitElement { this._related = await findRelated(this.hass, "area", this.areaId); } + private _handleMenuAction(ev: HaDropdownSelectEvent) { + const action = ev.detail?.item?.value; + const entry = (ev.detail?.item as any)?.data as AreaRegistryEntry; + switch (action) { + case "edit": + this._openDialog(entry); + break; + case "delete": + this._deleteConfirm(); + break; + } + } + private _showSettings(ev: MouseEvent) { const entry: AreaRegistryEntry = (ev.currentTarget! as any).entry; this._openDialog(entry); diff --git a/src/panels/config/areas/ha-config-areas-dashboard.ts b/src/panels/config/areas/ha-config-areas-dashboard.ts index 502df07cc5..1c44bbfa6f 100644 --- a/src/panels/config/areas/ha-config-areas-dashboard.ts +++ b/src/panels/config/areas/ha-config-areas-dashboard.ts @@ -1,4 +1,4 @@ -import type { ActionDetail } from "@material/mwc-list"; +import "@home-assistant/webawesome/dist/components/divider/divider"; import { mdiDelete, mdiDotsVertical, @@ -24,10 +24,11 @@ import { type AreasFloorHierarchy, } from "../../../common/areas/areas-floor-hierarchy"; import { formatListWithAnds } from "../../../common/string/format-list"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-fab"; import "../../../components/ha-floor-icon"; import "../../../components/ha-icon-button"; -import "../../../components/ha-list-item"; import "../../../components/ha-sortable"; import type { HaSortableOptions } from "../../../components/ha-sortable"; import "../../../components/ha-svg-icon"; @@ -57,6 +58,7 @@ import { } from "./show-dialog-area-registry-detail"; import { showAreasFloorsOrderDialog } from "./show-dialog-areas-floors-order"; import { showFloorRegistryDetailDialog } from "./show-dialog-floor-registry-detail"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; const UNASSIGNED_FLOOR = "__unassigned__"; @@ -195,44 +197,44 @@ export class HaConfigAreasDashboard extends LitElement { ${floor.name}

    - - ${this.hass.localize( "ui.panel.config.areas.picker.reorder" - )} -
  • - + ${this.hass.localize( "ui.panel.config.areas.picker.floor.edit_floor" - )} - ${this.hass.localize( "ui.panel.config.areas.picker.floor.delete_floor" - )} -
    +
    - - ${this.hass.localize( "ui.panel.config.areas.picker.reorder" - )} - +
    ) { + private _handleFloorAction(ev: HaDropdownSelectEvent) { const floor = (ev.currentTarget as any).floor; - switch (ev.detail.index) { - case 0: + const action = ev.detail.item.value; + switch (action) { + case "reorder": this._showReorderDialog(); break; - case 1: + case "edit": this._editFloor(floor); break; - case 2: + case "delete": this._deleteFloor(floor); break; } } - private _handleUnassignedAreasAction(ev: CustomEvent) { - if (ev.detail.index === 0) { + private _handleUnassignedAreasAction(ev: HaDropdownSelectEvent) { + const action = ev.detail.item.value; + if (action === "reorder") { this._showReorderDialog(); } } @@ -720,9 +725,6 @@ export class HaConfigAreasDashboard extends LitElement { align-items: center; overflow-wrap: anywhere; } - .warning { - color: var(--error-color); - } `; } diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts index fdfd8d63b1..da62521949 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -36,7 +36,6 @@ import type { HaAutomationRow } from "../../../../components/ha-automation-row"; import "../../../../components/ha-card"; import "../../../../components/ha-dropdown"; import "../../../../components/ha-dropdown-item"; -import type { HaDropdownItem } from "../../../../components/ha-dropdown-item"; import "../../../../components/ha-expansion-panel"; import "../../../../components/ha-icon-button"; import "../../../../components/ha-service-icon"; @@ -92,6 +91,7 @@ import "./types/ha-automation-action-set_conversation_response"; import "./types/ha-automation-action-stop"; import "./types/ha-automation-action-wait_for_trigger"; import "./types/ha-automation-action-wait_template"; +import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown"; export const getAutomationActionType = memoizeOne( (action: Action | undefined) => { @@ -862,7 +862,7 @@ export default class HaAutomationActionRow extends LitElement { this._automationRowElement?.focus(); } - private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { ev.stopPropagation(); const action = ev.detail?.item?.value; diff --git a/src/panels/config/automation/action/types/ha-automation-action-condition.ts b/src/panels/config/automation/action/types/ha-automation-action-condition.ts index 9c99aceb1d..f1c8731588 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-condition.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-condition.ts @@ -2,13 +2,12 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../../common/dom/fire_event"; -import { stopPropagation } from "../../../../../common/dom/stop_propagation"; import { stringCompare } from "../../../../../common/string/compare"; import type { LocalizeFunc } from "../../../../../common/translations/localize"; import { CONDITION_ICONS } from "../../../../../components/ha-condition-icon"; -import "../../../../../components/ha-list-item"; +import "../../../../../components/ha-dropdown-item"; import "../../../../../components/ha-select"; -import type { HaSelect } from "../../../../../components/ha-select"; +import type { HaSelectSelectEvent } from "../../../../../components/ha-select"; import { DYNAMIC_PREFIX, getValueFromDynamic, @@ -85,37 +84,47 @@ export class HaConditionAction this.action.condition ); + const value = + this.action.condition in this._conditionDescriptions + ? `${DYNAMIC_PREFIX}${this.action.condition}` + : this.action.condition; + + let valueLabel = value; + + const items = html`${this._processedTypes( + this._conditionDescriptions, + this.hass.localize + ).map(([opt, label, condition]) => { + const selected = value === opt; + + if (selected) { + valueLabel = label; + } + + return html` + + + ${label} + + `; + })}`; + return html` ${this.inSidebar || (!this.inSidebar && !this.indent) ? html` - ${this._processedTypes( - this._conditionDescriptions, - this.hass.localize - ).map( - ([opt, label, condition]) => html` - - ${label} - - - ` - )} + ${items} ` : nothing} @@ -192,8 +201,8 @@ export class HaConditionAction }); } - private _typeChanged(ev: CustomEvent) { - const type = (ev.target as HaSelect).value; + private _typeChanged(ev: HaSelectSelectEvent) { + const type = ev.detail.value; if (!type) { return; @@ -242,6 +251,7 @@ export class HaConditionAction static styles = css` ha-select { margin-bottom: 24px; + display: block; } `; } diff --git a/src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger.ts b/src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger.ts index 939d79bf83..496e00d60a 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger.ts @@ -8,7 +8,7 @@ import "../../../../../components/ha-duration-input"; import "../../../../../components/ha-formfield"; import "../../../../../components/ha-textfield"; import type { WaitForTriggerAction } from "../../../../../data/script"; -import type { HomeAssistant } from "../../../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../../../types"; import "../../trigger/ha-automation-trigger"; import type { ActionElement } from "../ha-automation-action-row"; import { handleChangeEvent } from "../ha-automation-action-row"; @@ -78,7 +78,7 @@ export class HaWaitForTriggerAction `; } - private _timeoutChanged(ev: CustomEvent<{ value: TimeChangedEvent }>): void { + private _timeoutChanged(ev: ValueChangedEvent): void { ev.stopPropagation(); const value = ev.detail.value; fireEvent(this, "value-changed", { diff --git a/src/panels/config/automation/add-automation-element-dialog.ts b/src/panels/config/automation/add-automation-element-dialog.ts index 5d0db1d5b4..30a964a2d6 100644 --- a/src/panels/config/automation/add-automation-element-dialog.ts +++ b/src/panels/config/automation/add-automation-element-dialog.ts @@ -1,3 +1,4 @@ +import "@home-assistant/webawesome/dist/components/divider/divider"; import { consume } from "@lit/context"; import { mdiAppleKeyboardCommand, @@ -42,7 +43,6 @@ import "../../../components/ha-icon"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button-prev"; import "../../../components/ha-icon-next"; -import "../../../components/ha-md-divider"; import "../../../components/ha-md-list"; import "../../../components/ha-md-list-item"; import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box"; @@ -114,7 +114,7 @@ import { } from "../../../data/trigger"; import type { HassDialog } from "../../../dialogs/make-dialog-manager"; import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin"; -import type { HomeAssistant } from "../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../types"; import { isMac } from "../../../util/is_mac"; import { showToast } from "../../../util/toast"; import "./add-automation-element/ha-automation-add-from-target"; @@ -657,10 +657,7 @@ class DialogAddAutomationElement .path=${mdiPlus} > - ` + ` : nothing} ${collections.map( (collection, index) => html` @@ -1752,7 +1749,7 @@ class DialogAddAutomationElement this.closeDialog(); } - private _selected(ev: CustomEvent<{ value: string }>) { + private _selected(ev: ValueChangedEvent) { let target: HassServiceTarget | undefined; if ( this._tab === "targets" && @@ -1766,7 +1763,7 @@ class DialogAddAutomationElement } private _handleTargetSelected = ( - ev: CustomEvent<{ value: SingleHassServiceTarget }> + ev: ValueChangedEvent ) => { this._targetItems = undefined; this._loadItemsError = false; @@ -2177,8 +2174,8 @@ class DialogAddAutomationElement width: var(--ha-space-6); } - ha-md-list-item.paste { - border-bottom: 1px solid var(--ha-color-border-neutral-quiet); + wa-divider { + --spacing: 0; } ha-svg-icon.plus { diff --git a/src/panels/config/automation/automation-save-dialog/dialog-automation-save.ts b/src/panels/config/automation/automation-save-dialog/dialog-automation-save.ts index 30bbe60ae0..25ed3ec723 100644 --- a/src/panels/config/automation/automation-save-dialog/dialog-automation-save.ts +++ b/src/panels/config/automation/automation-save-dialog/dialog-automation-save.ts @@ -1,5 +1,4 @@ import { mdiPlus } from "@mdi/js"; -import { dump } from "js-yaml"; import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -21,13 +20,10 @@ import "../../../../components/ha-textfield"; import "../../../../components/ha-wa-dialog"; import "../../category/ha-category-picker"; -import { computeStateDomain } from "../../../../common/entity/compute_state_domain"; import { supportsMarkdownHelper } from "../../../../common/translations/markdown_support"; -import { subscribeOne } from "../../../../common/util/subscribe-one"; import type { GenDataTaskResult } from "../../../../data/ai_task"; -import { fetchCategoryRegistry } from "../../../../data/category_registry"; -import { subscribeEntityRegistry } from "../../../../data/entity/entity_registry"; -import { subscribeLabelRegistry } from "../../../../data/label/label_registry"; +import type { AutomationConfig } from "../../../../data/automation"; +import type { ScriptConfig } from "../../../../data/script"; import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyle, haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; @@ -35,6 +31,12 @@ import type { EntityRegistryUpdate, SaveDialogParams, } from "./show-dialog-automation-save"; +import { + type MetadataSuggestionResult, + generateMetadataSuggestionTask, + processMetadataSuggestion, +} from "../../common/suggest-metadata-ai"; +import { buildEntityMetadataInspirations } from "../../common/suggest-metadata-inspirations"; @customElement("ha-dialog-automation-save") class DialogAutomationSave extends LitElement implements HassDialog { @@ -333,184 +335,64 @@ class DialogAutomationSave extends LitElement implements HassDialog { this.closeDialog(); } - private _getSuggestData() { - return Promise.all([ - subscribeOne(this.hass.connection, subscribeLabelRegistry).then((labs) => - Object.fromEntries(labs.map((lab) => [lab.label_id, lab.name])) - ), - subscribeOne(this.hass.connection, subscribeEntityRegistry).then((ents) => - Object.fromEntries(ents.map((ent) => [ent.entity_id, ent])) - ), - fetchCategoryRegistry(this.hass.connection, "automation").then((cats) => - Object.fromEntries(cats.map((cat) => [cat.category_id, cat.name])) - ), - ]); - } - private _generateTask = async (): Promise => { if (!this._params) { throw new Error("Dialog params not set"); } - - const [labels, entities, categories] = await this._getSuggestData(); - const inspirations: string[] = []; - - const domain = this._params.domain; - - for (const entity of Object.values(this.hass.states)) { - const entityEntry = entities[entity.entity_id]; - if ( - computeStateDomain(entity) !== domain || - entity.attributes.restored || - !entity.attributes.friendly_name || - !entityEntry - ) { - continue; - } - - let inspiration = `- ${entity.attributes.friendly_name}`; - - const category = categories[entityEntry.categories.automation]; - if (category) { - inspiration += ` (category: ${category})`; - } - - if (entityEntry.labels.length) { - inspiration += ` (labels: ${entityEntry.labels - .map((label) => labels[label]) - .join(", ")})`; - } - - inspirations.push(inspiration); - } - - const term = this._params.domain === "script" ? "script" : "automation"; - - return { - type: "data", - task: { - task_name: `frontend__${term}__save`, - instructions: `Suggest in language "${this.hass.language}" a name, description, category and labels for the following Home Assistant ${term}. - -The name should be relevant to the ${term}'s purpose. -${ - inspirations.length - ? `The name should be in same style and sentence capitalization as existing ${term}s. -Suggest a category and labels if relevant to the ${term}'s purpose. -Only suggest category and labels that are already used by existing ${term}s.` - : `The name should be short, descriptive, sentence case, and written in the language ${this.hass.language}.` -} -If the ${term} contains 5+ steps, include a short description. - -For inspiration, here are existing ${term}s: -${inspirations.join("\n")} - -The ${term} configuration is as follows: - -${dump(this._params.config)} -`, - structure: { - name: { - description: "The name of the automation", - required: true, - selector: { - text: {}, - }, - }, - description: { - description: "A short description of the automation", - required: false, - selector: { - text: {}, - }, - }, - labels: { - description: "Labels for the automation", - required: false, - selector: { - text: { - multiple: true, - }, - }, - }, - category: { - description: "The category of the automation", - required: false, - selector: { - select: { - options: Object.entries(categories).map(([id, name]) => ({ - value: id, - label: name, - })), - }, - }, - }, - }, - }, - }; + return generateMetadataSuggestionTask( + this.hass.connection, + this.hass.language, + this._params.domain, + this._params.config, + await buildEntityMetadataInspirations( + this.hass.connection, + this.hass.states, + this._params.domain + ) + ); }; private async _handleSuggestion( - event: CustomEvent< - GenDataTaskResult<{ - name: string; - description?: string; - category?: string; - labels?: string[]; - }> - > + event: CustomEvent> ) { + if (!this._params) { + throw new Error("Dialog params not set"); + } const result = event.detail; - const [labels, _entities, categories] = await this._getSuggestData(); + const processed = await processMetadataSuggestion( + this.hass.connection, + this._params.domain, + result + ); - this._newName = result.data.name; - if (result.data.description) { - this._newDescription = result.data.description; + if (processed.name) { + this._newName = processed.name; + } + + if (processed.description) { + this._newDescription = processed.description; if (!this._visibleOptionals.includes("description")) { this._visibleOptionals = [...this._visibleOptionals, "description"]; } } - if (result.data.category) { - // We get back category name, convert it to ID - const categoryId = Object.entries(categories).find( - ([, name]) => name === result.data.category - )?.[0]; - if (categoryId) { - this._entryUpdates = { - ...this._entryUpdates, - category: categoryId, - }; - if (!this._visibleOptionals.includes("category")) { - this._visibleOptionals = [...this._visibleOptionals, "category"]; - } + + if (processed.category) { + this._entryUpdates = { + ...this._entryUpdates, + category: processed.category, + }; + if (!this._visibleOptionals.includes("category")) { + this._visibleOptionals = [...this._visibleOptionals, "category"]; } } - if (result.data.labels?.length) { - // We get back label names, convert them to IDs - const newLabels: Record = Object.fromEntries( - result.data.labels.map((name) => [name, undefined]) - ); - let toFind = result.data.labels.length; - for (const [labelId, labelName] of Object.entries(labels)) { - if (labelName in newLabels && newLabels[labelName] === undefined) { - newLabels[labelName] = labelId; - toFind--; - if (toFind === 0) { - break; - } - } - } - const foundLabels = Object.values(newLabels).filter( - (labelId) => labelId !== undefined - ); - if (foundLabels.length) { - this._entryUpdates = { - ...this._entryUpdates, - labels: foundLabels, - }; - if (!this._visibleOptionals.includes("labels")) { - this._visibleOptionals = [...this._visibleOptionals, "labels"]; - } + + if (processed.labels?.length) { + this._entryUpdates = { + ...this._entryUpdates, + labels: processed.labels, + }; + if (!this._visibleOptionals.includes("labels")) { + this._visibleOptionals = [...this._visibleOptionals, "labels"]; } } } @@ -555,7 +437,8 @@ ${dump(this._params.config)} haStyleDialog, css` ha-wa-dialog { - --dialog-content-padding: 0 24px 24px 24px; + --dialog-content-padding: 0 var(--ha-space-6) var(--ha-space-6) + var(--ha-space-6); } ha-textfield, @@ -571,15 +454,15 @@ ${dump(this._params.config)} ha-labels-picker, ha-area-picker, ha-chip-set:has(> ha-assist-chip) { - margin-top: 16px; + margin-top: var(--ha-space-4); } ha-alert { display: block; - margin-bottom: 16px; + margin-bottom: var(--ha-space-4); } ha-suggest-with-ai-button { - margin: 8px 16px; + margin: var(--ha-space-2) var(--ha-space-4); } `, ]; diff --git a/src/panels/config/automation/automation-save-timeout-dialog/show-dialog-automation-save-timeout.ts b/src/panels/config/automation/automation-save-timeout-dialog/show-dialog-automation-save-timeout.ts index 317864c377..d0e587ac05 100644 --- a/src/panels/config/automation/automation-save-timeout-dialog/show-dialog-automation-save-timeout.ts +++ b/src/panels/config/automation/automation-save-timeout-dialog/show-dialog-automation-save-timeout.ts @@ -6,7 +6,7 @@ export const loadAutomationSaveTimeoutDialog = () => export interface AutomationSaveTimeoutDialogParams { onClose?: () => void; savedPromise: Promise; - type: "automation" | "script"; + type: "automation" | "script" | "scene"; } export const showAutomationSaveTimeoutDialog = ( diff --git a/src/panels/config/automation/condition/ha-automation-condition-row.ts b/src/panels/config/automation/condition/ha-automation-condition-row.ts index 0330a024d2..c2e27ed69f 100644 --- a/src/panels/config/automation/condition/ha-automation-condition-row.ts +++ b/src/panels/config/automation/condition/ha-automation-condition-row.ts @@ -37,7 +37,6 @@ import "../../../../components/ha-card"; import "../../../../components/ha-condition-icon"; import "../../../../components/ha-dropdown"; import "../../../../components/ha-dropdown-item"; -import type { HaDropdownItem } from "../../../../components/ha-dropdown-item"; import "../../../../components/ha-expansion-panel"; import "../../../../components/ha-icon-button"; import type { @@ -52,6 +51,7 @@ import type { ConditionDescriptions } from "../../../../data/condition"; import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition"; import { validateConfig } from "../../../../data/config"; import { fullEntitiesContext } from "../../../../data/context"; +import type { DeviceCondition } from "../../../../data/device/device_automation"; import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry"; import { showAlertDialog, @@ -76,7 +76,7 @@ import "./types/ha-automation-condition-template"; import "./types/ha-automation-condition-time"; import "./types/ha-automation-condition-trigger"; import "./types/ha-automation-condition-zone"; -import type { DeviceCondition } from "../../../../data/device/device_automation"; +import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown"; export interface ConditionElement extends LitElement { condition: Condition; @@ -84,31 +84,6 @@ export interface ConditionElement extends LitElement { collapseAll?: () => void; } -export const handleChangeEvent = ( - element: ConditionElement, - ev: CustomEvent -) => { - ev.stopPropagation(); - const name = (ev.currentTarget as any)?.name; - if (!name) { - return; - } - const newVal = ev.detail?.value || (ev.currentTarget as any)?.value; - - if ((element.condition[name] || "") === newVal) { - return; - } - - let newCondition: Condition; - if (!newVal) { - newCondition = { ...element.condition }; - delete newCondition[name]; - } else { - newCondition = { ...element.condition, [name]: newVal }; - } - fireEvent(element, "value-changed", { value: newCondition }); -}; - @customElement("ha-automation-condition-row") export default class HaAutomationConditionRow extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -838,7 +813,7 @@ export default class HaAutomationConditionRow extends LitElement { this._automationRowElement?.focus(); } - private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { ev.stopPropagation(); const action = ev.detail?.item?.value; diff --git a/src/panels/config/automation/condition/ha-automation-condition.ts b/src/panels/config/automation/condition/ha-automation-condition.ts index 31bcc063fc..b1dff0e259 100644 --- a/src/panels/config/automation/condition/ha-automation-condition.ts +++ b/src/panels/config/automation/condition/ha-automation-condition.ts @@ -14,7 +14,6 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { nextRender } from "../../../../common/util/render-status"; import "../../../../components/ha-button"; -import "../../../../components/ha-button-menu"; import "../../../../components/ha-sortable"; import "../../../../components/ha-svg-icon"; import { diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-template.ts b/src/panels/config/automation/condition/types/ha-automation-condition-template.ts index 588ac881e7..9526fc4ef3 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-template.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-template.ts @@ -1,9 +1,15 @@ -import { css, html, LitElement } from "lit"; +import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import "../../../../../components/ha-textarea"; import type { TemplateCondition } from "../../../../../data/automation"; import type { HomeAssistant } from "../../../../../types"; -import { handleChangeEvent } from "../ha-automation-condition-row"; +import type { SchemaUnion } from "../../../../../components/ha-form/types"; +import "../../../../../components/ha-form/ha-form"; +import { fireEvent } from "../../../../../common/dom/fire_event"; + +const SCHEMA = [ + { name: "value_template", required: true, selector: { template: {} } }, +] as const; @customElement("ha-automation-condition-template") export class HaTemplateCondition extends LitElement { @@ -18,36 +24,30 @@ export class HaTemplateCondition extends LitElement { } protected render() { - const { value_template } = this.condition; return html` -

    - ${this.hass.localize( - "ui.panel.config.automation.editor.conditions.type.template.value_template" - )} - * -

    - + .computeLabel=${this._computeLabelCallback} + .disabled=${this.disabled} + > `; } private _valueChanged(ev: CustomEvent): void { - handleChangeEvent(this, ev); + ev.stopPropagation(); + const newCondition = ev.detail.value; + fireEvent(this, "value-changed", { value: newCondition }); } - static styles = css` - p { - margin-top: 0; - } - `; + private _computeLabelCallback = ( + schema: SchemaUnion + ): string => + this.hass.localize( + `ui.panel.config.automation.editor.conditions.type.template.${schema.name}` + ); } declare global { diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index 68d019e6b5..1350820f9b 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -35,7 +35,6 @@ import { afterNextRender } from "../../../common/util/render-status"; import "../../../components/ha-button"; import "../../../components/ha-dropdown"; import "../../../components/ha-dropdown-item"; -import type { HaDropdownItem } from "../../../components/ha-dropdown-item"; import "../../../components/ha-fab"; import "../../../components/ha-fade-in"; import "../../../components/ha-icon"; @@ -79,7 +78,12 @@ import "../../../layouts/hass-subpage"; import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin"; import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin"; import { haStyle } from "../../../resources/styles"; -import type { Entries, HomeAssistant, Route } from "../../../types"; +import type { + Entries, + HomeAssistant, + Route, + ValueChangedEvent, +} from "../../../types"; import { isMac } from "../../../util/is_mac"; import { showToast } from "../../../util/toast"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; @@ -92,6 +96,7 @@ import { showAutomationSaveTimeoutDialog } from "./automation-save-timeout-dialo import "./blueprint-automation-editor"; import "./manual-automation-editor"; import type { HaManualAutomationEditor } from "./manual-automation-editor"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; declare global { interface HTMLElementTagNameMap { @@ -762,7 +767,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin( } } - private _valueChanged(ev: CustomEvent<{ value: AutomationConfig }>) { + private _valueChanged(ev: ValueChangedEvent) { ev.stopPropagation(); if (this._config) { @@ -1221,7 +1226,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin( this._undoRedoController.redo(); } - private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { const action = ev.detail?.item?.value; if (!action) { diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index 819bac7d24..b263b0fa4d 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -1,7 +1,7 @@ +import "@home-assistant/webawesome/dist/components/divider/divider"; import { ResizeController } from "@lit-labs/observers/resize-controller"; import { consume } from "@lit/context"; import { - mdiChevronRight, mdiCog, mdiContentDuplicate, mdiDelete, @@ -19,7 +19,6 @@ import { mdiToggleSwitchOffOutline, mdiTransitConnection, } from "@mdi/js"; -import { differenceInDays } from "date-fns"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { LitElement, css, html, nothing } from "lit"; @@ -28,14 +27,11 @@ import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; import { computeCssColor } from "../../../common/color/compute-color"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; -import { formatShortDateTimeWithConditionalYear } from "../../../common/datetime/format_date_time"; -import { relativeTime } from "../../../common/datetime/relative_time"; import { storage } from "../../../common/decorators/storage"; import type { HASSDomEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { navigate } from "../../../common/navigate"; -import { slugify } from "../../../common/string/slugify"; import type { LocalizeFunc } from "../../../common/translations/localize"; import { hasRejectedItems, @@ -50,6 +46,12 @@ import type { } from "../../../components/data-table/ha-data-table"; import "../../../components/data-table/ha-data-table-labels"; import "../../../components/entity/ha-entity-toggle"; +import "../../../components/ha-dropdown"; +import type { + HaDropdown, + HaDropdownSelectEvent, +} from "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-fab"; import "../../../components/ha-filter-blueprints"; import "../../../components/ha-filter-categories"; @@ -59,11 +61,6 @@ import "../../../components/ha-filter-floor-areas"; import "../../../components/ha-filter-labels"; import "../../../components/ha-filter-voice-assistants"; import "../../../components/ha-icon-button"; -import "../../../components/ha-md-divider"; -import "../../../components/ha-md-menu"; -import type { HaMdMenu } from "../../../components/ha-md-menu"; -import "../../../components/ha-md-menu-item"; -import type { HaMdMenuItem } from "../../../components/ha-md-menu-item"; import "../../../components/ha-sub-menu"; import "../../../components/ha-svg-icon"; import "../../../components/ha-tooltip"; @@ -87,6 +84,8 @@ import { fullEntitiesContext } from "../../../data/context"; import type { DataTableFilters } from "../../../data/data_table_filters"; import { deserializeFilters, + isFilterUsed, + isRelatedItemsFilterUsed, serializeFilters, } from "../../../data/data_table_filters"; import { UNAVAILABLE } from "../../../data/entity/entity"; @@ -95,6 +94,7 @@ import type { UpdateEntityRegistryEntryResult, } from "../../../data/entity/entity_registry"; import { updateEntityRegistryEntry } from "../../../data/entity/entity_registry"; +import { getEntityVoiceAssistantsIds } from "../../../data/expose"; import type { LabelRegistryEntry } from "../../../data/label/label_registry"; import { createLabelRegistryEntry, @@ -114,15 +114,21 @@ import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; +import { + getAreaTableColumn, + getCategoryTableColumn, + getEntityIdHiddenTableColumn, + getLabelsTableColumn, + getTriggeredAtTableColumn, +} from "../common/data-table-columns"; import { configSections } from "../ha-panel-config"; import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; -import { showNewAutomationDialog } from "./show-dialog-new-automation"; -import { getEntityVoiceAssistantsIds } from "../../../data/expose"; -import { getAvailableAssistants } from "../voice-assistants/expose/available-assistants"; import { - getAssistantsTableColumn, getAssistantsSortableKey, + getAssistantsTableColumn, } from "../voice-assistants/expose/assistants-table-column"; +import { getAvailableAssistants } from "../voice-assistants/expose/available-assistants"; +import { showNewAutomationDialog } from "./show-dialog-new-automation"; type AutomationItem = AutomationEntity & { name: string; @@ -130,7 +136,7 @@ type AutomationItem = AutomationEntity & { last_triggered: string | undefined; formatted_state: string; category: string | undefined; - labels: LabelRegistryEntry[]; + label_entries: LabelRegistryEntry[]; assistants: string[]; assistants_sortable_key: string | undefined; }; @@ -151,7 +157,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { @state() private _searchParms = new URLSearchParams(window.location.search); - @state() private _filteredAutomations?: string[] | null; + @state() private _filteredEntityIds?: string[] | null; @state() @storage({ @@ -216,7 +222,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { }) private _activeHiddenColumns?: string[]; - @query("#overflow-menu") private _overflowMenu!: HaMdMenu; + @query("#overflow-menu") private _overflowMenu!: HaDropdown; private _sizeController = new ResizeController(this, { callback: (entries) => entries[0]?.contentRect.width, @@ -226,6 +232,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { return getAvailableAssistants(this.cloudStatus, this.hass); } + private _openingOverflow = false; + private _automations = memoizeOne( ( automations: AutomationEntity[], @@ -265,7 +273,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { category: category ? categoryReg?.find((cat) => cat.category_id === category)?.name : undefined, - labels: (labels || []).map( + label_entries: (labels || []).map( (lbl) => labelReg!.find((label) => label.label_id === lbl)! ), assistants, @@ -280,7 +288,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { ( narrow: boolean, localize: LocalizeFunc, - locale: HomeAssistant["locale"], entitiesToCheck?: any[] ): DataTableColumnContainer => { const columns: DataTableColumnContainer = { @@ -302,11 +309,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { })} >`, }, - entity_id: { - title: "", - hidden: true, - filterable: true, - }, + entity_id: getEntityIdHiddenTableColumn(), name: { title: localize("ui.panel.config.automation.picker.headers.name"), main: true, @@ -315,59 +318,17 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { direction: "asc", flex: 2, extraTemplate: (automation) => - automation.labels.length + automation.label_entries.length ? html`` : nothing, }, - area: { - title: localize("ui.panel.config.automation.picker.headers.area"), - groupable: true, - filterable: true, - sortable: true, - }, - category: { - title: localize("ui.panel.config.automation.picker.headers.category"), - defaultHidden: true, - groupable: true, - filterable: true, - sortable: true, - }, - labels: { - title: "", - hidden: true, - filterable: true, - template: (automation) => - automation.labels.map((lbl) => lbl.name).join(" "), - }, - last_triggered: { - sortable: true, - title: localize("ui.card.automation.last_triggered"), - template: (automation) => { - if (!automation.last_triggered) { - return this.hass.localize("ui.components.relative_time.never"); - } - const date = new Date(automation.last_triggered); - const now = new Date(); - const dayDifference = differenceInDays(now, date); - const formattedTime = formatShortDateTimeWithConditionalYear( - date, - this.hass.locale, - this.hass.config - ); - const elementId = "last-triggered-" + slugify(automation.entity_id); - return html` - ${dayDifference > 3 - ? formattedTime - : html` - ${formattedTime} - ${relativeTime(date, locale)} - `} - `; - }, - }, + area: getAreaTableColumn(localize), + category: getCategoryTableColumn(localize), + labels: getLabelsTableColumn(), + last_triggered: getTriggeredAtTableColumn(localize, this.hass), formatted_state: { minWidth: "82px", maxWidth: "82px", @@ -411,16 +372,27 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { ); private _showOverflowMenu = (ev) => { - if ( - this._overflowMenu.open && - ev.target === this._overflowMenu.anchorElement - ) { - this._overflowMenu.close(); + if (this._overflowMenu.anchorElement === ev.target) { + this._overflowMenu.anchorElement = undefined; return; } - this._overflowAutomation = ev.target.automation; + this._openingOverflow = true; this._overflowMenu.anchorElement = ev.target; - this._overflowMenu.show(); + this._overflowAutomation = ev.target.automation; + this._overflowMenu.open = true; + }; + + private _overflowMenuOpened = () => { + this._openingOverflow = false; + }; + + private _overflowMenuClosed = () => { + // changing the anchorElement triggers a close event, ignore it + if (this._openingOverflow) { + return; + } + + this._overflowMenu.anchorElement = undefined; }; protected hassSubscribe(): (UnsubscribeFunc | Promise)[] { @@ -439,103 +411,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { } protected render(): TemplateResult { - const categoryItems = html`${this._categories?.map( - (category) => - html` - ${category.icon - ? html`` - : html``} -
    ${category.name}
    -
    ` - )} - -
    - ${this.hass.localize( - "ui.panel.config.automation.picker.bulk_actions.no_category" - )} -
    -
    - - -
    - ${this.hass.localize("ui.panel.config.category.editor.add")} -
    -
    `; - - const labelItems = html`${this._labels?.map((label) => { - const color = label.color ? computeCssColor(label.color) : undefined; - const selected = this._selected.every((entityId) => - this.hass.entities[entityId]?.labels.includes(label.label_id) - ); - const partial = - !selected && - this._selected.some((entityId) => - this.hass.entities[entityId]?.labels.includes(label.label_id) - ); - return html` - - - ${label.icon - ? html`` - : nothing} - ${label.name} - - `; - })} - - -
    - ${this.hass.localize("ui.panel.config.labels.add_label")} -
    `; - - const areaItems = html`${Object.values(this.hass.areas).map( - (area) => - html` - ${area.icon - ? html`` - : html``} -
    ${area.name}
    -
    ` - )} - -
    - ${this.hass.localize( - "ui.panel.config.devices.picker.bulk_actions.no_area" - )} -
    -
    - - -
    - ${this.hass.localize( - "ui.panel.config.devices.picker.bulk_actions.add_area" - )} -
    -
    `; - const areasInOverflow = (this._sizeController.value && this._sizeController.value < 900) || (!this._sizeController.value && this.hass.dockedSidebar === "docked"); @@ -550,15 +425,15 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { this.hass.areas, this._categories, this._labels, - this._filteredAutomations + this._filteredEntityIds ); return html` - Array.isArray(filter.value) - ? filter.value.length - : filter.value && - Object.values(filter.value).some((val) => - Array.isArray(val) ? val.length : val - ) - ).length - } - .columns=${this._columns( - this.narrow, - this.hass.localize, - this.hass.locale, - automations - )} + .filters=${Object.values(this._filters).filter((filter) => + Array.isArray(filter.value) + ? filter.value.length + : filter.value && + Object.values(filter.value).some((val) => + Array.isArray(val) ? val.length : val + ) + ).length} + .columns=${this._columns(this.narrow, this.hass.localize, automations)} .initialGroupColumn=${this._activeGrouping ?? "category"} .initialCollapsedGroups=${this._activeCollapsed} .initialSorting=${this._activeSorting} @@ -682,13 +550,34 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { .narrow=${this.narrow} @expanded-changed=${this._filterExpanded} > - ${ - !this.narrow - ? html` + ${!this.narrow + ? html` + + + + ${this._renderCategoryItems()} + + ${labelsInOverflow + ? nothing + : html` - ${categoryItems} - - ${labelsInOverflow - ? nothing - : html` - - - - ${labelItems} - `} - ${areasInOverflow - ? nothing - : html` - - - - ${areaItems} - `}` - : nothing - } - - ${ - this.narrow - ? html``} + ${areasInOverflow + ? nothing + : html` - - ` - : html`` - } - - ${ - this.narrow - ? html` - -
    - ${this.hass.localize( - "ui.panel.config.automation.picker.bulk_actions.move_category" - )} -
    + -
    - ${categoryItems} -
    ` - : nothing - } - ${ - this.narrow || labelsInOverflow - ? html` - -
    - ${this.hass.localize( - "ui.panel.config.automation.picker.bulk_actions.add_label" - )} -
    - -
    - ${labelItems} -
    ` - : nothing - } - ${ - this.narrow || areasInOverflow - ? html` - -
    - ${this.hass.localize( - "ui.panel.config.devices.picker.bulk_actions.move_area" - )} -
    - -
    - ${areaItems} -
    ` - : nothing - } - - -
    - ${this.hass.localize( - "ui.panel.config.automation.picker.bulk_actions.enable" + + ${this._renderAreaItems()} + `}` + : nothing} + + ${this.narrow + ? html` - - - -
    - ${this.hass.localize( - "ui.panel.config.automation.picker.bulk_actions.disable" + slot="trigger" + > + + ` + : html` - - - ${ - !this.automations.length - ? html`
    - -

    - ${this.hass.localize( - "ui.panel.config.automation.picker.empty_header" - )} -

    -

    - ${this.hass.localize( - "ui.panel.config.automation.picker.empty_text_1" - )} -

    -

    - ${this.hass.localize( - "ui.panel.config.automation.picker.empty_text_2", - { user: this.hass.user?.name || "Alice" } - )} -

    - - ${this.hass.localize("ui.panel.config.common.learn_more")} - - -
    ` - : nothing - } + slot="trigger" + >
    `} + ${this.narrow + ? html` + ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.move_category" + )} + ${this._renderCategoryItems("submenu")} + ` + : nothing} + ${this.narrow || labelsInOverflow + ? html` + ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.add_label" + )} + ${this._renderLabelItems("submenu")} + ` + : nothing} + ${this.narrow || areasInOverflow + ? html` + ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.move_area" + )} + ${this._renderAreaItems("submenu")} + ` + : nothing} + + + ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.enable" + )} + + + + ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.disable" + )} + + + ${!this.automations.length + ? html`
    + +

    + ${this.hass.localize( + "ui.panel.config.automation.picker.empty_header" + )} +

    +

    + ${this.hass.localize( + "ui.panel.config.automation.picker.empty_text_1" + )} +

    +

    + ${this.hass.localize( + "ui.panel.config.automation.picker.empty_text_2", + { user: this.hass.user?.name || "Alice" } + )} +

    + + ${this.hass.localize("ui.panel.config.common.learn_more")} + + +
    ` + : nothing} - - - -
    - ${this.hass.localize("ui.panel.config.automation.editor.show_info")} -
    -
    + + + + ${this.hass.localize("ui.panel.config.automation.editor.show_info")} + - - -
    - ${this.hass.localize( - "ui.panel.config.automation.picker.show_settings" - )} -
    -
    - - -
    - ${this.hass.localize( - `ui.panel.config.automation.picker.${this._overflowAutomation?.category ? "edit_category" : "assign_category"}` - )} -
    -
    - - -
    - ${this.hass.localize("ui.panel.config.automation.editor.run")} -
    -
    - - -
    - ${this.hass.localize( - "ui.panel.config.automation.editor.show_trace" - )} -
    -
    - - - -
    - ${this.hass.localize("ui.panel.config.automation.picker.duplicate")} -
    -
    - + + + ${this.hass.localize( + "ui.panel.config.automation.picker.show_settings" + )} + + + + ${this.hass.localize( + `ui.panel.config.automation.picker.${this._overflowAutomation?.category ? "edit_category" : "assign_category"}` + )} + + + + ${this.hass.localize("ui.panel.config.automation.editor.run")} + + + + ${this.hass.localize("ui.panel.config.automation.editor.show_trace")} + + + + + ${this.hass.localize("ui.panel.config.automation.picker.duplicate")} + + -
    - ${ - this._overflowAutomation?.state === "off" - ? this.hass.localize("ui.panel.config.automation.editor.enable") - : this.hass.localize( - "ui.panel.config.automation.editor.disable" - ) - } -
    -
    - - -
    - ${this.hass.localize("ui.panel.config.automation.picker.delete")} -
    -
    -
    + ${this._overflowAutomation?.state === "off" + ? this.hass.localize("ui.panel.config.automation.editor.enable") + : this.hass.localize("ui.panel.config.automation.editor.disable")} + + + + ${this.hass.localize("ui.panel.config.automation.picker.delete")} + + `; } @@ -1005,91 +812,49 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { private _applyFilters() { const filters = Object.entries(this._filters); - let items: Set | undefined; + let filteredEntityIds = this.automations.map( + (automation) => automation.entity_id + ); for (const [key, filter] of filters) { - if (filter.items) { - if (!items) { - items = filter.items; - continue; - } - items = - "intersection" in items - ? // @ts-ignore - items.intersection(filter.items) - : new Set([...items].filter((x) => filter.items!.has(x))); - } if ( - key === "ha-filter-categories" && - Array.isArray(filter.value) && - filter.value.length + // these 4 filters actually apply any selected options, and expose + // the list of automations that match these options as filter.items + isRelatedItemsFilterUsed(key, filter, [ + "ha-filter-floor-areas", + "ha-filter-devices", + "ha-filter-entities", + "ha-filter-blueprints", + ]) ) { - const categoryItems = new Set(); - this.automations - .filter( - (automation) => - filter.value![0] === - this._entityReg.find( - (reg) => reg.entity_id === automation.entity_id - )?.categories.automation + filteredEntityIds = filteredEntityIds.filter((entityId) => + filter.items!.has(entityId) + ); + + // the filters below only expose the selected options (as filter.value); + // applying the filter must be done here + } else if (isFilterUsed(key, filter, "ha-filter-categories")) { + // category filter only allows a single selected option + filteredEntityIds = filteredEntityIds.filter( + (entityId) => + filter.value![0] === + this._entityReg.find((reg) => reg.entity_id === entityId) + ?.categories.automation + ); + } else if (isFilterUsed(key, filter, "ha-filter-labels")) { + filteredEntityIds = filteredEntityIds.filter((entityId) => + this._entityReg + .find((reg) => reg.entity_id === entityId) + ?.labels.some((lbl) => (filter.value as string[]).includes(lbl)) + ); + } else if (isFilterUsed(key, filter, "ha-filter-voice-assistants")) { + filteredEntityIds = filteredEntityIds.filter((entityId) => + getEntityVoiceAssistantsIds(this._entityReg, entityId).some((va) => + (filter.value as string[]).includes(va) ) - .forEach((automation) => categoryItems.add(automation.entity_id)); - if (!items) { - items = categoryItems; - continue; - } - items = - "intersection" in items - ? // @ts-ignore - items.intersection(categoryItems) - : new Set([...items].filter((x) => categoryItems!.has(x))); - } else if ( - key === "ha-filter-labels" && - Array.isArray(filter.value) && - filter.value.length - ) { - const labelItems = new Set(); - this.automations - .filter((automation) => - this._entityReg - .find((reg) => reg.entity_id === automation.entity_id) - ?.labels.some((lbl) => (filter.value as string[]).includes(lbl)) - ) - .forEach((automation) => labelItems.add(automation.entity_id)); - if (!items) { - items = labelItems; - continue; - } - items = - "intersection" in items - ? // @ts-ignore - items.intersection(labelItems) - : new Set([...items].filter((x) => labelItems!.has(x))); - } else if ( - key === "ha-filter-voice-assistants" && - Array.isArray(filter.value) && - filter.value.length - ) { - const assistItems = new Set(); - this.automations - .filter((automation) => - getEntityVoiceAssistantsIds( - this._entityReg, - automation.entity_id - ).some((va) => (filter.value as string[]).includes(va)) - ) - .forEach((automation) => assistItems.add(automation.entity_id)); - if (!items) { - items = assistItems; - continue; - } - items = - "intersection" in items - ? // @ts-ignore - items.intersection(assistItems) - : new Set([...items].filter((x) => assistItems!.has(x))); + ); } } - this._filteredAutomations = items ? [...items] : undefined; + this._filteredEntityIds = filteredEntityIds; } private _filterLabel() { @@ -1132,33 +897,59 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { this._applyFilters(); } - private _showInfo = (item: HaMdMenuItem) => { - const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)! - .automation; - fireEvent(this, "hass-more-info", { entityId: automation.entity_id }); + private _handleOverflowAction = (ev: HaDropdownSelectEvent) => { + const action = ev.detail.item.value; + + if (!action || !this._overflowAutomation) { + return; + } + + switch (action) { + case "show_info": + this._showInfo(this._overflowAutomation); + break; + case "show_settings": + this._showSettings(this._overflowAutomation); + break; + case "edit_category": + this._editCategory(this._overflowAutomation); + break; + case "run_actions": + this._runActions(this._overflowAutomation); + break; + case "show_trace": + this._showTrace(this._overflowAutomation); + break; + case "toggle": + this._toggle(this._overflowAutomation); + break; + case "delete": + this._deleteConfirm(this._overflowAutomation); + break; + case "duplicate": + this._duplicate(this._overflowAutomation); + break; + } }; - private _showSettings = (item: HaMdMenuItem) => { - const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)! - .automation; + private _showInfo = (automation: AutomationItem) => { + fireEvent(this, "hass-more-info", { + entityId: automation.entity_id, + }); + }; + private _showSettings = (automation: AutomationItem) => { fireEvent(this, "hass-more-info", { entityId: automation.entity_id, view: "settings", }); }; - private _runActions = (item: HaMdMenuItem) => { - const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)! - .automation; - + private _runActions = (automation: AutomationItem) => { triggerAutomationActions(this.hass, automation.entity_id); }; - private _editCategory = (item: HaMdMenuItem) => { - const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)! - .automation; - + private _editCategory = (automation: AutomationItem) => { const entityReg = this._entityReg.find( (reg) => reg.entity_id === automation.entity_id ); @@ -1179,10 +970,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { }); }; - private _showTrace = (item: HaMdMenuItem) => { - const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)! - .automation; - + private _showTrace = (automation: AutomationItem) => { if (!automation.attributes.id) { showAlertDialog(this, { text: this.hass.localize( @@ -1196,20 +984,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { ); }; - private _toggle = async (item: HaMdMenuItem): Promise => { - const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)! - .automation; - + private _toggle = async (automation: AutomationItem): Promise => { const service = automation.state === "off" ? "turn_on" : "turn_off"; await this.hass.callService("automation", service, { entity_id: automation.entity_id, }); }; - private _deleteConfirm = async (item: HaMdMenuItem) => { - const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)! - .automation; - + private _deleteConfirm = async (automation: AutomationItem) => { showConfirmationDialog(this, { title: this.hass.localize( "ui.panel.config.automation.picker.delete_confirm_title" @@ -1225,9 +1007,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { }); }; - private async _delete(automation) { + private async _delete(automation: AutomationItem) { try { - await deleteAutomation(this.hass, automation.attributes.id); + await deleteAutomation(this.hass, automation.attributes.id!); this._selected = this._selected.filter( (entityId) => entityId !== automation.entity_id ); @@ -1246,14 +1028,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { } } - private _duplicate = async (item: HaMdMenuItem) => { - const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)! - .automation; - + private _duplicate = async (automation: AutomationItem) => { try { const config = await fetchAutomationFileConfig( this.hass, - automation.attributes.id + automation.attributes.id! ); duplicateAutomation(config); } catch (err: any) { @@ -1322,12 +1101,22 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { } } - private _handleBulkCategory = async (item) => { - const category = item.value; - this._bulkAddCategory(category); + private _handleBulkCategory = (ev: HaDropdownSelectEvent) => { + const value = ev.detail.item.value; + if (value === "category_create") { + this._bulkCreateCategory(); + return; + } + if (value === "category_none") { + this._bulkAddCategory(null); + return; + } + if (value?.startsWith("category_")) { + this._bulkAddCategory(value.substring(9)); + } }; - private async _bulkAddCategory(category: string) { + private async _bulkAddCategory(category: string | null) { const promises: Promise[] = []; this._selected.forEach((entityId) => { promises.push( @@ -1352,11 +1141,20 @@ ${rejected } } - private async _handleBulkLabel(ev) { - const label = ev.currentTarget.value; - const action = ev.currentTarget.action; - this._bulkLabel(label, action); - } + private _handleBulkLabel = (ev) => { + ev.preventDefault(); // keep menu open + const item = ev.detail.item; + const value = item.value; + if (value === "label_create") { + this._bulkCreateLabel(); + return; + } + + if (value?.startsWith("label_")) { + const action = item.action; + this._bulkLabel(value.substring(6), action); + } + }; private async _bulkLabel(label: string, action: "add" | "remove") { const promises: Promise[] = []; @@ -1388,12 +1186,23 @@ ${rejected } } - private _handleBulkArea = (item) => { - const area = item.value; - this._bulkAddArea(area); + private _handleBulkArea = (ev) => { + const value = ev.detail.item.value; + if (value === "area_create") { + this._bulkCreateArea(); + return; + } + if (value === "area_none") { + this._bulkAddArea(null); + return; + } + + if (value?.startsWith("area_")) { + this._bulkAddArea(value.substring(5)); + } }; - private async _bulkAddArea(area: string) { + private async _bulkAddArea(area: string | null) { const promises: Promise[] = []; this._selected.forEach((entityId) => { promises.push( @@ -1494,6 +1303,148 @@ ${rejected }); }; + private _renderCategoryItems = (slot = "") => + html`${this._categories?.map( + (category) => + html` + ${category.icon + ? html`` + : html``} + ${category.name} + ` + )} + + ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.no_category" + )} + + + + ${this.hass.localize("ui.panel.config.category.editor.add")} + `; + + private _renderLabelItems = (slot = "") => + html`${this._labels?.map((label) => { + const color = label.color ? computeCssColor(label.color) : undefined; + const selected = this._selected.every((entityId) => + this.hass.entities[entityId]?.labels.includes(label.label_id) + ); + const partial = + !selected && + this._selected.some((entityId) => + this.hass.entities[entityId]?.labels.includes(label.label_id) + ); + return html` + + + ${label.icon + ? html`` + : nothing} + ${label.name} + + `; + })} + + + ${this.hass.localize("ui.panel.config.labels.add_label")} + `; + + private _renderAreaItems = (slot = "") => + html`${Object.values(this.hass.areas).map( + (area) => + html` + ${area.icon + ? html`` + : html``} + ${area.name} + ` + )} + + ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.no_area" + )} + + + + ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.add_area" + )} + `; + + private _handleBulkAction = (ev) => { + const item = ev.detail.item; + const value = item.value; + + if (!value) { + return; + } + + if (value === "enable") { + this._handleBulkEnable(); + return; + } + if (value === "disable") { + this._handleBulkDisable(); + return; + } + + if (value.startsWith("category_")) { + if (value === "category_create") { + this._bulkCreateCategory(); + return; + } + if (value === "category_none") { + this._bulkAddCategory(null); + return; + } + + this._bulkAddCategory(value.substring(9)); + return; + } + + if (value.startsWith("label_")) { + if (value === "label_create") { + this._bulkCreateLabel(); + return; + } + + const action = item.action; + this._bulkLabel(value.substring(6), action); + return; + } + + if (value.startsWith("area_")) { + if (value === "area_create") { + this._bulkCreateArea(); + return; + } + if (value === "area_none") { + this._bulkAddArea(null); + return; + } + + this._bulkAddArea(value.substring(5)); + } + }; + private _handleSortingChanged(ev: CustomEvent) { this._activeSorting = ev.detail; } @@ -1538,7 +1489,11 @@ ${rejected ha-assist-chip { --ha-assist-chip-container-shape: 10px; } - ha-md-button-menu ha-assist-chip { + ha-dropdown::part(menu), + ha-dropdown::part(submenu) { + --auto-size-available-width: calc(50vw - var(--ha-space-4)); + } + ha-dropdown ha-assist-chip { --md-assist-chip-trailing-space: 8px; } ha-label { diff --git a/src/panels/config/automation/ha-automation-trace.ts b/src/panels/config/automation/ha-automation-trace.ts index d617095c5b..697272dbfd 100644 --- a/src/panels/config/automation/ha-automation-trace.ts +++ b/src/panels/config/automation/ha-automation-trace.ts @@ -21,7 +21,6 @@ import { computeRTL } from "../../../common/util/compute_rtl"; import "../../../components/ha-button"; import "../../../components/ha-dropdown"; import "../../../components/ha-dropdown-item"; -import type { HaDropdownItem } from "../../../components/ha-dropdown-item"; import "../../../components/ha-icon-button"; import "../../../components/trace/ha-trace-blueprint-config"; import "../../../components/trace/ha-trace-config"; @@ -47,6 +46,7 @@ import "../../../layouts/hass-subpage"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant, Route } from "../../../types"; import { fileDownload } from "../../../util/file_download"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; const TABS = ["details", "timeline", "logbook", "automation_config"] as const; @@ -515,7 +515,7 @@ export class HaAutomationTrace extends LitElement { } } - private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { const action = ev.detail?.item?.value; if (!action) { diff --git a/src/panels/config/automation/manual-automation-editor.ts b/src/panels/config/automation/manual-automation-editor.ts index 3f22272d86..9cf8325090 100644 --- a/src/panels/config/automation/manual-automation-editor.ts +++ b/src/panels/config/automation/manual-automation-editor.ts @@ -54,7 +54,7 @@ import { import { configEntriesContext } from "../../../data/context"; import { getActionType, type Action } from "../../../data/script"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; -import type { HomeAssistant } from "../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; import { showToast } from "../../../util/toast"; import "./action/ha-automation-action"; @@ -393,7 +393,7 @@ export class HaManualAutomationEditor extends SubscribeMixin(LitElement) { this._sidebarElement?.focus(); } - private _sidebarConfigChanged(ev: CustomEvent<{ value: SidebarConfig }>) { + private _sidebarConfigChanged(ev: ValueChangedEvent) { ev.stopPropagation(); if (!this._sidebarConfig) { return; @@ -448,7 +448,6 @@ export class HaManualAutomationEditor extends SubscribeMixin(LitElement) { } private _saveAutomation() { - this.triggerCloseSidebar(); fireEvent(this, "save-automation"); } diff --git a/src/panels/config/automation/option/ha-automation-option-row.ts b/src/panels/config/automation/option/ha-automation-option-row.ts index 3f0decfe30..6f2c1645d7 100644 --- a/src/panels/config/automation/option/ha-automation-option-row.ts +++ b/src/panels/config/automation/option/ha-automation-option-row.ts @@ -22,7 +22,6 @@ import type { HaAutomationRow } from "../../../../components/ha-automation-row"; import "../../../../components/ha-card"; import "../../../../components/ha-dropdown"; import "../../../../components/ha-dropdown-item"; -import type { HaDropdownItem } from "../../../../components/ha-dropdown-item"; import "../../../../components/ha-expansion-panel"; import "../../../../components/ha-icon-button"; import "../../../../components/ha-svg-icon"; @@ -48,6 +47,7 @@ import { overflowStyles, rowStyles, } from "../styles"; +import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown"; @customElement("ha-automation-option-row") export default class HaAutomationOptionRow extends LitElement { @@ -349,7 +349,7 @@ export default class HaAutomationOptionRow extends LitElement { fireEvent(this, "move-down"); } - private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { ev.stopPropagation(); const action = ev.detail?.item?.value; diff --git a/src/panels/config/automation/paste-replace-dialog/dialog-paste-replace.ts b/src/panels/config/automation/paste-replace-dialog/dialog-paste-replace.ts index 8deee5ec60..6cfe8fa780 100644 --- a/src/panels/config/automation/paste-replace-dialog/dialog-paste-replace.ts +++ b/src/panels/config/automation/paste-replace-dialog/dialog-paste-replace.ts @@ -39,7 +39,7 @@ class DialogPasteReplace extends LitElement { const localizationNamespace = this._params.domain === "blueprint" - ? "ui.panel.developer-tools.tabs.blueprints" + ? "ui.panel.config.developer-tools.tabs.blueprints" : `ui.panel.config.${this._params.domain}`; return html` ) { + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { const action = ev.detail?.item?.value; if (!action) { diff --git a/src/panels/config/automation/sidebar/ha-automation-sidebar-blueprint-input.ts b/src/panels/config/automation/sidebar/ha-automation-sidebar-blueprint-input.ts index 3e82c24299..c42783119e 100644 --- a/src/panels/config/automation/sidebar/ha-automation-sidebar-blueprint-input.ts +++ b/src/panels/config/automation/sidebar/ha-automation-sidebar-blueprint-input.ts @@ -29,11 +29,11 @@ import { import type { HaDropdownItem } from "../../../../components/ha-dropdown-item"; import type { BlueprintInputSidebarConfig } from "../../../../data/automation"; import type { HomeAssistant } from "../../../../types"; -import type HaBlueprintInputEditor from "../../../developer-tools/blueprints/input/ha-blueprint-input-editor"; +import type HaBlueprintInputEditor from "../../developer-tools/blueprints/input/ha-blueprint-input-editor"; import "../trigger/ha-automation-trigger-editor"; import "./ha-automation-sidebar-card"; import "../../../../components/ha-dropdown-item"; -import "../../../developer-tools/blueprints/input/ha-blueprint-input-editor"; +import "../../developer-tools/blueprints/input/ha-blueprint-input-editor"; @customElement("ha-automation-sidebar-blueprint-input") export default class HaAutomationSidebarBlueprintInput extends LitElement { @@ -84,7 +84,7 @@ export default class HaAutomationSidebarBlueprintInput extends LitElement { const isSection = isInputSection(input); if (isSection) { return this.hass.localize( - `ui.panel.developer-tools.tabs.blueprints.editor.inputs.editor.section` + `ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.editor.section` ); } @@ -98,7 +98,7 @@ export default class HaAutomationSidebarBlueprintInput extends LitElement { private _getSubtitle(input: BlueprintInput | BlueprintInputSection) { const isSection = isInputSection(input); return this.hass.localize( - `ui.panel.developer-tools.tabs.blueprints.editor.inputs.editor.${isSection ? "section" : "single"}` + `ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.editor.${isSection ? "section" : "single"}` ); } diff --git a/src/panels/config/automation/sidebar/ha-automation-sidebar-condition.ts b/src/panels/config/automation/sidebar/ha-automation-sidebar-condition.ts index afa961c5ad..eaf2abd6d8 100644 --- a/src/panels/config/automation/sidebar/ha-automation-sidebar-condition.ts +++ b/src/panels/config/automation/sidebar/ha-automation-sidebar-condition.ts @@ -18,7 +18,6 @@ import { keyed } from "lit/directives/keyed"; import { fireEvent } from "../../../../common/dom/fire_event"; import { handleStructError } from "../../../../common/structs/handle-errors"; import "../../../../components/ha-dropdown-item"; -import type { HaDropdownItem } from "../../../../components/ha-dropdown-item"; import type { ConditionSidebarConfig, LegacyCondition, @@ -37,6 +36,7 @@ import "../condition/ha-automation-condition-editor"; import type HaAutomationConditionEditor from "../condition/ha-automation-condition-editor"; import { overflowStyles, sidebarEditorStyles } from "../styles"; import "./ha-automation-sidebar-card"; +import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown"; @customElement("ha-automation-sidebar-condition") export default class HaAutomationSidebarCondition extends LitElement { @@ -413,7 +413,7 @@ export default class HaAutomationSidebarCondition extends LitElement { fireEvent(this, "toggle-yaml-mode"); }; - private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { const action = ev.detail?.item?.value; if (!action) { diff --git a/src/panels/config/automation/sidebar/ha-automation-sidebar-option.ts b/src/panels/config/automation/sidebar/ha-automation-sidebar-option.ts index caf48b47bd..db40fe639a 100644 --- a/src/panels/config/automation/sidebar/ha-automation-sidebar-option.ts +++ b/src/panels/config/automation/sidebar/ha-automation-sidebar-option.ts @@ -9,7 +9,6 @@ import { html, LitElement, nothing } from "lit"; import { customElement, property, query } from "lit/decorators"; import "../../../../components/ha-dropdown-item"; -import type { HaDropdownItem } from "../../../../components/ha-dropdown-item"; import "../../../../components/ha-svg-icon"; import type { OptionSidebarConfig } from "../../../../data/automation"; import type { HomeAssistant } from "../../../../types"; @@ -17,6 +16,7 @@ import { isMac } from "../../../../util/is_mac"; import type HaAutomationConditionEditor from "../action/ha-automation-action-editor"; import { overflowStyles, sidebarEditorStyles } from "../styles"; import "./ha-automation-sidebar-card"; +import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown"; @customElement("ha-automation-sidebar-option") export default class HaAutomationSidebarOption extends LitElement { @@ -129,7 +129,7 @@ export default class HaAutomationSidebarOption extends LitElement { `; } - private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { const action = ev.detail?.item?.value; if (!action) { diff --git a/src/panels/config/automation/sidebar/ha-automation-sidebar-script-field-selector.ts b/src/panels/config/automation/sidebar/ha-automation-sidebar-script-field-selector.ts index bd1f7202c8..f772400300 100644 --- a/src/panels/config/automation/sidebar/ha-automation-sidebar-script-field-selector.ts +++ b/src/panels/config/automation/sidebar/ha-automation-sidebar-script-field-selector.ts @@ -5,7 +5,6 @@ import { keyed } from "lit/directives/keyed"; import { fireEvent } from "../../../../common/dom/fire_event"; import type { LocalizeKeys } from "../../../../common/translations/localize"; import "../../../../components/ha-dropdown-item"; -import type { HaDropdownItem } from "../../../../components/ha-dropdown-item"; import type { ScriptFieldSidebarConfig } from "../../../../data/automation"; import type { HomeAssistant } from "../../../../types"; import { isMac } from "../../../../util/is_mac"; @@ -13,6 +12,7 @@ import "../../script/ha-script-field-selector-editor"; import type HaAutomationConditionEditor from "../action/ha-automation-action-editor"; import { sidebarEditorStyles } from "../styles"; import "./ha-automation-sidebar-card"; +import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown"; @customElement("ha-automation-sidebar-script-field-selector") export default class HaAutomationSidebarScriptFieldSelector extends LitElement { @@ -162,7 +162,7 @@ export default class HaAutomationSidebarScriptFieldSelector extends LitElement { fireEvent(this, "toggle-yaml-mode"); }; - private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { const action = ev.detail?.item?.value; if (!action) { diff --git a/src/panels/config/automation/sidebar/ha-automation-sidebar-script-field.ts b/src/panels/config/automation/sidebar/ha-automation-sidebar-script-field.ts index 32305c6010..a67989e3a8 100644 --- a/src/panels/config/automation/sidebar/ha-automation-sidebar-script-field.ts +++ b/src/panels/config/automation/sidebar/ha-automation-sidebar-script-field.ts @@ -4,7 +4,6 @@ import { customElement, property, query, state } from "lit/decorators"; import { keyed } from "lit/directives/keyed"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-dropdown-item"; -import type { HaDropdownItem } from "../../../../components/ha-dropdown-item"; import type { ScriptFieldSidebarConfig } from "../../../../data/automation"; import type { HomeAssistant } from "../../../../types"; import { isMac } from "../../../../util/is_mac"; @@ -12,6 +11,7 @@ import "../../script/ha-script-field-editor"; import type HaAutomationConditionEditor from "../action/ha-automation-action-editor"; import { overflowStyles, sidebarEditorStyles } from "../styles"; import "./ha-automation-sidebar-card"; +import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown"; @customElement("ha-automation-sidebar-script-field") export default class HaAutomationSidebarScriptField extends LitElement { @@ -156,7 +156,7 @@ export default class HaAutomationSidebarScriptField extends LitElement { fireEvent(this, "toggle-yaml-mode"); }; - private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { const action = ev.detail?.item?.value; if (!action) { diff --git a/src/panels/config/automation/sidebar/ha-automation-sidebar-trigger.ts b/src/panels/config/automation/sidebar/ha-automation-sidebar-trigger.ts index b2c86c5efe..de3fbf2b6b 100644 --- a/src/panels/config/automation/sidebar/ha-automation-sidebar-trigger.ts +++ b/src/panels/config/automation/sidebar/ha-automation-sidebar-trigger.ts @@ -17,7 +17,6 @@ import { keyed } from "lit/directives/keyed"; import { fireEvent } from "../../../../common/dom/fire_event"; import { handleStructError } from "../../../../common/structs/handle-errors"; import "../../../../components/ha-dropdown-item"; -import type { HaDropdownItem } from "../../../../components/ha-dropdown-item"; import type { LegacyTrigger, TriggerSidebarConfig, @@ -33,6 +32,7 @@ import { overflowStyles, sidebarEditorStyles } from "../styles"; import "../trigger/ha-automation-trigger-editor"; import type HaAutomationTriggerEditor from "../trigger/ha-automation-trigger-editor"; import "./ha-automation-sidebar-card"; +import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown"; @customElement("ha-automation-sidebar-trigger") export default class HaAutomationSidebarTrigger extends LitElement { @@ -328,7 +328,7 @@ export default class HaAutomationSidebarTrigger extends LitElement { this._requestShowId = true; }; - private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { const action = ev.detail?.item?.value; if (!action) { diff --git a/src/panels/config/automation/styles.ts b/src/panels/config/automation/styles.ts index 9928d664d7..12d69d07ef 100644 --- a/src/panels/config/automation/styles.ts +++ b/src/panels/config/automation/styles.ts @@ -40,9 +40,6 @@ export const rowStyles = css` .warning ul { margin: 4px 0; } - ha-md-menu-item > ha-svg-icon { - --mdc-icon-size: 24px; - } ha-tooltip { cursor: default; } @@ -272,7 +269,4 @@ export const overflowStyles = css` display: none; } } - ha-md-menu-item { - --mdc-icon-size: 24px; - } `; diff --git a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts index fb1719a686..fedc7f2e29 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts @@ -40,7 +40,6 @@ import type { HaAutomationRow } from "../../../../components/ha-automation-row"; import "../../../../components/ha-card"; import "../../../../components/ha-dropdown"; import "../../../../components/ha-dropdown-item"; -import type { HaDropdownItem } from "../../../../components/ha-dropdown-item"; import "../../../../components/ha-expansion-panel"; import "../../../../components/ha-icon-button"; import "../../../../components/ha-svg-icon"; @@ -90,6 +89,7 @@ import "./types/ha-automation-trigger-time"; import "./types/ha-automation-trigger-time_pattern"; import "./types/ha-automation-trigger-webhook"; import "./types/ha-automation-trigger-zone"; +import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown"; export interface TriggerElement extends LitElement { trigger: Trigger; @@ -814,7 +814,7 @@ export default class HaAutomationTriggerRow extends LitElement { this._automationRowElement?.focus(); } - private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { ev.stopPropagation(); const action = ev.detail?.item?.value; diff --git a/src/panels/config/automation/trigger/ha-automation-trigger.ts b/src/panels/config/automation/trigger/ha-automation-trigger.ts index 6be41806c8..ccfb756dc5 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger.ts @@ -14,7 +14,6 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { nextRender } from "../../../../common/util/render-status"; import "../../../../components/ha-button"; -import "../../../../components/ha-button-menu"; import "../../../../components/ha-sortable"; import "../../../../components/ha-svg-icon"; import { diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-persistent_notification.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-persistent_notification.ts index 55e5a81f97..a6be314423 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-persistent_notification.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-persistent_notification.ts @@ -3,7 +3,6 @@ import memoizeOne from "memoize-one"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; -import "../../../../../components/ha-button-menu"; import "../../../../../components/ha-check-list-item"; import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-textfield"; diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts index 224b9903d1..08cd19e0d6 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts @@ -4,7 +4,7 @@ import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; import { caseInsensitiveStringCompare } from "../../../../../common/string/compare"; import "../../../../../components/ha-select"; -import "../../../../../components/ha-list-item"; +import type { HaSelectSelectEvent } from "../../../../../components/ha-select"; import type { TagTrigger } from "../../../../../data/automation"; import type { Tag } from "../../../../../data/tag"; import { fetchTags } from "../../../../../data/tag"; @@ -42,16 +42,11 @@ export class HaTagTrigger extends LitElement implements TriggerElement { .disabled=${this.disabled || this._tags.length === 0} .value=${this.trigger.tag_id} @selected=${this._tagChanged} - fixedMenuPosition - naturalMenuWidth + .options=${this._tags.map((tag) => ({ + value: tag.id, + label: tag.name || tag.id, + }))} > - ${this._tags.map( - (tag) => html` - - ${tag.name || tag.id} - - ` - )} `; } @@ -66,18 +61,18 @@ export class HaTagTrigger extends LitElement implements TriggerElement { ); } - private _tagChanged(ev) { + private _tagChanged(ev: HaSelectSelectEvent) { if ( - !ev.target.value || + !ev.detail.value || !this._tags || - this.trigger.tag_id === ev.target.value + this.trigger.tag_id === ev.detail.value ) { return; } fireEvent(this, "value-changed", { value: { ...this.trigger, - tag_id: ev.target.value, + tag_id: ev.detail.value, }, }); } diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-time.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-time.ts index 8aa7cacad3..3b5e5a8bf1 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-time.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-time.ts @@ -4,6 +4,7 @@ import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { firstWeekdayIndex } from "../../../../../common/datetime/first_weekday"; import { fireEvent } from "../../../../../common/dom/fire_event"; +import { computeDomain } from "../../../../../common/entity/compute_domain"; import type { LocalizeFunc } from "../../../../../common/translations/localize"; import "../../../../../components/ha-form/ha-form"; import type { SchemaUnion } from "../../../../../components/ha-form/types"; @@ -11,7 +12,6 @@ import type { TimeTrigger } from "../../../../../data/automation"; import type { FrontendLocaleData } from "../../../../../data/translation"; import type { HomeAssistant } from "../../../../../types"; import type { TriggerElement } from "../ha-automation-trigger-row"; -import { computeDomain } from "../../../../../common/entity/compute_domain"; const MODE_TIME = "time"; const MODE_ENTITY = "entity"; diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-webhook.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-webhook.ts index a34f6936a5..4ffc0e149f 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-webhook.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-webhook.ts @@ -1,15 +1,14 @@ -import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; +import "@home-assistant/webawesome/dist/components/divider/divider"; import { mdiCog, mdiContentCopy } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { PropertyValues } from "lit"; -import { css, html, LitElement } from "lit"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; import { slugify } from "../../../../../common/string/slugify"; import { copyToClipboard } from "../../../../../common/util/copy-clipboard"; -import { stopPropagation } from "../../../../../common/dom/stop_propagation"; -import "../../../../../components/ha-button-menu"; -import "../../../../../components/ha-check-list-item"; +import "../../../../../components/ha-dropdown"; +import "../../../../../components/ha-dropdown-item"; import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-textfield"; import type { HaTextField } from "../../../../../components/ha-textfield"; @@ -20,6 +19,7 @@ import type { import type { HomeAssistant } from "../../../../../types"; import { showToast } from "../../../../../util/toast"; import { handleChangeEvent } from "../ha-automation-trigger-row"; +import type { HaDropdownSelectEvent } from "../../../../../components/ha-dropdown"; const SUPPORTED_METHODS = ["GET", "HEAD", "POST", "PUT"]; const DEFAULT_METHODS = ["POST", "PUT"]; @@ -125,7 +125,11 @@ export class HaWebhookTrigger extends LitElement { .path=${mdiContentCopy} > - + ${SUPPORTED_METHODS.map( (method) => html` - ${method} - + ` )} -
  • - ` + : nothing} + ${this.hass!.localize( "ui.panel.config.automation.editor.triggers.type.webhook.local_only" )} - -
    + +
    `; } @@ -164,23 +170,18 @@ export class HaWebhookTrigger extends LitElement { handleChangeEvent(this, ev); } - private _localOnlyChanged(ev: CustomEvent): void { - ev.stopPropagation(); - if (this.trigger.local_only === ev.detail.selected) { + private _localOnlyChanged(local_only: boolean): void { + if (this.trigger.local_only === local_only) { return; } const newTrigger = { ...this.trigger, - local_only: ev.detail.selected, + local_only, }; fireEvent(this, "value-changed", { value: newTrigger }); } - private _allowedMethodsChanged(ev: CustomEvent): void { - ev.stopPropagation(); - const method = (ev.target as any).value; - const selected = ev.detail.selected; - + private _allowedMethodsChanged(method: string, selected: boolean): void { if (selected === this.trigger.allowed_methods?.includes(method)) { return; } @@ -207,6 +208,22 @@ export class HaWebhookTrigger extends LitElement { }); } + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { + ev.preventDefault(); // don't close the dropdown to select multiple options + const action = ev.detail?.item?.value; + + if (!action) { + return; + } + + if (action === "local_only") { + this._localOnlyChanged(ev.detail.item.checked); + return; + } + + this._allowedMethodsChanged(ev.detail.item.value, ev.detail.item.checked); + } + static styles = css` .flex { display: flex; @@ -222,7 +239,7 @@ export class HaWebhookTrigger extends LitElement { color: var(--secondary-text-color); } - ha-button-menu { + ha-dropdown ha-icon-button { padding-top: 4px; } `; diff --git a/src/panels/config/backup/components/config/ha-backup-config-addon.ts b/src/panels/config/backup/components/config/ha-backup-config-addon.ts index d227793c36..b6f19d5ea0 100644 --- a/src/panels/config/backup/components/config/ha-backup-config-addon.ts +++ b/src/panels/config/backup/components/config/ha-backup-config-addon.ts @@ -62,7 +62,7 @@ class HaBackupConfigAddon extends LitElement { ${this.hass.localize( - `ui.panel.config.backup.settings.addon_update_backup.retention_description` + `ui.panel.config.backup.settings.app_update_backup.retention_description` )} 0) ) { - // It would be better if we could receive individual addon sizes in the WS request instead + // It would be better if we could receive individual app sizes in the WS request instead totalBytes += (segments.addons_data ?? 0) + (segments.addons_config ?? 0); } @@ -335,12 +335,12 @@ class HaBackupConfigData extends LitElement { > ${this.hass.localize( - "ui.panel.config.backup.data.local_addons" + "ui.panel.config.backup.data.local_apps" )} ${this.hass.localize( - "ui.panel.config.backup.data.local_addons_description" + "ui.panel.config.backup.data.local_apps_description" )} ${this.hass.localize( - "ui.panel.config.backup.data.addons" + "ui.panel.config.backup.data.apps" )} ${this.hass.localize( - "ui.panel.config.backup.data.addons_description" + "ui.panel.config.backup.data.apps_description" )}
    ${this.hass.localize( - "ui.panel.config.backup.data.addons_all" + "ui.panel.config.backup.data.apps_all" )}
    ${this.hass.localize( - "ui.panel.config.backup.data.addons_none" + "ui.panel.config.backup.data.apps_none" )}
    ${this.hass.localize( - "ui.panel.config.backup.data.addons_custom" + "ui.panel.config.backup.data.apps_custom" )}
    @@ -405,7 +405,11 @@ class HaBackupConfigData extends LitElement { ${isHassio && this._showAddons && this._addons.length ? html` - +
    ${this.hass.localize( - "ui.panel.config.backup.data.estimated_size_disclaimer_addons_custom" + "ui.panel.config.backup.data.estimated_size_disclaimer_apps_custom" )}` : nothing} diff --git a/src/panels/config/backup/components/config/ha-backup-config-schedule.ts b/src/panels/config/backup/components/config/ha-backup-config-schedule.ts index e572b7a442..7501075802 100644 --- a/src/panels/config/backup/components/config/ha-backup-config-schedule.ts +++ b/src/panels/config/backup/components/config/ha-backup-config-schedule.ts @@ -28,7 +28,7 @@ import { sortWeekdays, } from "../../../../../data/backup"; import type { SupervisorUpdateConfig } from "../../../../../data/supervisor/update"; -import type { HomeAssistant } from "../../../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../../../types"; import { documentationUrl } from "../../../../../util/documentation-url"; import "./ha-backup-config-retention"; @@ -405,7 +405,7 @@ class HaBackupConfigSchedule extends LitElement { }); } - private _retentionChanged(ev: CustomEvent<{ value: Retention }>) { + private _retentionChanged(ev: ValueChangedEvent) { ev.stopPropagation(); const retention = ev.detail.value; diff --git a/src/panels/config/backup/components/ha-backup-data-picker.ts b/src/panels/config/backup/components/ha-backup-data-picker.ts index 61417423d9..88c5ec9238 100644 --- a/src/panels/config/backup/components/ha-backup-data-picker.ts +++ b/src/panels/config/backup/components/ha-backup-data-picker.ts @@ -61,7 +61,7 @@ export class HaBackupDataPicker extends LitElement { | "page-onboarding.restore" | "config.backup" = "config.backup"; - @property({ type: Boolean, attribute: false }) public addonsDisabled = false; + @property({ attribute: false }) public addonsDisabled = false; @state() public _addonIcons: Record = {}; @@ -122,7 +122,7 @@ export class HaBackupDataPicker extends LitElement { return localize(`ui.panel.${this.translationKeyPanel}.data_picker.ssl`); case "addons/local": return localize( - `ui.panel.${this.translationKeyPanel}.data_picker.local_addons` + `ui.panel.${this.translationKeyPanel}.data_picker.local_apps` ); } return capitalizeFirstLetter(folder); @@ -294,7 +294,7 @@ export class HaBackupDataPicker extends LitElement { diff --git a/src/panels/config/backup/components/ha-backup-details-summary.ts b/src/panels/config/backup/components/ha-backup-details-summary.ts index 75a781a546..324b221211 100644 --- a/src/panels/config/backup/components/ha-backup-details-summary.ts +++ b/src/panels/config/backup/components/ha-backup-details-summary.ts @@ -34,7 +34,7 @@ class HaBackupDetailsSummary extends LitElement { if (this.backup.failed_addons?.length) { errors.push({ title: this.hass.localize( - "ui.panel.config.backup.details.summary.error.failed_addons" + "ui.panel.config.backup.details.summary.error.failed_apps" ), items: this.backup.failed_addons.map( (addon) => `${addon.name || addon.slug} (${addon.version})` @@ -127,7 +127,7 @@ class HaBackupDetailsSummary extends LitElement { return this.hass.localize(`ui.panel.config.backup.data_picker.ssl`); case "addons/local": return this.hass.localize( - `ui.panel.config.backup.data_picker.local_addons` + `ui.panel.config.backup.data_picker.local_apps` ); } return capitalizeFirstLetter(folder); diff --git a/src/panels/config/backup/components/overview/ha-backup-overview-backups.ts b/src/panels/config/backup/components/overview/ha-backup-overview-backups.ts index 0891ac4965..372022ed84 100644 --- a/src/panels/config/backup/components/overview/ha-backup-overview-backups.ts +++ b/src/panels/config/backup/components/overview/ha-backup-overview-backups.ts @@ -27,7 +27,7 @@ interface BackupStats { const TYPE_ICONS: Record = { automatic: mdiCalendarSync, manual: mdiGestureTap, - addon_update: mdiPuzzle, + app_update: mdiPuzzle, }; const computeBackupStats = (backups: BackupContent[]): BackupStats => diff --git a/src/panels/config/backup/components/overview/ha-backup-overview-settings.ts b/src/panels/config/backup/components/overview/ha-backup-overview-settings.ts index ae7eb76bef..e060d513b7 100644 --- a/src/panels/config/backup/components/overview/ha-backup-overview-settings.ts +++ b/src/panels/config/backup/components/overview/ha-backup-overview-settings.ts @@ -151,18 +151,18 @@ class HaBackupBackupsSummary extends LitElement { private _addonsDescription(config: BackupConfig): string { if (config.create_backup.include_all_addons) { return this.hass.localize( - "ui.panel.config.backup.overview.settings.addons_all" + "ui.panel.config.backup.overview.settings.apps_all" ); } const count = config.create_backup.include_addons?.length; if (count) { return this.hass.localize( - "ui.panel.config.backup.overview.settings.addons_many", + "ui.panel.config.backup.overview.settings.apps_many", { count } ); } return this.hass.localize( - "ui.panel.config.backup.overview.settings.addons_none" + "ui.panel.config.backup.overview.settings.apps_none" ); } @@ -260,7 +260,7 @@ class HaBackupBackupsSummary extends LitElement {
    ${this.hass.localize( - "ui.panel.config.backup.overview.settings.addons" + "ui.panel.config.backup.overview.settings.apps" )}
    diff --git a/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts b/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts index 3ff46faebc..f6bc08595e 100644 --- a/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts +++ b/src/panels/config/backup/dialogs/dialog-backup-onboarding.ts @@ -1,17 +1,16 @@ import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js"; import type { CSSResultGroup, PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { isComponentLoaded } from "../../../../common/config/is_component_loaded"; import { fireEvent } from "../../../../common/dom/fire_event"; import { copyToClipboard } from "../../../../common/util/copy-clipboard"; import "../../../../components/ha-button"; -import "../../../../components/ha-dialog-header"; +import "../../../../components/ha-dialog-footer"; import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button-prev"; import "../../../../components/ha-icon-next"; -import "../../../../components/ha-md-dialog"; -import type { HaMdDialog } from "../../../../components/ha-md-dialog"; +import "../../../../components/ha-wa-dialog"; import "../../../../components/ha-md-list"; import "../../../../components/ha-md-list-item"; import "../../../../components/ha-password-field"; @@ -86,14 +85,12 @@ const RECOMMENDED_CONFIG: BackupConfig = { class DialogBackupOnboarding extends LitElement implements HassDialog { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _opened = false; + @state() private _open = false; @state() private _step?: Step; @state() private _params?: BackupOnboardingDialogParams; - @query("ha-md-dialog") private _dialog!: HaMdDialog; - @state() private _config?: BackupConfig; public showDialog(params: BackupOnboardingDialogParams): void { @@ -115,21 +112,23 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { }; } - this._opened = true; + this._open = true; } public closeDialog() { - if (this._params!.cancel) { - this._params!.cancel(); + this._open = false; + return true; + } + + private _dialogClosed() { + if (this._params?.cancel) { + this._params.cancel(); } - if (this._opened) { - fireEvent(this, "dialog-closed", { dialog: this.localName }); - } - this._opened = false; this._step = undefined; this._config = undefined; this._params = undefined; - return true; + this._open = false; + fireEvent(this, "dialog-closed", { dialog: this.localName }); } private get _firstStep(): Step { @@ -168,7 +167,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { try { await this._save(true); this._params?.submit!(true); - this._dialog.close(); + this.closeDialog(); } catch (err) { // eslint-disable-next-line no-console console.error(err); @@ -202,7 +201,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { } protected render() { - if (!this._opened || !this._params || !this._step) { + if (!this._params || !this._step) { return nothing; } @@ -210,33 +209,37 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { const isFirstStep = this._step === this._firstStep; return html` - - - ${isFirstStep - ? html` - - ` - : html` - - `} - - ${this._stepTitle} - -
    ${this._renderStepContent()}
    + + ${isFirstStep + ? html` + + ` + : html` + + `} +
    ${this._renderStepContent()}
    ${!FULL_DIALOG_STEPS.has(this._step) ? html` -
    + ${isLastStep ? html` @@ -247,16 +250,17 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { ` : html` ${this.hass.localize("ui.common.next")} `} -
    + ` : nothing} -
    + `; } @@ -540,11 +544,9 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { haStyle, haStyleDialog, css` - ha-md-dialog { - width: 90vw; - max-width: 560px; - --dialog-content-padding: 8px 24px; - max-height: min(605px, 100% - 48px); + ha-wa-dialog { + --dialog-content-padding: var(--ha-space-2) var(--ha-space-6); + --ha-dialog-max-height: min(605px, 100% - 48px); } ha-md-list { background: none; @@ -557,14 +559,6 @@ class DialogBackupOnboarding extends LitElement implements HassDialog { margin-left: -24px; margin-right: -24px; } - @media all and (max-width: 450px), all and (max-height: 500px) { - ha-md-dialog { - max-width: none; - } - div[slot="content"] { - margin-top: 0; - } - } p { margin-top: 0; } diff --git a/src/panels/config/backup/dialogs/dialog-local-backup-location.ts b/src/panels/config/backup/dialogs/dialog-local-backup-location.ts index b228ea389c..f06f4b858a 100644 --- a/src/panels/config/backup/dialogs/dialog-local-backup-location.ts +++ b/src/panels/config/backup/dialogs/dialog-local-backup-location.ts @@ -4,7 +4,8 @@ import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-alert"; import "../../../../components/ha-button"; -import { createCloseHeading } from "../../../../components/ha-dialog"; +import "../../../../components/ha-dialog-footer"; +import "../../../../components/ha-wa-dialog"; import "../../../../components/ha-form/ha-form"; import type { HaFormSchema, @@ -36,13 +37,20 @@ class LocalBackupLocationDialog extends LitElement { @state() private _error?: string; + @state() private _open = false; + public async showDialog( dialogParams: LocalBackupLocationDialogParams ): Promise { this._dialogParams = dialogParams; + this._open = true; } public closeDialog(): void { + this._open = false; + } + + private _dialogClosed(): void { this._data = undefined; this._error = undefined; this._waiting = undefined; @@ -55,17 +63,13 @@ class LocalBackupLocationDialog extends LitElement { return nothing; } return html` - ${this._error ? html`${this._error}` @@ -77,34 +81,35 @@ class LocalBackupLocationDialog extends LitElement { )}

    ${this.hass.localize( `ui.panel.config.backup.dialogs.local_backup_location.note` )} - - ${this.hass.localize("ui.common.cancel")} - - - ${this.hass.localize("ui.common.save")} - -
    + + + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize("ui.common.save")} + + + `; } @@ -143,9 +148,6 @@ class LocalBackupLocationDialog extends LitElement { haStyle, haStyleDialog, css` - ha-dialog { - --mdc-dialog-max-width: 500px; - } ha-form { display: block; margin-bottom: 16px; diff --git a/src/panels/config/backup/dialogs/dialog-restore-backup.ts b/src/panels/config/backup/dialogs/dialog-restore-backup.ts index e2149ba044..ef13362802 100644 --- a/src/panels/config/backup/dialogs/dialog-restore-backup.ts +++ b/src/panels/config/backup/dialogs/dialog-restore-backup.ts @@ -1,20 +1,17 @@ -import { mdiClose } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-button"; +import "../../../../components/ha-dialog-footer"; import "../../../../components/ha-spinner"; -import "../../../../components/ha-dialog-header"; import "../../../../components/ha-password-field"; import { isComponentLoaded } from "../../../../common/config/is_component_loaded"; import "../../../../components/ha-alert"; -import "../../../../components/ha-icon-button"; -import "../../../../components/ha-md-dialog"; -import type { HaMdDialog } from "../../../../components/ha-md-dialog"; import "../../../../components/ha-svg-icon"; +import "../../../../components/ha-wa-dialog"; import type { RestoreBackupParams } from "../../../../data/backup"; import { fetchBackupConfig, @@ -54,6 +51,8 @@ class DialogRestoreBackup extends LitElement implements HassDialog { @state() private _formData?: FormData; + @state() private _open = false; + @state() private _backupEncryptionKey?: string; @state() private _userPassword?: string; @@ -68,8 +67,6 @@ class DialogRestoreBackup extends LitElement implements HassDialog { @state() private _unsub?: Promise; - @query("ha-md-dialog") private _dialog?: HaMdDialog; - public async showDialog(params: RestoreBackupDialogParams) { this._params = params; @@ -94,10 +91,12 @@ class DialogRestoreBackup extends LitElement implements HassDialog { } else { this._step = STEPS[0]; } + + this._open = true; } public closeDialog() { - this._dialog?.close(); + this._open = false; return true; } @@ -112,6 +111,7 @@ class DialogRestoreBackup extends LitElement implements HassDialog { this._stage = undefined; this._step = undefined; this._unsubscribe(); + this._open = false; fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -136,17 +136,14 @@ class DialogRestoreBackup extends LitElement implements HassDialog { ); return html` - - - - ${dialogTitle} - -
    + +
    ${this._error ? html`${this._error}` : this._step === "confirm" @@ -155,18 +152,18 @@ class DialogRestoreBackup extends LitElement implements HassDialog { ? this._renderEncryption() : this._renderProgress()}
    -
    - ${this._error - ? html` - + ${this._error + ? html` + + ${this.hass.localize("ui.common.close")} - ` - : this._step === "confirm" || this._step === "encryption" - ? this._renderConfirmActions() - : nothing} -
    - + + ` + : this._step === "confirm" || this._step === "encryption" + ? this._renderConfirmActions() + : nothing} +
    `; } @@ -216,6 +213,7 @@ class DialogRestoreBackup extends LitElement implements HassDialog { ${this._renderEncryptionIntro()} - ${this.hass.localize("ui.common.cancel")} - - - ${this.hass.localize( - "ui.panel.config.backup.dialogs.restore.actions.restore" - )} - + + + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize( + "ui.panel.config.backup.dialogs.restore.actions.restore" + )} + + `; } @@ -365,10 +373,6 @@ class DialogRestoreBackup extends LitElement implements HassDialog { haStyle, haStyleDialog, css` - ha-md-dialog { - max-width: 500px; - width: 100%; - } .content p { margin: 0 0 16px; } diff --git a/src/panels/config/backup/dialogs/dialog-set-backup-encryption-key.ts b/src/panels/config/backup/dialogs/dialog-set-backup-encryption-key.ts index 31cca50752..0ce006f5dc 100644 --- a/src/panels/config/backup/dialogs/dialog-set-backup-encryption-key.ts +++ b/src/panels/config/backup/dialogs/dialog-set-backup-encryption-key.ts @@ -1,15 +1,13 @@ -import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js"; +import { mdiContentCopy, mdiDownload } from "@mdi/js"; import type { CSSResultGroup } from "lit"; import { LitElement, css, html, nothing } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import { copyToClipboard } from "../../../../common/util/copy-clipboard"; import "../../../../components/ha-button"; -import "../../../../components/ha-dialog-header"; +import "../../../../components/ha-dialog-footer"; import "../../../../components/ha-icon-button"; -import "../../../../components/ha-icon-button-prev"; -import "../../../../components/ha-md-dialog"; -import type { HaMdDialog } from "../../../../components/ha-md-dialog"; +import "../../../../components/ha-wa-dialog"; import "../../../../components/ha-md-list"; import "../../../../components/ha-md-list-item"; import "../../../../components/ha-password-field"; @@ -31,40 +29,40 @@ type Step = (typeof STEPS)[number]; class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _opened = false; + @state() private _open = false; @state() private _step?: Step; @state() private _params?: SetBackupEncryptionKeyDialogParams; - @query("ha-md-dialog") private _dialog!: HaMdDialog; - @state() private _newEncryptionKey?: string; public showDialog(params: SetBackupEncryptionKeyDialogParams): void { this._params = params; this._step = STEPS[0]; - this._opened = true; + this._open = true; this._newEncryptionKey = generateEncryptionKey(); } public closeDialog() { - if (this._params!.cancel) { - this._params!.cancel(); + this._open = false; + return true; + } + + private _dialogClosed() { + if (this._params?.cancel) { + this._params.cancel(); } - if (this._opened) { - fireEvent(this, "dialog-closed", { dialog: this.localName }); - } - this._opened = false; this._step = undefined; this._params = undefined; this._newEncryptionKey = undefined; - return true; + this._open = false; + fireEvent(this, "dialog-closed", { dialog: this.localName }); } private _done() { this._params?.submit!(true); - this._dialog.close(); + this.closeDialog(); } private _nextStep() { @@ -76,7 +74,7 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { } protected render() { - if (!this._opened || !this._params) { + if (!this._params || !this._step) { return nothing; } @@ -88,21 +86,20 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { : ""; return html` - - - - ${dialogTitle} - -
    ${this._renderStepContent()}
    -
    + + ${this._renderStepContent()} + ${this._step === "key" ? html` @@ -112,14 +109,14 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { ` : html` - + ${this.hass.localize( "ui.panel.config.backup.dialogs.set_encryption_key.actions.done" )} `} -
    -
    + + `; } @@ -213,10 +210,8 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { haStyle, haStyleDialog, css` - ha-md-dialog { - width: 90vw; - max-width: 560px; - --dialog-content-padding: 8px 24px; + ha-wa-dialog { + --dialog-content-padding: var(--ha-space-2) var(--ha-space-6); } ha-md-list { background: none; @@ -247,14 +242,6 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog { flex: none; margin: -16px; } - @media all and (max-width: 450px), all and (max-height: 500px) { - ha-md-dialog { - max-width: none; - } - div[slot="content"] { - margin-top: 0; - } - } p { margin-top: 0; } diff --git a/src/panels/config/backup/ha-config-backup-backups.ts b/src/panels/config/backup/ha-config-backup-backups.ts index dc460fe408..b4ea092db1 100644 --- a/src/panels/config/backup/ha-config-backup-backups.ts +++ b/src/panels/config/backup/ha-config-backup-backups.ts @@ -16,7 +16,6 @@ import { relativeTime } from "../../../common/datetime/relative_time"; import { storage } from "../../../common/decorators/storage"; import { fireEvent, type HASSDomEvent } from "../../../common/dom/fire_event"; import { computeDomain } from "../../../common/entity/compute_domain"; -import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; import { navigate } from "../../../common/navigate"; import type { LocalizeFunc } from "../../../common/translations/localize"; import type { @@ -26,14 +25,18 @@ import type { SelectionChangedEvent, } from "../../../components/data-table/ha-data-table"; import "../../../components/ha-button"; -import "../../../components/ha-button-menu"; -import "../../../components/ha-spinner"; +import "../../../components/ha-dropdown"; +import type { + HaDropdown, + HaDropdownSelectEvent, +} from "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-fab"; import "../../../components/ha-filter-states"; import "../../../components/ha-icon"; import "../../../components/ha-icon-next"; import "../../../components/ha-icon-overflow-menu"; -import "../../../components/ha-list-item"; +import "../../../components/ha-spinner"; import "../../../components/ha-svg-icon"; import type { BackupAgent, @@ -71,9 +74,6 @@ import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup" import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup"; import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup"; import { downloadBackup } from "./helper/download_backup"; -import type { HaMdMenu } from "../../../components/ha-md-menu"; -import "../../../components/ha-md-menu"; -import "../../../components/ha-md-menu-item"; interface BackupRow extends DataTableRowData, BackupContent { formatted_type: string; @@ -123,7 +123,11 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { @query("hass-tabs-subpage-data-table", true) private _dataTable!: HaTabsSubpageDataTable; - @query("#overflow-menu") private _overflowMenu?: HaMdMenu; + @query("#overflow-menu") private _overflowMenu?: HaDropdown; + + private _openingOverflow = false; + + private _overflowBackup?: BackupRow; public connectedCallback() { super.connectedCallback(); @@ -287,12 +291,27 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { return; } - if (this._overflowMenu.open) { - this._overflowMenu.close(); + if (this._overflowMenu.anchorElement === ev.target) { + this._overflowMenu.anchorElement = undefined; return; } + this._openingOverflow = true; this._overflowMenu.anchorElement = ev.target; - this._overflowMenu.show(); + this._overflowBackup = ev.target.backup; + this._overflowMenu.open = true; + }; + + private _overflowMenuOpened = () => { + this._openingOverflow = false; + }; + + private _overflowMenuClosed = () => { + // changing the anchorElement triggers a close event, ignore it + if (this._openingOverflow || !this._overflowMenu) { + return; + } + + this._overflowMenu.anchorElement = undefined; }; private _handleGroupingChanged(ev: CustomEvent) { @@ -401,22 +420,22 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { )} >
    - + - - + + ${this.hass.localize( "ui.panel.config.backup.backups.menu.upload_backup" )} - - + +
    @@ -447,7 +466,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { - - - + + + ${this.hass.localize("ui.common.download")} - - - + + + ${this.hass.localize("ui.common.delete")} - - + + `; } @@ -519,13 +543,9 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { return !this.config?.automatic_backups_configured; } - private async _uploadBackup(ev) { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } - + private _uploadBackup = async () => { await showUploadBackupDialog(this, {}); - } + }; private async _newBackup(): Promise { const config = this.config!; @@ -560,16 +580,29 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { navigate(`/config/backup/details/${id}`); } - private _downloadBackup = async (ev): Promise => { - const backup = ev.parentElement.anchorElement.backup; + private _handleOverflowAction = (ev: HaDropdownSelectEvent) => { + const action = ev.detail.item.value; + + if (action === "download") { + this._downloadBackup(); + return; + } + + if (action === "delete") { + this._deleteBackup(); + } + }; + + private _downloadBackup = async (): Promise => { + const backup = this._overflowBackup; if (!backup) { return; } downloadBackup(this.hass, this, backup, this.config); }; - private _deleteBackup = async (ev): Promise => { - const backup = ev.parentElement.anchorElement.backup; + private _deleteBackup = async (): Promise => { + const backup = this._overflowBackup; if (!backup) { return; } @@ -635,6 +668,14 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { this._dataTable.clearSelection(); } + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { + const action = ev.detail?.item.value; + + if (action === "upload_backup") { + this._uploadBackup(); + } + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/config/backup/ha-config-backup-details.ts b/src/panels/config/backup/ha-config-backup-details.ts index 490f8f831b..3862697711 100644 --- a/src/panels/config/backup/ha-config-backup-details.ts +++ b/src/panels/config/backup/ha-config-backup-details.ts @@ -1,4 +1,3 @@ -import type { ActionDetail } from "@material/mwc-list"; import { mdiDelete, mdiDotsVertical, @@ -14,11 +13,11 @@ import { computeDomain } from "../../../common/entity/compute_domain"; import { navigate } from "../../../common/navigate"; import "../../../components/ha-alert"; import "../../../components/ha-button"; -import "../../../components/ha-button-menu"; import "../../../components/ha-card"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-fade-in"; import "../../../components/ha-icon-button"; -import "../../../components/ha-list-item"; import "../../../components/ha-md-list"; import "../../../components/ha-md-list-item"; import "../../../components/ha-spinner"; @@ -44,6 +43,7 @@ import "./components/ha-backup-details-restore"; import "./components/ha-backup-details-summary"; import { showRestoreBackupDialog } from "./dialogs/show-dialog-restore-backup"; import { downloadBackup } from "./helper/download_backup"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; interface Agent extends BackupContentAgent { id: string; @@ -114,21 +114,21 @@ class HaConfigBackupDetails extends LitElement { .header=${this._backup?.name || this.hass.localize("ui.panel.config.backup.details.header")} > - + - - + + ${this.hass.localize("ui.common.download")} - - - + + + ${this.hass.localize("ui.common.delete")} - - + +
    ${this._error && html`${this._error}`} @@ -252,9 +252,9 @@ class HaConfigBackupDetails extends LitElement { ${ success ? html` - @@ -265,16 +265,16 @@ class HaConfigBackupDetails extends LitElement { )} .path=${mdiDotsVertical} > - + ${this.hass.localize( "ui.panel.config.backup.details.locations.download" )} - - + + ` : nothing } @@ -312,18 +312,19 @@ class HaConfigBackupDetails extends LitElement { } } - private _handleAction(ev: CustomEvent) { - switch (ev.detail.index) { - case 0: + private _handleAction(ev: HaDropdownSelectEvent) { + const action = ev.detail.item.value; + switch (action) { + case "download": this._downloadBackup(); break; - case 1: + case "delete": this._deleteBackup(); break; } } - private _handleAgentAction(ev: CustomEvent) { + private _handleAgentAction(ev: HaDropdownSelectEvent) { const button = ev.currentTarget; const agentId = (button as any).agent; this._downloadBackup(agentId); @@ -385,12 +386,6 @@ class HaConfigBackupDetails extends LitElement { --mdc-icon-size: 48px; color: var(--primary-text-color); } - .warning { - color: var(--error-color); - } - .warning ha-svg-icon { - color: var(--error-color); - } ha-button.danger { --mdc-theme-primary: var(--error-color); } diff --git a/src/panels/config/backup/ha-config-backup-location.ts b/src/panels/config/backup/ha-config-backup-location.ts index bbee35968c..1f833cd361 100644 --- a/src/panels/config/backup/ha-config-backup-location.ts +++ b/src/panels/config/backup/ha-config-backup-location.ts @@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-alert"; import "../../../components/ha-button"; -import "../../../components/ha-button-menu"; import "../../../components/ha-card"; import "../../../components/ha-fade-in"; import "../../../components/ha-icon-button"; @@ -26,7 +25,7 @@ import { updateBackupConfig, } from "../../../data/backup"; import "../../../layouts/hass-subpage"; -import type { HomeAssistant } from "../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../types"; import { showConfirmationDialog } from "../../lovelace/custom-card-helpers"; import "./components/config/ha-backup-config-retention"; import "./components/ha-backup-data-picker"; @@ -285,7 +284,7 @@ class HaConfigBackupDetails extends LitElement { } } - private _retentionChanged(ev: CustomEvent<{ value: Retention }>) { + private _retentionChanged(ev: ValueChangedEvent) { const retention = ev.detail.value; this._updateAgentConfig({ retention, diff --git a/src/panels/config/backup/ha-config-backup-overview.ts b/src/panels/config/backup/ha-config-backup-overview.ts index d8bcf680bf..b11fcfc0db 100644 --- a/src/panels/config/backup/ha-config-backup-overview.ts +++ b/src/panels/config/backup/ha-config-backup-overview.ts @@ -3,16 +3,15 @@ import type { CSSResultGroup, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; -import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; import "../../../components/ha-button"; -import "../../../components/ha-button-menu"; import "../../../components/ha-card"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-fab"; -import "../../../components/ha-spinner"; import "../../../components/ha-icon"; import "../../../components/ha-icon-next"; import "../../../components/ha-icon-overflow-menu"; -import "../../../components/ha-list-item"; +import "../../../components/ha-spinner"; import "../../../components/ha-svg-icon"; import type { BackupAgent, @@ -40,6 +39,7 @@ import { showBackupOnboardingDialog } from "./dialogs/show-dialog-backup_onboard import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup"; import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup"; import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; @customElement("ha-config-backup-overview") class HaConfigBackupOverview extends LitElement { @@ -63,13 +63,9 @@ class HaConfigBackupOverview extends LitElement { @property({ attribute: false }) public agents: BackupAgent[] = []; - private async _uploadBackup(ev) { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } - + private _uploadBackup = async () => { await showUploadBackupDialog(this, {}); - } + }; private _handleOnboardingButtonClick(ev) { ev.stopPropagation(); @@ -143,19 +139,23 @@ class HaConfigBackupOverview extends LitElement { .narrow=${this.narrow} .header=${this.hass.localize("ui.panel.config.backup.overview.header")} > - + - - + + ${this.hass.localize( "ui.panel.config.backup.overview.menu.upload_backup" )} - - + +
    ${this.info && Object.keys(this.info.agent_errors).length ? html`${Object.entries(this.info.agent_errors).map( @@ -240,6 +240,14 @@ class HaConfigBackupOverview extends LitElement { `; } + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { + const action = ev.detail?.item.value; + + if (action === "upload_backup") { + this._uploadBackup(); + } + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/config/backup/ha-config-backup-settings.ts b/src/panels/config/backup/ha-config-backup-settings.ts index 6105d62179..e7020d0753 100644 --- a/src/panels/config/backup/ha-config-backup-settings.ts +++ b/src/panels/config/backup/ha-config-backup-settings.ts @@ -4,16 +4,15 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { fireEvent } from "../../../common/dom/fire_event"; -import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; import { debounce } from "../../../common/util/debounce"; import { nextRender } from "../../../common/util/render-status"; import "../../../components/ha-alert"; import "../../../components/ha-button"; -import "../../../components/ha-button-menu"; import "../../../components/ha-card"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-next"; -import "../../../components/ha-list-item"; import "../../../components/ha-password-field"; import "../../../components/ha-svg-icon"; import type { BackupAgent, BackupConfig } from "../../../data/backup"; @@ -26,6 +25,7 @@ import { } from "../../../data/supervisor/update"; import "../../../layouts/hass-subpage"; import type { HomeAssistant } from "../../../types"; +import { brandsUrl } from "../../../util/brands-url"; import { documentationUrl } from "../../../util/documentation-url"; import "./components/config/ha-backup-config-addon"; import "./components/config/ha-backup-config-agents"; @@ -35,7 +35,7 @@ import "./components/config/ha-backup-config-encryption-key"; import "./components/config/ha-backup-config-schedule"; import type { BackupConfigSchedule } from "./components/config/ha-backup-config-schedule"; import { showLocalBackupLocationDialog } from "./dialogs/show-dialog-local-backup-location"; -import { brandsUrl } from "../../../util/brands-url"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; @customElement("ha-config-backup-settings") class HaConfigBackupSettings extends LitElement { @@ -80,7 +80,7 @@ class HaConfigBackupSettings extends LitElement { // eslint-disable-next-line no-console console.error(err); this._supervisorUpdateConfigError = this.hass.localize( - "ui.panel.config.backup.settings.addon_update_backup.error_load", + "ui.panel.config.backup.settings.app_update_backup.error_load", { error: err?.message || err, } @@ -96,12 +96,12 @@ class HaConfigBackupSettings extends LitElement { !this._config?.create_backup.include_all_addons && this._config?.create_backup.include_addons?.length ) { - // Wait for the addons to be loaded before scrolling because the height can change and location section is below addons. + // Wait for the apps to be loaded before scrolling because the height can change and location section is below apps. this.addEventListener("backup-addons-fetched", async () => { await nextRender(); this._scrolltoHash(); }); - // Clear hash to cancel the scroll after 500ms if addons doesn't load + // Clear hash to cancel the scroll after 500ms if apps doesn't load setTimeout(() => { this._clearHash(); }, 500); @@ -140,25 +140,22 @@ class HaConfigBackupSettings extends LitElement { > ${supervisor ? html` - + - - + + ${this.hass.localize( "ui.panel.config.backup.settings.menu.change_default_location" )} - - + + ` : nothing} @@ -320,18 +317,18 @@ class HaConfigBackupSettings extends LitElement { ? html`
    ${this.hass.localize( - "ui.panel.config.backup.settings.addon_update_backup.title" + "ui.panel.config.backup.settings.app_update_backup.title" )}

    ${this.hass.localize( - "ui.panel.config.backup.settings.addon_update_backup.description" + "ui.panel.config.backup.settings.app_update_backup.description" )}

    ${this.hass.localize( - "ui.panel.config.backup.settings.addon_update_backup.local_only" + "ui.panel.config.backup.settings.app_update_backup.local_only" )}

    ${this._supervisorUpdateConfigError @@ -372,13 +369,9 @@ class HaConfigBackupSettings extends LitElement { `; } - private async _changeLocalLocation(ev) { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } - + private _changeLocalLocation = () => { showLocalBackupLocationDialog(this, {}); - } + }; private async _supervisorUpdateConfigChanged(ev) { const config = ev.detail.value as SupervisorUpdateConfig; @@ -473,7 +466,7 @@ class HaConfigBackupSettings extends LitElement { // eslint-disable-next-line no-console console.error(err); this._supervisorUpdateConfigError = this.hass.localize( - "ui.panel.config.backup.settings.addon_update_backup.error_save", + "ui.panel.config.backup.settings.app_update_backup.error_save", { error: err?.message || err?.toString(), } @@ -499,6 +492,14 @@ class HaConfigBackupSettings extends LitElement { fireEvent(this, "ha-refresh-backup-config"); } + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { + const action = ev.detail?.item?.value; + + if (action === "change_local_location") { + this._changeLocalLocation(); + } + } + static styles = css` ha-card { scroll-margin-top: 16px; diff --git a/src/panels/config/blueprint/dialog-import-blueprint.ts b/src/panels/config/blueprint/dialog-import-blueprint.ts index aa254ed375..d6ae408244 100644 --- a/src/panels/config/blueprint/dialog-import-blueprint.ts +++ b/src/panels/config/blueprint/dialog-import-blueprint.ts @@ -6,17 +6,19 @@ import { documentationUrl } from "../../../util/documentation-url"; import "../../../components/ha-alert"; import "../../../components/ha-button"; import "../../../components/ha-code-editor"; -import "../../../components/ha-dialog"; import "../../../components/ha-dialog-header"; +import "../../../components/ha-dialog-footer"; import "../../../components/ha-expansion-panel"; import "../../../components/ha-markdown"; import "../../../components/ha-spinner"; import "../../../components/ha-textfield"; +import "../../../components/ha-wa-dialog"; import type { HaTextField } from "../../../components/ha-textfield"; import type { BlueprintImportResult } from "../../../data/blueprint"; import { importBlueprint, saveBlueprint } from "../../../data/blueprint"; import { haStyleDialog } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; +import { withViewTransition } from "../../../common/util/view-transition"; @customElement("ha-dialog-import-blueprint") class DialogImportBlueprint extends LitElement { @@ -26,6 +28,8 @@ class DialogImportBlueprint extends LitElement { @state() private _params?; + @state() private _open = false; + @state() private _importing = false; @state() private _saving = false; @@ -43,9 +47,14 @@ class DialogImportBlueprint extends LitElement { this._error = undefined; this._url = this._params.url; this.large = false; + this._open = true; } public closeDialog(): void { + this._open = false; + } + + private _dialogClosed(): void { this._error = undefined; this._result = undefined; this._params = undefined; @@ -59,11 +68,16 @@ class DialogImportBlueprint extends LitElement { } const heading = this.hass.localize("ui.panel.config.blueprint.add.header"); return html` - - + + @@ -104,6 +118,7 @@ class DialogImportBlueprint extends LitElement { .label=${this.hass.localize( "ui.panel.config.blueprint.add.file_name" )} + autofocus > `} `}
    - - ${this.hass.localize("ui.common.cancel")} - - ${!this._result - ? html` - - ${this.hass.localize( - "ui.panel.config.blueprint.add.import_btn" - )} - - ` - : html` - - ${this._result.exists - ? this.hass.localize( - "ui.panel.config.blueprint.add.save_btn_override" - ) - : this.hass.localize( - "ui.panel.config.blueprint.add.save_btn" - )} - - `} - + + + ${this.hass.localize("ui.common.cancel")} + + ${!this._result + ? html` + + ${this.hass.localize( + "ui.panel.config.blueprint.add.import_btn" + )} + + ` + : html` + + ${this._result.exists + ? this.hass.localize( + "ui.panel.config.blueprint.add.save_btn_override" + ) + : this.hass.localize( + "ui.panel.config.blueprint.add.save_btn" + )} + + `} + + `; } private _enlarge() { - this.large = !this.large; + withViewTransition(() => { + this.large = !this.large; + }); } private async _import() { @@ -273,10 +292,6 @@ class DialogImportBlueprint extends LitElement { a ha-svg-icon { --mdc-icon-size: 16px; } - :host([large]) ha-dialog { - --mdc-dialog-min-width: 90vw; - --mdc-dialog-max-width: 90vw; - } ha-expansion-panel { --expansion-panel-content-padding: 0px; } diff --git a/src/panels/config/category/ha-category-picker.ts b/src/panels/config/category/ha-category-picker.ts index 219df09799..0e70a07ade 100644 --- a/src/panels/config/category/ha-category-picker.ts +++ b/src/panels/config/category/ha-category-picker.ts @@ -153,7 +153,7 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) { { id: ADD_NEW_ID + searchString, primary: this.hass.localize( - "ui.components.category-picker.add_new_sugestion", + "ui.components.category-picker.add_new_suggestion", { name: searchString, } diff --git a/src/panels/config/cloud/account/cloud-account.ts b/src/panels/config/cloud/account/cloud-account.ts index 6dd24ba959..70d486f4c1 100644 --- a/src/panels/config/cloud/account/cloud-account.ts +++ b/src/panels/config/cloud/account/cloud-account.ts @@ -6,9 +6,11 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import { debounce } from "../../../../common/util/debounce"; import "../../../../components/ha-alert"; import "../../../../components/ha-button"; -import "../../../../components/ha-button-menu"; import "../../../../components/ha-card"; +import "../../../../components/ha-dropdown"; +import "../../../../components/ha-dropdown-item"; import "../../../../components/ha-list-item"; +import "../../../../components/ha-svg-icon"; import "../../../../components/ha-tip"; import type { CloudStatusLoggedIn, @@ -33,6 +35,7 @@ import "./cloud-remote-pref"; import "./cloud-tts-pref"; import "./cloud-webhooks"; import { showSupportPackageDialog } from "./show-dialog-cloud-support-package"; +import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown"; @customElement("cloud-account") export class CloudAccount extends SubscribeMixin(LitElement) { @@ -53,26 +56,26 @@ export class CloudAccount extends SubscribeMixin(LitElement) { .narrow=${this.narrow} header="Home Assistant Cloud" > - + - + ${this.hass.localize( "ui.panel.config.cloud.account.reset_cloud_data" )} - - - + + + ${this.hass.localize( "ui.panel.config.cloud.account.download_support_package" )} - - - + + +
    Home Assistant Cloud @@ -297,13 +300,15 @@ export class CloudAccount extends SubscribeMixin(LitElement) { fireEvent(this, "ha-refresh-cloud-status"); } - private _handleMenuAction(ev) { - switch (ev.detail.index) { - case 0: + private _handleMenuAction(ev: HaDropdownSelectEvent) { + const value = ev.detail.item.value; + switch (value) { + case "reset": this._deleteCloudData(); break; - case 1: + case "download": this._downloadSupportPackage(); + break; } } diff --git a/src/panels/config/cloud/account/cloud-tts-pref.ts b/src/panels/config/cloud/account/cloud-tts-pref.ts index 959fbb0fc6..fb54238f50 100644 --- a/src/panels/config/cloud/account/cloud-tts-pref.ts +++ b/src/panels/config/cloud/account/cloud-tts-pref.ts @@ -1,13 +1,14 @@ -import { css, html, LitElement, nothing } from "lit"; import { mdiContentCopy } from "@mdi/js"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../common/dom/fire_event"; -import "../../../../components/ha-card"; +import { copyToClipboard } from "../../../../common/util/copy-clipboard"; import "../../../../components/ha-button"; +import "../../../../components/ha-card"; import "../../../../components/ha-language-picker"; -import "../../../../components/ha-list-item"; import "../../../../components/ha-select"; +import type { HaSelectSelectEvent } from "../../../../components/ha-select"; import "../../../../components/ha-svg-icon"; import "../../../../components/ha-switch"; import type { CloudStatusLoggedIn } from "../../../../data/cloud"; @@ -19,9 +20,8 @@ import { } from "../../../../data/cloud/tts"; import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box"; import type { HomeAssistant } from "../../../../types"; -import { showTryTtsDialog } from "./show-dialog-cloud-tts-try"; -import { copyToClipboard } from "../../../../common/util/copy-clipboard"; import { showToast } from "../../../../util/toast"; +import { showTryTtsDialog } from "./show-dialog-cloud-tts-try"; export const getCloudTtsSupportedVoices = ( language: string, @@ -96,13 +96,11 @@ export class CloudTTSPref extends LitElement { .disabled=${this.savingPreferences} .value=${defaultVoice[1]} @selected=${this._handleVoiceChange} + .options=${voices.map((voice) => ({ + value: voice.voiceId, + label: voice.voiceName, + }))} > - ${voices.map( - (voice) => - html` - ${voice.voiceName} - ` - )}
    @@ -134,16 +132,6 @@ export class CloudTTSPref extends LitElement { `; } - protected updated(changedProps) { - if ( - changedProps.has("cloudStatus") && - this.cloudStatus?.prefs.tts_default_voice?.[0] !== - changedProps.get("cloudStatus")?.prefs.tts_default_voice?.[0] - ) { - this.renderRoot.querySelector("ha-select")?.layoutOptions(); - } - } - protected willUpdate(changedProps) { super.willUpdate(changedProps); if (!this.hasUpdated) { @@ -195,13 +183,13 @@ export class CloudTTSPref extends LitElement { } } - private async _handleVoiceChange(ev) { - if (ev.target.value === this.cloudStatus!.prefs.tts_default_voice[1]) { + private async _handleVoiceChange(ev: HaSelectSelectEvent) { + const voice = ev.detail.value; + if (!voice || voice === this.cloudStatus!.prefs.tts_default_voice[1]) { return; } this.savingPreferences = true; const language = this.cloudStatus!.prefs.tts_default_voice[0]; - const voice = ev.target.value; try { await updateCloudPref(this.hass, { diff --git a/src/panels/config/cloud/account/dialog-cloud-support-package.ts b/src/panels/config/cloud/account/dialog-cloud-support-package.ts index 2183a4fec7..61819338d5 100644 --- a/src/panels/config/cloud/account/dialog-cloud-support-package.ts +++ b/src/panels/config/cloud/account/dialog-cloud-support-package.ts @@ -1,13 +1,11 @@ -import { mdiClose } from "@mdi/js"; -import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; +import { css, html, LitElement } from "lit"; +import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-alert"; import "../../../../components/ha-button"; -import "../../../../components/ha-dialog-header"; +import "../../../../components/ha-dialog-footer"; import "../../../../components/ha-markdown-element"; -import "../../../../components/ha-md-dialog"; -import type { HaMdDialog } from "../../../../components/ha-md-dialog"; +import "../../../../components/ha-wa-dialog"; import "../../../../components/ha-select"; import "../../../../components/ha-spinner"; import "../../../../components/ha-textarea"; @@ -23,8 +21,6 @@ export class DialogSupportPackage extends LitElement { @state() private _supportPackage?: string; - @query("ha-md-dialog") private _dialog?: HaMdDialog; - public showDialog() { this._open = true; this._loadSupportPackage(); @@ -37,53 +33,50 @@ export class DialogSupportPackage extends LitElement { } public closeDialog() { - this._dialog?.close(); + this._open = false; return true; } protected render() { - if (!this._open) { - return nothing; - } return html` - - - - Download support package - - -
    - ${this._supportPackage - ? html`` - : html` -
    - - Generating preview... -
    - `} -
    - - + - + ${this.hass.localize( "ui.panel.config.cloud.account.reset_cloud_data" )} - - - + + + ${this.hass.localize( "ui.panel.config.cloud.account.download_support_package" )} - - - + + +
    Home Assistant Cloud @@ -164,13 +167,15 @@ export class CloudLoginPanel extends LitElement { fireEvent(this, "flash-message-changed", { value: "" }); } - private _handleMenuAction(ev) { - switch (ev.detail.index) { - case 0: + private _handleMenuAction(ev: HaDropdownSelectEvent) { + const value = ev.detail.item.value; + switch (value) { + case "reset": this._deleteCloudData(); break; - case 1: + case "download": this._downloadSupportPackage(); + break; } } diff --git a/src/panels/config/common/data-table-columns.ts b/src/panels/config/common/data-table-columns.ts new file mode 100644 index 0000000000..22b3927717 --- /dev/null +++ b/src/panels/config/common/data-table-columns.ts @@ -0,0 +1,204 @@ +import { html, nothing } from "lit"; +import { differenceInDays } from "date-fns"; +import { mdiPencilOff } from "@mdi/js"; +import type { HomeAssistant } from "../../../types"; +import type { LocalizeFunc } from "../../../common/translations/localize"; +import type { DataTableColumnData } from "../../../components/data-table/ha-data-table"; +import { slugify } from "../../../common/string/slugify"; +import { relativeTime } from "../../../common/datetime/relative_time"; +import { formatShortDateTimeWithConditionalYear } from "../../../common/datetime/format_date_time"; +import { isUnavailableState } from "../../../data/entity/entity"; +import "../../../components/ha-tooltip"; +import "../../../components/ha-svg-icon"; + +export function getEntityIdHiddenTableColumn(): DataTableColumnData { + return { + title: "", + hidden: true, + filterable: true, + }; +} + +export function getEntityIdTableColumn( + localize: LocalizeFunc, + defaultHidden?: boolean +): DataTableColumnData { + return { + title: localize("ui.panel.config.generic.headers.entity_id"), + defaultHidden: defaultHidden, + filterable: true, + sortable: true, + }; +} + +export function getDomainTableColumn( + localize: LocalizeFunc +): DataTableColumnData { + return { + title: localize("ui.panel.config.generic.headers.domain"), + hidden: true, + groupable: true, + filterable: true, + sortable: false, + }; +} + +export function getAreaTableColumn( + localize: LocalizeFunc +): DataTableColumnData { + return { + title: localize("ui.panel.config.generic.headers.area"), + groupable: true, + filterable: true, + sortable: true, + minWidth: "120px", + }; +} + +export function getFloorTableColumn( + localize: LocalizeFunc +): DataTableColumnData { + return { + title: localize("ui.panel.config.generic.headers.floor"), + defaultHidden: true, + groupable: true, + filterable: true, + sortable: true, + minWidth: "120px", + }; +} + +export function getCategoryTableColumn( + localize: LocalizeFunc +): DataTableColumnData { + return { + title: localize("ui.panel.config.generic.headers.category"), + defaultHidden: true, + groupable: true, + filterable: true, + sortable: true, + }; +} + +export function getEditableTableColumn( + localize: LocalizeFunc, + tooltip: string +): DataTableColumnData { + return { + title: localize("ui.panel.config.generic.headers.editable"), + type: "icon", + showNarrow: true, + sortable: true, + minWidth: "88px", + maxWidth: "88px", + template: (entry: any) => html` + ${!entry.editable + ? html` + + + ${tooltip} + + ` + : nothing} + `, + }; +} + +export function getLabelsTableColumn(): DataTableColumnData { + return { + title: "", + hidden: true, + filterable: true, + template: (entry: any) => + entry.label_entries.map((lbl) => lbl.name).join(" "), + }; +} + +export function getTriggeredAtTableColumn( + localize: LocalizeFunc, + hass: HomeAssistant +): DataTableColumnData { + return { + title: localize("ui.card.automation.last_triggered"), + sortable: true, + template: (entry: any) => + renderRelativeTimeColumn( + entry.last_triggered, + "last-triggered", + entry.entity_id, + localize, + hass + ), + }; +} + +export const renderRelativeTimeColumn = ( + valueRelativeTime: string, + valueName: string, + entity_id: string, + localize: LocalizeFunc, + hass: HomeAssistant +) => { + if (!valueRelativeTime || isUnavailableState(valueRelativeTime)) { + return localize("ui.components.relative_time.never"); + } + const date = new Date(valueRelativeTime); + const now = new Date(); + const dayDifference = differenceInDays(now, date); + const formattedTime = formatShortDateTimeWithConditionalYear( + date, + hass.locale, + hass.config + ); + const elementId = valueName + "-" + slugify(entity_id); + return html` + ${dayDifference > 3 + ? formattedTime + : html` + ${formattedTime} + ${relativeTime(date, hass.locale)} + `} + `; +}; + +export function getCreatedAtTableColumn( + localize: LocalizeFunc, + hass: HomeAssistant +): DataTableColumnData { + return { + title: localize("ui.panel.config.generic.headers.created_at"), + defaultHidden: true, + sortable: true, + minWidth: "128px", + template: (entry: any) => renderDateTimeColumn(entry.created_at, hass), + }; +} + +export function getModifiedAtTableColumn( + localize: LocalizeFunc, + hass: HomeAssistant +): DataTableColumnData { + return { + title: localize("ui.panel.config.generic.headers.modified_at"), + defaultHidden: true, + sortable: true, + minWidth: "128px", + template: (entry: any) => renderDateTimeColumn(entry.modified_at, hass), + }; +} + +const renderDateTimeColumn = (valueDateTime: number, hass: HomeAssistant) => + html`${valueDateTime + ? formatShortDateTimeWithConditionalYear( + new Date(valueDateTime * 1000), + hass.locale, + hass.config + ) + : nothing}`; diff --git a/src/panels/config/common/suggest-metadata-ai.ts b/src/panels/config/common/suggest-metadata-ai.ts new file mode 100644 index 0000000000..8d8dac5025 --- /dev/null +++ b/src/panels/config/common/suggest-metadata-ai.ts @@ -0,0 +1,274 @@ +import { dump } from "js-yaml"; +import type { AITaskStructure, GenDataTaskResult } from "../../../data/ai_task"; +import type { HomeAssistant } from "../../../types"; +import type { SuggestWithAIGenerateTask } from "../../../components/ha-suggest-with-ai-button"; +import { + fetchCategories, + fetchFloors, + fetchLabels, +} from "./suggest-metadata-helpers"; + +export interface MetadataSuggestionResult { + name?: string; + description?: string; + category?: string; + labels?: string[]; + floor?: string; +} + +export type MetadataSuggestionDomain = + | "automation" + | "script" + | "scene" + | "area"; + +export interface MetadataSuggestionInclude { + name: boolean; + description?: boolean; + categories?: boolean; + labels?: boolean; + floor?: boolean; +} + +export const SUGGESTION_INCLUDE_DEFAULT: MetadataSuggestionInclude = { + name: true, + description: true, + categories: true, + labels: true, +} as const; + +// Always English to format lists in the prompt +const PROMPT_LIST_FORMAT = new Intl.ListFormat("en", { + style: "long", + type: "conjunction", +}); + +/** + * Generates an AI task for suggesting metadata based on their configuration. + * + * @param connection - Home Assistant connection + * @param language - User's language preference + * @param domain - The domain to suggest metadata for + * @param config - The configuration to suggest metadata for + * @param inspirations - Existing entries to use as inspiration + * @param include - The metadata fields to include in the suggestion + * @returns Promise resolving to the AI task structure + */ +export async function generateMetadataSuggestionTask( + connection: HomeAssistant["connection"], + language: HomeAssistant["language"], + domain: MetadataSuggestionDomain, + config: T, + inspirations: string[] = [], + include = SUGGESTION_INCLUDE_DEFAULT +): Promise { + const [categories, floors] = await Promise.all([ + include.categories + ? fetchCategories(connection, domain) + : Promise.resolve(undefined), + include.floor ? fetchFloors(connection) : Promise.resolve(undefined), + ]); + + const structure: AITaskStructure = { + ...(include.name && { + name: { + description: `The name of the ${domain}`, + required: true, + selector: { + text: {}, + }, + }, + }), + ...(include.description && { + description: { + description: `A short description of the ${domain}`, + required: false, + selector: { + text: {}, + }, + }, + }), + ...(include.labels && { + labels: { + description: `Labels for the ${domain}`, + required: false, + selector: { + text: { + multiple: true, + }, + }, + }, + }), + ...(include.categories && + categories && { + category: { + description: `The category of the ${domain}`, + required: false, + selector: { + select: { + options: Object.entries(categories).map(([id, name]) => ({ + value: id, + label: name, + })), + }, + }, + }, + }), + ...(include.floor && + floors && { + floor: { + description: `The floor of the ${domain}`, + required: false, + selector: { + select: { + options: Object.values(floors).map((floor) => ({ + value: floor.floor_id, + label: floor.name, + })), + }, + }, + }, + }), + }; + + const requestedParts = [ + include.name ? "a name" : null, + include.description ? "a description" : null, + include.categories ? "a category" : null, + include.labels ? "labels" : null, + include.floor ? "a floor" : null, + ].filter((entry): entry is string => entry !== null); + + const categoryLabels: string[] = [ + include.categories ? "category" : null, + include.labels ? "labels" : null, + include.floor ? "floor" : null, + ].filter((entry): entry is string => entry !== null); + + const categoryLabelsText = PROMPT_LIST_FORMAT.format(categoryLabels); + + const requestedPartsText = requestedParts.length + ? PROMPT_LIST_FORMAT.format(requestedParts) + : "suggestions"; + + return { + type: "data", + task: { + task_name: `frontend__${domain}__save`, + instructions: [ + `Suggest in language "${language}" ${requestedPartsText} for the following Home Assistant ${domain}.`, + "", + include.name + ? `The name should be relevant to the ${domain}'s purpose.` + : `The suggestions should be relevant to the ${domain}'s purpose.`, + ...(inspirations.length + ? [ + ...(include.name + ? [ + `The name should be in same style and sentence capitalization as existing ${domain}s.`, + ] + : []), + ...(include.categories || include.labels || include.floor + ? [ + `Suggest ${categoryLabelsText} if relevant to the ${domain}'s purpose.`, + `Only suggest ${categoryLabelsText} that are already used by existing ${domain}s.`, + ] + : []), + ] + : include.name + ? [ + `The name should be short, descriptive, sentence case, and written in the language ${language}.`, + ] + : []), + ...(include.description + ? [`If the ${domain} contains 5+ steps, include a short description.`] + : []), + "", + `For inspiration, here are existing ${domain}s:`, + inspirations.join("\n"), + "", + `The ${domain} configuration is as follows:`, + "", + `${dump(config)}`, + ].join("\n"), + structure, + }, + }; +} + +/** + * Processes the result of an AI task for suggesting metadata + * based on their configuration. + * + * @param connection - Home Assistant connection + * @param domain - The domain of the ${domain} + * @param result - The result of the AI task + * @param include - The metadata fields to include in the suggestion + * @returns Promise resolving to the processed metadata suggestion + */ +export async function processMetadataSuggestion( + connection: HomeAssistant["connection"], + domain: MetadataSuggestionDomain, + result: GenDataTaskResult, + include = SUGGESTION_INCLUDE_DEFAULT +): Promise { + const [categories, labels, floors] = await Promise.all([ + include.categories + ? fetchCategories(connection, domain) + : Promise.resolve(undefined), + include.labels ? fetchLabels(connection) : Promise.resolve(undefined), + include.floor ? fetchFloors(connection) : Promise.resolve(undefined), + ]); + + const processed: MetadataSuggestionResult = { + name: include.name ? result.data.name : undefined, + description: include.description ? result.data.description : undefined, + }; + + // Convert category name to ID + if (include.categories && categories && result.data.category) { + const categoryId = Object.entries(categories).find( + ([, name]) => name === result.data.category + )?.[0]; + if (categoryId) { + processed.category = categoryId; + } + } + + // Convert label names to IDs + if (include.labels && labels && result.data.labels?.length) { + const newLabels: Record = Object.fromEntries( + result.data.labels.map((name) => [name, undefined]) + ); + let toFind = result.data.labels.length; + for (const [labelId, labelName] of Object.entries(labels)) { + if (labelName in newLabels && newLabels[labelName] === undefined) { + newLabels[labelName] = labelId; + toFind--; + if (toFind === 0) { + break; + } + } + } + const foundLabels = Object.values(newLabels).filter( + (labelId): labelId is string => labelId !== undefined + ); + if (foundLabels.length) { + processed.labels = foundLabels; + } + } + + if (include.floor && floors && result.data.floor) { + const floorId = + result.data.floor in floors + ? result.data.floor + : Object.entries(floors).find( + ([, floor]) => floor.name === result.data.floor + )?.[0]; + if (floorId) { + processed.floor = floorId; + } + } + + return processed; +} diff --git a/src/panels/config/common/suggest-metadata-helpers.ts b/src/panels/config/common/suggest-metadata-helpers.ts new file mode 100644 index 0000000000..c45865f970 --- /dev/null +++ b/src/panels/config/common/suggest-metadata-helpers.ts @@ -0,0 +1,72 @@ +import { subscribeOne } from "../../../common/util/subscribe-one"; +import { subscribeAreaRegistry } from "../../../data/area/area_registry"; +import type { AreaRegistryEntry } from "../../../data/area/area_registry"; +import { fetchCategoryRegistry } from "../../../data/category_registry"; +import { + subscribeEntityRegistry, + type EntityRegistryEntry, +} from "../../../data/entity/entity_registry"; +import { subscribeFloorRegistry } from "../../../data/ws-floor_registry"; +import type { FloorRegistryEntry } from "../../../data/floor_registry"; +import { subscribeLabelRegistry } from "../../../data/label/label_registry"; +import type { HomeAssistant } from "../../../types"; +import type { MetadataSuggestionDomain } from "./suggest-metadata-ai"; + +export type Categories = Record; +export type Entities = Record; +export type Labels = Record; +export type Floors = Record; +export type Areas = Record; + +const tryCatchEmptyObject = (promise: Promise): Promise => + promise.catch((err) => { + // eslint-disable-next-line no-console + console.error("Error fetching data for suggestion: ", err); + return {} as T; + }); + +export const fetchCategories = ( + connection: HomeAssistant["connection"], + domain: MetadataSuggestionDomain +): Promise => + tryCatchEmptyObject( + fetchCategoryRegistry(connection, domain).then((cats) => + Object.fromEntries(cats.map((cat) => [cat.category_id, cat.name])) + ) + ); + +export const fetchLabels = ( + connection: HomeAssistant["connection"] +): Promise => + tryCatchEmptyObject( + subscribeOne(connection, subscribeLabelRegistry).then((labs) => + Object.fromEntries(labs.map((lab) => [lab.label_id, lab.name])) + ) + ); + +export const fetchFloors = ( + connection: HomeAssistant["connection"] +): Promise => + tryCatchEmptyObject( + subscribeOne(connection, subscribeFloorRegistry).then((floors) => + Object.fromEntries(floors.map((floor) => [floor.floor_id, floor])) + ) + ); + +export const fetchAreas = ( + connection: HomeAssistant["connection"] +): Promise => + tryCatchEmptyObject( + subscribeOne(connection, subscribeAreaRegistry).then((areas) => + Object.fromEntries(areas.map((area) => [area.area_id, area])) + ) + ); + +export const fetchEntities = ( + connection: HomeAssistant["connection"] +): Promise => + tryCatchEmptyObject( + subscribeOne(connection, subscribeEntityRegistry).then((ents) => + Object.fromEntries(ents.map((ent) => [ent.entity_id, ent])) + ) + ); diff --git a/src/panels/config/common/suggest-metadata-inspirations.ts b/src/panels/config/common/suggest-metadata-inspirations.ts new file mode 100644 index 0000000000..fd711e6528 --- /dev/null +++ b/src/panels/config/common/suggest-metadata-inspirations.ts @@ -0,0 +1,82 @@ +import { computeDomain } from "../../../common/entity/compute_domain"; +import type { HomeAssistant } from "../../../types"; +import type { MetadataSuggestionDomain } from "./suggest-metadata-ai"; +import { + fetchAreas, + fetchCategories, + fetchEntities, + fetchFloors, + fetchLabels, +} from "./suggest-metadata-helpers"; + +export const buildEntityMetadataInspirations = async ( + connection: HomeAssistant["connection"], + states: HomeAssistant["states"], + domain: MetadataSuggestionDomain +): Promise => { + const [categories, entities, labels] = await Promise.all([ + fetchCategories(connection, domain), + fetchEntities(connection), + fetchLabels(connection), + ]); + + return Object.values(entities).reduce((inspirations, entry) => { + if (!entry || computeDomain(entry.entity_id) !== domain) { + return inspirations; + } + + const entity = states[entry.entity_id]; + if ( + !entity || + entity.attributes.restored || + !entity.attributes.friendly_name + ) { + return inspirations; + } + + let inspiration = `- ${entity.attributes.friendly_name}`; + + const category = entry.categories[domain]; + if (category && categories[category]) { + inspiration += ` (category: ${categories[category]})`; + } + + if (entry.labels.length) { + const labelNames = entry.labels + .map((labelId) => labels[labelId]) + .filter(Boolean); + if (labelNames.length) { + inspiration += ` (labels: ${labelNames.join(", ")})`; + } + } + + inspirations.push(inspiration); + return inspirations; + }, []); +}; + +export const buildAreaMetadataInspirations = async ( + connection: HomeAssistant["connection"] +): Promise => { + const [labels, floors, areas] = await Promise.all([ + fetchLabels(connection), + fetchFloors(connection), + fetchAreas(connection), + ]); + + return Object.values(areas).reduce((inspirations, area) => { + if (!area.floor_id) { + return inspirations; + } + + const floorName = floors[area.floor_id]?.name; + const labelNames = area.labels + .map((labelId) => labels[labelId]) + .filter(Boolean); + + inspirations.push( + `- ${area.name} (${floorName ? `floor: ${floorName}` : "no floor"}${labelNames.length ? `, labels: ${labelNames.join(", ")}` : ""})` + ); + return inspirations; + }, []); +}; diff --git a/src/panels/config/core/ai-task-pref.ts b/src/panels/config/core/ai-task-pref.ts index 2bfdd937a4..e84c013440 100644 --- a/src/panels/config/core/ai-task-pref.ts +++ b/src/panels/config/core/ai-task-pref.ts @@ -14,7 +14,7 @@ import { saveAITaskPreferences, type AITaskPreferences, } from "../../../data/ai_task"; -import type { HomeAssistant } from "../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../types"; import { brandsUrl } from "../../../util/brands-url"; import { documentationUrl } from "../../../util/documentation-url"; import { computeDomain } from "../../../common/entity/compute_domain"; @@ -137,7 +137,7 @@ export class AITaskPref extends LitElement { `; } - private _handlePrefChange(ev: CustomEvent<{ value: string | undefined }>) { + private _handlePrefChange(ev: ValueChangedEvent) { const input = ev.target as HaEntityPicker; const key = input.dataset.name as keyof AITaskPreferences; const value = ev.detail.value || null; diff --git a/src/panels/config/core/ha-config-analytics.ts b/src/panels/config/core/ha-config-analytics.ts index 76e5c7c5c1..7c44ea8533 100644 --- a/src/panels/config/core/ha-config-analytics.ts +++ b/src/panels/config/core/ha-config-analytics.ts @@ -5,25 +5,29 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import "../../../components/ha-analytics"; import "../../../components/ha-card"; import "../../../components/ha-settings-row"; +import type { HaSwitch } from "../../../components/ha-switch"; import type { Analytics } from "../../../data/analytics"; import { getAnalyticsDetails, setAnalyticsPreferences, } from "../../../data/analytics"; +import type { LabPreviewFeature } from "../../../data/labs"; +import { subscribeLabFeature } from "../../../data/labs"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; -import type { HaSwitch } from "../../../components/ha-switch"; -import "../../../components/ha-alert"; @customElement("ha-config-analytics") -class ConfigAnalytics extends LitElement { +class ConfigAnalytics extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _analyticsDetails?: Analytics; @state() private _error?: string; + @state() private _snapshotsLabEnabled = false; + protected render(): TemplateResult { const error = this._error ? this._error @@ -56,8 +60,7 @@ class ConfigAnalytics extends LitElement { >
    - ${this._analyticsDetails && - "snapshots" in this._analyticsDetails.preferences + ${this._snapshotsLabEnabled ? html`

    ${this.hass.localize( - "ui.panel.config.analytics.preferences.snapshots.info" + "ui.panel.config.analytics.preferences.snapshots.info", + { + data_use_statement: html`${this.hass.localize( + "ui.panel.config.analytics.preferences.snapshots.data_use_statement" + )}`, + } )} - ${this.hass.localize( - "ui.panel.config.analytics.preferences.snapshots.learn_more" - )}.

    - ${this.hass.localize( - "ui.panel.config.analytics.preferences.snapshots.alert.content" - )} ${this.hass.localize( @@ -111,6 +108,19 @@ class ConfigAnalytics extends LitElement { `; } + public hassSubscribe() { + return [ + subscribeLabFeature( + this.hass.connection, + "analytics", + "snapshots", + (feature: LabPreviewFeature) => { + this._snapshotsLabEnabled = feature.enabled; + } + ), + ]; + } + protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); if (isComponentLoaded(this.hass, "analytics")) { diff --git a/src/panels/config/core/ha-config-section-ai-tasks.ts b/src/panels/config/core/ha-config-section-ai-tasks.ts new file mode 100644 index 0000000000..806d4a5b6c --- /dev/null +++ b/src/panels/config/core/ha-config-section-ai-tasks.ts @@ -0,0 +1,49 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import "../../../layouts/hass-subpage"; +import "./ai-task-pref"; +import type { HomeAssistant } from "../../../types"; + +@customElement("ha-config-section-ai-tasks") +class HaConfigSectionAITasks extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public narrow = false; + + protected render() { + return html` + +
    + +
    +
    + `; + } + + static styles = css` + .content { + padding: var(--ha-space-7) var(--ha-space-5) 0; + max-width: 1040px; + margin: 0 auto; + } + ai-task-pref { + max-width: 600px; + margin: 0 auto; + display: block; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-config-section-ai-tasks": HaConfigSectionAITasks; + } +} diff --git a/src/panels/config/core/ha-config-section-analytics.ts b/src/panels/config/core/ha-config-section-analytics.ts index 3b6df5ac48..e54b9d88c8 100644 --- a/src/panels/config/core/ha-config-section-analytics.ts +++ b/src/panels/config/core/ha-config-section-analytics.ts @@ -1,19 +1,17 @@ import { mdiDotsVertical, mdiDownload } from "@mdi/js"; import type { TemplateResult } from "lit"; -import { css, html, LitElement, nothing } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; import { getSignedPath } from "../../../data/auth"; import "../../../layouts/hass-subpage"; import type { HomeAssistant, Route } from "../../../types"; +import { fileDownload } from "../../../util/file_download"; import "./ha-config-analytics"; -import { - downloadFileSupported, - fileDownload, -} from "../../../util/file_download"; -import "../../../components/ha-dropdown-item"; -import "../../../components/ha-dropdown"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; @customElement("ha-config-section-analytics") class HaConfigSectionAnalytics extends LitElement { @@ -31,23 +29,19 @@ class HaConfigSectionAnalytics extends LitElement { .narrow=${this.narrow} .header=${this.hass.localize("ui.panel.config.analytics.caption")} > - ${downloadFileSupported(this.hass) - ? html` - - - - - - ${this.hass.localize( - "ui.panel.config.analytics.download_device_info" - )} - - - ` - : nothing} + + + + + + ${this.hass.localize( + "ui.panel.config.analytics.download_device_info" + )} + +
    @@ -56,7 +50,7 @@ class HaConfigSectionAnalytics extends LitElement { } private async _handleOverflowAction( - ev: CustomEvent<{ item: { value: string } }> + ev: HaDropdownSelectEvent ): Promise { if (ev.detail.item.value === "download_device_info") { const signedPath = await getSignedPath( @@ -77,6 +71,7 @@ class HaConfigSectionAnalytics extends LitElement { display: block; max-width: 600px; margin: 0 auto; + margin-bottom: 24px; } `; } diff --git a/src/panels/config/core/ha-config-section-general.ts b/src/panels/config/core/ha-config-section-general.ts index 412a0f3ab1..ecbc87161a 100644 --- a/src/panels/config/core/ha-config-section-general.ts +++ b/src/panels/config/core/ha-config-section-general.ts @@ -1,6 +1,6 @@ import type { TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, state, query } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { UNIT_C } from "../../../common/const"; import { stopPropagation } from "../../../common/dom/stop_propagation"; import { navigate } from "../../../common/navigate"; @@ -17,8 +17,6 @@ import "../../../components/ha-formfield"; import "../../../components/ha-language-picker"; import "../../../components/ha-radio"; import type { HaRadio } from "../../../components/ha-radio"; -import "../../../components/ha-select"; -import "../../../components/ha-settings-row"; import "../../../components/ha-textfield"; import type { HaTextField } from "../../../components/ha-textfield"; import "../../../components/ha-timezone-picker"; @@ -26,8 +24,7 @@ import type { ConfigUpdateValues } from "../../../data/core"; import { saveCoreConfig } from "../../../data/core"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-subpage"; -import "./ai-task-pref"; -import type { AITaskPref } from "./ai-task-pref"; +import "../../../components/map/ha-map"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant, ValueChangedEvent } from "../../../types"; @@ -37,7 +34,9 @@ class HaConfigSectionGeneral extends LitElement { @property({ type: Boolean }) public narrow = false; - @state() private _submitting = false; + @state() private _submittingName = false; + + @state() private _submittingRegional = false; @state() private _unitSystem?: ConfigUpdateValues["unit_system"]; @@ -59,13 +58,10 @@ class HaConfigSectionGeneral extends LitElement { @state() private _updateUnits?: boolean; - @query("ai-task-pref") private _aiTaskPref!: AITaskPref; - protected render(): TemplateResult { const canEdit = ["storage", "default"].includes( this.hass.config.config_source ); - const disabled = this._submitting || !canEdit; return html` ${this._error}` : ""} - -
    - ${!canEdit - ? html` - - ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.edit_requires_storage" - )} - - ` - : nothing} - - - - - -
    -
    + ${!canEdit + ? html` + ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.unit_system" + "ui.panel.config.core.section.core.core_config.edit_requires_storage" )} -
    - - ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.metric_example" - )} - -
    - ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.unit_system_metric" - )} -
    - `} - > - -
    - - ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.us_customary_example" - )} - -
    - ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.unit_system_us_customary" - )} -
    - `} - > - -
    - ${this._unitSystem !== this._configuredUnitSystem() - ? html` - - - -
    - ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.update_units_text_1" - )} - ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.update_units_text_2" - )}

    - ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.update_units_text_3" - )} -
    - ` - : ""} -
    - - - - -
    - - -
    - ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.edit_location" - )} -
    -
    - ${this.hass.localize( - "ui.panel.config.core.section.core.core_config.edit_location_description" - )} -
    - ${this.hass.localize("ui.common.edit")} -
    -
    - - ${this.hass!.localize("ui.common.save")} - -
    -
    - + + ` + : nothing} + ${this._renderHomeNameCard(canEdit)} + ${this._renderLocationCard(canEdit)} + ${this._renderRegionalSettingsCard(canEdit)}
    `; } + private _renderHomeNameCard(canEdit: boolean): TemplateResult { + const disabled = this._submittingName || !canEdit; + + return html` + +
    + +
    +
    + + ${this.hass.localize("ui.common.save")} + +
    +
    + `; + } + + private _renderLocationCard(canEdit: boolean): TemplateResult { + const hasHomeZone = "zone.home" in this.hass.states; + + return html` + + ${hasHomeZone + ? html` +
    + +
    + ` + : nothing} +
    + + ${this.hass.localize("ui.common.edit")} + +
    +
    + `; + } + + private _renderRegionalSettingsCard(canEdit: boolean): TemplateResult { + const disabled = this._submittingRegional || !canEdit; + + return html` + +
    + + + +
    +
    + ${this.hass.localize( + "ui.panel.config.core.section.core.core_config.unit_system" + )} +
    + + ${this.hass.localize( + "ui.panel.config.core.section.core.core_config.metric_example" + )} + +
    + ${this.hass.localize( + "ui.panel.config.core.section.core.core_config.unit_system_metric" + )} +
    + `} + > + +
    + + ${this.hass.localize( + "ui.panel.config.core.section.core.core_config.us_customary_example" + )} + +
    + ${this.hass.localize( + "ui.panel.config.core.section.core.core_config.unit_system_us_customary" + )} +
    + `} + > + +
    + ${this._unitSystem !== this._configuredUnitSystem() + ? html` + + + +
    + ${this.hass.localize( + "ui.panel.config.core.section.core.core_config.update_units_text_1" + )} + ${this.hass.localize( + "ui.panel.config.core.section.core.core_config.update_units_text_2" + )} +

    + ${this.hass.localize( + "ui.panel.config.core.section.core.core_config.update_units_text_3" + )} +
    + ` + : ""} +
    + + + + +
    +
    + + ${this.hass.localize("ui.common.save")} + +
    +
    + `; + } + private _configuredUnitSystem() { return this.hass.config.unit_system.temperature === UNIT_C ? "metric" @@ -298,12 +351,6 @@ class HaConfigSectionGeneral extends LitElement { this._timeZone = this.hass.config.time_zone || "Etc/GMT"; this._name = this.hass.config.location_name; this._updateUnits = true; - - if (window.location.hash === "#ai-task") { - this._aiTaskPref.updateComplete.then(() => { - this._aiTaskPref.scrollIntoView(); - }); - } } private _handleValueChanged(ev: ValueChangedEvent) { @@ -326,7 +373,31 @@ class HaConfigSectionGeneral extends LitElement { this._updateUnits = (ev.target as HaCheckbox).checked; } - private async _updateEntry(ev: CustomEvent) { + private async _updateHomeName(ev: CustomEvent) { + const button = ev.target as HaProgressButton; + if (button.progress) { + return; + } + + button.progress = true; + this._submittingName = true; + this._error = undefined; + + try { + await saveCoreConfig(this.hass, { + location_name: this._name, + }); + button.actionSuccess(); + } catch (err: any) { + button.actionError(); + this._error = err.message; + } finally { + button.progress = false; + this._submittingName = false; + } + } + + private async _updateRegionalSettings(ev: CustomEvent) { const button = ev.target as HaProgressButton; if (button.progress) { return; @@ -351,6 +422,8 @@ class HaConfigSectionGeneral extends LitElement { } } button.progress = true; + this._submittingRegional = true; + this._error = undefined; let locationConfig; @@ -365,14 +438,13 @@ class HaConfigSectionGeneral extends LitElement { try { await saveCoreConfig(this.hass, { - currency: this._currency, + time_zone: this._timeZone, elevation: Number(this._elevation), unit_system: this._unitSystem, update_units: this._updateUnits && unitSystemChanged, - time_zone: this._timeZone, - location_name: this._name, - language: this._language, + currency: this._currency, country: this._country, + language: this._language, ...locationConfig, }); button.actionSuccess(); @@ -381,6 +453,7 @@ class HaConfigSectionGeneral extends LitElement { this._error = err.message; } finally { button.progress = false; + this._submittingRegional = false; } } @@ -392,48 +465,39 @@ class HaConfigSectionGeneral extends LitElement { haStyle, css` .content { - padding: 28px 20px 0; + padding: var(--ha-space-7) var(--ha-space-5) 0; max-width: 1040px; margin: 0 auto; } - ha-card, - ai-task-pref { + ha-card { max-width: 600px; - margin: 0 auto; - height: 100%; - justify-content: space-between; - flex-direction: column; - display: flex; - } - ha-card, - ai-task-pref { - margin-bottom: 24px; + margin: 0 auto var(--ha-space-6); } .card-content { display: flex; - justify-content: space-between; flex-direction: column; - padding: 16px 16px 0 16px; - } - .card-actions { - text-align: right; - height: 48px; - display: flex; - justify-content: flex-end; - align-items: center; - margin-top: 16px; } .card-content > * { display: block; - margin-top: 16px; } - ha-select { - display: block; + .card-content > *:not(:first-child) { + margin-top: var(--ha-space-4); + } + .card-actions { + display: flex; + justify-content: flex-end; } a.find-value { - margin-top: 8px; + margin-top: var(--ha-space-2); display: inline-block; } + .map-preview { + height: 200px; + width: 100%; + display: block; + border-radius: var(--ha-card-border-radius, 8px); + overflow: hidden; + } `, ]; } diff --git a/src/panels/config/core/ha-config-section-updates.ts b/src/panels/config/core/ha-config-section-updates.ts index 80b0a8e8f1..8b7574cd92 100644 --- a/src/panels/config/core/ha-config-section-updates.ts +++ b/src/panels/config/core/ha-config-section-updates.ts @@ -33,6 +33,7 @@ import { showJoinBetaDialog } from "./updates/show-dialog-join-beta"; import "../../../components/ha-dropdown"; import "../../../components/ha-dropdown-item"; import "@home-assistant/webawesome/dist/components/divider/divider"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; @customElement("ha-config-section-updates") class HaConfigSectionUpdates extends LitElement { @@ -107,11 +108,6 @@ class HaConfigSectionUpdates extends LitElement { ${this.hass.localize( `ui.panel.config.updates.${this._supervisorInfo.channel === "stable" ? "join" : "leave"}_beta` )} - ${this._supervisorInfo.channel === "stable" - ? this.hass.localize("ui.panel.config.updates.join_beta") - : this.hass.localize( - "ui.panel.config.updates.leave_beta" - )} ` : nothing} @@ -166,9 +162,7 @@ class HaConfigSectionUpdates extends LitElement { this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass); } - private _handleOverflowAction( - ev: CustomEvent<{ item: { value: string } }> - ): void { + private _handleOverflowAction(ev: HaDropdownSelectEvent): void { if (ev.detail.item.value === "toggle_beta") { if (this._supervisorInfo!.channel === "stable") { showJoinBetaDialog(this, { diff --git a/src/panels/config/core/ha-config-system-navigation.ts b/src/panels/config/core/ha-config-system-navigation.ts index 1da597e734..e895d94bb1 100644 --- a/src/panels/config/core/ha-config-system-navigation.ts +++ b/src/panels/config/core/ha-config-system-navigation.ts @@ -87,7 +87,9 @@ class HaConfigSystemNavigation extends LitElement { description = this._storageInfo ? this.hass.localize("ui.panel.config.storage.description", { percent_used: `${Math.round( - (this._storageInfo.used / this._storageInfo.total) * 100 + ((this._storageInfo.total - this._storageInfo.free) / + this._storageInfo.total) * + 100 )}${blankBeforePercent(this.hass.locale)}%`, free_space: `${this._storageInfo.free} GB`, }) diff --git a/src/panels/config/core/updates/dialog-join-beta.ts b/src/panels/config/core/updates/dialog-join-beta.ts index 32018be0ba..46c3a68d6f 100644 --- a/src/panels/config/core/updates/dialog-join-beta.ts +++ b/src/panels/config/core/updates/dialog-join-beta.ts @@ -5,7 +5,8 @@ import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-alert"; import "../../../../components/ha-button"; -import { createCloseHeading } from "../../../../components/ha-dialog"; +import "../../../../components/ha-dialog-footer"; +import "../../../../components/ha-wa-dialog"; import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; @@ -21,14 +22,21 @@ export class DialogJoinBeta @state() private _dialogParams?: JoinBetaDialogParams; + @state() private _open = false; + public showDialog(dialogParams: JoinBetaDialogParams): void { this._dialogParams = dialogParams; + this._open = true; } public closeDialog() { + this._open = false; + return true; + } + + private _dialogClosed() { this._dialogParams = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); - return true; } protected render() { @@ -37,13 +45,11 @@ export class DialogJoinBeta } return html` - ${this.hass.localize("ui.dialogs.join_beta_channel.backup")} @@ -67,17 +73,19 @@ export class DialogJoinBeta )} - - ${this.hass.localize("ui.common.cancel")} - - - ${this.hass.localize("ui.dialogs.join_beta_channel.join")} - - + + + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize("ui.dialogs.join_beta_channel.join")} + + + `; } diff --git a/src/panels/config/dashboard/dialog-new-dashboard.ts b/src/panels/config/dashboard/dialog-new-dashboard.ts index 319b3de620..089a9722f1 100644 --- a/src/panels/config/dashboard/dialog-new-dashboard.ts +++ b/src/panels/config/dashboard/dialog-new-dashboard.ts @@ -1,21 +1,23 @@ +import type { IFuseOptions } from "fuse.js"; +import Fuse from "fuse.js"; import type { CSSResultGroup } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; -import type { IFuseOptions } from "fuse.js"; -import Fuse from "fuse.js"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; -import { createCloseHeading } from "../../../components/ha-dialog"; +import type { + LocalizeFunc, + LocalizeKeys, +} from "../../../common/translations/localize"; +import "../../../components/ha-wa-dialog"; import "../../../components/search-input"; -import type { LovelaceRawConfig } from "../../../data/lovelace/config/types"; +import type { LovelaceConfig } from "../../../data/lovelace/config/types"; import type { HassDialog } from "../../../dialogs/make-dialog-manager"; -import { haStyle, haStyleDialog } from "../../../resources/styles"; +import { haStyleScrollbar } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; -import type { NewDashboardDialogParams } from "./show-dialog-new-dashboard"; +import { generateDefaultView } from "../../lovelace/views/default-view"; import "./dashboard-card"; -import type { LocalizeKeys } from "../../../common/translations/localize"; - -const EMPTY_CONFIG: LovelaceRawConfig = { views: [{ title: "Home" }] }; +import type { NewDashboardDialogParams } from "./show-dialog-new-dashboard"; interface Strategy { type: string; @@ -26,15 +28,15 @@ interface Strategy { const STRATEGIES = [ { - type: "overview", + type: "original-states", images: { light: - "/static/images/dashboard-options/light/icon-dashboard-overview.svg", - dark: "/static/images/dashboard-options/dark/icon-dashboard-overview.svg", + "/static/images/dashboard-options/light/icon-dashboard-overview-legacy.svg", + dark: "/static/images/dashboard-options/dark/icon-dashboard-overview-legacy.svg", }, - name: "ui.panel.config.lovelace.dashboards.dialog_new.strategy.overview.title", + name: "ui.panel.config.lovelace.dashboards.dialog_new.strategy.overview-legacy.title", description: - "ui.panel.config.lovelace.dashboards.dialog_new.strategy.overview.description", + "ui.panel.config.lovelace.dashboards.dialog_new.strategy.overview-legacy.description", }, { type: "map", @@ -63,7 +65,7 @@ const STRATEGIES = [ class DialogNewDashboard extends LitElement implements HassDialog { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _opened = false; + @state() private _open = false; @state() private _params?: NewDashboardDialogParams; @@ -75,7 +77,7 @@ class DialogNewDashboard extends LitElement implements HassDialog { })[] = []; public showDialog(params: NewDashboardDialogParams): void { - this._opened = true; + this._open = true; this._params = params; this._localizedStrategies = STRATEGIES.map((strategy) => ({ ...strategy, @@ -87,105 +89,116 @@ class DialogNewDashboard extends LitElement implements HassDialog { } public closeDialog() { - if (this._opened) { - fireEvent(this, "dialog-closed", { dialog: this.localName }); - } - this._opened = false; - this._params = undefined; + this._open = false; return true; } + private _dialogClosed(): void { + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + private _generateDefaultConfig = memoizeOne( + (localize: LocalizeFunc): LovelaceConfig => ({ + views: [generateDefaultView(localize, true)], + }) + ); + protected render() { - if (!this._opened) { + if (!this._params) { return nothing; } + const defaultConfig = this._generateDefaultConfig(this.hass.localize); + return html` - - -
    - ${this._filter - ? html` -
    - ${this._filterStrategies( - this._localizedStrategies, - this._filter - ).map( - (strategy) => html` - - ` - )} -
    - ` - : html` -
    - -
    -
    -
    - ${this.hass.localize( - `ui.panel.config.lovelace.dashboards.dialog_new.heading.default` +
    + +
    + ${this._filter + ? html` +
    + ${this._filterStrategies( + this._localizedStrategies, + this._filter + ).map( + (strategy) => html` + + ` )}
    - ${this._localizedStrategies.map( - (strategy) => html` - - ` - )} -
    - `} + ` + : html` +
    + +
    +
    +
    + ${this.hass.localize( + `ui.panel.config.lovelace.dashboards.dialog_new.heading.default` + )} +
    + ${this._localizedStrategies.map( + (strategy) => html` + + ` + )} +
    + `} +
    - + `; } @@ -234,11 +247,7 @@ class DialogNewDashboard extends LitElement implements HassDialog { if (target.config) { config = target.config; } else if (target.strategy) { - if (target.strategy === "overview") { - config = null; - } else { - config = this._generateStrategyConfig(target.strategy); - } + config = this._generateStrategyConfig(target.strategy); } this._params?.selectConfig(config); @@ -247,33 +256,16 @@ class DialogNewDashboard extends LitElement implements HassDialog { static get styles(): CSSResultGroup { return [ - haStyle, - haStyleDialog, + haStyleScrollbar, css` - @media all and (max-width: 450px), all and (max-height: 500px) { - /* overrule the ha-style-dialog max-height on small screens */ - ha-dialog { - --mdc-dialog-max-height: 100%; - height: 100%; - } - } - - @media all and (min-width: 850px) { - ha-dialog { - --mdc-dialog-min-width: 845px; - --mdc-dialog-min-height: calc( - 100vh - var(--ha-space-18) - var(--safe-area-inset-y) - ); - --mdc-dialog-max-height: calc( - 100vh - var(--ha-space-18) - var(--safe-area-inset-y) - ); - } - } - - ha-dialog { - --mdc-dialog-max-width: 845px; + ha-wa-dialog { --dialog-content-padding: 0; --dialog-z-index: 6; + --ha-dialog-min-height: 60svh; + } + ha-wa-dialog::part(body) { + overflow: hidden; + min-height: 0; } .cards-container-header { font-size: var(--ha-font-size-l); @@ -309,8 +301,17 @@ class DialogNewDashboard extends LitElement implements HassDialog { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); margin-top: 20px; } + .content-wrapper { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + } .content { - padding: 0 24px 0 24px; + padding: 0 var(--ha-space-6) var(--ha-space-6) var(--ha-space-6); + flex: 1; + min-height: 0; + overflow: auto; } `, ]; diff --git a/src/panels/config/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts index 0d49a2d2d1..ae036e45cd 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.ts +++ b/src/panels/config/dashboard/ha-config-dashboard.ts @@ -1,4 +1,3 @@ -import type { ActionDetail } from "@material/mwc-list"; import { mdiCloudLock, mdiDotsVertical, @@ -13,11 +12,11 @@ import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import "../../../components/chips/ha-assist-chip"; -import "../../../components/ha-button-menu"; import "../../../components/ha-card"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-next"; -import "../../../components/ha-list-item"; import "../../../components/ha-menu-button"; import "../../../components/ha-svg-icon"; import "../../../components/ha-tip"; @@ -40,12 +39,14 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; +import { isMac } from "../../../util/is_mac"; import { isMobileClient } from "../../../util/is_mobile"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; import "../repairs/ha-config-repairs"; import "./ha-config-navigation"; import "./ha-config-updates"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; const randomTip = (openFn: any, hass: HomeAssistant, narrow: boolean) => { const weighted: string[] = []; @@ -124,6 +125,14 @@ const randomTip = (openFn: any, hass: HomeAssistant, narrow: boolean) => { content: hass.localize("ui.tips.key_a_tip", localizeParam), weight: 1, narrow: false, + }, + { + content: hass.localize("ui.tips.key_shortcut_quick_search", { + ...localizeParam, + modifier: isMac ? "⌘" : "Ctrl", + }), + weight: 1, + narrow: false, } ); } @@ -226,25 +235,25 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) { .path=${mdiMagnify} @click=${this._showQuickBar} > - + - + ${this.hass.localize("ui.panel.config.updates.check_updates")} - - + + - + ${this.hass.localize( "ui.panel.config.system_dashboard.restart_homeassistant" )} - - - + + + ${this.hass.localize("ui.tips.keyboard_shortcut")}`, - }; - - showQuickBar(this, { - hint: this.hass.enableShortcuts - ? this.hass.localize("ui.dialogs.quick-bar.key_c_tip", params) - : undefined, - }); + showQuickBar(this, { showHint: this.hass.enableShortcuts }); } - private async _handleMenuAction(ev: CustomEvent) { - switch (ev.detail.index) { - case 0: + private async _handleMenuAction(ev: HaDropdownSelectEvent) { + const action = ev.detail.item.value; + switch (action) { + case "check-updates": checkForEntityUpdates(this, this.hass); break; - case 1: + case "restart": showRestartDialog(this); break; } diff --git a/src/panels/config/dashboard/ha-config-navigation.ts b/src/panels/config/dashboard/ha-config-navigation.ts index be67f75325..89b33c526a 100644 --- a/src/panels/config/dashboard/ha-config-navigation.ts +++ b/src/panels/config/dashboard/ha-config-navigation.ts @@ -1,7 +1,7 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { canShowPage } from "../../../common/config/can_show_page"; +import { filterNavigationPages } from "../../../common/config/filter_navigation_pages"; import "../../../components/ha-card"; import "../../../components/ha-icon-next"; import "../../../components/ha-navigation-list"; @@ -30,38 +30,29 @@ class HaConfigNavigation extends LitElement { } protected render(): TemplateResult { - const pages = this.pages - .filter((page) => { - if (page.path === "#external-app-configuration") { - return this.hass.auth.external?.config.hasSettingsScreen; - } - // Only show Bluetooth page if there are Bluetooth config entries - if (page.component === "bluetooth") { - return this._hasBluetoothConfigEntries; - } - return canShowPage(this.hass, page); - }) - .map((page) => ({ - ...page, - name: - page.name || - this.hass.localize( - `ui.panel.config.dashboard.${page.translationKey}.main` - ), - description: - page.component === "cloud" && (page.info as CloudStatus) - ? page.info.logged_in - ? ` + const pages = filterNavigationPages(this.hass, this.pages, { + hasBluetoothConfigEntries: this._hasBluetoothConfigEntries, + }).map((page) => ({ + ...page, + name: + page.name || + this.hass.localize( + `ui.panel.config.dashboard.${page.translationKey}.main` + ), + description: + page.component === "cloud" && (page.info as CloudStatus) + ? page.info.logged_in + ? ` ${this.hass.localize( "ui.panel.config.cloud.description_login" )} ` - : ` + : ` ${this.hass.localize( "ui.panel.config.cloud.description_features" )} ` - : ` + : ` ${ page.description || this.hass.localize( @@ -69,7 +60,7 @@ class HaConfigNavigation extends LitElement { ) } `, - })); + })); return html`
    ${this.hass.localize("panel.config")} diff --git a/src/panels/developer-tools/action/developer-tools-action.ts b/src/panels/config/developer-tools/action/developer-tools-action.ts similarity index 86% rename from src/panels/developer-tools/action/developer-tools-action.ts rename to src/panels/config/developer-tools/action/developer-tools-action.ts index c2ca28cb89..05f4fcc00e 100644 --- a/src/panels/developer-tools/action/developer-tools-action.ts +++ b/src/panels/config/developer-tools/action/developer-tools-action.ts @@ -7,38 +7,41 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { until } from "lit/directives/until"; import memoizeOne from "memoize-one"; -import { storage } from "../../../common/decorators/storage"; -import { computeDomain } from "../../../common/entity/compute_domain"; -import { computeObjectId } from "../../../common/entity/compute_object_id"; -import { hasTemplate, isTemplate } from "../../../common/string/has-template"; -import type { LocalizeFunc } from "../../../common/translations/localize"; -import { extractSearchParam } from "../../../common/url/search-params"; -import { copyToClipboard } from "../../../common/util/copy-clipboard"; -import type { HaProgressButton } from "../../../components/buttons/ha-progress-button"; -import { showToast } from "../../../util/toast"; +import { storage } from "../../../../common/decorators/storage"; +import { computeDomain } from "../../../../common/entity/compute_domain"; +import { computeObjectId } from "../../../../common/entity/compute_object_id"; +import { + hasTemplate, + isTemplate, +} from "../../../../common/string/has-template"; +import type { LocalizeFunc } from "../../../../common/translations/localize"; +import { extractSearchParam } from "../../../../common/url/search-params"; +import { copyToClipboard } from "../../../../common/util/copy-clipboard"; +import type { HaProgressButton } from "../../../../components/buttons/ha-progress-button"; +import { showToast } from "../../../../util/toast"; -import "../../../components/entity/ha-entity-picker"; -import "../../../components/ha-alert"; -import "../../../components/ha-button"; -import "../../../components/ha-card"; -import "../../../components/buttons/ha-progress-button"; -import "../../../components/ha-expansion-panel"; -import "../../../components/ha-icon-button"; -import "../../../components/ha-service-control"; -import "../../../components/ha-service-picker"; -import "../../../components/ha-yaml-editor"; -import type { HaYamlEditor } from "../../../components/ha-yaml-editor"; -import { forwardHaptic } from "../../../data/haptics"; -import type { Action, ServiceAction } from "../../../data/script"; -import { migrateAutomationAction } from "../../../data/script"; +import "../../../../components/entity/ha-entity-picker"; +import "../../../../components/ha-alert"; +import "../../../../components/ha-button"; +import "../../../../components/ha-card"; +import "../../../../components/buttons/ha-progress-button"; +import "../../../../components/ha-expansion-panel"; +import "../../../../components/ha-icon-button"; +import "../../../../components/ha-service-control"; +import "../../../../components/ha-service-picker"; +import "../../../../components/ha-yaml-editor"; +import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; +import { forwardHaptic } from "../../../../data/haptics"; +import type { Action, ServiceAction } from "../../../../data/script"; +import { migrateAutomationAction } from "../../../../data/script"; import { callExecuteScript, serviceCallWillDisconnect, -} from "../../../data/service"; -import { haStyle } from "../../../resources/styles"; -import type { HomeAssistant } from "../../../types"; -import { documentationUrl } from "../../../util/documentation-url"; -import { resolveMediaSource } from "../../../data/media_source"; +} from "../../../../data/service"; +import { haStyle } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import { documentationUrl } from "../../../../util/documentation-url"; +import { resolveMediaSource } from "../../../../data/media_source"; @customElement("developer-tools-action") class HaPanelDevAction extends LitElement { @@ -144,7 +147,7 @@ class HaPanelDevAction extends LitElement {

    ${this.hass.localize( - "ui.panel.developer-tools.tabs.actions.description" + "ui.panel.config.developer-tools.tabs.actions.description" )}

    @@ -189,23 +192,23 @@ class HaPanelDevAction extends LitElement { > ${this._yamlMode ? this.hass.localize( - "ui.panel.developer-tools.tabs.actions.ui_mode" + "ui.panel.config.developer-tools.tabs.actions.ui_mode" ) : this.hass.localize( - "ui.panel.developer-tools.tabs.actions.yaml_mode" + "ui.panel.config.developer-tools.tabs.actions.yaml_mode" )} ${!this._uiAvailable ? html`${this.hass.localize( - "ui.panel.developer-tools.tabs.actions.no_template_ui_support" + "ui.panel.config.developer-tools.tabs.actions.no_template_ui_support" )}` : ""}
    ${this.hass.localize( - "ui.panel.developer-tools.tabs.actions.call_service" + "ui.panel.config.developer-tools.tabs.actions.call_service" )}
    @@ -214,7 +217,7 @@ class HaPanelDevAction extends LitElement { ? html`
    @@ -230,7 +233,7 @@ class HaPanelDevAction extends LitElement { slot="extra-actions" @click=${this._copyTemplate} >${this.hass.localize( - "ui.panel.developer-tools.tabs.actions.copy_clipboard_template" + "ui.panel.config.developer-tools.tabs.actions.copy_clipboard_template" )} @@ -244,10 +247,10 @@ class HaPanelDevAction extends LitElement { ${this.hass.localize( - "ui.panel.developer-tools.tabs.actions.column_parameter" + "ui.panel.config.developer-tools.tabs.actions.column_parameter" )} ${this.hass.localize( - "ui.panel.developer-tools.tabs.actions.column_description" + "ui.panel.config.developer-tools.tabs.actions.column_description" )} ${this.hass.localize( - "ui.panel.developer-tools.tabs.actions.column_example" + "ui.panel.config.developer-tools.tabs.actions.column_example" )} @@ -333,7 +336,7 @@ class HaPanelDevAction extends LitElement { appearance="plain" @click=${this._fillExampleData} >${this.hass.localize( - "ui.panel.developer-tools.tabs.actions.fill_example_data" + "ui.panel.config.developer-tools.tabs.actions.fill_example_data" )}` : ""} @@ -366,14 +369,14 @@ class HaPanelDevAction extends LitElement { const errorCategory = yamlMode ? "yaml" : "ui"; if (!serviceData?.action) { return localize( - `ui.panel.developer-tools.tabs.actions.errors.${errorCategory}.no_action` + `ui.panel.config.developer-tools.tabs.actions.errors.${errorCategory}.no_action` ); } const domain = computeDomain(serviceData.action); const service = computeObjectId(serviceData.action); if (!domain || !service) { return localize( - `ui.panel.developer-tools.tabs.actions.errors.${errorCategory}.invalid_action` + `ui.panel.config.developer-tools.tabs.actions.errors.${errorCategory}.invalid_action` ); } const dataIsTemplate = @@ -387,7 +390,7 @@ class HaPanelDevAction extends LitElement { !serviceData.data?.area_id ) { return localize( - `ui.panel.developer-tools.tabs.actions.errors.${errorCategory}.no_target` + `ui.panel.config.developer-tools.tabs.actions.errors.${errorCategory}.no_target` ); } for (const field of fields) { @@ -397,7 +400,7 @@ class HaPanelDevAction extends LitElement { (!serviceData.data || serviceData.data[field.key] === undefined) ) { return localize( - `ui.panel.developer-tools.tabs.actions.errors.${errorCategory}.missing_required_field`, + `ui.panel.config.developer-tools.tabs.actions.errors.${errorCategory}.missing_required_field`, { key: field.key } ); } @@ -456,7 +459,7 @@ class HaPanelDevAction extends LitElement { forwardHaptic(this, "failure"); button.actionError(); this._error = this.hass.localize( - "ui.panel.developer-tools.tabs.actions.errors.yaml.invalid_yaml" + "ui.panel.config.developer-tools.tabs.actions.errors.yaml.invalid_yaml" ); return; } @@ -529,7 +532,7 @@ class HaPanelDevAction extends LitElement { rel="noreferrer" > ${this.hass.localize( - "ui.panel.developer-tools.tabs.actions.open_media" + "ui.panel.config.developer-tools.tabs.actions.open_media" )} diff --git a/src/panels/developer-tools/assist/developer-tools-assist.ts b/src/panels/config/developer-tools/assist/developer-tools-assist.ts similarity index 83% rename from src/panels/developer-tools/assist/developer-tools-assist.ts rename to src/panels/config/developer-tools/assist/developer-tools-assist.ts index 1041f66c23..1cfa4cf9a1 100644 --- a/src/panels/developer-tools/assist/developer-tools-assist.ts +++ b/src/panels/config/developer-tools/assist/developer-tools-assist.ts @@ -3,21 +3,21 @@ import { dump } from "js-yaml"; import type { CSSResultGroup } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; -import { storage } from "../../../common/decorators/storage"; -import { formatLanguageCode } from "../../../common/language/format_language"; -import "../../../components/ha-alert"; -import "../../../components/ha-button"; -import "../../../components/ha-card"; -import "../../../components/ha-code-editor"; -import "../../../components/ha-language-picker"; -import "../../../components/ha-textarea"; -import type { HaTextArea } from "../../../components/ha-textarea"; -import type { AssistDebugResult } from "../../../data/conversation"; -import { debugAgent, listAgents } from "../../../data/conversation"; -import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; -import { haStyle } from "../../../resources/styles"; -import type { HomeAssistant } from "../../../types"; -import { fileDownload } from "../../../util/file_download"; +import { storage } from "../../../../common/decorators/storage"; +import { formatLanguageCode } from "../../../../common/language/format_language"; +import "../../../../components/ha-alert"; +import "../../../../components/ha-button"; +import "../../../../components/ha-card"; +import "../../../../components/ha-code-editor"; +import "../../../../components/ha-language-picker"; +import "../../../../components/ha-textarea"; +import type { HaTextArea } from "../../../../components/ha-textarea"; +import type { AssistDebugResult } from "../../../../data/conversation"; +import { debugAgent, listAgents } from "../../../../data/conversation"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; +import { haStyle } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import { fileDownload } from "../../../../util/file_download"; interface SentenceParsingResult { sentence: string; @@ -118,14 +118,14 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {

    ${this.hass.localize( - "ui.panel.developer-tools.tabs.assist.description" + "ui.panel.config.developer-tools.tabs.assist.description" )}

    ${this.supportedLanguages @@ -141,7 +141,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) { ${this.hass.localize( - "ui.panel.developer-tools.tabs.assist.parse_sentences" + "ui.panel.config.developer-tools.tabs.assist.parse_sentences" )}
    @@ -175,7 +175,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) { ${this.hass.localize( - "ui.panel.developer-tools.tabs.assist.download_results" + "ui.panel.config.developer-tools.tabs.assist.download_results" )}
    @@ -194,7 +194,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
    ${this.hass.localize( - "ui.panel.developer-tools.tabs.assist.language" + "ui.panel.config.developer-tools.tabs.assist.language" )}: ${formatLanguageCode(language, this.hass.locale)} (${language}) @@ -211,7 +211,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) { ` : html` ${this.hass.localize( - "ui.panel.developer-tools.tabs.assist.no_match" + "ui.panel.config.developer-tools.tabs.assist.no_match" )} `}
    diff --git a/src/panels/developer-tools/blueprints/blueprint-metadata-editor.ts b/src/panels/config/developer-tools/blueprints/blueprint-metadata-editor.ts similarity index 75% rename from src/panels/developer-tools/blueprints/blueprint-metadata-editor.ts rename to src/panels/config/developer-tools/blueprints/blueprint-metadata-editor.ts index 2ba5371239..ed95c13943 100644 --- a/src/panels/developer-tools/blueprints/blueprint-metadata-editor.ts +++ b/src/panels/config/developer-tools/blueprints/blueprint-metadata-editor.ts @@ -1,15 +1,15 @@ import { nothing, LitElement, html, type CSSResultGroup, css } from "lit"; import { customElement, property } from "lit/decorators"; -import "../../../components/ha-yaml-editor"; -import "../../../components/ha-textfield"; -import "../../../components/ha-button"; -import "../../../components/ha-card"; -import "../../../components/ha-form/ha-form"; -import type { HomeAssistant } from "../../../types"; +import "../../../../components/ha-yaml-editor"; +import "../../../../components/ha-textfield"; +import "../../../../components/ha-button"; +import "../../../../components/ha-card"; +import "../../../../components/ha-form/ha-form"; +import type { HomeAssistant } from "../../../../types"; import "./ha-blueprint-editor"; -import type { BlueprintMetaDataEditorSchema } from "../../../data/blueprint"; -import type { SchemaUnion } from "../../../components/ha-form/types"; -import { haStyle } from "../../../resources/styles"; +import type { BlueprintMetaDataEditorSchema } from "../../../../data/blueprint"; +import type { SchemaUnion } from "../../../../components/ha-form/types"; +import { haStyle } from "../../../../resources/styles"; const SCHEMA = [ { @@ -49,7 +49,7 @@ class BlueprintMetadataEditor extends LitElement { private _computeLabel(step: SchemaUnion) { return this.hass.localize( - `ui.panel.developer-tools.tabs.blueprints.editor.${step.name}.label` + `ui.panel.config.developer-tools.tabs.blueprints.editor.${step.name}.label` ); } @@ -63,7 +63,7 @@ class BlueprintMetadataEditor extends LitElement {

    ${this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.metadata" + "ui.panel.config.developer-tools.tabs.blueprints.editor.metadata" )}

    diff --git a/src/panels/developer-tools/blueprints/developer-tools-blueprints.ts b/src/panels/config/developer-tools/blueprints/developer-tools-blueprints.ts similarity index 81% rename from src/panels/developer-tools/blueprints/developer-tools-blueprints.ts rename to src/panels/config/developer-tools/blueprints/developer-tools-blueprints.ts index bb268a9191..5b0ace548f 100644 --- a/src/panels/developer-tools/blueprints/developer-tools-blueprints.ts +++ b/src/panels/config/developer-tools/blueprints/developer-tools-blueprints.ts @@ -3,22 +3,19 @@ import { css, LitElement, html } from "lit"; import { customElement, property, state } from "lit/decorators"; import { mdiContentSave } from "@mdi/js"; import yaml, { dump } from "js-yaml"; -import type { HomeAssistant } from "../../../types"; -import { haStyle } from "../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import { haStyle } from "../../../../resources/styles"; import type { Blueprint, BlueprintDomain, Blueprints, -} from "../../../data/blueprint"; +} from "../../../../data/blueprint"; import { showPickBlueprintDialog } from "./pick-blueprint-dialog/show-dialog-pick-blueprint"; import { showAlertDialog, showConfirmationDialog, -} from "../../../dialogs/generic/show-dialog-box"; -import { - manualEditorStyles, - saveFabStyles, -} from "../../config/automation/styles"; +} from "../../../../dialogs/generic/show-dialog-box"; +import { manualEditorStyles, saveFabStyles } from "../../automation/styles"; import { DefaultAutomationBlueprint, DefaultScriptBlueprint, @@ -27,11 +24,11 @@ import { saveBlueprint, fetchBlueprints, BlueprintYamlSchema, -} from "../../../data/blueprint"; -import "../../../components/ha-textfield"; -import "../../../components/ha-button"; -import "../../../components/ha-card"; -import "../../../components/ha-fab"; +} from "../../../../data/blueprint"; +import "../../../../components/ha-textfield"; +import "../../../../components/ha-button"; +import "../../../../components/ha-card"; +import "../../../../components/ha-fab"; import "./ha-blueprint-editor"; import "./blueprint-metadata-editor"; @@ -78,10 +75,10 @@ class HaPanelDevBlueprints extends LitElement { text: err.status_code === 404 ? this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.load_error_not_editable" + "ui.panel.config.developer-tools.tabs.blueprints.editor.load_error_not_editable" ) : this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.load_error_unknown", + "ui.panel.config.developer-tools.tabs.blueprints.editor.load_error_unknown", { err_no: err.status_code } ), }); @@ -99,10 +96,10 @@ class HaPanelDevBlueprints extends LitElement { } catch { await showAlertDialog(this, { title: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.load_blueprints_error_title" + "ui.panel.config.developer-tools.tabs.blueprints.editor.load_blueprints_error_title" ), text: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.load_blueprints_error_text" + "ui.panel.config.developer-tools.tabs.blueprints.editor.load_blueprints_error_text" ), }); } @@ -156,13 +153,13 @@ class HaPanelDevBlueprints extends LitElement { if (this._dirty) { const shouldContinue = await showConfirmationDialog(this, { title: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.abandon_changes_title" + "ui.panel.config.developer-tools.tabs.blueprints.editor.abandon_changes_title" ), text: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.abandon_changes_text" + "ui.panel.config.developer-tools.tabs.blueprints.editor.abandon_changes_text" ), confirmText: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.abandon_changes_confirm_text" + "ui.panel.config.developer-tools.tabs.blueprints.editor.abandon_changes_confirm_text" ), destructive: true, }); @@ -187,7 +184,7 @@ class HaPanelDevBlueprints extends LitElement { if (!this._selectedBlueprint) { await showAlertDialog(this, { title: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.error" + "ui.panel.config.developer-tools.tabs.blueprints.editor.error" ), }); return; @@ -196,10 +193,10 @@ class HaPanelDevBlueprints extends LitElement { if (this._selectedBlueprint.blueprint.source_url) { const shouldSave = await showConfirmationDialog(this, { title: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.overwrite_existing_title" + "ui.panel.config.developer-tools.tabs.blueprints.editor.overwrite_existing_title" ), text: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.overwrite_existing_text" + "ui.panel.config.developer-tools.tabs.blueprints.editor.overwrite_existing_text" ), }); if (!shouldSave) { @@ -222,10 +219,10 @@ class HaPanelDevBlueprints extends LitElement { } catch { await showAlertDialog(this, { title: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.save_error_title" + "ui.panel.config.developer-tools.tabs.blueprints.editor.save_error_title" ), text: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.save_error_text" + "ui.panel.config.developer-tools.tabs.blueprints.editor.save_error_text" ), }); } @@ -255,7 +252,7 @@ class HaPanelDevBlueprints extends LitElement { if (!this._selectedBlueprint) { return html` ${this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.none_selected" + "ui.panel.config.developer-tools.tabs.blueprints.editor.none_selected" )} `; } @@ -285,7 +282,7 @@ class HaPanelDevBlueprints extends LitElement {
    ${this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.actions.pick" + "ui.panel.config.developer-tools.tabs.blueprints.editor.actions.pick" )} ${this._yamlMode ? this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.actions.edit_ui" + "ui.panel.config.developer-tools.tabs.blueprints.editor.actions.edit_ui" ) : this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.actions.edit_yaml" + "ui.panel.config.developer-tools.tabs.blueprints.editor.actions.edit_yaml" )}
    diff --git a/src/panels/developer-tools/blueprints/double-sidebar-padding-fix.ts b/src/panels/config/developer-tools/blueprints/double-sidebar-padding-fix.ts similarity index 100% rename from src/panels/developer-tools/blueprints/double-sidebar-padding-fix.ts rename to src/panels/config/developer-tools/blueprints/double-sidebar-padding-fix.ts diff --git a/src/panels/developer-tools/blueprints/ha-blueprint-editor.ts b/src/panels/config/developer-tools/blueprints/ha-blueprint-editor.ts similarity index 89% rename from src/panels/developer-tools/blueprints/ha-blueprint-editor.ts rename to src/panels/config/developer-tools/blueprints/ha-blueprint-editor.ts index 73771e5964..fb9adf7de8 100644 --- a/src/panels/developer-tools/blueprints/ha-blueprint-editor.ts +++ b/src/panels/config/developer-tools/blueprints/ha-blueprint-editor.ts @@ -17,43 +17,43 @@ import { union, } from "superstruct"; import { load } from "js-yaml"; -import type { HomeAssistant, Route } from "../../../types"; +import type { HomeAssistant, Route } from "../../../../types"; import type { Blueprint, BlueprintDomain, BlueprintInput, BlueprintMetaDataEditorSchema, -} from "../../../data/blueprint"; +} from "../../../../data/blueprint"; import { BlueprintYamlSchema, DefaultBlueprintMetadata, normalizeBlueprint, -} from "../../../data/blueprint"; -import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin"; -import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin"; -import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; -import { documentationUrl } from "../../../util/documentation-url"; -import { haStyle } from "../../../resources/styles"; -import { fireEvent } from "../../../common/dom/fire_event"; -import { manualEditorStyles } from "../../config/automation/styles"; -import type { SidebarConfig } from "../../../data/automation"; -import { SIDEBAR_DEFAULT_WIDTH } from "../../config/automation/manual-automation-editor"; -import { storage } from "../../../common/decorators/storage"; -import type HaAutomationSidebar from "../../config/automation/ha-automation-sidebar"; -import { canOverrideAlphanumericInput } from "../../../common/dom/can-override-input"; -import { showToast } from "../../../util/toast"; -import { ensureArray } from "../../../common/array/ensure-array"; -import { showPasteReplaceDialog } from "../../config/automation/paste-replace-dialog/show-dialog-paste-replace"; -import "../../../components/ha-button"; -import "../../../components/ha-fab"; -import "../../../components/ha-list-item"; -import "../../../components/ha-yaml-editor"; -import "../../../layouts/hass-subpage"; +} from "../../../../data/blueprint"; +import { PreventUnsavedMixin } from "../../../../mixins/prevent-unsaved-mixin"; +import { KeyboardShortcutMixin } from "../../../../mixins/keyboard-shortcut-mixin"; +import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; +import { documentationUrl } from "../../../../util/documentation-url"; +import { haStyle } from "../../../../resources/styles"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { manualEditorStyles } from "../../automation/styles"; +import type { SidebarConfig } from "../../../../data/automation"; +import { SIDEBAR_DEFAULT_WIDTH } from "../../automation/manual-automation-editor"; +import { storage } from "../../../../common/decorators/storage"; +import type HaAutomationSidebar from "../../automation/ha-automation-sidebar"; +import { canOverrideAlphanumericInput } from "../../../../common/dom/can-override-input"; +import { showToast } from "../../../../util/toast"; +import { ensureArray } from "../../../../common/array/ensure-array"; +import { showPasteReplaceDialog } from "../../automation/paste-replace-dialog/show-dialog-paste-replace"; +import "../../../../components/ha-button"; +import "../../../../components/ha-fab"; +import "../../../../components/ha-list-item"; +import "../../../../components/ha-yaml-editor"; +import "../../../../layouts/hass-subpage"; import "./input/ha-blueprint-input"; import "./blueprint-metadata-editor"; import "./double-sidebar-padding-fix"; -import type { Action } from "../../../data/script"; -import "../../config/script/manual-script-editor"; +import type { Action } from "../../../../data/script"; +import "../../script/manual-script-editor"; const blueprintInputStruct = object({ name: nullable(string()), @@ -169,7 +169,7 @@ export class HaBlueprintEditor extends PreventUnsavedMixin( private async _resetBlueprint() { const shouldReset = await showConfirmationDialog(this, { text: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.reset_text" + "ui.panel.config.developer-tools.tabs.blueprints.editor.reset_text" ), destructive: true, }); @@ -310,7 +310,7 @@ export class HaBlueprintEditor extends PreventUnsavedMixin( } catch (_err: any) { showToast(this, { message: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.paste_invalid_config" + "ui.panel.config.developer-tools.tabs.blueprints.paste_invalid_config" ), duration: 4000, dismissable: true, @@ -334,7 +334,7 @@ export class HaBlueprintEditor extends PreventUnsavedMixin( } catch (_err: any) { showToast(this, { message: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.paste_invalid_config" + "ui.panel.config.developer-tools.tabs.blueprints.paste_invalid_config" ), duration: 4000, dismissable: true, @@ -473,7 +473,7 @@ export class HaBlueprintEditor extends PreventUnsavedMixin( private _showPastedToastWithUndo() { showToast(this, { message: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.paste_toast_message" + "ui.panel.config.developer-tools.tabs.blueprints.paste_toast_message" ), duration: 4000, action: { @@ -536,7 +536,7 @@ export class HaBlueprintEditor extends PreventUnsavedMixin(

    ${this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.inputs.header" + "ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.header" )}

    @@ -555,7 +555,7 @@ export class HaBlueprintEditor extends PreventUnsavedMixin( ${!Object.entries(this.blueprint?.blueprint?.input || {})?.length ? html`

    ${this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.inputs.section_description" + "ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.section_description" )}

    ` : nothing} @@ -613,7 +613,7 @@ export class HaBlueprintEditor extends PreventUnsavedMixin( .disabled=${!this.dirty} > ${this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.actions.reset" + "ui.panel.config.developer-tools.tabs.blueprints.editor.actions.reset" )}
    diff --git a/src/panels/developer-tools/blueprints/input/ha-blueprint-input-editor.ts b/src/panels/config/developer-tools/blueprints/input/ha-blueprint-input-editor.ts similarity index 77% rename from src/panels/developer-tools/blueprints/input/ha-blueprint-input-editor.ts rename to src/panels/config/developer-tools/blueprints/input/ha-blueprint-input-editor.ts index 132dd26a57..218d7af9f2 100644 --- a/src/panels/developer-tools/blueprints/input/ha-blueprint-input-editor.ts +++ b/src/panels/config/developer-tools/blueprints/input/ha-blueprint-input-editor.ts @@ -1,16 +1,16 @@ import { html, LitElement, nothing } from "lit"; import { customElement, property, query } from "lit/decorators"; -import { fireEvent } from "../../../../common/dom/fire_event"; -import { haStyle } from "../../../../resources/styles"; -import type { HomeAssistant } from "../../../../types"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { haStyle } from "../../../../../resources/styles"; +import type { HomeAssistant } from "../../../../../types"; import type { BlueprintInput, BlueprintInputSection, -} from "../../../../data/blueprint"; -import { isInputSection } from "../../../../data/blueprint"; -import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; -import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; -import "../../../../components/ha-yaml-editor"; +} from "../../../../../data/blueprint"; +import { isInputSection } from "../../../../../data/blueprint"; +import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive"; +import type { HaYamlEditor } from "../../../../../components/ha-yaml-editor"; +import "../../../../../components/ha-yaml-editor"; @customElement("ha-blueprint-input-editor") export default class HaBlueprintInputEditor extends LitElement { diff --git a/src/panels/developer-tools/blueprints/input/ha-blueprint-input-row.ts b/src/panels/config/developer-tools/blueprints/input/ha-blueprint-input-row.ts similarity index 80% rename from src/panels/developer-tools/blueprints/input/ha-blueprint-input-row.ts rename to src/panels/config/developer-tools/blueprints/input/ha-blueprint-input-row.ts index ba4b3a1d04..4eb2d394c5 100644 --- a/src/panels/developer-tools/blueprints/input/ha-blueprint-input-row.ts +++ b/src/panels/config/developer-tools/blueprints/input/ha-blueprint-input-row.ts @@ -17,37 +17,38 @@ import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { dump } from "js-yaml"; -import { storage } from "../../../../common/decorators/storage"; -import { fireEvent } from "../../../../common/dom/fire_event"; -import { stopPropagation } from "../../../../common/dom/stop_propagation"; -import "../../../../components/ha-button-menu"; -import "../../../../components/ha-card"; -import "../../../../components/ha-icon-button"; -import "../../../../components/ha-list-item"; -import "../../../../components/ha-alert"; -import "../../../../components/ha-automation-row"; +import { storage } from "../../../../../common/decorators/storage"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { stopPropagation } from "../../../../../common/dom/stop_propagation"; +import "../../../../../components/ha-card"; +import "../../../../../components/ha-icon-button"; +import "../../../../../components/ha-list-item"; +import "../../../../../components/ha-dropdown"; +import "../../../../../components/ha-dropdown-item"; +import "../../../../../components/ha-alert"; +import "../../../../../components/ha-automation-row"; import type { BlueprintInputSection, BlueprintClipboard, BlueprintInput, BlueprintInputEntry, -} from "../../../../data/blueprint"; +} from "../../../../../data/blueprint"; import { getInputAtPath, getContainingSection, isInputSection, INPUT_ICONS, -} from "../../../../data/blueprint"; +} from "../../../../../data/blueprint"; import { showConfirmationDialog, showPromptDialog, -} from "../../../../dialogs/generic/show-dialog-box"; -import { haStyle } from "../../../../resources/styles"; -import type { HomeAssistant } from "../../../../types"; +} from "../../../../../dialogs/generic/show-dialog-box"; +import { haStyle } from "../../../../../resources/styles"; +import type { HomeAssistant } from "../../../../../types"; import "./ha-blueprint-input-editor"; -import type { BlueprintInputSidebarConfig } from "../../../../data/automation"; -import { deepEqual } from "../../../../common/util/deep-equal"; -import { copyToClipboard } from "../../../../common/util/copy-clipboard"; +import type { BlueprintInputSidebarConfig } from "../../../../../data/automation"; +import { deepEqual } from "../../../../../common/util/deep-equal"; +import { copyToClipboard } from "../../../../../common/util/copy-clipboard"; @customElement("ha-blueprint-input-row") export default class HaBlueprintInputRow extends LitElement { @@ -219,7 +220,7 @@ export default class HaBlueprintInputRow extends LitElement { - - + ${this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.inputs.rename" + "ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.rename" )} - - + +
  • - + ${this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.actions.duplicate" + "ui.panel.config.developer-tools.tabs.blueprints.editor.actions.duplicate" )} - + - + ${this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.inputs.copy" + "ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.copy" )} - - + + - + ${this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.inputs.cut" + "ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.cut" )} - - + + - ${this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.move_up" + "ui.panel.config.developer-tools.tabs.blueprints.editor.move_up" )} - + - ${this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.move_down" + "ui.panel.config.developer-tools.tabs.blueprints.editor.move_down" )} - + - + ${this.hass.localize( - `ui.panel.developer-tools.tabs.blueprints.editor.edit_${!this._yamlMode ? "yaml" : "ui"}` + `ui.panel.config.developer-tools.tabs.blueprints.editor.edit_${!this._yamlMode ? "yaml" : "ui"}` )} - - + +
  • - ${this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.actions.delete" + "ui.panel.config.developer-tools.tabs.blueprints.editor.actions.delete" )} - -
    + +
    @@ -434,10 +432,10 @@ export default class HaBlueprintInputRow extends LitElement { private _onDelete(path: string[] | undefined): void { showConfirmationDialog(this, { title: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.inputs.delete_confirm_title" + "ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.delete_confirm_title" ), text: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.inputs.delete_confirm_text" + "ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.delete_confirm_text" ), dismissText: this.hass.localize("ui.common.cancel"), confirmText: this.hass.localize("ui.common.delete"), @@ -466,10 +464,10 @@ export default class HaBlueprintInputRow extends LitElement { if (!path || path.length === 0) { const id = await showPromptDialog(this, { title: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.inputs.change_id" + "ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.change_id" ), inputLabel: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.inputs.id" + "ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.id" ), inputType: "string", placeholder: this.input[0], @@ -492,10 +490,10 @@ export default class HaBlueprintInputRow extends LitElement { const id = await showPromptDialog(this, { title: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.inputs.change_id" + "ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.change_id" ), inputLabel: this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.inputs.id" + "ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.id" ), inputType: "string", placeholder: lastPathKey, diff --git a/src/panels/developer-tools/blueprints/input/ha-blueprint-input.ts b/src/panels/config/developer-tools/blueprints/input/ha-blueprint-input.ts similarity index 93% rename from src/panels/developer-tools/blueprints/input/ha-blueprint-input.ts rename to src/panels/config/developer-tools/blueprints/input/ha-blueprint-input.ts index 3804882577..518287f79c 100644 --- a/src/panels/developer-tools/blueprints/input/ha-blueprint-input.ts +++ b/src/panels/config/developer-tools/blueprints/input/ha-blueprint-input.ts @@ -8,15 +8,15 @@ import type { BlueprintInput, BlueprintInputEntry, BlueprintInputSection, -} from "../../../../data/blueprint"; -import { getContainingSection } from "../../../../data/blueprint"; -import "../../../../components/ha-sortable"; -import "../../../../components/ha-button"; -import "../../../../components/ha-svg-icon"; -import type { HomeAssistant } from "../../../../types"; -import { fireEvent } from "../../../../common/dom/fire_event"; -import { nextRender } from "../../../../common/util/render-status"; -import { storage } from "../../../../common/decorators/storage"; +} from "../../../../../data/blueprint"; +import { getContainingSection } from "../../../../../data/blueprint"; +import "../../../../../components/ha-sortable"; +import "../../../../../components/ha-button"; +import "../../../../../components/ha-svg-icon"; +import type { HomeAssistant } from "../../../../../types"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { nextRender } from "../../../../../common/util/render-status"; +import { storage } from "../../../../../common/decorators/storage"; import type HaBlueprintInputRow from "./ha-blueprint-input-row"; import type { HaBlueprintInputSection } from "./types/ha-blueprint-input-section"; import { showNewInputDialog } from "../new-input-dialog/show-dialog-new-input"; @@ -285,7 +285,7 @@ export class HaBlueprintInput extends LitElement { > ${this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.inputs.add" + "ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.add" )}
    diff --git a/src/panels/developer-tools/blueprints/input/types/ha-blueprint-input-input.ts b/src/panels/config/developer-tools/blueprints/input/types/ha-blueprint-input-input.ts similarity index 73% rename from src/panels/developer-tools/blueprints/input/types/ha-blueprint-input-input.ts rename to src/panels/config/developer-tools/blueprints/input/types/ha-blueprint-input-input.ts index 242e5f0096..9a478ab038 100644 --- a/src/panels/developer-tools/blueprints/input/types/ha-blueprint-input-input.ts +++ b/src/panels/config/developer-tools/blueprints/input/types/ha-blueprint-input-input.ts @@ -1,15 +1,15 @@ import { customElement, property } from "lit/decorators"; import { css, html, LitElement } from "lit"; import memoizeOne from "memoize-one"; -import type { HomeAssistant } from "../../../../../types"; -import type { BlueprintInput } from "../../../../../data/blueprint"; -import type { SchemaUnion } from "../../../../../components/ha-form/types"; -import type { Selector } from "../../../../../data/selector"; -import "../../../../../components/ha-textarea"; -import "../../../../../components/ha-textfield"; -import "../../../../../components/ha-select"; -import "../../../../../components/ha-form/ha-form"; -import "../../../../../components/ha-selector/ha-selector"; +import type { HomeAssistant } from "../../../../../../types"; +import type { BlueprintInput } from "../../../../../../data/blueprint"; +import type { SchemaUnion } from "../../../../../../components/ha-form/types"; +import type { Selector } from "../../../../../../data/selector"; +import "../../../../../../components/ha-textarea"; +import "../../../../../../components/ha-textfield"; +import "../../../../../../components/ha-select"; +import "../../../../../../components/ha-form/ha-form"; +import "../../../../../../components/ha-selector/ha-selector"; @customElement("ha-blueprint-input-input") export class HaBlueprintInputInput extends LitElement { @@ -54,7 +54,7 @@ export class HaBlueprintInputInput extends LitElement { schema: SchemaUnion> ): string => this.hass.localize( - `ui.panel.developer-tools.tabs.blueprints.editor.inputs.type.single.${schema.name}` + `ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.type.single.${schema.name}` ); protected render() { diff --git a/src/panels/developer-tools/blueprints/input/types/ha-blueprint-input-section.ts b/src/panels/config/developer-tools/blueprints/input/types/ha-blueprint-input-section.ts similarity index 79% rename from src/panels/developer-tools/blueprints/input/types/ha-blueprint-input-section.ts rename to src/panels/config/developer-tools/blueprints/input/types/ha-blueprint-input-section.ts index aa3622f3da..3421724c40 100644 --- a/src/panels/developer-tools/blueprints/input/types/ha-blueprint-input-section.ts +++ b/src/panels/config/developer-tools/blueprints/input/types/ha-blueprint-input-section.ts @@ -1,15 +1,15 @@ import { customElement, property } from "lit/decorators"; import { css, type CSSResultGroup, html, LitElement } from "lit"; -import type { HomeAssistant } from "../../../../../types"; -import type { BlueprintInputSection } from "../../../../../data/blueprint"; -import { fireEvent } from "../../../../../common/dom/fire_event"; -import type { SchemaUnion } from "../../../../../components/ha-form/types"; -import { haStyle } from "../../../../../resources/styles"; +import type { HomeAssistant } from "../../../../../../types"; +import type { BlueprintInputSection } from "../../../../../../data/blueprint"; +import { fireEvent } from "../../../../../../common/dom/fire_event"; +import type { SchemaUnion } from "../../../../../../components/ha-form/types"; +import { haStyle } from "../../../../../../resources/styles"; -import "../../../../../components/ha-textarea"; -import "../../../../../components/ha-textfield"; -import "../../../../../components/ha-select"; -import "../../../../../components/ha-selector/ha-selector"; +import "../../../../../../components/ha-textarea"; +import "../../../../../../components/ha-textfield"; +import "../../../../../../components/ha-select"; +import "../../../../../../components/ha-selector/ha-selector"; import "../ha-blueprint-input"; @customElement("ha-blueprint-input-section") @@ -78,7 +78,7 @@ export class HaBlueprintInputSection extends LitElement { schema: SchemaUnion ): string => this.hass.localize( - `ui.panel.developer-tools.tabs.blueprints.editor.inputs.type.section.${schema.name}` + `ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.type.section.${schema.name}` ); protected render() { diff --git a/src/panels/developer-tools/blueprints/new-input-dialog/dialog-new-input.ts b/src/panels/config/developer-tools/blueprints/new-input-dialog/dialog-new-input.ts similarity index 78% rename from src/panels/developer-tools/blueprints/new-input-dialog/dialog-new-input.ts rename to src/panels/config/developer-tools/blueprints/new-input-dialog/dialog-new-input.ts index dbee93e3af..6f85641836 100644 --- a/src/panels/developer-tools/blueprints/new-input-dialog/dialog-new-input.ts +++ b/src/panels/config/developer-tools/blueprints/new-input-dialog/dialog-new-input.ts @@ -1,16 +1,16 @@ import { customElement, property, state } from "lit/decorators"; import { css, type CSSResultGroup, html, LitElement, nothing } from "lit"; import memoizeOne from "memoize-one"; -import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; -import type { HomeAssistant } from "../../../../types"; -import { fireEvent } from "../../../../common/dom/fire_event"; -import { createCloseHeading } from "../../../../components/ha-dialog"; +import type { HassDialog } from "../../../../../dialogs/make-dialog-manager"; +import type { HomeAssistant } from "../../../../../types"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { createCloseHeading } from "../../../../../components/ha-dialog"; import type { ShowDialogNewInputParams } from "./show-dialog-new-input"; -import type { SchemaUnion } from "../../../../components/ha-form/types"; +import type { SchemaUnion } from "../../../../../components/ha-form/types"; -import "../../../../components/ha-form/ha-form"; -import "../../../../components/ha-button"; -import { haStyleDialog } from "../../../../resources/styles"; +import "../../../../../components/ha-form/ha-form"; +import "../../../../../components/ha-button"; +import { haStyleDialog } from "../../../../../resources/styles"; @customElement("ha-dialog-new-input") class DialogNewInput extends LitElement implements HassDialog { @@ -40,13 +40,13 @@ class DialogNewInput extends LitElement implements HassDialog { [ "input", this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.inputs.type.new.input" + "ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.type.new.input" ), ], [ "section", this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.editor.inputs.type.new.section" + "ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.type.new.section" ), ], ], @@ -83,7 +83,7 @@ class DialogNewInput extends LitElement implements HassDialog { schema: SchemaUnion> ) => this.hass.localize( - `ui.panel.developer-tools.tabs.blueprints.editor.inputs.type.new.${schema.name}` + `ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.type.new.${schema.name}` ); protected render() { @@ -98,7 +98,7 @@ class DialogNewInput extends LitElement implements HassDialog { .heading=${createCloseHeading( this.hass, this.hass.localize( - `ui.panel.developer-tools.tabs.blueprints.editor.inputs.add` + `ui.panel.config.developer-tools.tabs.blueprints.editor.inputs.add` ) )} > diff --git a/src/panels/developer-tools/blueprints/new-input-dialog/show-dialog-new-input.ts b/src/panels/config/developer-tools/blueprints/new-input-dialog/show-dialog-new-input.ts similarity index 86% rename from src/panels/developer-tools/blueprints/new-input-dialog/show-dialog-new-input.ts rename to src/panels/config/developer-tools/blueprints/new-input-dialog/show-dialog-new-input.ts index 283306c515..5323798d69 100644 --- a/src/panels/developer-tools/blueprints/new-input-dialog/show-dialog-new-input.ts +++ b/src/panels/config/developer-tools/blueprints/new-input-dialog/show-dialog-new-input.ts @@ -1,4 +1,4 @@ -import { fireEvent } from "../../../../common/dom/fire_event"; +import { fireEvent } from "../../../../../common/dom/fire_event"; export interface ShowDialogNewInputParams { onSubmit: (id: string, type: "input" | "section") => void; diff --git a/src/panels/developer-tools/blueprints/pick-blueprint-dialog/dialog-pick-blueprint.ts b/src/panels/config/developer-tools/blueprints/pick-blueprint-dialog/dialog-pick-blueprint.ts similarity index 77% rename from src/panels/developer-tools/blueprints/pick-blueprint-dialog/dialog-pick-blueprint.ts rename to src/panels/config/developer-tools/blueprints/pick-blueprint-dialog/dialog-pick-blueprint.ts index c793f2b1b0..1141ecbd4d 100644 --- a/src/panels/developer-tools/blueprints/pick-blueprint-dialog/dialog-pick-blueprint.ts +++ b/src/panels/config/developer-tools/blueprints/pick-blueprint-dialog/dialog-pick-blueprint.ts @@ -1,19 +1,18 @@ import { customElement, property, state } from "lit/decorators"; import { css, type CSSResultGroup, html, LitElement, nothing } from "lit"; import { mdiPencilOutline, mdiRobot, mdiScript } from "@mdi/js"; -import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; -import type { HomeAssistant } from "../../../../types"; -import { fireEvent } from "../../../../common/dom/fire_event"; -import { createCloseHeading } from "../../../../components/ha-dialog"; -import { haStyle, haStyleDialog } from "../../../../resources/styles"; -import type { Blueprint, BlueprintDomain } from "../../../../data/blueprint"; -import { isValidBlueprint } from "../../../../data/blueprint"; +import type { HassDialog } from "../../../../../dialogs/make-dialog-manager"; +import type { HomeAssistant } from "../../../../../types"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import { createCloseHeading } from "../../../../../components/ha-dialog"; +import { haStyle, haStyleDialog } from "../../../../../resources/styles"; +import type { Blueprint, BlueprintDomain } from "../../../../../data/blueprint"; +import { isValidBlueprint } from "../../../../../data/blueprint"; -import "../../../../components/ha-md-divider"; -import "../../../../components/ha-textfield"; -import "../../../../components/ha-icon-next"; -import "../../../../components/ha-md-list"; -import "../../../../components/ha-md-list-item"; +import "../../../../../components/ha-textfield"; +import "../../../../../components/ha-icon-next"; +import "../../../../../components/ha-md-list"; +import "../../../../../components/ha-md-list-item"; import type { PickBlueprintDialogParams } from "./show-dialog-pick-blueprint"; @customElement("ha-dialog-pick-blueprint") @@ -71,7 +70,7 @@ class HaDialogPickBlueprint extends LitElement implements HassDialog { innerRole="listbox" itemRoles="option" innerAriaLabel=${this.hass.localize( - `ui.panel.developer-tools.tabs.blueprints.dialog_pick.header` + `ui.panel.config.developer-tools.tabs.blueprints.dialog_pick.header` )} rootTabbable dialogInitialFocus @@ -82,14 +81,14 @@ class HaDialogPickBlueprint extends LitElement implements HassDialog { > ${this.hass.localize( - `ui.panel.developer-tools.tabs.blueprints.dialog_pick.automation` + `ui.panel.config.developer-tools.tabs.blueprints.dialog_pick.automation` )} ${this.hass.localize( - `ui.panel.developer-tools.tabs.blueprints.dialog_pick.script` + `ui.panel.config.developer-tools.tabs.blueprints.dialog_pick.script` )} @@ -108,7 +107,7 @@ class HaDialogPickBlueprint extends LitElement implements HassDialog { innerRole="listbox" itemRoles="option" innerAriaLabel=${this.hass.localize( - `ui.panel.developer-tools.tabs.blueprints.dialog_pick.header` + `ui.panel.config.developer-tools.tabs.blueprints.dialog_pick.header` )} rootTabbable dialogInitialFocus @@ -116,11 +115,11 @@ class HaDialogPickBlueprint extends LitElement implements HassDialog { ${this.hass.localize( - `ui.panel.developer-tools.tabs.blueprints.dialog_pick.create_empty_${this._pickType}` + `ui.panel.config.developer-tools.tabs.blueprints.dialog_pick.create_empty_${this._pickType}` )} ${this.hass.localize( - `ui.panel.developer-tools.tabs.blueprints.dialog_pick.create_empty_${this._pickType}_description` + `ui.panel.config.developer-tools.tabs.blueprints.dialog_pick.create_empty_${this._pickType}_description` )} @@ -150,7 +149,7 @@ class HaDialogPickBlueprint extends LitElement implements HassDialog { .heading=${createCloseHeading( this.hass, this.hass.localize( - `ui.panel.developer-tools.tabs.blueprints.dialog_pick.header` + `ui.panel.config.developer-tools.tabs.blueprints.dialog_pick.header` ) )} > diff --git a/src/panels/developer-tools/blueprints/pick-blueprint-dialog/show-dialog-pick-blueprint.ts b/src/panels/config/developer-tools/blueprints/pick-blueprint-dialog/show-dialog-pick-blueprint.ts similarity index 79% rename from src/panels/developer-tools/blueprints/pick-blueprint-dialog/show-dialog-pick-blueprint.ts rename to src/panels/config/developer-tools/blueprints/pick-blueprint-dialog/show-dialog-pick-blueprint.ts index 1715790f30..bc9333463f 100644 --- a/src/panels/developer-tools/blueprints/pick-blueprint-dialog/show-dialog-pick-blueprint.ts +++ b/src/panels/config/developer-tools/blueprints/pick-blueprint-dialog/show-dialog-pick-blueprint.ts @@ -1,5 +1,8 @@ -import { fireEvent } from "../../../../common/dom/fire_event"; -import type { BlueprintDomain, Blueprints } from "../../../../data/blueprint"; +import { fireEvent } from "../../../../../common/dom/fire_event"; +import type { + BlueprintDomain, + Blueprints, +} from "../../../../../data/blueprint"; export const loadPickBlueprintDialog = () => import("./dialog-pick-blueprint"); diff --git a/src/panels/developer-tools/debug/developer-tools-debug.ts b/src/panels/config/developer-tools/debug/developer-tools-debug.ts similarity index 60% rename from src/panels/developer-tools/debug/developer-tools-debug.ts rename to src/panels/config/developer-tools/debug/developer-tools-debug.ts index 350f69c12e..b75450cb86 100644 --- a/src/panels/developer-tools/debug/developer-tools-debug.ts +++ b/src/panels/config/developer-tools/debug/developer-tools-debug.ts @@ -1,27 +1,28 @@ import { LitElement, css, html } from "lit"; import { customElement, property, state } from "lit/decorators"; -import "../../../components/ha-card"; -import "../../../components/ha-button"; -import "../../../components/entity/ha-entity-picker"; -import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; -import { haStyle } from "../../../resources/styles"; -import type { HomeAssistant } from "../../../types"; +import "../../../../components/ha-card"; +import "../../../../components/ha-button"; +import "../../../../components/ha-md-list"; +import "../../../../components/entity/ha-entity-picker"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; +import { haStyle } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; import "./ha-debug-connection-row"; +import "./ha-debug-disable-view-transition-row"; import { getStatisticMetadata, validateStatistics, -} from "../../../data/recorder"; -import { computeDomain } from "../../../common/entity/compute_domain"; -import { copyToClipboard } from "../../../common/util/copy-clipboard"; -import { showToast } from "../../../util/toast"; -import { getExtendedEntityRegistryEntry } from "../../../data/entity/entity_registry"; +} from "../../../../data/recorder"; +import { computeDomain } from "../../../../common/entity/compute_domain"; +import { copyToClipboard } from "../../../../common/util/copy-clipboard"; +import { showToast } from "../../../../util/toast"; +import { getExtendedEntityRegistryEntry } from "../../../../data/entity/entity_registry"; +import type { ExtEntityRegistryEntry } from "../../../../data/entity/entity_registry"; @customElement("developer-tools-debug") class HaPanelDevDebug extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ type: Boolean }) public narrow = false; - @state() private _entityId?: string; protected render() { @@ -29,24 +30,28 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) {
    - + + + +
    @@ -57,7 +62,7 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) { appearance="filled" .disabled=${!this._entityId} >${this.hass.localize( - "ui.panel.developer-tools.tabs.debug.entity_diagnostic.copy_to_clipboard" + "ui.panel.config.developer-tools.tabs.debug.entity_diagnostic.copy_to_clipboard" )}
    @@ -82,8 +87,12 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) { }; } } - - const entity = await getExtendedEntityRegistryEntry(this.hass, id); + let entity: ExtEntityRegistryEntry | undefined; + try { + entity = await getExtendedEntityRegistryEntry(this.hass, id); + } catch { + // not in the registry + } const device = entity?.device_id && this.hass.devices[entity.device_id]; const data = { @@ -118,6 +127,11 @@ class HaPanelDevDebug extends SubscribeMixin(LitElement) { max-width: 600px; margin: 0 auto; } + ha-md-list { + padding-top: 0; + padding-bottom: 0; + background: none; + } `, ]; } diff --git a/src/panels/developer-tools/debug/ha-debug-connection-row.ts b/src/panels/config/developer-tools/debug/ha-debug-connection-row.ts similarity index 54% rename from src/panels/developer-tools/debug/ha-debug-connection-row.ts rename to src/panels/config/developer-tools/debug/ha-debug-connection-row.ts index c97172c9c2..029a30ff7e 100644 --- a/src/panels/developer-tools/debug/ha-debug-connection-row.ts +++ b/src/panels/config/developer-tools/debug/ha-debug-connection-row.ts @@ -1,36 +1,35 @@ import type { TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import "../../../components/ha-settings-row"; -import "../../../components/ha-switch"; -import type { HaSwitch } from "../../../components/ha-switch"; -import type { HomeAssistant } from "../../../types"; -import { storeState } from "../../../util/ha-pref-storage"; +import "../../../../components/ha-md-list-item"; +import "../../../../components/ha-switch"; +import type { HaSwitch } from "../../../../components/ha-switch"; +import type { HomeAssistant } from "../../../../types"; +import { storeState } from "../../../../util/ha-pref-storage"; @customElement("ha-debug-connection-row") class HaDebugConnectionRow extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ type: Boolean }) public narrow = false; - protected render(): TemplateResult { return html` - - - ${this.hass.localize( - "ui.panel.developer-tools.tabs.debug.debug_connection.title" - )} - - - ${this.hass.localize( - "ui.panel.developer-tools.tabs.debug.debug_connection.description" - )} - + + ${this.hass.localize( + "ui.panel.config.developer-tools.tabs.debug.debug_connection.title" + )} + ${this.hass.localize( + "ui.panel.config.developer-tools.tabs.debug.debug_connection.description" + )} - + `; } diff --git a/src/panels/config/developer-tools/debug/ha-debug-disable-view-transition-row.ts b/src/panels/config/developer-tools/debug/ha-debug-disable-view-transition-row.ts new file mode 100644 index 0000000000..0438793f24 --- /dev/null +++ b/src/panels/config/developer-tools/debug/ha-debug-disable-view-transition-row.ts @@ -0,0 +1,50 @@ +import type { TemplateResult } from "lit"; +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { storage } from "../../../../common/decorators/storage"; +import { setViewTransitionDisabled } from "../../../../common/util/view-transition"; +import "../../../../components/ha-md-list-item"; +import "../../../../components/ha-switch"; +import type { HaSwitch } from "../../../../components/ha-switch"; +import type { HomeAssistant } from "../../../../types"; + +@customElement("ha-debug-disable-view-transition-row") +class HaDebugDisableViewTransitionRow extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @storage({ key: "disableViewTransition", state: true, subscribe: false }) + private _disabled = false; + + protected render(): TemplateResult { + return html` + + ${this.hass.localize( + "ui.panel.config.developer-tools.tabs.debug.disable_view_transition.title" + )} + ${this.hass.localize( + "ui.panel.config.developer-tools.tabs.debug.disable_view_transition.description" + )} + + + `; + } + + private _checkedChanged(ev: Event) { + this._disabled = (ev.target as HaSwitch).checked; + setViewTransitionDisabled(this._disabled); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-debug-disable-view-transition-row": HaDebugDisableViewTransitionRow; + } +} diff --git a/src/panels/developer-tools/developer-tools-router.ts b/src/panels/config/developer-tools/developer-tools-router.ts similarity index 92% rename from src/panels/developer-tools/developer-tools-router.ts rename to src/panels/config/developer-tools/developer-tools-router.ts index b4fdbb1a3f..eb228563c5 100644 --- a/src/panels/developer-tools/developer-tools-router.ts +++ b/src/panels/config/developer-tools/developer-tools-router.ts @@ -1,7 +1,7 @@ import { customElement, property } from "lit/decorators"; -import type { RouterOptions } from "../../layouts/hass-router-page"; -import { HassRouterPage } from "../../layouts/hass-router-page"; -import type { HomeAssistant } from "../../types"; +import type { RouterOptions } from "../../../layouts/hass-router-page"; +import { HassRouterPage } from "../../../layouts/hass-router-page"; +import type { HomeAssistant } from "../../../types"; @customElement("developer-tools-router") class DeveloperToolsRouter extends HassRouterPage { diff --git a/src/panels/developer-tools/event/developer-tools-event.ts b/src/panels/config/developer-tools/event/developer-tools-event.ts similarity index 80% rename from src/panels/developer-tools/event/developer-tools-event.ts rename to src/panels/config/developer-tools/event/developer-tools-event.ts index a8ba412ab1..72ce29730b 100644 --- a/src/panels/developer-tools/event/developer-tools-event.ts +++ b/src/panels/config/developer-tools/event/developer-tools-event.ts @@ -1,17 +1,17 @@ import type { CSSResultGroup, TemplateResult } from "lit"; import { LitElement, css, html } from "lit"; import { customElement, property, state } from "lit/decorators"; -import "../../../components/ha-yaml-editor"; -import "../../../components/ha-textfield"; -import "../../../components/ha-button"; -import "../../../components/ha-card"; -import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; -import { documentationUrl } from "../../../util/documentation-url"; +import "../../../../components/ha-yaml-editor"; +import "../../../../components/ha-textfield"; +import "../../../../components/ha-button"; +import "../../../../components/ha-card"; +import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box"; +import { documentationUrl } from "../../../../util/documentation-url"; import "./event-subscribe-card"; import "./events-list"; -import { haStyle } from "../../../resources/styles"; -import type { HomeAssistant } from "../../../types"; -import { fireEvent } from "../../../common/dom/fire_event"; +import { haStyle } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import { fireEvent } from "../../../../common/dom/fire_event"; @customElement("developer-tools-event") class HaPanelDevEvent extends LitElement { @@ -39,7 +39,7 @@ class HaPanelDevEvent extends LitElement {

    ${this.hass.localize( - "ui.panel.developer-tools.tabs.events.description" + "ui.panel.config.developer-tools.tabs.events.description" )} ${this.hass.localize( - "ui.panel.developer-tools.tabs.events.documentation" + "ui.panel.config.developer-tools.tabs.events.documentation" )}

    ${this.hass.localize( - "ui.panel.developer-tools.tabs.events.data" + "ui.panel.config.developer-tools.tabs.events.data" )}

    @@ -85,7 +85,7 @@ class HaPanelDevEvent extends LitElement { appearance="filled" .disabled=${!this._isValid} >${this.hass.localize( - "ui.panel.developer-tools.tabs.events.fire_event" + "ui.panel.config.developer-tools.tabs.events.fire_event" )}
    @@ -100,7 +100,7 @@ class HaPanelDevEvent extends LitElement {

    ${this.hass.localize( - "ui.panel.developer-tools.tabs.events.active_listeners" + "ui.panel.config.developer-tools.tabs.events.active_listeners" )}

    ${this._error @@ -96,10 +96,10 @@ class EventSubscribeCard extends LitElement { > ${this._subscribed ? this.hass!.localize( - "ui.panel.developer-tools.tabs.events.stop_listening" + "ui.panel.config.developer-tools.tabs.events.stop_listening" ) : this.hass!.localize( - "ui.panel.developer-tools.tabs.events.start_listening" + "ui.panel.config.developer-tools.tabs.events.start_listening" )} ${this.hass!.localize( - "ui.panel.developer-tools.tabs.events.clear_events" + "ui.panel.config.developer-tools.tabs.events.clear_events" )}
    @@ -122,7 +122,7 @@ class EventSubscribeCard extends LitElement { (event) => html`
    ${this.hass!.localize( - "ui.panel.developer-tools.tabs.events.event_fired", + "ui.panel.config.developer-tools.tabs.events.event_fired", { name: event.id } )} ${formatTime( @@ -216,7 +216,7 @@ class EventSubscribeCard extends LitElement { }, this._eventType); } catch (error: any) { this._error = this.hass!.localize( - "ui.panel.developer-tools.tabs.events.subscribe_failed", + "ui.panel.config.developer-tools.tabs.events.subscribe_failed", { error: error.message || "Unknown error" } ); } diff --git a/src/panels/developer-tools/event/events-list.ts b/src/panels/config/developer-tools/event/events-list.ts similarity index 87% rename from src/panels/developer-tools/event/events-list.ts rename to src/panels/config/developer-tools/event/events-list.ts index ee784c0e75..fd440cc31f 100644 --- a/src/panels/developer-tools/event/events-list.ts +++ b/src/panels/config/developer-tools/event/events-list.ts @@ -1,9 +1,9 @@ import type { TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import { stringCompare } from "../../../common/string/compare"; -import { fireEvent } from "../../../common/dom/fire_event"; -import type { HomeAssistant } from "../../../types"; +import { stringCompare } from "../../../../common/string/compare"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import type { HomeAssistant } from "../../../../types"; interface EventListenerCount { event: string; @@ -27,7 +27,7 @@ class EventsList extends LitElement { > ${this.hass.localize( - "ui.panel.developer-tools.tabs.events.count_listeners", + "ui.panel.config.developer-tools.tabs.events.count_listeners", { count: event.listener_count, } diff --git a/src/panels/developer-tools/ha-panel-developer-tools.ts b/src/panels/config/developer-tools/ha-panel-developer-tools.ts similarity index 71% rename from src/panels/developer-tools/ha-panel-developer-tools.ts rename to src/panels/config/developer-tools/ha-panel-developer-tools.ts index f0c5e2d6be..18f708851b 100644 --- a/src/panels/developer-tools/ha-panel-developer-tools.ts +++ b/src/panels/config/developer-tools/ha-panel-developer-tools.ts @@ -1,18 +1,20 @@ -import type { ActionDetail } from "@material/mwc-list"; import { mdiDotsVertical } from "@mdi/js"; import type { CSSResultGroup, TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import { navigate } from "../../common/navigate"; -import "../../components/ha-button-menu"; -import "../../components/ha-icon-button"; -import "../../components/ha-list-item"; -import "../../components/ha-menu-button"; -import "../../components/ha-tab-group"; -import "../../components/ha-tab-group-tab"; -import { haStyle } from "../../resources/styles"; -import type { HomeAssistant, Route } from "../../types"; +import { classMap } from "lit/directives/class-map"; +import { navigate } from "../../../common/navigate"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; +import "../../../components/ha-icon-button"; +import "../../../components/ha-icon-button-arrow-prev"; +import "../../../components/ha-menu-button"; +import "../../../components/ha-tab-group"; +import "../../../components/ha-tab-group-tab"; +import { haStyle } from "../../../resources/styles"; +import type { HomeAssistant, Route } from "../../../types"; import "./developer-tools-router"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; @customElement("ha-panel-developer-tools") class PanelDeveloperTools extends LitElement { @@ -30,44 +32,54 @@ class PanelDeveloperTools extends LitElement { protected render(): TemplateResult { const page = this._page; return html` -
    +
    - + @click=${this._handleBack} + >
    - ${this.hass.localize("panel.developer_tools")} + ${this.hass.localize( + "ui.panel.config.dashboard.developer_tools.main" + )}
    - + - - ${this.hass.localize("ui.panel.developer-tools.tabs.debug.title")} - - + + ${this.hass.localize( + "ui.panel.config.developer-tools.tabs.debug.title" + )} + +
    - ${this.hass.localize("ui.panel.developer-tools.tabs.yaml.title")} + ${this.hass.localize( + "ui.panel.config.developer-tools.tabs.yaml.title" + )} - ${this.hass.localize("ui.panel.developer-tools.tabs.states.title")} + ${this.hass.localize( + "ui.panel.config.developer-tools.tabs.states.title" + )} - ${this.hass.localize("ui.panel.developer-tools.tabs.actions.title")} + ${this.hass.localize( + "ui.panel.config.developer-tools.tabs.actions.title" + )} ${this.hass.localize( - "ui.panel.developer-tools.tabs.templates.title" + "ui.panel.config.developer-tools.tabs.templates.title" )} - ${this.hass.localize("ui.panel.developer-tools.tabs.events.title")} + ${this.hass.localize( + "ui.panel.config.developer-tools.tabs.events.title" + )} ${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.title" + "ui.panel.config.developer-tools.tabs.statistics.title" )} ${this.hass.localize( - "ui.panel.developer-tools.tabs.blueprints.title" + "ui.panel.config.developer-tools.tabs.blueprints.title" )} @@ -125,17 +139,16 @@ class PanelDeveloperTools extends LitElement { return; } if (newPage !== this._page) { - navigate(`/developer-tools/${newPage}`); + navigate(`/config/developer-tools/${newPage}`); } else { scrollTo({ behavior: "smooth", top: 0 }); } } - private async _handleMenuAction(ev: CustomEvent) { - switch (ev.detail.index) { - case 0: - navigate(`/developer-tools/debug`); - break; + private async _handleMenuAction(ev: HaDropdownSelectEvent) { + const action = ev.detail.item.value; + if (action === "debug") { + navigate(`/config/developer-tools/debug`); } } @@ -143,6 +156,10 @@ class PanelDeveloperTools extends LitElement { return this.route.path.substr(1); } + private _handleBack() { + navigate("/config"); + } + static get styles(): CSSResultGroup { return [ haStyle, @@ -166,7 +183,6 @@ class PanelDeveloperTools extends LitElement { padding-top: var(--safe-area-inset-top); padding-right: var(--safe-area-inset-right); color: var(--app-header-text-color, white); - border-bottom: var(--app-header-border-bottom, none); -webkit-backdrop-filter: var(--app-header-backdrop-filter, none); backdrop-filter: var(--app-header-backdrop-filter, none); } @@ -193,10 +209,13 @@ class PanelDeveloperTools extends LitElement { padding: var(--ha-space-1); } .main-title { - margin: var(--margin-title); + margin-inline-start: var(--ha-space-6); line-height: var(--ha-line-height-normal); flex-grow: 1; } + .narrow .main-title { + margin-inline-start: var(--ha-space-2); + } developer-tools-router { display: block; padding-top: calc( @@ -220,6 +239,7 @@ class PanelDeveloperTools extends LitElement { --ha-tab-active-text-color: var(--app-header-text-color, white); --ha-tab-indicator-color: var(--app-header-text-color, white); --ha-tab-track-color: transparent; + border-bottom: var(--app-header-border-bottom, none); } `, ]; diff --git a/src/panels/developer-tools/state/developer-tools-state-renderer.ts b/src/panels/config/developer-tools/state/developer-tools-state-renderer.ts similarity index 89% rename from src/panels/developer-tools/state/developer-tools-state-renderer.ts rename to src/panels/config/developer-tools/state/developer-tools-state-renderer.ts index a4da8077c6..15147965b7 100644 --- a/src/panels/developer-tools/state/developer-tools-state-renderer.ts +++ b/src/panels/config/developer-tools/state/developer-tools-state-renderer.ts @@ -9,14 +9,14 @@ import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize"; import { css, html, LitElement, nothing } from "lit"; import { classMap } from "lit/directives/class-map"; import { customElement, property } from "lit/decorators"; -import { fireEvent } from "../../../common/dom/fire_event"; -import { loadVirtualizer } from "../../../resources/virtualizer"; -import { copyToClipboard } from "../../../common/util/copy-clipboard"; -import "../../../components/ha-checkbox"; -import "../../../components/ha-svg-icon"; -import type { HomeAssistant } from "../../../types"; -import { showToast } from "../../../util/toast"; -import { haStyle } from "../../../resources/styles"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { loadVirtualizer } from "../../../../resources/virtualizer"; +import { copyToClipboard } from "../../../../common/util/copy-clipboard"; +import "../../../../components/ha-checkbox"; +import "../../../../components/ha-svg-icon"; +import type { HomeAssistant } from "../../../../types"; +import { showToast } from "../../../../util/toast"; +import { haStyle } from "../../../../resources/styles"; @customElement("developer-tools-state-renderer") class HaPanelDevStateRenderer extends LitElement { @@ -30,7 +30,7 @@ class HaPanelDevStateRenderer extends LitElement { @property({ type: Boolean, attribute: "virtualize", reflect: true }) public virtualize = true; - @property({ type: Boolean, attribute: false }) + @property({ attribute: false }) public showAttributes = true; protected willUpdate(changedProps: PropertyValues) { @@ -69,21 +69,21 @@ class HaPanelDevStateRenderer extends LitElement {
    ${this.hass.localize( - "ui.panel.developer-tools.tabs.states.entity" + "ui.panel.config.developer-tools.tabs.states.entity" )}
    ${this.hass.localize( - "ui.panel.developer-tools.tabs.states.state" + "ui.panel.config.developer-tools.tabs.states.state" )}
    ${this.hass.localize( - "ui.panel.developer-tools.tabs.states.attributes" + "ui.panel.config.developer-tools.tabs.states.attributes" )}
    @@ -105,7 +105,7 @@ class HaPanelDevStateRenderer extends LitElement {
    ${this.hass.localize( - "ui.panel.developer-tools.tabs.states.no_entities" + "ui.panel.config.developer-tools.tabs.states.no_entities" )}
    @@ -152,10 +152,10 @@ class HaPanelDevStateRenderer extends LitElement { @click=${this._copyEntity} .entity=${item} alt=${this.hass.localize( - "ui.panel.developer-tools.tabs.states.copy_id" + "ui.panel.config.developer-tools.tabs.states.copy_id" )} title=${this.hass.localize( - "ui.panel.developer-tools.tabs.states.copy_id" + "ui.panel.config.developer-tools.tabs.states.copy_id" )} .path=${mdiClipboardTextMultipleOutline} > @@ -168,10 +168,10 @@ class HaPanelDevStateRenderer extends LitElement { @click=${this._entityMoreInfo} .entity=${item} alt=${this.hass.localize( - "ui.panel.developer-tools.tabs.states.more_info" + "ui.panel.config.developer-tools.tabs.states.more_info" )} title=${this.hass.localize( - "ui.panel.developer-tools.tabs.states.more_info" + "ui.panel.config.developer-tools.tabs.states.more_info" )} .path=${mdiInformationOutline} > diff --git a/src/panels/developer-tools/state/developer-tools-state.ts b/src/panels/config/developer-tools/state/developer-tools-state.ts similarity index 87% rename from src/panels/developer-tools/state/developer-tools-state.ts rename to src/panels/config/developer-tools/state/developer-tools-state.ts index 718c26c3c4..2b6a1ef97e 100644 --- a/src/panels/developer-tools/state/developer-tools-state.ts +++ b/src/panels/config/developer-tools/state/developer-tools-state.ts @@ -9,26 +9,26 @@ import type { CSSResultGroup } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time"; -import { storage } from "../../../common/decorators/storage"; -import { escapeRegExp } from "../../../common/string/escape_regexp"; -import { copyToClipboard } from "../../../common/util/copy-clipboard"; -import "../../../components/entity/ha-entity-picker"; -import "../../../components/ha-alert"; -import "../../../components/ha-button"; -import "../../../components/ha-checkbox"; -import "../../../components/ha-expansion-panel"; -import "../../../components/ha-icon-button"; -import "../../../components/ha-input-helper-text"; -import "../../../components/ha-svg-icon"; -import "../../../components/ha-tip"; -import "../../../components/ha-yaml-editor"; -import type { HaYamlEditor } from "../../../components/ha-yaml-editor"; -import "../../../components/search-input"; -import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; -import { haStyle } from "../../../resources/styles"; -import type { HomeAssistant } from "../../../types"; -import { showToast } from "../../../util/toast"; +import { formatDateTimeWithSeconds } from "../../../../common/datetime/format_date_time"; +import { storage } from "../../../../common/decorators/storage"; +import { escapeRegExp } from "../../../../common/string/escape_regexp"; +import { copyToClipboard } from "../../../../common/util/copy-clipboard"; +import "../../../../components/entity/ha-entity-picker"; +import "../../../../components/ha-alert"; +import "../../../../components/ha-button"; +import "../../../../components/ha-checkbox"; +import "../../../../components/ha-expansion-panel"; +import "../../../../components/ha-icon-button"; +import "../../../../components/ha-input-helper-text"; +import "../../../../components/ha-svg-icon"; +import "../../../../components/ha-tip"; +import "../../../../components/ha-yaml-editor"; +import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; +import "../../../../components/search-input"; +import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box"; +import { haStyle } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import { showToast } from "../../../../util/toast"; import "./developer-tools-state-renderer"; // Use virtualizer after threshold to avoid performance issues @@ -101,13 +101,13 @@ class HaPanelDevState extends LitElement {

    ${this.hass.localize( - "ui.panel.developer-tools.tabs.states.current_entities" + "ui.panel.config.developer-tools.tabs.states.current_entities" )}

    ${!this.narrow ? html`

    ${this.hass.localize( - "ui.panel.developer-tools.tabs.states.description1" + "ui.panel.config.developer-tools.tabs.states.description1" )}
    ${this.hass.localize( - "ui.panel.developer-tools.tabs.states.description2" + "ui.panel.config.developer-tools.tabs.states.description2" )}

    ${this._error @@ -154,7 +154,7 @@ class HaPanelDevState extends LitElement { .path=${mdiContentCopy} @click=${this._copyStateEntity} title=${this.hass.localize( - "ui.panel.developer-tools.tabs.states.copy_id" + "ui.panel.config.developer-tools.tabs.states.copy_id" )} >
    @@ -162,7 +162,7 @@ class HaPanelDevState extends LitElement { : nothing}

    ${this.hass.localize( - "ui.panel.developer-tools.tabs.states.state_attributes" + "ui.panel.config.developer-tools.tabs.states.state_attributes" )}

    ${this.hass.localize( - "ui.panel.developer-tools.tabs.states.set_state" + "ui.panel.config.developer-tools.tabs.states.set_state" )} ${this.hass.localize( - "ui.panel.developer-tools.tabs.states.last_changed" + "ui.panel.config.developer-tools.tabs.states.last_changed" )}:
    ${this.hass.localize( - "ui.panel.developer-tools.tabs.states.last_updated" + "ui.panel.config.developer-tools.tabs.states.last_updated" )}:
    localize( - `ui.panel.developer-tools.tabs.statistics.issues.${issue.type}`, + `ui.panel.config.developer-tools.tabs.statistics.issues.${issue.type}`, issue.data ) || issue.type ) @@ -152,7 +139,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) { ): DataTableColumnContainer => ({ displayName: { title: localize( - "ui.panel.developer-tools.tabs.statistics.data_table.name" + "ui.panel.config.developer-tools.tabs.statistics.data_table.name" ), main: true, sortable: true, @@ -161,7 +148,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) { }, statistic_id: { title: localize( - "ui.panel.developer-tools.tabs.statistics.data_table.statistic_id" + "ui.panel.config.developer-tools.tabs.statistics.data_table.statistic_id" ), sortable: true, filterable: true, @@ -169,7 +156,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) { }, statistics_unit_of_measurement: { title: localize( - "ui.panel.developer-tools.tabs.statistics.data_table.statistics_unit" + "ui.panel.config.developer-tools.tabs.statistics.data_table.statistics_unit" ), sortable: true, filterable: true, @@ -177,7 +164,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) { }, source: { title: localize( - "ui.panel.developer-tools.tabs.statistics.data_table.source" + "ui.panel.config.developer-tools.tabs.statistics.data_table.source" ), sortable: true, filterable: true, @@ -185,7 +172,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) { }, issues_string: { title: localize( - "ui.panel.developer-tools.tabs.statistics.data_table.issue" + "ui.panel.config.developer-tools.tabs.statistics.data_table.issue" ), sortable: true, filterable: true, @@ -194,12 +181,14 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) { flex: 2, template: (statistic) => html`${statistic.issues_string ?? - localize("ui.panel.developer-tools.tabs.statistics.no_issue")}`, + localize( + "ui.panel.config.developer-tools.tabs.statistics.no_issue" + )}`, }, fix: { title: "", label: this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.fix" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.fix" ), type: "icon", template: (statistic) => @@ -214,8 +203,8 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) { statistic.issues.some((issue) => FIXABLE_ISSUES.includes(issue.type) ) - ? "ui.panel.developer-tools.tabs.statistics.fix_issue.fix" - : "ui.panel.developer-tools.tabs.statistics.fix_issue.info" + ? "ui.panel.config.developer-tools.tabs.statistics.fix_issue.fix" + : "ui.panel.config.developer-tools.tabs.statistics.fix_issue.info" )} ` : "—"}`, @@ -225,7 +214,9 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) { }, actions: { title: "", - label: localize("ui.panel.developer-tools.tabs.statistics.adjust_sum"), + label: localize( + "ui.panel.config.developer-tools.tabs.statistics.adjust_sum" + ), type: "icon-button", showNarrow: true, template: (statistic) => @@ -233,7 +224,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) { ? html` col.sortable) ? html` - - - + + + + + ${Object.entries(columns).map(([id, column]) => + column.sortable + ? html` + + ${this._sortColumn === id + ? html` + + ` + : nothing} + ${column.title || column.label} + + ` + : nothing + )} + ` : nothing; const groupByMenu = Object.values(columns).find((col) => col.groupable) ? html` - - + + + + ${Object.entries(columns).map(([id, column]) => + column.groupable + ? html` + + ${column.title || column.label} + + ` + : nothing + )} + + ${localize("ui.components.subpage-data-table.dont_group_by")} + + + + + ${localize( + "ui.components.subpage-data-table.collapse_all_groups" + )} + + + + ${localize("ui.components.subpage-data-table.expand_all_groups")} + + ` : nothing; @@ -325,7 +385,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) { "ui.components.subpage-data-table.exit_selection_mode" )} > - + - -
    - ${localize("ui.components.subpage-data-table.select_all")} -
    -
    - -
    - ${localize( - "ui.panel.developer-tools.tabs.statistics.data_table.select_all_issues" - )} -
    -
    - -
    - ${localize( - "ui.components.subpage-data-table.select_none" - )} -
    -
    - - -
    - ${localize( - "ui.components.subpage-data-table.exit_selection_mode" - )} -
    -
    -
    + + ${localize("ui.components.subpage-data-table.select_all")} + + + ${localize( + "ui.panel.config.developer-tools.tabs.statistics.data_table.select_all_issues" + )} + + + ${localize("ui.components.subpage-data-table.select_none")} + + + + ${localize( + "ui.components.subpage-data-table.exit_selection_mode" + )} + +

    ${localize("ui.components.subpage-data-table.selected", { selected: this._selected.length, @@ -392,7 +430,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {

    ${!this.narrow ? html` @@ -449,82 +488,6 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
    `}
    - - ${Object.entries(columns).map(([id, column]) => - column.groupable - ? html` - - ${column.title || column.label} - - ` - : nothing - )} - - ${localize("ui.components.subpage-data-table.dont_group_by")} - - - - - ${localize("ui.components.subpage-data-table.collapse_all_groups")} - - - - ${localize("ui.components.subpage-data-table.expand_all_groups")} - - - - ${Object.entries(columns).map(([id, column]) => - column.sortable - ? html` - - ${this._sortColumn === id - ? html` - - ` - : nothing} - ${column.title || column.label} - - ` - : nothing - )} - `; } @@ -541,8 +504,17 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) { this._selected = ev.detail.value; } - private _handleSortBy(ev) { - const columnId = ev.currentTarget.value; + private _handleTableSortingChanged( + ev: CustomEvent<{ column: string; direction: SortingDirection }> + ) { + const { column, direction } = ev.detail; + this._sortColumn = column; + this._sortDirection = direction; + } + + private _handleSortBy(ev: HaDropdownSelectEvent) { + ev.preventDefault(); // keep dropdown open + const columnId = ev.detail.item.value; if (!this._sortDirection || this._sortColumn !== columnId) { this._sortDirection = "asc"; } else if (this._sortDirection === "asc") { @@ -553,11 +525,29 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) { this._sortColumn = columnId; } - private _handleGroupBy(ev) { - this._setGroupColumn(ev.currentTarget.value); - } + private _handleOverflowGroupBy = (ev: HaDropdownSelectEvent) => { + const action = ev.detail.item.value; - private _setGroupColumn(columnId: string) { + if (!action) { + return; + } + + switch (action) { + case "collapse_all": + this._collapseAllGroups(); + return; + case "expand_all": + this._expandAllGroups(); + return; + case "none": + this._setGroupColumn(); + return; + default: + this._setGroupColumn(action); + } + }; + + private _setGroupColumn(columnId?: string) { this._groupColumn = columnId; } @@ -669,10 +659,10 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) { await showConfirmationDialog(this, { title: this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.multi_delete.title" + "ui.panel.config.developer-tools.tabs.statistics.multi_delete.title" ), text: html`${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.multi_delete.info_text", + "ui.panel.config.developer-tools.tabs.statistics.multi_delete.info_text", { statistic_count: deletableIds.length } )}`, confirmText: this.hass.localize("ui.common.delete"), @@ -812,9 +802,7 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) { --dialog-content-padding: 0; } - #sort-by-anchor, - #group-by-anchor, - ha-md-button-menu ha-assist-chip { + ha-dropdown ha-assist-chip { --md-assist-chip-trailing-space: 8px; } `, diff --git a/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts b/src/panels/config/developer-tools/statistics/dialog-statistics-adjust-sum.ts similarity index 85% rename from src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts rename to src/panels/config/developer-tools/statistics/dialog-statistics-adjust-sum.ts index 85470ea22a..bbc65b7402 100644 --- a/src/panels/developer-tools/statistics/dialog-statistics-adjust-sum.ts +++ b/src/panels/config/developer-tools/statistics/dialog-statistics-adjust-sum.ts @@ -3,28 +3,31 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { formatDateTime } from "../../../common/datetime/format_date_time"; -import { fireEvent } from "../../../common/dom/fire_event"; -import "../../../components/ha-spinner"; -import "../../../components/ha-dialog"; -import "../../../components/ha-button"; -import "../../../components/ha-form/ha-form"; -import "../../../components/ha-icon-next"; -import "../../../components/ha-list-item"; -import "../../../components/ha-selector/ha-selector-datetime"; -import "../../../components/ha-selector/ha-selector-number"; -import "../../../components/ha-svg-icon"; -import type { StatisticValue } from "../../../data/recorder"; +import { formatDateTime } from "../../../../common/datetime/format_date_time"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-spinner"; +import "../../../../components/ha-dialog"; +import "../../../../components/ha-button"; +import "../../../../components/ha-form/ha-form"; +import "../../../../components/ha-icon-next"; +import "../../../../components/ha-list-item"; +import "../../../../components/ha-selector/ha-selector-datetime"; +import "../../../../components/ha-selector/ha-selector-number"; +import "../../../../components/ha-svg-icon"; +import type { StatisticValue } from "../../../../data/recorder"; import { adjustStatisticsSum, fetchStatistics, getDisplayUnit, -} from "../../../data/recorder"; -import type { DateTimeSelector, NumberSelector } from "../../../data/selector"; -import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; -import { haStyle, haStyleDialog } from "../../../resources/styles"; -import type { HomeAssistant } from "../../../types"; -import { showToast } from "../../../util/toast"; +} from "../../../../data/recorder"; +import type { + DateTimeSelector, + NumberSelector, +} from "../../../../data/selector"; +import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box"; +import { haStyle, haStyleDialog } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import { showToast } from "../../../../util/toast"; import type { DialogStatisticsAdjustSumParams } from "./show-dialog-statistics-adjust-sum"; interface CombinedStat { @@ -108,7 +111,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { escapeKeyAction @closed=${this.closeDialog} .heading=${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.adjust_sum.title" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.title" )} > ${content} @@ -133,7 +136,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { } else if (this._statsHour.length < 1 && this._stats5min.length < 1) { stats = html`

    ${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.adjust_sum.no_statistics_found" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.no_statistics_found" )}

    `; } else { @@ -172,20 +175,20 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { return html`
    ${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.adjust_sum.info_text_1" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.info_text_1" )}
    ${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.adjust_sum.statistic" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.statistic" )} ${this._params!.statistic.statistic_id}
    ${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.adjust_sum.outliers" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.outliers" )} @@ -236,7 +239,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
    ${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.adjust_sum.statistic" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.statistic" )} ${this._params!.statistic.statistic_id} @@ -245,7 +248,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement {
    ${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.adjust_sum.start" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.start" )} ${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.adjust_sum.end" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.end" )} ${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.adjust_sum.adjust" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.adjust" )} `; @@ -470,7 +473,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { this._busy = false; showAlertDialog(this, { text: this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.adjust_sum.error_sum_adjusted", + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.error_sum_adjusted", { message: err.message || err } ), }); @@ -478,7 +481,7 @@ export class DialogStatisticsFixUnsupportedUnitMetadata extends LitElement { } showToast(this, { message: this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.adjust_sum.sum_adjusted" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.adjust_sum.sum_adjusted" ), }); this.closeDialog(); diff --git a/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts b/src/panels/config/developer-tools/statistics/dialog-statistics-fix-units-changed.ts similarity index 76% rename from src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts rename to src/panels/config/developer-tools/statistics/dialog-statistics-fix-units-changed.ts index d39d766c69..0979aa803c 100644 --- a/src/panels/developer-tools/statistics/dialog-statistics-fix-units-changed.ts +++ b/src/panels/config/developer-tools/statistics/dialog-statistics-fix-units-changed.ts @@ -1,18 +1,18 @@ import type { CSSResultGroup } from "lit"; import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { fireEvent } from "../../../common/dom/fire_event"; -import "../../../components/ha-dialog"; -import "../../../components/ha-button"; -import "../../../components/ha-formfield"; -import "../../../components/ha-radio"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-dialog"; +import "../../../../components/ha-button"; +import "../../../../components/ha-formfield"; +import "../../../../components/ha-radio"; import { clearStatistics, getStatisticLabel, updateStatisticsMetadata, -} from "../../../data/recorder"; -import { haStyle, haStyleDialog } from "../../../resources/styles"; -import type { HomeAssistant } from "../../../types"; +} from "../../../../data/recorder"; +import { haStyle, haStyleDialog } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; import type { DialogStatisticsUnitsChangedParams } from "./show-dialog-statistics-fix-units-changed"; @customElement("dialog-statistics-fix-units-changed") @@ -50,12 +50,12 @@ export class DialogStatisticsFixUnitsChanged extends LitElement { escapeKeyAction @closed=${this._closeDialog} .heading=${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.units_changed.title" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.title" )} >

    ${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.units_changed.info_text_1", + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.info_text_1", { name: getStatisticLabel( this.hass, @@ -68,21 +68,21 @@ export class DialogStatisticsFixUnitsChanged extends LitElement { } )}
    ${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.units_changed.info_text_2" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.info_text_2" )}
    ${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.units_changed.info_text_3" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.info_text_3" )}

    ${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.units_changed.how_to_fix" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.units_changed.how_to_fix" )}

    @@ -96,7 +96,7 @@ export class DialogStatisticsFixUnitsChanged extends LitElement { ${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.fix" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.fix" )} diff --git a/src/panels/developer-tools/statistics/dialog-statistics-fix.ts b/src/panels/config/developer-tools/statistics/dialog-statistics-fix.ts similarity index 70% rename from src/panels/developer-tools/statistics/dialog-statistics-fix.ts rename to src/panels/config/developer-tools/statistics/dialog-statistics-fix.ts index 27e2c960f6..d20ca1f474 100644 --- a/src/panels/developer-tools/statistics/dialog-statistics-fix.ts +++ b/src/panels/config/developer-tools/statistics/dialog-statistics-fix.ts @@ -1,15 +1,15 @@ import type { CSSResultGroup } from "lit"; import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { fireEvent } from "../../../common/dom/fire_event"; -import "../../../components/ha-button"; -import "../../../components/ha-dialog"; -import "../../../components/ha-spinner"; -import { clearStatistics, getStatisticLabel } from "../../../data/recorder"; -import { haStyle, haStyleDialog } from "../../../resources/styles"; -import type { HomeAssistant } from "../../../types"; -import { documentationUrl } from "../../../util/documentation-url"; -import { showAlertDialog } from "../../lovelace/custom-card-helpers"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/ha-button"; +import "../../../../components/ha-dialog"; +import "../../../../components/ha-spinner"; +import { clearStatistics, getStatisticLabel } from "../../../../data/recorder"; +import { haStyle, haStyleDialog } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import { documentationUrl } from "../../../../util/documentation-url"; +import { showAlertDialog } from "../../../lovelace/custom-card-helpers"; import type { DialogStatisticsFixParams } from "./show-dialog-statistics-fix"; @customElement("dialog-statistics-fix") @@ -48,12 +48,12 @@ export class DialogStatisticsFix extends LitElement { escapeKeyAction @closed=${this._closeDialog} .heading=${this.hass.localize( - `ui.panel.developer-tools.tabs.statistics.fix_issue.${issue.type}.title` + `ui.panel.config.developer-tools.tabs.statistics.fix_issue.${issue.type}.title` )} >

    ${this.hass.localize( - `ui.panel.developer-tools.tabs.statistics.fix_issue.${issue.type}.info_text_1`, + `ui.panel.config.developer-tools.tabs.statistics.fix_issue.${issue.type}.info_text_1`, { name: getStatisticLabel( this.hass, @@ -64,23 +64,23 @@ export class DialogStatisticsFix extends LitElement { ...(issue.type === "mean_type_changed" ? { metadata_mean_type: this.hass.localize( - `ui.panel.developer-tools.tabs.statistics.mean_type.${issue.data.metadata_mean_type}` + `ui.panel.config.developer-tools.tabs.statistics.mean_type.${issue.data.metadata_mean_type}` ), state_mean_type: this.hass.localize( - `ui.panel.developer-tools.tabs.statistics.mean_type.${issue.data.state_mean_type}` + `ui.panel.config.developer-tools.tabs.statistics.mean_type.${issue.data.state_mean_type}` ), } : {}), } )}

    ${this.hass.localize( - `ui.panel.developer-tools.tabs.statistics.fix_issue.${issue.type}.info_text_2`, + `ui.panel.config.developer-tools.tabs.statistics.fix_issue.${issue.type}.info_text_2`, { statistic_id: issue.data.statistic_id } )} ${issue.type === "mean_type_changed" ? html`

    ${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.mean_type_changed.info_text_3", + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.mean_type_changed.info_text_3", { statistic_id: issue.data.statistic_id } )}` : issue.type === "entity_not_recorded" @@ -94,7 +94,7 @@ export class DialogStatisticsFix extends LitElement { rel="noreferrer noopener" > ${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_3_link" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.entity_not_recorded.info_text_3_link" )}
    ` : issue.type === "entity_no_longer_recorded" @@ -107,22 +107,22 @@ export class DialogStatisticsFix extends LitElement { rel="noreferrer noopener" > ${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_3_link" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_3_link" )}

    ${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_4" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_4" )}` : issue.type === "state_class_removed" ? html`

    ${this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_6", + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.state_class_removed.info_text_6", { statistic_id: issue.data.statistic_id } )}` : nothing} @@ -185,15 +185,15 @@ export class DialogStatisticsFix extends LitElement { title: err.code === "timeout" ? this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.clearing_timeout_title" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.clearing_timeout_title" ) : this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.clearing_failed" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.clearing_failed" ), text: err.code === "timeout" ? this.hass.localize( - "ui.panel.developer-tools.tabs.statistics.fix_issue.clearing_timeout_text" + "ui.panel.config.developer-tools.tabs.statistics.fix_issue.clearing_timeout_text" ) : err.message, }); diff --git a/src/panels/developer-tools/statistics/fix-statistics.ts b/src/panels/config/developer-tools/statistics/fix-statistics.ts similarity index 85% rename from src/panels/developer-tools/statistics/fix-statistics.ts rename to src/panels/config/developer-tools/statistics/fix-statistics.ts index 49f192cb9c..335522c5c6 100644 --- a/src/panels/developer-tools/statistics/fix-statistics.ts +++ b/src/panels/config/developer-tools/statistics/fix-statistics.ts @@ -1,4 +1,4 @@ -import type { StatisticsValidationResult } from "../../../data/recorder"; +import type { StatisticsValidationResult } from "../../../../data/recorder"; import { showFixStatisticsDialog } from "./show-dialog-statistics-fix"; import { showFixStatisticsUnitsChangedDialog } from "./show-dialog-statistics-fix-units-changed"; diff --git a/src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts b/src/panels/config/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts similarity index 78% rename from src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts rename to src/panels/config/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts index beb87aa2c7..a58a5d9af8 100644 --- a/src/panels/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts +++ b/src/panels/config/developer-tools/statistics/show-dialog-statistics-adjust-sum.ts @@ -1,5 +1,5 @@ -import { fireEvent } from "../../../common/dom/fire_event"; -import type { StatisticsMetaData } from "../../../data/recorder"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import type { StatisticsMetaData } from "../../../../data/recorder"; export const loadAdjustSumDialog = () => import("./dialog-statistics-adjust-sum"); diff --git a/src/panels/developer-tools/statistics/show-dialog-statistics-fix-units-changed.ts b/src/panels/config/developer-tools/statistics/show-dialog-statistics-fix-units-changed.ts similarity index 91% rename from src/panels/developer-tools/statistics/show-dialog-statistics-fix-units-changed.ts rename to src/panels/config/developer-tools/statistics/show-dialog-statistics-fix-units-changed.ts index 7cfcad649c..4c915383e9 100644 --- a/src/panels/developer-tools/statistics/show-dialog-statistics-fix-units-changed.ts +++ b/src/panels/config/developer-tools/statistics/show-dialog-statistics-fix-units-changed.ts @@ -1,5 +1,5 @@ -import { fireEvent } from "../../../common/dom/fire_event"; -import type { StatisticsValidationResultUnitsChanged } from "../../../data/recorder"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import type { StatisticsValidationResultUnitsChanged } from "../../../../data/recorder"; export const loadFixUnitsDialog = () => import("./dialog-statistics-fix-units-changed"); diff --git a/src/panels/developer-tools/statistics/show-dialog-statistics-fix.ts b/src/panels/config/developer-tools/statistics/show-dialog-statistics-fix.ts similarity index 84% rename from src/panels/developer-tools/statistics/show-dialog-statistics-fix.ts rename to src/panels/config/developer-tools/statistics/show-dialog-statistics-fix.ts index bfa7c5e5d7..de5ce6bea2 100644 --- a/src/panels/developer-tools/statistics/show-dialog-statistics-fix.ts +++ b/src/panels/config/developer-tools/statistics/show-dialog-statistics-fix.ts @@ -1,5 +1,5 @@ -import { fireEvent } from "../../../common/dom/fire_event"; -import type { StatisticsValidationResult } from "../../../data/recorder"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import type { StatisticsValidationResult } from "../../../../data/recorder"; export const loadFixDialog = () => import("./dialog-statistics-fix"); diff --git a/src/panels/developer-tools/template/developer-tools-template.ts b/src/panels/config/developer-tools/template/developer-tools-template.ts similarity index 87% rename from src/panels/developer-tools/template/developer-tools-template.ts rename to src/panels/config/developer-tools/template/developer-tools-template.ts index 879e63e53b..7374654146 100644 --- a/src/panels/developer-tools/template/developer-tools-template.ts +++ b/src/panels/config/developer-tools/template/developer-tools-template.ts @@ -3,18 +3,18 @@ import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; -import { debounce } from "../../../common/util/debounce"; -import "../../../components/ha-alert"; -import "../../../components/ha-button"; -import "../../../components/ha-card"; -import "../../../components/ha-code-editor"; -import "../../../components/ha-spinner"; -import type { RenderTemplateResult } from "../../../data/ws-templates"; -import { subscribeRenderTemplate } from "../../../data/ws-templates"; -import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; -import { haStyle } from "../../../resources/styles"; -import type { HomeAssistant } from "../../../types"; -import { documentationUrl } from "../../../util/documentation-url"; +import { debounce } from "../../../../common/util/debounce"; +import "../../../../components/ha-alert"; +import "../../../../components/ha-button"; +import "../../../../components/ha-card"; +import "../../../../components/ha-code-editor"; +import "../../../../components/ha-spinner"; +import type { RenderTemplateResult } from "../../../../data/ws-templates"; +import { subscribeRenderTemplate } from "../../../../data/ws-templates"; +import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; +import { haStyle } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import { documentationUrl } from "../../../../util/documentation-url"; const DEMO_TEMPLATE = `{## Imitate available variables: ##} {% set my_test_json = { @@ -92,7 +92,7 @@ class HaPanelDevTemplate extends LitElement {

    ${this.hass.localize( - "ui.panel.developer-tools.tabs.templates.description" + "ui.panel.config.developer-tools.tabs.templates.description" )}

      @@ -102,7 +102,7 @@ class HaPanelDevTemplate extends LitElement { target="_blank" rel="noreferrer" >${this.hass.localize( - "ui.panel.developer-tools.tabs.templates.jinja_documentation" + "ui.panel.config.developer-tools.tabs.templates.jinja_documentation" )} @@ -116,7 +116,7 @@ class HaPanelDevTemplate extends LitElement { rel="noreferrer" > ${this.hass.localize( - "ui.panel.developer-tools.tabs.templates.template_extensions" + "ui.panel.config.developer-tools.tabs.templates.template_extensions" )} @@ -132,7 +132,7 @@ class HaPanelDevTemplate extends LitElement {
      @@ -151,7 +151,7 @@ class HaPanelDevTemplate extends LitElement {
      ${this.hass.localize( - "ui.panel.developer-tools.tabs.templates.reset" + "ui.panel.config.developer-tools.tabs.templates.reset" )} @@ -163,7 +163,7 @@ class HaPanelDevTemplate extends LitElement {
      @@ -191,7 +191,7 @@ ${type === "object" >

      ${this.hass.localize( - "ui.panel.developer-tools.tabs.templates.result_type" + "ui.panel.config.developer-tools.tabs.templates.result_type" )}: ${resultType}

      @@ -199,7 +199,7 @@ ${type === "object" ? html`

      ${this.hass.localize( - "ui.panel.developer-tools.tabs.templates.time" + "ui.panel.config.developer-tools.tabs.templates.time" )}

      ` @@ -210,7 +210,7 @@ ${type === "object" ? html`

      ${this.hass.localize( - "ui.panel.developer-tools.tabs.templates.all_listeners" + "ui.panel.config.developer-tools.tabs.templates.all_listeners" )}

      ` @@ -219,7 +219,7 @@ ${type === "object" ? html`

      ${this.hass.localize( - "ui.panel.developer-tools.tabs.templates.listeners" + "ui.panel.config.developer-tools.tabs.templates.listeners" )}

        @@ -230,7 +230,7 @@ ${type === "object"
      • ${this.hass.localize( - "ui.panel.developer-tools.tabs.templates.domain" + "ui.panel.config.developer-tools.tabs.templates.domain" )}: ${domain}
      • @@ -243,7 +243,7 @@ ${type === "object"
      • ${this.hass.localize( - "ui.panel.developer-tools.tabs.templates.entity" + "ui.panel.config.developer-tools.tabs.templates.entity" )}: ${entity_id}
      • @@ -254,7 +254,7 @@ ${type === "object" : !this._templateResult.listeners.time ? html` ${this.hass.localize( - "ui.panel.developer-tools.tabs.templates.no_listeners" + "ui.panel.config.developer-tools.tabs.templates.no_listeners" )} ` : nothing}` @@ -457,7 +457,7 @@ ${type === "object" if ( !(await showConfirmationDialog(this, { text: this.hass.localize( - "ui.panel.developer-tools.tabs.templates.confirm_reset" + "ui.panel.config.developer-tools.tabs.templates.confirm_reset" ), warning: true, })) @@ -473,7 +473,7 @@ ${type === "object" if ( !(await showConfirmationDialog(this, { text: this.hass.localize( - "ui.panel.developer-tools.tabs.templates.confirm_clear" + "ui.panel.config.developer-tools.tabs.templates.confirm_clear" ), warning: true, })) diff --git a/src/panels/developer-tools/yaml_configuration/developer-yaml-config.ts b/src/panels/config/developer-tools/yaml_configuration/developer-yaml-config.ts similarity index 77% rename from src/panels/developer-tools/yaml_configuration/developer-yaml-config.ts rename to src/panels/config/developer-tools/yaml_configuration/developer-yaml-config.ts index 9787602b08..ac0e6309ce 100644 --- a/src/panels/developer-tools/yaml_configuration/developer-yaml-config.ts +++ b/src/panels/config/developer-tools/yaml_configuration/developer-yaml-config.ts @@ -1,22 +1,22 @@ import type { CSSResultGroup, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { componentsWithService } from "../../../common/config/components_with_service"; -import { stringCompare } from "../../../common/string/compare"; -import "../../../components/buttons/ha-call-service-button"; -import "../../../components/ha-alert"; -import "../../../components/ha-button"; -import "../../../components/ha-card"; -import "../../../components/ha-spinner"; -import type { CheckConfigResult } from "../../../data/core"; -import { checkCoreConfig } from "../../../data/core"; -import { domainToName } from "../../../data/integration"; -import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart"; -import { haStyle } from "../../../resources/styles"; -import type { HomeAssistant, Route, TranslationDict } from "../../../types"; +import { componentsWithService } from "../../../../common/config/components_with_service"; +import { stringCompare } from "../../../../common/string/compare"; +import "../../../../components/buttons/ha-call-service-button"; +import "../../../../components/ha-alert"; +import "../../../../components/ha-button"; +import "../../../../components/ha-card"; +import "../../../../components/ha-spinner"; +import type { CheckConfigResult } from "../../../../data/core"; +import { checkCoreConfig } from "../../../../data/core"; +import { domainToName } from "../../../../data/integration"; +import { showRestartDialog } from "../../../../dialogs/restart/show-dialog-restart"; +import { haStyle } from "../../../../resources/styles"; +import type { HomeAssistant, Route, TranslationDict } from "../../../../types"; type ReloadableDomain = Exclude< - keyof TranslationDict["ui"]["panel"]["developer-tools"]["tabs"]["yaml"]["section"]["reloading"], + keyof TranslationDict["ui"]["panel"]["config"]["developer-tools"]["tabs"]["yaml"]["section"]["reloading"], "heading" | "introduction" | "reload" >; @@ -63,10 +63,10 @@ export class DeveloperYamlConfig extends LitElement { domain, name: this.hass.localize( - `ui.panel.developer-tools.tabs.yaml.section.reloading.${domain}` + `ui.panel.config.developer-tools.tabs.yaml.section.reloading.${domain}` ) || this.hass.localize( - "ui.panel.developer-tools.tabs.yaml.section.reloading.reload", + "ui.panel.config.developer-tools.tabs.yaml.section.reloading.reload", { domain: domainToName(this.hass.localize, domain) } ), })) @@ -82,12 +82,12 @@ export class DeveloperYamlConfig extends LitElement {
        ${this.hass.localize( - "ui.panel.developer-tools.tabs.yaml.section.validation.introduction" + "ui.panel.config.developer-tools.tabs.yaml.section.validation.introduction" )} ${!this._validateResult ? this._validating @@ -104,10 +104,10 @@ export class DeveloperYamlConfig extends LitElement { ${ this._validateResult.result === "valid" ? this.hass.localize( - "ui.panel.developer-tools.tabs.yaml.section.validation.valid" + "ui.panel.config.developer-tools.tabs.yaml.section.validation.valid" ) : this.hass.localize( - "ui.panel.developer-tools.tabs.yaml.section.validation.invalid" + "ui.panel.config.developer-tools.tabs.yaml.section.validation.invalid" ) }
        @@ -117,7 +117,7 @@ export class DeveloperYamlConfig extends LitElement { ? html` @@ -131,7 +131,7 @@ export class DeveloperYamlConfig extends LitElement { ? html` @@ -146,7 +146,7 @@ export class DeveloperYamlConfig extends LitElement {
        ${this.hass.localize( - "ui.panel.developer-tools.tabs.yaml.section.validation.check_config" + "ui.panel.config.developer-tools.tabs.yaml.section.validation.check_config" )} ${this.hass.localize( - "ui.panel.developer-tools.tabs.yaml.section.server_management.restart" + "ui.panel.config.developer-tools.tabs.yaml.section.server_management.restart" )}
        @@ -164,12 +164,12 @@ export class DeveloperYamlConfig extends LitElement {
        ${this.hass.localize( - "ui.panel.developer-tools.tabs.yaml.section.reloading.introduction" + "ui.panel.config.developer-tools.tabs.yaml.section.reloading.introduction" )}
        @@ -178,7 +178,7 @@ export class DeveloperYamlConfig extends LitElement { domain="homeassistant" service="reload_all" >${this.hass.localize( - "ui.panel.developer-tools.tabs.yaml.section.reloading.all" + "ui.panel.config.developer-tools.tabs.yaml.section.reloading.all" )}
        @@ -188,7 +188,7 @@ export class DeveloperYamlConfig extends LitElement { domain="homeassistant" service="reload_core_config" >${this.hass.localize( - "ui.panel.developer-tools.tabs.yaml.section.reloading.core" + "ui.panel.config.developer-tools.tabs.yaml.section.reloading.core" )}
      diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 1b84ceb650..74cf314320 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -1,3 +1,4 @@ +import "@home-assistant/webawesome/dist/components/divider/divider"; import { consume } from "@lit/context"; import { mdiCog, @@ -19,6 +20,7 @@ import { ifDefined } from "lit/directives/if-defined"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { ASSIST_ENTITIES, SENSOR_ENTITIES } from "../../../common/const"; +import { fireEvent } from "../../../common/dom/fire_event"; import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name"; import { computeDomain } from "../../../common/entity/compute_domain"; import { computeEntityEntryName } from "../../../common/entity/compute_entity_name"; @@ -26,14 +28,15 @@ import { computeStateDomain } from "../../../common/entity/compute_state_domain" import { computeStateName } from "../../../common/entity/compute_state_name"; import { stringCompare } from "../../../common/string/compare"; import { slugify } from "../../../common/string/slugify"; +import { computeRTL } from "../../../common/util/compute_rtl"; import { groupBy } from "../../../common/util/group-by"; import "../../../components/entity/ha-battery-icon"; import "../../../components/ha-alert"; import "../../../components/ha-button"; -import "../../../components/ha-button-menu"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-next"; -import "../../../components/ha-list-item"; import "../../../components/ha-svg-icon"; import "../../../components/ha-tooltip"; import { assistSatelliteSupportsSetupFlow } from "../../../data/assist_satellite"; @@ -91,6 +94,7 @@ import { loadDeviceRegistryDetailDialog, showDeviceRegistryDetailDialog, } from "./device-registry-detail/show-dialog-device-registry-detail"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; export interface EntityRegistryStateEntry extends EntityRegistryEntry { stateName?: string | null; @@ -300,6 +304,11 @@ export class HaConfigDevicePage extends LitElement { super.updated(changedProps); if (changedProps.has("deviceId")) { this._findRelated(); + // Broadcast device context for quick bar + fireEvent(this, "hass-quick-bar-context", { + itemType: "device", + itemId: this.deviceId, + }); } } @@ -567,7 +576,9 @@ export class HaConfigDevicePage extends LitElement { ${this.hass.localize( "ui.panel.config.devices.cant_edit" @@ -688,36 +699,34 @@ export class HaConfigDevicePage extends LitElement { @click=${this._showSettings} .label=${this.hass.localize("ui.panel.config.devices.edit_settings")} > - + - - - - - + + + - + - - -
      - ${this.hass.localize("ui.panel.config.devices.restore_entity_ids")} -
      -
      -
      + + + ${this.hass.localize("ui.panel.config.devices.restore_entity_ids")} + +
      @@ -823,39 +832,43 @@ export class HaConfigDevicePage extends LitElement { ${actions.length ? html` - + - ${actions.map((deviceAction) => { - const listItem = html` { + const dropdownItem = html` - ${deviceAction.label} ${deviceAction.icon ? html` ` : ""} + ${deviceAction.label} ${deviceAction.trailingIcon ? html` ` : ""} - `; + `; return deviceAction.href ? html`${listItem} + >${dropdownItem} ` - : listItem; + : dropdownItem; })} - + ` : ""}
      @@ -998,9 +1011,8 @@ export class HaConfigDevicePage extends LitElement { this._diagnosticDownloadLinks = ( links as { link: string; domain: string }[] ).map((link) => ({ - href: link.link, icon: mdiDownload, - action: (ev) => this._signUrl(ev), + action: () => this._signUrl(link.link), label: links.length > 1 ? this.hass.localize( @@ -1334,6 +1346,13 @@ export class HaConfigDevicePage extends LitElement { } } + private _handleToolbarMenuAction(ev: HaDropdownSelectEvent) { + const action = ev.detail?.item?.value; + if (action === "reset_entity_ids") { + this._resetEntityIds(); + } + } + private _resetEntityIds = () => { const entities = this._entities(this.deviceId, this._entityReg).map( (e) => e.entity_id @@ -1466,15 +1485,18 @@ export class HaConfigDevicePage extends LitElement { }); } - private async _signUrl(ev) { - const a = ev.currentTarget.getAttribute("href") - ? ev.currentTarget - : ev.currentTarget.closest("a"); - - const signedUrl = await getSignedPath(this.hass, a.getAttribute("href")); + private async _signUrl(link: string) { + const signedUrl = await getSignedPath(this.hass, link); fileDownload(signedUrl.path); } + private _deviceActionSelected(ev: HaDropdownSelectEvent) { + const deviceAction = (ev.detail?.item as any)?.data as DeviceAction; + if (deviceAction?.action) { + deviceAction.action(ev); + } + } + private _deviceActionClicked(ev) { if (!ev.currentTarget.action) { return; diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index 7bac8f3f4a..e9b74b4fb0 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -1,7 +1,7 @@ +import "@home-assistant/webawesome/dist/components/divider/divider"; import { consume } from "@lit/context"; import { mdiCancel, - mdiChevronRight, mdiDelete, mdiDotsVertical, mdiMenuDown, @@ -16,7 +16,6 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { computeCssColor } from "../../../common/color/compute-color"; -import { formatShortDateTime } from "../../../common/datetime/format_date_time"; import { storage } from "../../../common/decorators/storage"; import type { HASSDomEvent } from "../../../common/dom/fire_event"; import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name"; @@ -42,8 +41,9 @@ import type { import "../../../components/data-table/ha-data-table-labels"; import "../../../components/entity/ha-battery-icon"; import "../../../components/ha-alert"; -import "../../../components/ha-button-menu"; import "../../../components/ha-check-list-item"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-fab"; import "../../../components/ha-filter-devices"; import "../../../components/ha-filter-floor-areas"; @@ -51,8 +51,6 @@ import "../../../components/ha-filter-integrations"; import "../../../components/ha-filter-labels"; import "../../../components/ha-filter-states"; import "../../../components/ha-icon-button"; -import "../../../components/ha-md-divider"; -import "../../../components/ha-md-menu-item"; import "../../../components/ha-sub-menu"; import { createAreaRegistryEntry } from "../../../data/area/area_registry"; import type { ConfigEntry, SubEntry } from "../../../data/config_entries"; @@ -97,13 +95,21 @@ import { configSections } from "../ha-panel-config"; import "../integrations/ha-integration-overflow-menu"; import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog"; import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; +import { + getAreaTableColumn, + getFloorTableColumn, + getLabelsTableColumn, + getCreatedAtTableColumn, + getModifiedAtTableColumn, +} from "../common/data-table-columns"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; interface DeviceRowData extends DeviceRegistryEntry { device?: DeviceRowData; area?: string; integration?: string; battery_entity?: [string | undefined, string | undefined]; - label_entries: EntityRegistryEntry[]; + label_entries: LabelRegistryEntry[]; } @customElement("ha-config-devices-dashboard") @@ -450,7 +456,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { (lbl) => labelReg!.find((label) => label.label_id === lbl)! ); - let floorName = "—"; + let floorName; if ( device.area_id && areas[device.area_id]?.floor_id && @@ -556,22 +562,8 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { : nothing} `, }, - area: { - title: localize("ui.panel.config.devices.data_table.area"), - sortable: true, - filterable: true, - groupable: true, - minWidth: "120px", - template: (device) => device.area || "—", - }, - floor: { - title: localize("ui.panel.config.devices.data_table.floor"), - sortable: true, - filterable: true, - groupable: true, - minWidth: "120px", - defaultHidden: true, - }, + area: getAreaTableColumn(localize), + floor: getFloorTableColumn(localize), integration: { title: localize("ui.panel.config.devices.data_table.integration"), sortable: true, @@ -629,34 +621,8 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { : "—"; }, }, - created_at: { - title: localize("ui.panel.config.generic.headers.created_at"), - defaultHidden: true, - sortable: true, - minWidth: "128px", - template: (entry) => - entry.created_at - ? formatShortDateTime( - new Date(entry.created_at * 1000), - this.hass.locale, - this.hass.config - ) - : "—", - }, - modified_at: { - title: localize("ui.panel.config.generic.headers.modified_at"), - defaultHidden: true, - sortable: true, - minWidth: "128px", - template: (entry) => - entry.modified_at - ? formatShortDateTime( - new Date(entry.modified_at * 1000), - this.hass.locale, - this.hass.config - ) - : "—", - }, + created_at: getCreatedAtTableColumn(localize, this.hass), + modified_at: getModifiedAtTableColumn(localize, this.hass), disabled_by: { title: localize("ui.panel.config.devices.picker.state"), type: "icon", @@ -685,13 +651,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { ` : "—", }, - labels: { - title: "", - hidden: true, - filterable: true, - template: (device) => - device.label_entries.map((lbl) => lbl.name).join(" "), - }, + labels: getLabelsTableColumn(), } as DataTableColumnContainer; }); @@ -703,6 +663,70 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { ]; } + private _renderAreaItems = (slot = "") => + html`${Object.values(this.hass.areas).map( + (area) => + html` + ${area.icon + ? html`` + : html``} + ${area.name} + ` + )} + + ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.no_area" + )} + + + + ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.add_area" + )} + `; + + private _renderLabelItems = (slot = "") => + html`${this._labels?.map((label) => { + const color = label.color ? computeCssColor(label.color) : undefined; + const selected = this._selected.every((deviceId) => + this.hass.devices[deviceId]?.labels.includes(label.label_id) + ); + const partial = + !selected && + this._selected.some((deviceId) => + this.hass.devices[deviceId]?.labels.includes(label.label_id) + ); + return html` + + + ${label.icon + ? html`` + : nothing} + ${label.name} + + `; + })} + + + ${this.hass.localize("ui.panel.config.labels.add_label")} + `; + protected render(): TemplateResult { const { devicesOutput } = this._devicesAndFilterDomains( this.hass.devices, @@ -719,77 +743,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { (this._sizeController.value && this._sizeController.value < 700) || (!this._sizeController.value && this.hass.dockedSidebar === "docked"); - const areaItems = html`${Object.values(this.hass.areas).map( - (area) => - html` - ${area.icon - ? html`` - : html``} -
      ${area.name}
      -
      ` - )} - -
      - ${this.hass.localize( - "ui.panel.config.devices.picker.bulk_actions.no_area" - )} -
      -
      - - -
      - ${this.hass.localize( - "ui.panel.config.devices.picker.bulk_actions.add_area" - )} -
      -
      `; - - const labelItems = html`${this._labels?.map((label) => { - const color = label.color ? computeCssColor(label.color) : undefined; - const selected = this._selected.every((deviceId) => - this.hass.devices[deviceId]?.labels.includes(label.label_id) - ); - const partial = - !selected && - this._selected.some((deviceId) => - this.hass.devices[deviceId]?.labels.includes(label.label_id) - ); - return html` - - - ${label.icon - ? html`` - : nothing} - ${label.name} - - `; - })} - - -
      - ${this.hass.localize("ui.panel.config.labels.add_label")} -
      `; - return html` ${!this.narrow - ? html` + ? html` - ${labelItems} - + ${this._renderLabelItems()} + ${areasInOverflow ? nothing - : html` + : html` - ${areaItems} - `}` + ${this._renderAreaItems()} + `}` : nothing} - + ${this.narrow ? html``} ${this.narrow - ? html` - -
      - ${this.hass.localize( - "ui.panel.config.automation.picker.bulk_actions.add_label" - )} -
      - -
      - ${labelItems} -
      ` + ? html` + ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.add_label" + )} + ${this._renderLabelItems("submenu")} + ` : nothing} ${areasInOverflow - ? html` - -
      - ${this.hass.localize( - "ui.panel.config.devices.picker.bulk_actions.move_area" - )} -
      - -
      - ${areaItems} -
      - ` + ? html` + ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.move_area" + )} + ${this._renderAreaItems("submenu")} + + ` : nothing} - - -
      - ${this.hass.localize( - "ui.panel.config.devices.picker.bulk_actions.delete_selected.button" - )} -
      -
      -
      + + ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.delete_selected.button" + )} + +
      `; } @@ -1085,6 +1026,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { } showAddIntegrationDialog(this, { domain: this._searchParms.get("domain") || undefined, + navigateToResult: true, }); } @@ -1094,12 +1036,22 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { this._selected = ev.detail.value; } - private _handleBulkArea = (item) => { - const area = item.value; - this._bulkAddArea(area); - }; + private _handleBulkArea(ev: HaDropdownSelectEvent) { + const area = ev.detail.item.value; - private async _bulkAddArea(area: string) { + if (area === "area_create") { + this._bulkCreateArea(); + return; + } + if (area === "area_no") { + this._bulkAddArea(null); + return; + } + + this._bulkAddArea(area.substring(5)); + } + + private async _bulkAddArea(area: string | null) { const promises: Promise[] = []; this._selected.forEach((deviceId) => { promises.push( @@ -1134,10 +1086,20 @@ ${rejected }); }; - private async _handleBulkLabel(ev) { - const label = ev.currentTarget.value; - const action = ev.currentTarget.action; - this._bulkLabel(label, action); + private async _handleBulkLabel(ev: HaDropdownSelectEvent) { + const label = ev.detail.item.value; + + if (label === "label_create") { + this._bulkCreateLabel(); + return; + } + + if (!label) { + return; + } + + const action = (ev.detail.item as any).action; + this._bulkLabel(label.substring(6), action); } private async _bulkLabel(label: string, action: "add" | "remove") { @@ -1251,6 +1213,27 @@ ${rejected this._activeHiddenColumns = ev.detail.hiddenColumns; } + private _handleBulkAction(ev: HaDropdownSelectEvent) { + const action = ev.detail.item.value; + + if (!action) { + return; + } + + if (action === "delete_selected") { + this._deleteSelected(); + } + + if (action.startsWith("label_")) { + this._handleBulkLabel(ev); + return; + } + + if (action.startsWith("area_")) { + this._handleBulkArea(ev); + } + } + static get styles(): CSSResultGroup { return [ css` @@ -1263,11 +1246,6 @@ ${rejected hass-tabs-subpage-data-table.narrow { --data-table-row-height: 72px; } - ha-button-menu { - margin-left: 8px; - margin-inline-start: 8px; - margin-inline-end: initial; - } .clear { color: var(--primary-color); padding-left: 8px; @@ -1278,7 +1256,11 @@ ${rejected ha-assist-chip { --ha-assist-chip-container-shape: 10px; } - ha-md-button-menu ha-assist-chip { + ha-dropdown::part(menu), + ha-dropdown::part(submenu) { + --auto-size-available-width: calc(50vw - var(--ha-space-4)); + } + ha-dropdown ha-assist-chip { --md-assist-chip-trailing-space: 8px; } ha-label { diff --git a/src/panels/config/energy/components/ha-energy-grid-settings.ts b/src/panels/config/energy/components/ha-energy-grid-settings.ts index eb48e4089a..906f915687 100644 --- a/src/panels/config/energy/components/ha-energy-grid-settings.ts +++ b/src/panels/config/energy/components/ha-energy-grid-settings.ts @@ -27,6 +27,7 @@ import type { FlowFromGridSourceEnergyPreference, FlowToGridSourceEnergyPreference, GridPowerSourceEnergyPreference, + GridPowerSourceInput, GridSourceTypeEnergyPreference, } from "../../../../data/energy"; import { @@ -559,7 +560,7 @@ export class EnergyGridSettings extends LitElement { ) as GridSourceTypeEnergyPreference | undefined; showEnergySettingsGridPowerDialog(this, { grid_source: gridSource, - saveCallback: async (power) => { + saveCallback: async (power: GridPowerSourceInput) => { let preferences: EnergyPreferences; if (!gridSource) { preferences = { @@ -568,7 +569,7 @@ export class EnergyGridSettings extends LitElement { ...this.preferences.energy_sources, { ...emptyGridSourceEnergyPreference(), - power: [power], + power: [power as GridPowerSourceEnergyPreference], }, ], }; @@ -577,7 +578,13 @@ export class EnergyGridSettings extends LitElement { ...this.preferences, energy_sources: this.preferences.energy_sources.map((src) => src.type === "grid" - ? { ...src, power: [...(gridSource.power || []), power] } + ? { + ...src, + power: [ + ...(gridSource.power || []), + power as GridPowerSourceEnergyPreference, + ], + } : src ), }; @@ -596,7 +603,7 @@ export class EnergyGridSettings extends LitElement { showEnergySettingsGridPowerDialog(this, { source: { ...origSource }, grid_source: gridSource, - saveCallback: async (source) => { + saveCallback: async (source: GridPowerSourceInput) => { const power = energySourcesByType(this.preferences).grid![0].power || []; @@ -606,7 +613,11 @@ export class EnergyGridSettings extends LitElement { src.type === "grid" ? { ...src, - power: power.map((p) => (p === origSource ? source : p)), + power: power.map((p) => + p === origSource + ? (source as GridPowerSourceEnergyPreference) + : p + ), } : src ), diff --git a/src/panels/config/energy/dialogs/dialog-energy-battery-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-battery-settings.ts index e341d94e49..0f35f27d82 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-battery-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-battery-settings.ts @@ -6,7 +6,13 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/entity/ha-statistic-picker"; import "../../../../components/ha-dialog"; import "../../../../components/ha-button"; -import type { BatterySourceTypeEnergyPreference } from "../../../../data/energy"; +import "../../../../components/ha-formfield"; +import "../../../../components/ha-radio"; +import type { HaRadio } from "../../../../components/ha-radio"; +import type { + BatterySourceTypeEnergyPreference, + PowerConfig, +} from "../../../../data/energy"; import { emptyBatteryEnergyPreference, energyStatisticHelpUrl, @@ -14,12 +20,14 @@ import { import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor"; import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyle, haStyleDialog } from "../../../../resources/styles"; -import type { HomeAssistant } from "../../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../../types"; import type { EnergySettingsBatteryDialogParams } from "./show-dialogs-energy"; const energyUnitClasses = ["energy"]; const powerUnitClasses = ["power"]; +type PowerType = "none" | "standard" | "inverted" | "two_sensors"; + @customElement("dialog-energy-battery-settings") export class DialogEnergyBatterySettings extends LitElement @@ -31,6 +39,10 @@ export class DialogEnergyBatterySettings @state() private _source?: BatterySourceTypeEnergyPreference; + @state() private _powerType: PowerType = "none"; + + @state() private _powerConfig: PowerConfig = {}; + @state() private _energy_units?: string[]; @state() private _power_units?: string[]; @@ -48,12 +60,37 @@ export class DialogEnergyBatterySettings this._source = params.source ? { ...params.source } : emptyBatteryEnergyPreference(); + + // Initialize power type from existing config + if (params.source?.power_config) { + const pc = params.source.power_config; + this._powerConfig = { ...pc }; + if (pc.stat_rate_inverted) { + this._powerType = "inverted"; + } else if (pc.stat_rate_from || pc.stat_rate_to) { + this._powerType = "two_sensors"; + } else if (pc.stat_rate) { + this._powerType = "standard"; + } else { + this._powerType = "none"; + } + } else if (params.source?.stat_rate) { + // Legacy format - treat as standard + this._powerType = "standard"; + this._powerConfig = { stat_rate: params.source.stat_rate }; + } else { + this._powerType = "none"; + this._powerConfig = {}; + } + this._energy_units = ( await getSensorDeviceClassConvertibleUnits(this.hass, "energy") ).units; this._power_units = ( await getSensorDeviceClassConvertibleUnits(this.hass, "power") ).units; + + // Build energy exclude list const allSources: string[] = []; this._params.battery_sources.forEach((entry) => { allSources.push(entry.stat_energy_from); @@ -64,16 +101,44 @@ export class DialogEnergyBatterySettings id !== this._source?.stat_energy_from && id !== this._source?.stat_energy_to ); - this._excludeListPower = this._params.battery_sources - .map((entry) => entry.stat_rate) - .filter((id) => id && id !== this._source?.stat_rate) as string[]; + + // Build power exclude list + const powerIds: string[] = []; + this._params.battery_sources.forEach((entry) => { + if (entry.stat_rate) powerIds.push(entry.stat_rate); + if (entry.power_config) { + if (entry.power_config.stat_rate) + powerIds.push(entry.power_config.stat_rate); + if (entry.power_config.stat_rate_inverted) + powerIds.push(entry.power_config.stat_rate_inverted); + if (entry.power_config.stat_rate_from) + powerIds.push(entry.power_config.stat_rate_from); + if (entry.power_config.stat_rate_to) + powerIds.push(entry.power_config.stat_rate_to); + } + }); + + const currentPowerIds = [ + this._powerConfig.stat_rate, + this._powerConfig.stat_rate_inverted, + this._powerConfig.stat_rate_from, + this._powerConfig.stat_rate_to, + params.source?.stat_rate, + ].filter(Boolean) as string[]; + + this._excludeListPower = powerIds.filter( + (id) => !currentPowerIds.includes(id) + ); } public closeDialog() { this._params = undefined; this._source = undefined; + this._powerType = "none"; + this._powerConfig = {}; this._error = undefined; this._excludeList = undefined; + this._excludeListPower = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); return true; } @@ -93,7 +158,7 @@ export class DialogEnergyBatterySettings ${this.hass.localize("ui.panel.config.energy.battery.dialog.header")}`} @closed=${this.closeDialog} > - ${this._error ? html`

      ${this._error}

      ` : ""} + ${this._error ? html`

      ${this._error}

      ` : nothing} - + > + + + + + + + + + + ${this._powerType === "standard" + ? html` + + ` + : nothing} + ${this._powerType === "inverted" + ? html` + + ` + : nothing} + ${this._powerType === "two_sensors" + ? html` + Boolean(id))} + @value-changed=${this._dischargePowerChanged} + > + Boolean(id))} + @value-changed=${this._chargePowerChanged} + > + ` + : nothing} ${this.hass.localize("ui.common.save")} @@ -168,21 +338,84 @@ export class DialogEnergyBatterySettings `; } - private _statisticToChanged(ev: CustomEvent<{ value: string }>) { + private _isValid(): boolean { + // Energy fields are always required + if (!this._source?.stat_energy_from || !this._source?.stat_energy_to) { + return false; + } + + // Power fields depend on selected type + switch (this._powerType) { + case "none": + return true; + case "standard": + return !!this._powerConfig.stat_rate; + case "inverted": + return !!this._powerConfig.stat_rate_inverted; + case "two_sensors": + return ( + !!this._powerConfig.stat_rate_from && !!this._powerConfig.stat_rate_to + ); + default: + return false; + } + } + + private _statisticToChanged(ev: ValueChangedEvent) { this._source = { ...this._source!, stat_energy_to: ev.detail.value }; } - private _statisticFromChanged(ev: CustomEvent<{ value: string }>) { + private _statisticFromChanged(ev: ValueChangedEvent) { this._source = { ...this._source!, stat_energy_from: ev.detail.value }; } - private _powerChanged(ev: CustomEvent<{ value: string }>) { - this._source = { ...this._source!, stat_rate: ev.detail.value }; + private _handlePowerTypeChanged(ev: Event) { + const input = ev.currentTarget as HaRadio; + this._powerType = input.value as PowerType; + // Clear power config when switching types + this._powerConfig = {}; + } + + private _standardPowerChanged(ev: ValueChangedEvent) { + this._powerConfig = { + stat_rate: ev.detail.value, + }; + } + + private _invertedPowerChanged(ev: ValueChangedEvent) { + this._powerConfig = { + stat_rate_inverted: ev.detail.value, + }; + } + + private _dischargePowerChanged(ev: ValueChangedEvent) { + this._powerConfig = { + ...this._powerConfig, + stat_rate_from: ev.detail.value, + }; + } + + private _chargePowerChanged(ev: ValueChangedEvent) { + this._powerConfig = { + ...this._powerConfig, + stat_rate_to: ev.detail.value, + }; } private async _save() { try { - await this._params!.saveCallback(this._source!); + const source: BatterySourceTypeEnergyPreference = { + type: "battery", + stat_energy_from: this._source!.stat_energy_from, + stat_energy_to: this._source!.stat_energy_to, + }; + + // Only include power_config if a power type is selected + if (this._powerType !== "none") { + source.power_config = { ...this._powerConfig }; + } + + await this._params!.saveCallback(source); this.closeDialog(); } catch (err: any) { this._error = err.message; @@ -204,6 +437,13 @@ export class DialogEnergyBatterySettings ha-statistic-picker:last-of-type { margin-bottom: 0; } + ha-formfield { + display: block; + } + .power-section-label { + margin-top: var(--ha-space-4); + margin-bottom: var(--ha-space-2); + } `, ]; } diff --git a/src/panels/config/energy/dialogs/dialog-energy-device-settings-water.ts b/src/panels/config/energy/dialogs/dialog-energy-device-settings-water.ts index 7e2d625469..c522d25661 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-device-settings-water.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-device-settings-water.ts @@ -3,21 +3,20 @@ import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { stopPropagation } from "../../../../common/dom/stop_propagation"; import "../../../../components/entity/ha-entity-picker"; import "../../../../components/entity/ha-statistic-picker"; +import "../../../../components/ha-button"; import "../../../../components/ha-dialog"; import "../../../../components/ha-radio"; -import "../../../../components/ha-button"; import "../../../../components/ha-select"; -import "../../../../components/ha-list-item"; +import type { HaSelectSelectEvent } from "../../../../components/ha-select"; import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy"; import { energyStatisticHelpUrl } from "../../../../data/energy"; import { getStatisticLabel } from "../../../../data/recorder"; import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor"; import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyleDialog } from "../../../../resources/styles"; -import type { HomeAssistant } from "../../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../../types"; import type { EnergySettingsDeviceWaterDialogParams } from "./show-dialogs-energy"; const volumeUnitClasses = ["volume"]; @@ -95,6 +94,27 @@ export class DialogEnergyDeviceSettingsWater const pickableUnit = this._volume_units?.join(", ") || ""; + const includedInDeviceOptions = !this._possibleParents.length + ? [ + { + value: "-", + disabled: true, + label: this.hass.localize( + "ui.panel.config.energy.device_consumption_water.dialog.no_upstream_devices" + ), + }, + ] + : this._possibleParents.map((stat) => ({ + value: stat.stat_consumption, + label: + stat.name || + getStatisticLabel( + this.hass, + stat.stat_consumption, + this._params?.statsMetadata?.[stat.stat_consumption] + ), + })); + return html` - ${!this._possibleParents.length - ? html` - ${this.hass.localize( - "ui.panel.config.energy.device_consumption_water.dialog.no_upstream_devices" - )} - ` - : this._possibleParents.map( - (stat) => html` - ${stat.name || - getStatisticLabel( - this.hass, - stat.stat_consumption, - this._params?.statsMetadata?.[stat.stat_consumption] - )} - ` - )} ) { + private _statisticChanged(ev: ValueChangedEvent) { if (!ev.detail.value) { this._device = undefined; return; @@ -221,10 +219,10 @@ export class DialogEnergyDeviceSettingsWater this._device = newDevice; } - private _parentSelected(ev) { + private _parentSelected(ev: HaSelectSelectEvent) { const newDevice = { ...this._device!, - included_in_stat: ev.target!.value, + included_in_stat: ev.detail.value, } as DeviceConsumptionEnergyPreference; if (!newDevice.included_in_stat) { delete newDevice.included_in_stat; @@ -249,6 +247,7 @@ export class DialogEnergyDeviceSettingsWater width: 100%; } ha-select { + display: block; margin-top: 16px; width: 100%; } diff --git a/src/panels/config/energy/dialogs/dialog-energy-device-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-device-settings.ts index afc2d732ae..910d5f4922 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-device-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-device-settings.ts @@ -3,21 +3,23 @@ import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { stopPropagation } from "../../../../common/dom/stop_propagation"; import "../../../../components/entity/ha-entity-picker"; import "../../../../components/entity/ha-statistic-picker"; +import "../../../../components/ha-button"; import "../../../../components/ha-dialog"; import "../../../../components/ha-radio"; -import "../../../../components/ha-button"; import "../../../../components/ha-select"; -import "../../../../components/ha-list-item"; +import type { + HaSelectOption, + HaSelectSelectEvent, +} from "../../../../components/ha-select"; import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy"; import { energyStatisticHelpUrl } from "../../../../data/energy"; import { getStatisticLabel } from "../../../../data/recorder"; import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor"; import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyleDialog } from "../../../../resources/styles"; -import type { HomeAssistant } from "../../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../../types"; import type { EnergySettingsDeviceDialogParams } from "./show-dialogs-energy"; const energyUnitClasses = ["energy"]; @@ -104,6 +106,28 @@ export class DialogEnergyDeviceSettings return nothing; } + const includedInDeviceOptions: HaSelectOption[] = this._possibleParents + .length + ? this._possibleParents.map((stat) => ({ + value: stat.stat_consumption, + label: + stat.name || + getStatisticLabel( + this.hass, + stat.stat_consumption, + this._params?.statsMetadata?.[stat.stat_consumption] + ), + })) + : [ + { + value: "-", + disabled: true, + label: this.hass.localize( + "ui.panel.config.energy.device_consumption.dialog.no_upstream_devices" + ), + }, + ]; + return html` - ${!this._possibleParents.length - ? html` - ${this.hass.localize( - "ui.panel.config.energy.device_consumption.dialog.no_upstream_devices" - )} - ` - : this._possibleParents.map( - (stat) => html` - ${stat.name || - getStatisticLabel( - this.hass, - stat.stat_consumption, - this._params?.statsMetadata?.[stat.stat_consumption] - )} - ` - )} ) { + private _statisticChanged(ev: ValueChangedEvent) { if (!ev.detail.value) { this._device = undefined; return; @@ -232,7 +234,7 @@ export class DialogEnergyDeviceSettings this._computePossibleParents(); } - private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) { + private _powerStatisticChanged(ev: ValueChangedEvent) { if (!this._device) { return; } @@ -257,10 +259,10 @@ export class DialogEnergyDeviceSettings this._device = newDevice; } - private _parentSelected(ev) { + private _parentSelected(ev: HaSelectSelectEvent) { const newDevice = { ...this._device!, - included_in_stat: ev.target!.value, + included_in_stat: ev.detail.value, } as DeviceConsumptionEnergyPreference; if (!newDevice.included_in_stat) { delete newDevice.included_in_stat; @@ -289,6 +291,7 @@ export class DialogEnergyDeviceSettings width: 100%; } ha-select { + display: block; margin-top: var(--ha-space-4); width: 100%; } diff --git a/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts index dc121b9fdf..44fb3a3887 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-gas-settings.ts @@ -25,7 +25,7 @@ import { import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor"; import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyle, haStyleDialog } from "../../../../resources/styles"; -import type { HomeAssistant } from "../../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../../types"; import type { EnergySettingsGasDialogParams } from "./show-dialogs-energy"; const gasDeviceClasses = ["gas", "energy"]; @@ -331,7 +331,7 @@ export class DialogEnergyGasSettings }; } - private async _statisticChanged(ev: CustomEvent<{ value: string }>) { + private async _statisticChanged(ev: ValueChangedEvent) { if (ev.detail.value) { const metadata = await getStatisticMetadata(this.hass, [ev.detail.value]); this._pickedDisplayUnit = getDisplayUnit( diff --git a/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts index 9bcfb9a20d..25bf92153a 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-grid-flow-settings.ts @@ -24,7 +24,7 @@ import { isExternalStatistic } from "../../../../data/recorder"; import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor"; import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyleDialog } from "../../../../resources/styles"; -import type { HomeAssistant } from "../../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../../types"; import type { EnergySettingsGridFlowDialogParams } from "./show-dialogs-energy"; const energyUnitClasses = ["energy"]; @@ -325,7 +325,7 @@ export class DialogEnergyGridFlowSettings }; } - private async _statisticChanged(ev: CustomEvent<{ value: string }>) { + private async _statisticChanged(ev: ValueChangedEvent) { if ( ev.detail.value && isExternalStatistic(ev.detail.value) && diff --git a/src/panels/config/energy/dialogs/dialog-energy-grid-power-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-grid-power-settings.ts index 0410a34bab..55f89c7c2f 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-grid-power-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-grid-power-settings.ts @@ -6,16 +6,24 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/entity/ha-statistic-picker"; import "../../../../components/ha-dialog"; import "../../../../components/ha-button"; -import type { GridPowerSourceEnergyPreference } from "../../../../data/energy"; +import "../../../../components/ha-formfield"; +import "../../../../components/ha-radio"; +import type { HaRadio } from "../../../../components/ha-radio"; +import type { + GridPowerSourceInput, + PowerConfig, +} from "../../../../data/energy"; import { energyStatisticHelpUrl } from "../../../../data/energy"; import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor"; import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyleDialog } from "../../../../resources/styles"; -import type { HomeAssistant } from "../../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../../types"; import type { EnergySettingsGridPowerDialogParams } from "./show-dialogs-energy"; const powerUnitClasses = ["power"]; +type SensorType = "standard" | "inverted" | "two_sensors"; + @customElement("dialog-energy-grid-power-settings") export class DialogEnergyGridPowerSettings extends LitElement @@ -25,7 +33,9 @@ export class DialogEnergyGridPowerSettings @state() private _params?: EnergySettingsGridPowerDialogParams; - @state() private _source?: GridPowerSourceEnergyPreference; + @state() private _sensorType: SensorType = "standard"; + + @state() private _powerConfig: PowerConfig = {}; @state() private _power_units?: string[]; @@ -37,23 +47,65 @@ export class DialogEnergyGridPowerSettings params: EnergySettingsGridPowerDialogParams ): Promise { this._params = params; - this._source = params.source ? { ...params.source } : { stat_rate: "" }; - const initialSourceIdPower = this._source.stat_rate; + // Initialize from existing source + if (params.source?.power_config) { + const pc = params.source.power_config; + this._powerConfig = { ...pc }; + if (pc.stat_rate_inverted) { + this._sensorType = "inverted"; + } else if (pc.stat_rate_from || pc.stat_rate_to) { + this._sensorType = "two_sensors"; + } else { + this._sensorType = "standard"; + } + } else if (params.source?.stat_rate) { + // Legacy format - treat as standard + this._sensorType = "standard"; + this._powerConfig = { stat_rate: params.source.stat_rate }; + } else { + this._sensorType = "standard"; + this._powerConfig = {}; + } this._power_units = ( await getSensorDeviceClassConvertibleUnits(this.hass, "power") ).units; - this._excludeListPower = [ - ...(this._params.grid_source?.power?.map((entry) => entry.stat_rate) || - []), - ].filter((id) => id && id !== initialSourceIdPower) as string[]; + // Build exclude list from all power sources + const excludeIds: string[] = []; + this._params.grid_source?.power?.forEach((entry) => { + if (entry.stat_rate) excludeIds.push(entry.stat_rate); + if (entry.power_config) { + if (entry.power_config.stat_rate) + excludeIds.push(entry.power_config.stat_rate); + if (entry.power_config.stat_rate_inverted) + excludeIds.push(entry.power_config.stat_rate_inverted); + if (entry.power_config.stat_rate_from) + excludeIds.push(entry.power_config.stat_rate_from); + if (entry.power_config.stat_rate_to) + excludeIds.push(entry.power_config.stat_rate_to); + } + }); + + // Filter out current source's IDs + const currentIds = [ + this._powerConfig.stat_rate, + this._powerConfig.stat_rate_inverted, + this._powerConfig.stat_rate_from, + this._powerConfig.stat_rate_to, + params.source?.stat_rate, + ].filter(Boolean) as string[]; + + this._excludeListPower = excludeIds.filter( + (id) => !currentIds.includes(id) + ); } public closeDialog() { this._params = undefined; - this._source = undefined; + this._powerConfig = {}; + this._sensorType = "standard"; this._error = undefined; this._excludeListPower = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); @@ -61,7 +113,7 @@ export class DialogEnergyGridPowerSettings } protected render() { - if (!this._params || !this._source) { + if (!this._params) { return nothing; } @@ -77,24 +129,123 @@ export class DialogEnergyGridPowerSettings )}`} @closed=${this.closeDialog} > - ${this._error ? html`

      ${this._error}

      ` : ""} + ${this._error ? html`

      ${this._error}

      ` : nothing} - + ${this.hass.localize( + "ui.panel.config.energy.grid.power_dialog.sensor_type" + )} +

      + + + + +
      + > + + + + + + + ${this._sensorType === "standard" + ? html` + + ` + : nothing} + ${this._sensorType === "inverted" + ? html` + + ` + : nothing} + ${this._sensorType === "two_sensors" + ? html` + Boolean(id))} + @value-changed=${this._fromStatisticChanged} + dialogInitialFocus + > + Boolean(id))} + @value-changed=${this._toStatisticChanged} + > + ` + : nothing} ${this.hass.localize("ui.common.save")} @@ -114,16 +265,60 @@ export class DialogEnergyGridPowerSettings `; } - private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) { - this._source = { - ...this._source!, + private _isValid(): boolean { + switch (this._sensorType) { + case "standard": + return !!this._powerConfig.stat_rate; + case "inverted": + return !!this._powerConfig.stat_rate_inverted; + case "two_sensors": + return ( + !!this._powerConfig.stat_rate_from && !!this._powerConfig.stat_rate_to + ); + default: + return false; + } + } + + private _handleSensorTypeChanged(ev: Event) { + const input = ev.currentTarget as HaRadio; + this._sensorType = input.value as SensorType; + // Clear config when switching types + this._powerConfig = {}; + } + + private _standardStatisticChanged(ev: ValueChangedEvent) { + this._powerConfig = { stat_rate: ev.detail.value, }; } + private _invertedStatisticChanged(ev: ValueChangedEvent) { + this._powerConfig = { + stat_rate_inverted: ev.detail.value, + }; + } + + private _fromStatisticChanged(ev: ValueChangedEvent) { + this._powerConfig = { + ...this._powerConfig, + stat_rate_from: ev.detail.value, + }; + } + + private _toStatisticChanged(ev: ValueChangedEvent) { + this._powerConfig = { + ...this._powerConfig, + stat_rate_to: ev.detail.value, + }; + } + private async _save() { try { - await this._params!.saveCallback(this._source!); + const source: GridPowerSourceInput = { + power_config: { ...this._powerConfig }, + }; + await this._params!.saveCallback(source); this.closeDialog(); } catch (err: any) { this._error = err.message; @@ -137,9 +332,15 @@ export class DialogEnergyGridPowerSettings ha-dialog { --mdc-dialog-max-width: 430px; } + ha-formfield { + display: block; + } ha-statistic-picker { display: block; - margin: var(--ha-space-4) 0; + margin-top: var(--ha-space-4); + } + p { + margin-bottom: var(--ha-space-2); } `, ]; diff --git a/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts index 80a2f2312d..7798477db1 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts @@ -23,7 +23,7 @@ import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor"; import { showConfigFlowDialog } from "../../../../dialogs/config-flow/show-dialog-config-flow"; import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyle, haStyleDialog } from "../../../../resources/styles"; -import type { HomeAssistant } from "../../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../../types"; import { brandsUrl } from "../../../../util/brands-url"; import type { EnergySettingsSolarDialogParams } from "./show-dialogs-energy"; @@ -285,11 +285,11 @@ export class DialogEnergySolarSettings }); } - private _statisticChanged(ev: CustomEvent<{ value: string }>) { + private _statisticChanged(ev: ValueChangedEvent) { this._source = { ...this._source!, stat_energy_from: ev.detail.value }; } - private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) { + private _powerStatisticChanged(ev: ValueChangedEvent) { this._source = { ...this._source!, stat_rate: ev.detail.value }; } diff --git a/src/panels/config/energy/dialogs/dialog-energy-water-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-water-settings.ts index 037dfd9b77..e81328fa35 100644 --- a/src/panels/config/energy/dialogs/dialog-energy-water-settings.ts +++ b/src/panels/config/energy/dialogs/dialog-energy-water-settings.ts @@ -21,7 +21,7 @@ import { isExternalStatistic } from "../../../../data/recorder"; import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor"; import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; import { haStyle, haStyleDialog } from "../../../../resources/styles"; -import type { HomeAssistant } from "../../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../../types"; import type { EnergySettingsWaterDialogParams } from "./show-dialogs-energy"; @customElement("dialog-energy-water-settings") @@ -277,7 +277,7 @@ export class DialogEnergyWaterSettings }; } - private async _statisticChanged(ev: CustomEvent<{ value: string }>) { + private async _statisticChanged(ev: ValueChangedEvent) { if ( ev.detail.value && isExternalStatistic(ev.detail.value) && diff --git a/src/panels/config/energy/dialogs/show-dialogs-energy.ts b/src/panels/config/energy/dialogs/show-dialogs-energy.ts index 49a9069c43..3631dd0c37 100644 --- a/src/panels/config/energy/dialogs/show-dialogs-energy.ts +++ b/src/panels/config/energy/dialogs/show-dialogs-energy.ts @@ -8,6 +8,7 @@ import type { FlowToGridSourceEnergyPreference, GasSourceTypeEnergyPreference, GridPowerSourceEnergyPreference, + GridPowerSourceInput, GridSourceTypeEnergyPreference, SolarSourceTypeEnergyPreference, WaterSourceTypeEnergyPreference, @@ -45,7 +46,7 @@ export interface EnergySettingsGridFlowToDialogParams { export interface EnergySettingsGridPowerDialogParams { source?: GridPowerSourceEnergyPreference; grid_source?: GridSourceTypeEnergyPreference; - saveCallback: (source: GridPowerSourceEnergyPreference) => Promise; + saveCallback: (source: GridPowerSourceInput) => Promise; } export interface EnergySettingsSolarDialogParams { diff --git a/src/panels/config/entities/entity-registry-settings-editor.ts b/src/panels/config/entities/entity-registry-settings-editor.ts index 30fe6e87ad..de536436c5 100644 --- a/src/panels/config/entities/entity-registry-settings-editor.ts +++ b/src/panels/config/entities/entity-registry-settings-editor.ts @@ -1,3 +1,4 @@ +import "@home-assistant/webawesome/dist/components/divider/divider"; import { mdiContentCopy, mdiRestore } from "@mdi/js"; import type { HassEntity } from "home-assistant-js-websocket"; import type { CSSResultGroup, PropertyValues } from "lit"; @@ -7,7 +8,6 @@ import { until } from "lit/directives/until"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { fireEvent } from "../../../common/dom/fire_event"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; import { computeDomain } from "../../../common/entity/compute_domain"; import { computeObjectId } from "../../../common/entity/compute_object_id"; import { supportsFeature } from "../../../common/entity/supports-feature"; @@ -21,6 +21,8 @@ import type { import { copyToClipboard } from "../../../common/util/copy-clipboard"; import "../../../components/ha-alert"; import "../../../components/ha-area-picker"; +import "../../../components/ha-color-picker"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-icon"; import "../../../components/ha-icon-button-next"; import "../../../components/ha-icon-picker"; @@ -28,6 +30,7 @@ import "../../../components/ha-labels-picker"; import "../../../components/ha-list-item"; import "../../../components/ha-radio"; import "../../../components/ha-select"; +import type { HaSelectSelectEvent } from "../../../components/ha-select"; import "../../../components/ha-settings-row"; import "../../../components/ha-state-icon"; import "../../../components/ha-switch"; @@ -53,6 +56,7 @@ import type { DeviceRegistryEntry } from "../../../data/device/device_registry"; import { updateDeviceRegistryEntry } from "../../../data/device/device_registry"; import type { AlarmControlPanelEntityOptions, + CalendarEntityOptions, EntityRegistryEntry, EntityRegistryEntryUpdateParams, ExtEntityRegistryEntry, @@ -195,6 +199,8 @@ export class EntityRegistrySettingsEditor extends LitElement { @state() private _defaultCode?: string | null; + @state() private _calendarColor?: string | null; + @state() private _noDeviceArea?: boolean; private _origEntityId!: string; @@ -253,6 +259,10 @@ export class EntityRegistrySettingsEditor extends LitElement { this._defaultCode = this.entry.options?.alarm_control_panel?.default_code; } + if (domain === "calendar") { + this._calendarColor = this.entry.options?.calendar?.color; + } + if (domain === "weather") { const stateObj: HassEntity | undefined = this.hass.states[this.entry.entity_id]; @@ -416,34 +426,40 @@ export class EntityRegistrySettingsEditor extends LitElement { .label=${this.hass.localize( "ui.dialogs.entity_registry.editor.device_class" )} - naturalMenuWidth - fixedMenuPosition @selected=${this._switchAsDomainChanged} - @closed=${stopPropagation} + value=${this._switchAsLabel( + this._switchAsDomain, + this._deviceClass + )} > - ${domainToName(this.hass.localize, "switch")} - - + ${this.hass.localize( "ui.dialogs.entity_registry.editor.device_classes.switch.outlet" )} - -
    • + + ${this._switchAsDomainsSorted( SWITCH_AS_DOMAINS, this.hass.localize ).map( (entry) => html` - + ${entry.label} - + ` )} ` @@ -452,19 +468,22 @@ export class EntityRegistrySettingsEditor extends LitElement { .label=${this.hass.localize( "ui.dialogs.entity_registry.editor.switch_as_x" )} - .value=${this._switchAsDomain} - naturalMenuWidth - fixedMenuPosition + .value=${this._switchAsLabel(this._switchAsDomain)} @selected=${this._switchAsDomainChanged} - @closed=${stopPropagation} > - + ${domainToName(this.hass.localize, "switch")} - - + + ${domainToName(this.hass.localize, domain)} - -
    • + + ${this._switchAsDomainsSorted( SWITCH_AS_DOMAINS, this.hass.localize @@ -472,9 +491,12 @@ export class EntityRegistrySettingsEditor extends LitElement { domain === entry.domain ? nothing : html` - + ${entry.label} - + ` )} @@ -505,12 +527,13 @@ export class EntityRegistrySettingsEditor extends LitElement { .label=${this.hass.localize( "ui.dialogs.entity_registry.editor.device_class" )} - .value=${this._deviceClass} - naturalMenuWidth - fixedMenuPosition + .value=${this._deviceClass + ? this.hass.localize( + `ui.dialogs.entity_registry.editor.device_classes.${domain}.${this._deviceClass}` + ) + : undefined} clearable @selected=${this._deviceClassChanged} - @closed=${stopPropagation} > ${this._deviceClassesSorted( domain, @@ -518,29 +541,35 @@ export class EntityRegistrySettingsEditor extends LitElement { this.hass.localize ).map( (entry) => html` - + ${entry.label} - + ` )} ${this._deviceClassOptions[0].length && this._deviceClassOptions[1].length - ? html`
    • ` - : ""} + ? html`` + : nothing} ${this._deviceClassesSorted( domain, this._deviceClassOptions[1], this.hass.localize ).map( (entry) => html` - + ${entry.label} - + ` )} ` - : ""} + : nothing} ${domain === "number" && this._deviceClass && stateObj?.attributes.unit_of_measurement && @@ -552,20 +581,14 @@ export class EntityRegistrySettingsEditor extends LitElement { .label=${this.hass.localize( "ui.dialogs.entity_registry.editor.unit_of_measurement" )} - .value=${stateObj.attributes.unit_of_measurement} - naturalMenuWidth - fixedMenuPosition + .value=${this._unit_of_measurement || + stateObj.attributes.unit_of_measurement} @selected=${this._unitChanged} - @closed=${stopPropagation} + .options=${this._numberDeviceClassConvertibleUnits} > - ${this._numberDeviceClassConvertibleUnits.map( - (unit: string) => html` - ${unit} - ` - )} ` - : ""} + : nothing} ${domain === "lock" ? html` ` - : ""} + : nothing} ${domain === "alarm_control_panel" ? html` ` - : ""} + : nothing} + ${domain === "calendar" + ? html` + + ` + : nothing} ${domain === "sensor" && this._deviceClass && stateObj?.attributes.unit_of_measurement && @@ -607,20 +643,14 @@ export class EntityRegistrySettingsEditor extends LitElement { .label=${this.hass.localize( "ui.dialogs.entity_registry.editor.unit_of_measurement" )} - .value=${stateObj.attributes.unit_of_measurement} - naturalMenuWidth - fixedMenuPosition + .value=${this._unit_of_measurement || + stateObj.attributes.unit_of_measurement} @selected=${this._unitChanged} - @closed=${stopPropagation} + .options=${this._sensorDeviceClassConvertibleUnits} > - ${this._sensorDeviceClassConvertibleUnits.map( - (unit: string) => html` - ${unit} - ` - )} ` - : ""} + : nothing} ${domain === "sensor" && // Allow customizing the precision for a sensor with numerical device class, // a unit of measurement or state class @@ -636,116 +666,78 @@ export class EntityRegistrySettingsEditor extends LitElement { .value=${this._precision == null ? "default" : this._precision.toString()} - naturalMenuWidth - fixedMenuPosition @selected=${this._precisionChanged} - @closed=${stopPropagation} + .options=${[ + { + value: "default", + label: this.hass.localize( + "ui.dialogs.entity_registry.editor.precision_default", + { + value: this._precisionLabel( + defaultPrecision, + stateObj?.state + ), + } + ), + }, + ...PRECISIONS.map((precision) => ({ + value: precision.toString(), + label: this._precisionLabel(precision, stateObj?.state), + })), + ]} > - ${this.hass.localize( - "ui.dialogs.entity_registry.editor.precision_default", - { - value: this._precisionLabel( - defaultPrecision, - stateObj?.state - ), - } - )} - ${PRECISIONS.map( - (precision) => html` - - ${this._precisionLabel(precision, stateObj?.state)} - - ` - )} ` - : ""} + : nothing} ${domain === "weather" ? html` - ${this._weatherConvertibleUnits?.precipitation_unit.map( - (unit: string) => html` - ${unit} - ` - )} - ${this._weatherConvertibleUnits?.pressure_unit.map( - (unit: string) => html` - ${unit} - ` - )} - ${this._weatherConvertibleUnits?.temperature_unit.map( - (unit: string) => html` - ${unit} - ` - )} - ${this._weatherConvertibleUnits?.visibility_unit.map( - (unit: string) => html` - ${unit} - ` - )} - ${this._weatherConvertibleUnits?.wind_speed_unit.map( - (unit: string) => html` - ${unit} - ` - )} ` - : ""} + : nothing} ` - : ""} + : nothing} ({ + value: num.toString(), + label: this.hass.localize( + ("ui.dialogs.entity_registry.editor.stream.stream_orientation_" + + num.toString()) as LocalizeKeys + ), + }))} + .value=${this._cameraPrefs.orientation.toString()} > - ${CAMERA_ORIENTATIONS.map((num) => { - const localizeStr = - "ui.dialogs.entity_registry.editor.stream.stream_orientation_" + - num.toString(); - return html` - - ${this.hass.localize(localizeStr as LocalizeKeys)} - - `; - })} ` - : ""} + : nothing} ${this.helperConfigEntry && this.helperConfigEntry.supports_options && this.helperConfigEntry.domain !== "switch_as_x" @@ -878,7 +865,7 @@ export class EntityRegistrySettingsEditor extends LitElement { ` - : ""} + : nothing} ` - : ""} + : nothing} ` - : ""} + : nothing} ${this.hass.localize( @@ -1015,8 +1002,8 @@ export class EntityRegistrySettingsEditor extends LitElement { .disabled=${this.disabled} @value-changed=${this._areaPicked} >` - : ""} ` - : ""} + : nothing} ` + : nothing} `; } @@ -1097,6 +1084,15 @@ export class EntityRegistrySettingsEditor extends LitElement { (params.options as AlarmControlPanelEntityOptions).default_code = this._defaultCode; } + if (domain === "calendar") { + const currentColor = this.entry.options?.calendar?.color ?? null; + const newColor = this._calendarColor ?? null; + if (currentColor !== newColor) { + params.options_domain = domain; + params.options = this.entry.options?.calendar || {}; + (params.options as CalendarEntityOptions).color = this._calendarColor; + } + } if ( domain === "weather" && (stateObj?.attributes?.precipitation_unit !== this._precipitation_unit || @@ -1313,14 +1309,14 @@ export class EntityRegistrySettingsEditor extends LitElement { this._entityId = `${computeDomain(this._origEntityId)}.${ev.target.value}`; } - private _deviceClassChanged(ev): void { + private _deviceClassChanged(ev: HaSelectSelectEvent): void { fireEvent(this, "change"); - this._deviceClass = ev.target.value; + this._deviceClass = ev.detail.value; } - private _unitChanged(ev): void { + private _unitChanged(ev: HaSelectSelectEvent): void { fireEvent(this, "change"); - this._unit_of_measurement = ev.target.value; + this._unit_of_measurement = ev.detail.value; } private _defaultcodeChanged(ev): void { @@ -1328,53 +1324,58 @@ export class EntityRegistrySettingsEditor extends LitElement { this._defaultCode = ev.target.value === "" ? null : ev.target.value; } - private _precipitationUnitChanged(ev): void { + private _calendarColorChanged(ev: CustomEvent): void { fireEvent(this, "change"); - this._precipitation_unit = ev.target.value; + this._calendarColor = ev.detail.value || null; } - private _precisionChanged(ev): void { + private _precipitationUnitChanged(ev: HaSelectSelectEvent): void { + fireEvent(this, "change"); + this._precipitation_unit = ev.detail.value; + } + + private _precisionChanged(ev: HaSelectSelectEvent): void { fireEvent(this, "change"); this._precision = - ev.target.value === "default" ? null : Number(ev.target.value); + ev.detail.value === "default" ? null : Number(ev.detail.value); } - private _pressureUnitChanged(ev): void { + private _pressureUnitChanged(ev: HaSelectSelectEvent): void { fireEvent(this, "change"); - this._pressure_unit = ev.target.value; + this._pressure_unit = ev.detail.value; } - private _temperatureUnitChanged(ev): void { + private _temperatureUnitChanged(ev: HaSelectSelectEvent): void { fireEvent(this, "change"); - this._temperature_unit = ev.target.value; + this._temperature_unit = ev.detail.value; } - private _visibilityUnitChanged(ev): void { + private _visibilityUnitChanged(ev: HaSelectSelectEvent): void { fireEvent(this, "change"); - this._visibility_unit = ev.target.value; + this._visibility_unit = ev.detail.value; } - private _windSpeedUnitChanged(ev): void { + private _windSpeedUnitChanged(ev: HaSelectSelectEvent): void { fireEvent(this, "change"); - this._wind_speed_unit = ev.target.value; + this._wind_speed_unit = ev.detail.value; } - private _switchAsDomainChanged(ev): void { - if (ev.target.value === "") { + private _switchAsDomainChanged(ev: HaSelectSelectEvent): void { + const value = ev.detail.value; + if (value === "") { return; } // If value is "outlet" that means the user kept the "switch" domain, but actually changed // the device_class of the switch to "outlet". - const switchAs = ev.target.value === "outlet" ? "switch" : ev.target.value; - this._switchAsDomain = switchAs; + this._switchAsDomain = value === "outlet" ? "switch" : value; if ( (computeDomain(this.entry.entity_id) === "switch" && - ev.target.value === "outlet") || - ev.target.value === "switch" + value === "outlet") || + value === "switch" ) { - this._deviceClass = ev.target.value; + this._deviceClass = value; } } @@ -1431,13 +1432,13 @@ export class EntityRegistrySettingsEditor extends LitElement { } } - private async _handleCameraOrientationChanged(ev) { + private async _handleCameraOrientationChanged(ev: HaSelectSelectEvent) { try { this._cameraPrefs = await updateCameraPrefs( this.hass, this.entry.entity_id, { - orientation: ev.currentTarget.value, + orientation: Number(ev.detail.value), } ); } catch (err: any) { @@ -1512,6 +1513,35 @@ export class EntityRegistrySettingsEditor extends LitElement { ) ); + private _switchAsLabel = memoizeOne( + (switchAsDomain: string, deviceClass?: string) => { + if (switchAsDomain !== "switch") { + const switchAsDomains = this._switchAsDomainsSorted( + SWITCH_AS_DOMAINS, + this.hass.localize + ); + + for (const entry of switchAsDomains) { + if (entry.domain === switchAsDomain) { + return entry.label; + } + } + } + + if (!deviceClass || deviceClass === "switch") { + return domainToName(this.hass.localize, "switch"); + } + + if (deviceClass === "outlet") { + return this.hass.localize( + "ui.dialogs.entity_registry.editor.device_classes.switch.outlet" + ); + } + + return switchAsDomain; + } + ); + static get styles(): CSSResultGroup { return [ haStyle, @@ -1557,9 +1587,6 @@ export class EntityRegistrySettingsEditor extends LitElement { margin: var(--ha-space-2) 0; width: 100%; } - li[divider] { - border-bottom-color: var(--divider-color); - } .menu-item { border-radius: var(--ha-border-radius-sm); margin-top: 3px; diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 9826e36d7e..1dbf185952 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -1,3 +1,4 @@ +import "@home-assistant/webawesome/dist/components/divider/divider"; import { consume } from "@lit/context"; import { mdiAlertCircle, @@ -23,7 +24,6 @@ import { ifDefined } from "lit/directives/if-defined"; import { styleMap } from "lit/directives/style-map"; import memoize from "memoize-one"; import { computeCssColor } from "../../../common/color/compute-color"; -import { formatShortDateTimeWithConditionalYear } from "../../../common/datetime/format_date_time"; import { storage } from "../../../common/decorators/storage"; import type { HASSDomEvent } from "../../../common/dom/fire_event"; import { computeAreaName } from "../../../common/entity/compute_area_name"; @@ -56,8 +56,9 @@ import type { } from "../../../components/data-table/ha-data-table"; import "../../../components/data-table/ha-data-table-labels"; import "../../../components/ha-alert"; -import "../../../components/ha-button-menu"; import "../../../components/ha-check-list-item"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-filter-devices"; import "../../../components/ha-filter-domains"; import "../../../components/ha-filter-floor-areas"; @@ -67,8 +68,6 @@ import "../../../components/ha-filter-states"; import "../../../components/ha-filter-voice-assistants"; import "../../../components/ha-icon"; import "../../../components/ha-icon-button"; -import "../../../components/ha-md-divider"; -import "../../../components/ha-md-menu-item"; import "../../../components/ha-sub-menu"; import "../../../components/ha-svg-icon"; import "../../../components/ha-tooltip"; @@ -83,11 +82,14 @@ import type { import { UNAVAILABLE } from "../../../data/entity/entity"; import type { EntityRegistryEntry, + EntityRegistryOptions, UpdateEntityRegistryEntryResult, } from "../../../data/entity/entity_registry"; import { updateEntityRegistryEntry } from "../../../data/entity/entity_registry"; import type { EntitySources } from "../../../data/entity/entity_sources"; import { fetchEntitySourcesWithCache } from "../../../data/entity/entity_sources"; +import type { ExposeEntitySettings } from "../../../data/expose"; +import { listExposedEntities, voiceAssistants } from "../../../data/expose"; import { HELPERS_CRUD } from "../../../data/helpers_crud"; import type { IntegrationManifest } from "../../../data/integration"; import { @@ -117,12 +119,20 @@ import { isHelperDomain } from "../helpers/const"; import "../integrations/ha-integration-overflow-menu"; import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog"; import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; -import { getEntityVoiceAssistantsIds } from "../../../data/expose"; -import { getAvailableAssistants } from "../voice-assistants/expose/available-assistants"; import { - getAssistantsTableColumn, + getEntityIdTableColumn, + getDomainTableColumn, + getAreaTableColumn, + getLabelsTableColumn, + getCreatedAtTableColumn, + getModifiedAtTableColumn, +} from "../common/data-table-columns"; +import { getAssistantsSortableKey, + getAssistantsTableColumn, } from "../voice-assistants/expose/assistants-table-column"; +import { getAvailableAssistants } from "../voice-assistants/expose/available-assistants"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; export interface StateEntity extends Omit< EntityRegistryEntry, @@ -164,18 +174,23 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { @property({ attribute: false }) public cloudStatus?: CloudStatus; - @state() private _stateEntities: StateEntity[] = []; - @state() private _entries?: ConfigEntry[]; @state() private _subEntries?: SubEntry[]; @state() private _manifests?: IntegrationManifest[]; + // does NOT contain the entities without unique id (e.g. Sun) @state() @consume({ context: fullEntitiesContext, subscribe: true }) _entities!: EntityRegistryEntry[]; + @state() private _entitiesWithoutUniqueId: StateEntity[] = []; + + // entities exposed to one or more voice assistants, + // including entities without unique id + @state() private _exposedEntities?: Record; + @state() @storage({ storage: "sessionStorage", @@ -245,12 +260,20 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { super.connectedCallback(); window.addEventListener("location-changed", this._locationChanged); window.addEventListener("popstate", this._popState); + window.addEventListener( + "exposed-entities-changed", + this._fetchExposedEntities + ); } - disconnectedCallback(): void { + public disconnectedCallback(): void { super.disconnectedCallback(); window.removeEventListener("location-changed", this._locationChanged); window.removeEventListener("popstate", this._popState); + window.removeEventListener( + "exposed-entities-changed", + this._fetchExposedEntities + ); } private _locationChanged = () => { @@ -360,32 +383,15 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { groupable: true, hidden: true, }, - area: { - title: localize("ui.panel.config.entities.picker.headers.area"), - sortable: true, - filterable: true, - groupable: true, - template: (entry) => entry.area || "—", - }, - entity_id: { - title: localize("ui.panel.config.entities.picker.headers.entity_id"), - sortable: true, - filterable: true, - defaultHidden: true, - }, + area: getAreaTableColumn(localize), + entity_id: getEntityIdTableColumn(localize, true), localized_platform: { title: localize("ui.panel.config.entities.picker.headers.integration"), sortable: true, groupable: true, filterable: true, }, - domain: { - title: localize("ui.panel.config.entities.picker.headers.domain"), - sortable: false, - hidden: true, - filterable: true, - groupable: true, - }, + domain: getDomainTableColumn(localize), disabled_by: { title: localize("ui.panel.config.entities.picker.headers.disabled_by"), hidden: true, @@ -459,34 +465,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { ` : "—", }, - created_at: { - title: localize("ui.panel.config.generic.headers.created_at"), - defaultHidden: true, - sortable: true, - minWidth: "128px", - template: (entry) => - entry.created_at - ? formatShortDateTimeWithConditionalYear( - new Date(entry.created_at * 1000), - this.hass.locale, - this.hass.config - ) - : "—", - }, - modified_at: { - title: localize("ui.panel.config.generic.headers.modified_at"), - defaultHidden: true, - sortable: true, - minWidth: "128px", - template: (entry) => - entry.modified_at - ? formatShortDateTimeWithConditionalYear( - new Date(entry.modified_at * 1000), - this.hass.locale, - this.hass.config - ) - : "—", - }, + created_at: getCreatedAtTableColumn(localize, this.hass), + modified_at: getModifiedAtTableColumn(localize, this.hass), available: { title: localize("ui.panel.config.entities.picker.headers.availability"), sortable: true, @@ -505,13 +485,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { groupable: true, hidden: true, }, - labels: { - title: "", - hidden: true, - filterable: true, - template: (entry) => - entry.label_entries.map((lbl) => lbl.name).join(" "), - }, + labels: getLabelsTableColumn(), assistants: getAssistantsTableColumn( localize, this.hass, @@ -527,7 +501,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { entities: StateEntity[], devices: HomeAssistant["devices"], areas: HomeAssistant["areas"], - stateEntities: StateEntity[], + entitiesWithoutUniqueId: StateEntity[], filters: DataTableFiltersValues, filteredItems: DataTableFiltersItems, entries?: ConfigEntry[], @@ -554,7 +528,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { const showReadOnly = !stateFilters?.length || stateFilters.includes("readonly"); - let filteredEntities = entities.concat(stateEntities); + // take both entities with and without unique id into account + let filteredEntities = entities.concat(entitiesWithoutUniqueId); let filteredConfigEntry: ConfigEntry | undefined; const filteredDomains = new Set(); @@ -668,7 +643,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { filter.length ) { filteredEntities = filteredEntities.filter((entity) => - getEntityVoiceAssistantsIds(this._entities, entity.entity_id).some( + this._getExposedEntityVoiceAssistantIds(entity.entity_id).some( (va) => (filter as string[]).includes(va) ) ); @@ -731,8 +706,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { ? `${deviceName} (${areaName})` : deviceName : undefined; - const assistants = getEntityVoiceAssistantsIds( - entities as EntityRegistryEntry[], + const assistants = this._getExposedEntityVoiceAssistantIds( entry.entity_id ); @@ -789,31 +763,8 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { ]; } - protected render() { - if (!this.hass || this._entities === undefined) { - return html` `; - } - - const { filteredEntities, filteredDomains } = - this._filteredEntitiesAndDomains( - this.hass.localize, - this._entities, - this.hass.devices, - this.hass.areas, - this._stateEntities, - this._filters, - this._filteredItems, - this._entries, - this._labels - ); - - const includeAddDeviceFab = - filteredDomains.size === 1 && - (PROTOCOL_INTEGRATIONS as readonly string[]).includes( - [...filteredDomains][0] - ); - - const labelItems = html` ${this._labels?.map((label) => { + private _renderLabelItems = (slot = "") => + html`${this._labels?.map((label) => { const color = label.color ? computeCssColor(label.color) : undefined; const selected = this._selected.every((entityId) => this.hass.entities[entityId]?.labels.includes(label.label_id) @@ -823,14 +774,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { this._selected.some((entityId) => this.hass.entities[entityId]?.labels.includes(label.label_id) ); - return html` - `; + `; })} - - -
      - ${this.hass.localize("ui.panel.config.labels.add_label")} -
      `; + + + ${this.hass.localize("ui.panel.config.labels.add_label")} + `; + + protected render() { + if (!this.hass || this._entities === undefined) { + return html` `; + } + + const { filteredEntities, filteredDomains } = + this._filteredEntitiesAndDomains( + this.hass.localize, + this._entities, + this.hass.devices, + this.hass.areas, + this._entitiesWithoutUniqueId, + this._filters, + this._filteredItems, + this._entries, + this._labels + ); + + const includeAddDeviceFab = + filteredDomains.size === 1 && + (PROTOCOL_INTEGRATIONS as readonly string[]).includes( + [...filteredDomains][0] + ); return html` - Array.isArray(filter) - ? filter.length - : filter && - Object.values(filter).some((val) => - Array.isArray(val) ? val.length : val - ) - ).length - } + .filters=${Object.values(this._filters).filter((filter) => + Array.isArray(filter) + ? filter.length + : filter && + Object.values(filter).some((val) => + Array.isArray(val) ? val.length : val + ) + ).length} selectable .selected=${this._selected.length} .initialGroupColumn=${this._activeGrouping ?? "device_full"} @@ -905,157 +875,125 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { slot="toolbar-icon" > - -${ - !this.narrow - ? html` - - - - ${labelItems} - ` - : nothing -} - - ${ - this.narrow - ? html` - - ` - : html`` - } - - ${ - this.narrow - ? html` - -
      - ${this.hass.localize( + ${!this.narrow + ? html` + - - - ${labelItems} - - ` - : nothing - } - - - -
      - ${this.hass.localize( - "ui.panel.config.entities.picker.enable_selected.button" - )} -
      -
      - - -
      - ${this.hass.localize( - "ui.panel.config.entities.picker.disable_selected.button" - )} -
      -
      - - - - -
      - ${this.hass.localize( - "ui.panel.config.entities.picker.unhide_selected.button" - )} -
      -
      - - -
      - ${this.hass.localize( - "ui.panel.config.entities.picker.hide_selected.button" - )} -
      -
      - - - - - -
      - ${this.hass.localize( - "ui.panel.config.entities.picker.restore_entity_id_selected.button" - )} -
      -
      - - - - - -
      - ${this.hass.localize( - "ui.panel.config.entities.picker.delete_selected.button" - )} -
      -
      - - - ${ - Array.isArray(this._filters.config_entry) && - this._filters.config_entry.length - ? html` - ${this.hass.localize( - "ui.panel.config.entities.picker.filtering_by_config_entry" + > + +
      + ${this._renderLabelItems()} +
      ` + : nothing} + + ${this.narrow + ? html` entry.entry_id === this._filters.config_entry![0] - )?.title || this._filters.config_entry[0]}${this._filters - .config_entry.length === 1 && - Array.isArray(this._filters.sub_entry) && - this._filters.sub_entry.length - ? html` (${this._subEntries?.find( - (entry) => - entry.subentry_id === this._filters.sub_entry![0] - )?.title || this._filters.sub_entry[0]})` - : nothing} - ` - : nothing - } + slot="trigger" + > + + ` + : html``} + ${this.narrow + ? html` + ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.add_label" + )} + + ${this._renderLabelItems("submenu")} + + ` + : nothing} + + + + ${this.hass.localize( + "ui.panel.config.entities.picker.enable_selected.button" + )} + + + + ${this.hass.localize( + "ui.panel.config.entities.picker.disable_selected.button" + )} + + + + + + ${this.hass.localize( + "ui.panel.config.entities.picker.unhide_selected.button" + )} + + + + ${this.hass.localize( + "ui.panel.config.entities.picker.hide_selected.button" + )} + + + + + + + ${this.hass.localize( + "ui.panel.config.entities.picker.restore_entity_id_selected.button" + )} + + + + + + + ${this.hass.localize( + "ui.panel.config.entities.picker.delete_selected.button" + )} + + + ${Array.isArray(this._filters.config_entry) && + this._filters.config_entry.length + ? html` + ${this.hass.localize( + "ui.panel.config.entities.picker.filtering_by_config_entry" + )} + ${this._entries?.find( + (entry) => entry.entry_id === this._filters.config_entry![0] + )?.title || this._filters.config_entry[0]}${this._filters + .config_entry.length === 1 && + Array.isArray(this._filters.sub_entry) && + this._filters.sub_entry.length + ? html` (${this._subEntries?.find( + (entry) => entry.subentry_id === this._filters.sub_entry![0] + )?.title || this._filters.sub_entry[0]})` + : nothing} + ` + : nothing} - ${ - includeAddDeviceFab - ? html` - - ` - : nothing - } + ${includeAddDeviceFab + ? html` + + ` + : nothing} `; } @@ -1160,6 +1094,7 @@ ${ protected firstUpdated() { this._setFiltersFromUrl(); + this._fetchExposedEntities(); fetchEntitySourcesWithCache(this.hass).then((sources) => { this._entitySources = sources; }); @@ -1201,6 +1136,40 @@ ${ this._filteredItems = {}; } + private _fetchExposedEntities = async () => { + try { + this._exposedEntities = ( + await listExposedEntities(this.hass) + ).exposed_entities; + } catch (err) { + // eslint-disable-next-line no-console + console.error("Failed to fetch exposed entities", err); + this._exposedEntities = {}; + } + }; + + // local variant of data/expose/getExposedEntityVoiceAssistantIds + // which works for both entities with and without unique id + private _getExposedEntityVoiceAssistantIds(entityId: string) { + return Object.keys(voiceAssistants).filter( + (vaId) => this._exposedEntities?.[entityId]?.[vaId] + ); + } + + private _getExposedEntitySettingsAsOptions( + entityId: string + ): EntityRegistryOptions { + const exposeSettings: ExposeEntitySettings | undefined = + this._exposedEntities?.[entityId]; + const options: EntityRegistryOptions = {}; + Object.keys(voiceAssistants).forEach((vaId) => { + options[vaId] = { + should_expose: exposeSettings?.[vaId], + }; + }); + return options; + } + public willUpdate(changedProps: PropertyValues): void { super.willUpdate(changedProps); @@ -1218,12 +1187,18 @@ ${ (changedProps.has("hass") && (!oldHass || oldHass.states !== this.hass.states)) || changedProps.has("_entities") || - changedProps.has("_entitySources") + changedProps.has("_entitySources") || + changedProps.has("_exposedEntities") ) { - const stateEntities: StateEntity[] = []; + // represent all entities without unique id + // (entities present in this.hass.states but not in fullEntitiesContext) + // as StateEntities, so this component can treat them the same as the + // entities with id + const entitiesWithoutUniqueId: StateEntity[] = []; const regEntityIds = new Set( this._entities.map((entity) => entity.entity_id) ); + for (const entityId of Object.keys(this.hass.states)) { if (regEntityIds.has(entityId)) { continue; @@ -1234,7 +1209,7 @@ ${ ) { changed = true; } - stateEntities.push({ + entitiesWithoutUniqueId.push({ name: computeStateName(this.hass.states[entityId]), entity_id: entityId, platform: @@ -1250,7 +1225,7 @@ ${ selectable: false, entity_category: null, has_entity_name: false, - options: null, + options: this._getExposedEntitySettingsAsOptions(entityId), labels: [], categories: {}, created_at: 0, @@ -1258,7 +1233,7 @@ ${ }); } if (changed) { - this._stateEntities = stateEntities; + this._entitiesWithoutUniqueId = entitiesWithoutUniqueId; } } } @@ -1397,10 +1372,24 @@ ${ this._clearSelection(); }; - private async _handleBulkLabel(ev) { - const label = ev.currentTarget.value; - const action = ev.currentTarget.action; - await this._bulkLabel(label, action); + private async _handleBulkLabel(ev: HaDropdownSelectEvent) { + ev.preventDefault(); // Prevent the dropdown from closing + + const label = ev.detail.item.value; + + if (!label) { + return; + } + + if (label === "label_create") { + this._bulkCreateLabel(); + return; + } + + const labelId = label.substring(6); + + const action = (ev.detail.item as any).action; + await this._bulkLabel(labelId, action); } private async _bulkLabel(label: string, action: "add" | "remove") { @@ -1551,7 +1540,7 @@ ${rejected this._entities!, this.hass.devices, this.hass.areas, - this._stateEntities, + this._entitiesWithoutUniqueId, this._filters, this._filteredItems, this._entries, @@ -1570,6 +1559,7 @@ ${rejected } showAddIntegrationDialog(this, { domain: this._searchParms.get("domain") || undefined, + navigateToResult: true, }); } @@ -1590,6 +1580,39 @@ ${rejected this._activeHiddenColumns = ev.detail.hiddenColumns; } + private _handleBulkAction(ev: HaDropdownSelectEvent) { + const action = ev.detail.item.value; + + if (!action) { + return; + } + + switch (action) { + case "enable_selected": + this._enableSelected(); + return; + case "disable_selected": + this._disableSelected(); + return; + case "unhide_selected": + this._unhideSelected(); + return; + case "hide_selected": + this._hideSelected(); + return; + case "restore_entity_id_selected": + this._restoreEntityIdSelected(); + return; + case "delete_selected": + this._removeSelected(); + return; + } + + if (action.startsWith("label_")) { + this._handleBulkLabel(ev); + } + } + static get styles(): CSSResultGroup { return [ haStyle, @@ -1647,11 +1670,6 @@ ${rejected .header-btns > ha-icon-button { margin: 8px; } - ha-button-menu { - margin-left: 8px; - margin-inline-start: 8px; - margin-inline-end: initial; - } .clear { color: var(--primary-color); padding-left: 8px; @@ -1664,7 +1682,11 @@ ${rejected ha-assist-chip { --ha-assist-chip-container-shape: 10px; } - ha-md-button-menu ha-assist-chip { + ha-dropdown::part(menu), + ha-dropdown::part(submenu) { + --auto-size-available-width: calc(50vw - var(--ha-space-4)); + } + ha-dropdown ha-assist-chip { --md-assist-chip-trailing-space: 8px; } ha-label { diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index fb6dbd526a..58b0db96d0 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -9,12 +9,12 @@ import { mdiDatabase, mdiDevices, mdiFlask, + mdiHammer, mdiInformation, mdiInformationOutline, mdiLabel, mdiLightningBolt, mdiMapMarkerRadius, - mdiMathLog, mdiMemory, mdiMicrophone, mdiNetwork, @@ -27,6 +27,8 @@ import { mdiScriptText, mdiShape, mdiSofa, + mdiStarFourPoints, + mdiTextBoxOutline, mdiTools, mdiUpdate, mdiViewDashboard, @@ -57,7 +59,6 @@ declare global { // for fire event interface HASSDomEvents { "ha-refresh-cloud-status": undefined; - "ha-refresh-supervisor": undefined; } } @@ -85,8 +86,8 @@ export const configSections: Record = { component: "zone", }, { - path: "/hassio", - translationKey: "supervisor", + path: "/config/apps", + translationKey: "apps", iconPath: mdiPuzzle, iconColor: "#F1C447", component: "hassio", @@ -116,16 +117,15 @@ export const configSections: Record = { dashboard_2: [ { path: "/config/matter", - name: "Matter", iconPath: "M7.228375 6.41685c0.98855 0.80195 2.16365 1.3412 3.416275 1.56765V1.30093l1.3612 -0.7854275 1.360125 0.7854275V7.9845c1.252875 -0.226675 2.4283 -0.765875 3.41735 -1.56765l2.471225 1.4293c-4.019075 3.976275 -10.490025 3.976275 -14.5091 0l2.482925 -1.4293Zm3.00335 17.067575c1.43325 -5.47035 -1.8052 -11.074775 -7.2604 -12.564675v2.859675c1.189125 0.455 2.244125 1.202875 3.0672 2.174275L0.25 19.2955v1.5719l1.3611925 0.781175L7.39865 18.3068c0.430175 1.19825 0.550625 2.48575 0.35015 3.743l2.482925 1.434625ZM21.034 10.91975c-5.452225 1.4932 -8.6871 7.09635 -7.254025 12.564675l2.47655 -1.43035c-0.200025 -1.257275 -0.079575 -2.544675 0.35015 -3.743025l5.7832 3.337525L23.75 20.86315V19.2955L17.961475 15.9537c0.8233 -0.97115 1.878225 -1.718975 3.0672 -2.174275l0.005325 -2.859675Z", + iconViewBox: "0 1 24 24", iconColor: "#2458B3", component: "matter", translationKey: "matter", }, { path: "/config/zha", - name: "Zigbee", iconPath: mdiZigbee, iconColor: "#E74011", component: "zha", @@ -133,7 +133,6 @@ export const configSections: Record = { }, { path: "/config/zwave_js", - name: "Z-Wave", iconPath: mdiZWave, iconColor: "#153163", component: "zwave_js", @@ -141,7 +140,6 @@ export const configSections: Record = { }, { path: "/knx", - name: "KNX", iconPath: "M 3.9861338,14.261456 3.7267552,13.934877 6.3179131,11.306266 H 4.466374 l -2.6385205,2.68258 V 11.312882 H 0.00440574 L 0,17.679803 l 1.8278535,5.43e-4 v -1.818482 l 0.7225444,-0.732459 2.1373588,2.543782 2.1869324,-5.44e-4 M 24,17.680369 21.809238,17.669359 19.885559,15.375598 17.640262,17.68037 h -1.828407 l 3.236048,-3.302138 -2.574075,-3.067547 2.135161,0.0016 1.610309,1.87687 1.866403,-1.87687 h 1.828429 l -2.857742,2.87478 m -10.589867,-2.924898 2.829625,3.990552 -0.01489,-3.977887 1.811889,-0.0044 0.0011,6.357564 -2.093314,-5.44e-4 -2.922133,-3.947594 -0.0314,3.947594 H 8.2581097 V 11.261677 M 11.971203,6.3517488 c 0,0 2.800714,-0.093203 6.172001,1.0812045 3.462393,1.0898845 5.770926,3.4695627 5.770926,3.4695627 l -1.823898,-5.43e-4 C 22.088532,10.900273 20.577938,9.4271528 17.660223,8.5024618 15.139256,7.703366 12.723057,7.645835 12.111178,7.6449876 l -9.71e-4,0.0011 c 0,0 -0.0259,-6.4e-4 -0.07527,-9.714e-4 -0.04726,3.33e-4 -0.07201,9.714e-4 -0.07201,9.714e-4 v -0.00113 C 11.337007,7.6453728 8.8132091,7.7001736 6.2821829,8.5024618 3.3627914,9.4276738 1.8521646,10.901973 1.8521646,10.901973 l -1.82398708,5.43e-4 C 0.03128403,10.899322 2.339143,8.5221038 5.799224,7.4329533 9.170444,6.2585642 11.971203,6.3517488 11.971203,6.3517488 Z", iconColor: "#4EAA66", @@ -150,7 +148,6 @@ export const configSections: Record = { }, { path: "/config/thread", - name: "Thread", iconPath: "m 17.126982,8.0730792 c 0,-0.7297242 -0.593746,-1.32357 -1.323637,-1.32357 -0.729454,0 -1.323199,0.5938458 -1.323199,1.32357 v 1.3234242 l 1.323199,1.458e-4 c 0.729891,0 1.323637,-0.5937006 1.323637,-1.32357 z M 11.999709,0 C 5.3829818,0 0,5.3838955 0,12.001455 0,18.574352 5.3105455,23.927406 11.865164,24 V 12.012075 l -3.9275642,-2.91e-4 c -1.1669814,0 -2.1169453,0.949979 -2.1169453,2.118323 0,1.16718 0.9499639,2.116868 2.1169453,2.116868 v 2.615717 c -2.6093089,0 -4.732218,-2.12327 -4.732218,-4.732585 0,-2.61048 2.1229091,-4.7343308 4.732218,-4.7343308 l 3.9275642,5.82e-4 v -1.323279 c 0,-2.172296 1.766691,-3.9395777 3.938181,-3.9395777 2.171928,0 3.9392,1.7672817 3.9392,3.9395777 0,2.1721498 -1.767272,3.9395768 -3.9392,3.9395768 l -1.323199,-1.45e-4 V 23.744102 C 19.911127,22.597726 24,17.768833 24,12.001455 24,5.3838955 18.616727,0 11.999709,0 Z", iconColor: "#ED7744", @@ -159,7 +156,6 @@ export const configSections: Record = { }, { path: "/config/bluetooth", - name: "Bluetooth", iconPath: mdiBluetooth, iconColor: "#0082FC", component: "bluetooth", @@ -167,7 +163,6 @@ export const configSections: Record = { }, { path: "/insteon", - name: "Insteon", iconPath: "m 12.001571,6.3842473 h 0.02973 c 3.652189,0 6.767389,-2.29456 7.987462,-5.5177193 L 15.389382,0 Z m 0,0 h -0.02972 c -3.6522186,0 -6.7673314,-2.2918546 -7.9874477,-5.5177193 h -0.00271 L 8.6111273,0 Z M 6.3840436,11.999287 v -0.02972 c 0,-3.6524074 -2.2944727,-6.7675928 -5.51754469,-7.9877383 L 0,8.6114473 Z m 0,0 v 0.02964 c 0,3.652378 -2.2917818,6.767578 -5.51754469,7.987796 v 0.0026 L 0,15.389818 Z M 24,8.6114473 23.133527,3.9818327 v 0.00269 C 19.907636,5.2046836 17.616,8.3198691 17.616,11.972276 v 0.02966 0.02972 0.0027 c 0,3.65232 2.2944,6.76752 5.517527,7.987738 L 24,15.392436 17.616,12.001935 Z M 20.018618,23.133527 15.389091,24 11.99872,17.615709 h 0.02964 c 3.652218,0 6.767418,2.291927 7.987491,5.517818 z M 11.99872,17.615709 8.6082618,24 3.9788364,23.133527 C 5.1989527,19.9104 8.3140655,17.615709 11.966284,17.615709 h 0.0027 z", iconColor: "#E4002C", @@ -197,6 +192,13 @@ export const configSections: Record = { iconColor: "#301ABE", core: true, }, + { + path: "/config/developer-tools", + translationKey: "developer_tools", + iconPath: mdiHammer, + iconColor: "#7A5AA6", + core: true, + }, { path: "/config/info", translationKey: "about", @@ -381,7 +383,7 @@ export const configSections: Record = { component: "logs", path: "/config/logs", translationKey: "logs", - iconPath: mdiMathLog, + iconPath: mdiTextBoxOutline, iconColor: "#C65326", core: true, }, @@ -398,6 +400,13 @@ export const configSections: Record = { iconPath: mdiShape, iconColor: "#f1c447", }, + { + path: "/config/ai-tasks", + translationKey: "ai_tasks", + iconPath: mdiStarFourPoints, + iconColor: "#8B69E3", + core: true, + }, { path: "/config/labs", translationKey: "labs", @@ -510,6 +519,11 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) { tag: "ha-config-system-navigation", load: () => import("./core/ha-config-system-navigation"), }, + "developer-tools": { + tag: "ha-panel-developer-tools", + load: () => import("./developer-tools/ha-panel-developer-tools"), + cache: true, + }, logs: { tag: "ha-config-logs", load: () => import("./logs/ha-config-logs"), @@ -596,6 +610,10 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) { tag: "ha-config-labs", load: () => import("./labs/ha-config-labs"), }, + "ai-tasks": { + tag: "ha-config-section-ai-tasks", + load: () => import("./core/ha-config-section-ai-tasks"), + }, zha: { tag: "zha-config-dashboard-router", load: () => @@ -646,6 +664,14 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) { load: () => import("./application_credentials/ha-config-application-credentials"), }, + apps: { + tag: "ha-config-apps", + load: () => import("./apps/ha-config-apps"), + }, + app: { + tag: "ha-config-app-dashboard", + load: () => import("./apps/ha-config-app-dashboard"), + }, }, }; diff --git a/src/panels/config/hardware/ha-config-hardware.ts b/src/panels/config/hardware/ha-config-hardware.ts index 81e9f174d7..7562c51c2d 100644 --- a/src/panels/config/hardware/ha-config-hardware.ts +++ b/src/panels/config/hardware/ha-config-hardware.ts @@ -12,10 +12,11 @@ import "../../../components/chart/ha-chart-base"; import "../../../components/ha-alert"; import "../../../components/ha-button"; import "../../../components/ha-card"; +import "../../../components/ha-fade-in"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-next"; import "../../../components/ha-md-list-item"; -import "../../../components/ha-settings-row"; +import "../../../components/ha-spinner"; import type { ConfigEntry } from "../../../data/config_entries"; import { subscribeConfigEntries } from "../../../data/config_entries"; import type { @@ -365,7 +366,7 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { })} ` : nothing} - ${this._systemStatusData + ${isComponentLoaded(this.hass, "hardware") ? html`
      @@ -374,16 +375,25 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { )}
      - ${this._systemStatusData.cpu_percent || - "-"}${blankBeforePercent(this.hass.locale)}% + ${this._systemStatusData + ? html`${this._systemStatusData + .cpu_percent}${blankBeforePercent( + this.hass.locale + )}%` + : "-"}
      -
      +
      + ${!this._systemStatusData + ? html` + + ` + : nothing}
      @@ -392,37 +402,38 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { ${this.hass.localize("ui.panel.config.hardware.memory")}
      - ${round(this._systemStatusData.memory_used_mb / 1024, 1)} - GB / - ${round( - (this._systemStatusData.memory_used_mb! + - this._systemStatusData.memory_free_mb!) / - 1024, - 0 - )} - GB + ${this._systemStatusData + ? html`${round( + this._systemStatusData.memory_used_mb / 1024, + 1 + )} + GB / + ${round( + (this._systemStatusData.memory_used_mb + + this._systemStatusData.memory_free_mb) / + 1024, + 0 + )} + GB` + : "-"}
      -
      +
      + ${!this._systemStatusData + ? html` + + + + ` + : nothing}
      ` - : isComponentLoaded(this.hass, "hardware") - ? html` -
      - - - ${this.hass.localize( - "ui.panel.config.hardware.loading_system_data" - )} - -
      -
      ` - : nothing} + : nothing}
      `; @@ -502,6 +513,22 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { flex-direction: column; padding: 16px; } + + .loading-container { + position: relative; + } + + .loading-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: rgba(var(--rgb-card-background-color), 0.75); + display: flex; + justify-content: center; + align-items: center; + } .card-content img { max-width: 300px; margin: auto; @@ -548,10 +575,6 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { ha-alert { --ha-alert-icon-size: 24px; } - - ha-alert ha-spinner { - --ha-spinner-size: 24px; - } `, ]; } diff --git a/src/panels/config/helpers/dialog-helper-detail.ts b/src/panels/config/helpers/dialog-helper-detail.ts index c109a4c91c..95614c39fd 100644 --- a/src/panels/config/helpers/dialog-helper-detail.ts +++ b/src/panels/config/helpers/dialog-helper-detail.ts @@ -1,21 +1,22 @@ import { mdiAlertOutline } from "@mdi/js"; import type { CSSResultGroup, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; -import { classMap } from "lit/directives/class-map"; +import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { dynamicElement } from "../../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../../common/dom/fire_event"; import { stopPropagation } from "../../../common/dom/stop_propagation"; import { stringCompare } from "../../../common/string/compare"; -import { createCloseHeading } from "../../../components/ha-dialog"; import "../../../components/ha-list"; import "../../../components/ha-button"; +import "../../../components/ha-dialog-footer"; import "../../../components/ha-list-item"; import "../../../components/ha-spinner"; import "../../../components/ha-svg-icon"; import "../../../components/ha-tooltip"; +import "../../../components/ha-wa-dialog"; +import "../../../components/search-input"; import { getConfigFlowHandlers } from "../../../data/config_flow"; import { createCounter } from "../../../data/counter"; import { createInputBoolean } from "../../../data/input_boolean"; @@ -27,6 +28,7 @@ import { createInputText } from "../../../data/input_text"; import { domainToName, fetchIntegrationManifest, + type IntegrationManifest, } from "../../../data/integration"; import { createSchedule } from "../../../data/schedule"; import { createTimer } from "../../../data/timer"; @@ -37,6 +39,7 @@ import { brandsUrl } from "../../../util/brands-url"; import type { Helper, HelperDomain } from "./const"; import { isHelperDomain } from "./const"; import type { ShowDialogHelperDetailParams } from "./show-dialog-helper-detail"; +import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; type HelperCreators = Record< HelperDomain, @@ -102,7 +105,7 @@ export class DialogHelperDetail extends LitElement { @state() private _item?: Helper; - @state() private _opened = false; + @state() private _open = false; @state() private _domain?: string; @@ -110,14 +113,18 @@ export class DialogHelperDetail extends LitElement { @state() private _submitting = false; - @query(".form") private _form?: HTMLDivElement; - @state() private _helperFlows?: string[]; @state() private _loading = false; @state() private _filter?: string; + private _pendingConfigFlow?: { + startFlowHandler: string; + manifest: IntegrationManifest; + dialogClosedCallback?: ShowDialogHelperDetailParams["dialogClosedCallback"]; + }; + private _params?: ShowDialogHelperDetailParams; public async showDialog(params: ShowDialogHelperDetailParams): Promise { @@ -127,29 +134,47 @@ export class DialogHelperDetail extends LitElement { if (this._domain && this._domain in HELPERS) { await HELPERS[this._domain].import(); } - this._opened = true; + this._open = true; await this.updateComplete; this.hass.loadFragmentTranslation("config"); const flows = await getConfigFlowHandlers(this.hass, ["helper"]); await this.hass.loadBackendTranslation("title", flows, true); // Ensure the titles are loaded before we render the flows. this._helperFlows = flows; + + await this.updateComplete; + await this._focusSearchInput(); } public closeDialog(): void { - this._opened = false; + this._open = false; + } + + private _dialogClosed(): void { + this._open = false; this._error = undefined; this._domain = undefined; this._params = undefined; this._filter = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); + + if (this._pendingConfigFlow) { + const pendingConfigFlow = this._pendingConfigFlow; + this._pendingConfigFlow = undefined; + showConfigFlowDialog(this, { + startFlowHandler: pendingConfigFlow.startFlowHandler, + manifest: pendingConfigFlow.manifest, + dialogClosedCallback: pendingConfigFlow.dialogClosedCallback, + }); + } } protected render() { - if (!this._opened) { + if (!this._params) { return nothing; } let content: TemplateResult; + let footer: TemplateResult | typeof nothing = nothing; if (this._domain) { content = html` @@ -159,25 +184,30 @@ export class DialogHelperDetail extends LitElement { hass: this.hass, item: this._item, new: true, + autofocus: true, })}
      - - ${this.hass!.localize("ui.panel.config.helpers.dialog.create")} - - ${this._params?.domain - ? nothing - : html` - ${this.hass!.localize("ui.common.back")} - `} + `; + footer = html` + + ${this._params?.domain + ? nothing + : html` + ${this.hass!.localize("ui.common.back")} + `} + + ${this.hass!.localize("ui.panel.config.helpers.dialog.create")} + + `; } else if (this._loading || this._helperFlows === undefined) { content = html``; @@ -190,8 +220,8 @@ export class DialogHelperDetail extends LitElement { content = html` ${items.map(([domain, label]) => { // Only OG helpers need to be loaded prior adding one @@ -214,7 +243,6 @@ export class DialogHelperDetail extends LitElement { !(domain in HELPERS) || isComponentLoaded(this.hass, domain); return html` ${label} ${isLoaded ? html`` - : html` - ${content} - + ${content} ${footer} + `; } @@ -361,6 +385,17 @@ export class DialogHelperDetail extends LitElement { private async _domainPicked(ev): Promise { const domain = ev.target.closest("ha-list-item").domain; + const isLoaded = + !(domain in HELPERS) || isComponentLoaded(this.hass, domain); + if (!isLoaded) { + showAlertDialog(this, { + text: this.hass.localize( + "ui.dialogs.helper_settings.platform_not_loaded", + { platform: domain } + ), + }); + return; + } if (domain in HELPERS) { this._loading = true; @@ -370,26 +405,35 @@ export class DialogHelperDetail extends LitElement { } finally { this._loading = false; } - this._focusForm(); } else { - showConfigFlowDialog(this, { + this._pendingConfigFlow = { startFlowHandler: domain, manifest: await fetchIntegrationManifest(this.hass, domain), - dialogClosedCallback: this._params!.dialogClosedCallback, - }); + dialogClosedCallback: this._params?.dialogClosedCallback, + }; this.closeDialog(); } } - private async _focusForm(): Promise { - await this.updateComplete; - (this._form?.lastElementChild as HTMLElement).focus(); - } - - private _goBack() { + private async _goBack() { this._domain = undefined; this._item = undefined; this._error = undefined; + await this.updateComplete; + await this._focusSearchInput(); + } + + private async _focusSearchInput() { + const searchInput = this.shadowRoot?.querySelector("search-input") as + | (HTMLElement & { updateComplete?: Promise }) + | null; + + if (!searchInput) { + return; + } + + await searchInput.updateComplete; + searchInput.focus(); } static get styles(): CSSResultGroup { @@ -397,31 +441,21 @@ export class DialogHelperDetail extends LitElement { haStyleScrollbar, haStyleDialog, css` - ha-dialog.button-left { - --justify-action-buttons: flex-start; - } - ha-dialog { + ha-wa-dialog { --dialog-content-padding: 0; - --dialog-scroll-divider-color: transparent; - --mdc-dialog-max-height: 90vh; - } - @media all and (min-width: 550px) { - ha-dialog { - --mdc-dialog-min-width: 500px; - } } ha-icon-next { - width: 24px; + width: var(--ha-space-6); } ha-tooltip { pointer-events: auto; } .form { - padding: 24px; + padding: var(--ha-space-6); } search-input { display: block; - margin: 16px 16px 0; + margin: 0 var(--ha-space-4) 0; } ha-list { height: calc(60vh - 184px); diff --git a/src/panels/config/helpers/forms/dialog-schedule-block-info.ts b/src/panels/config/helpers/forms/dialog-schedule-block-info.ts index df53e0859d..d59f202fc5 100644 --- a/src/panels/config/helpers/forms/dialog-schedule-block-info.ts +++ b/src/panels/config/helpers/forms/dialog-schedule-block-info.ts @@ -3,7 +3,8 @@ import { html, LitElement, nothing } from "lit"; import memoizeOne from "memoize-one"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { createCloseHeading } from "../../../../components/ha-dialog"; +import "../../../../components/ha-dialog-footer"; +import "../../../../components/ha-wa-dialog"; import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-button"; import { haStyleDialog } from "../../../../resources/styles"; @@ -24,6 +25,8 @@ class DialogScheduleBlockInfo extends LitElement { @state() private _params?: ScheduleBlockInfoDialogParams; + @state() private _open = false; + private _expand = false; private _schema = memoizeOne((expand: boolean) => [ @@ -57,9 +60,14 @@ class DialogScheduleBlockInfo extends LitElement { this._error = undefined; this._data = params.block; this._expand = !!params.block?.data; + this._open = true; } public closeDialog(): void { + this._open = false; + } + + private _dialogClosed(): void { this._params = undefined; this._data = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); @@ -71,18 +79,17 @@ class DialogScheduleBlockInfo extends LitElement { } return html` -
      - - ${this.hass!.localize("ui.common.delete")} - - - ${this.hass!.localize("ui.common.save")} - -
      + + + ${this.hass!.localize("ui.common.delete")} + + + ${this.hass!.localize("ui.common.save")} + + + `; } diff --git a/src/panels/config/helpers/ha-config-helpers.ts b/src/panels/config/helpers/ha-config-helpers.ts index 8c614e9404..883730828a 100644 --- a/src/panels/config/helpers/ha-config-helpers.ts +++ b/src/panels/config/helpers/ha-config-helpers.ts @@ -1,15 +1,14 @@ +import "@home-assistant/webawesome/dist/components/divider/divider"; import { ResizeController } from "@lit-labs/observers/resize-controller"; import { consume } from "@lit/context"; import { mdiAlertCircle, mdiCancel, - mdiChevronRight, mdiCog, mdiDelete, mdiDotsVertical, mdiDownload, mdiMenuDown, - mdiPencilOff, mdiPlus, mdiProgressHelper, mdiTag, @@ -27,7 +26,6 @@ import type { HASSDomEvent } from "../../../common/dom/fire_event"; import { computeAreaName } from "../../../common/entity/compute_area_name"; import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { navigate } from "../../../common/navigate"; -import { slugify } from "../../../common/string/slugify"; import type { LocalizeFunc, LocalizeKeys, @@ -45,6 +43,8 @@ import type { SortingChangedEvent, } from "../../../components/data-table/ha-data-table"; import "../../../components/data-table/ha-data-table-labels"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-fab"; import "../../../components/ha-filter-categories"; import "../../../components/ha-filter-devices"; @@ -54,7 +54,6 @@ import "../../../components/ha-filter-labels"; import "../../../components/ha-filter-voice-assistants"; import "../../../components/ha-icon"; import "../../../components/ha-icon-overflow-menu"; -import "../../../components/ha-md-divider"; import "../../../components/ha-state-icon"; import "../../../components/ha-svg-icon"; import "../../../components/ha-tooltip"; @@ -91,6 +90,7 @@ import { updateEntityRegistryEntry, } from "../../../data/entity/entity_registry"; import { fetchEntitySourcesWithCache } from "../../../data/entity/entity_sources"; +import { getEntityVoiceAssistantsIds } from "../../../data/expose"; import { HELPERS_CRUD } from "../../../data/helpers_crud"; import type { IntegrationManifest } from "../../../data/integration"; import { @@ -116,20 +116,27 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant, Route } from "../../../types"; import { fileDownload } from "../../../util/file_download"; +import { + getEntityIdTableColumn, + getAreaTableColumn, + getCategoryTableColumn, + getLabelsTableColumn, + getEditableTableColumn, +} from "../common/data-table-columns"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; import { configSections } from "../ha-panel-config"; import { renderConfigEntryError } from "../integrations/ha-config-integration-page"; import "../integrations/ha-integration-overflow-menu"; import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; +import { + getAssistantsSortableKey, + getAssistantsTableColumn, +} from "../voice-assistants/expose/assistants-table-column"; +import { getAvailableAssistants } from "../voice-assistants/expose/available-assistants"; import { isHelperDomain, type HelperDomain } from "./const"; import { showHelperDetailDialog } from "./show-dialog-helper-detail"; -import { getEntityVoiceAssistantsIds } from "../../../data/expose"; -import { getAvailableAssistants } from "../voice-assistants/expose/available-assistants"; -import { - getAssistantsTableColumn, - getAssistantsSortableKey, -} from "../voice-assistants/expose/assistants-table-column"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; interface HelperItem { id: string; @@ -358,68 +365,20 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { ` : nothing, }, - entity_id: { - title: localize("ui.panel.config.helpers.picker.headers.entity_id"), - sortable: true, - filterable: true, - }, - category: { - title: localize("ui.panel.config.helpers.picker.headers.category"), - hidden: true, - groupable: true, - filterable: true, - sortable: true, - }, - area: { - title: localize("ui.panel.config.helpers.picker.headers.area"), - sortable: true, - filterable: true, - groupable: true, - template: (helper) => helper.area || "—", - }, - labels: { - title: "", - hidden: true, - filterable: true, - template: (helper) => - helper.label_entries.map((lbl) => lbl.name).join(" "), - }, + entity_id: getEntityIdTableColumn(localize), + category: getCategoryTableColumn(localize), + area: getAreaTableColumn(localize), + labels: getLabelsTableColumn(), localized_type: { title: localize("ui.panel.config.helpers.picker.headers.type"), sortable: true, filterable: true, groupable: true, }, - editable: { - title: localize("ui.panel.config.helpers.picker.headers.editable"), - type: "icon", - sortable: true, - minWidth: "88px", - maxWidth: "88px", - showNarrow: true, - template: (helper) => html` - ${!helper.editable - ? html` -
      - - ${this.hass.localize( - "ui.panel.config.entities.picker.status.unmanageable" - )} - -
      - ` - : ""} - `, - }, + editable: getEditableTableColumn( + localize, + localize("ui.panel.config.entities.picker.status.unmanageable") + ), actions: { title: "", label: this.hass.localize("ui.panel.config.generic.headers.actions"), @@ -601,7 +560,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { entityRegistryByEntityId(entityReg)[item.entity_id]; const labels = labelReg && entityRegEntry?.labels; const category = entityRegEntry?.categories.helpers; - const areaId = entityRegEntry?.area_id; + const deviceId = entityRegEntry?.device_id; + const areaId = + entityRegEntry?.area_id || this.hass.devices?.[deviceId!]?.area_id; const area = areaId && this.hass.areas?.[areaId] ? computeAreaName(this.hass.areas[areaId]) @@ -650,69 +611,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { return html``; } - const categoryItems = html`${this._categories?.map( - (category) => - html` - ${category.icon - ? html`` - : html``} -
      ${category.name}
      -
      ` - )} - -
      - ${this.hass.localize( - "ui.panel.config.automation.picker.bulk_actions.no_category" - )} -
      -
      - - -
      - ${this.hass.localize("ui.panel.config.category.editor.add")} -
      -
      `; - const labelItems = html`${this._labels?.map((label) => { - const color = label.color ? computeCssColor(label.color) : undefined; - const selected = this._selected.every((entityId) => - this._labelsForEntity(entityId).includes(label.label_id) - ); - const partial = - !selected && - this._selected.some((entityId) => - this._labelsForEntity(entityId).includes(label.label_id) - ); - return html` - - - ${label.icon - ? html`` - : nothing} - ${label.name} - - `; - })} - -
      - ${this.hass.localize("ui.panel.config.labels.add_label")} -
      -
      `; const labelsInOverflow = (this._sizeController.value && this._sizeController.value < 700) || (!this._sizeController.value && this.hass.dockedSidebar === "docked"); @@ -823,7 +721,10 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { > ${!this.narrow - ? html` + ? html`
      - ${categoryItems} - + ${this._renderCategoryItems()} + ${labelsInOverflow ? nothing - : html` + : html` - ${labelItems} - `}` + ${this._renderLabelItems()} + `}` : nothing} ${this.narrow || labelsInOverflow - ? html` - - ${ - this.narrow + ? html` + ${this.narrow ? html`` - } - - ${ - this.narrow - ? html` - -
      - ${this.hass.localize( - "ui.panel.config.automation.picker.bulk_actions.move_category" - )} -
      - -
      - ${categoryItems} -
      ` - : nothing - } - ${ - this.narrow || this.hass.dockedSidebar === "docked" - ? html` - -
      - ${this.hass.localize( - "ui.panel.config.automation.picker.bulk_actions.add_label" - )} -
      - -
      - ${labelItems} -
      ` - : nothing - } -
      ` + >`} + ${this.narrow + ? html` + ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.move_category" + )} + ${this._renderCategoryItems("submenu")} + ` + : nothing} + ${this.narrow || this.hass.dockedSidebar === "docked" + ? html` + ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.add_label" + )} + ${this._renderLabelItems("submenu")} + ` + : nothing} + ` : nothing} { - const category = item.value; - this._bulkAddCategory(category); + private _handleBulkCategory = (ev: HaDropdownSelectEvent) => { + const value = ev.detail.item.value; + if (value === "category_create") { + this._bulkCreateCategory(); + return; + } + if (value === "category_none") { + this._bulkAddCategory(null); + return; + } + if (value?.startsWith("category_")) { + this._bulkAddCategory(value.substring(9)); + } }; - private async _bulkAddCategory(category: string) { + private async _bulkAddCategory(category: string | null) { const promises: Promise[] = []; this._selected.forEach((entityId) => { promises.push( @@ -1166,11 +1055,18 @@ ${rejected } } - private async _handleBulkLabel(ev) { - const label = ev.currentTarget.value; - const action = ev.currentTarget.action; - this._bulkLabel(label, action); - } + private _handleBulkLabel = (ev: HaDropdownSelectEvent) => { + ev.preventDefault(); + const value = ev.detail.item.value; + if (value === "label_create") { + this._bulkCreateLabel(); + return; + } + if (value?.startsWith("label_")) { + const action = (ev.detail.item as any).action; + this._bulkLabel(value.substring(6), action); + } + }; private async _bulkLabel(label: string, action: "add" | "remove") { const promises: Promise[] = []; @@ -1514,6 +1410,96 @@ ${rejected }); }; + private _renderCategoryItems = (slot = "") => + html`${this._categories?.map( + (category) => + html` + ${category.icon + ? html`` + : html``} + ${category.name} + ` + )} + + ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.no_category" + )} + + + + ${this.hass.localize("ui.panel.config.category.editor.add")} + `; + + private _renderLabelItems = (slot = "") => + html`${this._labels?.map((label) => { + const color = label.color ? computeCssColor(label.color) : undefined; + const selected = this._selected.every((entityId) => + this._labelsForEntity(entityId).includes(label.label_id) + ); + const partial = + !selected && + this._selected.some((entityId) => + this._labelsForEntity(entityId).includes(label.label_id) + ); + return html` + + + ${label.icon + ? html`` + : nothing} + ${label.name} + + `; + })} + + + ${this.hass.localize("ui.panel.config.labels.add_label")} + `; + + private _handleBulkAction = (ev) => { + const item = ev.detail.item; + const value = item.value; + + if (!value) { + return; + } + + if (value.startsWith("category_")) { + if (value === "category_create") { + this._bulkCreateCategory(); + } else if (value === "category_none") { + this._bulkAddCategory(null); + } else { + this._bulkAddCategory(value.substring(9)); + } + return; + } + + if (value.startsWith("label_")) { + if (value === "label_create") { + this._bulkCreateLabel(); + } else { + const action = item.action; + this._bulkLabel(value.substring(6), action); + } + } + }; + private _handleSortingChanged(ev: CustomEvent) { this._activeSorting = ev.detail; } @@ -1551,7 +1537,11 @@ ${rejected ha-assist-chip { --ha-assist-chip-container-shape: 10px; } - ha-md-button-menu ha-assist-chip { + ha-dropdown::part(menu), + ha-dropdown::part(submenu) { + --auto-size-available-width: calc(50vw - var(--ha-space-4)); + } + ha-dropdown ha-assist-chip { --md-assist-chip-trailing-space: 8px; } ha-label { diff --git a/src/panels/config/integrations/dialog-add-integration.ts b/src/panels/config/integrations/dialog-add-integration.ts index 3cfdc8531b..0351c34af8 100644 --- a/src/panels/config/integrations/dialog-add-integration.ts +++ b/src/panels/config/integrations/dialog-add-integration.ts @@ -1,3 +1,4 @@ +import { mdiClose } from "@mdi/js"; import type { IFuseOptions } from "fuse.js"; import Fuse from "fuse.js"; import type { HassConfig } from "home-assistant-js-websocket"; @@ -22,7 +23,10 @@ import "../../../components/ha-list"; import "../../../components/ha-spinner"; import "../../../components/search-input"; import { getConfigEntries } from "../../../data/config_entries"; -import { fetchConfigFlowInProgress } from "../../../data/config_flow"; +import { + DISCOVERY_SOURCES, + fetchConfigFlowInProgress, +} from "../../../data/config_flow"; import type { DataEntryFlowProgress } from "../../../data/data_entry_flow"; import { domainToName, @@ -65,6 +69,7 @@ export interface IntegrationListItem { overwrites_built_in?: boolean; is_add?: boolean; single_config_entry?: boolean; + is_discovered?: boolean; } @customElement("dialog-add-integration") @@ -85,6 +90,12 @@ class AddIntegrationDialog extends LitElement { @state() private _flowsInProgress?: DataEntryFlowProgress[]; + @state() private _showDiscovered = false; + + @state() private _openedDirectly = false; + + @state() private _navigateToResult = false; + @state() private _open = false; @state() private _narrow = false; @@ -95,23 +106,38 @@ class AddIntegrationDialog extends LitElement { public async showDialog(params?: AddIntegrationDialogParams): Promise { const loadPromise = this._load(); - if (params?.domain) { - // Just open the config flow dialog, do not show this dialog - await this._createFlow(params.domain); - return; - } - if (params?.brand) { + if (params?.domain) { + // If we get here we clicked the button to add an entry for a specific integration + // If there is discovery in process, show this dialog to select a new flow + // or continue an existing flow. + // If no flow in process, just open the config flow dialog directly await loadPromise; - const brand = this._integrations?.[params.brand]; - if (brand && "integrations" in brand && brand.integrations) { - this._fetchFlowsInProgress(Object.keys(brand.integrations)); + const flowsInProgress = this._getFlowsInProgressForDomains([ + params.domain, + ]); + + if (!flowsInProgress.length) { + await this._createFlow(params.domain); + return; } } - // Only open the dialog if no domain is provided + + if (params?.brand === "_discovered") { + // Wait for load to complete before showing discovered flows + await loadPromise; + this._showDiscovered = true; + } + + // Only open the dialog if no domain is provided or we need to select a flow this._open = true; - this._pickedBrand = params?.brand; + this._pickedBrand = + params?.brand === "_discovered" + ? undefined + : params?.domain || params?.brand; + this._openedDirectly = !!(params?.brand || params?.domain); this._initialFilter = params?.initialFilter; + this._navigateToResult = params?.navigateToResult ?? false; this._narrow = matchMedia( "all and (max-width: 450px), all and (max-height: 500px)" ).matches; @@ -124,6 +150,9 @@ class AddIntegrationDialog extends LitElement { this._pickedBrand = undefined; this._prevPickedBrand = undefined; this._flowsInProgress = undefined; + this._showDiscovered = false; + this._openedDirectly = false; + this._navigateToResult = false; this._filter = undefined; this._width = undefined; this._height = undefined; @@ -165,8 +194,26 @@ class AddIntegrationDialog extends LitElement { h: Integrations, components: HassConfig["components"], localize: LocalizeFunc, + discoveredFlowsCount: number, filter?: string ): IntegrationListItem[] => { + // Create a single discovered devices row if there are any discovered flows + const discoveredRows: IntegrationListItem[] = + discoveredFlowsCount > 0 + ? [ + { + name: localize( + "ui.panel.config.integrations.discovered_devices", + { count: discoveredFlowsCount } + ), + domain: "_discovered", + config_flow: true, + is_built_in: true, + is_discovered: true, + }, + ] + : []; + const addDeviceRows: IntegrationListItem[] = PROTOCOL_INTEGRATIONS.filter( (domain) => components.includes(domain) ) @@ -289,6 +336,7 @@ class AddIntegrationDialog extends LitElement { ]; } return [ + ...discoveredRows, ...addDeviceRows, ...integrations.sort((a, b) => caseInsensitiveStringCompare( @@ -307,6 +355,7 @@ class AddIntegrationDialog extends LitElement { this._helpers!, this.hass.config.components, this.hass.localize, + this._flowsInProgress?.length ?? 0, this._filter ); } @@ -333,59 +382,92 @@ class AddIntegrationDialog extends LitElement { this.hass.localize("ui.panel.config.integrations.new") )} > - ${this._pickedBrand && (!this._integrations || pickedIntegration) - ? html`
      - -

      - ${this._calculateBrandHeading(pickedIntegration)} -

      -
      - ${this._renderIntegration(pickedIntegration)}` + ${(this._pickedBrand && (!this._integrations || pickedIntegration)) || + this._showDiscovered + ? this._renderBrandView(pickedIntegration) : this._renderAll(integrations)} `; } - private _calculateBrandHeading(integration: Brand | Integration | undefined) { + private _getFlowsForCurrentView( + integration: Brand | Integration | undefined + ): DataEntryFlowProgress[] { + if (this._showDiscovered) { + // Show all discovered flows + return this._flowsInProgress || []; + } + if (!this._pickedBrand || !integration) { + return []; + } + // Get domains for this brand + let domains: string[] = []; + if ("integrations" in integration && integration.integrations) { + domains = Object.keys(integration.integrations); + if (this._pickedBrand === "apple") { + // we show discovered homekit devices in their own brand section, dont show them in apple + domains = domains.filter((domain) => domain !== "homekit_controller"); + } + } else { + domains = [this._pickedBrand]; + } + return this._getFlowsInProgressForDomains(domains); + } + + private _renderBrandView( + integration: Brand | Integration | undefined + ): TemplateResult { + const flowsInProgress = this._getFlowsForCurrentView(integration); + + let heading: string; if ( integration?.iot_standards && !("integrations" in integration) && - !this._flowsInProgress?.length + !flowsInProgress.length ) { - return this.hass.localize( + heading = this.hass.localize( "ui.panel.config.integrations.what_device_type" ); - } - if ( + } else if ( integration && !integration?.iot_standards && !("integrations" in integration) && - this._flowsInProgress?.length + flowsInProgress.length ) { - return this.hass.localize( + heading = this.hass.localize( "ui.panel.config.integrations.confirm_add_discovered" ); + } else { + heading = this.hass.localize("ui.panel.config.integrations.what_to_add"); } - return this.hass.localize("ui.panel.config.integrations.what_to_add"); - } - private _renderIntegration( - integration: Brand | Integration | undefined - ): TemplateResult { - return html``; + return html`
      + ${this._openedDirectly + ? html`` + : html``} +

      ${heading}

      +
      + `; } private _handleSelectBrandEvent(ev: CustomEvent) { @@ -496,7 +578,24 @@ class AddIntegrationDialog extends LitElement { }; private async _load() { - const descriptions = await getIntegrationDescriptions(this.hass); + const [descriptions, flowsInProgress] = await Promise.all([ + getIntegrationDescriptions(this.hass), + fetchConfigFlowInProgress(this.hass.connection), + ]); + + // Filter discovered flows + this._flowsInProgress = flowsInProgress.filter((flow) => + DISCOVERY_SOURCES.includes(flow.context.source) + ); + + // Load translations for discovered flow handlers + if (this._flowsInProgress.length) { + const discoveredHandlers = [ + ...new Set(this._flowsInProgress.map((flow) => flow.handler)), + ]; + await this.hass.loadBackendTranslation("title", discoveredHandlers, true); + } + for (const integration in descriptions.custom.integration) { if ( !Object.prototype.hasOwnProperty.call( @@ -558,6 +657,12 @@ class AddIntegrationDialog extends LitElement { return; } + if (integration.is_discovered) { + // Show all discovered flows + this._showDiscovered = true; + return; + } + if (integration.is_add) { protocolIntegrationPicked(this, this.hass, integration.domain); this.closeDialog(); @@ -571,12 +676,6 @@ class AddIntegrationDialog extends LitElement { } if (integration.integrations) { - let domains = integration.domains || []; - if (integration.domain === "apple") { - // we show discovered homekit devices in their own brand section, dont show them in apple - domains = domains.filter((domain) => domain !== "homekit_controller"); - } - this._fetchFlowsInProgress(domains); this._pickedBrand = integration.domain; return; } @@ -652,9 +751,9 @@ class AddIntegrationDialog extends LitElement { } private async _createFlow(domain: string) { - const flowsInProgress = await this._fetchFlowsInProgress([domain]); + const flowsInProgress = this._getFlowsInProgressForDomains([domain]); - if (flowsInProgress?.length) { + if (flowsInProgress.length) { this._pickedBrand = domain; return; } @@ -667,14 +766,15 @@ class AddIntegrationDialog extends LitElement { startFlowHandler: domain, showAdvanced: this.hass.userData?.showAdvanced, manifest, - navigateToResult: true, + navigateToResult: this._navigateToResult, }); } - private async _fetchFlowsInProgress(domains: string[]) { - const flowsInProgress = ( - await fetchConfigFlowInProgress(this.hass.connection) - ).filter( + private _getFlowsInProgressForDomains(domains: string[]) { + if (!this._flowsInProgress) { + return []; + } + return this._flowsInProgress.filter( (flow) => // filter config flows that are not for the integration we are looking for domains.includes(flow.handler) || @@ -682,11 +782,6 @@ class AddIntegrationDialog extends LitElement { ("alternative_domain" in flow.context && domains.includes(flow.context.alternative_domain)) ); - - if (flowsInProgress.length) { - this._flowsInProgress = flowsInProgress; - } - return flowsInProgress; } private _maybeSubmit(ev: KeyboardEvent) { @@ -702,10 +797,11 @@ class AddIntegrationDialog extends LitElement { } private _prevClicked() { - this._pickedBrand = this._prevPickedBrand; - if (!this._prevPickedBrand) { - this._flowsInProgress = undefined; + if (this._showDiscovered) { + this._showDiscovered = false; + return; } + this._pickedBrand = this._prevPickedBrand; this._prevPickedBrand = undefined; } @@ -757,7 +853,8 @@ class AddIntegrationDialog extends LitElement { ha-integration-list-item { width: 100%; } - ha-icon-button-prev { + ha-icon-button-prev, + .header-close-button { color: var(--secondary-text-color); position: absolute; left: 16px; diff --git a/src/panels/config/integrations/dialog-pick-config-entry.ts b/src/panels/config/integrations/dialog-pick-config-entry.ts index 218e180f42..d7972be349 100644 --- a/src/panels/config/integrations/dialog-pick-config-entry.ts +++ b/src/panels/config/integrations/dialog-pick-config-entry.ts @@ -1,13 +1,9 @@ -import { mdiClose } from "@mdi/js"; import { LitElement, css, html, nothing } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; -import "../../../components/ha-dialog-header"; -import "../../../components/ha-icon-button"; -import "../../../components/ha-md-dialog"; -import type { HaMdDialog } from "../../../components/ha-md-dialog"; import "../../../components/ha-md-list"; import "../../../components/ha-md-list-item"; +import "../../../components/ha-wa-dialog"; import { ERROR_STATES, RECOVERABLE_STATES } from "../../../data/config_entries"; import type { HomeAssistant } from "../../../types"; import type { PickConfigEntryDialogParams } from "./show-pick-config-entry-dialog"; @@ -18,10 +14,11 @@ export class DialogPickConfigEntry extends LitElement { @state() private _params?: PickConfigEntryDialogParams; - @query("ha-md-dialog") private _dialog?: HaMdDialog; + @state() private _open = false; public showDialog(params: PickConfigEntryDialogParams): void { this._params = params; + this._open = true; } private _dialogClosed(): void { @@ -29,35 +26,25 @@ export class DialogPickConfigEntry extends LitElement { fireEvent(this, "dialog-closed", { dialog: this.localName }); } - public closeDialog() { - this._dialog?.close(); - return true; + public closeDialog(): void { + this._open = false; } protected render() { if (!this._params) { return nothing; } + const title = this.hass.localize( + `component.${this._params.domain}.config_subentries.${this._params.subFlowType}.initiate_flow.user` + ); return html` - - - - ${this.hass.localize( - `component.${this._params.domain}.config_subentries.${this._params.subFlowType}.initiate_flow.user` - )} - - + + ${this._params.configEntries.map( (entry) => html`` )} - + `; } @@ -80,14 +67,9 @@ export class DialogPickConfigEntry extends LitElement { } static styles = css` - :host { + ha-wa-dialog { --dialog-content-padding: 0; } - @media all and (min-width: 600px) { - ha-dialog { - --mdc-dialog-min-width: 400px; - } - } `; } diff --git a/src/panels/config/integrations/ha-config-entry-device-row.ts b/src/panels/config/integrations/ha-config-entry-device-row.ts index eb9d2bc327..7ae3ae058f 100644 --- a/src/panels/config/integrations/ha-config-entry-device-row.ts +++ b/src/panels/config/integrations/ha-config-entry-device-row.ts @@ -14,6 +14,8 @@ import { stopPropagation } from "../../../common/dom/stop_propagation"; import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name"; import { getDeviceContext } from "../../../common/entity/context/get_device_context"; import { navigate } from "../../../common/navigate"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import { disableConfigEntry, type ConfigEntry, @@ -95,10 +97,10 @@ class HaConfigEntryDeviceRow extends LitElement { >` : nothing} - ${this.narrow - ? html` - + ? html` + ${this.hass.localize( "ui.panel.config.integrations.config_entry.device.edit" )} - ` + ` : nothing} ${entities.length ? html` - - + + ${this.hass.localize( `ui.panel.config.integrations.config_entry.entities`, { count: entities.length } )} - - + + ` : nothing} - - + ${device.disabled_by && device.disabled_by !== "user" ? this.hass.localize( @@ -156,25 +155,40 @@ class HaConfigEntryDeviceRow extends LitElement { : this.hass.localize( "ui.panel.config.integrations.config_entry.device.disable" )} - + ${this.entry.supports_remove_device - ? html` - + ? html` + ${this.hass.localize( "ui.panel.config.integrations.config_entry.device.delete" )} - ` + ` : nothing} - + `; } private _getEntities = (): EntityRegistryEntry[] => this.entities?.filter((entity) => entity.device_id === this.device.id); + private _handleMenuAction = (ev: CustomEvent) => { + ev.stopPropagation(); + const value = ev.detail.item.value; + switch (value) { + case "edit": + this._handleEditDevice(); + return; + case "entities": + this._handleNavigateToEntities(); + return; + case "disable": + this._doDisableDevice(); + return; + case "delete": + this._handleDeleteDevice(); + } + }; + private _handleEditDeviceButton(ev: MouseEvent) { ev.stopPropagation(); // Prevent triggering the click handler on the list item this._handleEditDevice(); @@ -193,7 +207,7 @@ class HaConfigEntryDeviceRow extends LitElement { navigate(`/config/entities/?historyBack=1&device=${this.device.id}`); }; - private _handleDisableDevice = async () => { + private _doDisableDevice = async () => { const disable = this.device.disabled_by === null; if (disable) { @@ -205,10 +219,8 @@ class HaConfigEntryDeviceRow extends LitElement { ) ) { const config_entry = this.entry; - if ( - config_entry && - !config_entry.disabled_by && - (await showConfirmationDialog(this, { + if (config_entry && !config_entry.disabled_by) { + const confirm = await showConfirmationDialog(this, { title: this.hass.localize( "ui.panel.config.devices.confirm_disable_config_entry_title" ), @@ -219,8 +231,12 @@ class HaConfigEntryDeviceRow extends LitElement { destructive: true, confirmText: this.hass.localize("ui.common.yes"), dismissText: this.hass.localize("ui.common.no"), - })) - ) { + }); + + if (!confirm) { + return; + } + let result: DisableConfigEntryResult; try { result = await disableConfigEntry(this.hass, this.entry.entry_id); diff --git a/src/panels/config/integrations/ha-config-entry-row.ts b/src/panels/config/integrations/ha-config-entry-row.ts index 64be533665..32eef06659 100644 --- a/src/panels/config/integrations/ha-config-entry-row.ts +++ b/src/panels/config/integrations/ha-config-entry-row.ts @@ -26,6 +26,8 @@ import memoizeOne from "memoize-one"; import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name"; import { caseInsensitiveStringCompare } from "../../../common/string/compare"; import { copyToClipboard } from "../../../common/util/copy-clipboard"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import { deleteApplicationCredential, fetchApplicationCredentialsConfigEntry, @@ -73,6 +75,7 @@ import { import "./ha-config-entry-device-row"; import { renderConfigEntryError } from "./ha-config-integration-page"; import "./ha-config-sub-entry-row"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; @customElement("ha-config-entry-row") class HaConfigEntryRow extends LitElement { @@ -237,7 +240,7 @@ class HaConfigEntryRow extends LitElement { ` : nothing} - + ${devices.length ? html` - - - ${this.hass.localize( - `ui.panel.config.integrations.config_entry.devices`, - { count: devices.length } - )} - - + + + ${this.hass.localize( + `ui.panel.config.integrations.config_entry.devices`, + { count: devices.length } + )} + + + ` : nothing} ${services.length - ? html` - - ${this.hass.localize( - `ui.panel.config.integrations.config_entry.services`, - { count: services.length } - )} - - ` + ? html` + + + + ${this.hass.localize( + `ui.panel.config.integrations.config_entry.services`, + { count: services.length } + )} + + + + ` : nothing} ${entities.length ? html` - - - ${this.hass.localize( - `ui.panel.config.integrations.config_entry.entities`, - { count: entities.length } - )} - - + + + ${this.hass.localize( + `ui.panel.config.integrations.config_entry.entities`, + { count: entities.length } + )} + + + ` : nothing} ${!item.disabled_by && @@ -298,126 +309,108 @@ class HaConfigEntryRow extends LitElement { item.supports_unload && item.source !== "system" ? html` - - + + ${this.hass.localize( "ui.panel.config.integrations.config_entry.reload" )} - + ` : nothing} - - + + ${this.hass.localize( "ui.panel.config.integrations.config_entry.rename" )} - + - - + + ${this.hass.localize( "ui.panel.config.integrations.config_entry.copy" )} - + ${Object.keys(item.supported_subentry_types).map( (flowType) => - html` - + html` + ${this.hass.localize( `component.${item.domain}.config_subentries.${flowType}.initiate_flow.user` - )}` + )} + ` )} - + ${this.diagnosticHandler && item.state === "loaded" ? html` - - - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.download_diagnostics" - )} - + + + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.download_diagnostics" + )} + + ` : nothing} ${!item.disabled_by && item.supports_reconfigure && item.source !== "system" ? html` - - + + ${this.hass.localize( "ui.panel.config.integrations.config_entry.reconfigure" )} - + ` : nothing} - - + + ${this.hass.localize( "ui.panel.config.integrations.config_entry.system_options" )} - + ${item.disabled_by === "user" ? html` - + ${this.hass.localize("ui.common.enable")} - + ` : item.source !== "system" ? html` - + ${this.hass.localize("ui.common.disable")} - + ` : nothing} ${item.source !== "system" ? html` - - + + ${this.hass.localize( "ui.panel.config.integrations.config_entry.delete" )} - + ` : nothing} - + ${this._expanded ? subEntries.length @@ -548,6 +541,48 @@ class HaConfigEntryRow extends LitElement { this._devicesExpanded = !this._devicesExpanded; } + private _handleMenuAction = (ev: HaDropdownSelectEvent) => { + ev.stopPropagation(); + const value = ev.detail.item.value; + if (value === "reload") { + this._handleReload(); + return; + } + if (value === "rename") { + this._handleRename(); + return; + } + if (value === "copy") { + this._handleCopy(); + return; + } + if (value === "reconfigure") { + this._handleReconfigure(); + return; + } + if (value === "system_options") { + this._handleSystemOptions(); + return; + } + if (value === "enable") { + this._handleEnable(); + return; + } + if (value === "disable") { + this._handleDisable(); + return; + } + if (value === "delete") { + this._handleDelete(); + return; + } + if (value?.startsWith("subentry_")) { + const flowType = value.substring(9); + this._addSubEntry(flowType); + } + // devices, services, entities, diagnostics are handled by href navigation + }; + private _showOptions() { showOptionsFlowDialog(this, this.entry, { manifest: this.manifest }); } @@ -778,8 +813,8 @@ class HaConfigEntryRow extends LitElement { }); }; - private _addSubEntry = (item) => { - showSubConfigFlowDialog(this, this.entry, item.flowType, { + private _addSubEntry = (flowType: string) => { + showSubConfigFlowDialog(this, this.entry, flowType, { startFlowHandler: this.entry.entry_id, }); }; @@ -821,6 +856,9 @@ class HaConfigEntryRow extends LitElement { border-bottom-left-radius: 0; border-bottom-right-radius: 0; } + ha-dropdown a { + text-decoration: none; + } `, ]; } diff --git a/src/panels/config/integrations/ha-config-flow-card.ts b/src/panels/config/integrations/ha-config-flow-card.ts index 7f9e2f9cd5..7fd1787749 100644 --- a/src/panels/config/integrations/ha-config-flow-card.ts +++ b/src/panels/config/integrations/ha-config-flow-card.ts @@ -6,13 +6,13 @@ import { mdiOpenInNew, } from "@mdi/js"; import type { TemplateResult } from "lit"; -import { LitElement, css, html } from "lit"; +import { LitElement, css, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-button"; -import "../../../components/ha-button-menu"; -import "../../../components/ha-list-item"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import { deleteApplicationCredential, fetchApplicationCredentialsConfigEntry, @@ -34,6 +34,7 @@ import type { HomeAssistant } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; import type { DataEntryFlowProgressExtended } from "./ha-config-integrations"; import "./ha-integration-action-card"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; @customElement("ha-config-flow-card") export class HaConfigFlowCard extends LitElement { @@ -53,7 +54,7 @@ export class HaConfigFlowCard extends LitElement { .hass=${this.hass} .manifest=${this.manifest} .domain=${this.flow.handler} - .label=${this.flow.localized_title} + .label=${this.flow.localized_title ?? ""} > ${DISCOVERY_SOURCES.includes(this.flow.context.source) && this.flow.context.unique_id @@ -62,7 +63,7 @@ export class HaConfigFlowCard extends LitElement { "ui.panel.config.integrations.ignore.ignore" )}
      ` - : ""} + : nothing} ${this.flow.context.configuration_url || this.manifest || attention - ? html` + ? html` - + ${this.hass.localize( "ui.panel.config.integrations.config_entry.open_configuration_url" )} - + - + ` - : ""} + : nothing} ${this.manifest ? html` - + ${this.hass.localize( "ui.panel.config.integrations.config_entry.documentation" )} - + ` - : ""} + : nothing} ${attention - ? html` - + ? html` + ${this.hass.localize( "ui.panel.config.integrations.config_entry.delete" )} - ` - : ""} - ` - : ""} + ` + : nothing} + ` + : nothing} `; } @@ -263,7 +260,7 @@ export class HaConfigFlowCard extends LitElement { } } - private async _handleDelete() { + private _handleDelete = async () => { const entryId = this.flow.context.entry_id; if (!entryId) { @@ -306,6 +303,14 @@ export class HaConfigFlowCard extends LitElement { } this._handleFlowUpdated(); + }; + + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { + const action = ev.detail?.item?.value; + + if (action === "delete") { + this._handleDelete(); + } } static styles = css` @@ -313,7 +318,7 @@ export class HaConfigFlowCard extends LitElement { text-decoration: none; color: var(--primary-color); } - ha-button-menu { + ha-dropdown { color: var(--secondary-text-color); } ha-svg-icon[slot="meta"] { @@ -324,9 +329,6 @@ export class HaConfigFlowCard extends LitElement { --mdc-theme-primary: var(--error-color); --ha-card-border-color: var(--error-color); } - .warning { - --mdc-theme-text-primary-on-background: var(--error-color); - } `; } diff --git a/src/panels/config/integrations/ha-config-integration-page.ts b/src/panels/config/integrations/ha-config-integration-page.ts index db7e1d4bb1..6ff3aedf7a 100644 --- a/src/panels/config/integrations/ha-config-integration-page.ts +++ b/src/panels/config/integrations/ha-config-integration-page.ts @@ -24,11 +24,10 @@ import { import { caseInsensitiveStringCompare } from "../../../common/string/compare"; import { nextRender } from "../../../common/util/render-status"; import "../../../components/ha-button"; -import "../../../components/ha-md-button-menu"; -import "../../../components/ha-md-divider"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-md-list"; import "../../../components/ha-md-list-item"; -import "../../../components/ha-md-menu-item"; import { getSignedPath } from "../../../data/auth"; import type { ConfigEntry } from "../../../data/config_entries"; import { ERROR_STATES, getConfigEntries } from "../../../data/config_entries"; @@ -169,6 +168,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { protected willUpdate(changedProperties: PropertyValues): void { if (changedProperties.has("domain")) { this.hass.loadBackendTranslation("title", [this.domain]); + this.hass.loadBackendTranslation("config", [this.domain]); this.hass.loadBackendTranslation("config_subentries", [this.domain]); this._extraConfigEntries = undefined; this._fetchManifest(); @@ -316,7 +316,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { ` : nothing} ${this._manifest?.config_flow || this._logInfo - ? html` + ? html` - ${this.hass.localize( - "ui.panel.config.integrations.config_entry.known_issues" - )} - - - + + + ${this.hass.localize( + "ui.panel.config.integrations.config_entry.known_issues" + )} + + + ` : nothing} ${this._logInfo - ? html` + ${this._logInfo.level === LogSeverity.DEBUG ? this.hass.localize( "ui.panel.config.integrations.config_entry.disable_debug_logging" @@ -354,18 +365,9 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { : this.hass.localize( "ui.panel.config.integrations.config_entry.enable_debug_logging" )} - - ` + ` : nothing} - ` + ` : nothing}
      @@ -533,13 +535,16 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { .appearance=${canAddDevice ? "filled" : "accent"} @click=${this._addIntegration} > - ${this._manifest?.integration_type + ${this.hass.localize( + `component.${this.domain}.config.initiate_flow.user` + ) || + (this._manifest?.integration_type ? this.hass.localize( `ui.panel.config.integrations.integration_page.add_${this._manifest.integration_type}` ) : this.hass.localize( `ui.panel.config.integrations.integration_page.add_entry` - )} + ))} ` : nothing} ${Array.from(supportedSubentryTypes).map( @@ -870,6 +875,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) { } showAddIntegrationDialog(this, { domain: this.domain, + navigateToResult: true, }); } diff --git a/src/panels/config/integrations/ha-config-integrations-dashboard.ts b/src/panels/config/integrations/ha-config-integrations-dashboard.ts index 86fbd405fd..2900e68f70 100644 --- a/src/panels/config/integrations/ha-config-integrations-dashboard.ts +++ b/src/panels/config/integrations/ha-config-integrations-dashboard.ts @@ -1,4 +1,3 @@ -import type { ActionDetail } from "@material/mwc-list"; import { mdiFilterVariant, mdiPlus } from "@mdi/js"; import type { IFuseOptions } from "fuse.js"; import Fuse from "fuse.js"; @@ -18,9 +17,9 @@ import { caseInsensitiveStringCompare } from "../../../common/string/compare"; import { extractSearchParam } from "../../../common/url/search-params"; import { nextRender } from "../../../common/util/render-status"; import "../../../components/ha-button"; -import "../../../components/ha-button-menu"; -import "../../../components/ha-check-list-item"; import "../../../components/ha-checkbox"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-fab"; import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; @@ -69,6 +68,7 @@ import "./ha-integration-card"; import type { HaIntegrationCard } from "./ha-integration-card"; import "./ha-integration-overflow-menu"; import { showAddIntegrationDialog } from "./show-add-integration-dialog"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; export interface ConfigEntryExtended extends Omit { entry_id?: string; @@ -126,10 +126,12 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin( @state() private _showDisabled = false; - @state() private _searchParms = new URLSearchParams( + @state() private _hashParams = new URLSearchParams( window.location.hash.substring(1) ); + @state() private _searchParams = new URLSearchParams(window.location.search); + @state() private _filter: string = history.state?.filter || ""; @state() private _diagnosticHandlers?: Record; @@ -357,8 +359,8 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin( this._handleRouteChanged(); } if ( - (this._searchParms.has("config_entry") || - this._searchParms.has("domain")) && + (this._hashParams.has("config_entry") || + this._hashParams.has("domain")) && changed.has("configEntries") && !changed.get("configEntries") && this.configEntries @@ -408,24 +410,32 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin( ${!this._showDisabled && this.narrow && disabledConfigEntries.length ? html`${disabledConfigEntries.length}` : ""} - + - + ${this.hass.localize( "ui.panel.config.integrations.ignore.show_ignored" )} - - + + ${this.hass.localize( "ui.panel.config.integrations.disable.show_disabled" )} - - + +
      ${this.narrow ? html` @@ -442,7 +452,9 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin( ) { - switch (ev.detail.index) { - case 0: + private _handleMenuAction(ev: HaDropdownSelectEvent) { + const action = ev.detail.item.value; + switch (action) { + case "show-ignored": this._showIgnored = !this._showIgnored; break; - case 1: + case "show-disabled": this._toggleShowDisabled(); break; } @@ -790,7 +804,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin( private async _highlightEntry() { await nextRender(); - const entryId = this._searchParms.get("config_entry"); + const entryId = this._hashParams.get("config_entry"); let domain: string | null; if (entryId) { const configEntry = this.configEntries!.find( @@ -801,7 +815,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin( } domain = configEntry.domain; } else { - domain = this._searchParms.get("domain"); + domain = this._hashParams.get("domain"); } const card: HaIntegrationCard = this.shadowRoot!.querySelector( `[data-domain=${domain}]` @@ -825,6 +839,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin( if (brand) { showAddIntegrationDialog(this, { brand, + navigateToResult: true, }); return; } @@ -878,6 +893,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin( ) { showAddIntegrationDialog(this, { domain, + navigateToResult: true, }); } return; @@ -979,7 +995,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin( :host([narrow]) hass-tabs-subpage { --main-title-margin: 0; } - ha-button-menu { + ha-dropdown { margin-left: 8px; margin-inline-start: 8px; margin-inline-end: initial; @@ -1077,7 +1093,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin( margin-inline-start: 16px; margin-inline-end: initial; } - ha-button-menu { + ha-dropdown ha-icon-button { color: var(--primary-text-color); } `, diff --git a/src/panels/config/integrations/ha-config-sub-entry-row.ts b/src/panels/config/integrations/ha-config-sub-entry-row.ts index a2c7c32de0..cad0c049c5 100644 --- a/src/panels/config/integrations/ha-config-sub-entry-row.ts +++ b/src/panels/config/integrations/ha-config-sub-entry-row.ts @@ -11,6 +11,8 @@ import { import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import type { ConfigEntry, SubEntry } from "../../../data/config_entries"; import { deleteSubEntry, updateSubEntry } from "../../../data/config_entries"; import type { DeviceRegistryEntry } from "../../../data/device/device_registry"; @@ -89,7 +91,7 @@ class HaConfigSubEntryRow extends LitElement { ` : nothing} - + ${devices.length || services.length ? html` - - - ${this.hass.localize( - `ui.panel.config.integrations.config_entry.devices`, - { count: devices.length } - )} - - + + + ${this.hass.localize( + `ui.panel.config.integrations.config_entry.devices`, + { count: devices.length } + )} + + + ` : nothing} ${services.length - ? html` - - ${this.hass.localize( - `ui.panel.config.integrations.config_entry.services`, - { count: services.length } - )} - - ` + ? html` + + + + ${this.hass.localize( + `ui.panel.config.integrations.config_entry.services`, + { count: services.length } + )} + + + + ` : nothing} ${entities.length ? html` - - - ${this.hass.localize( - `ui.panel.config.integrations.config_entry.entities`, - { count: entities.length } - )} - - + + + ${this.hass.localize( + `ui.panel.config.integrations.config_entry.entities`, + { count: entities.length } + )} + + + ` : nothing} - - + + ${this.hass.localize( "ui.panel.config.integrations.config_entry.rename" )} - - - + + + ${this.hass.localize( "ui.panel.config.integrations.config_entry.delete" )} - - + + ${this._expanded ? html` @@ -225,6 +228,19 @@ class HaConfigSubEntryRow extends LitElement { }); } + private _handleMenuAction = (ev: CustomEvent) => { + const value = ev.detail.item.value; + switch (value) { + case "rename": + this._handleRenameSub(); + break; + case "delete": + this._handleDeleteSub(); + break; + // devices, services, entities are handled by href navigation + } + }; + private _handleRenameSub = async (): Promise => { const newName = await showPromptDialog(this, { title: this.hass.localize("ui.common.rename"), @@ -286,6 +302,9 @@ class HaConfigSubEntryRow extends LitElement { ha-md-list-item.has-subentries { border-bottom: 1px solid var(--divider-color); } + ha-dropdown a { + text-decoration: none; + } `; } diff --git a/src/panels/config/integrations/ha-domain-integrations.ts b/src/panels/config/integrations/ha-domain-integrations.ts index be1b62bbf0..cc1546b050 100644 --- a/src/panels/config/integrations/ha-domain-integrations.ts +++ b/src/panels/config/integrations/ha-domain-integrations.ts @@ -1,5 +1,5 @@ import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item-base"; -import { css, html, LitElement } from "lit"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { fireEvent } from "../../../common/dom/fire_event"; @@ -39,6 +39,12 @@ class HaDomainIntegrations extends LitElement { @property({ attribute: false }) public flowsInProgress?: DataEntryFlowProgress[]; + @property({ attribute: false }) + public navigateToResult = false; + + @property({ attribute: false }) + public showManageLink = false; + protected render() { return html` ${this.flowsInProgress?.length @@ -49,6 +55,7 @@ class HaDomainIntegrations extends LitElement { (flow) => html`${localizeConfigFlowTitle(this.hass.localize, flow)} + ${domainToName(this.hass.localize, flow.handler)} ` )} @@ -80,8 +90,8 @@ class HaDomainIntegrations extends LitElement { "ui.panel.config.integrations.available_integrations" )} ` - : ""}` - : ""} + : nothing}` + : nothing} ${this.integration?.iot_standards ? this.integration.iot_standards .filter((standard) => @@ -224,6 +234,27 @@ class HaDomainIntegrations extends LitElement { > `}` : ""} + ${this.showManageLink && + // Only show manage link if not already on the integrations dashboard + !location.pathname.startsWith("/config/integrations") + ? html` + ${this.hass.localize( + "ui.panel.config.integrations.manage_discovered" + )} + ${this.hass.localize( + "ui.panel.config.integrations.manage_discovered_description" + )} + + ` + : nothing} `; } @@ -279,7 +310,7 @@ class HaDomainIntegrations extends LitElement { { startFlowHandler: domain, showAdvanced: this.hass.userData?.showAdvanced, - navigateToResult: true, + navigateToResult: this.navigateToResult, manifest: await fetchIntegrationManifest(this.hass, domain), } ); @@ -296,7 +327,7 @@ class HaDomainIntegrations extends LitElement { root instanceof ShadowRoot ? (root.host as HTMLElement) : this, { continueFlowId: flow.flow_id, - navigateToResult: true, + navigateToResult: this.navigateToResult, showAdvanced: this.hass.userData?.showAdvanced, manifest: await fetchIntegrationManifest(this.hass, flow.handler), } @@ -304,6 +335,14 @@ class HaDomainIntegrations extends LitElement { fireEvent(this, "close-dialog"); } + private _manageDiscovered(ev: CustomEvent) { + if (!shouldHandleRequestSelectedEvent(ev)) { + return; + } + fireEvent(this, "close-dialog"); + navigate("/config/integrations/dashboard?historyBack=1"); + } + private _standardPicked(ev: CustomEvent) { if (!shouldHandleRequestSelectedEvent(ev)) { return; diff --git a/src/panels/config/integrations/ha-integration-list-item.ts b/src/panels/config/integrations/ha-integration-list-item.ts index 36039e3f91..58ae58230e 100644 --- a/src/panels/config/integrations/ha-integration-list-item.ts +++ b/src/panels/config/integrations/ha-integration-list-item.ts @@ -1,7 +1,12 @@ import type { GraphicType } from "@material/mwc-list/mwc-list-item-base"; import { ListItemBase } from "@material/mwc-list/mwc-list-item-base"; import { styles } from "@material/mwc-list/mwc-list-item.css"; -import { mdiFileCodeOutline, mdiPackageVariant, mdiWeb } from "@mdi/js"; +import { + mdiDevices, + mdiFileCodeOutline, + mdiPackageVariant, + mdiWeb, +} from "@mdi/js"; import type { CSSResultGroup } from "lit"; import { css, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; @@ -51,18 +56,23 @@ export class HaIntegrationListItem extends ListItemBase { graphicClasses )}" > - + ${this.integration.is_discovered + ? html`` + : html``} `; } @@ -145,6 +155,10 @@ export class HaIntegrationListItem extends ListItemBase { width: 40px; height: 40px; } + .discovered-icon { + --mdc-icon-size: 40px; + color: var(--primary-color); + } .mdc-deprecated-list-item__meta { width: auto; white-space: nowrap; diff --git a/src/panels/config/integrations/ha-integration-overflow-menu.ts b/src/panels/config/integrations/ha-integration-overflow-menu.ts index 2b3413a684..135a3e9f41 100644 --- a/src/panels/config/integrations/ha-integration-overflow-menu.ts +++ b/src/panels/config/integrations/ha-integration-overflow-menu.ts @@ -1,10 +1,10 @@ import { mdiDotsVertical } from "@mdi/js"; -import { html, LitElement } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-icon-button"; import type { HomeAssistant } from "../../../types"; -import "../../../components/ha-md-list-item"; -import "../../../components/ha-md-button-menu"; @customElement("ha-integration-overflow-menu") export class HaIntegrationOverflowMenu extends LitElement { @@ -12,22 +12,32 @@ export class HaIntegrationOverflowMenu extends LitElement { protected render() { return html` - + - - ${this.hass.localize( - "ui.panel.config.application_credentials.caption" - )} - - + + + ${this.hass.localize( + "ui.panel.config.application_credentials.caption" + )} + + + `; } -} + static styles = css` + :host { + display: flex; + } + a { + text-decoration: none; + } + `; +} declare global { interface HTMLElementTagNameMap { "ha-integration-overflow-menu": HaIntegrationOverflowMenu; diff --git a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts index 8b70fc05ba..1f5a13839e 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts @@ -369,7 +369,6 @@ export class BluetoothConfigDashboard extends LitElement { padding: 24px 0 32px; max-width: 600px; margin: 0 auto; - direction: ltr; } ha-card { margin-bottom: 16px; diff --git a/src/panels/config/integrations/integration-panels/matter/matter-config-dashboard.ts b/src/panels/config/integrations/integration-panels/matter/matter-config-dashboard.ts index ee73213f70..a5f3ebafb4 100644 --- a/src/panels/config/integrations/integration-panels/matter/matter-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/matter/matter-config-dashboard.ts @@ -333,9 +333,9 @@ export class MatterConfigDashboard extends LitElement { const configEntries = await getConfigEntries(this.hass, { domain: "matter", }); - if (configEntries.length) { - this._configEntry = configEntries[0]; - } + this._configEntry = configEntries.find( + (entry) => entry.disabled_by === null && entry.source !== "ignore" + ); } static get styles(): CSSResultGroup { @@ -400,7 +400,7 @@ export class MatterConfigDashboard extends LitElement { } .dev-tools-content { - padding: 0 0 var(--ha-space-4); + padding: var(--ha-space-3) 0; } .dev-tools-content p { diff --git a/src/panels/config/integrations/integration-panels/matter/matter-config-panel.ts b/src/panels/config/integrations/integration-panels/matter/matter-config-panel.ts index 14ec0bf728..fb6473e3db 100644 --- a/src/panels/config/integrations/integration-panels/matter/matter-config-panel.ts +++ b/src/panels/config/integrations/integration-panels/matter/matter-config-panel.ts @@ -1,4 +1,4 @@ -import { mdiMathLog, mdiServerNetwork } from "@mdi/js"; +import { mdiServerNetwork, mdiTextBoxOutline } from "@mdi/js"; import { customElement, property } from "lit/decorators"; import type { RouterOptions } from "../../../../../layouts/hass-router-page"; import { HassRouterPage } from "../../../../../layouts/hass-router-page"; @@ -14,7 +14,7 @@ export const configTabs: PageNavigation[] = [ { translationKey: "ui.panel.config.zwave_js.navigation.logs", path: `/config/zwave_js/logs`, - iconPath: mdiMathLog, + iconPath: mdiTextBoxOutline, }, ]; diff --git a/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts b/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts index 6ff2efad38..cc3878319b 100644 --- a/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts +++ b/src/panels/config/integrations/integration-panels/mqtt/mqtt-config-panel.ts @@ -2,21 +2,21 @@ import type { CSSResultGroup, TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import { storage } from "../../../../../common/decorators/storage"; +import "../../../../../components/ha-button"; import "../../../../../components/ha-card"; import "../../../../../components/ha-code-editor"; import "../../../../../components/ha-formfield"; -import "../../../../../components/ha-list-item"; +import type { HaSelectSelectEvent } from "../../../../../components/ha-select"; import "../../../../../components/ha-switch"; -import "../../../../../components/ha-button"; import { getConfigEntries } from "../../../../../data/config_entries"; +import type { Action } from "../../../../../data/script"; +import { callExecuteScript } from "../../../../../data/service"; import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow"; import "../../../../../layouts/hass-subpage"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant } from "../../../../../types"; -import "./mqtt-subscribe-card"; -import type { Action } from "../../../../../data/script"; -import { callExecuteScript } from "../../../../../data/service"; import { showToast } from "../../../../../util/toast"; +import "./mqtt-subscribe-card"; const qosLevel = ["0", "1", "2"]; @@ -89,10 +89,8 @@ export class MQTTConfigPanel extends LitElement { .label=${this.hass.localize("ui.panel.config.mqtt.qos")} .value=${this._qos} @selected=${this._handleQos} - >${qosLevel.map( - (qos) => - html`${qos}` - )} + .options=${qosLevel} + > = 0 && newValue !== this._qos) { + private _handleQos(ev: HaSelectSelectEvent) { + const newValue = ev.detail.value; + if (Number(newValue) >= 0 && newValue !== this._qos) { this._qos = newValue; } } diff --git a/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts b/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts index 489245fd41..25ed2e5ad0 100644 --- a/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts +++ b/src/panels/config/integrations/integration-panels/mqtt/mqtt-subscribe-card.ts @@ -2,8 +2,9 @@ import type { TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import { formatTime } from "../../../../../common/datetime/format_time"; -import "../../../../../components/ha-card"; import "../../../../../components/ha-button"; +import "../../../../../components/ha-card"; +import type { HaSelectSelectEvent } from "../../../../../components/ha-select"; import "../../../../../components/ha-select"; import "../../../../../components/ha-textfield"; import type { MQTTMessage } from "../../../../../data/mqtt"; @@ -12,7 +13,6 @@ import type { HomeAssistant } from "../../../../../types"; import { storage } from "../../../../../common/decorators/storage"; import "../../../../../components/ha-formfield"; -import "../../../../../components/ha-list-item"; import "../../../../../components/ha-switch"; const qosLevel = ["0", "1", "2"]; @@ -96,9 +96,8 @@ class MqttSubscribeCard extends LitElement { .disabled=${this._subscribed !== undefined} .value=${this._qos} @selected=${this._handleQos} - >${qosLevel.map( - (qos) => html`${qos}` - )} + .options=${qosLevel} + > = 0 && newValue !== this._qos) { + private _handleQos(ev: HaSelectSelectEvent): void { + const newValue = ev.detail.value; + if (Number(newValue) >= 0 && newValue !== this._qos) { this._qos = newValue; } } @@ -193,8 +192,8 @@ class MqttSubscribeCard extends LitElement { static styles = css` form { - display: block; - padding: 16px; + padding: var(--ha-space-4); + padding-bottom: var(--ha-space-8); } .events { margin: -16px 0; @@ -229,6 +228,7 @@ class MqttSubscribeCard extends LitElement { } @media screen and (max-width: 600px) { ha-select { + display: block; margin-left: 0px; margin-top: 8px; margin-inline-start: 0px; diff --git a/src/panels/config/integrations/integration-panels/thread/thread-config-panel.ts b/src/panels/config/integrations/integration-panels/thread/thread-config-panel.ts index 934cb86618..a6286ce96e 100644 --- a/src/panels/config/integrations/integration-panels/thread/thread-config-panel.ts +++ b/src/panels/config/integrations/integration-panels/thread/thread-config-panel.ts @@ -1,4 +1,3 @@ -import type { ActionDetail } from "@material/mwc-list"; import { mdiCellphoneKey, mdiDeleteOutline, @@ -14,9 +13,9 @@ import { isComponentLoaded } from "../../../../../common/config/is_component_loa import { stringCompare } from "../../../../../common/string/compare"; import { extractSearchParam } from "../../../../../common/url/search-params"; import "../../../../../components/ha-button"; -import "../../../../../components/ha-button-menu"; import "../../../../../components/ha-card"; -import "../../../../../components/ha-list-item"; +import "../../../../../components/ha-dropdown"; +import "../../../../../components/ha-dropdown-item"; import { getSignedPath } from "../../../../../data/auth"; import { getConfigEntryDiagnosticsDownloadUrl } from "../../../../../data/diagnostics"; import type { OTBRInfo, OTBRInfoDict } from "../../../../../data/otbr"; @@ -50,6 +49,7 @@ import { brandsUrl } from "../../../../../util/brands-url"; import { documentationUrl } from "../../../../../util/documentation-url"; import { fileDownload } from "../../../../../util/file_download"; import { showThreadDatasetDialog } from "./show-dialog-thread-dataset"; +import type { HaDropdownSelectEvent } from "../../../../../components/ha-dropdown"; export interface ThreadNetwork { name: string; @@ -76,10 +76,11 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) { return html` - + - + ${this.hass.localize( "ui.panel.config.integrations.config_entry.download_diagnostics" )} - + - ${this.hass.localize( "ui.panel.config.thread.add_dataset_from_tlv" - )} - ${this.hass.localize( "ui.panel.config.thread.add_open_thread_border_router" - )} - +

      ${this.hass.localize("ui.panel.config.thread.my_network")}

      ${networks.preferred @@ -242,12 +243,12 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) { )} >` : ""} - ${showDefaultRouter - ? html` + ? html` ${isDefaultRouter ? this.hass.localize( "ui.panel.config.thread.default_router" @@ -265,28 +269,28 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) { : this.hass.localize( "ui.panel.config.thread.set_default_router" )} - ` + ` : ""} ${otbr - ? html` + ? html` ${this.hass.localize( "ui.panel.config.thread.reset_border_router" - )} - + ${this.hass.localize( "ui.panel.config.thread.change_channel" - )} ${network.dataset?.preferred ? "" - : html` + : html` ${this.hass.localize( "ui.panel.config.thread.add_to_my_network" )} - `}` + `}` : ""} - ` + ` : ""} `; })}` @@ -480,24 +484,22 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) { }); } - private _handleRouterAction(ev: CustomEvent) { + private _handleRouterAction(ev: HaDropdownSelectEvent) { const network = (ev.currentTarget as any).network as ThreadNetwork; const router = (ev.currentTarget as any).router as ThreadRouter; const otbr = (ev.currentTarget as any).otbr as OTBRInfo; - const index = network.dataset - ? Number(ev.detail.index) - : Number(ev.detail.index) + 1; - switch (index) { - case 0: + const action = ev.detail.item.value; + switch (action) { + case "set-default": this._setPreferredBorderAgent(network.dataset!, router); break; - case 1: + case "reset-router": this._resetBorderRouter(otbr); break; - case 2: + case "change-channel": this._changeChannel(otbr); break; - case 3: + case "add-to-network": this._setDataset(otbr); break; } @@ -713,7 +715,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) { ha-svg-icon[slot="meta"] { width: 24px; } - ha-button-menu a { + ha-dropdown a { text-decoration: none; } .routers { diff --git a/src/panels/config/integrations/integration-panels/zha/dialog-zha-change-channel.ts b/src/panels/config/integrations/integration-panels/zha/dialog-zha-change-channel.ts index 58b1a13dd8..459284bac6 100644 --- a/src/panels/config/integrations/integration-panels/zha/dialog-zha-change-channel.ts +++ b/src/panels/config/integrations/integration-panels/zha/dialog-zha-change-channel.ts @@ -2,13 +2,12 @@ import type { TemplateResult } from "lit"; import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; -import { stopPropagation } from "../../../../../common/dom/stop_propagation"; import "../../../../../components/buttons/ha-progress-button"; import "../../../../../components/ha-alert"; import "../../../../../components/ha-button"; import { createCloseHeading } from "../../../../../components/ha-dialog"; -import "../../../../../components/ha-list-item"; import "../../../../../components/ha-select"; +import type { HaSelectSelectEvent } from "../../../../../components/ha-select"; import { changeZHANetworkChannel } from "../../../../../data/zha"; import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box"; import type { HassDialog } from "../../../../../dialogs/make-dialog-manager"; @@ -96,22 +95,18 @@ class DialogZHAChangeChannel extends LitElement implements HassDialog { .label=${this.hass.localize( "ui.panel.config.zha.change_channel_dialog.new_channel" )} - fixedMenuPosition - naturalMenuWidth @selected=${this._newChannelChosen} - @closed=${stopPropagation} .value=${String(this._newChannel)} + .options=${VALID_CHANNELS.map((channel) => ({ + value: String(channel), + label: + channel === "auto" + ? this.hass.localize( + "ui.panel.config.zha.change_channel_dialog.channel_auto" + ) + : String(channel), + }))} > - ${VALID_CHANNELS.map( - (newChannel) => - html`${newChannel === "auto" - ? this.hass.localize( - "ui.panel.config.zha.change_channel_dialog.channel_auto" - ) - : newChannel}` - )}

      @@ -137,8 +132,8 @@ class DialogZHAChangeChannel extends LitElement implements HassDialog { `; } - private _newChannelChosen(evt: Event): void { - const value: string = (evt.target! as HTMLSelectElement).value; + private _newChannelChosen(ev: HaSelectSelectEvent): void { + const value = ev.detail.value; this._newChannel = value === "auto" ? "auto" : parseInt(value, 10); } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-add-group-page.ts b/src/panels/config/integrations/integration-panels/zha/zha-add-group-page.ts index 3ec0cf2573..e72f768d15 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-add-group-page.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-add-group-page.ts @@ -20,7 +20,7 @@ export class ZHAAddGroupPage extends LitElement { @property({ type: Boolean }) public narrow = false; - @property({ attribute: false, type: Array }) + @property({ attribute: false }) public deviceEndpoints: ZHADeviceEndpoint[] = []; @state() private _processingAdd = false; diff --git a/src/panels/config/integrations/integration-panels/zha/zha-cluster-attributes.ts b/src/panels/config/integrations/integration-panels/zha/zha-cluster-attributes.ts index 002508ca52..748733fce6 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-cluster-attributes.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-cluster-attributes.ts @@ -1,12 +1,11 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { stopPropagation } from "../../../../../common/dom/stop_propagation"; import "../../../../../components/buttons/ha-call-service-button"; import "../../../../../components/buttons/ha-progress-button"; import "../../../../../components/ha-card"; -import "../../../../../components/ha-list-item"; import "../../../../../components/ha-select"; +import type { HaSelectSelectEvent } from "../../../../../components/ha-select"; import "../../../../../components/ha-textfield"; import { forwardHaptic } from "../../../../../data/haptics"; import type { @@ -22,7 +21,7 @@ import { import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant } from "../../../../../types"; import { formatAsPaddedHex } from "./functions"; -import type { ItemSelectedEvent, SetAttributeServiceData } from "./types"; +import type { SetAttributeServiceData } from "./types"; @customElement("zha-cluster-attributes") export class ZHAClusterAttributes extends LitElement { @@ -30,7 +29,7 @@ export class ZHAClusterAttributes extends LitElement { @property({ attribute: false }) public device?: ZHADevice; - @property({ attribute: false, type: Object }) + @property({ attribute: false }) public selectedCluster?: Cluster; @state() private _attributes: Attribute[] | undefined; @@ -68,24 +67,18 @@ export class ZHAClusterAttributes extends LitElement { "ui.panel.config.zha.cluster_attributes.attributes_of_cluster" )} class="menu" - .value=${String(this._selectedAttributeId)} + .value=${this._selectedAttributeId} @selected=${this._selectedAttributeChanged} - @closed=${stopPropagation} - fixedMenuPosition - naturalMenuWidth + .options=${this._attributes.map((entry) => ({ + value: entry.id, + label: `${entry.name} (id: ${formatAsPaddedHex(entry.id)})`, + }))} > - ${this._attributes.map( - (entry) => html` - - ${`${entry.name} (id: ${formatAsPaddedHex(entry.id)})`} - - ` - )}
      ${this._selectedAttributeId !== undefined ? this._renderAttributeInteractions() - : ""} + : nothing} `; } @@ -221,8 +214,8 @@ export class ZHAClusterAttributes extends LitElement { } } - private _selectedAttributeChanged(event: ItemSelectedEvent): void { - this._selectedAttributeId = Number(event.target!.value); + private _selectedAttributeChanged(event: HaSelectSelectEvent): void { + this._selectedAttributeId = Number(event.detail.value); this._attributeValue = ""; } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts b/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts index 2d59dc04c4..534d243ee5 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-cluster-commands.ts @@ -1,12 +1,11 @@ import type { CSSResultGroup, PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { stopPropagation } from "../../../../../common/dom/stop_propagation"; import "../../../../../components/buttons/ha-call-service-button"; import "../../../../../components/ha-card"; import "../../../../../components/ha-form/ha-form"; -import "../../../../../components/ha-list-item"; import "../../../../../components/ha-select"; +import type { HaSelectSelectEvent } from "../../../../../components/ha-select"; import "../../../../../components/ha-textfield"; import type { Cluster, Command, ZHADevice } from "../../../../../data/zha"; import { fetchCommandsForCluster } from "../../../../../data/zha"; @@ -23,7 +22,7 @@ export class ZHAClusterCommands extends LitElement { @property({ attribute: false }) public device?: ZHADevice; - @property({ attribute: false, type: Object }) + @property({ attribute: false }) public selectedCluster?: Cluster; @state() private _commands: Command[] | undefined; @@ -64,17 +63,11 @@ export class ZHAClusterCommands extends LitElement { class="menu" .value=${String(this._selectedCommandId)} @selected=${this._selectedCommandChanged} - @closed=${stopPropagation} - fixedMenuPosition - naturalMenuWidth + .options=${this._commands.map((entry) => ({ + value: String(entry.id), + label: `${entry.name} (id: ${formatAsPaddedHex(entry.id)})`, + }))} > - ${this._commands.map( - (entry) => html` - - ${entry.name} (id: ${formatAsPaddedHex(entry.id)}) - - ` - )}
      ${this._selectedCommandId !== undefined @@ -179,8 +172,8 @@ export class ZHAClusterCommands extends LitElement { this._computeIssueClusterCommandServiceData(); } - private _selectedCommandChanged(event): void { - this._selectedCommandId = Number(event.target.value); + private _selectedCommandChanged(event: HaSelectSelectEvent): void { + this._selectedCommandId = Number(event.detail.value); this._issueClusterCommandServiceData = this._computeIssueClusterCommandServiceData(); } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts b/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts index d3b064285e..77bccaa507 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts @@ -323,9 +323,9 @@ class ZHAConfigDashboard extends LitElement { const configEntries = await getConfigEntries(this.hass, { domain: "zha", }); - if (configEntries.length) { - this._configEntry = configEntries[0]; - } + this._configEntry = configEntries.find( + (entry) => entry.disabled_by === null && entry.source !== "ignore" + ); } private async _fetchConfiguration(): Promise { diff --git a/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts index f2e17027ed..8306b36e16 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-device-binding.ts @@ -1,16 +1,14 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { stopPropagation } from "../../../../../common/dom/stop_propagation"; import "../../../../../components/buttons/ha-progress-button"; import "../../../../../components/ha-card"; import "../../../../../components/ha-select"; -import "../../../../../components/ha-list-item"; +import type { HaSelectSelectEvent } from "../../../../../components/ha-select"; import type { ZHADevice } from "../../../../../data/zha"; import { bindDevices, unbindDevices } from "../../../../../data/zha"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant } from "../../../../../types"; -import type { ItemSelectedEvent } from "./types"; @customElement("zha-device-binding-control") export class ZHADeviceBindingControl extends LitElement { @@ -44,19 +42,13 @@ export class ZHADeviceBindingControl extends LitElement { class="menu" .value=${String(this._bindTargetIndex)} @selected=${this._bindTargetIndexChanged} - @closed=${stopPropagation} - fixedMenuPosition - naturalMenuWidth + .options=${this.bindableDevices.map((device, idx) => ({ + value: String(idx), + label: device.user_given_name + ? device.user_given_name + : device.name, + }))} > - ${this.bindableDevices.map( - (device, idx) => html` - - ${device.user_given_name - ? device.user_given_name - : device.name} - - ` - )}
      @@ -81,8 +73,8 @@ export class ZHADeviceBindingControl extends LitElement { `; } - private _bindTargetIndexChanged(event: ItemSelectedEvent): void { - this._bindTargetIndex = Number(event.target!.value); + private _bindTargetIndexChanged(event: HaSelectSelectEvent): void { + this._bindTargetIndex = Number(event.detail.value); this._deviceToBind = this._bindTargetIndex === -1 ? undefined diff --git a/src/panels/config/integrations/integration-panels/zha/zha-device-endpoint-data-table.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-endpoint-data-table.ts index 0d24216dba..1831a66e1e 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-device-endpoint-data-table.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-device-endpoint-data-table.ts @@ -31,7 +31,7 @@ export class ZHADeviceEndpointDataTable extends LitElement { @property({ type: Boolean }) public selectable = false; - @property({ attribute: false, type: Array }) + @property({ attribute: false }) public deviceEndpoints: ZHADeviceEndpoint[] = []; @query("ha-data-table", true) private _dataTable!: HaDataTable; diff --git a/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts b/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts index 59142d155a..69925f8a76 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts @@ -1,13 +1,12 @@ import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; -import { customElement, property, state, query } from "lit/decorators"; +import { customElement, property, query, state } from "lit/decorators"; import type { HASSDomEvent } from "../../../../../common/dom/fire_event"; -import { stopPropagation } from "../../../../../common/dom/stop_propagation"; import "../../../../../components/buttons/ha-progress-button"; import type { SelectionChangedEvent } from "../../../../../components/data-table/ha-data-table"; import "../../../../../components/ha-card"; import "../../../../../components/ha-select"; -import "../../../../../components/ha-list-item"; +import type { HaSelectSelectEvent } from "../../../../../components/ha-select"; import type { Cluster, ZHADevice, ZHAGroup } from "../../../../../data/zha"; import { bindDeviceToGroup, @@ -16,7 +15,6 @@ import { } from "../../../../../data/zha"; import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant } from "../../../../../types"; -import type { ItemSelectedEvent } from "./types"; import "./zha-clusters-data-table"; import type { ZHAClustersDataTable } from "./zha-clusters-data-table"; @@ -64,16 +62,11 @@ export class ZHAGroupBindingControl extends LitElement { class="menu" .value=${String(this._bindTargetIndex)} @selected=${this._bindTargetIndexChanged} - @closed=${stopPropagation} - fixedMenuPosition - naturalMenuWidth + .options=${this.groups.map((group, idx) => ({ + value: String(idx), + label: group.name, + }))} > - ${this.groups.map( - (group, idx) => - html`${group.name} ` - )}
      @@ -109,8 +102,8 @@ export class ZHAGroupBindingControl extends LitElement { `; } - private _bindTargetIndexChanged(event: ItemSelectedEvent): void { - this._bindTargetIndex = Number(event.target!.value); + private _bindTargetIndexChanged(event: HaSelectSelectEvent): void { + this._bindTargetIndex = Number(event.detail.value); this._groupToBind = this._bindTargetIndex === -1 ? undefined diff --git a/src/panels/config/integrations/integration-panels/zha/zha-group-page.ts b/src/panels/config/integrations/integration-panels/zha/zha-group-page.ts index 14f509600c..80fe664361 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-group-page.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-group-page.ts @@ -32,13 +32,13 @@ export class ZHAGroupPage extends LitElement { @property({ type: Object }) public group?: ZHAGroup; - @property({ attribute: false, type: Number }) public groupId!: number; + @property({ attribute: false }) public groupId!: number; @property({ type: Boolean }) public narrow = false; @property({ attribute: "is-wide", type: Boolean }) public isWide = false; - @property({ attribute: false, type: Array }) + @property({ attribute: false }) public deviceEndpoints: ZHADeviceEndpoint[] = []; @state() private _processingAdd = false; diff --git a/src/panels/config/integrations/integration-panels/zha/zha-manage-clusters.ts b/src/panels/config/integrations/integration-panels/zha/zha-manage-clusters.ts index 8f8cc46c70..cf223cb429 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-manage-clusters.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-manage-clusters.ts @@ -2,10 +2,9 @@ import type { CSSResultGroup, PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { cache } from "lit/directives/cache"; -import { stopPropagation } from "../../../../../common/dom/stop_propagation"; import "../../../../../components/ha-card"; -import "../../../../../components/ha-list-item"; import "../../../../../components/ha-select"; +import type { HaSelectSelectEvent } from "../../../../../components/ha-select"; import "../../../../../components/ha-tab-group"; import "../../../../../components/ha-tab-group-tab"; import type { Cluster, ZHADevice } from "../../../../../data/zha"; @@ -77,17 +76,11 @@ export class ZHAManageClusters extends LitElement { class="menu" .value=${String(this._selectedClusterIndex)} @selected=${this._selectedClusterChanged} - @closed=${stopPropagation} - fixedMenuPosition - naturalMenuWidth + .options=${this._clusters.map((entry, idx) => ({ + value: String(idx), + label: computeClusterKey(entry), + }))} > - ${this._clusters.map( - (entry, idx) => html` - ${computeClusterKey(entry)} - ` - )}
      ${this._selectedCluster @@ -155,8 +148,8 @@ export class ZHAManageClusters extends LitElement { this._currTab = newTab; } - private _selectedClusterChanged(event): void { - this._selectedClusterIndex = Number(event.target!.value); + private _selectedClusterChanged(event: HaSelectSelectEvent): void { + this._selectedClusterIndex = Number(event.detail.value); this._selectedCluster = this._clusters[this._selectedClusterIndex]; } diff --git a/src/panels/config/integrations/integration-panels/zwave_js/capability-controls/zwave_js-capability-control-door-lock.ts b/src/panels/config/integrations/integration-panels/zwave_js/capability-controls/zwave_js-capability-control-door-lock.ts index 530d7b17b8..4dbbf94db1 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/capability-controls/zwave_js-capability-control-door-lock.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/capability-controls/zwave_js-capability-control-door-lock.ts @@ -5,8 +5,8 @@ import type { HaProgressButton } from "../../../../../../components/buttons/ha-p import "../../../../../../components/ha-alert"; import "../../../../../../components/ha-button"; import "../../../../../../components/ha-formfield"; -import "../../../../../../components/ha-list-item"; import "../../../../../../components/ha-select"; +import type { HaSelectSelectEvent } from "../../../../../../components/ha-select"; import "../../../../../../components/ha-spinner"; import "../../../../../../components/ha-switch"; import type { HaSwitch } from "../../../../../../components/ha-switch"; @@ -126,16 +126,13 @@ class ZWaveJSCapabilityDoorLock extends LitElement { )} .value=${this._currentDoorLockMode?.toString() ?? ""} @selected=${this._doorLockModeChanged} + .options=${supportedDoorLockModes.map((mode) => ({ + value: mode.toString(), + label: this.hass.localize( + `ui.panel.config.zwave_js.node_installer.capability_controls.door_lock.modes.${mode}` + ), + }))} > - ${supportedDoorLockModes.map( - (mode) => html` - - ${this.hass.localize( - `ui.panel.config.zwave_js.node_installer.capability_controls.door_lock.modes.${mode}` - )} - - ` - )}
    @@ -145,16 +142,13 @@ class ZWaveJSCapabilityDoorLock extends LitElement { )} .value=${this._configuration.operationType.toString()} @selected=${this._operationTypeChanged} + .options=${this._capabilities.supportedOperationTypes.map((type) => ({ + value: type.toString(), + label: this.hass.localize( + `ui.panel.config.zwave_js.node_installer.capability_controls.door_lock.operation_types.${type}` + ), + }))} > - ${this._capabilities.supportedOperationTypes.map( - (type) => html` - - ${this.hass.localize( - `ui.panel.config.zwave_js.node_installer.capability_controls.door_lock.operation_types.${type}` - )} - - ` - )}
    @@ -346,9 +340,8 @@ class ZWaveJSCapabilityDoorLock extends LitElement { ); } - private _operationTypeChanged(ev: CustomEvent) { - const target = ev.target as HTMLSelectElement; - const newType = parseInt(target.value); + private _operationTypeChanged(ev: HaSelectSelectEvent) { + const newType = parseInt(ev.detail.value); if (this._configuration) { this._configuration = { ...this._configuration, @@ -393,9 +386,8 @@ class ZWaveJSCapabilityDoorLock extends LitElement { } } - private _doorLockModeChanged(ev: CustomEvent) { - const target = ev.target as HTMLSelectElement; - this._currentDoorLockMode = parseInt(target.value) as DoorLockMode; + private _doorLockModeChanged(ev: HaSelectSelectEvent) { + this._currentDoorLockMode = ev.detail.value; } private async _saveConfig(ev: CustomEvent) { diff --git a/src/panels/config/integrations/integration-panels/zwave_js/capability-controls/zwave_js-capability-control-multilevel-switch.ts b/src/panels/config/integrations/integration-panels/zwave_js/capability-controls/zwave_js-capability-control-multilevel-switch.ts index 60371c449f..8a10913358 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/capability-controls/zwave_js-capability-control-multilevel-switch.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/capability-controls/zwave_js-capability-control-multilevel-switch.ts @@ -1,12 +1,11 @@ -import { LitElement, css, html } from "lit"; +import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import "../../../../../../components/buttons/ha-progress-button"; import type { HaProgressButton } from "../../../../../../components/buttons/ha-progress-button"; import "../../../../../../components/ha-alert"; import "../../../../../../components/ha-formfield"; -import "../../../../../../components/ha-list-item"; import "../../../../../../components/ha-select"; -import type { HaSelect } from "../../../../../../components/ha-select"; +import type { HaSelectSelectEvent } from "../../../../../../components/ha-select"; import "../../../../../../components/ha-switch"; import type { HaSwitch } from "../../../../../../components/ha-switch"; import "../../../../../../components/ha-textfield"; @@ -35,6 +34,8 @@ class ZWaveJSCapabilityMultiLevelSwitch extends LitElement { @state() private _error?: string; + @state() private _direction = "up"; + protected render() { return html`

    @@ -44,23 +45,28 @@ class ZWaveJSCapabilityMultiLevelSwitch extends LitElement {

    ${this._error ? html`${this._error}` - : ""} + : nothing} - ${this.hass.localize( - "ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.up" - )} - ${this.hass.localize( - "ui.panel.config.zwave_js.node_installer.capability_controls.multilevel_switch.down" - )} ${this._error ? html`${this._error}` - : ""} + : nothing} ({ + value: String(index), + label: this.hass.localize( + `ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.setback_type.${translationKey}` + ), + }))} > - ${SETBACK_TYPE_OPTIONS.map( - (translationKey, index) => - html` - ${this.hass.localize( - `ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.setback_type.${translationKey}` - )} - ` - )}
    - - ${Object.entries(SpecialState).map( - ([translationKey, value]) => - html` - ${this.hass.localize( - `ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.setback_special_state.${translationKey}` - )} - ` + .value=${this._setbackSpecialType} + clearable + .options=${Object.entries(SpecialState).map( + ([translationKey, value]) => ({ + value: value, + label: this.hass.localize( + `ui.panel.config.zwave_js.node_installer.capability_controls.thermostat_setback.setback_special_state.${translationKey}` + ), + }) )} + >
    @@ -144,12 +141,12 @@ class ZWaveJSCapabilityThermostatSetback extends LitElement { true )) as { setbackType: number; setbackState: number | SpecialState }; - this._setbackTypeInput.value = String(setbackType); + this._setbackType = String(setbackType); if (typeof setbackState === "number") { this._setbackStateInput.value = String(setbackState); - this._setbackSpecialStateSelect.value = ""; + this._setbackSpecialType = undefined; } else { - this._setbackSpecialStateSelect.value = setbackState; + this._setbackSpecialType = setbackState; } } catch (err) { this._error = this.hass.localize( @@ -161,8 +158,9 @@ class ZWaveJSCapabilityThermostatSetback extends LitElement { this._loading = false; } - private _changeSpecialState() { - this._disableSetbackState = !!this._setbackSpecialStateSelect.value; + private _changeSpecialState(ev: HaSelectSelectEvent) { + this._disableSetbackState = !ev.detail.value; + this._setbackSpecialType = ev.detail.value; } private async _saveSetback(ev: CustomEvent) { @@ -170,11 +168,11 @@ class ZWaveJSCapabilityThermostatSetback extends LitElement { button.progress = true; this._error = undefined; - const setbackType = this._setbackTypeInput.value; + const setbackType = this._setbackType; let setbackState: number | string = Number(this._setbackStateInput.value); - if (this._setbackSpecialStateSelect.value) { - setbackState = this._setbackSpecialStateSelect.value; + if (this._setbackSpecialType) { + setbackState = this._setbackSpecialType; } try { @@ -204,6 +202,10 @@ class ZWaveJSCapabilityThermostatSetback extends LitElement { this._loadSetback(); } + private _setbackTypeChanged(ev: HaSelectSelectEvent) { + this._setbackType = ev.detail.value; + } + static styles = css` :host { display: flex; diff --git a/src/panels/config/integrations/integration-panels/zwave_js/functions.ts b/src/panels/config/integrations/integration-panels/zwave_js/functions.ts new file mode 100644 index 0000000000..14d788b0e9 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zwave_js/functions.ts @@ -0,0 +1,9 @@ +/** + * Formats a Z-Wave home ID as an uppercase hexadecimal string with 0x prefix. + * Z-Wave home IDs are 32-bit values (4 bytes). + * + * @param homeId - The home ID as a number + * @returns Formatted hex string (e.g., "0xD34DB33F") + */ +export const formatHomeIdAsHex = (homeId: number): string => + "0x" + homeId.toString(16).toUpperCase().padStart(8, "0"); diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts index 7cf4fe8c33..d2f70c5e6e 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-dashboard.ts @@ -56,6 +56,7 @@ import { showZWaveJSAddNodeDialog } from "./add-node/show-dialog-zwave_js-add-no import { showZWaveJSRebuildNetworkRoutesDialog } from "./show-dialog-zwave_js-rebuild-network-routes"; import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node"; import { configTabs } from "./zwave_js-config-router"; +import { formatHomeIdAsHex } from "./functions"; @customElement("zwave_js-config-dashboard") class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) { @@ -271,7 +272,11 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) { "ui.panel.config.zwave_js.dashboard.home_id" )}: - ${this._network.controller.home_id} + ${formatHomeIdAsHex( + this._network.controller.home_id + )}
    diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-entry-picker.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-entry-picker.ts index 0d55c3f717..806a3a6396 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-entry-picker.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-entry-picker.ts @@ -82,9 +82,11 @@ class ZWaveJSConfigEntryPicker extends LitElement { const entries = await getConfigEntries(this.hass, { domain: "zwave_js", }); - this._configEntries = entries.sort((a, b) => - caseInsensitiveStringCompare(a.title, b.title) - ); + this._configEntries = entries + .filter( + (entry) => entry.disabled_by === null && entry.source !== "ignore" + ) + .sort((a, b) => caseInsensitiveStringCompare(a.title, b.title)); if (this._configEntries.length === 1) { navigate( `/config/zwave_js/dashboard?config_entry=${this._configEntries[0].entry_id}`, diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-router.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-router.ts index c74a376ce8..1da7efe55f 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-router.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-config-router.ts @@ -1,4 +1,4 @@ -import { mdiServerNetwork, mdiMathLog, mdiNetwork } from "@mdi/js"; +import { mdiNetwork, mdiServerNetwork, mdiTextBoxOutline } from "@mdi/js"; import { customElement, property } from "lit/decorators"; import type { RouterOptions } from "../../../../../layouts/hass-router-page"; import { HassRouterPage } from "../../../../../layouts/hass-router-page"; @@ -14,7 +14,7 @@ export const configTabs: PageNavigation[] = [ { translationKey: "ui.panel.config.zwave_js.navigation.logs", path: `/config/zwave_js/logs`, - iconPath: mdiMathLog, + iconPath: mdiTextBoxOutline, }, { translationKey: "ui.panel.config.zwave_js.navigation.visualization", diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-custom-param.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-custom-param.ts index 948864acf0..7408a60de1 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-custom-param.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-custom-param.ts @@ -1,17 +1,17 @@ -import { LitElement, html, css, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; import { mdiCloseCircle } from "@mdi/js"; -import "../../../../../components/ha-textfield"; -import "../../../../../components/ha-select"; +import { LitElement, css, html, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../../common/dom/fire_event"; import "../../../../../components/ha-button"; +import "../../../../../components/ha-select"; +import type { HaSelectSelectEvent } from "../../../../../components/ha-select"; import "../../../../../components/ha-spinner"; -import "../../../../../components/ha-list-item"; -import type { HomeAssistant } from "../../../../../types"; +import "../../../../../components/ha-textfield"; import { getZwaveNodeRawConfigParameter, setZwaveNodeRawConfigParameter, } from "../../../../../data/zwave_js"; -import { fireEvent } from "../../../../../common/dom/fire_event"; +import type { HomeAssistant } from "../../../../../types"; @customElement("zwave_js-custom-param") class ZWaveJSCustomParam extends LitElement { @@ -48,10 +48,8 @@ class ZWaveJSCustomParam extends LitElement { )} .value=${String(this._valueSize)} @selected=${this._customValueSizeChanged} + .options=${["1", "2", "4"]} > - 1 - 2 - 4 - ${this.hass.localize( - "ui.panel.config.zwave_js.node_config.signed" - )} - ${this.hass.localize( - "ui.panel.config.zwave_js.node_config.unsigned" - )} - ${this.hass.localize( - "ui.panel.config.zwave_js.node_config.enumerated" - )} - ${this.hass.localize( - "ui.panel.config.zwave_js.node_config.bitfield" - )}
    @@ -129,18 +133,16 @@ class ZWaveJSCustomParam extends LitElement { ); } - private _customValueSizeChanged(ev: Event) { - this._valueSize = - this._tryParseNumber((ev.target as HTMLSelectElement).value) ?? 1; + private _customValueSizeChanged(ev: HaSelectSelectEvent) { + this._valueSize = this._tryParseNumber(ev.detail.value) ?? 1; } private _customValueChanged(ev: Event) { this._value = this._tryParseNumber((ev.target as HTMLInputElement).value); } - private _customValueFormatChanged(ev: Event) { - this._valueFormat = - this._tryParseNumber((ev.target as HTMLSelectElement).value) ?? 0; + private _customValueFormatChanged(ev: HaSelectSelectEvent) { + this._valueFormat = this._tryParseNumber(ev.detail.value) ?? 0; } private async _getCustomConfigValue() { diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-logs.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-logs.ts index 9621277490..1f0aa756e4 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-logs.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-logs.ts @@ -5,8 +5,8 @@ import { css, html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter"; import "../../../../../components/ha-icon-button"; -import "../../../../../components/ha-list-item"; import "../../../../../components/ha-select"; +import type { HaSelectSelectEvent } from "../../../../../components/ha-select"; import type { ZWaveJSLogConfig } from "../../../../../data/zwave_js"; import { fetchZWaveJSLogConfig, @@ -84,13 +84,18 @@ class ZWaveJSLogs extends SubscribeMixin(LitElement) { )} .value=${this._logConfig.level} @selected=${this._dropdownSelected} + .options=${[ + "error", + "warn", + "info", + "verbose", + "debug", + "silly", + ].map((level) => ({ + value: level, + label: capitalizeFirstLetter(level), + }))} > - Error - Warn - Info - Verbose - Debug - Silly ` : ""} @@ -133,11 +138,11 @@ class ZWaveJSLogs extends SubscribeMixin(LitElement) { ); } - private _dropdownSelected(ev) { + private _dropdownSelected(ev: HaSelectSelectEvent) { if (ev.target === undefined || this._logConfig === undefined) { return; } - const selected = ev.target.value; + const selected = ev.detail.value; if (this._logConfig.level === selected) { return; } diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts index de215f4fc8..5eb83da413 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts @@ -17,9 +17,9 @@ import type { HaProgressButton } from "../../../../../components/buttons/ha-prog import "../../../../../components/ha-alert"; import "../../../../../components/ha-card"; import "../../../../../components/ha-generic-picker"; -import "../../../../../components/ha-list-item"; import type { PickerComboBoxItem } from "../../../../../components/ha-picker-combo-box"; import "../../../../../components/ha-select"; +import type { HaSelectSelectEvent } from "../../../../../components/ha-select"; import "../../../../../components/ha-selector/ha-selector-boolean"; import "../../../../../components/ha-settings-row"; import "../../../../../components/ha-svg-icon"; @@ -43,7 +43,11 @@ import "../../../../../layouts/hass-error-screen"; import "../../../../../layouts/hass-loading-screen"; import "../../../../../layouts/hass-tabs-subpage"; import { haStyle } from "../../../../../resources/styles"; -import type { HomeAssistant, Route } from "../../../../../types"; +import type { + HomeAssistant, + Route, + ValueChangedEvent, +} from "../../../../../types"; import "../../../ha-config-section"; import { configTabs } from "./zwave_js-config-router"; import "./zwave_js-custom-param"; @@ -374,7 +378,6 @@ class ZWaveJSNodeConfig extends LitElement { return html` ${labelAndDescription} - ${Object.entries(item.metadata.states).map( - ([key, entityState]) => html` - ${entityState} - ` + .options=${Object.entries(item.metadata.states).map( + ([key, entityState]) => ({ + value: key, + label: entityState, + }) )} + > `; } @@ -457,8 +461,8 @@ class ZWaveJSNodeConfig extends LitElement { this._updateConfigParameter(ev.target, ev.detail.value ? 1 : 0); } - private _dropdownSelected(ev) { - this._handleEnumeratedPickerValueChanged(ev, ev.target.value); + private _dropdownSelected(ev: HaSelectSelectEvent) { + this._handleEnumeratedPickerValueChanged(ev, ev.detail.value); } private _pickerValueChanged(ev) { @@ -469,7 +473,7 @@ class ZWaveJSNodeConfig extends LitElement { if (ev.target === undefined || this._config![ev.target.key] === undefined) { return; } - if (this._config![ev.target.key].value?.toString() === value) { + if (this._config![ev.target.key].value === value) { return; } this._setResult(ev.target.key, undefined); @@ -547,7 +551,7 @@ class ZWaveJSNodeConfig extends LitElement { id: string, item: ZWaveJSNodeConfigParam ) { - return (ev: CustomEvent<{ value: number }>) => + return (ev: ValueChangedEvent) => this._numericInputChanged({ ...ev, target: { diff --git a/src/panels/config/integrations/show-add-integration-dialog.ts b/src/panels/config/integrations/show-add-integration-dialog.ts index 83ee10094f..8a6bc7e33d 100644 --- a/src/panels/config/integrations/show-add-integration-dialog.ts +++ b/src/panels/config/integrations/show-add-integration-dialog.ts @@ -5,6 +5,7 @@ export interface AddIntegrationDialogParams { brand?: string; domain?: string; initialFilter?: string; + navigateToResult?: boolean; } export interface YamlIntegrationDialogParams { diff --git a/src/panels/config/labels/ha-config-labels.ts b/src/panels/config/labels/ha-config-labels.ts index e4d8aaef28..895d6575c7 100644 --- a/src/panels/config/labels/ha-config-labels.ts +++ b/src/panels/config/labels/ha-config-labels.ts @@ -1,3 +1,4 @@ +import "@home-assistant/webawesome/dist/components/divider/divider"; import { mdiDelete, mdiDevices, @@ -11,10 +12,7 @@ import { import type { PropertyValues } from "lit"; import { LitElement, html, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; -import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; -import { computeCssColor } from "../../../common/color/compute-color"; -import { formatShortDateTime } from "../../../common/datetime/format_date_time"; import { storage } from "../../../common/decorators/storage"; import { navigate } from "../../../common/navigate"; import type { LocalizeFunc } from "../../../common/translations/localize"; @@ -23,10 +21,16 @@ import type { RowClickedEvent, SortingChangedEvent, } from "../../../components/data-table/ha-data-table"; +import "../../../components/ha-dropdown"; +import type { + HaDropdown, + HaDropdownSelectEvent, +} from "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-fab"; import "../../../components/ha-icon"; import "../../../components/ha-icon-button"; -import type { HaMdMenu } from "../../../components/ha-md-menu"; +import { renderLabelColorBadge } from "../../../components/ha-label-picker"; import "../../../components/ha-svg-icon"; import type { LabelRegistryEntry, @@ -44,6 +48,10 @@ import { } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-tabs-subpage-data-table"; import type { HomeAssistant, Route } from "../../../types"; +import { + getCreatedAtTableColumn, + getModifiedAtTableColumn, +} from "../common/data-table-columns"; import { configSections } from "../ha-panel-config"; import { showLabelDetailDialog } from "./show-dialog-label-detail"; @@ -89,10 +97,12 @@ export class HaConfigLabels extends LitElement { }) private _activeHiddenColumns?: string[]; - @query("#overflow-menu") private _overflowMenu?: HaMdMenu; + @query("#overflow-menu") private _overflowMenu?: HaDropdown; private _overflowLabel!: LabelRegistryEntry; + private _openingOverflow = false; + private _columns = memoizeOne((localize: LocalizeFunc, narrow: boolean) => { const columns: DataTableColumnContainer = { icon: { @@ -111,19 +121,7 @@ export class HaConfigLabels extends LitElement { showNarrow: true, label: localize("ui.panel.config.labels.headers.color"), type: "icon", - template: (label) => - html`
    `, + template: (label) => renderLabelColorBadge(label.color ?? undefined), }, name: { title: localize("ui.panel.config.labels.headers.name"), @@ -146,34 +144,8 @@ export class HaConfigLabels extends LitElement { filterable: true, hideable: true, }, - created_at: { - title: localize("ui.panel.config.generic.headers.created_at"), - defaultHidden: true, - sortable: true, - minWidth: "128px", - template: (label) => - label.created_at - ? formatShortDateTime( - new Date(label.created_at * 1000), - this.hass.locale, - this.hass.config - ) - : "—", - }, - modified_at: { - title: localize("ui.panel.config.generic.headers.modified_at"), - defaultHidden: true, - sortable: true, - minWidth: "128px", - template: (label) => - label.modified_at - ? formatShortDateTime( - new Date(label.modified_at * 1000), - this.hass.locale, - this.hass.config - ) - : "—", - }, + created_at: getCreatedAtTableColumn(localize, this.hass), + modified_at: getModifiedAtTableColumn(localize, this.hass), actions: { title: "", label: localize("ui.panel.config.generic.headers.actions"), @@ -206,13 +178,27 @@ export class HaConfigLabels extends LitElement { return; } - if (this._overflowMenu.open) { - this._overflowMenu.close(); + if (this._overflowMenu.anchorElement === ev.target) { + this._overflowMenu.anchorElement = undefined; return; } - this._overflowLabel = ev.target.selected; + this._openingOverflow = true; this._overflowMenu.anchorElement = ev.target; - this._overflowMenu.show(); + this._overflowLabel = ev.target.selected; + this._overflowMenu.open = true; + }; + + private _overflowMenuOpened = () => { + this._openingOverflow = false; + }; + + private _overflowMenuClosed = () => { + // changing the anchorElement triggers a close event, ignore it + if (this._openingOverflow || !this._overflowMenu) { + return; + } + + this._overflowMenu.anchorElement = undefined; }; protected firstUpdated(changedProperties: PropertyValues) { @@ -258,32 +244,30 @@ export class HaConfigLabels extends LitElement { - - - + + + ${this.hass.localize("ui.panel.config.entities.caption")} - - - + + + ${this.hass.localize("ui.panel.config.devices.caption")} - - - + + + ${this.hass.localize("ui.panel.config.automation.caption")} - - - - + + + + ${this.hass.localize("ui.common.delete")} - - + + `; } @@ -375,6 +359,28 @@ export class HaConfigLabels extends LitElement { } } + private _handleOverflowAction = (ev: HaDropdownSelectEvent) => { + const action = ev.detail.item.value; + + if (!action) { + return; + } + switch (action) { + case "navigate-entities": + this._navigateEntities(); + break; + case "navigate-devices": + this._navigateDevices(); + break; + case "navigate-automations": + this._navigateAutomations(); + break; + case "remove": + this._handleRemoveLabelClick(); + break; + } + }; + private _navigateEntities = () => { navigate( `/config/entities?historyBack=1&label=${this._overflowLabel.label_id}` diff --git a/src/panels/config/labs/ha-config-labs.ts b/src/panels/config/labs/ha-config-labs.ts index 843b776265..803243eee7 100644 --- a/src/panels/config/labs/ha-config-labs.ts +++ b/src/panels/config/labs/ha-config-labs.ts @@ -5,31 +5,31 @@ import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import type { LocalizeFunc } from "../../../common/translations/localize"; import { extractSearchParam } from "../../../common/url/search-params"; -import { domainToName } from "../../../data/integration"; -import { - labsUpdatePreviewFeature, - subscribeLabFeatures, -} from "../../../data/labs"; -import type { LabPreviewFeature } from "../../../data/labs"; -import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; -import type { HomeAssistant } from "../../../types"; -import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; -import { brandsUrl } from "../../../util/brands-url"; -import { showToast } from "../../../util/toast"; -import { documentationUrl } from "../../../util/documentation-url"; -import { haStyle } from "../../../resources/styles"; -import { showLabsPreviewFeatureEnableDialog } from "./show-dialog-labs-preview-feature-enable"; -import { - showLabsProgressDialog, - closeLabsProgressDialog, -} from "./show-dialog-labs-progress"; import "../../../components/ha-alert"; import "../../../components/ha-button"; import "../../../components/ha-card"; import "../../../components/ha-icon-button"; import "../../../components/ha-markdown"; import "../../../components/ha-switch"; +import { domainToName } from "../../../data/integration"; +import type { LabPreviewFeature } from "../../../data/labs"; +import { + labsUpdatePreviewFeature, + subscribeLabFeatures, +} from "../../../data/labs"; +import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-subpage"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; +import { haStyle } from "../../../resources/styles"; +import type { HomeAssistant } from "../../../types"; +import { brandsUrl } from "../../../util/brands-url"; +import { documentationUrl } from "../../../util/documentation-url"; +import { showToast } from "../../../util/toast"; +import { showLabsPreviewFeatureEnableDialog } from "./show-dialog-labs-preview-feature-enable"; +import { + closeLabsProgressDialog, + showLabsProgressDialog, +} from "./show-dialog-labs-progress"; @customElement("ha-config-labs") class HaConfigLabs extends SubscribeMixin(LitElement) { @@ -42,21 +42,31 @@ class HaConfigLabs extends SubscribeMixin(LitElement) { @state() private _highlightedPreviewFeature?: string; private _sortedPreviewFeatures = memoizeOne( - (localize: LocalizeFunc, features: LabPreviewFeature[]) => - // Sort by localized integration name alphabetically - [...features].sort((a, b) => - domainToName(localize, a.domain).localeCompare( + (localize: LocalizeFunc, features: LabPreviewFeature[]) => { + const featuresToSort = [...features]; + + return featuresToSort.sort((a, b) => { + // Place frontend.winter_mode at the bottom + if (a.domain === "frontend" && a.preview_feature === "winter_mode") + return 1; + if (b.domain === "frontend" && b.preview_feature === "winter_mode") + return -1; + + // Sort everything else alphabetically + return domainToName(localize, a.domain).localeCompare( domainToName(localize, b.domain) - ) - ) + ); + }); + } ); public hassSubscribe() { return [ subscribeLabFeatures(this.hass.connection, (features) => { - // Load title translations for integrations with preview features + // Load title and preview_features translations for integrations with preview features const domains = [...new Set(features.map((f) => f.domain))]; this.hass.loadBackendTranslation("title", domains); + this.hass.loadBackendTranslation("preview_features", domains); this._preview_features = features; }), @@ -165,6 +175,8 @@ class HaConfigLabs extends SubscribeMixin(LitElement) { private _renderPreviewFeature( preview_feature: LabPreviewFeature ): TemplateResult { + const previewFeatureId = `${preview_feature.domain}.${preview_feature.preview_feature}`; + const featureName = this.hass.localize( `component.${preview_feature.domain}.preview_features.${preview_feature.preview_feature}.name` ); @@ -182,7 +194,6 @@ class HaConfigLabs extends SubscribeMixin(LitElement) { ? `${integrationName} • ${this.hass.localize("ui.panel.config.labs.custom_integration")}` : integrationName; - const previewFeatureId = `${preview_feature.domain}.${preview_feature.preview_feature}`; const isHighlighted = this._highlightedPreviewFeature === previewFeatureId; // Build description with learn more link if available @@ -382,6 +393,7 @@ class HaConfigLabs extends SubscribeMixin(LitElement) { css` :host { display: block; + height: 100%; } a[slot="toolbar-icon"] { @@ -392,7 +404,6 @@ class HaConfigLabs extends SubscribeMixin(LitElement) { max-width: 800px; margin: 0 auto; padding: var(--ha-space-4); - min-height: calc(100vh - 64px); display: flex; flex-direction: column; } diff --git a/src/panels/config/logs/dialog-download-logs.ts b/src/panels/config/logs/dialog-download-logs.ts index e651c3cd9f..4b49f57cdd 100644 --- a/src/panels/config/logs/dialog-download-logs.ts +++ b/src/panels/config/logs/dialog-download-logs.ts @@ -1,15 +1,12 @@ -import { mdiClose } from "@mdi/js"; import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-button"; -import "../../../components/ha-dialog-header"; -import "../../../components/ha-icon-button"; -import "../../../components/ha-md-dialog"; -import type { HaMdDialog } from "../../../components/ha-md-dialog"; +import "../../../components/ha-dialog-footer"; import "../../../components/ha-md-select"; import "../../../components/ha-md-select-option"; +import "../../../components/ha-wa-dialog"; import { getSignedPath } from "../../../data/auth"; import { getHassioLogDownloadLinesUrl } from "../../../data/hassio/supervisor"; import { haStyle, haStyleDialog } from "../../../resources/styles"; @@ -25,17 +22,19 @@ class DownloadLogsDialog extends LitElement { @state() private _dialogParams?: DownloadLogsDialogParams; - @state() private _lineCount = DEFAULT_LINE_COUNT; + @state() private _open = false; - @query("ha-md-dialog") private _dialogElement!: HaMdDialog; + @state() private _lineCount = DEFAULT_LINE_COUNT; public showDialog(dialogParams: DownloadLogsDialogParams) { this._dialogParams = dialogParams; - this._lineCount = this._dialogParams?.defaultLineCount || 500; + this._lineCount = + this._dialogParams?.defaultLineCount || DEFAULT_LINE_COUNT; + this._open = true; } public closeDialog() { - this._dialogElement.close(); + this._open = false; } private _dialogClosed() { @@ -55,25 +54,28 @@ class DownloadLogsDialog extends LitElement { numberOfLinesOptions.sort((a, b) => a - b); } + const headerSubtitle = `${this._dialogParams.header}${ + this._dialogParams.boot === 0 + ? "" + : ` · ${ + this._dialogParams.boot === -1 + ? this.hass.localize("ui.panel.config.logs.previous") + : this.hass.localize("ui.panel.config.logs.startups_ago", { + boot: this._dialogParams.boot * -1, + }) + }` + }`; + return html` - - - - - ${this.hass.localize("ui.panel.config.logs.download_logs")} - - - ${this._dialogParams.header}${this._dialogParams.boot === 0 - ? "" - : ` · ${this._dialogParams.boot === -1 ? this.hass.localize("ui.panel.config.logs.previous") : this.hass.localize("ui.panel.config.logs.startups_ago", { boot: this._dialogParams.boot * -1 })}`} - - -
    + +
    ${this.hass.localize( "ui.panel.config.logs.select_number_of_lines" @@ -93,15 +95,19 @@ class DownloadLogsDialog extends LitElement { )}
    -
    - + + ${this.hass.localize("ui.common.cancel")} - + ${this.hass.localize("ui.common.download")} -
    - + + `; } @@ -135,7 +141,6 @@ class DownloadLogsDialog extends LitElement { css` :host { direction: var(--direction); - --dialog-content-overflow: visible; } .content { display: flex; diff --git a/src/panels/config/logs/error-log-card.ts b/src/panels/config/logs/error-log-card.ts index 1c0e742240..a902234011 100644 --- a/src/panels/config/logs/error-log-card.ts +++ b/src/panels/config/logs/error-log-card.ts @@ -1,5 +1,4 @@ -import type { ActionDetail } from "@material/mwc-list"; - +import "@home-assistant/webawesome/dist/components/divider/divider"; import { mdiArrowCollapseDown, mdiCircle, @@ -31,13 +30,10 @@ import "../../../components/ha-alert"; import "../../../components/ha-ansi-to-html"; import type { HaAnsiToHtml } from "../../../components/ha-ansi-to-html"; import "../../../components/ha-button"; -import "../../../components/ha-button-menu"; import "../../../components/ha-card"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-icon-button"; -import "../../../components/ha-list-item"; -import "../../../components/ha-md-divider"; -import "../../../components/ha-md-menu"; -import "../../../components/ha-md-menu-item"; import "../../../components/ha-spinner"; import "../../../components/ha-svg-icon"; @@ -48,7 +44,7 @@ import { atLeastVersion } from "../../../common/config/version"; import { fireEvent, type HASSDomEvent } from "../../../common/dom/fire_event"; import type { LocalizeFunc } from "../../../common/translations/localize"; import { debounce } from "../../../common/util/debounce"; -import type { HaMdMenu } from "../../../components/ha-md-menu"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; import type { ConnectionStatus } from "../../../data/connection-status"; import { fetchErrorLog, getErrorLogDownloadUrl } from "../../../data/error_log"; import { extractApiErrorMessage } from "../../../data/hassio/common"; @@ -58,14 +54,10 @@ import { fetchHassioLogsFollow, fetchHassioLogsFollowSkip, fetchHassioLogsLegacy, - getHassioLogDownloadLinesUrl, getHassioLogDownloadUrl, } from "../../../data/hassio/supervisor"; import type { HomeAssistant } from "../../../types"; -import { - downloadFileSupported, - fileDownload, -} from "../../../util/file_download"; +import { fileDownload } from "../../../util/file_download"; import { showDownloadLogsDialog } from "./show-dialog-download-logs"; const NUMBER_OF_LINES = 100; @@ -94,8 +86,6 @@ class ErrorLogCard extends LitElement { @query("ha-ansi-to-html") private _ansiToHtmlElement?: HaAnsiToHtml; - @query("#boots-menu") private _bootsMenu?: HaMdMenu; - @state() private _firstCursor?: string; @state() private _scrolledToBottomController = @@ -132,10 +122,6 @@ class ErrorLogCard extends LitElement { @state() private _wrapLines = true; - @state() private _downloadSupported?: boolean; - - @state() private _logsFileLink?: string; - protected render(): TemplateResult { const streaming = this._streamSupported && @@ -159,35 +145,30 @@ class ErrorLogCard extends LitElement {
    ${hasBoots && this._showBootsSelect ? html` - - - + + + + ${this._boots!.map( (boot) => html` - ${boot === 0 @@ -198,41 +179,20 @@ class ErrorLogCard extends LitElement { "ui.panel.config.logs.startups_ago", { boot: boot * -1 } )} - + ${boot === 0 - ? html`` + ? html`` : nothing} ` )} - + ` : nothing} - ${this._downloadSupported - ? html` - - ` - : this._logsFileLink - ? html` - - - - ` - : nothing} + - - + + ${this.allowSwitch && this.provider === "core" - ? html` + ? html` ${this.hass.localize( "ui.panel.config.logs.show_condensed_logs" )} - ` + ` : nothing} ${hasBoots - ? html` + ? html` ${localize( `ui.panel.config.logs.${this._showBootsSelect ? "hide" : "show"}_haos_boots` )} - ` + ` : nothing} - + ` : nothing}
    @@ -337,9 +300,11 @@ class ErrorLogCard extends LitElement { protected willUpdate(changedProps: PropertyValues) { super.willUpdate(changedProps); if (!this.hasUpdated) { - this._downloadSupported = downloadFileSupported(this.hass); - this._streamSupported = - !__SUPERVISOR__ || atLeastVersion(this.hass.config.version, 2024, 11); + this._streamSupported = atLeastVersion( + this.hass.config.version, + 2024, + 11 + ); // just needs to be loaded once, because only the host endpoints provide boots information this._loadBoots(); @@ -539,17 +504,6 @@ class ErrorLogCard extends LitElement { this._newLogsIndicator = true; } - if (!this._downloadSupported) { - const downloadUrl = getHassioLogDownloadLinesUrl( - this.provider!, - this._numberOfLines, - this._boot - ); - getSignedPath(this.hass, downloadUrl).then((signedUrl) => { - this._logsFileLink = signedUrl.path; - }); - } - // first chunk loads successfully, reset retry param retry = false; } @@ -719,30 +673,31 @@ class ErrorLogCard extends LitElement { this._loadLogs(); } - private _handleOverflowAction(ev: CustomEvent) { - let index = ev.detail.index; - if (this.provider === "core") { - index--; + private _handleOverflowAction(ev: HaDropdownSelectEvent) { + const action = ev.detail.item.value; + + if (!action) { + return; } - switch (index) { - case -1: + + if (action.startsWith("boot_")) { + this._setBoot(Number(action.substring(5))); + return; + } + + switch (action) { + case "switch-log-view": // @ts-ignore fireEvent(this, "switch-log-view"); break; - case 0: + case "toggle-boots": this._showBootsSelect = !this._showBootsSelect; break; } } - private _toggleBootsMenu() { - if (this._bootsMenu) { - this._bootsMenu.open = !this._bootsMenu.open; - } - } - - private _setBoot(ev: any) { - this._boot = ev.target.value; + private _setBoot(boot: number) { + this._boot = boot; this._loadLogs(); } @@ -795,6 +750,10 @@ class ErrorLogCard extends LitElement { float: right; } + .card-content { + margin-top: -8px; + } + .error-log { position: relative; font-family: var(--ha-font-family-code); @@ -802,7 +761,7 @@ class ErrorLogCard extends LitElement { text-align: start; padding-top: 16px; padding-bottom: 16px; - overflow-y: scroll; + overflow: auto; min-height: var(--error-log-card-height, calc(100vh - 244px)); max-height: var(--error-log-card-height, calc(100vh - 244px)); border-top: 1px solid var(--divider-color); diff --git a/src/panels/config/logs/ha-config-logs.ts b/src/panels/config/logs/ha-config-logs.ts index dcc995d931..4ee5f96b79 100644 --- a/src/panels/config/logs/ha-config-logs.ts +++ b/src/panels/config/logs/ha-config-logs.ts @@ -27,7 +27,7 @@ import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-subpage"; import { mdiHomeAssistant } from "../../../resources/home-assistant-logo-svg"; import { haStyle } from "../../../resources/styles"; -import type { HomeAssistant, Route } from "../../../types"; +import type { HomeAssistant, Route, ValueChangedEvent } from "../../../types"; import "./error-log-card"; import "./system-log-card"; import type { SystemLogCard } from "./system-log-card"; @@ -201,7 +201,7 @@ export class HaConfigLogs extends LitElement { this.providerPicker?.open(); } - private _handleDropdownSelect(ev: CustomEvent<{ value: string }>) { + private _handleDropdownSelect(ev: ValueChangedEvent) { const provider = ev.detail?.value; if (!provider) { return; diff --git a/src/panels/config/logs/system-log-card.ts b/src/panels/config/logs/system-log-card.ts index 6c858c7f99..fda72294a6 100644 --- a/src/panels/config/logs/system-log-card.ts +++ b/src/panels/config/logs/system-log-card.ts @@ -5,8 +5,9 @@ import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; import type { LocalizeFunc } from "../../../common/translations/localize"; import "../../../components/buttons/ha-call-service-button"; -import "../../../components/ha-button-menu"; import "../../../components/ha-card"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-icon-button"; import "../../../components/ha-list"; import "../../../components/ha-list-item"; @@ -24,6 +25,7 @@ import type { HomeAssistant } from "../../../types"; import { fileDownload } from "../../../util/file_download"; import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail"; import { formatSystemLogTime } from "./util"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; @customElement("system-log-card") export class SystemLogCard extends LitElement { @@ -121,19 +123,19 @@ export class SystemLogCard extends LitElement { .label=${this.hass.localize("ui.common.refresh")} > - - - - - + + + + ${this.hass.localize( "ui.panel.config.logs.show_full_logs" )} - - + +
    ${this._items.length === 0 @@ -221,9 +223,11 @@ export class SystemLogCard extends LitElement { } } - private _handleOverflowAction() { - // @ts-ignore - fireEvent(this, "switch-log-view"); + private _handleOverflowAction(ev: HaDropdownSelectEvent) { + if (ev.detail.item.value === "show-full-logs") { + // @ts-ignore + fireEvent(this, "switch-log-view"); + } } private async _downloadLogs() { diff --git a/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts b/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts index 01ce8be937..763cbc100d 100644 --- a/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts +++ b/src/panels/config/lovelace/dashboards/dialog-lovelace-dashboard-detail.ts @@ -60,7 +60,6 @@ export class DialogLovelaceDashboardDetail extends LitElement { } const titleInvalid = !this._data.title || !this._data.title.trim(); - const isLovelaceDashboard = this._params.urlPath === "lovelace"; return html`
    - ${this._params.dashboard && !this._params.dashboard.id + ${this._params.dashboard?.mode === "yaml" ? this.hass.localize( "ui.panel.config.lovelace.dashboards.cant_edit_yaml" ) - : isLovelaceDashboard - ? this.hass.localize( - "ui.panel.config.lovelace.dashboards.cant_edit_lovelace" - ) - : html` - - `} + : html` + + `}
    ${this._params.urlPath ? html` - ${this._params.dashboard?.id + ${this._params.dashboard?.mode === "storage" ? html` ${this._params.urlPath - ? this._params.dashboard?.id + ? this._params.dashboard?.mode === "storage" ? this.hass.localize( "ui.panel.config.lovelace.dashboards.detail.update" ) @@ -237,8 +232,9 @@ export class DialogLovelaceDashboardDetail extends LitElement { } private async _updateDashboard() { - if (this._params?.urlPath && !this._params.dashboard?.id) { + if (this._params?.urlPath && this._params.dashboard?.mode === "yaml") { this.closeDialog(); + return; } this._submitting = true; try { diff --git a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts index af27793629..5562df620a 100644 --- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts +++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts @@ -8,7 +8,7 @@ import { mdiPlus, } from "@mdi/js"; import type { PropertyValues } from "lit"; -import { LitElement, html, nothing } from "lit"; +import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoize from "memoize-one"; import { storage } from "../../../../common/decorators/storage"; @@ -21,16 +21,15 @@ import type { SortingChangedEvent, } from "../../../../components/data-table/ha-data-table"; import "../../../../components/ha-button"; +import "../../../../components/ha-dropdown"; +import "../../../../components/ha-dropdown-item"; import "../../../../components/ha-fab"; import "../../../../components/ha-icon"; import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-overflow-menu"; -import "../../../../components/ha-md-button-menu"; -import "../../../../components/ha-md-list-item"; import "../../../../components/ha-svg-icon"; import "../../../../components/ha-tooltip"; import { saveFrontendSystemData } from "../../../../data/frontend"; -import type { LovelacePanelConfig } from "../../../../data/lovelace"; import type { LovelaceRawConfig } from "../../../../data/lovelace/config/types"; import { isStrategyDashboard, @@ -282,7 +281,8 @@ export class HaConfigLovelaceDashboards extends LitElement { action: () => this._handleSetAsDefault(dashboard), disabled: dashboard.default, }, - ...(dashboard.type === "user_created" + ...(dashboard.type === "user_created" && + dashboard.mode === "storage" ? [ { path: mdiPencil, @@ -313,23 +313,7 @@ export class HaConfigLovelaceDashboards extends LitElement { private _getItems = memoize( (dashboards: LovelaceDashboard[], defaultUrlPath: string | null) => { - const mode = (this.hass.panels?.lovelace?.config as LovelacePanelConfig) - .mode; - const isDefault = defaultUrlPath === "lovelace"; - const result: DataTableItem[] = [ - { - icon: "mdi:view-dashboard", - title: this.hass.localize("panel.states"), - default: isDefault, - show_in_sidebar: true, - require_admin: false, - url_path: "lovelace", - mode: mode, - filename: mode === "yaml" ? "ui-lovelace.yaml" : "", - type: "built_in", - localized_type: this._localizeType("built_in"), - }, - ]; + const result: DataTableItem[] = []; PANEL_DASHBOARDS.forEach((panel) => { const panelInfo = this.hass.panels[panel]; @@ -413,16 +397,20 @@ export class HaConfigLovelaceDashboards extends LitElement { has-fab clickable > - + - - ${this.hass.localize("ui.panel.config.lovelace.resources.caption")} - - + + + ${this.hass.localize( + "ui.panel.config.lovelace.resources.caption" + )} + + + + ${isYamlMode + ? html` + + ` + : ""} { + if (this._lovelaceInfo?.resource_mode !== "storage") { + showAlertDialog(this, { + text: this.hass!.localize( + "ui.panel.config.lovelace.resources.cant_edit_yaml" + ), + }); + return false; + } + const resource = event.currentTarget.resource as LovelaceResource; if ( @@ -303,6 +338,21 @@ export class HaConfigLovelaceResources extends LitElement { this._activeHiddenColumns = ev.detail.hiddenColumns; } + private _handleReloadResources() { + this.hass.callService("lovelace", "reload_resources"); + showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.lovelace.resources.reload_refresh_header" + ), + text: this.hass.localize( + "ui.panel.config.lovelace.resources.reload_refresh_body" + ), + confirmText: this.hass.localize("ui.common.refresh"), + dismissText: this.hass.localize("ui.common.not_now"), + confirm: () => location.reload(), + }); + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/config/network/supervisor-network.ts b/src/panels/config/network/supervisor-network.ts index 0f1d0ac7e5..c9509241ed 100644 --- a/src/panels/config/network/supervisor-network.ts +++ b/src/panels/config/network/supervisor-network.ts @@ -4,8 +4,9 @@ import { customElement, property, state } from "lit/decorators"; import { cache } from "lit/directives/cache"; import "../../../components/ha-alert"; import "../../../components/ha-button"; -import "../../../components/ha-button-menu"; import "../../../components/ha-card"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-expansion-panel"; import "../../../components/ha-formfield"; import "../../../components/ha-icon-button"; @@ -35,6 +36,7 @@ import { showConfirmationDialog, } from "../../../dialogs/generic/show-dialog-box"; import type { HomeAssistant } from "../../../types"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; const IP_VERSIONS = ["ipv4", "ipv6"]; @@ -476,9 +478,9 @@ export class HassioNetwork extends LitElement {
    - ${this.hass.localize( @@ -519,27 +520,38 @@ export class HassioNetwork extends LitElement { ${Object.entries(PREDEFINED_DNS[version]).map( ([name, addresses]) => html` - ${name} - + ` )} - + ${this.hass.localize( "ui.panel.config.network.supervisor.custom_dns" )} - - + + ` : nothing} `; } + private _getPredefinedDnsName(nameserver: string, version: string) { + for (const [name, addresses] of Object.entries( + PREDEFINED_DNS[version as "ipv4" | "ipv6"] + )) { + if (addresses.includes(nameserver)) { + return ` - ${name}`; + } + } + return ""; + } + private async _updateNetwork() { this._processing = true; let interfaceOptions: Partial = {}; @@ -747,10 +759,7 @@ export class HassioNetwork extends LitElement { this._dnsMenuOpen = false; } - private _addPredefinedDNS(ev: Event) { - const source = ev.target as any; - const version = source.version as "ipv4" | "ipv6"; - const addresses = source.addresses as string[]; + private _addPredefinedDNS(version: "ipv4" | "ipv6", addresses: string[]) { if (!this._interface![version]!.nameservers) { this._interface![version]!.nameservers = []; } @@ -759,9 +768,7 @@ export class HassioNetwork extends LitElement { this.requestUpdate("_interface"); } - private _addCustomDNS(ev: Event) { - const source = ev.target as any; - const version = source.version as "ipv4" | "ipv6"; + private _addCustomDNS(version: "ipv4" | "ipv6") { if (!this._interface![version]!.nameservers) { this._interface![version]!.nameservers = []; } @@ -779,6 +786,22 @@ export class HassioNetwork extends LitElement { this.requestUpdate("_interface"); } + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { + const action = ev.detail?.item?.value; + + if (action === "add_predefined") { + this._addPredefinedDNS( + (ev.detail.item as any).version, + (ev.detail.item as any).addresses + ); + return; + } + + if (action === "add_custom") { + this._addCustomDNS((ev.detail.item as any).version); + } + } + static get styles(): CSSResultGroup { return [ css` @@ -817,6 +840,10 @@ export class HassioNetwork extends LitElement { --mdc-icon-button-size: 36px; margin-top: 16px; } + ha-dropdown { + display: block; + } + .add-address, .add-nameserver { margin-top: 16px; diff --git a/src/panels/config/repairs/dialog-integration-startup.ts b/src/panels/config/repairs/dialog-integration-startup.ts index 95a4c17849..6088f1021c 100644 --- a/src/panels/config/repairs/dialog-integration-startup.ts +++ b/src/panels/config/repairs/dialog-integration-startup.ts @@ -2,8 +2,7 @@ import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; -import "../../../components/ha-card"; -import { createCloseHeading } from "../../../components/ha-dialog"; +import "../../../components/ha-wa-dialog"; import { haStyleDialog } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import "./integrations-startup-time"; @@ -12,44 +11,47 @@ import "./integrations-startup-time"; class DialogIntegrationStartup extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _opened = false; + @state() private _open = false; public showDialog(): void { - this._opened = true; + this._open = true; } public closeDialog() { - this._opened = false; + this._open = false; + } + + private _dialogClosed(): void { + this._open = false; fireEvent(this, "dialog-closed", { dialog: this.localName }); } protected render() { - if (!this._opened) { + if (!this._open) { return nothing; } return html` - - + `; } static styles: CSSResultGroup = [ haStyleDialog, css` - ha-dialog { + ha-wa-dialog { --dialog-content-padding: 0; } `, diff --git a/src/panels/config/repairs/dialog-system-information.ts b/src/panels/config/repairs/dialog-system-information.ts index 61f1f38c87..c08a398d98 100644 --- a/src/panels/config/repairs/dialog-system-information.ts +++ b/src/panels/config/repairs/dialog-system-information.ts @@ -9,8 +9,8 @@ import { copyToClipboard } from "../../../common/util/copy-clipboard"; import { subscribePollingCollection } from "../../../common/util/subscribe-polling"; import "../../../components/ha-alert"; import "../../../components/ha-button"; -import "../../../components/ha-card"; -import { createCloseHeading } from "../../../components/ha-dialog"; +import "../../../components/ha-dialog-footer"; +import "../../../components/ha-wa-dialog"; import "../../../components/ha-metric"; import "../../../components/ha-spinner"; import type { HassioStats } from "../../../data/hassio/common"; @@ -62,20 +62,24 @@ class DialogSystemInformation extends LitElement { @state() private _coreStats?: HassioStats; - @state() private _opened = false; + @state() private _open = false; private _systemHealthSubscription?: Promise; private _hassIOSubscription?: UnsubscribeFunc; public showDialog(): void { - this._opened = true; + this._open = true; this.hass!.loadBackendTranslation("system_health"); this._subscribe(); } public closeDialog() { - this._opened = false; + this._open = false; + } + + private _dialogClosed(): void { + this._open = false; this._unsubscribe(); fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -126,20 +130,20 @@ class DialogSystemInformation extends LitElement { } protected render() { - if (!this._opened) { + if (!this._open) { return nothing; } const sections = this._getSections(); return html` -
    ${this._resolutionInfo @@ -224,10 +228,12 @@ class DialogSystemInformation extends LitElement {
    `}
    - - ${this.hass.localize("ui.panel.config.repairs.copy")} - -
    + + + ${this.hass.localize("ui.panel.config.repairs.copy")} + + + `; } diff --git a/src/panels/config/repairs/ha-config-repairs-dashboard.ts b/src/panels/config/repairs/ha-config-repairs-dashboard.ts index cbbe39f394..8424758fb8 100644 --- a/src/panels/config/repairs/ha-config-repairs-dashboard.ts +++ b/src/panels/config/repairs/ha-config-repairs-dashboard.ts @@ -1,17 +1,16 @@ -import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item-base"; +import "@home-assistant/webawesome/dist/components/divider/divider"; import { mdiDotsVertical } from "@mdi/js"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { TemplateResult } from "lit"; -import { css, html, LitElement } from "lit"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; -import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; import { navigate } from "../../../common/navigate"; import { extractSearchParam } from "../../../common/url/search-params"; import "../../../components/ha-card"; -import "../../../components/ha-check-list-item"; -import "../../../components/ha-list-item"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import type { RepairsIssue } from "../../../data/repairs"; import { severitySort, @@ -23,6 +22,7 @@ import type { HomeAssistant } from "../../../types"; import "./ha-config-repairs"; import { showIntegrationStartupDialog } from "./show-integration-startup-dialog"; import { showSystemInformationDialog } from "./show-system-information-dialog"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; @customElement("ha-config-repairs-dashboard") class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) { @@ -81,40 +81,36 @@ class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) { .header=${this.hass.localize("ui.panel.config.repairs.caption")} >
    - + - ${this.hass.localize("ui.panel.config.repairs.show_ignored")} - -
  • + + ${isComponentLoaded(this.hass, "system_health") || isComponentLoaded(this.hass, "hassio") ? html` - + ${this.hass.localize( "ui.panel.config.repairs.system_information" )} - + ` - : ""} - + : nothing} + ${this.hass.localize( "ui.panel.config.repairs.integration_startup_time" )} - -
    + +
    @@ -141,34 +137,34 @@ class HaConfigRepairsDashboard extends SubscribeMixin(LitElement) { `; } - private _showSystemInformationDialog( - ev: CustomEvent - ): void { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } - + private _showSystemInformationDialog(): void { showSystemInformationDialog(this); } - private _showIntegrationStartupDialog( - ev: CustomEvent - ): void { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } - + private _showIntegrationStartupDialog(): void { showIntegrationStartupDialog(this); } - private _toggleIgnored(ev: CustomEvent): void { - if (ev.detail.source !== "property") { - return; - } - + private _toggleIgnored(): void { this._showIgnored = !this._showIgnored; } + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { + const action = ev.detail?.item?.value; + + switch (action) { + case "toggle_ignored": + this._toggleIgnored(); + break; + case "system_information": + this._showSystemInformationDialog(); + break; + case "integration_startup_time": + this._showIntegrationStartupDialog(); + break; + } + } + static styles = css` .content { padding: 28px 20px 0; diff --git a/src/panels/config/repairs/ha-config-repairs.ts b/src/panels/config/repairs/ha-config-repairs.ts index b658c3db33..0dbe0ac424 100644 --- a/src/panels/config/repairs/ha-config-repairs.ts +++ b/src/panels/config/repairs/ha-config-repairs.ts @@ -2,6 +2,7 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import { relativeTime } from "../../../common/datetime/relative_time"; import { capitalizeFirstLetter } from "../../../common/string/capitalize-first-letter"; +import { STRINGS_SEPARATOR_DOT } from "../../../common/const"; import "../../../components/ha-md-list"; import "../../../components/ha-md-list-item"; import { domainToName } from "../../../data/integration"; @@ -12,7 +13,7 @@ import { import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; import type { HomeAssistant } from "../../../types"; import { brandsUrl } from "../../../util/brands-url"; -import { fixStatisticsIssue } from "../../developer-tools/statistics/fix-statistics"; +import { fixStatisticsIssue } from "../developer-tools/statistics/fix-statistics"; import { showRepairsFlowDialog } from "./show-dialog-repair-flow"; import { showRepairsIssueDialog } from "./show-repair-issue-dialog"; import type { StatisticsValidationResult } from "../../../data/recorder"; @@ -99,7 +100,7 @@ class HaConfigRepairs extends LitElement { ${(issue.severity === "critical" || issue.severity === "error") && issue.created - ? " · " + ? STRINGS_SEPARATOR_DOT : ""} ${createdBy ? html`${createdBy}` diff --git a/src/panels/config/scene/ha-scene-dashboard.ts b/src/panels/config/scene/ha-scene-dashboard.ts index ea0ff3f1e0..525e482fd5 100644 --- a/src/panels/config/scene/ha-scene-dashboard.ts +++ b/src/panels/config/scene/ha-scene-dashboard.ts @@ -1,7 +1,7 @@ +import "@home-assistant/webawesome/dist/components/divider/divider"; import { ResizeController } from "@lit-labs/observers/resize-controller"; import { consume } from "@lit/context"; import { - mdiChevronRight, mdiCog, mdiContentDuplicate, mdiDelete, @@ -11,27 +11,23 @@ import { mdiMenuDown, mdiOpenInNew, mdiPalette, - mdiPencilOff, + mdiPencil, mdiPlay, mdiPlus, mdiTag, mdiTextureBox, } from "@mdi/js"; -import { differenceInDays } from "date-fns"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { computeCssColor } from "../../../common/color/compute-color"; -import { formatShortDateTimeWithConditionalYear } from "../../../common/datetime/format_date_time"; -import { relativeTime } from "../../../common/datetime/relative_time"; import { storage } from "../../../common/decorators/storage"; import type { HASSDomEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { navigate } from "../../../common/navigate"; -import { slugify } from "../../../common/string/slugify"; import type { LocalizeFunc } from "../../../common/translations/localize"; import { hasRejectedItems, @@ -45,6 +41,8 @@ import type { } from "../../../components/data-table/ha-data-table"; import "../../../components/data-table/ha-data-table-labels"; import "../../../components/ha-button"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-fab"; import "../../../components/ha-filter-categories"; import "../../../components/ha-filter-devices"; @@ -54,9 +52,6 @@ import "../../../components/ha-filter-labels"; import "../../../components/ha-filter-voice-assistants"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-overflow-menu"; -import "../../../components/ha-md-divider"; -import "../../../components/ha-md-menu"; -import "../../../components/ha-md-menu-item"; import "../../../components/ha-state-icon"; import "../../../components/ha-sub-menu"; import "../../../components/ha-svg-icon"; @@ -72,14 +67,16 @@ import { fullEntitiesContext } from "../../../data/context"; import type { DataTableFilters } from "../../../data/data_table_filters"; import { deserializeFilters, + isFilterUsed, + isRelatedItemsFilterUsed, serializeFilters, } from "../../../data/data_table_filters"; -import { isUnavailableState } from "../../../data/entity/entity"; import type { EntityRegistryEntry, UpdateEntityRegistryEntryResult, } from "../../../data/entity/entity_registry"; import { updateEntityRegistryEntry } from "../../../data/entity/entity_registry"; +import { getEntityVoiceAssistantsIds } from "../../../data/expose"; import { forwardHaptic } from "../../../data/haptics"; import type { LabelRegistryEntry } from "../../../data/label/label_registry"; import { @@ -91,6 +88,7 @@ import { activateScene, deleteScene, getSceneConfig, + saveScene, showSceneEditor, } from "../../../data/scene"; import { @@ -109,20 +107,29 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; import { configSections } from "../ha-panel-config"; import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; -import { getEntityVoiceAssistantsIds } from "../../../data/expose"; -import { getAvailableAssistants } from "../voice-assistants/expose/available-assistants"; import { - getAssistantsTableColumn, + getAreaTableColumn, + getCategoryTableColumn, + getLabelsTableColumn, + getEditableTableColumn, + renderRelativeTimeColumn, +} from "../common/data-table-columns"; +import { getAssistantsSortableKey, + getAssistantsTableColumn, } from "../voice-assistants/expose/assistants-table-column"; +import { getAvailableAssistants } from "../voice-assistants/expose/available-assistants"; +import { showSceneSaveDialog } from "./scene-save-dialog/show-dialog-scene-save"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; type SceneItem = SceneEntity & { name: string; area: string | undefined; category: string | undefined; - labels: LabelRegistryEntry[]; + label_entries: LabelRegistryEntry[]; assistants: string[]; assistants_sortable_key: string | undefined; + editable: boolean; }; @customElement("ha-scene-dashboard") @@ -145,7 +152,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { @state() private _activeFilters?: string[]; - @state() private _filteredScenes?: string[] | null; + @state() private _filteredSceneEntityIds?: string[] | null; @state() @storage({ @@ -249,12 +256,13 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { category: category ? categoryReg?.find((cat) => cat.category_id === category)?.name : undefined, - labels: (labels || []).map( + label_entries: (labels || []).map( (lbl) => labelReg!.find((label) => label.label_id === lbl)! ), assistants, assistants_sortable_key: getAssistantsSortableKey(assistants), selectable: entityRegEntry !== undefined, + editable: Boolean(scene.attributes.id), }; }); } @@ -287,89 +295,34 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { direction: "asc", flex: 2, extraTemplate: (scene) => - scene.labels.length + scene.label_entries.length ? html`` : nothing, }, - area: { - title: localize("ui.panel.config.scene.picker.headers.area"), - groupable: true, - filterable: true, - sortable: true, - }, - category: { - title: localize("ui.panel.config.scene.picker.headers.category"), - defaultHidden: true, - groupable: true, - filterable: true, - sortable: true, - }, - labels: { - title: "", - hidden: true, - filterable: true, - template: (scene) => scene.labels.map((lbl) => lbl.name).join(" "), - }, + area: getAreaTableColumn(localize), + category: getCategoryTableColumn(localize), + labels: getLabelsTableColumn(), state: { title: localize( "ui.panel.config.scene.picker.headers.last_activated" ), sortable: true, - template: (scene) => { - const lastActivated = scene.state; - if (!lastActivated || isUnavailableState(lastActivated)) { - return localize("ui.components.relative_time.never"); - } - const date = new Date(scene.state); - const now = new Date(); - const dayDifference = differenceInDays(now, date); - const formattedTime = formatShortDateTimeWithConditionalYear( - date, - this.hass.locale, - this.hass.config - ); - const elementId = "last-activated-" + slugify(scene.entity_id); - return html` - ${dayDifference > 3 - ? formattedTime - : html` - ${formattedTime} - ${relativeTime(date, this.hass.locale)} - `} - `; - }, - }, - only_editable: { - title: "", - label: this.hass.localize( - "ui.panel.config.scene.picker.headers.editable" - ), - type: "icon", - showNarrow: true, template: (scene) => - !scene.attributes.id - ? html` - - - ${this.hass.localize( - "ui.panel.config.scene.picker.only_editable" - )} - - ` - : nothing, + renderRelativeTimeColumn( + scene.state, + "last-activated", + scene.entity_id, + localize, + this.hass + ), }, + only_editable: getEditableTableColumn( + localize, + localize("ui.panel.config.scene.picker.only_editable") + ), actions: { title: "", label: this.hass.localize("ui.panel.config.generic.headers.actions"), @@ -410,6 +363,14 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { ), action: () => this._editCategory(scene), }, + { + path: mdiPencil, + label: this.hass.localize( + "ui.panel.config.scene.editor.rename" + ), + action: () => this._rename(scene), + disabled: !scene.editable, + }, { divider: true, }, @@ -419,7 +380,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { "ui.panel.config.scene.picker.duplicate" ), action: () => this._duplicate(scene), - disabled: !scene.attributes.id, + disabled: !scene.editable, }, { label: this.hass.localize( @@ -427,8 +388,8 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { ), path: mdiDelete, action: () => this._deleteConfirm(scene), - warning: scene.attributes.id, - disabled: !scene.attributes.id, + warning: scene.editable, + disabled: !scene.editable, }, ]} > @@ -466,103 +427,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { } protected render(): TemplateResult { - const categoryItems = html`${this._categories?.map( - (category) => - html` - ${category.icon - ? html`` - : html``} -
    ${category.name}
    -
    ` - )} - -
    - ${this.hass.localize( - "ui.panel.config.automation.picker.bulk_actions.no_category" - )} -
    -
    - - -
    - ${this.hass.localize("ui.panel.config.category.editor.add")} -
    -
    `; - - const labelItems = html` ${this._labels?.map((label) => { - const color = label.color ? computeCssColor(label.color) : undefined; - const selected = this._selected.every((entityId) => - this.hass.entities[entityId]?.labels.includes(label.label_id) - ); - const partial = - !selected && - this._selected.some((entityId) => - this.hass.entities[entityId]?.labels.includes(label.label_id) - ); - return html` - - - ${label.icon - ? html`` - : nothing} - ${label.name} - - `; - })} - - -
    - ${this.hass.localize("ui.panel.config.labels.add_label")} -
    `; - - const areaItems = html`${Object.values(this.hass.areas).map( - (area) => - html` - ${area.icon - ? html`` - : html``} -
    ${area.name}
    -
    ` - )} - -
    - ${this.hass.localize( - "ui.panel.config.devices.picker.bulk_actions.no_area" - )} -
    -
    - - -
    - ${this.hass.localize( - "ui.panel.config.devices.picker.bulk_actions.add_area" - )} -
    -
    `; - const areasInOverflow = (this._sizeController.value && this._sizeController.value < 900) || (!this._sizeController.value && this.hass.dockedSidebar === "docked"); @@ -577,7 +441,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { this.hass.areas, this._categories, this._labels, - this._filteredScenes + this._filteredSceneEntityIds ); return html` @@ -694,7 +558,10 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { > ${!this.narrow - ? html` + ? html` - ${categoryItems} - + ${this._renderCategoryItems()} + ${labelsInOverflow ? nothing - : html` + : html` - ${labelItems} - `} + ${this._renderLabelItems()} + `} ${areasInOverflow ? nothing - : html` + : html` - ${areaItems} - `}` + ${this._renderAreaItems()} + `}` : nothing} ${this.narrow || areasInOverflow - ? html` - - ${ - this.narrow + ? html` + ${this.narrow ? html`` - } - - ${ - this.narrow - ? html` - -
    - ${this.hass.localize( - "ui.panel.config.automation.picker.bulk_actions.move_category" - )} -
    - -
    - ${categoryItems} -
    ` - : nothing - } - ${ - this.narrow || labelsInOverflow - ? html` - -
    - ${this.hass.localize( - "ui.panel.config.automation.picker.bulk_actions.add_label" - )} -
    - -
    - ${labelItems} -
    ` - : nothing - } - ${ - this.narrow || areasInOverflow - ? html` - -
    - ${this.hass.localize( - "ui.panel.config.devices.picker.bulk_actions.move_area" - )} -
    - -
    - ${areaItems} -
    ` - : nothing - } -
    ` + >`} + ${this.narrow + ? html` + ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.move_category" + )} + ${this._renderCategoryItems("submenu")} + ` + : nothing} + ${this.narrow || labelsInOverflow + ? html` + ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.add_label" + )} + ${this._renderLabelItems("submenu")} + ` + : nothing} + ${this.narrow || areasInOverflow + ? html` + ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.move_area" + )} + ${this._renderAreaItems("submenu")} + ` + : nothing} + ` : nothing} ${!this.scenes.length ? html`
    @@ -891,89 +729,46 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { private _applyFilters() { const filters = Object.entries(this._filters); - let items: Set | undefined; + let filteredEntityIds = this.scenes.map((scene) => scene.entity_id); for (const [key, filter] of filters) { - if (filter.items) { - if (!items) { - items = filter.items; - continue; - } - items = - "intersection" in items - ? // @ts-ignore - items.intersection(filter.items) - : new Set([...items].filter((x) => filter.items!.has(x))); - } if ( - key === "ha-filter-categories" && - Array.isArray(filter.value) && - filter.value.length + // these 3 filters actually apply any selected options, and expose + // the list of scenes that match these options as filter.items + isRelatedItemsFilterUsed(key, filter, [ + "ha-filter-floor-areas", + "ha-filter-devices", + "ha-filter-entities", + ]) ) { - const categoryItems = new Set(); - this.scenes - .filter( - (scene) => - filter.value![0] === - this._entityReg.find((reg) => reg.entity_id === scene.entity_id) - ?.categories.scene + filteredEntityIds = filteredEntityIds.filter((entityId) => + filter.items!.has(entityId) + ); + + // the filters below only expose the selected options (as filter.value); + // applying the filter must be done here + } else if (isFilterUsed(key, filter, "ha-filter-categories")) { + // category filter only allows a single selected option + filteredEntityIds = filteredEntityIds.filter( + (entityId) => + filter.value![0] === + this._entityReg.find((reg) => reg.entity_id === entityId) + ?.categories.scene + ); + } else if (isFilterUsed(key, filter, "ha-filter-labels")) { + filteredEntityIds = filteredEntityIds.filter((entityId) => + this._entityReg + .find((reg) => reg.entity_id === entityId) + ?.labels.some((lbl) => (filter.value as string[]).includes(lbl)) + ); + } else if (isFilterUsed(key, filter, "ha-filter-voice-assistants")) { + filteredEntityIds = filteredEntityIds.filter((entityId) => + getEntityVoiceAssistantsIds(this._entityReg, entityId).some((va) => + (filter.value as string[]).includes(va) ) - .forEach((scene) => categoryItems.add(scene.entity_id)); - if (!items) { - items = categoryItems; - continue; - } - items = - "intersection" in items - ? // @ts-ignore - items.intersection(categoryItems) - : new Set([...items].filter((x) => categoryItems!.has(x))); - } else if ( - key === "ha-filter-labels" && - Array.isArray(filter.value) && - filter.value.length - ) { - const labelItems = new Set(); - this.scenes - .filter((scene) => - this._entityReg - .find((reg) => reg.entity_id === scene.entity_id) - ?.labels.some((lbl) => (filter.value as string[]).includes(lbl)) - ) - .forEach((scene) => labelItems.add(scene.entity_id)); - if (!items) { - items = labelItems; - continue; - } - items = - "intersection" in items - ? // @ts-ignore - items.intersection(labelItems) - : new Set([...items].filter((x) => labelItems!.has(x))); - } else if ( - key === "ha-filter-voice-assistants" && - Array.isArray(filter.value) && - filter.value.length - ) { - const assistItems = new Set(); - this.scenes - .filter((scene) => - getEntityVoiceAssistantsIds(this._entityReg, scene.entity_id).some( - (va) => (filter.value as string[]).includes(va) - ) - ) - .forEach((scene) => assistItems.add(scene.entity_id)); - if (!items) { - items = assistItems; - continue; - } - items = - "intersection" in items - ? // @ts-ignore - items.intersection(assistItems) - : new Set([...items].filter((x) => assistItems!.has(x))); + ); } } - this._filteredScenes = items ? [...items] : undefined; + this._filteredSceneEntityIds = filteredEntityIds; } private _clearFilter() { @@ -1016,12 +811,22 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { } } - private _handleBulkCategory = (item) => { - const category = item.value; - this._bulkAddCategory(category); + private _handleBulkCategory = (ev: HaDropdownSelectEvent) => { + const value = ev.detail.item.value; + if (value === "category_create") { + this._bulkCreateCategory(); + return; + } + if (value === "category_none") { + this._bulkAddCategory(null); + return; + } + if (value?.startsWith("category_")) { + this._bulkAddCategory(value.substring(9)); + } }; - private async _bulkAddCategory(category: string) { + private async _bulkAddCategory(category: string | null) { const promises: Promise[] = []; this._selected.forEach((entityId) => { promises.push( @@ -1046,11 +851,18 @@ ${rejected } } - private async _handleBulkLabel(ev) { - const label = ev.currentTarget.value; - const action = ev.currentTarget.action; - this._bulkLabel(label, action); - } + private _handleBulkLabel = (ev: HaDropdownSelectEvent) => { + ev.preventDefault(); + const value = ev.detail.item.value; + if (value === "label_create") { + this._bulkCreateLabel(); + return; + } + if (value?.startsWith("label_")) { + const action = (ev.detail.item as any).action; + this._bulkLabel(value.substring(6), action); + } + }; private async _bulkLabel(label: string, action: "add" | "remove") { const promises: Promise[] = []; @@ -1082,12 +894,22 @@ ${rejected } } - private _handleBulkArea = (item) => { - const area = item.value; - this._bulkAddArea(area); + private _handleBulkArea = (ev: HaDropdownSelectEvent) => { + const value = ev.detail.item.value; + if (value === "area_create") { + this._bulkCreateArea(); + return; + } + if (value === "area_none") { + this._bulkAddArea(null); + return; + } + if (value?.startsWith("area_")) { + this._bulkAddArea(value.substring(5)); + } }; - private async _bulkAddArea(area: string) { + private async _bulkAddArea(area: string | null) { const promises: Promise[] = []; this._selected.forEach((entityId) => { promises.push( @@ -1189,7 +1011,7 @@ ${rejected } } - private async _duplicate(scene) { + private async _duplicate(scene: SceneEntity) { if (scene.attributes.id) { const config = await getSceneConfig(this.hass, scene.attributes.id); const entityRegEntry = this._entityReg.find( @@ -1208,6 +1030,31 @@ ${rejected } } + private async _rename(scene: SceneEntity): Promise { + if (!scene.attributes.id) { + return; + } + const config = await getSceneConfig(this.hass, scene.attributes.id); + const entityRegEntry = this._entityReg.find( + (reg) => reg.entity_id === scene.entity_id + ); + showSceneSaveDialog(this, { + config, + domain: "scene", + entityRegistryEntry: entityRegEntry, + updateConfig: async (newConfig, entityRegistryUpdate) => { + await saveScene(this.hass, scene.attributes.id!, newConfig); + if (entityRegEntry) { + await updateEntityRegistryEntry(this.hass, scene.entity_id, { + area_id: entityRegistryUpdate.area || null, + labels: entityRegistryUpdate.labels, + categories: { scene: entityRegistryUpdate.category || null }, + }); + } + }, + }); + } + private _showHelp() { showAlertDialog(this, { title: this.hass.localize("ui.panel.config.scene.picker.header"), @@ -1250,6 +1097,132 @@ ${rejected }); }; + private _renderCategoryItems = (slot = "") => + html`${this._categories?.map( + (category) => + html` + ${category.icon + ? html`` + : html``} + ${category.name} + ` + )} + + ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.no_category" + )} + + + + ${this.hass.localize("ui.panel.config.category.editor.add")} + `; + + private _renderLabelItems = (slot = "") => + html`${this._labels?.map((label) => { + const color = label.color ? computeCssColor(label.color) : undefined; + const selected = this._selected.every((entityId) => + this.hass.entities[entityId]?.labels.includes(label.label_id) + ); + const partial = + !selected && + this._selected.some((entityId) => + this.hass.entities[entityId]?.labels.includes(label.label_id) + ); + return html` + + + ${label.icon + ? html`` + : nothing} + ${label.name} + + `; + })} + + + ${this.hass.localize("ui.panel.config.labels.add_label")} + `; + + private _renderAreaItems = (slot = "") => + html`${Object.values(this.hass.areas).map( + (area) => + html` + ${area.icon + ? html`` + : html``} + ${area.name} + ` + )} + + ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.no_area" + )} + + + + ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.add_area" + )} + `; + + private _handleBulkAction = (ev) => { + const item = ev.detail.item; + const value = item.value; + + if (!value) { + return; + } + + if (value.startsWith("category_")) { + if (value === "category_create") { + this._bulkCreateCategory(); + } else if (value === "category_none") { + this._bulkAddCategory(null); + } else { + this._bulkAddCategory(value.substring(9)); + } + return; + } + + if (value.startsWith("label_")) { + if (value === "label_create") { + this._bulkCreateLabel(); + } else { + const action = item.action; + this._bulkLabel(value.substring(6), action); + } + return; + } + + if (value.startsWith("area_")) { + if (value === "area_create") { + this._bulkCreateArea(); + } else if (value === "area_none") { + this._bulkAddArea(null); + } else { + this._bulkAddArea(value.substring(5)); + } + } + }; + private _handleSortingChanged(ev: CustomEvent) { this._activeSorting = ev.detail; } @@ -1298,7 +1271,11 @@ ${rejected ha-assist-chip { --ha-assist-chip-container-shape: 10px; } - ha-md-button-menu ha-assist-chip { + ha-dropdown::part(menu), + ha-dropdown::part(submenu) { + --auto-size-available-width: calc(50vw - var(--ha-space-4)); + } + ha-dropdown ha-assist-chip { --md-assist-chip-trailing-space: 8px; } ha-label { diff --git a/src/panels/config/scene/ha-scene-editor.ts b/src/panels/config/scene/ha-scene-editor.ts index a6d41123d5..dac0cf2495 100644 --- a/src/panels/config/scene/ha-scene-editor.ts +++ b/src/panels/config/scene/ha-scene-editor.ts @@ -1,6 +1,6 @@ +import "@home-assistant/webawesome/dist/components/divider/divider"; import { consume } from "@lit/context"; -import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import { mdiCog, mdiContentDuplicate, @@ -10,6 +10,7 @@ import { mdiEye, mdiInformationOutline, mdiMotionPlayOutline, + mdiPencil, mdiPlay, mdiPlaylistEdit, mdiTag, @@ -20,27 +21,26 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; +import { transform } from "../../../common/decorators/transform"; import { fireEvent } from "../../../common/dom/fire_event"; import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name"; import { computeDomain } from "../../../common/entity/compute_domain"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { goBack, navigate } from "../../../common/navigate"; import { computeRTL } from "../../../common/util/compute_rtl"; +import { promiseTimeout } from "../../../common/util/promise-timeout"; import { afterNextRender } from "../../../common/util/render-status"; import "../../../components/device/ha-device-picker"; import "../../../components/entity/ha-entities-picker"; import "../../../components/ha-alert"; -import "../../../components/ha-area-picker"; import "../../../components/ha-button"; -import "../../../components/ha-button-menu"; import "../../../components/ha-card"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-fab"; import "../../../components/ha-icon-button"; -import "../../../components/ha-icon-picker"; import "../../../components/ha-list"; -import "../../../components/ha-list-item"; import "../../../components/ha-svg-icon"; -import "../../../components/ha-textfield"; import { fullEntitiesContext } from "../../../data/context"; import type { DeviceRegistryEntry } from "../../../data/device/device_registry"; import type { EntityRegistryEntry } from "../../../data/entity/entity_registry"; @@ -72,8 +72,14 @@ import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant, Route } from "../../../types"; import { showToast } from "../../../util/toast"; +import { showAutomationSaveTimeoutDialog } from "../automation/automation-save-timeout-dialog/show-dialog-automation-save-timeout"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import "../ha-config-section"; +import { + showSceneSaveDialog, + type EntityRegistryUpdate, +} from "./scene-save-dialog/show-dialog-scene-save"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; interface DeviceEntities { id: string; @@ -113,6 +119,18 @@ export class HaSceneEditor extends PreventUnsavedMixin( @state() private _devices: string[] = []; + @state() + @consume({ context: fullEntitiesContext, subscribe: true }) + @transform({ + transformer: function (this: HaSceneEditor, value) { + return value.find( + ({ entity_id }) => entity_id === this._scene?.entity_id + ); + }, + watch: ["_scene"], + }) + private _registryEntry?: EntityRegistryEntry; + @state() @consume({ context: fullEntitiesContext, subscribe: true }) private _entityRegistryEntries: EntityRegistryEntry[] = []; @@ -131,29 +149,13 @@ export class HaSceneEditor extends PreventUnsavedMixin( @state() private _saving = false; - // undefined means not set in this session - // null means picked nothing. - @state() private _updatedAreaId?: string | null; + private _entityRegistryUpdate?: EntityRegistryUpdate; - // Callback to be called when scene is set. - private _scenesSet?: () => void; + private _newSceneId?: string; - private _getRegistryAreaId = memoizeOne( - (entries: EntityRegistryEntry[], entity_id: string) => { - const entry = entries.find((ent) => ent.entity_id === entity_id); - return entry ? entry.area_id : null; - } - ); - - private _getCategory = memoizeOne( - (entries: EntityRegistryEntry[], entity_id: string | undefined) => { - if (!entity_id) { - return undefined; - } - const entry = entries.find((ent) => ent.entity_id === entity_id); - return entry?.categories?.scene; - } - ); + private _entityRegCreated?: ( + value: PromiseLike | EntityRegistryEntry + ) => void; private _getEntitiesDevices = memoizeOne( ( @@ -223,13 +225,12 @@ export class HaSceneEditor extends PreventUnsavedMixin( .narrow=${this.narrow} .route=${this.route} .backCallback=${this._backTapped} - .header=${this._scene - ? computeStateName(this._scene) - : this.hass.localize("ui.panel.config.scene.editor.default_name")} + .header=${this._config?.name || + this.hass.localize("ui.panel.config.scene.editor.default_name")} > - - ${this.hass.localize("ui.panel.config.scene.picker.apply")} - - - + + + + ${this.hass.localize("ui.panel.config.scene.picker.show_info")} - - + + + ${this.hass.localize( "ui.panel.config.automation.picker.show_settings" )} - - + + - + ${this.hass.localize( - `ui.panel.config.scene.picker.${this._getCategory(this._entityRegistryEntries, this._scene?.entity_id) ? "edit_category" : "assign_category"}` + `ui.panel.config.scene.picker.${this._registryEntry?.categories?.scene ? "edit_category" : "assign_category"}` )} - - + + - + + ${this.hass.localize("ui.panel.config.scene.editor.rename")} + + + + ${this.hass.localize( `ui.panel.config.automation.editor.edit_${this._mode !== "yaml" ? "yaml" : "ui"}` )} - - + + -
  • + - + ${this.hass.localize( "ui.panel.config.scene.picker.duplicate_scene" )} - - + + - ${this.hass.localize("ui.panel.config.scene.picker.delete_scene")} - -
    + + ${this._errors ? html`
    ${this._errors}
    ` : ""} ${this._mode === "yaml" ? this._renderYamlMode() : this._renderUiMode()} @@ -374,38 +382,6 @@ export class HaSceneEditor extends PreventUnsavedMixin( )} - -
    - - - - - -
    -
    @@ -577,6 +553,31 @@ export class HaSceneEditor extends PreventUnsavedMixin(
    `; } + protected willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + + if ( + this._entityRegCreated && + this._newSceneId && + (changedProps.has("scenes") || changedProps.has("_entityRegistryEntries")) + ) { + const scene = this.scenes.find( + (entity: SceneEntity) => entity.attributes.id === this._newSceneId + ); + if (scene) { + // Scene appeared in state machine, now look for registry entry + const registryEntry = this._entityRegistryEntries.find( + (reg) => reg.entity_id === scene.entity_id + ); + if (registryEntry) { + // We have both the scene and its registry entry, resolve + this._entityRegCreated(registryEntry); + this._entityRegCreated = undefined; + } + } + } + } + protected updated(changedProps: PropertyValues): void { super.updated(changedProps); @@ -601,8 +602,12 @@ export class HaSceneEditor extends PreventUnsavedMixin( ...initData?.config, }; this._initEntities(this._config); - if (initData?.areaId) { - this._updatedAreaId = initData.areaId; + if (initData?.areaId !== undefined) { + this._entityRegistryUpdate = { + area: initData.areaId || "", + labels: [], + category: "", + }; } this._dirty = initData !== undefined && @@ -633,9 +638,6 @@ export class HaSceneEditor extends PreventUnsavedMixin( } } } - if (this._scenesSet && changedProps.has("scenes")) { - this._scenesSet(); - } if (changedProps.has("hass")) { if (this._scene) { @@ -652,24 +654,32 @@ export class HaSceneEditor extends PreventUnsavedMixin( } } - private async _handleMenuAction(ev: CustomEvent) { - switch (ev.detail.index) { - case 0: + private _handleMenuAction(ev: HaDropdownSelectEvent) { + const action = ev.detail?.item?.value; + if (!action) { + return; + } + + switch (action) { + case "apply": activateScene(this.hass, this._scene!.entity_id); break; - case 1: + case "show-info": fireEvent(this, "hass-more-info", { entityId: this._scene!.entity_id }); break; - case 2: + case "show-settings": showMoreInfoDialog(this, { entityId: this._scene!.entity_id, view: "settings", }); break; - case 3: - this._editCategory(this._scene!); + case "edit-category": + this._editCategory(); break; - case 4: + case "rename": + this._promptSceneRename(); + break; + case "toggle-yaml": if (this._mode === "yaml") { this._initEntities(this._config!); this._exitYamlMode(); @@ -677,10 +687,10 @@ export class HaSceneEditor extends PreventUnsavedMixin( this._enterYamlMode(); } break; - case 5: + case "duplicate": this._duplicate(); break; - case 6: + case "delete": this._deleteTapped(); break; } @@ -930,44 +940,6 @@ export class HaSceneEditor extends PreventUnsavedMixin( this._dirty = true; } - private _valueChanged(ev: Event) { - ev.stopPropagation(); - const target = ev.target as any; - const name = target.name; - if (!name) { - return; - } - let newVal = (ev as CustomEvent).detail?.value ?? target.value; - if (target.type === "number") { - newVal = Number(newVal); - } - if ((this._config![name] || "") === newVal) { - return; - } - if (!newVal) { - delete this._config![name]; - this._config = { ...this._config! }; - } else { - this._config = { ...this._config!, [name]: newVal }; - } - this._dirty = true; - } - - private _areaChanged(ev: CustomEvent) { - const newValue = ev.detail.value === "" ? null : ev.detail.value; - - if (newValue === (this._sceneAreaIdWithUpdates || "")) { - return; - } - - if (newValue === this._sceneAreaIdCurrent) { - this._updatedAreaId = undefined; - } else { - this._updatedAreaId = newValue; - this._dirty = true; - } - } - private _stateChanged(event: HassEvent) { if ( event.context.id !== this._activateContextId && @@ -1008,7 +980,10 @@ export class HaSceneEditor extends PreventUnsavedMixin( } private async _delete(): Promise { - await deleteScene(this.hass, this.sceneId!); + if (!this.sceneId) { + return; + } + await deleteScene(this.hass, this.sceneId); if (this._mode === "live") { applyScene(this.hass, this._storedStates); } @@ -1112,59 +1087,91 @@ export class HaSceneEditor extends PreventUnsavedMixin( return; } - const id = !this.sceneId ? "" + Date.now() : this.sceneId!; if (this._mode === "live") { this._generateConfigFromLive(); } + + const isNewScene = !this.sceneId; + if (isNewScene) { + const saved = await this._promptSceneSave(); + if (!saved) { + return; + } + } + + const id = this.sceneId || String(Date.now()); + + this._saving = true; + + let entityRegPromise: Promise | undefined; + if (this._entityRegistryUpdate !== undefined && !this.sceneId) { + this._newSceneId = id; + entityRegPromise = new Promise((resolve) => { + this._entityRegCreated = resolve; + }); + } + try { - this._saving = true; await saveScene(this.hass, id, this._config!); + this._errors = undefined; - if (this._updatedAreaId !== undefined) { - let scene = - this._scene || - this.scenes.find( - (entity: SceneEntity) => entity.attributes.id === id - ); + if (this._entityRegistryUpdate !== undefined) { + let entityId = this._scene?.entity_id; - if (!scene) { + // wait for scene to appear in entity registry when creating a new scene + if (entityRegPromise) { try { - await new Promise((resolve, reject) => { - setTimeout(reject, 3000); - this._scenesSet = resolve; - }); - scene = this.scenes.find( - (entity: SceneEntity) => entity.attributes.id === id - ); - } catch (_err) { - // We do nothing. - } finally { - this._scenesSet = undefined; + const scene = await promiseTimeout(5000, entityRegPromise); + entityId = scene.entity_id; + } catch (e) { + if (e instanceof Error && e.name === "TimeoutError") { + // Show the dialog and give user a chance to wait for the registry + // to respond. + await showAutomationSaveTimeoutDialog(this, { + savedPromise: entityRegPromise, + type: "scene", + }); + try { + // We already gave the user a chance to wait once, so if they skipped + // the dialog and it's still not there just immediately timeout. + const scene = await promiseTimeout(0, entityRegPromise); + entityId = scene.entity_id; + } catch (e2) { + if (!(e2 instanceof Error && e2.name === "TimeoutError")) { + throw e2; + } + } + } else { + throw e; + } } } - if (scene) { - await updateEntityRegistryEntry(this.hass, scene.entity_id, { - area_id: this._updatedAreaId, + if (entityId) { + await updateEntityRegistryEntry(this.hass, entityId, { + area_id: this._entityRegistryUpdate.area || null, + labels: this._entityRegistryUpdate.labels || [], + categories: { + scene: this._entityRegistryUpdate.category || null, + }, }); } - - this._updatedAreaId = undefined; } this._dirty = false; - - if (!this.sceneId) { + if (isNewScene) { navigate(`/config/scene/edit/${id}`, { replace: true }); } } catch (err: any) { - this._errors = err.body.message || err.message; + this._errors = err.body?.message || err.message || err.body; showToast(this, { - message: err.body.message || err.message, + message: err.body?.message || err.message || err.body, }); throw err; } finally { this._saving = false; + this._entityRegCreated = undefined; + this._newSceneId = undefined; } } @@ -1174,26 +1181,12 @@ export class HaSceneEditor extends PreventUnsavedMixin( }; } - private get _sceneAreaIdWithUpdates(): string | undefined | null { - return this._updatedAreaId !== undefined - ? this._updatedAreaId - : this._sceneAreaIdCurrent; - } - private get _sceneAreaIdCurrent(): string | undefined | null { - return this._scene - ? this._getRegistryAreaId( - this._entityRegistryEntries, - this._scene.entity_id - ) - : undefined; + return this._registryEntry?.area_id || undefined; } - private _editCategory(scene: any) { - const entityReg = this._entityRegistryEntries.find( - (reg) => reg.entity_id === scene.entity_id - ); - if (!entityReg) { + private _editCategory() { + if (!this._registryEntry) { showAlertDialog(this, { title: this.hass.localize( "ui.panel.config.scene.picker.no_category_support" @@ -1206,7 +1199,45 @@ export class HaSceneEditor extends PreventUnsavedMixin( } showAssignCategoryDialog(this, { scope: "scene", - entityReg, + entityReg: this._registryEntry, + }); + } + + private async _promptSceneSave(): Promise { + return new Promise((resolve) => { + showSceneSaveDialog(this, { + config: this._config!, + domain: "scene", + entityRegistryEntry: this._registryEntry, + entityRegistryUpdate: this._entityRegistryUpdate, + updateConfig: async (newConfig, entityRegistryUpdate) => { + this._config = newConfig; + this._entityRegistryUpdate = entityRegistryUpdate; + this._dirty = true; + this.requestUpdate(); + resolve(true); + }, + onClose: () => resolve(false), + }); + }); + } + + private async _promptSceneRename(): Promise { + return new Promise((resolve) => { + showSceneSaveDialog(this, { + config: this._config!, + domain: "scene", + entityRegistryEntry: this._registryEntry, + entityRegistryUpdate: this._entityRegistryUpdate, + updateConfig: async (newConfig, entityRegistryUpdate) => { + this._config = newConfig; + this._entityRegistryUpdate = entityRegistryUpdate; + this._dirty = true; + this.requestUpdate(); + resolve(true); + }, + onClose: () => resolve(false), + }); }); } @@ -1275,23 +1306,15 @@ export class HaSceneEditor extends PreventUnsavedMixin( ha-fab.saving { opacity: var(--light-disabled-opacity); } - ha-icon-picker, - ha-area-picker, ha-entity-picker { display: block; margin-top: 8px; } - ha-textfield { - display: block; - } div[slot="meta"] { display: flex; justify-content: center; align-items: center; } - li[role="separator"] { - border-bottom-color: var(--divider-color); - } ha-list-item.entity { padding-right: 28px; } diff --git a/src/panels/config/scene/scene-save-dialog/dialog-scene-save.ts b/src/panels/config/scene/scene-save-dialog/dialog-scene-save.ts new file mode 100644 index 0000000000..1c21c818bd --- /dev/null +++ b/src/panels/config/scene/scene-save-dialog/dialog-scene-save.ts @@ -0,0 +1,391 @@ +import { mdiPlus } from "@mdi/js"; +import type { CSSResultGroup } from "lit"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/chips/ha-assist-chip"; +import "../../../../components/chips/ha-chip-set"; +import "../../../../components/ha-alert"; +import "../../../../components/ha-area-picker"; +import "../../../../components/ha-domain-icon"; + +import "../../../../components/ha-icon-picker"; +import "../../../../components/ha-labels-picker"; +import "../../../../components/ha-suggest-with-ai-button"; +import type { SuggestWithAIGenerateTask } from "../../../../components/ha-suggest-with-ai-button"; +import "../../../../components/ha-svg-icon"; +import "../../../../components/ha-textfield"; +import "../../../../components/ha-wa-dialog"; +import "../../../../components/ha-button"; +import "../../../../components/ha-dialog-footer"; +import "../../category/ha-category-picker"; + +import type { GenDataTaskResult } from "../../../../data/ai_task"; +import { haStyleDialog } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import type { + EntityRegistryUpdate, + SceneSaveDialogParams, +} from "./show-dialog-scene-save"; +import { + type MetadataSuggestionInclude, + type MetadataSuggestionResult, + generateMetadataSuggestionTask, + processMetadataSuggestion, +} from "../../common/suggest-metadata-ai"; +import { buildEntityMetadataInspirations } from "../../common/suggest-metadata-inspirations"; +import type { SceneConfig } from "../../../../data/scene"; + +const SUGGESTION_INCLUDE: MetadataSuggestionInclude = { + name: true, + categories: true, + labels: true, +}; + +@customElement("ha-dialog-scene-save") +class DialogSceneSave extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _open = false; + + @state() private _error = false; + + @state() private _visibleOptionals: string[] = []; + + @state() private _entryUpdates!: EntityRegistryUpdate; + + private _params!: SceneSaveDialogParams; + + @state() private _newName?: string; + + private _newIcon?: string; + + public showDialog(params: SceneSaveDialogParams): void { + this._open = true; + this._params = params; + this._error = false; + this._newIcon = params.config.icon; + this._newName = + params.config.name || + this.hass.localize( + `ui.panel.config.${this._params.domain}.editor.default_name` + ); + this._entryUpdates = params.entityRegistryUpdate || { + area: params.entityRegistryEntry?.area_id || "", + labels: params.entityRegistryEntry?.labels || [], + category: params.entityRegistryEntry?.categories?.[params.domain] || "", + }; + + this._visibleOptionals = [ + this._entryUpdates.category ? "category" : "", + this._entryUpdates.labels.length > 0 ? "labels" : "", + ].filter(Boolean); + } + + public closeDialog() { + this._open = false; + this._error = false; + } + + private _dialogClosed() { + this._open = false; + this._error = false; + this._params.onClose?.(); + this._visibleOptionals = []; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected _renderOptionalChip(id: string, label: string) { + if (this._visibleOptionals.includes(id)) { + return nothing; + } + + return html` + + + + `; + } + + protected _renderInputs() { + if (this._params.hideInputs) { + return nothing; + } + + return html` + + + + + + + + + + ${this._visibleOptionals.includes("category") + ? html` ` + : nothing} + ${this._visibleOptionals.includes("labels") + ? html` ` + : nothing} + + + ${this._renderOptionalChip( + "category", + this.hass.localize("ui.panel.config.scene.editor.dialog.add_category") + )} + ${this._renderOptionalChip( + "labels", + this.hass.localize("ui.panel.config.scene.editor.dialog.add_labels") + )} + + `; + } + + protected render() { + if (!this._params) { + return nothing; + } + + const title = this.hass.localize( + this._params.config.id + ? "ui.panel.config.scene.editor.rename" + : "ui.common.save" + ); + + return html` + + ${this._params.hideInputs + ? nothing + : html` `} + ${this._error + ? html`${this.hass.localize( + "ui.panel.config.scene.editor.missing_name" + )}` + : ""} + ${this._params.description + ? html`

    ${this._params.description}

    ` + : nothing} + ${this._renderInputs()} + + + ${this._params.onDiscard + ? html` + + ${this.hass.localize("ui.common.dont_save")} + + ` + : nothing} + + ${this.hass.localize("ui.common.cancel")} + + + ${this.hass.localize( + this._params.config.id && !this._params.onDiscard + ? "ui.panel.config.scene.editor.rename" + : "ui.common.save" + )} + + +
    + `; + } + + private _addOptional(ev) { + ev.stopPropagation(); + const option: string = ev.target.id; + this._visibleOptionals = [...this._visibleOptionals, option]; + } + + private _registryEntryChanged(ev) { + ev.stopPropagation(); + const id: string = ev.target.id; + const value = ev.detail.value; + + this._entryUpdates = { ...this._entryUpdates, [id]: value }; + } + + private _iconChanged(ev: CustomEvent) { + ev.stopPropagation(); + this._newIcon = ev.detail.value || undefined; + } + + private _valueChanged(ev: CustomEvent) { + ev.stopPropagation(); + this._newName = (ev.target as HTMLInputElement).value; + if (this._error && this._newName.trim()) { + this._error = false; + } + } + + private _handleDiscard() { + this._params.onDiscard?.(); + this.closeDialog(); + } + + private _generateTask = async (): Promise => + generateMetadataSuggestionTask( + this.hass.connection, + this.hass.language, + "scene", + this._params.config, + await buildEntityMetadataInspirations( + this.hass.connection, + this.hass.states, + "scene" + ), + SUGGESTION_INCLUDE + ); + + private async _handleSuggestion( + event: CustomEvent> + ) { + const result = event.detail; + const processed = await processMetadataSuggestion( + this.hass.connection, + "scene", + result, + SUGGESTION_INCLUDE + ); + + if (processed.name) { + this._newName = processed.name; + if (this._error && this._newName.trim()) { + this._error = false; + } + } + + if (processed.category) { + this._entryUpdates = { + ...this._entryUpdates, + category: processed.category, + }; + if (!this._visibleOptionals.includes("category")) { + this._visibleOptionals = [...this._visibleOptionals, "category"]; + } + } + + if (processed.labels?.length) { + this._entryUpdates = { + ...this._entryUpdates, + labels: processed.labels, + }; + if (!this._visibleOptionals.includes("labels")) { + this._visibleOptionals = [...this._visibleOptionals, "labels"]; + } + } + } + + private async _save(): Promise { + if (!this._newName) { + this._error = true; + return; + } + + await this._params.updateConfig( + { + ...this._params.config, + name: this._newName, + icon: this._newIcon, + }, + this._entryUpdates + ); + + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + ha-textfield, + ha-icon-picker, + ha-category-picker, + ha-labels-picker, + ha-area-picker { + display: block; + } + ha-icon-picker, + ha-category-picker, + ha-labels-picker, + ha-area-picker, + ha-chip-set:has(> ha-assist-chip) { + margin-top: var(--ha-space-4); + } + ha-alert { + display: block; + margin-bottom: var(--ha-space-4); + } + + ha-suggest-with-ai-button { + margin: var(--ha-space-2) var(--ha-space-4); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-dialog-scene-save": DialogSceneSave; + } +} diff --git a/src/panels/config/scene/scene-save-dialog/show-dialog-scene-save.ts b/src/panels/config/scene/scene-save-dialog/show-dialog-scene-save.ts new file mode 100644 index 0000000000..d8dfab14d9 --- /dev/null +++ b/src/panels/config/scene/scene-save-dialog/show-dialog-scene-save.ts @@ -0,0 +1,42 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; +import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry"; +import type { SceneConfig } from "../../../../data/scene"; + +export const loadSceneSaveDialog = () => import("./dialog-scene-save"); + +interface BaseRenameDialogParams { + entityRegistryUpdate?: EntityRegistryUpdate; + entityRegistryEntry?: EntityRegistryEntry; + onClose?: () => void; + onDiscard?: () => void; + saveText?: string; + description?: string; + title?: string; + hideInputs?: boolean; +} + +export interface EntityRegistryUpdate { + area: string; + labels: string[]; + category: string; +} + +export interface SceneSaveDialogParams extends BaseRenameDialogParams { + config: SceneConfig; + domain: "scene"; + updateConfig: ( + config: SceneConfig, + entityRegistryUpdate: EntityRegistryUpdate + ) => Promise; +} + +export const showSceneSaveDialog = ( + element: HTMLElement, + dialogParams: SceneSaveDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "ha-dialog-scene-save", + dialogImport: loadSceneSaveDialog, + dialogParams, + }); +}; diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index 7ff1b45443..036b223b9b 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -34,7 +34,6 @@ import { afterNextRender } from "../../../common/util/render-status"; import "../../../components/ha-button"; import "../../../components/ha-dropdown"; import "../../../components/ha-dropdown-item"; -import type { HaDropdownItem } from "../../../components/ha-dropdown-item"; import "../../../components/ha-fab"; import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; @@ -79,6 +78,7 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor import "./blueprint-script-editor"; import "./manual-script-editor"; import type { HaManualScriptEditor } from "./manual-script-editor"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; @customElement("ha-script-editor") export class HaScriptEditor extends SubscribeMixin( @@ -1121,7 +1121,7 @@ export class HaScriptEditor extends SubscribeMixin( this._undoRedoController.redo(); } - private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { const action = ev.detail?.item?.value; if (!action) { diff --git a/src/panels/config/script/ha-script-field-editor.ts b/src/panels/config/script/ha-script-field-editor.ts index 78e0b44022..b873ce127e 100644 --- a/src/panels/config/script/ha-script-field-editor.ts +++ b/src/panels/config/script/ha-script-field-editor.ts @@ -17,8 +17,7 @@ export default class HaScriptFieldEditor extends LitElement { @property() public key!: string; - @property({ attribute: false, type: Array }) public excludeKeys: string[] = - []; + @property({ attribute: false }) public excludeKeys: string[] = []; @property({ attribute: false }) public field!: Field; diff --git a/src/panels/config/script/ha-script-field-row.ts b/src/panels/config/script/ha-script-field-row.ts index 43220616dd..d4305c9f55 100644 --- a/src/panels/config/script/ha-script-field-row.ts +++ b/src/panels/config/script/ha-script-field-row.ts @@ -17,7 +17,6 @@ import type { HaAutomationRow } from "../../../components/ha-automation-row"; import "../../../components/ha-card"; import "../../../components/ha-dropdown"; import "../../../components/ha-dropdown-item"; -import type { HaDropdownItem } from "../../../components/ha-dropdown-item"; import type { ScriptFieldSidebarConfig } from "../../../data/automation"; import type { Field } from "../../../data/script"; import { SELECTOR_SELECTOR_BUILDING_BLOCKS } from "../../../data/selector/selector_selector"; @@ -28,6 +27,7 @@ import { showToast } from "../../../util/toast"; import { indentStyle, overflowStyles } from "../automation/styles"; import "./ha-script-field-selector-editor"; import type HaScriptFieldSelectorEditor from "./ha-script-field-selector-editor"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; @customElement("ha-script-field-row") export default class HaScriptFieldRow extends LitElement { @@ -35,8 +35,7 @@ export default class HaScriptFieldRow extends LitElement { @property() public key!: string; - @property({ attribute: false, type: Array }) public excludeKeys: string[] = - []; + @property({ attribute: false }) public excludeKeys: string[] = []; @property({ attribute: false }) public field!: Field; @@ -407,7 +406,7 @@ export default class HaScriptFieldRow extends LitElement { this._selectorRowElement?.focus(); } - private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { const action = ev.detail?.item?.value; if (!action) { diff --git a/src/panels/config/script/ha-script-fields.ts b/src/panels/config/script/ha-script-fields.ts index 6e4d245676..3cd5ec8bd2 100644 --- a/src/panels/config/script/ha-script-fields.ts +++ b/src/panels/config/script/ha-script-fields.ts @@ -4,7 +4,6 @@ import { LitElement, css, html, nothing } from "lit"; import { customElement, property, queryAll } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/ha-button"; -import "../../../components/ha-button-menu"; import "../../../components/ha-svg-icon"; import type { Fields } from "../../../data/script"; import type { HomeAssistant } from "../../../types"; diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index 7f89c43f60..4a2bba4cba 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -1,7 +1,7 @@ +import "@home-assistant/webawesome/dist/components/divider/divider"; import { ResizeController } from "@lit-labs/observers/resize-controller"; import { consume } from "@lit/context"; import { - mdiChevronRight, mdiCog, mdiContentDuplicate, mdiDelete, @@ -17,7 +17,6 @@ import { mdiTextureBox, mdiTransitConnection, } from "@mdi/js"; -import { differenceInDays } from "date-fns"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { LitElement, css, html, nothing } from "lit"; @@ -26,14 +25,11 @@ import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; import { computeCssColor } from "../../../common/color/compute-color"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; -import { formatShortDateTimeWithConditionalYear } from "../../../common/datetime/format_date_time"; -import { relativeTime } from "../../../common/datetime/relative_time"; import { storage } from "../../../common/decorators/storage"; import type { HASSDomEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event"; import { computeStateName } from "../../../common/entity/compute_state_name"; import { navigate } from "../../../common/navigate"; -import { slugify } from "../../../common/string/slugify"; import type { LocalizeFunc } from "../../../common/translations/localize"; import { hasRejectedItems, @@ -46,6 +42,8 @@ import type { SortingChangedEvent, } from "../../../components/data-table/ha-data-table"; import "../../../components/data-table/ha-data-table-labels"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-fab"; import "../../../components/ha-filter-blueprints"; import "../../../components/ha-filter-categories"; @@ -56,9 +54,6 @@ import "../../../components/ha-filter-labels"; import "../../../components/ha-filter-voice-assistants"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-overflow-menu"; -import "../../../components/ha-md-divider"; -import "../../../components/ha-md-menu"; -import "../../../components/ha-md-menu-item"; import "../../../components/ha-sub-menu"; import "../../../components/ha-svg-icon"; import "../../../components/ha-tooltip"; @@ -73,6 +68,8 @@ import { fullEntitiesContext } from "../../../data/context"; import type { DataTableFilters } from "../../../data/data_table_filters"; import { deserializeFilters, + isFilterUsed, + isRelatedItemsFilterUsed, serializeFilters, } from "../../../data/data_table_filters"; import { UNAVAILABLE } from "../../../data/entity/entity"; @@ -81,6 +78,7 @@ import type { UpdateEntityRegistryEntryResult, } from "../../../data/entity/entity_registry"; import { updateEntityRegistryEntry } from "../../../data/entity/entity_registry"; +import { getEntityVoiceAssistantsIds } from "../../../data/expose"; import type { LabelRegistryEntry } from "../../../data/label/label_registry"; import { createLabelRegistryEntry, @@ -111,21 +109,28 @@ import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry import { showNewAutomationDialog } from "../automation/show-dialog-new-automation"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; +import { + getEntityIdHiddenTableColumn, + getAreaTableColumn, + getCategoryTableColumn, + getLabelsTableColumn, + getTriggeredAtTableColumn, +} from "../common/data-table-columns"; import { configSections } from "../ha-panel-config"; import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; -import { getEntityVoiceAssistantsIds } from "../../../data/expose"; -import { getAvailableAssistants } from "../voice-assistants/expose/available-assistants"; import { - getAssistantsTableColumn, getAssistantsSortableKey, + getAssistantsTableColumn, } from "../voice-assistants/expose/assistants-table-column"; +import { getAvailableAssistants } from "../voice-assistants/expose/available-assistants"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; type ScriptItem = ScriptEntity & { name: string; area: string | undefined; last_triggered: string | undefined; category: string | undefined; - labels: LabelRegistryEntry[]; + label_entries: LabelRegistryEntry[]; assistants: string[]; assistants_sortable_key: string | undefined; }; @@ -152,7 +157,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { @state() private _activeFilters?: string[]; - @state() private _filteredScripts?: string[] | null; + @state() private _filteredEntityIds?: string[] | null; @state() @storage({ @@ -259,7 +264,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { category: category ? categoryReg?.find((cat) => cat.category_id === category)?.name : undefined, - labels: (labels || []).map( + label_entries: (labels || []).map( (lbl) => labelReg!.find((label) => label.label_id === lbl)! ), assistants, @@ -292,6 +297,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { })} >`, }, + entity_id: getEntityIdHiddenTableColumn(), name: { title: localize("ui.panel.config.script.picker.headers.name"), main: true, @@ -300,60 +306,17 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { direction: "asc", flex: 2, extraTemplate: (script) => - script.labels.length + script.label_entries.length ? html`` : nothing, }, - area: { - title: localize("ui.panel.config.script.picker.headers.area"), - groupable: true, - filterable: true, - sortable: true, - }, - category: { - title: localize("ui.panel.config.script.picker.headers.category"), - defaultHidden: true, - groupable: true, - filterable: true, - sortable: true, - }, - labels: { - title: "", - hidden: true, - filterable: true, - template: (script) => script.labels.map((lbl) => lbl.name).join(" "), - }, - last_triggered: { - sortable: true, - title: localize("ui.card.automation.last_triggered"), - template: (script) => { - if (!script.last_triggered) { - return this.hass.localize("ui.components.relative_time.never"); - } - const date = new Date(script.last_triggered); - const now = new Date(); - const dayDifference = differenceInDays(now, date); - const formattedTime = formatShortDateTimeWithConditionalYear( - date, - this.hass.locale, - this.hass.config - ); - const elementId = "last-triggered-" + slugify(script.entity_id); - return html` - ${dayDifference > 3 - ? formattedTime - : html` - ${formattedTime} - ${relativeTime(date, this.hass.locale)} - `} - `; - }, - }, + area: getAreaTableColumn(localize), + category: getCategoryTableColumn(localize), + labels: getLabelsTableColumn(), + last_triggered: getTriggeredAtTableColumn(localize, this.hass), actions: { title: "", label: this.hass.localize("ui.panel.config.generic.headers.actions"), @@ -451,102 +414,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { } protected render(): TemplateResult { - const categoryItems = html`${this._categories?.map( - (category) => - html` - ${category.icon - ? html`` - : html``} -
    ${category.name}
    -
    ` - )} - -
    - ${this.hass.localize( - "ui.panel.config.automation.picker.bulk_actions.no_category" - )} -
    - -
    - ${this.hass.localize("ui.panel.config.category.editor.add")} -
    -
    `; - - const labelItems = html`${this._labels?.map((label) => { - const color = label.color ? computeCssColor(label.color) : undefined; - const selected = this._selected.every((entityId) => - this.hass.entities[entityId]?.labels.includes(label.label_id) - ); - const partial = - !selected && - this._selected.some((entityId) => - this.hass.entities[entityId]?.labels.includes(label.label_id) - ); - return html` - - - ${label.icon - ? html`` - : nothing} - ${label.name} - - `; - })} - - -
    - ${this.hass.localize("ui.panel.config.labels.add_label")} -
    `; - - const areaItems = html`${Object.values(this.hass.areas).map( - (area) => - html` - ${area.icon - ? html`` - : html``} -
    ${area.name}
    -
    ` - )} - -
    - ${this.hass.localize( - "ui.panel.config.devices.picker.bulk_actions.no_area" - )} -
    -
    - - -
    - ${this.hass.localize( - "ui.panel.config.devices.picker.bulk_actions.add_area" - )} -
    -
    `; - const areasInOverflow = (this._sizeController.value && this._sizeController.value < 900) || (!this._sizeController.value && this.hass.dockedSidebar === "docked"); @@ -561,7 +428,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { this.hass.areas, this._categories, this._labels, - this._filteredScripts + this._filteredEntityIds ); return html` ${!this.narrow - ? html` + ? html` - ${categoryItems} - + ${this._renderCategoryItems()} + ${labelsInOverflow ? nothing - : html` + : html` - ${labelItems} - `} + ${this._renderLabelItems()} + `} ${areasInOverflow ? nothing - : html` + : html` - ${areaItems} - `}` + ${this._renderAreaItems()} + `}` : nothing} ${this.narrow || areasInOverflow - ? html` - - ${ - this.narrow + ? html` + ${this.narrow ? html`` - } - - ${ - this.narrow - ? html` - -
    - ${this.hass.localize( - "ui.panel.config.automation.picker.bulk_actions.move_category" - )} -
    - -
    - ${categoryItems} -
    ` - : nothing - } - ${ - this.narrow || labelsInOverflow - ? html` - -
    - ${this.hass.localize( - "ui.panel.config.automation.picker.bulk_actions.add_label" - )} -
    - -
    - ${labelItems} -
    ` - : nothing - } - ${ - this.narrow || areasInOverflow - ? html` - -
    - ${this.hass.localize( - "ui.panel.config.devices.picker.bulk_actions.move_area" - )} -
    - -
    - ${areaItems} -
    ` - : nothing - } -
    ` + >`} + ${this.narrow + ? html` + ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.move_category" + )} + ${this._renderCategoryItems("submenu")} + ` + : nothing} + ${this.narrow || labelsInOverflow + ? html` + ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.add_label" + )} + ${this._renderLabelItems("submenu")} + ` + : nothing} + ${this.narrow || areasInOverflow + ? html` + ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.move_area" + )} + ${this._renderAreaItems("submenu")} + ` + : nothing} + ` : nothing} ${!this.scripts.length ? html`
    @@ -897,89 +738,47 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { private _applyFilters() { const filters = Object.entries(this._filters); - let items: Set | undefined; + let filteredEntityIds = this.scripts.map((script) => script.entity_id); for (const [key, filter] of filters) { - if (filter.items) { - if (!items) { - items = filter.items; - continue; - } - items = - "intersection" in items - ? // @ts-ignore - items.intersection(filter.items) - : new Set([...items].filter((x) => filter.items!.has(x))); - } if ( - key === "ha-filter-categories" && - Array.isArray(filter.value) && - filter.value.length + // these 4 filters actually apply any selected options, and expose + // the list of scripts that match these options as filter.items + isRelatedItemsFilterUsed(key, filter, [ + "ha-filter-floor-areas", + "ha-filter-devices", + "ha-filter-entities", + "ha-filter-blueprints", + ]) ) { - const categoryItems = new Set(); - this.scripts - .filter( - (script) => - filter.value![0] === - this._entityReg.find((reg) => reg.entity_id === script.entity_id) - ?.categories.script + filteredEntityIds = filteredEntityIds.filter((entityId) => + filter.items!.has(entityId) + ); + + // the filters below only expose the selected options (as filter.value); + // applying the filter must be done here + } else if (isFilterUsed(key, filter, "ha-filter-categories")) { + // category filter only allows a single selected option + filteredEntityIds = filteredEntityIds.filter( + (entityId) => + filter.value![0] === + this._entityReg.find((reg) => reg.entity_id === entityId) + ?.categories.script + ); + } else if (isFilterUsed(key, filter, "ha-filter-labels")) { + filteredEntityIds = filteredEntityIds.filter((entityId) => + this._entityReg + .find((reg) => reg.entity_id === entityId) + ?.labels.some((lbl) => (filter.value as string[]).includes(lbl)) + ); + } else if (isFilterUsed(key, filter, "ha-filter-voice-assistants")) { + filteredEntityIds = filteredEntityIds.filter((entityId) => + getEntityVoiceAssistantsIds(this._entityReg, entityId).some((va) => + (filter.value as string[]).includes(va) ) - .forEach((script) => categoryItems.add(script.entity_id)); - if (!items) { - items = categoryItems; - continue; - } - items = - "intersection" in items - ? // @ts-ignore - items.intersection(categoryItems) - : new Set([...items].filter((x) => categoryItems!.has(x))); - } else if ( - key === "ha-filter-labels" && - Array.isArray(filter.value) && - filter.value.length - ) { - const labelItems = new Set(); - this.scripts - .filter((script) => - this._entityReg - .find((reg) => reg.entity_id === script.entity_id) - ?.labels.some((lbl) => (filter.value as string[]).includes(lbl)) - ) - .forEach((script) => labelItems.add(script.entity_id)); - if (!items) { - items = labelItems; - continue; - } - items = - "intersection" in items - ? // @ts-ignore - items.intersection(labelItems) - : new Set([...items].filter((x) => labelItems!.has(x))); - } else if ( - key === "ha-filter-voice-assistants" && - Array.isArray(filter.value) && - filter.value.length - ) { - const assistItems = new Set(); - this.scripts - .filter((script) => - getEntityVoiceAssistantsIds(this._entityReg, script.entity_id).some( - (va) => (filter.value as string[]).includes(va) - ) - ) - .forEach((script) => assistItems.add(script.entity_id)); - if (!items) { - items = assistItems; - continue; - } - items = - "intersection" in items - ? // @ts-ignore - items.intersection(assistItems) - : new Set([...items].filter((x) => assistItems!.has(x))); + ); } } - this._filteredScripts = items ? [...items] : undefined; + this._filteredEntityIds = filteredEntityIds; } protected updated(changedProps: PropertyValues) { @@ -1056,12 +855,22 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { this._selected = ev.detail.value; } - private _handleBulkCategory = (item) => { - const category = item.value; - this._bulkAddCategory(category); + private _handleBulkCategory = (ev: HaDropdownSelectEvent) => { + const value = ev.detail.item.value; + if (value === "category_create") { + this._bulkCreateCategory(); + return; + } + if (value === "category_none") { + this._bulkAddCategory(null); + return; + } + if (value?.startsWith("category_")) { + this._bulkAddCategory(value.substring(9)); + } }; - private async _bulkAddCategory(category: string) { + private async _bulkAddCategory(category: string | null) { const promises: Promise[] = []; this._selected.forEach((entityId) => { promises.push( @@ -1086,11 +895,18 @@ ${rejected } } - private async _handleBulkLabel(ev) { - const label = ev.currentTarget.value; - const action = ev.currentTarget.action; - this._bulkLabel(label, action); - } + private _handleBulkLabel = (ev: HaDropdownSelectEvent) => { + ev.preventDefault(); + const value = ev.detail.item.value; + if (value === "label_create") { + this._bulkCreateLabel(); + return; + } + if (value?.startsWith("label_")) { + const action = (ev.detail.item as any).action; + this._bulkLabel(value.substring(6), action); + } + }; private async _bulkLabel(label: string, action: "add" | "remove") { const promises: Promise[] = []; @@ -1296,12 +1112,22 @@ ${rejected }); }; - private _handleBulkArea = (item) => { - const area = item.value; - this._bulkAddArea(area); + private _handleBulkArea = (ev: HaDropdownSelectEvent) => { + const value = ev.detail.item.value; + if (value === "area_create") { + this._bulkCreateArea(); + return; + } + if (value === "area_none") { + this._bulkAddArea(null); + return; + } + if (value?.startsWith("area_")) { + this._bulkAddArea(value.substring(5)); + } }; - private async _bulkAddArea(area: string) { + private async _bulkAddArea(area: string | null) { const promises: Promise[] = []; this._selected.forEach((entityId) => { promises.push( @@ -1336,6 +1162,132 @@ ${rejected }); }; + private _renderCategoryItems = (slot = "") => + html`${this._categories?.map( + (category) => + html` + ${category.icon + ? html`` + : html``} + ${category.name} + ` + )} + + ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.no_category" + )} + + + + ${this.hass.localize("ui.panel.config.category.editor.add")} + `; + + private _renderLabelItems = (slot = "") => + html`${this._labels?.map((label) => { + const color = label.color ? computeCssColor(label.color) : undefined; + const selected = this._selected.every((entityId) => + this.hass.entities[entityId]?.labels.includes(label.label_id) + ); + const partial = + !selected && + this._selected.some((entityId) => + this.hass.entities[entityId]?.labels.includes(label.label_id) + ); + return html` + + + ${label.icon + ? html`` + : nothing} + ${label.name} + + `; + })} + + + ${this.hass.localize("ui.panel.config.labels.add_label")} + `; + + private _renderAreaItems = (slot = "") => + html`${Object.values(this.hass.areas).map( + (area) => + html` + ${area.icon + ? html`` + : html``} + ${area.name} + ` + )} + + ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.no_area" + )} + + + + ${this.hass.localize( + "ui.panel.config.devices.picker.bulk_actions.add_area" + )} + `; + + private _handleBulkAction = (ev) => { + const item = ev.detail.item; + const value = item.value; + + if (!value) { + return; + } + + if (value.startsWith("category_")) { + if (value === "category_create") { + this._bulkCreateCategory(); + } else if (value === "category_none") { + this._bulkAddCategory(null); + } else { + this._bulkAddCategory(value.substring(9)); + } + return; + } + + if (value.startsWith("label_")) { + if (value === "label_create") { + this._bulkCreateLabel(); + } else { + const action = item.action; + this._bulkLabel(value.substring(6), action); + } + return; + } + + if (value.startsWith("area_")) { + if (value === "area_create") { + this._bulkCreateArea(); + } else if (value === "area_none") { + this._bulkAddArea(null); + } else { + this._bulkAddArea(value.substring(5)); + } + } + }; + private _handleSortingChanged(ev: CustomEvent) { this._activeSorting = ev.detail; } @@ -1383,7 +1335,11 @@ ${rejected ha-assist-chip { --ha-assist-chip-container-shape: 10px; } - ha-md-button-menu ha-assist-chip { + ha-dropdown::part(menu), + ha-dropdown::part(submenu) { + --auto-size-available-width: calc(50vw - var(--ha-space-4)); + } + ha-dropdown ha-assist-chip { --md-assist-chip-trailing-space: 8px; } ha-label { diff --git a/src/panels/config/script/ha-script-trace.ts b/src/panels/config/script/ha-script-trace.ts index efb5c44fd2..ee51205a34 100644 --- a/src/panels/config/script/ha-script-trace.ts +++ b/src/panels/config/script/ha-script-trace.ts @@ -20,7 +20,6 @@ import { navigate } from "../../../common/navigate"; import "../../../components/ha-button"; import "../../../components/ha-dropdown"; import "../../../components/ha-dropdown-item"; -import type { HaDropdownItem } from "../../../components/ha-dropdown-item"; import "../../../components/ha-icon-button"; import "../../../components/trace/ha-trace-blueprint-config"; import "../../../components/trace/ha-trace-config"; @@ -44,6 +43,7 @@ import "../../../layouts/hass-subpage"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant, Route } from "../../../types"; import { fileDownload } from "../../../util/file_download"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; @customElement("ha-script-trace") export class HaScriptTrace extends LitElement { @@ -525,7 +525,7 @@ export class HaScriptTrace extends LitElement { } } - private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { const action = ev.detail?.item?.value; if (!action) { diff --git a/src/panels/config/script/manual-script-editor.ts b/src/panels/config/script/manual-script-editor.ts index e32ec1396e..651cd88752 100644 --- a/src/panels/config/script/manual-script-editor.ts +++ b/src/panels/config/script/manual-script-editor.ts @@ -51,7 +51,7 @@ import { normalizeScriptConfig, } from "../../../data/script"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; -import type { HomeAssistant } from "../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; import { showToast } from "../../../util/toast"; import "../automation/action/ha-automation-action"; @@ -553,7 +553,7 @@ export class HaManualScriptEditor extends SubscribeMixin(LitElement) { this._sidebarElement?.focus(); } - private _sidebarConfigChanged(ev: CustomEvent<{ value: SidebarConfig }>) { + private _sidebarConfigChanged(ev: ValueChangedEvent) { ev.stopPropagation(); if (!this._sidebarConfig) { return; @@ -581,7 +581,6 @@ export class HaManualScriptEditor extends SubscribeMixin(LitElement) { } private _saveScript() { - this.triggerCloseSidebar(); fireEvent(this, "save-script"); } diff --git a/src/panels/config/storage/dialog-move-datadisk.ts b/src/panels/config/storage/dialog-move-datadisk.ts index b6ff482365..8da2a3c758 100644 --- a/src/panels/config/storage/dialog-move-datadisk.ts +++ b/src/panels/config/storage/dialog-move-datadisk.ts @@ -3,11 +3,10 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; -import "../../../components/ha-dialog"; import "../../../components/ha-button"; -import "../../../components/ha-list-item"; +import "../../../components/ha-dialog"; import "../../../components/ha-select"; +import type { HaSelectSelectEvent } from "../../../components/ha-select"; import "../../../components/ha-spinner"; import { extractApiErrorMessage, @@ -132,26 +131,18 @@ class MoveDatadiskDialog extends LitElement { "ui.panel.config.storage.datadisk.select_device" )} @selected=${this._selectDevice} - @closed=${stopPropagation} - dialogInitialFocus - fixedMenuPosition - > - ${this._disks.map( - (disk) => - html` - ${disk.vendor} ${disk.model} - - ${this.hass.localize( - "ui.panel.config.storage.datadisk.extra_information", - { - size: bytesToString(disk.size), - serial: disk.serial, - } - )} - - ` - )} - + .options=${this._disks.map((disk) => ({ + value: disk.id, + label: `${disk.vendor} ${disk.model}`, + secondary: this.hass.localize( + "ui.panel.config.storage.datadisk.extra_information", + { + size: bytesToString(disk.size), + serial: disk.serial, + } + ), + }))} + > ${heading} ${description}
    - ${hasChildren - ? html`` - : nothing} +
    @@ -106,9 +105,11 @@ export class StorageBreakdownChart extends LitElement { storageInfo: HostDisksUsage | null | undefined ) => { let totalSpaceGB = hostInfo.disk_total; - let usedSpaceGB = hostInfo.disk_used; let freeSpaceGB = hostInfo.disk_free || hostInfo.disk_total - hostInfo.disk_used; + // hostInfo.disk_used doesn't include system reserved space, + // so we calculate used space based on total and free space + let usedSpaceGB = totalSpaceGB - freeSpaceGB; if (storageInfo) { const totalSpace = @@ -213,26 +214,24 @@ export class StorageBreakdownChart extends LitElement { static styles = css` .header { display: flex; - align-items: flex-start; - justify-content: space-between; - margin-bottom: var(--ha-space-2); + align-items: flex-end; + gap: var(--ha-space-2); } .heading-text { display: flex; - flex-direction: column; - gap: var(--ha-space-1); + flex: 1; } .heading { - font-weight: 500; - font-size: var(--ha-font-size-m); color: var(--primary-text-color); + line-height: var(--ha-line-height-expanded); + margin-right: var(--ha-space-2); } .description { - font-size: var(--ha-font-size-s); color: var(--secondary-text-color); + line-height: var(--ha-line-height-expanded); } ha-icon-button { @@ -242,7 +241,7 @@ export class StorageBreakdownChart extends LitElement { } .chart-container { - transition: height var(--ha-animation-base-duration) ease; + transition: height var(--ha-animation-duration-slow) ease; overflow: hidden; } @@ -264,7 +263,7 @@ export class StorageBreakdownChart extends LitElement { ha-segmented-bar, ha-sunburst-chart { - animation: fade-in var(--ha-animation-base-duration) ease; + animation: fade-in var(--ha-animation-duration-slow) ease; } @keyframes fade-in { diff --git a/src/panels/config/tags/ha-config-tags.ts b/src/panels/config/tags/ha-config-tags.ts index efa5370cb3..835ab3ab54 100644 --- a/src/panels/config/tags/ha-config-tags.ts +++ b/src/panels/config/tags/ha-config-tags.ts @@ -88,6 +88,12 @@ export class HaConfigTags extends SubscribeMixin(LitElement) { filterable: true, flex: 2, }, + id: { + title: localize("ui.panel.config.tag.headers.tag_id"), + main: true, + sortable: true, + filterable: true, + }, last_scanned_datetime: { title: localize("ui.panel.config.tag.headers.last_scanned"), sortable: true, diff --git a/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-config.ts b/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-config.ts index 79a71e4a80..8115fe4b39 100644 --- a/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-config.ts +++ b/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-config.ts @@ -12,7 +12,7 @@ export class AssistPipelineDetailConfig extends LitElement { @property({ attribute: false }) public data?: Partial; - @property({ attribute: false, type: Array }) + @property({ attribute: false }) public supportedLanguages?: string[]; public async focus() { diff --git a/src/panels/config/voice-assistants/assist-pref.ts b/src/panels/config/voice-assistants/assist-pref.ts index e39197669f..0e0007651f 100644 --- a/src/panels/config/voice-assistants/assist-pref.ts +++ b/src/panels/config/voice-assistants/assist-pref.ts @@ -1,3 +1,4 @@ +import "@home-assistant/webawesome/dist/components/divider/divider"; import { mdiBug, mdiCommentProcessingOutline, @@ -18,14 +19,16 @@ import { formatLanguageCode } from "../../../common/language/format_language"; import { navigate } from "../../../common/navigate"; import "../../../components/ha-alert"; import "../../../components/ha-button"; -import "../../../components/ha-button-menu"; import "../../../components/ha-card"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-icon-button"; import "../../../components/ha-list"; import "../../../components/ha-list-item"; import "../../../components/ha-svg-icon"; import "../../../components/ha-switch"; import type { HaSwitch } from "../../../components/ha-switch"; +import "../../../components/voice-assistant-brand-icon"; import type { AssistPipeline } from "../../../data/assist_pipeline"; import { createAssistPipeline, @@ -46,9 +49,9 @@ import { } from "../../../dialogs/generic/show-dialog-box"; import { showVoiceCommandDialog } from "../../../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; import type { HomeAssistant } from "../../../types"; -import { brandsUrl } from "../../../util/brands-url"; import { documentationUrl } from "../../../util/documentation-url"; import { showVoiceAssistantPipelineDetailDialog } from "./show-dialog-voice-assistant-pipeline-detail"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; @customElement("assist-pref") export class AssistPref extends LitElement { @@ -103,16 +106,12 @@ export class AssistPref extends LitElement { return html`

    - Assist + + Assist

    ${formatLanguageCode(pipeline.language, this.hass.locale)} - + - + ${this.hass!.localize( "ui.panel.config.voice_assistants.assistants.pipeline.start_conversation" )} - - + ${this.hass.localize( "ui.panel.config.voice_assistants.assistants.pipeline.detail.set_as_preferred" )} - - - + + + ${this.hass.localize( "ui.panel.config.voice_assistants.assistants.pipeline.detail.debug" )} - - - + + + ${this.hass.localize("ui.common.duplicate")} - - + + ${this.hass.localize("ui.common.delete")} - - - + + + ` )} @@ -290,24 +278,42 @@ export class AssistPref extends LitElement { } } - private _talkWithPipeline(ev) { - const id = ev.currentTarget.id as string; + private _handlePipelineMenuAction(ev: HaDropdownSelectEvent) { + const value = ev.detail.item.value; + const id = (ev.detail.item as any).data as string; + switch (value) { + case "talk": + this._talkWithPipeline(id); + break; + case "set-preferred": + this._setPreferredPipeline(id); + break; + case "debug": + this._debugPipeline(id); + break; + case "duplicate": + this._duplicatePipeline(id); + break; + case "delete": + this._deletePipeline(id); + break; + } + } + + private _talkWithPipeline(id: string) { showVoiceCommandDialog(this, this.hass, { pipeline_id: id }); } - private async _setPreferredPipeline(ev) { - const id = ev.currentTarget.id as string; + private async _setPreferredPipeline(id: string) { await setAssistPipelinePreferred(this.hass!, id); this._preferred = id; } - private async _debugPipeline(ev) { - const id = ev.currentTarget.id as string; + private async _debugPipeline(id: string) { navigate(`/config/voice-assistants/debug/${id}`); } - private async _duplicatePipeline(ev: Event) { - const id = (ev.currentTarget as HTMLElement).id as string; + private async _duplicatePipeline(id: string) { const pipeline = this._pipelines.find((res) => res.id === id); if (!pipeline) { showAlertDialog(this, { @@ -330,8 +336,7 @@ export class AssistPref extends LitElement { this._openDialog(newPipeline); } - private async _deletePipeline(ev) { - const id = ev.currentTarget.id as string; + private async _deletePipeline(id: string) { if (this._preferred === id) { showAlertDialog(this, { text: this.hass!.localize( @@ -430,16 +435,7 @@ export class AssistPref extends LitElement { --mdc-list-side-padding-left: 16px; } - ha-list-item.danger { - color: var(--error-color); - border-top: 1px solid var(--divider-color); - } - - ha-button-menu a { - text-decoration: none; - } - - ha-svg-icon { + ha-list-item span ha-svg-icon { color: currentColor; width: 16px; } @@ -458,12 +454,18 @@ export class AssistPref extends LitElement { align-items: center; padding-bottom: 0; } - img { + voice-assistant-brand-icon { height: 28px; margin-right: 16px; margin-inline-end: 16px; margin-inline-start: initial; } + + ha-dropdown { + font-size: var(--ha-font-size-m); + font-family: var(--ha-font-family-body); + letter-spacing: normal; + } `; } diff --git a/src/panels/config/voice-assistants/cloud-alexa-pref.ts b/src/panels/config/voice-assistants/cloud-alexa-pref.ts index 1898957d5d..49d0540e5e 100644 --- a/src/panels/config/voice-assistants/cloud-alexa-pref.ts +++ b/src/panels/config/voice-assistants/cloud-alexa-pref.ts @@ -18,7 +18,7 @@ import { setExposeNewEntities, } from "../../../data/expose"; import type { HomeAssistant } from "../../../types"; -import { brandsUrl } from "../../../util/brands-url"; +import "../../../components/voice-assistant-brand-icon"; @customElement("cloud-alexa-pref") export class CloudAlexaPref extends LitElement { @@ -64,16 +64,12 @@ export class CloudAlexaPref extends LitElement { return html`

    - ${this.hass.localize("ui.panel.config.cloud.account.alexa.title")} + + ${this.hass.localize("ui.panel.config.cloud.account.alexa.title")}

    - Google Assistant - Amazon Alexa + + + +

    ${this.hass.localize( @@ -162,11 +152,12 @@ export class CloudDiscover extends LitElement { } .feature .logos { margin-bottom: 16px; + display: flex; + gap: var(--ha-space-4); } .feature .logos > * { width: 40px; height: 40px; - margin: 0 4px; } .round-icon { border-radius: var(--ha-border-radius-circle); diff --git a/src/panels/config/voice-assistants/cloud-google-pref.ts b/src/panels/config/voice-assistants/cloud-google-pref.ts index 9c8b8b4c3c..f9cdc01478 100644 --- a/src/panels/config/voice-assistants/cloud-google-pref.ts +++ b/src/panels/config/voice-assistants/cloud-google-pref.ts @@ -20,8 +20,8 @@ import { setExposeNewEntities, } from "../../../data/expose"; import type { HomeAssistant } from "../../../types"; -import { brandsUrl } from "../../../util/brands-url"; import { showSaveSuccessToast } from "../../../util/toast-saved-success"; +import "../../../components/voice-assistant-brand-icon"; @customElement("cloud-google-pref") export class CloudGooglePref extends LitElement { @@ -70,16 +70,12 @@ export class CloudGooglePref extends LitElement { return html`

    - ${this.hass.localize("ui.panel.config.cloud.account.google.title")} + + ${this.hass.localize("ui.panel.config.cloud.account.google.title")}

    - + ${this.hass.localize( "ui.panel.config.voice_assistants.assistants.pipeline.detail.add_streaming_wake_word" )} - `}
    @@ -243,8 +241,12 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { `; } - private _handleShowWakeWord() { - this._hideWakeWord = false; + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { + const action = ev.detail?.item?.value; + + if (action === "show_wake_word") { + this._hideWakeWord = false; + } } private _valueChanged(ev: CustomEvent) { diff --git a/src/panels/config/voice-assistants/entity-voice-settings.ts b/src/panels/config/voice-assistants/entity-voice-settings.ts index a2a6e790cd..3a98774e23 100644 --- a/src/panels/config/voice-assistants/entity-voice-settings.ts +++ b/src/panels/config/voice-assistants/entity-voice-settings.ts @@ -37,9 +37,9 @@ import { fetchCloudGoogleEntity } from "../../../data/google_assistant"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; -import { brandsUrl } from "../../../util/brands-url"; import { documentationUrl } from "../../../util/documentation-url"; import type { EntityRegistrySettings } from "../entities/entity-registry-settings"; +import "../../../components/voice-assistant-brand-icon"; @customElement("entity-voice-settings") export class EntityVoiceSettings extends SubscribeMixin(LitElement) { @@ -213,17 +213,12 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) { return html` - + .voiceAssistantId=${key} + .hass=${this.hass} + > + ${voiceAssistants[key].name} ${!supported ? html`
    diff --git a/src/panels/config/voice-assistants/expose/assistants-table-column.ts b/src/panels/config/voice-assistants/expose/assistants-table-column.ts index 328006d1a4..d3b8505ac1 100644 --- a/src/panels/config/voice-assistants/expose/assistants-table-column.ts +++ b/src/panels/config/voice-assistants/expose/assistants-table-column.ts @@ -16,36 +16,39 @@ export function getAssistantsTableColumn( visible?: boolean ): DataTableColumnData { return { - title: localize( - "ui.panel.config.voice_assistants.expose.headers.assistants" - ), + title: localize("ui.panel.config.generic.headers.assistants"), type: "flex", defaultHidden: !visible, sortable: true, + showNarrow: true, minWidth: "160px", maxWidth: "160px", valueColumn: "assistants_sortable_key", template: (entry: any) => html`${entry.assistants.length !== 0 - ? availableAssistants.map((vaId) => { - const supported = - !supportedEntities?.[vaId] || - supportedEntities[vaId].includes(entry.entity_id); - const manual = entry.manAssistants?.includes(vaId); - return getAssistantsTableColumnIcon( - entry.assistants.includes(vaId), - vaId, - hass, - entitiesToCheck, - manual, - !supported - ); - }) + ? html`
    + ${availableAssistants.map((vaId) => { + const supported = + !supportedEntities?.[vaId] || + supportedEntities[vaId].includes(entry.entity_id); + const manual = entry.manAssistants?.includes(vaId); + return getAssistantsTableColumnIcon( + entry.entity_id, + entry.assistants.includes(vaId), + vaId, + hass, + entitiesToCheck, + manual, + !supported + ); + })} +
    ` : nothing}`, }; } export const getAssistantsTableColumnIcon = ( + id: string, show: boolean, vaId: string, hass: HomeAssistant, @@ -58,6 +61,7 @@ export const getAssistantsTableColumnIcon = ( ); return show ? html` { +): string | undefined => { let result = 0; if (!entityAssistants.length) { - return "z"; + return undefined; } const assistantsOrdered = [ "conversation", diff --git a/src/panels/config/voice-assistants/expose/expose-assistant-icon.ts b/src/panels/config/voice-assistants/expose/expose-assistant-icon.ts index 7721d8a8da..d66fdc911a 100644 --- a/src/panels/config/voice-assistants/expose/expose-assistant-icon.ts +++ b/src/panels/config/voice-assistants/expose/expose-assistant-icon.ts @@ -2,11 +2,12 @@ import { mdiAlertCircle } from "@mdi/js"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; +import { slugify } from "../../../../common/string/slugify"; import { voiceAssistants } from "../../../../data/expose"; import type { HomeAssistant } from "../../../../types"; -import { brandsUrl } from "../../../../util/brands-url"; import "../../../../components/ha-svg-icon"; import "../../../../components/ha-tooltip"; +import "../../../../components/voice-assistant-brand-icon"; @customElement("voice-assistants-expose-assistant-icon") export class VoiceAssistantExposeAssistantIcon extends LitElement { @@ -23,23 +24,17 @@ export class VoiceAssistantExposeAssistantIcon extends LitElement { render() { if (!this.assistant || !voiceAssistants[this.assistant]) return nothing; + const id = slugify(this.id) + "-" + this.assistant; return html` -
    - + + .voiceAssistantId=${this.assistant} + .hass=${this.hass} + > + ${this.unsupported ? html` @@ -73,13 +68,6 @@ export class VoiceAssistantExposeAssistantIcon extends LitElement { .container { position: relative; } - .logo { - position: relative; - height: 24px; - margin-right: 16px; - margin-inline-end: 16px; - margin-inline-start: initial; - } .unsupported { color: var(--error-color); position: absolute; diff --git a/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts b/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts index 2a6612b14f..9e4854f4f5 100644 --- a/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts +++ b/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts @@ -49,6 +49,11 @@ import "../../../layouts/hass-tabs-subpage-data-table"; import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant, Route } from "../../../types"; +import { + getEntityIdTableColumn, + getDomainTableColumn, + getAreaTableColumn, +} from "../common/data-table-columns"; import "./expose/expose-assistant-icon"; import { getAssistantsTableColumn, @@ -178,30 +183,9 @@ export class VoiceAssistantsExpose extends LitElement { direction: "asc", flex: 2, }, - area: { - title: localize("ui.panel.config.voice_assistants.expose.headers.area"), - sortable: true, - groupable: true, - filterable: true, - template: (entry) => entry.area || "—", - }, - entity_id: { - title: localize( - "ui.panel.config.voice_assistants.expose.headers.entity_id" - ), - sortable: true, - filterable: true, - defaultHidden: true, - }, - domain: { - title: localize( - "ui.panel.config.voice_assistants.expose.headers.domain" - ), - sortable: false, - hidden: true, - filterable: true, - groupable: true, - }, + area: getAreaTableColumn(localize), + entity_id: getEntityIdTableColumn(localize, true), + domain: getDomainTableColumn(localize), assistants: getAssistantsTableColumn( localize, this.hass, @@ -802,11 +786,6 @@ export class VoiceAssistantsExpose extends LitElement { .header-btns > ha-icon-button { margin: var(--ha-space-2); } - ha-button-menu { - margin-left: var(--ha-space-2); - margin-inline-start: var(--ha-space-2); - margin-inline-end: initial; - } .clear { color: var(--primary-color); padding-left: var(--ha-space-2); diff --git a/src/panels/energy/ha-panel-energy.ts b/src/panels/energy/ha-panel-energy.ts index 371b951cfe..afaeb9c27c 100644 --- a/src/panels/energy/ha-panel-energy.ts +++ b/src/panels/energy/ha-panel-energy.ts @@ -115,6 +115,8 @@ class PanelEnergy extends LitElement { @state() private _prefs?: EnergyPreferences; + @state() private _searchParms = new URLSearchParams(window.location.search); + @state() private _error?: string; @@ -142,7 +144,7 @@ class PanelEnergy extends LitElement { } const oldHass = changedProps.get("hass") as this["hass"]; - if (oldHass && oldHass.localize !== this.hass.localize) { + if (this._lovelace && oldHass && oldHass.localize !== this.hass.localize) { this._setLovelace(); } } @@ -248,6 +250,8 @@ class PanelEnergy extends LitElement { .lovelace=${this._lovelace} .route=${this.route} .panel=${this.panel} + .backButton=${this._searchParms.has("historyBack")} + .backPath=${this._searchParms.get("backPath") || "/"} .extraActionItems=${this._extraActionItems} @reload-energy-panel=${this._reloadConfig} class=${classMap({ "has-period-selector": showEnergySelector })} @@ -259,7 +263,9 @@ class PanelEnergy extends LitElement { ` @@ -694,8 +700,13 @@ class PanelEnergy extends LitElement { var(--safe-area-inset-left, 0px) ); inset-inline-end: var(--safe-area-inset-right, 0); + transition: + left var(--ha-animation-duration-normal) ease, + right var(--ha-animation-duration-normal) ease, + inset-inline-start var(--ha-animation-duration-normal) ease, + inset-inline-end var(--ha-animation-duration-normal) ease; margin: 0 auto; - max-width: calc(min(450px, 100% - var(--ha-space-4))); + max-width: calc(min(470px, 100% - var(--ha-space-4))); box-sizing: border-box; padding-left: var(--ha-space-2); padding-right: 0; diff --git a/src/panels/history/ha-panel-history.ts b/src/panels/history/ha-panel-history.ts index 12d5f32bae..67f6bb3738 100644 --- a/src/panels/history/ha-panel-history.ts +++ b/src/panels/history/ha-panel-history.ts @@ -1,4 +1,3 @@ -import type { ActionDetail } from "@material/mwc-list"; import { mdiDotsVertical, mdiDownload, @@ -27,11 +26,11 @@ import { import { MIN_TIME_BETWEEN_UPDATES } from "../../components/chart/ha-chart-base"; import "../../components/chart/state-history-charts"; import type { StateHistoryCharts } from "../../components/chart/state-history-charts"; -import "../../components/ha-button-menu"; import "../../components/ha-date-range-picker"; +import "../../components/ha-dropdown"; +import "../../components/ha-dropdown-item"; import "../../components/ha-icon-button"; import "../../components/ha-icon-button-arrow-prev"; -import "../../components/ha-list-item"; import "../../components/ha-menu-button"; import "../../components/ha-spinner"; import "../../components/ha-target-picker"; @@ -51,6 +50,7 @@ import { haStyle } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; import { fileDownload } from "../../util/file_download"; import { addEntitiesToLovelaceView } from "../lovelace/editor/add-entities-to-view"; +import type { HaDropdownSelectEvent } from "../../components/ha-dropdown"; @customElement("ha-panel-history") class HaPanelHistory extends LitElement { @@ -148,23 +148,23 @@ class HaPanelHistory extends LitElement { > ` : ""} - + - + ${this.hass.localize("ui.panel.history.download_data")} - - + + - + ${this.hass.localize("ui.panel.history.add_card")} - - - + + +
    @@ -477,12 +477,13 @@ class HaPanelHistory extends LitElement { navigate(`/history?${createSearchParam(params)}`, { replace: true }); } - private async _handleMenuAction(ev: CustomEvent) { - switch (ev.detail.index) { - case 0: + private async _handleMenuAction(ev: HaDropdownSelectEvent) { + const action = ev.detail.item.value; + switch (action) { + case "download": this._downloadHistory(); break; - case 1: + case "add-card": this._suggestCard(); break; } diff --git a/src/panels/home/dialogs/dialog-new-overview.ts b/src/panels/home/dialogs/dialog-new-overview.ts new file mode 100644 index 0000000000..517d56ae03 --- /dev/null +++ b/src/panels/home/dialogs/dialog-new-overview.ts @@ -0,0 +1,163 @@ +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/ha-button"; +import "../../../components/ha-dialog-footer"; +import "../../../components/ha-wa-dialog"; +import type { HassDialog } from "../../../dialogs/make-dialog-manager"; +import { haStyleDialog } from "../../../resources/styles"; +import type { HomeAssistant } from "../../../types"; +import type { NewOverviewDialogParams } from "./show-dialog-new-overview"; + +@customElement("dialog-new-overview") +export class DialogNewOverview + extends LitElement + implements HassDialog +{ + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: NewOverviewDialogParams; + + @state() private _open = false; + + public showDialog(params: NewOverviewDialogParams): void { + this._params = params; + this._open = true; + } + + public closeDialog(): boolean { + this._open = false; + return true; + } + + private _dialogClosed(): void { + if (this._params) { + this._params.dismiss(); + } + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render() { + if (!this._params) { + return nothing; + } + + return html` + + + + + ${this.hass.localize( + "ui.panel.home.new_overview_dialog.ok_understood" + )} + + + + `; + } + + static styles = [ + haStyleDialog, + css` + ha-wa-dialog { + --dialog-content-padding: var(--ha-space-6); + } + + .content { + line-height: var(--ha-line-height-normal); + } + + p { + margin: 0 0 var(--ha-space-4) 0; + color: var(--secondary-text-color); + } + + h3 { + margin: var(--ha-space-4) 0 var(--ha-space-2) 0; + font-size: var(--ha-font-size-l); + font-weight: var(--ha-font-weight-medium); + } + + ul { + margin: 0 0 var(--ha-space-4) 0; + padding-left: var(--ha-space-6); + color: var(--secondary-text-color); + } + + li { + margin-bottom: var(--ha-space-2); + } + + li strong { + color: var(--primary-text-color); + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-new-overview": DialogNewOverview; + } +} diff --git a/src/panels/home/dialogs/show-dialog-new-overview.ts b/src/panels/home/dialogs/show-dialog-new-overview.ts new file mode 100644 index 0000000000..909f79651f --- /dev/null +++ b/src/panels/home/dialogs/show-dialog-new-overview.ts @@ -0,0 +1,18 @@ +import { fireEvent } from "../../../common/dom/fire_event"; + +export interface NewOverviewDialogParams { + dismiss: () => void; +} + +export const loadNewOverviewDialog = () => import("./dialog-new-overview"); + +export const showNewOverviewDialog = ( + element: HTMLElement, + params: NewOverviewDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-new-overview", + dialogImport: loadNewOverviewDialog, + dialogParams: params, + }); +}; diff --git a/src/panels/home/ha-panel-home.ts b/src/panels/home/ha-panel-home.ts index b07af08fbc..e15495d13c 100644 --- a/src/panels/home/ha-panel-home.ts +++ b/src/panels/home/ha-panel-home.ts @@ -1,20 +1,36 @@ +import { ResizeController } from "@lit-labs/observers/resize-controller"; +import { mdiPencil } from "@mdi/js"; import type { CSSResultGroup, PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import { atLeastVersion } from "../../common/config/version"; +import { navigate } from "../../common/navigate"; import { debounce } from "../../common/util/debounce"; import { deepEqual } from "../../common/util/deep-equal"; +import "../../components/ha-button"; +import "../../components/ha-svg-icon"; +import { updateAreaRegistryEntry } from "../../data/area/area_registry"; +import { updateDeviceRegistryEntry } from "../../data/device/device_registry"; import { fetchFrontendSystemData, saveFrontendSystemData, type HomeFrontendSystemData, } from "../../data/frontend"; import type { LovelaceDashboardStrategyConfig } from "../../data/lovelace/config/types"; +import { mdiHomeAssistant } from "../../resources/home-assistant-logo-svg"; import type { HomeAssistant, PanelInfo, Route } from "../../types"; import { showToast } from "../../util/toast"; +import { showAreaRegistryDetailDialog } from "../config/areas/show-dialog-area-registry-detail"; +import { showDeviceRegistryDetailDialog } from "../config/devices/device-registry-detail/show-dialog-device-registry-detail"; +import { showAddIntegrationDialog } from "../config/integrations/show-add-integration-dialog"; import "../lovelace/hui-root"; +import type { ExtraActionItem } from "../lovelace/hui-root"; import { expandLovelaceConfigStrategies } from "../lovelace/strategies/get-strategy"; import type { Lovelace } from "../lovelace/types"; import { showEditHomeDialog } from "./dialogs/show-dialog-edit-home"; +import { showNewOverviewDialog } from "./dialogs/show-dialog-new-overview"; +import { hasLegacyOverviewPanel } from "../../data/panel"; @customElement("ha-panel-home") class PanelHome extends LitElement { @@ -30,6 +46,33 @@ class PanelHome extends LitElement { @state() private _config: FrontendSystemData["home"] = {}; + @state() private _extraActionItems?: ExtraActionItem[]; + + private get _showBanner(): boolean { + // Don't show if already dismissed + if (this._config.welcome_banner_dismissed) { + return false; + } + // Don't show if HA is not running + if (this.hass.config.state !== "RUNNING") { + return false; + } + // Show banner only for users who: + // 1. Were onboarded before 2026.2 (or have no onboarded_version) + // 2. Don't have a custom "lovelace" dashboard (old overview) + const onboardedVersion = this.hass.systemData?.onboarded_version; + const isNewInstance = + onboardedVersion && atLeastVersion(onboardedVersion, 2026, 2); + const hasOldOverview = hasLegacyOverviewPanel(this.hass); + return !isNewInstance && !hasOldOverview; + } + + private _bannerHeight = new ResizeController(this, { + target: null, + callback: (entries) => + (entries[0]?.target as HTMLElement | undefined)?.offsetHeight ?? 0, + }); + public willUpdate(changedProps: PropertyValues) { super.willUpdate(changedProps); // Initial setup @@ -38,6 +81,10 @@ class PanelHome extends LitElement { return; } + if (changedProps.has("route")) { + this._updateExtraActionItems(); + } + if (!changedProps.has("hass")) { return; } @@ -66,12 +113,13 @@ class PanelHome extends LitElement { this.hass.config.state === "RUNNING" && oldHass.config.state !== "RUNNING" ) { - this._setLovelace(); + this._setup(); } } } private async _setup() { + this._updateExtraActionItems(); try { const [_, data] = await Promise.all([ this.hass.loadFragmentTranslation("lovelace"), @@ -92,30 +140,183 @@ class PanelHome extends LitElement { ); private _registriesChanged = async () => { + // If on an area view that no longer exists, redirect to overview + const path = this.route?.path?.split("/")[1]; + if (path?.startsWith("areas-")) { + const areaId = path.replace("areas-", ""); + if (!this.hass.areas[areaId]) { + navigate("/home"); + return; + } + } this._setLovelace(); }; + private _updateExtraActionItems() { + const path = this.route?.path?.split("/")[1]; + + if (path?.startsWith("areas-")) { + this._extraActionItems = [ + { + icon: mdiPencil, + labelKey: "ui.panel.lovelace.menu.edit_area", + action: this._editArea, + }, + ]; + } else if (!path || path === "overview") { + this._extraActionItems = [ + { + icon: mdiPencil, + labelKey: "ui.panel.lovelace.menu.edit_overview", + action: this._editHome, + }, + ]; + } else { + this._extraActionItems = undefined; + } + } + + private _editHome = () => { + showEditHomeDialog(this, { + config: this._config, + saveConfig: async (config) => { + await this._saveConfig(config); + }, + }); + }; + + private _editArea = async () => { + const path = this.route?.path?.split("/")[1]; + if (!path?.startsWith("areas-")) { + return; + } + const areaId = path.replace("areas-", ""); + const area = this.hass.areas[areaId]; + if (!area) { + return; + } + await this.hass.loadFragmentTranslation("config"); + showAreaRegistryDetailDialog(this, { + entry: area, + updateEntry: (values) => + updateAreaRegistryEntry(this.hass, areaId, values), + }); + }; + + private _handleLLCustomEvent = (ev: Event) => { + const detail = (ev as CustomEvent).detail; + if (detail.home_panel) { + const { type } = detail.home_panel; + switch (type) { + case "assign_area": { + const { device_id } = detail.home_panel; + this._showAssignAreaDialog(device_id); + break; + } + case "add_integration": { + this._showAddIntegrationDialog(); + break; + } + } + } + }; + + private async _showAddIntegrationDialog() { + await this.hass.loadFragmentTranslation("config"); + showAddIntegrationDialog(this, { navigateToResult: false }); + } + + private _showAssignAreaDialog(deviceId: string) { + const device = this.hass.devices[deviceId]; + if (!device) { + return; + } + showDeviceRegistryDetailDialog(this, { + device, + updateEntry: async (updates) => { + await updateDeviceRegistryEntry(this.hass, deviceId, updates); + }, + }); + } + protected render() { if (!this._lovelace) { return nothing; } + const huiRootStyle = styleMap({ + "--view-container-padding-top": this._bannerHeight.value + ? `${this._bannerHeight.value}px` + : undefined, + }); + return html` + ${this._renderBanner()} `; } + private _renderBanner() { + if (!this._showBanner) { + return nothing; + } + + return html` + + `; + } + + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + if (changedProps.has("_showBanner") || changedProps.has("_lovelace")) { + const banner = this.shadowRoot?.querySelector(".banner"); + if (banner) { + this._bannerHeight.observe(banner); + } + } + } + + private _learnMore() { + showNewOverviewDialog(this, { + dismiss: async () => { + const newConfig = { + ...this._config, + welcome_banner_dismissed: true, + }; + this._config = newConfig; + await saveFrontendSystemData(this.hass.connection, "home", newConfig); + }, + }); + } + private async _setLovelace() { const strategyConfig: LovelaceDashboardStrategyConfig = { strategy: { type: "home", favorite_entities: this._config.favorite_entities, + home_panel: true, }, }; @@ -138,20 +339,11 @@ class PanelHome extends LitElement { enableFullEditMode: () => undefined, saveConfig: async () => undefined, deleteConfig: async () => undefined, - setEditMode: this._setEditMode, + setEditMode: () => undefined, showToast: () => undefined, }; } - private _setEditMode = () => { - showEditHomeDialog(this, { - config: this._config, - saveConfig: async (config) => { - await this._saveConfig(config); - }, - }); - }; - private async _saveConfig(config: HomeFrontendSystemData): Promise { try { await saveFrontendSystemData(this.hass.connection, "home", config); @@ -176,6 +368,45 @@ class PanelHome extends LitElement { :host { display: block; } + .banner { + display: flex; + align-items: center; + flex-wrap: wrap; + padding: var(--ha-space-2) var(--ha-space-4); + background-color: var(--primary-color); + color: var(--text-primary-color); + gap: var(--ha-space-2); + position: fixed; + top: var(--header-height, 56px); + left: var(--mdc-drawer-width, 0px); + right: 0; + z-index: 5; + } + .banner-content { + display: flex; + align-items: center; + gap: var(--ha-space-2); + flex: 1; + min-width: 200px; + } + .banner ha-svg-icon { + --mdc-icon-size: 24px; + flex-shrink: 0; + } + .banner-text { + font-size: 14px; + font-weight: 500; + } + .banner-actions { + display: flex; + flex: none; + gap: var(--ha-space-2); + align-items: center; + margin-inline-start: auto; + } + .banner-actions ha-button::part(base) { + text-wrap: nowrap; + } `; } diff --git a/src/panels/light/ha-panel-light.ts b/src/panels/light/ha-panel-light.ts index 603d7997a6..f2036e3415 100644 --- a/src/panels/light/ha-panel-light.ts +++ b/src/panels/light/ha-panel-light.ts @@ -1,6 +1,7 @@ import type { CSSResultGroup, PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; import { goBack } from "../../common/navigate"; import { debounce } from "../../common/util/debounce"; import { deepEqual } from "../../common/util/deep-equal"; @@ -95,7 +96,7 @@ class PanelLight extends LitElement { protected render() { return html` -
    +
    ${ this._searchParms.has("historyBack") @@ -175,7 +176,6 @@ class PanelLight extends LitElement { .header { background-color: var(--app-header-background-color); color: var(--app-header-text-color, white); - border-bottom: var(--app-header-border-bottom, none); position: fixed; top: 0; width: calc( @@ -186,7 +186,6 @@ class PanelLight extends LitElement { ); padding-top: var(--safe-area-inset-top); z-index: 4; - transition: box-shadow 200ms linear; display: flex; flex-direction: row; -webkit-backdrop-filter: var(--app-header-backdrop-filter, none); @@ -220,15 +219,19 @@ class PanelLight extends LitElement { padding: 0px 12px; font-weight: var(--ha-font-weight-normal); box-sizing: border-box; + border-bottom: var(--app-header-border-bottom, none); } :host([narrow]) .toolbar { padding: 0 4px; } .main-title { - margin: var(--margin-title); + margin-inline-start: var(--ha-space-6); line-height: var(--ha-line-height-normal); flex-grow: 1; } + .narrow .main-title { + margin-inline-start: var(--ha-space-2); + } hui-view-container { position: relative; display: flex; diff --git a/src/panels/lovelace/card-features/hui-area-controls-card-feature.ts b/src/panels/lovelace/card-features/hui-area-controls-card-feature.ts index 846c89cf3b..080d91bc3d 100644 --- a/src/panels/lovelace/card-features/hui-area-controls-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-area-controls-card-feature.ts @@ -5,7 +5,7 @@ import { classMap } from "lit/directives/class-map"; import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; import { ensureArray } from "../../../common/array/ensure-array"; -import { generateEntityFilter } from "../../../common/entity/entity_filter"; +import { computeDomain } from "../../../common/entity/compute_domain"; import { computeGroupEntitiesState, toggleGroupEntities, @@ -15,64 +15,37 @@ import { domainColorProperties } from "../../../common/entity/state_color"; import "../../../components/ha-control-button"; import "../../../components/ha-control-button-group"; import "../../../components/ha-domain-icon"; +import "../../../components/ha-state-icon"; import "../../../components/ha-svg-icon"; import type { AreaRegistryEntry } from "../../../data/area/area_registry"; +import { + AREA_CONTROLS_BUTTONS, + getAreaControlEntities, + MAX_DEFAULT_AREA_CONTROLS, +} from "../../../data/area/area_controls"; import { forwardHaptic } from "../../../data/haptics"; import { computeCssVariable } from "../../../resources/css-variables"; import type { HomeAssistant } from "../../../types"; import type { AreaCardFeatureContext } from "../cards/hui-area-card"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { - AreaControl, - AreaControlsCardFeatureConfig, - LovelaceCardFeatureContext, - LovelaceCardFeaturePosition, +import { + AREA_CONTROL_DOMAINS, + type AreaControl, + type AreaControlDomain, + type AreaControlsCardFeatureConfig, + type LovelaceCardFeatureContext, + type LovelaceCardFeaturePosition, } from "./types"; -import { AREA_CONTROLS } from "./types"; -interface AreaControlsButton { - filter: { - domain: string; - device_class?: string; - }; +type NormalizedControl = + | { type: "domain"; value: AreaControlDomain } + | { type: "entity"; value: string }; + +interface ControlButtonElement extends HTMLElement { + control: NormalizedControl; } -const coverButton = (deviceClass: string) => ({ - filter: { - domain: "cover", - device_class: deviceClass, - }, -}); - -export const AREA_CONTROLS_BUTTONS: Record = { - light: { - filter: { - domain: "light", - }, - }, - fan: { - filter: { - domain: "fan", - }, - }, - switch: { - filter: { - domain: "switch", - }, - }, - "cover-blind": coverButton("blind"), - "cover-curtain": coverButton("curtain"), - "cover-damper": coverButton("damper"), - "cover-awning": coverButton("awning"), - "cover-door": coverButton("door"), - "cover-garage": coverButton("garage"), - "cover-gate": coverButton("gate"), - "cover-shade": coverButton("shade"), - "cover-shutter": coverButton("shutter"), - "cover-window": coverButton("window"), -}; - export const supportsAreaControlsCardFeature = ( hass: HomeAssistant, context: LovelaceCardFeatureContext @@ -81,31 +54,6 @@ export const supportsAreaControlsCardFeature = ( return !!area; }; -export const getAreaControlEntities = ( - controls: AreaControl[], - areaId: string, - excludeEntities: string[] | undefined, - hass: HomeAssistant -): Record => - controls.reduce( - (acc, control) => { - const controlButton = AREA_CONTROLS_BUTTONS[control]; - const filter = generateEntityFilter(hass, { - area: areaId, - entity_category: "none", - ...controlButton.filter, - }); - - acc[control] = Object.keys(hass.entities).filter( - (entityId) => filter(entityId) && !excludeEntities?.includes(entityId) - ); - return acc; - }, - {} as Record - ); - -export const MAX_DEFAULT_AREA_CONTROLS = 4; - @customElement("hui-area-controls-card-feature") class HuiAreaControlsCardFeature extends LitElement @@ -129,10 +77,23 @@ class HuiAreaControlsCardFeature | undefined; } - private get _controls() { - return ( - this._config?.controls || (AREA_CONTROLS as unknown as AreaControl[]) - ); + private get _controls(): AreaControl[] { + return this._config?.controls || [...AREA_CONTROL_DOMAINS]; + } + + private _normalizeControl(control: AreaControl): NormalizedControl { + // Handle explicit entity format + if (typeof control === "object" && "entity_id" in control) { + return { type: "entity", value: control.entity_id }; + } + + // String format: domain control (if valid) or invalid + if (AREA_CONTROL_DOMAINS.includes(control as AreaControlDomain)) { + return { type: "domain", value: control as AreaControlDomain }; + } + + // Invalid domain string - treat as entity + return { type: "entity", value: control }; } static getStubConfig(): AreaControlsCardFeatureConfig { @@ -156,22 +117,37 @@ class HuiAreaControlsCardFeature private _handleButtonTap(ev: MouseEvent) { ev.stopPropagation(); - if (!this.context?.area_id || !this.hass || !this._config) { + if (!this.hass || !this._config) { return; } - const control = (ev.currentTarget as any).control as AreaControl; + + const normalized = (ev.currentTarget as ControlButtonElement).control; + + if (normalized.type === "entity") { + const entity = this.hass.states[normalized.value]; + if (entity) { + forwardHaptic(this, "light"); + toggleGroupEntities(this.hass, [entity]); + } + return; + } + + if (!this.context?.area_id) { + return; + } + + const domainControls = this._domainControls(this._controls); const controlEntities = this._controlEntities( - this._controls, + domainControls, this.context.area_id, this.context.exclude_entities, - this.hass!.entities, - this.hass!.devices, - this.hass!.areas + this.hass.entities, + this.hass.devices, + this.hass.areas ); - const entitiesIds = controlEntities[control]; - const entities = entitiesIds + const entities = controlEntities[normalized.value] .map((entityId) => this.hass!.states[entityId] as HassEntity | undefined) .filter((v): v is HassEntity => Boolean(v)); @@ -179,9 +155,19 @@ class HuiAreaControlsCardFeature toggleGroupEntities(this.hass, entities); } + private _domainControls = memoizeOne((controls: AreaControl[]) => + controls + .map((c) => this._normalizeControl(c)) + .filter( + (n): n is { type: "domain"; value: AreaControlDomain } => + n.type === "domain" + ) + .map((n) => n.value) + ); + private _controlEntities = memoizeOne( ( - controls: AreaControl[], + controls: AreaControlDomain[], areaId: string, excludeEntities: string[] | undefined, // needed to update memoized function when entities, devices or areas change @@ -202,8 +188,15 @@ class HuiAreaControlsCardFeature return nothing; } + const normalizedControls = this._controls.map((c) => + this._normalizeControl(c) + ); + + // Get domain controls for entity lookup + const domainControls = this._domainControls(this._controls); + const controlEntities = this._controlEntities( - this._controls, + domainControls, this.context.area_id!, this.context.exclude_entities, this.hass!.entities, @@ -211,13 +204,17 @@ class HuiAreaControlsCardFeature this.hass!.areas ); - const supportedControls = this._controls.filter( - (control) => controlEntities[control].length > 0 + // Filter controls while preserving original order + const allControls = normalizedControls.filter((n) => + n.type === "domain" + ? controlEntities[n.value].length > 0 + : this.hass!.states[n.value] && + !this.context?.exclude_entities?.includes(n.value) ); const displayControls = this._config.controls - ? supportedControls - : supportedControls.slice(0, MAX_DEFAULT_AREA_CONTROLS); // Limit to max if using default controls + ? allControls + : allControls.slice(0, MAX_DEFAULT_AREA_CONTROLS); // Limit to max if using default controls if (!displayControls.length) { return nothing; @@ -229,35 +226,54 @@ class HuiAreaControlsCardFeature "no-stretch": this.position === "inline", })} > - ${displayControls.map((control) => { - const button = AREA_CONTROLS_BUTTONS[control]; + ${displayControls.map((normalized) => { + let active: boolean; + let label: string; + let domain: string; + let deviceClass: string | undefined; + let entityState: string; + let entity: HassEntity | undefined; - const entityIds = controlEntities[control]; + if (normalized.type === "domain") { + const button = AREA_CONTROLS_BUTTONS[normalized.value]; + const controlEntityIds = controlEntities[normalized.value]; - const entities = entityIds - .map( - (entityId) => - this.hass!.states[entityId] as HassEntity | undefined - ) - .filter((v): v is HassEntity => Boolean(v)); + const entities = controlEntityIds + .map( + (entityId) => + this.hass!.states[entityId] as HassEntity | undefined + ) + .filter((v): v is HassEntity => Boolean(v)); - const groupState = computeGroupEntitiesState(entities); + const groupState = computeGroupEntitiesState(entities); - const active = entities[0] - ? stateActive(entities[0], groupState) - : false; + active = entities[0] ? stateActive(entities[0], groupState) : false; + label = this.hass!.localize( + `ui.card_features.area_controls.${normalized.value}.${active ? "off" : "on"}` + ); + domain = button.filter.domain; + deviceClass = button.filter.device_class + ? ensureArray(button.filter.device_class)[0] + : undefined; + entityState = groupState; + } else if (normalized.type === "entity") { + entity = this.hass!.states[normalized.value]; + if (!entity) { + return nothing; + } - const label = this.hass!.localize( - `ui.card_features.area_controls.${control}.${active ? "off" : "on"}` - ); - - const domain = button.filter.domain; - const deviceClass = button.filter.device_class - ? ensureArray(button.filter.device_class)[0] - : undefined; + active = stateActive(entity); + label = this.hass!.localize( + `ui.card.common.turn_${active ? "off" : "on"}` + ); + domain = computeDomain(entity.entity_id); + entityState = entity.state; + } else { + return nothing; + } const activeColor = computeCssVariable( - domainColorProperties(domain, deviceClass, groupState, true) + domainColorProperties(domain, deviceClass, entityState, true) ); return html` @@ -268,15 +284,20 @@ class HuiAreaControlsCardFeature .title=${label} aria-label=${label} class=${active ? "active" : ""} - .control=${control} + .control=${normalized} @click=${this._handleButtonTap} > - + ${normalized.type === "domain" + ? html`` + : html``} `; })} diff --git a/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts index f4d804dcb1..1a7259f12f 100644 --- a/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts @@ -1,15 +1,13 @@ import { mdiFan } from "@mdi/js"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; +import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-attribute-icon"; import "../../../components/ha-control-select"; import type { ControlSelectOption } from "../../../components/ha-control-select"; import "../../../components/ha-control-select-menu"; -import type { HaControlSelectMenu } from "../../../components/ha-control-select-menu"; import "../../../components/ha-list-item"; import type { ClimateEntity } from "../../../data/climate"; import { ClimateEntityFeature } from "../../../data/climate"; @@ -51,9 +49,6 @@ class HuiClimateFanModesCardFeature @state() _currentFanMode?: string; - @query("ha-control-select-menu", true) - private _haSelect?: HaControlSelectMenu; - private get _stateObj() { if (!this.hass || !this.context || !this.context.entity_id) { return undefined; @@ -96,27 +91,16 @@ class HuiClimateFanModesCardFeature } } - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - if (this._haSelect && changedProps.has("hass")) { - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if ( - this.hass && - this.hass.formatEntityAttributeValue !== - oldHass?.formatEntityAttributeValue - ) { - this._haSelect.layoutOptions(); - } - } - } - - private async _valueChanged(ev: CustomEvent) { - const fanMode = - (ev.detail as any).value ?? ((ev.target as any).value as string); + private async _valueChanged( + ev: CustomEvent<{ value?: string; item?: { value: string } }> + ) { + const fanMode = ev.detail.value ?? ev.detail.item?.value; const oldFanMode = this._stateObj!.attributes.fan_mode; - if (fanMode === oldFanMode) return; + if (fanMode === oldFanMode || !fanMode) { + return; + } this._currentFanMode = fanMode; @@ -157,19 +141,21 @@ class HuiClimateFanModesCardFeature "fan_mode", mode ), - icon: html``, })); if (this._config.style === "icons") { return html` ({ + ...option, + icon: html``, + }))} .value=${this._currentFanMode} @value-changed=${this._valueChanged} hide-option-label @@ -182,32 +168,22 @@ class HuiClimateFanModesCardFeature return html` - ${this._currentFanMode - ? html`` - : html` `} - ${options.map( - (option) => html` - - ${option.icon}${option.label} - - ` - )} + @wa-select=${this._valueChanged} + .options=${options.map((option) => ({ + ...option, + attributeIcon: { + stateObj: stateObj, + attribute: "fan_mode", + attributeValue: option.value, + }, + }))} + > `; } diff --git a/src/panels/lovelace/card-features/hui-climate-hvac-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-hvac-modes-card-feature.ts index 1c8874fbaf..202f4d5aa5 100644 --- a/src/panels/lovelace/card-features/hui-climate-hvac-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-hvac-modes-card-feature.ts @@ -1,15 +1,12 @@ import { mdiThermostat } from "@mdi/js"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; import { computeDomain } from "../../../common/entity/compute_domain"; import { stateColorCss } from "../../../common/entity/state_color"; import "../../../components/ha-control-select"; -import type { ControlSelectOption } from "../../../components/ha-control-select"; import "../../../components/ha-control-select-menu"; -import type { HaControlSelectMenu } from "../../../components/ha-control-select-menu"; import "../../../components/ha-list-item"; import type { ClimateEntity, HvacMode } from "../../../data/climate"; import { @@ -51,9 +48,6 @@ class HuiClimateHvacModesCardFeature @state() _currentHvacMode?: HvacMode; - @query("ha-control-select-menu", true) - private _haSelect?: HaControlSelectMenu; - private get _stateObj() { if (!this.hass || !this.context || !this.context.entity_id) { return undefined; @@ -95,31 +89,20 @@ class HuiClimateHvacModesCardFeature } } - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - if (this._haSelect && changedProps.has("hass")) { - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if ( - this.hass && - this.hass.formatEntityAttributeValue !== - oldHass?.formatEntityAttributeValue - ) { - this._haSelect.layoutOptions(); - } + private async _valueChanged( + ev: CustomEvent<{ value?: string; item?: { value: string } }> + ) { + const mode = ev.detail.value ?? ev.detail.item?.value; + + if (mode === this._stateObj!.state || !mode) { + return; } - } - - private async _valueChanged(ev: CustomEvent) { - const mode = - (ev.detail as any).value ?? ((ev.target as any).value as HvacMode); - - if (mode === this._stateObj!.state) return; const oldMode = this._stateObj!.state as HvacMode; - this._currentHvacMode = mode; + this._currentHvacMode = mode as HvacMode; try { - await this._setMode(mode); + await this._setMode(this._currentHvacMode); } catch (_err) { this._currentHvacMode = oldMode; } @@ -150,19 +133,13 @@ class HuiClimateHvacModesCardFeature .sort(compareClimateHvacModes) .reverse(); - const options = filterModes( - ordererHvacModes, - this._config.hvac_modes - ).map((mode) => ({ - value: mode, - label: this.hass!.formatEntityState(this._stateObj!, mode), - icon: html` - - `, - })); + const options = filterModes(ordererHvacModes, this._config.hvac_modes).map( + (mode) => ({ + value: mode as string, + label: this.hass!.formatEntityState(this._stateObj!, mode), + iconPath: climateHvacModeIcon(mode), + }) + ); if (this._config.style === "dropdown") { return html` @@ -172,35 +149,23 @@ class HuiClimateHvacModesCardFeature .label=${this.hass.localize("ui.card.climate.mode")} .value=${this._currentHvacMode} .disabled=${this._stateObj.state === UNAVAILABLE} - fixedMenuPosition - naturalMenuWidth - @selected=${this._valueChanged} - @closed=${stopPropagation} + @wa-select=${this._valueChanged} + .options=${options} > - ${this._currentHvacMode - ? html` - - ` - : html` - - `} - ${options.map( - (option) => html` - - ${option.icon}${option.label} - - ` - )} + `; } return html` ({ + ...option, + icon: html``, + }))} .value=${this._currentHvacMode} @value-changed=${this._valueChanged} hide-option-label diff --git a/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts index b4c8aa04da..d13941c40d 100644 --- a/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts @@ -1,15 +1,12 @@ import { mdiTuneVariant } from "@mdi/js"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; +import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-attribute-icon"; import "../../../components/ha-control-select"; -import type { ControlSelectOption } from "../../../components/ha-control-select"; import "../../../components/ha-control-select-menu"; -import type { HaControlSelectMenu } from "../../../components/ha-control-select-menu"; import "../../../components/ha-list-item"; import type { ClimateEntity } from "../../../data/climate"; import { ClimateEntityFeature } from "../../../data/climate"; @@ -51,9 +48,6 @@ class HuiClimatePresetModesCardFeature @state() _currentPresetMode?: string; - @query("ha-control-select-menu", true) - private _haSelect?: HaControlSelectMenu; - private get _stateObj() { if (!this.hass || !this.context || !this.context.entity_id) { return undefined; @@ -98,27 +92,16 @@ class HuiClimatePresetModesCardFeature } } - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - if (this._haSelect && changedProps.has("hass")) { - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if ( - this.hass && - this.hass.formatEntityAttributeValue !== - oldHass?.formatEntityAttributeValue - ) { - this._haSelect.layoutOptions(); - } - } - } - - private async _valueChanged(ev: CustomEvent) { - const presetMode = - (ev.detail as any).value ?? ((ev.target as any).value as string); + private async _valueChanged( + ev: CustomEvent<{ value?: string; item?: { value: string } }> + ) { + const presetMode = ev.detail.value ?? ev.detail.item?.value; const oldPresetMode = this._stateObj!.attributes.preset_mode; - if (presetMode === oldPresetMode) return; + if (presetMode === oldPresetMode || !presetMode) { + return; + } this._currentPresetMode = presetMode; @@ -152,26 +135,28 @@ class HuiClimatePresetModesCardFeature const options = filterModes( stateObj.attributes.preset_modes, this._config!.preset_modes - ).map((mode) => ({ + ).map((mode) => ({ value: mode, label: this.hass!.formatEntityAttributeValue( this._stateObj!, "preset_mode", mode ), - icon: html``, })); if (this._config.style === "icons") { return html` ({ + ...option, + icon: html``, + }))} .value=${this._currentPresetMode} @value-changed=${this._valueChanged} hide-option-label @@ -187,34 +172,23 @@ class HuiClimatePresetModesCardFeature return html` ({ + ...option, + attributeIcon: { + stateObj: stateObj, + attribute: "preset_mode", + attributeValue: option.value, + }, + }))} > - ${this._currentPresetMode - ? html`` - : html` - - `} - ${options.map( - (option) => html` - - ${option.icon}${option.label} - - ` - )} + `; } diff --git a/src/panels/lovelace/card-features/hui-climate-swing-horizontal-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-swing-horizontal-modes-card-feature.ts index 6d99d380c8..cfc0386575 100644 --- a/src/panels/lovelace/card-features/hui-climate-swing-horizontal-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-swing-horizontal-modes-card-feature.ts @@ -1,15 +1,12 @@ import { mdiArrowOscillating } from "@mdi/js"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; +import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-attribute-icon"; import "../../../components/ha-control-select"; -import type { ControlSelectOption } from "../../../components/ha-control-select"; import "../../../components/ha-control-select-menu"; -import type { HaControlSelectMenu } from "../../../components/ha-control-select-menu"; import "../../../components/ha-list-item"; import type { ClimateEntity } from "../../../data/climate"; import { ClimateEntityFeature } from "../../../data/climate"; @@ -51,9 +48,6 @@ class HuiClimateSwingHorizontalModesCardFeature @state() _currentSwingHorizontalMode?: string; - @query("ha-control-select-menu", true) - private _haSelect?: HaControlSelectMenu; - private get _stateObj() { if (!this.hass || !this.context || !this.context.entity_id) { return undefined; @@ -99,28 +93,20 @@ class HuiClimateSwingHorizontalModesCardFeature } } - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - if (this._haSelect && changedProps.has("hass")) { - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if ( - this.hass && - this.hass.formatEntityAttributeValue !== - oldHass?.formatEntityAttributeValue - ) { - this._haSelect.layoutOptions(); - } - } - } - - private async _valueChanged(ev: CustomEvent) { - const swingHorizontalMode = - (ev.detail as any).value ?? ((ev.target as any).value as string); + private async _valueChanged( + ev: CustomEvent<{ value?: string; item?: { value: string } }> + ) { + const swingHorizontalMode = ev.detail.value ?? ev.detail.item?.value; const oldSwingHorizontalMode = this._stateObj!.attributes.swing_horizontal_mode; - if (swingHorizontalMode === oldSwingHorizontalMode) return; + if ( + swingHorizontalMode === oldSwingHorizontalMode || + !swingHorizontalMode + ) { + return; + } this._currentSwingHorizontalMode = swingHorizontalMode; @@ -154,26 +140,28 @@ class HuiClimateSwingHorizontalModesCardFeature const options = filterModes( stateObj.attributes.swing_horizontal_modes, this._config!.swing_horizontal_modes - ).map((mode) => ({ + ).map((mode) => ({ value: mode, label: this.hass!.formatEntityAttributeValue( this._stateObj!, "swing_horizontal_mode", mode ), - icon: html``, })); if (this._config.style === "icons") { return html` ({ + ...option, + icon: html``, + }))} .value=${this._currentSwingHorizontalMode} @value-changed=${this._valueChanged} hide-option-label @@ -189,6 +177,7 @@ class HuiClimateSwingHorizontalModesCardFeature return html` ({ + ...option, + attributeIcon: { + stateObj: stateObj, + attribute: "swing_horizontal_mode", + attributeValue: option.value, + }, + }))} > - ${this._currentSwingHorizontalMode - ? html`` - : html` `} - ${options.map( - (option) => html` - - ${option.icon}${option.label} - - ` - )} + `; } diff --git a/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts index f4c231b462..5cc902591c 100644 --- a/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts @@ -1,15 +1,12 @@ import { mdiArrowOscillating } from "@mdi/js"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; +import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-attribute-icon"; import "../../../components/ha-control-select"; -import type { ControlSelectOption } from "../../../components/ha-control-select"; import "../../../components/ha-control-select-menu"; -import type { HaControlSelectMenu } from "../../../components/ha-control-select-menu"; import "../../../components/ha-list-item"; import type { ClimateEntity } from "../../../data/climate"; import { ClimateEntityFeature } from "../../../data/climate"; @@ -51,9 +48,6 @@ class HuiClimateSwingModesCardFeature @state() _currentSwingMode?: string; - @query("ha-control-select-menu", true) - private _haSelect?: HaControlSelectMenu; - private get _stateObj() { if (!this.hass || !this.context || !this.context.entity_id) { return undefined; @@ -98,27 +92,16 @@ class HuiClimateSwingModesCardFeature } } - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - if (this._haSelect && changedProps.has("hass")) { - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if ( - this.hass && - this.hass.formatEntityAttributeValue !== - oldHass?.formatEntityAttributeValue - ) { - this._haSelect.layoutOptions(); - } - } - } - - private async _valueChanged(ev: CustomEvent) { - const swingMode = - (ev.detail as any).value ?? ((ev.target as any).value as string); + private async _valueChanged( + ev: CustomEvent<{ value?: string; item?: { value: string } }> + ) { + const swingMode = ev.detail.value ?? ev.detail.item?.value; const oldSwingMode = this._stateObj!.attributes.swing_mode; - if (swingMode === oldSwingMode) return; + if (swingMode === oldSwingMode || !swingMode) { + return; + } this._currentSwingMode = swingMode; @@ -152,26 +135,28 @@ class HuiClimateSwingModesCardFeature const options = filterModes( stateObj.attributes.swing_modes, this._config!.swing_modes - ).map((mode) => ({ + ).map((mode) => ({ value: mode, label: this.hass!.formatEntityAttributeValue( this._stateObj!, "swing_mode", mode ), - icon: html``, })); if (this._config.style === "icons") { return html` ({ + ...option, + icon: html``, + }))} .value=${this._currentSwingMode} @value-changed=${this._valueChanged} hide-option-label @@ -187,35 +172,22 @@ class HuiClimateSwingModesCardFeature return html` - ${this._currentSwingMode - ? html`` - : html` `} - ${options.map( - (option) => html` - - ${option.icon}${option.label} - - ` - )} + @wa-select=${this._valueChanged} + .options=${options.map((option) => ({ + ...option, + attributeIcon: { + stateObj, + attribute: "swing_mode", + attributeValue: option.value, + }, + }))} + > `; } diff --git a/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts index 73b877e3a7..2f71cadec3 100644 --- a/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts @@ -1,15 +1,12 @@ import { mdiTuneVariant } from "@mdi/js"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; +import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-attribute-icon"; import "../../../components/ha-control-select"; -import type { ControlSelectOption } from "../../../components/ha-control-select"; import "../../../components/ha-control-select-menu"; -import type { HaControlSelectMenu } from "../../../components/ha-control-select-menu"; import "../../../components/ha-list-item"; import { UNAVAILABLE } from "../../../data/entity/entity"; import type { FanEntity } from "../../../data/fan"; @@ -50,9 +47,6 @@ class HuiFanPresetModesCardFeature @state() _currentPresetMode?: string; - @query("ha-control-select-menu", true) - private _haSelect?: HaControlSelectMenu; - private get _stateObj() { if (!this.hass || !this.context || !this.context.entity_id) { return undefined; @@ -92,27 +86,16 @@ class HuiFanPresetModesCardFeature } } - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - if (this._haSelect && changedProps.has("hass")) { - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if ( - this.hass && - this.hass.formatEntityAttributeValue !== - oldHass?.formatEntityAttributeValue - ) { - this._haSelect.layoutOptions(); - } - } - } - - private async _valueChanged(ev: CustomEvent) { - const presetMode = - (ev.detail as any).value ?? ((ev.target as any).value as string); + private async _valueChanged( + ev: CustomEvent<{ value?: string; item?: { value: string } }> + ) { + const presetMode = ev.detail.value ?? ev.detail.item?.value; const oldPresetMode = this._stateObj!.attributes.preset_mode; - if (presetMode === oldPresetMode) return; + if (presetMode === oldPresetMode || !presetMode) { + return; + } this._currentPresetMode = presetMode; @@ -146,26 +129,28 @@ class HuiFanPresetModesCardFeature const options = filterModes( stateObj.attributes.preset_modes, this._config!.preset_modes - ).map((mode) => ({ + ).map((mode) => ({ value: mode, label: this.hass!.formatEntityAttributeValue( this._stateObj!, "preset_mode", mode ), - icon: html``, })); if (this._config.style === "icons") { return html` ({ + ...option, + icon: html``, + }))} .value=${this._currentPresetMode} @value-changed=${this._valueChanged} hide-option-label @@ -181,34 +166,23 @@ class HuiFanPresetModesCardFeature return html` ({ + ...option, + attributeIcon: { + stateObj: stateObj, + attribute: "preset_mode", + attributeValue: option.value, + }, + }))} > - ${this._currentPresetMode - ? html`` - : html` - - `} - ${options.map( - (option) => html` - - ${option.icon}${option.label} - - ` - )} + `; } diff --git a/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts index ee133d412b..07a10e7848 100644 --- a/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts @@ -1,15 +1,12 @@ import { mdiTuneVariant } from "@mdi/js"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; +import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-attribute-icon"; import "../../../components/ha-control-select"; -import type { ControlSelectOption } from "../../../components/ha-control-select"; import "../../../components/ha-control-select-menu"; -import type { HaControlSelectMenu } from "../../../components/ha-control-select-menu"; import "../../../components/ha-list-item"; import { UNAVAILABLE } from "../../../data/entity/entity"; import type { HumidifierEntity } from "../../../data/humidifier"; @@ -60,9 +57,6 @@ class HuiHumidifierModesCardFeature | undefined; } - @query("ha-control-select-menu", true) - private _haSelect?: HaControlSelectMenu; - static getStubConfig(): HumidifierModesCardFeatureConfig { return { type: "humidifier-modes", @@ -96,27 +90,16 @@ class HuiHumidifierModesCardFeature } } - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - if (this._haSelect && changedProps.has("hass")) { - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if ( - this.hass && - this.hass.formatEntityAttributeValue !== - oldHass?.formatEntityAttributeValue - ) { - this._haSelect.layoutOptions(); - } - } - } - - private async _valueChanged(ev: CustomEvent) { - const mode = - (ev.detail as any).value ?? ((ev.target as any).value as string); + private async _valueChanged( + ev: CustomEvent<{ value?: string; item?: { value: string } }> + ) { + const mode = ev.detail.value ?? ev.detail.item?.value; const oldMode = this._stateObj!.attributes.mode; - if (mode === oldMode) return; + if (mode === oldMode || !mode) { + return; + } this._currentMode = mode; @@ -150,26 +133,28 @@ class HuiHumidifierModesCardFeature const options = filterModes( stateObj.attributes.available_modes, this._config!.modes - ).map((mode) => ({ + ).map((mode) => ({ value: mode, label: this.hass!.formatEntityAttributeValue( this._stateObj!, "mode", mode ), - icon: html``, })); if (this._config.style === "icons") { return html` ({ + ...option, + icon: html``, + }))} .value=${this._currentMode} @value-changed=${this._valueChanged} hide-option-label @@ -182,35 +167,23 @@ class HuiHumidifierModesCardFeature return html` ({ + ...option, + attributeIcon: { + stateObj, + attribute: "mode", + attributeValue: option.value, + }, + }))} > - ${this._currentMode - ? html`` - : html``} - ${options.map( - (option) => html` - - ${option.icon}${option.label} - - ` - )} + `; } diff --git a/src/panels/lovelace/card-features/hui-select-options-card-feature.ts b/src/panels/lovelace/card-features/hui-select-options-card-feature.ts index a554b5cf01..1180dd9a8c 100644 --- a/src/panels/lovelace/card-features/hui-select-options-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-select-options-card-feature.ts @@ -1,11 +1,9 @@ import type { PropertyValues } from "lit"; import { html, LitElement, nothing } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; import { computeDomain } from "../../../common/entity/compute_domain"; import "../../../components/ha-control-select-menu"; -import type { HaControlSelectMenu } from "../../../components/ha-control-select-menu"; import "../../../components/ha-list-item"; import { UNAVAILABLE } from "../../../data/entity/entity"; import type { InputSelectEntity } from "../../../data/input_select"; @@ -18,6 +16,7 @@ import type { LovelaceCardFeatureContext, SelectOptionsCardFeatureConfig, } from "./types"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; export const supportsSelectOptionsCardFeature = ( hass: HomeAssistant, @@ -44,9 +43,6 @@ class HuiSelectOptionsCardFeature @state() _currentOption?: string; - @query("ha-control-select-menu", true) - private _haSelect!: HaControlSelectMenu; - private get _stateObj() { if (!this.hass || !this.context || !this.context.entity_id) { return undefined; @@ -89,30 +85,17 @@ class HuiSelectOptionsCardFeature } } - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - if (changedProps.has("hass")) { - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if ( - this.hass && - this.hass.formatEntityAttributeValue !== - oldHass?.formatEntityAttributeValue - ) { - this._haSelect.layoutOptions(); - } - } - } - - private async _valueChanged(ev: CustomEvent) { - const option = (ev.target as any).value as string; + private async _valueChanged(ev: HaDropdownSelectEvent) { + const option = ev.detail.item?.value; const oldOption = this._stateObj!.state; if ( option === oldOption || !this._stateObj!.attributes.options.includes(option) - ) + ) { return; + } this._currentOption = option; @@ -157,25 +140,18 @@ class HuiSelectOptionsCardFeature .value=${stateObj.state} .options=${options} .disabled=${this._stateObj.state === UNAVAILABLE} - fixedMenuPosition - naturalMenuWidth - @selected=${this._valueChanged} - @closed=${stopPropagation} + @wa-select=${this._valueChanged} > - ${options.map( - (option) => html` - - ${this.hass!.formatEntityState(stateObj, option)} - - ` - )} `; } private _getOptions = memoizeOne( (attributeOptions: string[], configOptions: string[] | undefined) => - filterModes(attributeOptions, configOptions) + filterModes(attributeOptions, configOptions).map((option) => ({ + value: option, + label: this.hass!.formatEntityState(this._stateObj!, option), + })) ); static get styles() { diff --git a/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts index 5d12fdaacc..8a9f554b88 100644 --- a/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts @@ -1,25 +1,20 @@ import { mdiWaterBoiler } from "@mdi/js"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; import { computeDomain } from "../../../common/entity/compute_domain"; import { stateColorCss } from "../../../common/entity/state_color"; +import "../../../components/ha-attribute-icon"; import "../../../components/ha-control-select"; -import type { ControlSelectOption } from "../../../components/ha-control-select"; import "../../../components/ha-control-select-menu"; -import type { HaControlSelectMenu } from "../../../components/ha-control-select-menu"; import "../../../components/ha-list-item"; import { UNAVAILABLE } from "../../../data/entity/entity"; import type { OperationMode, WaterHeaterEntity, } from "../../../data/water_heater"; -import { - compareWaterHeaterOperationMode, - computeOperationModeIcon, -} from "../../../data/water_heater"; +import { compareWaterHeaterOperationMode } from "../../../data/water_heater"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; @@ -54,9 +49,6 @@ class HuiWaterHeaterOperationModeCardFeature @state() _currentOperationMode?: OperationMode; - @query("ha-control-select-menu", true) - private _haSelect?: HaControlSelectMenu; - private get _stateObj() { if (!this.hass || !this.context || !this.context.entity_id) { return undefined; @@ -100,31 +92,20 @@ class HuiWaterHeaterOperationModeCardFeature } } - protected updated(changedProps: PropertyValues) { - super.updated(changedProps); - if (this._haSelect && changedProps.has("hass")) { - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if ( - this.hass && - this.hass.formatEntityAttributeValue !== - oldHass?.formatEntityAttributeValue - ) { - this._haSelect.layoutOptions(); - } + private async _valueChanged( + ev: CustomEvent<{ value?: string; item?: { value: string } }> + ) { + const mode = ev.detail.value ?? ev.detail.item?.value; + + if (mode === this._stateObj!.state || !mode) { + return; } - } - - private async _valueChanged(ev: CustomEvent) { - const mode = - (ev.detail as any).value ?? ((ev.target as any).value as OperationMode); - - if (mode === this._stateObj!.state) return; const oldMode = this._stateObj!.state as OperationMode; - this._currentOperationMode = mode; + this._currentOperationMode = mode as OperationMode; try { - await this._setMode(mode); + await this._setMode(this._currentOperationMode); } catch (_err) { this._currentOperationMode = oldMode; } @@ -155,57 +136,51 @@ class HuiWaterHeaterOperationModeCardFeature .sort(compareWaterHeaterOperationMode) .reverse(); - const options = filterModes( - orderedModes, - this._config.operation_modes - ).map((mode) => ({ - value: mode, - label: this.hass!.formatEntityState(this._stateObj!, mode), - icon: html` - - `, - })); + const options = filterModes(orderedModes, this._config.operation_modes).map( + (mode) => ({ + value: mode, + label: this.hass!.formatEntityState(this._stateObj!, mode), + }) + ); if (this._config.style === "dropdown") { return html` ({ + ...option, + attributeIcon: { + stateObj: this._stateObj, + attribute: "operation_mode", + attributeValue: option.value, + }, + }))} > - ${this._currentOperationMode - ? html` - - ` - : html` - - `} - ${options.map( - (option) => html` - - ${option.icon}${option.label} - - ` - )} + `; } return html` ({ + ...option, + icon: html` + + `, + }))} .value=${this._currentOperationMode} @value-changed=${this._valueChanged} hide-option-label diff --git a/src/panels/lovelace/card-features/types.ts b/src/panels/lovelace/card-features/types.ts index 99bcf19ec7..9e6acaf11d 100644 --- a/src/panels/lovelace/card-features/types.ts +++ b/src/panels/lovelace/card-features/types.ts @@ -202,7 +202,7 @@ export interface TrendGraphCardFeatureConfig { detail?: boolean; } -export const AREA_CONTROLS = [ +export const AREA_CONTROL_DOMAINS = [ "light", "fan", "cover-shutter", @@ -218,7 +218,9 @@ export const AREA_CONTROLS = [ "switch", ] as const; -export type AreaControl = (typeof AREA_CONTROLS)[number]; +export type AreaControlDomain = (typeof AREA_CONTROL_DOMAINS)[number]; + +export type AreaControl = AreaControlDomain | { entity_id: string }; export interface AreaControlsCardFeatureConfig { type: "area-controls"; diff --git a/src/panels/lovelace/cards/energy/common/energy-chart-options.ts b/src/panels/lovelace/cards/energy/common/energy-chart-options.ts index 4c3bb2c8e4..9bf0560aa6 100644 --- a/src/panels/lovelace/cards/energy/common/energy-chart-options.ts +++ b/src/panels/lovelace/cards/energy/common/energy-chart-options.ts @@ -1,8 +1,9 @@ import type { HassConfig } from "home-assistant-js-websocket"; import { - differenceInMonths, subHours, differenceInDays, + differenceInMonths, + differenceInCalendarMonths, differenceInYears, startOfYear, addMilliseconds, @@ -12,6 +13,7 @@ import { addHours, startOfDay, addDays, + subDays, } from "date-fns"; import type { BarSeriesOption, @@ -33,15 +35,27 @@ import { filterXSS } from "../../../../../common/util/xss"; import type { StatisticPeriod } from "../../../../../data/recorder"; import { getSuggestedPeriod } from "../../../../../data/energy"; -export function getSuggestedMax(period: StatisticPeriod, end: Date): number { +// Number of days of padding when showing time axis in months +const MONTH_TIME_AXIS_PADDING = 5; + +export function getSuggestedMax( + period: StatisticPeriod, + end: Date, + noRounding: boolean +): Date { + // Maximum period depends on whether plotting a line chart or discrete bars. + // - For line charts we must be plotting all the way to end of a given period, + // otherwise we cut off the last period of data. + // - For bar charts we need to round down to the start of the final bars period + // to avoid unnecessary padding of the chart. let suggestedMax = new Date(end); - if (period === "5minute") { - return suggestedMax.getTime(); + if (noRounding || period === "5minute") { + return suggestedMax; } suggestedMax.setMinutes(0, 0, 0); if (period === "hour") { - return suggestedMax.getTime(); + return suggestedMax; } // Sometimes around DST we get a time of 0:59 instead of 23:59 as expected. // Correct for this when showing days/months so we don't get an extra day. @@ -50,11 +64,11 @@ export function getSuggestedMax(period: StatisticPeriod, end: Date): number { } suggestedMax.setHours(0); if (period === "day" || period === "week") { - return suggestedMax.getTime(); + return suggestedMax; } // period === month suggestedMax.setDate(1); - return suggestedMax.getTime(); + return suggestedMax; } function createYAxisLabelFormatter(locale: FrontendLocaleData) { @@ -81,21 +95,45 @@ export function getCommonOptions( formatTotal?: (total: number) => string, detailedDailyData = false ): ECOption { - const dayDifference = differenceInDays(end, start); + const suggestedPeriod = getSuggestedPeriod(start, end, detailedDailyData); + const suggestedMax = getSuggestedMax(suggestedPeriod, end, detailedDailyData); const compare = compareStart !== undefined && compareEnd !== undefined; const showCompareYear = compare && start.getFullYear() !== compareStart.getFullYear(); - const options: ECOption = { + const monthTimeAxis: ECOption = { + xAxis: { + type: "time", + min: subDays(start, MONTH_TIME_AXIS_PADDING), + max: addDays(suggestedMax, MONTH_TIME_AXIS_PADDING), + axisLabel: { + formatter: { + year: "{yearStyle|{MMMM} {yyyy}}", + month: "{MMMM}", + }, + rich: { + yearStyle: { + fontWeight: "bold", + }, + }, + }, + // For shorter month ranges, force splitting to ensure time axis renders + // as whole month intervals. Limit the number of forced ticks to 6 months + // (so a max calendar difference of 5) to reduce clutter. + splitNumber: Math.min(differenceInCalendarMonths(end, start), 5), + }, + }; + const normalTimeAxis: ECOption = { xAxis: { type: "time", min: start, - max: getSuggestedMax( - getSuggestedPeriod(start, end, detailedDailyData), - end - ), + max: suggestedMax, }, + }; + + const options: ECOption = { + ...(suggestedPeriod === "month" ? monthTimeAxis : normalTimeAxis), yAxis: { type: "value", name: unit, @@ -137,7 +175,7 @@ export function getCommonOptions( items, locale, config, - dayDifference, + suggestedPeriod, compare, showCompareYear, unit, @@ -151,7 +189,7 @@ export function getCommonOptions( [params], locale, config, - dayDifference, + suggestedPeriod, compare, showCompareYear, unit, @@ -167,7 +205,7 @@ function formatTooltip( params: CallbackDataParams[], locale: FrontendLocaleData, config: HassConfig, - dayDifference: number, + suggestedPeriod: string, compare: boolean | null, showCompareYear: boolean, unit?: string, @@ -181,9 +219,9 @@ function formatTooltip( const date = new Date(params[0].value?.[2] ?? params[0].value?.[0]); let period: string; - if (dayDifference >= 89) { + if (suggestedPeriod === "month") { period = `${formatDateMonthYear(date, locale, config)}`; - } else if (dayDifference > 0) { + } else if (suggestedPeriod === "day") { period = `${(showCompareYear ? formatDateShort : formatDateVeryShort)(date, locale, config)}`; } else { period = `${ diff --git a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts index ca1cb0b1cb..ff2cb7d67d 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts @@ -65,6 +65,17 @@ class HuiEnergyDistrubutionCard ]; } + private get _energyDashboardHref(): string { + const params = new URLSearchParams({ + historyBack: "1", + }); + const backPath = window.location.pathname; + if (backPath) { + params.append("backPath", backPath); + } + return `/energy?${params.toString()}`; + } + public getCardSize(): Promise | number { return 3; } @@ -789,7 +800,11 @@ class HuiEnergyDistrubutionCard ${this._config.link_dashboard ? html`
    - + ${this.hass.localize( "ui.panel.lovelace.cards.energy.energy_distribution.go_to_energy_dashboard" )} diff --git a/src/panels/lovelace/cards/energy/hui-power-sankey-card.ts b/src/panels/lovelace/cards/energy/hui-power-sankey-card.ts index c65beaa3d1..6ad3ef565b 100644 --- a/src/panels/lovelace/cards/energy/hui-power-sankey-card.ts +++ b/src/panels/lovelace/cards/energy/hui-power-sankey-card.ts @@ -27,8 +27,9 @@ const DEFAULT_CONFIG: Partial = { group_by_area: true, }; -// Minimum power threshold in watts (W) to display a device node -const MIN_POWER_THRESHOLD = 10; +// Minimum power threshold as a fraction of total consumption to display a device node +// Devices below this threshold will be grouped into an "Other" node +const MIN_POWER_THRESHOLD_FACTOR = 0.001; // 0.1% of used_total interface PowerData { solar: number; @@ -46,6 +47,14 @@ interface PowerData { used_total: number; } +interface SmallConsumer { + statRate: string; + name: string | undefined; + value: number; + effectiveParent: string | undefined; + idx: number; +} + @customElement("hui-power-sankey-card") class HuiPowerSankeyCard extends SubscribeMixin(MobileAwareMixin(LitElement)) @@ -132,6 +141,9 @@ class HuiPowerSankeyCard const powerData = this._computePowerData(prefs); const computedStyle = getComputedStyle(this); + // Calculate dynamic threshold based on total consumption + const minPowerThreshold = powerData.used_total * MIN_POWER_THRESHOLD_FACTOR; + const nodes: Node[] = []; const links: Link[] = []; @@ -277,7 +289,7 @@ class HuiPowerSankeyCard prefs.device_consumption.forEach((device) => { if (device.stat_rate) { const value = this._getCurrentPower(device.stat_rate); - if (value >= MIN_POWER_THRESHOLD) { + if (value >= minPowerThreshold) { renderedStatRates.add(device.stat_rate); } } @@ -307,19 +319,34 @@ class HuiPowerSankeyCard return undefined; }; + // Collect small consumers by their effective parent + const smallConsumersByParent = new Map(); + prefs.device_consumption.forEach((device, idx) => { if (!device.stat_rate) { return; } const value = this._getCurrentPower(device.stat_rate); - if (value < MIN_POWER_THRESHOLD) { - return; - } - // Find the effective parent (may be different from direct parent if parent has no stat_rate) const effectiveParent = findEffectiveParent(device.included_in_stat); + if (value < minPowerThreshold) { + // Collect small consumers instead of skipping them + const parentKey = effectiveParent ?? "home"; + if (!smallConsumersByParent.has(parentKey)) { + smallConsumersByParent.set(parentKey, []); + } + smallConsumersByParent.get(parentKey)!.push({ + statRate: device.stat_rate, + name: device.name, + value, + effectiveParent, + idx, + }); + return; + } + const node = { id: device.stat_rate, label: device.name || this._getEntityLabel(device.stat_rate), @@ -340,6 +367,65 @@ class HuiPowerSankeyCard } deviceNodes.push(node); }); + + // Process small consumers - create "Other" nodes or show single entities + smallConsumersByParent.forEach((consumers, parentKey) => { + const totalValue = consumers.reduce((sum, c) => sum + c.value, 0); + if (totalValue <= 0) { + return; + } + + if (consumers.length === 1) { + // Single entity - show it directly instead of grouping + const consumer = consumers[0]; + const node = { + id: consumer.statRate, + label: consumer.name || this._getEntityLabel(consumer.statRate), + value: consumer.value, + color: getGraphColorByIndex(consumer.idx, computedStyle), + index: 4, + parent: consumer.effectiveParent, + entityId: consumer.statRate, + }; + if (node.parent) { + parentLinks[node.id] = node.parent; + links.push({ + source: node.parent, + target: node.id, + }); + } else { + untrackedConsumption -= consumer.value; + } + deviceNodes.push(node); + } else { + // Multiple entities - create "Other" group + const otherNodeId = `other_${parentKey}`; + const otherNode: Node = { + id: otherNodeId, + label: this.hass.localize( + "ui.panel.lovelace.cards.energy.energy_devices_detail_graph.other" + ), + value: Math.ceil(totalValue), + color: computedStyle + .getPropertyValue("--state-unavailable-color") + .trim(), + index: 4, + }; + + if (parentKey !== "home") { + // Has a parent device + parentLinks[otherNodeId] = parentKey; + links.push({ + source: parentKey, + target: otherNodeId, + }); + } else { + // Top-level "Other" - will be linked to home/floor/area later + untrackedConsumption -= totalValue; + } + deviceNodes.push(otherNode); + } + }); const devicesWithoutParent = deviceNodes.filter( (node) => !parentLinks[node.id] ); @@ -422,8 +508,8 @@ class HuiPowerSankeyCard }); }); - // untracked consumption - if (untrackedConsumption > 0) { + // untracked consumption (only show if larger than 1W) + if (untrackedConsumption > 1) { nodes.push({ id: "untracked", label: this.hass.localize( diff --git a/src/panels/lovelace/cards/hui-area-card.ts b/src/panels/lovelace/cards/hui-area-card.ts index 60bd6cf40d..37dcab3d6d 100644 --- a/src/panels/lovelace/cards/hui-area-card.ts +++ b/src/panels/lovelace/cards/hui-area-card.ts @@ -9,15 +9,17 @@ import { type TemplateResult, } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; import { computeCssColor } from "../../../common/color/compute-color"; -import { BINARY_STATE_ON } from "../../../common/const"; +import { BINARY_STATE_ON, STRINGS_SEPARATOR_DOT } from "../../../common/const"; import { computeAreaName } from "../../../common/entity/compute_area_name"; import { generateEntityFilter } from "../../../common/entity/entity_filter"; -import { navigate } from "../../../common/navigate"; +import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; +import { actionHandler } from "../common/directives/action-handler-directive"; +import { handleAction } from "../common/handle-action"; +import { hasAction } from "../common/has-action"; import { formatNumber, isNumericState, @@ -30,21 +32,20 @@ import "../../../components/ha-control-button"; import "../../../components/ha-control-button-group"; import "../../../components/ha-domain-icon"; import "../../../components/ha-icon"; -import "../../../components/ha-ripple"; -import "../../../components/ha-svg-icon"; import "../../../components/tile/ha-tile-badge"; +import "../../../components/tile/ha-tile-container"; import "../../../components/tile/ha-tile-icon"; import "../../../components/tile/ha-tile-info"; import { isUnavailableState } from "../../../data/entity/entity"; import type { HomeAssistant } from "../../../types"; import "../card-features/hui-card-features"; import type { LovelaceCardFeatureContext } from "../card-features/types"; -import { actionHandler } from "../common/directives/action-handler-directive"; import type { LovelaceCard, LovelaceCardEditor, LovelaceGridOptions, } from "../types"; +import { tileCardStyle } from "./tile/tile-card-style"; import type { AreaCardConfig } from "./types"; export const DEFAULT_ASPECT_RATIO = "16:9"; @@ -112,10 +113,28 @@ export class HuiAreaCard extends LitElement implements LovelaceCard { const displayType = config.display_type || (config.show_camera ? "camera" : "picture"); const vertical = displayType === "compact" ? config.vertical : false; + + // Backwards compatibility: convert navigation_path to tap_action + let tapAction = config.tap_action; + if (config.navigation_path && !tapAction) { + tapAction = { + action: "navigate", + navigation_path: config.navigation_path, + }; + } + + // Set smart default for image_tap_action only for camera display type + let imageTapAction = config.image_tap_action; + if (displayType === "camera" && !imageTapAction) { + imageTapAction = { action: "more-info" }; + } + this._config = { ...config, vertical, display_type: displayType, + tap_action: tapAction || { action: "none" }, + image_tap_action: imageTapAction, }; this._featureContext = { @@ -184,12 +203,38 @@ export class HuiAreaCard extends LitElement implements LovelaceCard { } private get _hasCardAction() { - return this._config?.navigation_path; + return hasAction(this._config?.tap_action); } - private _handleAction() { - if (this._config?.navigation_path) { - navigate(this._config.navigation_path); + private get _hasImageAction() { + if (this._config?.display_type === "compact") { + return false; + } + // Image is interactive if it has its own action OR if card has an action + return ( + hasAction(this._config?.image_tap_action) || + hasAction(this._config?.tap_action) + ); + } + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + private _handleImageAction(ev: ActionHandlerEvent) { + if (hasAction(this._config?.image_tap_action)) { + const entity = + this._config?.display_type === "camera" + ? this._getCameraEntity(this.hass.entities, this._config.area!) + : undefined; + handleAction( + this, + this.hass!, + { entity, tap_action: this._config!.image_tap_action }, + ev.detail.action! + ); + } else { + handleAction(this, this.hass!, this._config!, ev.detail.action!); } } @@ -477,7 +522,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard { return `${formattedValue}${formattedUnit}`; }) .filter(Boolean) - .join(" · "); + .join(STRINGS_SEPARATOR_DOT); return sensorStates; } @@ -548,9 +593,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard { `; } - const contentClasses = { vertical: Boolean(this._config.vertical) }; - - const icon = area.icon; + const icon = area.icon || undefined; const name = this._config.name || computeAreaName(area); @@ -560,9 +603,6 @@ export class HuiAreaCard extends LitElement implements LovelaceCard { const featurePosition = this._featurePosition(this._config); const features = this._displayedFeatures(this._config); - const containerOrientationClass = - featurePosition === "inline" ? "horizontal" : ""; - const displayType = this._config.display_type || "picture"; const cameraEntityId = @@ -582,21 +622,19 @@ export class HuiAreaCard extends LitElement implements LovelaceCard { return html` -
    - -
    ${displayType === "compact" ? nothing : html`
    -
    +
    ${(displayType === "picture" || displayType === "camera") && (cameraEntityId || area.picture) ? html` @@ -628,30 +666,30 @@ export class HuiAreaCard extends LitElement implements LovelaceCard { ${this._renderAlertSensors()}
    `} -
    -
    - - ${displayType === "compact" - ? this._renderAlertSensorBadge() - : nothing} - ${icon - ? html`` - : html` - - `} - - -
    + + + ${displayType === "compact" + ? this._renderAlertSensorBadge() + : nothing} + + ${features.length > 0 ? html` ` : nothing} -
    + `; } - static styles = css` - :host { - --tile-color: var(--state-icon-color); - -webkit-tap-highlight-color: transparent; - } - ha-card:has(.background:focus-visible) { - --shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent); - --shadow-focus: 0 0 0 1px var(--tile-color); - border-color: var(--tile-color); - box-shadow: var(--shadow-default), var(--shadow-focus); - } - ha-card { - --ha-ripple-color: var(--tile-color); - --ha-ripple-hover-opacity: 0.04; - --ha-ripple-pressed-opacity: 0.12; - height: 100%; - transition: - box-shadow 180ms ease-in-out, - border-color 180ms ease-in-out; - display: flex; - flex-direction: column; - justify-content: space-between; - } - [role="button"] { - cursor: pointer; - pointer-events: auto; - } - [role="button"]:focus { - outline: none; - } - .background { - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg)); - margin: calc(-1 * var(--ha-card-border-width, 1px)); - overflow: hidden; - } - .header { - flex: 1; - overflow: hidden; - border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg)); - border-end-end-radius: 0; - border-end-start-radius: 0; - pointer-events: none; - } - .picture { - height: 100%; - width: 100%; - background-size: cover; - background-position: center; - position: relative; - } - .picture hui-image { - height: 100%; - } - .picture .icon-container { - height: 100%; - width: 100%; - display: flex; - align-items: center; - justify-content: center; - --mdc-icon-size: var(--ha-space-12); - color: var(--tile-color); - } - .picture .icon-container::before { - position: absolute; - content: ""; - width: 100%; - height: 100%; - background-color: var(--tile-color); - opacity: 0.12; - } - .container { - margin: calc(-1 * var(--ha-card-border-width, 1px)); - display: flex; - flex-direction: column; - flex: 1; - } - .header + .container { - height: auto; - flex: none; - } - .container.horizontal { - flex-direction: row; - } - - .content { - position: relative; - display: flex; - flex-direction: row; - align-items: center; - padding: 10px; - flex: 1; - min-width: 0; - box-sizing: border-box; - pointer-events: none; - gap: 10px; - } - - .vertical { - flex-direction: column; - text-align: center; - justify-content: center; - } - .vertical ha-tile-info { - width: 100%; - flex: none; - } - - ha-tile-icon { - --tile-icon-color: var(--tile-color); - position: relative; - padding: 6px; - margin: -6px; - } - ha-tile-badge { - position: absolute; - top: 3px; - right: 3px; - inset-inline-end: 3px; - inset-inline-start: initial; - } - ha-tile-info { - position: relative; - min-width: 0; - transition: background-color 180ms ease-in-out; - box-sizing: border-box; - } - hui-card-features { - --feature-color: var(--tile-color); - padding: 0 var(--ha-space-3) var(--ha-space-3) var(--ha-space-3); - } - .container.horizontal hui-card-features { - width: calc(50% - var(--column-gap, 0px) / 2 - var(--ha-space-3)); - flex: none; - --feature-height: var(--ha-space-9); - padding: 0 var(--ha-space-3); - padding-inline-start: 0; - } - .alert-badge { - --tile-badge-background-color: var(--orange-color); - } - .alerts { - position: absolute; - top: 0; - left: 0; - display: flex; - flex-direction: row; - gap: var(--ha-space-2); - padding: var(--ha-space-2); - pointer-events: none; - z-index: 1; - } - .alert { - background-color: var(--orange-color); - border-radius: var(--ha-border-radius-lg); - width: var(--ha-space-6); - height: var(--ha-space-6); - padding: 2px; - box-sizing: border-box; - --mdc-icon-size: var(--ha-space-4); - display: flex; - align-items: center; - justify-content: center; - color: white; - } - `; + static styles = [ + tileCardStyle, + css` + :host { + --tile-color: var(--state-icon-color); + } + ha-card { + display: flex; + flex-direction: column; + justify-content: space-between; + } + .header { + flex: 1; + overflow: hidden; + border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg)); + border-end-end-radius: 0; + border-end-start-radius: 0; + position: relative; + z-index: 1; + } + .picture { + height: 100%; + width: 100%; + background-size: cover; + background-position: center; + position: relative; + pointer-events: none; + } + .picture[role="button"] { + pointer-events: auto; + cursor: pointer; + } + .picture[role="button"]:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: -2px; + } + .picture hui-image { + height: 100%; + } + .picture .icon-container { + height: 100%; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + --mdc-icon-size: var(--ha-space-12); + color: var(--tile-color); + } + .picture .icon-container::before { + position: absolute; + content: ""; + width: 100%; + height: 100%; + background-color: var(--tile-color); + opacity: 0.12; + } + .header + ha-tile-container { + height: auto; + flex: none; + } + ha-tile-badge { + position: absolute; + top: 3px; + right: 3px; + inset-inline-end: 3px; + inset-inline-start: initial; + } + hui-card-features { + --feature-color: var(--tile-color); + } + .alert-badge { + --tile-badge-background-color: var(--orange-color); + } + .alerts { + position: absolute; + top: 0; + left: 0; + display: flex; + flex-direction: row; + gap: var(--ha-space-2); + padding: var(--ha-space-2); + pointer-events: none; + z-index: 1; + } + .alert { + background-color: var(--orange-color); + border-radius: var(--ha-border-radius-lg); + width: var(--ha-space-6); + height: var(--ha-space-6); + padding: 2px; + box-sizing: border-box; + --mdc-icon-size: var(--ha-space-4); + display: flex; + align-items: center; + justify-content: center; + color: white; + } + `, + ]; } declare global { diff --git a/src/panels/lovelace/cards/hui-calendar-card.ts b/src/panels/lovelace/cards/hui-calendar-card.ts index e6ad8abdf1..45d1d3926c 100644 --- a/src/panels/lovelace/cards/hui-calendar-card.ts +++ b/src/panels/lovelace/cards/hui-calendar-card.ts @@ -1,14 +1,23 @@ +import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { classMap } from "lit/directives/class-map"; import { customElement, property, state } from "lit/decorators"; +import { + computeCssColor, + isValidColorString, +} from "../../../common/color/compute-color"; import { getColorByIndex } from "../../../common/color/colors"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import type { HASSDomEvent } from "../../../common/dom/fire_event"; import { debounce } from "../../../common/util/debounce"; import "../../../components/ha-card"; +import "../../../components/ha-spinner"; import type { Calendar, CalendarEvent } from "../../../data/calendar"; import { fetchCalendarEvents } from "../../../data/calendar"; +import type { EntityRegistryEntry } from "../../../data/entity/entity_registry"; +import { subscribeEntityRegistry } from "../../../data/entity/entity_registry"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import type { CalendarViewChanged, FullCalendarView, @@ -25,7 +34,10 @@ import type { import type { CalendarCardConfig } from "./types"; @customElement("hui-calendar-card") -export class HuiCalendarCard extends LitElement implements LovelaceCard { +export class HuiCalendarCard + extends SubscribeMixin(LitElement) + implements LovelaceCard +{ public static async getConfigElement(): Promise { await import("../editor/config-elements/hui-calendar-card-editor"); return document.createElement("hui-calendar-card-editor"); @@ -65,6 +77,10 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard { @state() private _error?: string = undefined; + @state() private _entityRegistry?: EntityRegistryEntry[]; + + @state() private _eventsLoaded = false; + private _startDate?: Date; private _endDate?: Date; @@ -89,16 +105,45 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard { public willUpdate(changedProps: PropertyValues): void { super.willUpdate(changedProps); + + // Don't build calendars until entity registry is loaded + if (!this._entityRegistry) { + return; + } + + // Reset loading state when config changes or entity registry updates + if (changedProps.has("_config") || changedProps.has("_entityRegistry")) { + this._eventsLoaded = false; + } + if ( !this.hasUpdated || - (changedProps.has("_config") && this._config?.entities) + (changedProps.has("_config") && this._config?.entities) || + changedProps.has("_entityRegistry") ) { const computedStyles = getComputedStyle(this); + const entityOptionsMap = new Map( + this._entityRegistry?.map((entry) => [ + entry.entity_id, + entry.options, + ]) ?? [] + ); if (this._config?.entities) { - this._calendars = this._config.entities.map((entity, idx) => ({ - entity_id: entity, - backgroundColor: getColorByIndex(idx, computedStyles), - })); + this._calendars = this._config.entities.map((entity, idx) => { + const entityColor = entityOptionsMap.get(entity)?.calendar?.color; + let backgroundColor: string; + // Validate and use the color from entity registry if valid + if (entityColor && isValidColorString(entityColor)) { + backgroundColor = computeCssColor(entityColor); + } else { + // Fall back to default color by index + backgroundColor = getColorByIndex(idx, computedStyles); + } + return { + entity_id: entity, + backgroundColor, + }; + }); } } } @@ -116,6 +161,14 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard { }; } + public hassSubscribe(): UnsubscribeFunc[] { + return [ + subscribeEntityRegistry(this.hass!.connection!, (entities) => { + this._entityRegistry = entities; + }), + ]; + } + public connectedCallback(): void { super.connectedCallback(); this.updateComplete.then(() => this._attachObserver()); @@ -129,10 +182,12 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard { } protected render() { - if (!this._config || !this.hass || !this._calendars.length) { + if (!this._config || !this.hass) { return nothing; } + const loading = !this._entityRegistry || !this._eventsLoaded; + const views: FullCalendarView[] = [ "dayGridMonth", "dayGridDay", @@ -141,31 +196,55 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard { return html` -
    ${this._config.title}
    + ${this._config.title + ? html`
    ${this._config.title}
    ` + : nothing} + ${loading + ? html`
    + +
    ` + : nothing}
    `; } protected updated(changedProps: PropertyValues) { super.updated(changedProps); + if (!this._config || !this.hass) { return; } + // Refetch events when entity registry changes (to update colors) + if (changedProps.has("_entityRegistry") && this._entityRegistry) { + this._fetchCalendarEvents(); + } + + // If no calendars configured, mark events as loaded to hide spinner + if ( + this._entityRegistry && + !this._calendars.length && + !this._eventsLoaded + ) { + this._eventsLoaded = true; + } + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; const oldConfig = changedProps.get("_config") as | CalendarCardConfig @@ -184,6 +263,7 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard { private _handleViewChanged(ev: HASSDomEvent): void { this._startDate = ev.detail.start; this._endDate = ev.detail.end; + this._eventsLoaded = false; this._fetchCalendarEvents(); } @@ -200,6 +280,12 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard { this._calendars ); this._events = result.events; + // Wait for component update and one animation frame for FullCalendar to render + this.updateComplete.then(() => { + requestAnimationFrame(() => { + this._eventsLoaded = true; + }); + }); if (result.errors.length > 0) { this._error = `${this.hass!.localize( @@ -253,7 +339,14 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard { ha-full-calendar { --calendar-height: 400px; + display: block; + width: 100%; height: var(--calendar-height); + min-height: var(--calendar-height); + } + + ha-full-calendar.loading { + visibility: hidden; } ha-full-calendar.is-grid, @@ -267,6 +360,16 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard { 100% - var(--ha-card-header-font-size, var(--ha-font-size-2xl)) - 22px ); } + + .loading { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: var(--card-background-color, var(--ha-card-background)); + z-index: 1; + } `; } diff --git a/src/panels/lovelace/cards/hui-discovered-devices-card.ts b/src/panels/lovelace/cards/hui-discovered-devices-card.ts new file mode 100644 index 0000000000..014b89ece3 --- /dev/null +++ b/src/panels/lovelace/cards/hui-discovered-devices-card.ts @@ -0,0 +1,194 @@ +import { mdiDevices } from "@mdi/js"; +import type { UnsubscribeFunc } from "home-assistant-js-websocket"; +import type { PropertyValues, TemplateResult } from "lit"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/ha-card"; +import "../../../components/tile/ha-tile-container"; +import "../../../components/tile/ha-tile-icon"; +import "../../../components/tile/ha-tile-info"; +import { + DISCOVERY_SOURCES, + subscribeConfigFlowInProgress, + type ConfigFlowInProgressMessage, +} from "../../../data/config_flow"; +import type { DataEntryFlowProgress } from "../../../data/data_entry_flow"; +import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; +import { showAddIntegrationDialog } from "../../config/integrations/show-add-integration-dialog"; +import type { HomeAssistant } from "../../../types"; +import { handleAction } from "../common/handle-action"; +import { hasAction } from "../common/has-action"; +import type { LovelaceCard, LovelaceGridOptions } from "../types"; +import { tileCardStyle } from "./tile/tile-card-style"; +import type { DiscoveredDevicesCardConfig } from "./types"; + +@customElement("hui-discovered-devices-card") +export class HuiDiscoveredDevicesCard + extends SubscribeMixin(LitElement) + implements LovelaceCard +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: DiscoveredDevicesCardConfig; + + @state() private _discoveredFlows: DataEntryFlowProgress[] = []; + + public hassSubscribe(): (UnsubscribeFunc | Promise)[] { + return [ + subscribeConfigFlowInProgress( + this.hass!, + (messages: ConfigFlowInProgressMessage[]) => { + if (messages.length === 0) { + this._discoveredFlows = []; + return; + } + + let fullUpdate = false; + const newFlows: DataEntryFlowProgress[] = []; + + messages.forEach((message) => { + if (message.type === "removed") { + this._discoveredFlows = this._discoveredFlows.filter( + (flow) => flow.flow_id !== message.flow_id + ); + return; + } + + if (message.type === null || message.type === "added") { + if (message.type === null) { + fullUpdate = true; + } + // Only include flows from discovery sources + if (DISCOVERY_SOURCES.includes(message.flow.context.source)) { + newFlows.push(message.flow); + } + } + }); + + if (!newFlows.length && !fullUpdate) { + return; + } + + const existingFlows = fullUpdate ? [] : this._discoveredFlows; + this._discoveredFlows = [...existingFlows, ...newFlows]; + } + ), + ]; + } + + public setConfig(config: DiscoveredDevicesCardConfig): void { + this._config = config; + } + + public getCardSize(): number { + return this._config?.vertical ? 2 : 1; + } + + public getGridOptions(): LovelaceGridOptions { + const columns = 6; + let min_columns = 6; + let rows = 1; + + if (this._config?.vertical) { + rows++; + min_columns = 3; + } + return { + columns, + rows, + min_columns, + min_rows: rows, + }; + } + + private async _handleAction(ev: ActionHandlerEvent) { + if (ev.detail.action === "tap" && !hasAction(this._config?.tap_action)) { + await this.hass!.loadFragmentTranslation("config"); + showAddIntegrationDialog(this, { brand: "_discovered" }); + return; + } + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + private get _hasCardAction() { + return ( + !this._config?.tap_action || + hasAction(this._config?.tap_action) || + hasAction(this._config?.hold_action) || + hasAction(this._config?.double_tap_action) + ); + } + + protected willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + + if (!this._config || !this.hass) { + return; + } + + // Update visibility based on admin status and discovered devices count + const shouldBeHidden = + !this.hass.user?.is_admin || + (this._config.hide_empty && this._discoveredFlows.length === 0); + + if (shouldBeHidden !== this.hidden) { + this.style.display = shouldBeHidden ? "none" : ""; + this.toggleAttribute("hidden", shouldBeHidden); + fireEvent(this, "card-visibility-changed", { value: !shouldBeHidden }); + } + } + + protected render(): TemplateResult | typeof nothing { + if (!this._config || !this.hass || this.hidden) { + return nothing; + } + + const count = this._discoveredFlows.length; + + const label = this.hass.localize("ui.card.discovered-devices.title"); + const secondary = + count > 0 + ? this.hass.localize("ui.card.discovered-devices.count_devices", { + count, + }) + : this.hass.localize("ui.card.discovered-devices.no_devices"); + + return html` + + + + + + + `; + } + + static styles = [ + tileCardStyle, + css` + :host { + --tile-color: var(--primary-color); + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-discovered-devices-card": HuiDiscoveredDevicesCard; + } +} diff --git a/src/panels/lovelace/cards/hui-distribution-card.ts b/src/panels/lovelace/cards/hui-distribution-card.ts new file mode 100644 index 0000000000..5b2e43e520 --- /dev/null +++ b/src/panels/lovelace/cards/hui-distribution-card.ts @@ -0,0 +1,609 @@ +import { mdiChartBox, mdiChevronDown, mdiChevronUp } from "@mdi/js"; +import type { HassEntity } from "home-assistant-js-websocket"; +import { css, html, LitElement, nothing, type TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { styleMap } from "lit/directives/style-map"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { getGraphColorByIndex } from "../../../common/color/colors"; +import { computeCssColor } from "../../../common/color/compute-color"; +import { computeDomain } from "../../../common/entity/compute_domain"; +import { normalizeValueBySIPrefix } from "../../../common/number/normalize-by-si-prefix"; +import { MobileAwareMixin } from "../../../mixins/mobile-aware-mixin"; +import type { EntityNameItem } from "../../../common/entity/compute_entity_name_display"; +import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; +import "../../../components/chips/ha-assist-chip"; +import "../../../components/ha-card"; +import "../../../components/ha-segmented-bar"; +import type { Segment } from "../../../components/ha-segmented-bar"; +import "../../../components/ha-svg-icon"; +import type { HomeAssistant } from "../../../types"; +import { createEntityNotFoundWarning } from "../components/hui-warning"; +import { processConfigEntities } from "../common/process-config-entities"; +import { findEntities } from "../common/find-entities"; +import type { + LovelaceCard, + LovelaceCardEditor, + LovelaceGridOptions, +} from "../types"; +import type { DistributionCardConfig, DistributionEntityConfig } from "./types"; + +const LEGEND_OVERFLOW_LIMIT = 10; +const LEGEND_OVERFLOW_LIMIT_MOBILE = 6; + +interface ProcessedEntity { + entity: string; + name?: string | EntityNameItem | EntityNameItem[]; + color?: string; +} + +interface LegendItem { + entity: string; + name: string; + value: number; + formattedValue: string; + color: string; + isHidden: boolean; + isDisabled: boolean; +} + +@customElement("hui-distribution-card") +export class HuiDistributionCard + extends MobileAwareMixin(LitElement) + implements LovelaceCard +{ + public static async getConfigElement(): Promise { + await import("../editor/config-elements/hui-distribution-card-editor"); + return document.createElement("hui-distribution-card-editor"); + } + + public static getStubConfig( + hass: HomeAssistant, + entities: string[], + entitiesFallback: string[] + ): DistributionCardConfig { + const includeDomains = ["sensor"]; + const maxEntities = 3; + + // Strategy 1: Try to find power sensors (W, kW) - most common use case + const powerFilter = (stateObj: HassEntity): boolean => { + const unit = stateObj.attributes.unit_of_measurement; + const stateValue = Number(stateObj.state); + return (unit === "W" || unit === "kW") && !isNaN(stateValue); + }; + + let foundEntities = findEntities( + hass, + maxEntities, + entities, + entitiesFallback, + includeDomains, + powerFilter + ); + + // Strategy 2: If not enough power entities, find largest group with matching device_class + if (foundEntities.length < 2) { + // Get all numeric sensors + const allNumericSensors = entitiesFallback.filter((entityId) => { + if (!entityId.startsWith("sensor.")) return false; + const stateObj = hass.states[entityId]; + if (!stateObj) return false; + const stateValue = Number(stateObj.state); + return !isNaN(stateValue); + }); + + // Group by device_class + const deviceClassGroups = new Map(); + allNumericSensors.forEach((entityId) => { + const deviceClass = + hass.states[entityId].attributes.device_class || "none"; + if (!deviceClassGroups.has(deviceClass)) { + deviceClassGroups.set(deviceClass, []); + } + deviceClassGroups.get(deviceClass)!.push(entityId); + }); + + // Find largest group with at least 2 entities + let largestGroup: string[] = []; + deviceClassGroups.forEach((group) => { + if (group.length > largestGroup.length) { + largestGroup = group; + } + }); + + // Take first maxEntities from largest group + if (largestGroup.length >= 2) { + foundEntities = largestGroup.slice(0, maxEntities); + } + } + + return { + type: "distribution", + entities: foundEntities, + }; + } + + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: DistributionCardConfig; + + @state() private _configEntities?: ProcessedEntity[]; + + @state() private _hiddenEntities = new Set(); + + @state() private _expandLegend = false; + + public setConfig(config: DistributionCardConfig): void { + this._config = config; + + // Handle empty entities gracefully + if (!config.entities || config.entities.length === 0) { + this._configEntities = []; + return; + } + + const entities = processConfigEntities( + config.entities + ); + + this._configEntities = entities.map((entity) => ({ + entity: entity.entity, + name: entity.name, + color: entity.color, + })); + } + + public getCardSize(): number { + return 3; + } + + public getGridOptions(): LovelaceGridOptions { + return { + columns: 12, + rows: "auto", + min_columns: 3, + fixed_rows: true, + }; + } + + private _validateDeviceClasses = memoizeOne( + (entities: ProcessedEntity[], hass: HomeAssistant): string | null => { + const domains = new Set(); + const deviceClasses = new Set(); + + entities.forEach((entity) => { + const stateObj = hass.states[entity.entity]; + if (stateObj) { + // Check domain + const domain = computeDomain(entity.entity); + domains.add(domain); + + // Default to "none" if no device_class (Home Assistant pattern) + const deviceClass = stateObj.attributes.device_class || "none"; + deviceClasses.add(deviceClass); + } + }); + + // If more than one domain, entities are incompatible + if (domains.size > 1) { + return hass.localize( + "ui.panel.lovelace.cards.distribution.domain_mismatch", + { domains: Array.from(domains).join(", ") } + ); + } + + // If more than one device_class, entities are incompatible + if (deviceClasses.size > 1) { + return hass.localize( + "ui.panel.lovelace.cards.distribution.device_class_mismatch", + { classes: Array.from(deviceClasses).join(", ") } + ); + } + + return null; + } + ); + + private _convertToSegments(): { + segments: Segment[]; + hiddenIndices: number[]; + } { + const computedStyles = getComputedStyle(this); + const segments: Segment[] = []; + const hiddenIndices: number[] = []; + + // Access data from instance properties instead of parameters + if (!this._configEntities || !this.hass) { + return { segments, hiddenIndices }; + } + + // Map entities with their original index + const entitiesWithIndex = this._configEntities.map( + (entity, originalIndex) => ({ + ...entity, + originalIndex, + }) + ); + + // Create segments for ALL entities (including hidden ones with positive values) + entitiesWithIndex.forEach((entity) => { + const stateObj = this.hass!.states[entity.entity]; + if (!stateObj) return; + + const rawValue = Number(stateObj.state); + if (rawValue <= 0 || isNaN(rawValue)) return; + const value = normalizeValueBySIPrefix( + rawValue, + stateObj.attributes.unit_of_measurement + ); + + const color = entity.color + ? computeCssColor(entity.color) + : getGraphColorByIndex(entity.originalIndex, computedStyles); + const name = computeLovelaceEntityName(this.hass!, stateObj, entity.name); + const formattedValue = this.hass!.formatEntityState(stateObj); + + segments.push({ + value: value, + color: color, + label: html`${name} + ${formattedValue}`, + entityId: entity.entity, + }); + + // Track hidden indices + if (this._hiddenEntities.has(entity.entity)) { + hiddenIndices.push(segments.length - 1); + } + }); + + return { segments, hiddenIndices }; + } + + private _computeLegendItems(): LegendItem[] { + if (!this._configEntities || !this.hass) { + return []; + } + + const computedStyles = getComputedStyle(this); + + return this._configEntities.map((entity, index) => { + const stateObj = this.hass!.states[entity.entity]; + const value = stateObj ? Number(stateObj.state) : 0; + const isHidden = this._hiddenEntities.has(entity.entity); + const isZeroOrNegative = !stateObj || value <= 0 || isNaN(value); + + const name = stateObj + ? computeLovelaceEntityName(this.hass!, stateObj, entity.name) + : entity.entity; + + const formattedValue = stateObj + ? this.hass!.formatEntityState(stateObj) + : ""; + + return { + entity: entity.entity, + name: name, + value: value, + formattedValue: formattedValue, + color: entity.color + ? computeCssColor(entity.color) + : getGraphColorByIndex(index, computedStyles), + isHidden: isHidden, + isDisabled: isZeroOrNegative, + }; + }); + } + + private _toggleEntity(entityId: string): void { + const newHidden = new Set(this._hiddenEntities); + if (newHidden.has(entityId)) { + // Show: remove from hidden list + newHidden.delete(entityId); + } else { + // Hide: add to hidden list + newHidden.add(entityId); + } + this._hiddenEntities = newHidden; + } + + private _handleSegmentClick(ev: CustomEvent): void { + const { segment } = ev.detail; + if (segment.entityId) { + fireEvent(this, "hass-more-info", { entityId: segment.entityId }); + } + } + + private _handleLegendClick(ev: Event): void { + const target = ev.currentTarget as HTMLElement; + const entityId = target.dataset.entity; + const disabled = target.dataset.disabled === "true"; + if (entityId && !disabled) { + this._toggleEntity(entityId); + } + } + + private _handleLegendKeydown(ev: KeyboardEvent): void { + if (ev.key === "Enter" || ev.key === " ") { + ev.preventDefault(); + this._handleLegendClick(ev); + } + } + + private _toggleExpandLegend(): void { + this._expandLegend = !this._expandLegend; + } + + private _renderLegend(): TemplateResult { + const legendItems = this._computeLegendItems(); + const overflowLimit = this._isMobileSize + ? LEGEND_OVERFLOW_LIMIT_MOBILE + : LEGEND_OVERFLOW_LIMIT; + + return html` +
      + ${legendItems.map((item, index) => { + if (!this._expandLegend && index >= overflowLimit) { + return nothing; + } + return html` +
    • +
      + ${item.name} + ${item.formattedValue + ? html`${item.formattedValue}` + : nothing} +
    • + `; + })} + ${legendItems.length > overflowLimit + ? html` +
    • + + + +
    • + ` + : nothing} +
    + `; + } + + protected render(): TemplateResult | typeof nothing { + if (!this._config || !this.hass) { + return nothing; + } + + // Show friendly empty state when no entities are configured + if (!this._configEntities || this._configEntities.length === 0) { + return html` + +
    +
    + +

    + ${this.hass.localize( + "ui.panel.lovelace.cards.distribution.add_entities" + )} +

    +
    +
    +
    + `; + } + + // Check for missing entities + const missingEntities = this._configEntities.filter( + (entity) => !this.hass!.states[entity.entity] + ); + + if (missingEntities.length === this._configEntities.length) { + return html` + + ${missingEntities.map((entity) => + createEntityNotFoundWarning(this.hass!, entity.entity) + )} + + `; + } + + // Validate device classes + const deviceClassError = this._validateDeviceClasses( + this._configEntities, + this.hass + ); + if (deviceClassError) { + return html` + + ${deviceClassError} + + `; + } + + const segmentData = this._convertToSegments(); + + return html` + +
    + ${segmentData.segments.length === 0 + ? html` +
    + ${this.hass.localize( + "ui.panel.lovelace.cards.distribution.no_data" + )} +
    + ` + : html` + + `} + + + ${this._renderLegend()} +
    +
    + `; + } + + static styles = css` + :host { + display: block; + } + + ha-alert { + display: block; + word-wrap: break-word; + overflow-wrap: break-word; + } + + .card-content { + padding: var(--ha-space-4); + } + + .legend { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: var(--ha-space-3); + margin: var(--ha-space-3) 0 0 0; + padding: 0; + list-style: none; + } + + .legend-item { + display: flex; + align-items: center; + gap: var(--ha-space-1); + font-size: var(--ha-font-size-s); + cursor: pointer; + opacity: 1; + transition: opacity 0.2s; + } + + .legend-item:hover { + opacity: 0.8; + } + + .legend-item:focus { + outline: 2px solid var(--primary-color); + outline-offset: 2px; + opacity: 0.8; + } + + .legend-item.hidden { + opacity: 0.5; + } + + .legend-item.hidden .label { + text-decoration: line-through; + } + + .legend-item.disabled { + opacity: 0.5; + cursor: default; + } + + .legend-item .bullet { + width: 12px; + height: 12px; + border-radius: var(--ha-border-radius-circle); + flex-shrink: 0; + } + + .legend-item .label { + flex: 0 1 auto; + } + + .legend-item .value { + color: var(--secondary-text-color); + margin-left: var(--ha-space-1); + flex-shrink: 0; + } + + .empty-state { + text-align: center; + color: var(--secondary-text-color); + padding: var(--ha-space-4) 0; + } + + .empty-state ha-svg-icon { + display: block; + margin: 0 auto var(--ha-space-2); + width: 48px; + height: 48px; + opacity: 0.3; + } + + .empty-state p { + margin: 0; + } + + @media (max-width: 600px) { + .card-content { + padding: var(--ha-space-2); + } + } + + ha-assist-chip { + height: 24px; + --_label-text-weight: 500; + --_leading-space: 8px; + --_trailing-space: 8px; + --_icon-label-space: 4px; + } + + .legend li:has(ha-assist-chip) { + cursor: default; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-distribution-card": HuiDistributionCard; + } +} diff --git a/src/panels/lovelace/cards/hui-empty-state-card.ts b/src/panels/lovelace/cards/hui-empty-state-card.ts index b9a050690a..38a35ec48b 100644 --- a/src/panels/lovelace/cards/hui-empty-state-card.ts +++ b/src/panels/lovelace/cards/hui-empty-state-card.ts @@ -1,6 +1,9 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; +import { styleMap } from "lit/directives/style-map"; +import { ifDefined } from "lit/directives/if-defined"; +import { computeCssColor } from "../../../common/color/compute-color"; import "../../../components/ha-card"; import "../../../components/ha-button"; import "../../../components/ha-icon"; @@ -49,17 +52,44 @@ export class HuiEmptyStateCard extends LitElement implements LovelaceCard { >
    ${this._config.icon - ? html`` + ? html` + + ` : nothing} ${this._config.title ? html`

    ${this._config.title}

    ` : nothing} ${this._config.content ? html`

    ${this._config.content}

    ` : nothing} - ${this._config.tap_action && this._config.action_button_text + ${this._config.buttons?.length ? html` - - ${this._config.action_button_text} - +
    + ${this._config.buttons.map( + (button, index) => html` + + ${button.icon + ? html`` + : nothing} + ${button.text} + + ` + )} +
    ` : nothing}
    @@ -67,9 +97,11 @@ export class HuiEmptyStateCard extends LitElement implements LovelaceCard { `; } - private _handleAction(): void { - if (this._config?.tap_action && this.hass) { - handleAction(this, this.hass, this._config, "tap"); + private _handleButtonAction(ev: Event): void { + const index = (ev.currentTarget as any).index; + const button = this._config?.buttons?.[index]; + if (this.hass && button) { + handleAction(this, this.hass, { tap_action: button.tap_action }, "tap"); } } @@ -94,8 +126,8 @@ export class HuiEmptyStateCard extends LitElement implements LovelaceCard { max-width: 640px; margin: 0 auto; } - ha-icon { - --mdc-icon-size: var(--ha-space-12); + .card-icon { + --mdc-icon-size: var(--ha-space-16); color: var(--secondary-text-color); } h1 { @@ -107,6 +139,12 @@ export class HuiEmptyStateCard extends LitElement implements LovelaceCard { margin: 0; color: var(--secondary-text-color); } + .buttons { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: var(--ha-space-2); + } .content-only { background: none; box-shadow: none; diff --git a/src/panels/lovelace/cards/hui-entity-card.ts b/src/panels/lovelace/cards/hui-entity-card.ts index d4b26bbecc..d08458068e 100644 --- a/src/panels/lovelace/cards/hui-entity-card.ts +++ b/src/panels/lovelace/cards/hui-entity-card.ts @@ -6,24 +6,23 @@ import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; import { styleMap } from "lit/directives/style-map"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; -import { fireEvent } from "../../../common/dom/fire_event"; +import { stopPropagation } from "../../../common/dom/stop_propagation"; import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { stateColorBrightness, stateColorCss, } from "../../../common/entity/state_color"; import { isValidEntityId } from "../../../common/entity/valid_entity_id"; -import { - formatNumber, - getNumberFormatOptions, - isNumericState, -} from "../../../common/number/format_number"; import { iconColorCSS } from "../../../common/style/icon_color_css"; import "../../../components/ha-attribute-value"; import "../../../components/ha-card"; import "../../../components/ha-icon"; import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../../data/climate"; import { isUnavailableState } from "../../../data/entity/entity"; +import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; +import { actionHandler } from "../common/directives/action-handler-directive"; +import { handleAction } from "../common/handle-action"; +import { hasAction, hasAnyAction } from "../common/has-action"; import type { HomeAssistant } from "../../../types"; import { computeCardSize } from "../common/compute-card-size"; import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; @@ -121,9 +120,27 @@ export class HuiEntityCard extends LitElement implements LovelaceCard { } const domain = computeStateDomain(stateObj); - const showUnit = this._config.attribute - ? this._config.attribute in stateObj.attributes - : !isUnavailableState(stateObj.state); + const stateParts = this.hass.formatEntityStateToParts(stateObj); + + let unit; + if ( + !isUnavailableState(stateObj.state) && + (this._config.attribute || + stateObj.attributes.device_class !== "duration") + ) { + unit = this._config.unit; + if (!unit) { + if (!this._config.attribute) { + unit = stateParts.find((part) => part.type === "unit")?.value; + } else { + const parts = this.hass.formatEntityAttributeValueToParts( + stateObj, + this._config.attribute + ); + unit = parts.find((part) => part.type === "unit")?.value; + } + } + } const name = computeLovelaceEntityName( this.hass, @@ -138,9 +155,20 @@ export class HuiEntityCard extends LitElement implements LovelaceCard { return html`
    ${name}
    @@ -173,30 +201,20 @@ export class HuiEntityCard extends LitElement implements LovelaceCard { > ` : this.hass.localize("state.default.unknown") - : (isNumericState(stateObj) || this._config.unit) && - stateObj.attributes.device_class !== "duration" - ? formatNumber( - stateObj.state, - this.hass.locale, - getNumberFormatOptions( - stateObj, - this.hass.entities[this._config.entity] - ) - ) - : this.hass.formatEntityState(stateObj)}${showUnit - ? html` - ${this._config.unit || - (this._config.attribute || - stateObj.attributes.device_class === "duration" - ? "" - : stateObj.attributes.unit_of_measurement)} - ` - : ""} + : stateParts.find((part) => part.type === "value")?.value} + ${unit ? html`${unit}` : nothing} +
    + -
    `; } @@ -249,8 +267,8 @@ export class HuiEntityCard extends LitElement implements LovelaceCard { } } - private _handleClick(): void { - fireEvent(this, "hass-more-info", { entityId: this._config!.entity }); + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); } public getGridOptions(): LovelaceGridOptions { @@ -271,10 +289,16 @@ export class HuiEntityCard extends LitElement implements LovelaceCard { display: flex; flex-direction: column; justify-content: space-between; - cursor: pointer; outline: none; } + ha-card.action { + cursor: pointer; + } + .footer { + cursor: initial; + } + .header { display: flex; padding: 8px 16px 0; diff --git a/src/panels/lovelace/cards/hui-heading-card.ts b/src/panels/lovelace/cards/hui-heading-card.ts index f8553d4669..2cd9b0190f 100644 --- a/src/panels/lovelace/cards/hui-heading-card.ts +++ b/src/panels/lovelace/cards/hui-heading-card.ts @@ -153,7 +153,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard { flex-direction: row; justify-content: space-between; align-items: center; - overflow: hidden; + overflow: visible; gap: var(--ha-space-2); } .content:hover ha-icon-next { diff --git a/src/panels/lovelace/cards/hui-home-summary-card.ts b/src/panels/lovelace/cards/hui-home-summary-card.ts index ac07b42129..935d56ff99 100644 --- a/src/panels/lovelace/cards/hui-home-summary-card.ts +++ b/src/panels/lovelace/cards/hui-home-summary-card.ts @@ -2,8 +2,6 @@ import { endOfDay, startOfDay } from "date-fns"; import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { classMap } from "lit/directives/class-map"; -import { ifDefined } from "lit/directives/if-defined"; import { styleMap } from "lit/directives/style-map"; import { computeCssColor } from "../../../common/color/compute-color"; import { calcDate } from "../../../common/datetime/calc_date"; @@ -14,8 +12,7 @@ import { } from "../../../common/entity/entity_filter"; import { formatNumber } from "../../../common/number/format_number"; import "../../../components/ha-card"; -import "../../../components/ha-icon"; -import "../../../components/ha-ripple"; +import "../../../components/tile/ha-tile-container"; import "../../../components/tile/ha-tile-icon"; import "../../../components/tile/ha-tile-info"; import type { EnergyData } from "../../../data/energy"; @@ -28,7 +25,6 @@ import { import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../types"; -import { actionHandler } from "../common/directives/action-handler-directive"; import { handleAction } from "../common/handle-action"; import { hasAction } from "../common/has-action"; import { @@ -38,6 +34,7 @@ import { type HomeSummary, } from "../strategies/home/helpers/home-summaries"; import type { LovelaceCard, LovelaceGridOptions } from "../types"; +import { tileCardStyle } from "./tile/tile-card-style"; import type { HomeSummaryCard } from "./types"; const COLORS: Record = { @@ -269,8 +266,6 @@ export class HuiHomeSummaryCard return nothing; } - const contentClasses = { vertical: Boolean(this._config.vertical) }; - const color = computeCssColor(COLORS[this._config.summary]); const style = { @@ -284,125 +279,34 @@ export class HuiHomeSummaryCard return html` -
    - -
    -
    -
    - - - - -
    -
    + + +
    `; } - static styles = css` - :host { - --tile-color: var(--state-inactive-color); - -webkit-tap-highlight-color: transparent; - } - ha-card:has(.background:focus-visible) { - --shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent); - --shadow-focus: 0 0 0 1px var(--tile-color); - border-color: var(--tile-color); - box-shadow: var(--shadow-default), var(--shadow-focus); - } - ha-card { - --ha-ripple-color: var(--tile-color); - --ha-ripple-hover-opacity: 0.04; - --ha-ripple-pressed-opacity: 0.12; - height: 100%; - transition: - box-shadow 180ms ease-in-out, - border-color 180ms ease-in-out; - display: flex; - flex-direction: column; - justify-content: space-between; - } - ha-card.active { - --tile-color: var(--state-icon-color); - } - [role="button"] { - cursor: pointer; - pointer-events: auto; - } - [role="button"]:focus { - outline: none; - } - .background { - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg)); - margin: calc(-1 * var(--ha-card-border-width, 1px)); - overflow: hidden; - } - .container { - margin: calc(-1 * var(--ha-card-border-width, 1px)); - display: flex; - flex-direction: column; - flex: 1; - } - .container.horizontal { - flex-direction: row; - } - - .content { - position: relative; - display: flex; - flex-direction: row; - align-items: center; - padding: 10px; - flex: 1; - min-width: 0; - box-sizing: border-box; - pointer-events: none; - gap: 10px; - } - - .vertical { - flex-direction: column; - text-align: center; - justify-content: center; - } - .vertical ha-tile-info { - width: 100%; - flex: none; - } - - ha-tile-icon { - --tile-icon-color: var(--tile-color); - position: relative; - padding: 6px; - margin: -6px; - } - - ha-tile-info { - position: relative; - min-width: 0; - transition: background-color 180ms ease-in-out; - box-sizing: border-box; - } - `; + static styles = [ + tileCardStyle, + css` + :host { + --tile-color: var(--state-inactive-color); + } + `, + ]; } declare global { diff --git a/src/panels/lovelace/cards/hui-statistics-graph-card.ts b/src/panels/lovelace/cards/hui-statistics-graph-card.ts index 383f8bc238..6817d96d04 100644 --- a/src/panels/lovelace/cards/hui-statistics-graph-card.ts +++ b/src/panels/lovelace/cards/hui-statistics-graph-card.ts @@ -332,7 +332,11 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { .maxYAxis=${this._config.max_y_axis} .startTime=${this._energyStart} .endTime=${this._energyEnd && this._energyStart - ? getSuggestedMax(this._period!, this._energyEnd) + ? getSuggestedMax( + this._period!, + this._energyEnd, + (this._config.chart_type ?? "line") === "line" + ) : undefined} .fitYData=${this._config.fit_y_data || false} .hideLegend=${this._config.hide_legend || false} diff --git a/src/panels/lovelace/cards/hui-tile-card.ts b/src/panels/lovelace/cards/hui-tile-card.ts index 1487310ea6..71f2510831 100644 --- a/src/panels/lovelace/cards/hui-tile-card.ts +++ b/src/panels/lovelace/cards/hui-tile-card.ts @@ -12,10 +12,9 @@ import { computeDomain } from "../../../common/entity/compute_domain"; import { stateActive } from "../../../common/entity/state_active"; import { stateColorCss } from "../../../common/entity/state_color"; import "../../../components/ha-card"; -import "../../../components/ha-ripple"; import "../../../components/ha-state-icon"; -import "../../../components/ha-svg-icon"; import "../../../components/tile/ha-tile-badge"; +import "../../../components/tile/ha-tile-container"; import "../../../components/tile/ha-tile-icon"; import "../../../components/tile/ha-tile-info"; import { cameraUrlWithWidthHeight } from "../../../data/camera"; @@ -24,7 +23,6 @@ import "../../../state-display/state-display"; import type { HomeAssistant } from "../../../types"; import "../card-features/hui-card-features"; import type { LovelaceCardFeatureContext } from "../card-features/types"; -import { actionHandler } from "../common/directives/action-handler-directive"; import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; import { findEntities } from "../common/find-entities"; import { handleAction } from "../common/handle-action"; @@ -36,6 +34,7 @@ import type { LovelaceGridOptions, } from "../types"; import { renderTileBadge } from "./tile/badges/tile-badge"; +import { tileCardStyle } from "./tile/tile-card-style"; import type { TileCardConfig } from "./types"; export const getEntityDefaultTileIconAction = (entityId: string) => { @@ -253,8 +252,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard { `; } - const contentClasses = { vertical: Boolean(this._config.vertical) }; - const name = computeLovelaceEntityName( this.hass, stateObj, @@ -287,58 +284,55 @@ export class HuiTileCard extends LitElement implements LovelaceCard { const featurePosition = this._featurePosition(this._config); const features = this._displayedFeatures(this._config); - const containerOrientationClass = - featurePosition === "inline" ? "horizontal" : ""; + const hasImage = Boolean(imageUrl); return html` -
    - -
    -
    -
    - - - ${renderTileBadge(stateObj, this.hass)} - - - ${name} - ${stateDisplay - ? html`${stateDisplay}` - : nothing} - -
    + + ${hasImage + ? nothing + : html` + + `} + ${renderTileBadge(stateObj, this.hass)} + + + ${name} + ${stateDisplay + ? html`${stateDisplay}` + : nothing} + ${features.length > 0 ? html` ` : nothing} -
    +
    `; } - static styles = css` - :host { - --tile-color: var(--state-inactive-color); - -webkit-tap-highlight-color: transparent; - } - ha-card:has(.background:focus-visible) { - --shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent); - --shadow-focus: 0 0 0 1px var(--tile-color); - border-color: var(--tile-color); - box-shadow: var(--shadow-default), var(--shadow-focus); - } - ha-card { - --ha-ripple-color: var(--tile-color); - --ha-ripple-hover-opacity: 0.04; - --ha-ripple-pressed-opacity: 0.12; - height: 100%; - transition: - box-shadow 180ms ease-in-out, - border-color 180ms ease-in-out; - display: flex; - flex-direction: column; - justify-content: space-between; - } - ha-card.active { - --tile-color: var(--state-icon-color); - } - [role="button"] { - cursor: pointer; - pointer-events: auto; - } - [role="button"]:focus { - outline: none; - } - .background { - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg)); - margin: calc(-1 * var(--ha-card-border-width, 1px)); - overflow: hidden; - } - .container { - margin: calc(-1 * var(--ha-card-border-width, 1px)); - display: flex; - flex-direction: column; - flex: 1; - } - .container.horizontal { - flex-direction: row; - } - - .content { - position: relative; - display: flex; - flex-direction: row; - align-items: center; - padding: 10px; - flex: 1; - min-width: 0; - box-sizing: border-box; - pointer-events: none; - gap: 10px; - } - - .vertical { - flex-direction: column; - text-align: center; - justify-content: center; - } - .vertical ha-tile-info { - width: 100%; - flex: none; - } - ha-tile-icon { - --tile-icon-color: var(--tile-color); - position: relative; - padding: 6px; - margin: -6px; - } - ha-tile-badge { - position: absolute; - top: 3px; - right: 3px; - inset-inline-end: 3px; - inset-inline-start: initial; - } - ha-tile-info { - position: relative; - min-width: 0; - transition: background-color 180ms ease-in-out; - box-sizing: border-box; - } - hui-card-features { - --feature-color: var(--tile-color); - padding: 0 var(--ha-space-3) var(--ha-space-3) var(--ha-space-3); - } - .container.horizontal hui-card-features { - width: calc(50% - var(--column-gap, 0px) / 2 - var(--ha-space-3)); - flex: none; - --feature-height: var(--ha-space-9); - padding: 0 var(--ha-space-3); - padding-inline-start: 0; - } - - ha-tile-icon[data-domain="alarm_control_panel"][data-state="pending"], - ha-tile-icon[data-domain="alarm_control_panel"][data-state="arming"], - ha-tile-icon[data-domain="alarm_control_panel"][data-state="triggered"], - ha-tile-icon[data-domain="lock"][data-state="jammed"] { - animation: pulse 1s infinite; - } - - /* Make sure we display the whole image */ - ha-tile-icon.image[data-domain="update"] { - --tile-icon-border-radius: var(--ha-border-radius-square); - } - /* Make sure we display the almost the whole image but it often use text */ - ha-tile-icon.image[data-domain="media_player"] { - --tile-icon-border-radius: min( - var(--ha-tile-icon-border-radius, var(--ha-border-radius-sm)), - var(--ha-border-radius-sm) - ); - } - - @keyframes pulse { - 0% { - opacity: 1; + static styles = [ + tileCardStyle, + css` + :host { + --tile-color: var(--state-inactive-color); } - 50% { - opacity: 0; + ha-card.active { + --tile-color: var(--state-icon-color); } - 100% { - opacity: 1; + ha-tile-badge { + position: absolute; + top: 3px; + right: 3px; + inset-inline-end: 3px; + inset-inline-start: initial; } - } - `; + hui-card-features { + --feature-color: var(--tile-color); + } + + ha-tile-icon[data-domain="alarm_control_panel"][data-state="pending"], + ha-tile-icon[data-domain="alarm_control_panel"][data-state="arming"], + ha-tile-icon[data-domain="alarm_control_panel"][data-state="triggered"], + ha-tile-icon[data-domain="lock"][data-state="jammed"] { + animation: pulse 1s infinite; + } + + /* Make sure we display the whole image */ + ha-tile-icon.image[data-domain="update"] { + --tile-icon-border-radius: var(--ha-border-radius-square); + } + /* Make sure we display the almost the whole image but it often use text */ + ha-tile-icon.image[data-domain="media_player"] { + --tile-icon-border-radius: min( + var(--ha-tile-icon-border-radius, var(--ha-border-radius-sm)), + var(--ha-border-radius-sm) + ); + } + + @keyframes pulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + `, + ]; } declare global { diff --git a/src/panels/lovelace/cards/hui-todo-list-card.ts b/src/panels/lovelace/cards/hui-todo-list-card.ts index b6e5781e51..46fe981dc1 100644 --- a/src/panels/lovelace/cards/hui-todo-list-card.ts +++ b/src/panels/lovelace/cards/hui-todo-list-card.ts @@ -23,8 +23,8 @@ import "../../../components/ha-card"; import "../../../components/ha-check-list-item"; import "../../../components/ha-checkbox"; import "../../../components/ha-dropdown"; +import type { HaDropdownSelectEvent } from "../../../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-markdown-element"; @@ -394,7 +394,6 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard { "ui.panel.lovelace.cards.todo-list.clear_items" )} @@ -636,9 +635,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard { } } - private _handleCompletedMenuSelect( - ev: CustomEvent<{ item: HaDropdownItem }> - ) { + private _handleCompletedMenuSelect(ev: HaDropdownSelectEvent) { if (ev.detail?.item?.value === "clear") { this._clearCompletedItems(); } @@ -699,7 +696,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard { } } - private _handlePrimaryMenuSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { + private _handlePrimaryMenuSelect(ev: HaDropdownSelectEvent) { if (ev.detail?.item?.value === "reorder") { this._toggleReorder(); } diff --git a/src/panels/lovelace/cards/tile/tile-card-style.ts b/src/panels/lovelace/cards/tile/tile-card-style.ts new file mode 100644 index 0000000000..093a947f93 --- /dev/null +++ b/src/panels/lovelace/cards/tile/tile-card-style.ts @@ -0,0 +1,19 @@ +import { css } from "lit"; + +export const tileCardStyle = css` + ha-card:has(ha-tile-container[focused]) { + --shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent); + --shadow-focus: 0 0 0 1px var(--tile-color); + border-color: var(--tile-color); + box-shadow: var(--shadow-default), var(--shadow-focus); + } + ha-card { + height: 100%; + transition: + box-shadow 180ms ease-in-out, + border-color 180ms ease-in-out; + } + ha-tile-icon { + --tile-icon-color: var(--tile-color); + } +`; diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 6a3ad77c8a..d6d05476ca 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -57,13 +57,21 @@ export interface ConditionalCardConfig extends LovelaceCardConfig { conditions: (Condition | LegacyCondition)[]; } +export interface EmptyStateButtonConfig { + text: string; + icon?: string; + appearance?: "accent" | "filled" | "outlined" | "plain"; + variant?: "brand" | "neutral" | "success" | "warning" | "danger"; + tap_action: ActionConfig; +} + export interface EmptyStateCardConfig extends LovelaceCardConfig { content_only?: boolean; icon?: string; + icon_color?: string; title?: string; content?: string; - action_button_text?: string; - tap_action?: ActionConfig; + buttons?: EmptyStateButtonConfig[]; } export interface EntityCardConfig extends LovelaceCardConfig { @@ -74,6 +82,9 @@ export interface EntityCardConfig extends LovelaceCardConfig { unit?: string; theme?: string; state_color?: boolean; + tap_action?: ActionConfig; + hold_action?: ActionConfig; + double_tap_action?: ActionConfig; } export interface EntitiesCardEntityConfig extends EntityConfig { @@ -83,6 +94,7 @@ export interface EntitiesCardEntityConfig extends EntityConfig { | "last-changed" | "last-triggered" | "last-updated" + | "area" | "position" | "state" | "tilt-position" @@ -131,6 +143,9 @@ export interface AreaCardConfig extends LovelaceCardConfig { features?: LovelaceCardFeatureConfig[]; features_position?: LovelaceCardFeaturePosition; exclude_entities?: string[]; + vertical?: boolean; + tap_action?: ActionConfig; + image_tap_action?: ActionConfig; } export interface ButtonCardConfig extends LovelaceCardConfig { @@ -649,3 +664,21 @@ export interface HomeSummaryCard extends LovelaceCardConfig { hold_action?: ActionConfig; double_tap_action?: ActionConfig; } + +export interface DistributionEntityConfig extends EntityConfig { + color?: string; +} + +export interface DistributionCardConfig extends LovelaceCardConfig { + type: "distribution"; + title?: string; + entities: (string | DistributionEntityConfig)[]; +} + +export interface DiscoveredDevicesCardConfig extends LovelaceCardConfig { + hide_empty?: boolean; + vertical?: boolean; + tap_action?: ActionConfig; + hold_action?: ActionConfig; + double_tap_action?: ActionConfig; +} diff --git a/src/panels/lovelace/cards/water/hui-water-sankey-card.ts b/src/panels/lovelace/cards/water/hui-water-sankey-card.ts index 79718cfd16..5b31099195 100644 --- a/src/panels/lovelace/cards/water/hui-water-sankey-card.ts +++ b/src/panels/lovelace/cards/water/hui-water-sankey-card.ts @@ -157,7 +157,7 @@ class HuiWaterSankeyCard } nodes.push({ - id: source.stat_energy_from, + id: `source-${source.stat_energy_from}`, label: getStatisticLabel( this.hass, source.stat_energy_from, @@ -169,7 +169,7 @@ class HuiWaterSankeyCard }); links.push({ - source: source.stat_energy_from, + source: `source-${source.stat_energy_from}`, target: "home", value, }); diff --git a/src/panels/lovelace/common/validate-condition.ts b/src/panels/lovelace/common/validate-condition.ts index 39d239ef87..a375c15a03 100644 --- a/src/panels/lovelace/common/validate-condition.ts +++ b/src/panels/lovelace/common/validate-condition.ts @@ -105,6 +105,11 @@ function checkStateCondition( : UNKNOWN; let value = condition.state ?? condition.state_not; + // Guard against invalid/incomplete condition configuration + if (value === undefined) { + return false; + } + // Handle entity_id, UI should be updated for conditional card (filters does not have UI for now) if (Array.isArray(value)) { const entityValues = value diff --git a/src/panels/lovelace/components/hui-action-editor.ts b/src/panels/lovelace/components/hui-action-editor.ts index bedf1560df..6c588592c3 100644 --- a/src/panels/lovelace/components/hui-action-editor.ts +++ b/src/panels/lovelace/components/hui-action-editor.ts @@ -1,18 +1,16 @@ -import type { PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; -import { customElement, property, query } from "lit/decorators"; +import { customElement, property } from "lit/decorators"; import memoizeOne from "memoize-one"; +import { refine } from "superstruct"; import { fireEvent } from "../../../common/dom/fire_event"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; import "../../../components/ha-assist-pipeline-picker"; import type { HaFormSchema, SchemaUnion, } from "../../../components/ha-form/types"; import "../../../components/ha-help-tooltip"; -import "../../../components/ha-list-item"; import "../../../components/ha-navigation-picker"; -import type { HaSelect } from "../../../components/ha-select"; +import type { HaSelectSelectEvent } from "../../../components/ha-select"; import "../../../components/ha-service-control"; import type { ActionConfig, @@ -26,6 +24,16 @@ import type { EditorTarget } from "../editor/types"; export type UiAction = Exclude; +export interface ActionRelatedContext { + entity_id?: string; + area_id?: string; +} + +export const ACTION_RELATED_CONTEXT = { + entity_id: "entity", + area_id: "area", +} as const satisfies HaFormSchema["context"] & ActionRelatedContext; + const DEFAULT_ACTIONS: UiAction[] = [ "more-info", "toggle", @@ -36,14 +44,10 @@ const DEFAULT_ACTIONS: UiAction[] = [ "none", ]; -const NAVIGATE_SCHEMA = [ - { - name: "navigation_path", - selector: { - navigation: {}, - }, - }, -] as const satisfies readonly HaFormSchema[]; +export const supportedActions = (struct: any, supported_actions: UiAction[]) => + refine(struct, supported_actions.toString(), (value: any) => + supported_actions.includes(value.action) + ); const ASSIST_SCHEMA = [ { @@ -82,7 +86,8 @@ export class HuiActionEditor extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; - @query("ha-select") private _select!: HaSelect; + @property({ attribute: false }) + public context?: ActionRelatedContext; get _navigation_path(): string { const config = this.config as NavigateActionConfig | undefined; @@ -109,14 +114,22 @@ export class HuiActionEditor extends LitElement { }) ); - protected updated(changedProperties: PropertyValues) { - super.updated(changedProperties); - if (changedProperties.has("defaultAction")) { - if (changedProperties.get("defaultAction") !== this.defaultAction) { - this._select.layoutOptions(); - } - } - } + private _navigateSchema = memoizeOne( + ( + relatedEntityId?: string, + relatedAreaId?: string + ): readonly HaFormSchema[] => [ + { + name: "navigation_path", + selector: { + navigation: { + ...(relatedEntityId ? { entity_id: relatedEntityId } : {}), + ...(relatedAreaId ? { area_id: relatedAreaId } : {}), + }, + }, + }, + ] + ); protected render() { if (!this.hass) { @@ -138,29 +151,28 @@ export class HuiActionEditor extends LitElement { .configValue=${"action"} @selected=${this._actionPicked} .value=${action} - @closed=${stopPropagation} - fixedMenuPosition - naturalMenuWidth + .options=${[ + { + value: "default", + label: `${this.hass!.localize( + "ui.panel.lovelace.editor.action-editor.actions.default_action" + )} + ${ + this.defaultAction + ? ` (${this.hass!.localize( + `ui.panel.lovelace.editor.action-editor.actions.${this.defaultAction}` + ).toLowerCase()})` + : "" + }`, + }, + ...actions.map((actn) => ({ + value: actn, + label: this.hass!.localize( + `ui.panel.lovelace.editor.action-editor.actions.${actn}` + ), + })), + ]} > - - ${this.hass!.localize( - "ui.panel.lovelace.editor.action-editor.actions.default_action" - )} - ${this.defaultAction - ? ` (${this.hass!.localize( - `ui.panel.lovelace.editor.action-editor.actions.${this.defaultAction}` - ).toLowerCase()})` - : nothing} - - ${actions.map( - (actn) => html` - - ${this.hass!.localize( - `ui.panel.lovelace.editor.action-editor.actions.${actn}` - )} - - ` - )} ${this.tooltipText ? html` @@ -172,7 +184,10 @@ export class HuiActionEditor extends LitElement { ? html`
    - - - + + ${this.hass.localize("ui.panel.lovelace.editor.edit_card.edit")} - - + + ${this.hass.localize( "ui.panel.lovelace.editor.edit_card.duplicate" )} - - - + + + ${this.hass.localize("ui.panel.lovelace.editor.edit_card.copy")} - - - + + + ${this.hass.localize("ui.panel.lovelace.editor.edit_card.cut")} - -
  • - + + + ${this.hass.localize("ui.panel.lovelace.editor.edit_card.delete")} - - -
    + + +
    `; } @@ -186,21 +181,22 @@ export class HuiBadgeEditMode extends LitElement { this._editBadge(); } - private _handleAction(ev: CustomEvent) { - switch (ev.detail.index) { - case 0: + private _handleAction(ev: HaDropdownSelectEvent) { + const value = ev.detail.item.value; + switch (value) { + case "edit": this._editBadge(); break; - case 1: + case "duplicate": this._duplicateBadge(); break; - case 2: + case "copy": this._copyBadge(); break; - case 3: + case "cut": this._cutBadge(); break; - case 4: + case "delete": this._deleteBadge(); break; } @@ -297,7 +293,7 @@ export class HuiBadgeEditMode extends LitElement { background: var(--secondary-background-color); --mdc-icon-size: 16px; } - .more { + .more ha-icon-button { position: absolute; right: -8px; top: -8px; diff --git a/src/panels/lovelace/components/hui-card-edit-mode.ts b/src/panels/lovelace/components/hui-card-edit-mode.ts index 252b81b8d0..b0778d29dd 100644 --- a/src/panels/lovelace/components/hui-card-edit-mode.ts +++ b/src/panels/lovelace/components/hui-card-edit-mode.ts @@ -15,13 +15,13 @@ import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../../common/dom/fire_event"; 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-svg-icon"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardPath } from "../editor/lovelace-path"; import type { Lovelace } from "../types"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; @customElement("hui-card-edit-mode") export class HuiCardEditMode extends LitElement { @@ -193,7 +193,7 @@ export class HuiCardEditMode extends LitElement { this._editCard(); } - private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { const action = ev.detail?.item?.value; if (!action) { diff --git a/src/panels/lovelace/components/hui-card-options.ts b/src/panels/lovelace/components/hui-card-options.ts index 82431d9985..77d701016d 100644 --- a/src/panels/lovelace/components/hui-card-options.ts +++ b/src/panels/lovelace/components/hui-card-options.ts @@ -18,7 +18,6 @@ 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 type { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import { saveConfig } from "../../../data/lovelace/config/types"; @@ -45,6 +44,7 @@ import { } from "../editor/lovelace-path"; import { showSelectViewDialog } from "../editor/select-view/show-select-view-dialog"; import type { Lovelace, LovelaceCard } from "../types"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; @customElement("hui-card-options") export class HuiCardOptions extends LitElement { @@ -246,7 +246,7 @@ export class HuiCardOptions extends LitElement { ]; } - private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { const action = ev.detail?.item?.value; if (!action) { diff --git a/src/panels/lovelace/components/hui-energy-period-selector.ts b/src/panels/lovelace/components/hui-energy-period-selector.ts index 42267dadf6..74a78b2108 100644 --- a/src/panels/lovelace/components/hui-energy-period-selector.ts +++ b/src/panels/lovelace/components/hui-energy-period-selector.ts @@ -1,6 +1,13 @@ -import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; -import { mdiDotsVertical } from "@mdi/js"; import { + mdiChevronLeft, + mdiChevronRight, + mdiDotsVertical, + mdiCheckboxBlankOutline, + mdiCheckboxOutline, + mdiHomeClock, +} from "@mdi/js"; +import { + differenceInCalendarYears, differenceInDays, differenceInMonths, endOfDay, @@ -16,36 +23,39 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; import { classMap } from "lit/directives/class-map"; +import memoizeOne from "memoize-one"; +import { mainWindow } from "../../../common/dom/get_main_window"; +import { stopPropagation } from "../../../common/dom/stop_propagation"; import { calcDate, - calcDateProperty, calcDateDifferenceProperty, + calcDateProperty, shiftDateRange, } from "../../../common/datetime/calc_date"; +import type { DateRange } from "../../../common/datetime/calc_date_range"; +import { calcDateRange } from "../../../common/datetime/calc_date_range"; import { firstWeekdayIndex } from "../../../common/datetime/first_weekday"; import { - formatDate, - formatDateMonthYear, - formatDateShort, + formatDateMonth, formatDateVeryShort, formatDateYear, } from "../../../common/datetime/format_date"; import { debounce } from "../../../common/util/debounce"; -import "../../../components/ha-button-menu"; import "../../../components/ha-button"; -import "../../../components/ha-check-list-item"; import "../../../components/ha-date-range-picker"; -import type { DateRangePickerRanges } from "../../../components/ha-date-range-picker"; -import "../../../components/ha-icon-button-next"; -import "../../../components/ha-icon-button-prev"; +import type { + DateRangePickerRanges, + HaDateRangePicker, +} from "../../../components/ha-date-range-picker"; +import "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; +import "../../../components/ha-ripple"; +import "../../../components/ha-svg-icon"; import type { EnergyData } from "../../../data/energy"; import { CompareMode, getEnergyDataCollection } from "../../../data/energy"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../types"; -import { calcDateRange } from "../../../common/datetime/calc_date_range"; -import type { DateRange } from "../../../common/datetime/calc_date_range"; const RANGE_KEYS: DateRange[] = [ "today", @@ -59,13 +69,24 @@ const RANGE_KEYS: DateRange[] = [ "now-12m", ]; +interface OverflowMenuItem { + path: string; + label: string; + disabled?: boolean; + alwaysCollapse?: boolean; + hidden?: boolean; + action: () => void; +} + @customElement("hui-energy-period-selector") export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: "collection-key" }) public collectionKey?: string; - @property({ type: Boolean, reflect: true }) public narrow?; + @property({ type: Boolean, reflect: true }) public narrow?: boolean; + + @property({ type: Boolean, reflect: true }) public fixed?: boolean; @property({ type: Boolean, attribute: "allow-compare" }) public allowCompare = true; @@ -73,6 +94,9 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { @property({ attribute: "vertical-opening-direction" }) public verticalOpeningDirection?: "up" | "down"; + @property({ attribute: "opening-direction" }) + public openingDirection?: "right" | "left" | "center" | "inline"; + @state() _datepickerOpen = false; @state() _startDate?: Date; @@ -83,6 +107,8 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { @state() private _compare = false; + @state() private _collapseButtons = false; + private _resizeObserver?: ResizeObserver; public hassSubscribe(): UnsubscribeFunc[] { @@ -95,6 +121,7 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { private _measure() { this.narrow = this.offsetWidth < 450; + this._collapseButtons = this.offsetWidth < 320; } private async _attachObserver(): Promise { @@ -155,6 +182,64 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { this.hass.config ); + const today = new Date(); + const showStartYear = + calcDateDifferenceProperty( + today, + this._startDate, + differenceInCalendarYears, + this.hass.locale, + this.hass.config + ) !== 0; + const showBothYear = + this._endDate && + calcDateDifferenceProperty( + this._endDate, + this._startDate, + differenceInCalendarYears, + this.hass.locale, + this.hass.config + ) !== 0; + const showSubtitleYear = + simpleRange !== "year" && (showStartYear || showBothYear); + + const buttons = [ + { + path: + mainWindow.document.dir === "rtl" ? mdiChevronRight : mdiChevronLeft, + label: this.hass.localize( + "ui.panel.lovelace.components.energy_period_selector.previous" + ), + action: () => this._pickPrevious(), + }, + { + path: + mainWindow.document.dir === "rtl" ? mdiChevronLeft : mdiChevronRight, + label: this.hass.localize( + "ui.panel.lovelace.components.energy_period_selector.next" + ), + action: () => this._pickNext(), + }, + { + path: mdiHomeClock, + label: this.hass.localize( + "ui.panel.lovelace.components.energy_period_selector.now" + ), + alwaysCollapse: true, + hidden: !this.narrow, + action: () => this._pickNow(), + }, + { + path: this._compare ? mdiCheckboxOutline : mdiCheckboxBlankOutline, + disabled: !this.allowCompare, + alwaysCollapse: true, + label: this.hass.localize( + "ui.panel.lovelace.components.energy_period_selector.compare" + ), + action: () => this._toggleCompare(), + }, + ] as OverflowMenuItem[]; + return html`
    -
    - ${simpleRange === "day" - ? this.narrow - ? formatDateShort( - this._startDate, - this.hass.locale, - this.hass.config - ) - : formatDate(this._startDate, this.hass.locale, this.hass.config) - : simpleRange === "month" - ? formatDateMonthYear( - this._startDate, - this.hass.locale, - this.hass.config - ) - : simpleRange === "year" - ? formatDateYear( +
    +
    + +
    +
    + +
    + ${simpleRange === "year" + ? html`${formatDateYear( this._startDate, this.hass.locale, this.hass.config - ) - : `${formatDateVeryShort( + )}` + : html`${simpleRange === "month" + ? html`${formatDateMonth( + this._startDate, + this.hass.locale, + this.hass.config + )}` + : simpleRange === "day" + ? html`${formatDateVeryShort( + this._startDate, + this.hass.locale, + this.hass.config + )}` + : html`${formatDateVeryShort( + this._startDate, + this.hass.locale, + this.hass.config + )}–${formatDateVeryShort( + this._endDate || new Date(), + this.hass.locale, + this.hass.config + )}`}`} +
    + ${showSubtitleYear + ? html`
    + ${formatDateYear( this._startDate, this.hass.locale, this.hass.config - )} – ${formatDateVeryShort( - this._endDate || new Date(), - this.hass.locale, - this.hass.config - )}`} -
    -
    - - - -
    - - ${!this.narrow - ? html` - ${this.hass.localize( - "ui.panel.lovelace.components.energy_period_selector.now" + )}${showBothYear + ? html`–${formatDateYear( + this._endDate || new Date(), + this.hass.locale, + this.hass.config + )}` + : ``} +
    ` + : nothing} + +
    +
    + ${!this.narrow + ? html` + ${this.hass.localize( + "ui.panel.lovelace.components.energy_period_selector.now" + )} + ` + : nothing} + ${buttons.map((item) => + this._collapseButtons || item.alwaysCollapse + ? nothing + : html`` )} - ` - : nothing} - - - - ${this.allowCompare - ? html` - ${this.hass.localize( - "ui.panel.lovelace.components.energy_period_selector.compare" - )} - ` - : nothing} - - + ${this._collapseButtons || buttons.some((x) => x.alwaysCollapse) + ? html` + + ${buttons.map((item) => + (this._collapseButtons || item.alwaysCollapse) && + !item.hidden + ? html` + + ${item.label} + ` + : nothing + )} + ` + : nothing} +
    +
    +
    `; } @@ -297,6 +409,13 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { if ( calcDateProperty(startDate, isFirstDayOfMonth, locale, config) && calcDateProperty(endDate, isLastDayOfMonth, locale, config) && + calcDateDifferenceProperty( + endDate, + startDate, + differenceInCalendarYears, + locale, + config + ) === 0 && calcDateDifferenceProperty( endDate, startDate, @@ -311,6 +430,20 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { } ); + private get _datePicker(): HaDateRangePicker | undefined { + return this.shadowRoot!.querySelector("ha-date-range-picker") ?? undefined; + } + + private _handleIconOverflowMenuOpened(ev: Event) { + ev.stopPropagation(); + } + + private _openDatePicker(ev: Event) { + const datePicker = this._datePicker; + if (datePicker) datePicker.open(); + ev.stopPropagation(); + } + private _updateCollectionPeriod() { const energyCollection = getEnergyDataCollection(this.hass, { key: this.collectionKey, @@ -464,11 +597,8 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { this._datepickerOpen = ev.detail.open; } - private _toggleCompare(ev: CustomEvent) { - if (ev.detail.source !== "interaction") { - return; - } - this._compare = ev.detail.selected; + private _toggleCompare() { + this._compare = !this._compare; const energyCollection = getEnergyDataCollection(this.hass, { key: this.collectionKey, }); @@ -479,33 +609,66 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { } static styles = css` + :host { + display: block; + } .row { - display: flex; - align-items: center; + justify-content: space-between; + container-type: inline-size; } - :host .time-handle { + .content { display: flex; - justify-content: flex-end; + flex-direction: row; align-items: center; + box-sizing: border-box; } - :host([narrow]) .time-handle { - margin-left: auto; - margin-inline-start: auto; - margin-inline-end: initial; - } - .label { + .date-picker-icon { + flex: none; + min-width: var(--ha-space-2); + height: 100%; display: flex; - align-items: center; - justify-content: flex-end; + flex-direction: row; + } + .date-range { + flex: 1; + padding: 2px var(--ha-space-2); + display: flex; + flex-direction: column; + justify-content: center; + min-height: var(--ha-space-10); + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; + } + .header-title { font-size: var(--ha-font-size-xl); - margin-left: auto; - margin-inline-start: auto; - margin-inline-end: initial; + line-height: var(--ha-line-height-condensed); + font-weight: var(--ha-font-weight-medium); + color: var(--primary-text-color); } - :host([narrow]) .label { - margin-left: unset; - margin-inline-start: unset; - margin-inline-end: initial; + .header-subtitle { + font-size: var(--ha-font-size-m); + line-height: var(--ha-line-height-condensed); + color: var(--secondary-text-color); + } + :host([narrow]) .header-title { + font-size: var(--ha-font-size-m); + } + :host([narrow]) .header-subtitle { + font-size: var(--ha-font-size-s); + } + .date-actions { + flex: none; + min-width: var(--ha-space-2); + height: 100%; + display: flex; + flex-direction: row; + } + .date-actions .overflow { + display: flex; + align-items: center; } ha-button { margin-left: 8px; @@ -531,7 +694,7 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { ); pointer-events: none; opacity: 0; - transition: opacity var(--ha-animation-base-duration) ease-in-out; + transition: opacity var(--ha-animation-duration-slow) ease-in-out; } .datepicker-open .backdrop { opacity: 1; diff --git a/src/panels/lovelace/components/hui-entity-editor.ts b/src/panels/lovelace/components/hui-entity-editor.ts index 8d8b1d7680..30d8a0675c 100644 --- a/src/panels/lovelace/components/hui-entity-editor.ts +++ b/src/panels/lovelace/components/hui-entity-editor.ts @@ -246,6 +246,7 @@ export class HuiEntityEditor extends LitElement { } ha-md-list { gap: 8px; + padding-top: 0; } ha-md-list-item { border: 1px solid var(--divider-color); diff --git a/src/panels/lovelace/components/hui-generic-entity-row.ts b/src/panels/lovelace/components/hui-generic-entity-row.ts index 98db07fe54..c99b847640 100644 --- a/src/panels/lovelace/components/hui-generic-entity-row.ts +++ b/src/panels/lovelace/components/hui-generic-entity-row.ts @@ -8,6 +8,8 @@ import { uid } from "../../../common/util/uid"; import { stopPropagation } from "../../../common/dom/stop_propagation"; import { toggleAttribute } from "../../../common/dom/toggle_attribute"; import { computeDomain } from "../../../common/entity/compute_domain"; +import { computeAreaName } from "../../../common/entity/compute_area_name"; +import { getEntityContext } from "../../../common/entity/context/get_entity_context"; import { formatDateTimeWithSeconds } from "../../../common/datetime/format_date_time"; import "../../../components/entity/state-badge"; import "../../../components/ha-relative-time"; @@ -199,7 +201,9 @@ export class HuiGenericEntityRow extends LitElement { ? html`${this.hass.formatEntityState( stateObj )}` - : nothing)} + : this.config.secondary_info === "area" + ? (this._getArea(stateObj) ?? nothing) + : nothing)}
    ` : nothing} @@ -235,6 +239,17 @@ export class HuiGenericEntityRow extends LitElement { handleAction(this, this.hass!, this.config!, ev.detail.action!); } + private _getArea(stateObj) { + const context = getEntityContext( + stateObj, + this.hass!.entities, + this.hass!.devices, + this.hass!.areas, + this.hass!.floors + ); + return context.area ? computeAreaName(context.area) : undefined; + } + static styles = css` :host { display: flex; diff --git a/src/panels/lovelace/components/hui-graph-base.ts b/src/panels/lovelace/components/hui-graph-base.ts index 089a6410d3..f008244751 100644 --- a/src/panels/lovelace/components/hui-graph-base.ts +++ b/src/panels/lovelace/components/hui-graph-base.ts @@ -13,6 +13,8 @@ export class HuiGraphBase extends LitElement { @state() private _path?: string; + private _uniqueId = `graph-${Math.random().toString(36).substring(2, 9)}`; + protected render(): TemplateResult { const width = this.clientWidth || 500; const height = this.clientHeight || width / 5; @@ -21,15 +23,15 @@ export class HuiGraphBase extends LitElement { ${this._path ? svg` - + - - + + - + ` : svg``} diff --git a/src/panels/lovelace/components/hui-section-edit-mode.ts b/src/panels/lovelace/components/hui-section-edit-mode.ts index b1cf2404ad..d6e0da7bdb 100644 --- a/src/panels/lovelace/components/hui-section-edit-mode.ts +++ b/src/panels/lovelace/components/hui-section-edit-mode.ts @@ -2,9 +2,7 @@ import { mdiDelete, mdiDragHorizontalVariant, mdiPencil } from "@mdi/js"; import type { CSSResultGroup, TemplateResult } from "lit"; import { LitElement, css, html } from "lit"; import { customElement, property } from "lit/decorators"; -import "../../../components/ha-button-menu"; import "../../../components/ha-icon-button"; -import "../../../components/ha-list-item"; import "../../../components/ha-svg-icon"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../../resources/styles"; @@ -20,9 +18,9 @@ export class HuiSectionEditMode extends LitElement { @property({ attribute: false }) public lovelace!: Lovelace; - @property({ attribute: false, type: Number }) public index!: number; + @property({ attribute: false }) public index!: number; - @property({ attribute: false, type: Number }) public viewIndex!: number; + @property({ attribute: false }) public viewIndex!: number; protected render(): TemplateResult { return html` diff --git a/src/panels/lovelace/create-element/create-card-element.ts b/src/panels/lovelace/create-element/create-card-element.ts index c7e13d772d..ca5faa7395 100644 --- a/src/panels/lovelace/create-element/create-card-element.ts +++ b/src/panels/lovelace/create-element/create-card-element.ts @@ -73,9 +73,11 @@ const LAZY_LOAD_TYPES = { "entity-filter": () => import("../cards/hui-entity-filter-card"), error: () => import("../cards/hui-error-card"), "home-summary": () => import("../cards/hui-home-summary-card"), + "discovered-devices": () => import("../cards/hui-discovered-devices-card"), gauge: () => import("../cards/hui-gauge-card"), "history-graph": () => import("../cards/hui-history-graph-card"), "horizontal-stack": () => import("../cards/hui-horizontal-stack-card"), + distribution: () => import("../cards/hui-distribution-card"), humidifier: () => import("../cards/hui-humidifier-card"), iframe: () => import("../cards/hui-iframe-card"), logbook: () => import("../cards/hui-logbook-card"), diff --git a/src/panels/lovelace/create-element/create-heading-badge-element.ts b/src/panels/lovelace/create-element/create-heading-badge-element.ts index 282359295a..e50b8c427f 100644 --- a/src/panels/lovelace/create-element/create-heading-badge-element.ts +++ b/src/panels/lovelace/create-element/create-heading-badge-element.ts @@ -1,3 +1,4 @@ +import "../heading-badges/hui-button-heading-badge"; import "../heading-badges/hui-entity-heading-badge"; import { @@ -6,7 +7,7 @@ import { } from "./create-element-base"; import type { LovelaceHeadingBadgeConfig } from "../heading-badges/types"; -const ALWAYS_LOADED_TYPES = new Set(["error", "entity"]); +const ALWAYS_LOADED_TYPES = new Set(["error", "entity", "button"]); export const createHeadingBadgeElement = (config: LovelaceHeadingBadgeConfig) => createLovelaceElement( diff --git a/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts b/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts index 55f247efe7..75338c5516 100644 --- a/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts +++ b/src/panels/lovelace/editor/badge-editor/hui-dialog-edit-badge.ts @@ -37,6 +37,7 @@ import type { GUIModeChangedEvent } from "../types"; import "./hui-badge-element-editor"; import type { HuiBadgeElementEditor } from "./hui-badge-element-editor"; import type { EditBadgeDialogParams } from "./show-edit-badge-dialog"; +import { withViewTransition } from "../../../../common/util/view-transition"; declare global { // for fire event @@ -299,7 +300,9 @@ export class HuiDialogEditBadge } private _enlarge() { - this.large = !this.large; + withViewTransition(() => { + this.large = !this.large; + }); } private _ignoreKeydown(ev: KeyboardEvent) { diff --git a/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts b/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts index 66b17c6280..7c17d2aad4 100644 --- a/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-layout-editor.ts @@ -1,4 +1,3 @@ -import type { ActionDetail } from "@material/mwc-list"; import { mdiDotsVertical, mdiPlaylistEdit } from "@mdi/js"; import type { PropertyValues } from "lit"; import { css, html, LitElement } from "lit"; @@ -6,13 +5,11 @@ import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { preventDefault } from "../../../../common/dom/prevent_default"; -import { stopPropagation } from "../../../../common/dom/stop_propagation"; import "../../../../components/ha-button"; -import "../../../../components/ha-button-menu"; +import "../../../../components/ha-dropdown"; +import "../../../../components/ha-dropdown-item"; import "../../../../components/ha-grid-size-picker"; import "../../../../components/ha-icon-button"; -import "../../../../components/ha-list-item"; import "../../../../components/ha-settings-row"; import "../../../../components/ha-slider"; import "../../../../components/ha-svg-icon"; @@ -31,6 +28,7 @@ import { migrateLayoutToGridOptions, } from "../../common/compute-card-grid-size"; import type { LovelaceGridOptions } from "../../types"; +import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown"; @customElement("hui-card-layout-editor") export class HuiCardLayoutEditor extends LitElement { @@ -94,14 +92,10 @@ export class HuiCardLayoutEditor extends LitElement { "ui.panel.lovelace.editor.edit_card.layout.explanation" )}

    - - + ${this.hass.localize( `ui.panel.lovelace.editor.edit_view.edit_${!this._yamlMode ? "yaml" : "ui"}` )} - - - + + +
    ${this._yamlMode ? html` @@ -244,11 +238,11 @@ export class HuiCardLayoutEditor extends LitElement { } } - private async _handleAction(ev: CustomEvent) { - switch (ev.detail.index) { - case 0: - this._yamlMode = !this._yamlMode; - break; + private async _handleAction(ev: HaDropdownSelectEvent) { + const action = ev.detail.item.value; + + if (action === "toggle_yaml") { + this._yamlMode = !this._yamlMode; } } @@ -331,7 +325,7 @@ export class HuiCardLayoutEditor extends LitElement { margin: 0; color: var(--secondary-text-color); } - .header ha-button-menu { + .header ha-dropdown { --mdc-theme-text-primary-on-background: var(--primary-text-color); margin-top: -8px; } diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts index 47bf053330..18cab231db 100644 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts @@ -36,6 +36,7 @@ import type { GUIModeChangedEvent } from "../types"; import "./hui-card-element-editor"; import type { HuiCardElementEditor } from "./hui-card-element-editor"; import type { EditCardDialogParams } from "./show-edit-card-dialog"; +import { withViewTransition } from "../../../../common/util/view-transition"; declare global { // for fire event @@ -267,7 +268,9 @@ export class HuiDialogEditCard } private _enlarge() { - this.large = !this.large; + withViewTransition(() => { + this.large = !this.large; + }); } private _ignoreKeydown(ev: KeyboardEvent) { diff --git a/src/panels/lovelace/editor/conditions/ha-card-condition-editor.ts b/src/panels/lovelace/editor/conditions/ha-card-condition-editor.ts index 8097ee5cb4..f603fb1437 100644 --- a/src/panels/lovelace/editor/conditions/ha-card-condition-editor.ts +++ b/src/panels/lovelace/editor/conditions/ha-card-condition-editor.ts @@ -1,4 +1,4 @@ -import type { ActionDetail } from "@material/mwc-list"; +import "@home-assistant/webawesome/dist/components/divider/divider"; import { mdiContentCopy, mdiContentCut, @@ -16,15 +16,14 @@ import { classMap } from "lit/directives/class-map"; import { storage } from "../../../../common/decorators/storage"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { preventDefault } from "../../../../common/dom/prevent_default"; import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { handleStructError } from "../../../../common/structs/handle-errors"; import "../../../../components/ha-alert"; -import "../../../../components/ha-button-menu"; import "../../../../components/ha-card"; +import "../../../../components/ha-dropdown"; +import "../../../../components/ha-dropdown-item"; import "../../../../components/ha-expansion-panel"; import "../../../../components/ha-icon-button"; -import "../../../../components/ha-list-item"; import "../../../../components/ha-svg-icon"; import "../../../../components/ha-yaml-editor"; import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box"; @@ -40,6 +39,7 @@ import { validateConditionalConfig, } from "../../common/validate-condition"; import type { LovelaceConditionEditorConstructor } from "./types"; +import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown"; @customElement("ha-card-condition-editor") export class HaCardConditionEditor extends LitElement { @@ -126,14 +126,11 @@ export class HaCardConditionEditor extends LitElement { `ui.panel.lovelace.editor.condition-editor.condition.${condition.condition}.label` ) || condition.condition}

    - - + ${this.hass.localize( "ui.panel.lovelace.editor.condition-editor.test" )} - - + + - + ${this.hass.localize( "ui.panel.lovelace.editor.edit_card.duplicate" )} - + - + ${this.hass.localize("ui.panel.lovelace.editor.edit_card.copy")} - - + + - + ${this.hass.localize("ui.panel.lovelace.editor.edit_card.cut")} - - + + - + ${this.hass.localize( `ui.panel.lovelace.editor.edit_view.edit_${!this._yamlMode ? "yaml" : "ui"}` )} - - + + -
  • + - + ${this.hass!.localize("ui.common.delete")} - - -
    + + + ${!this._uiAvailable ? html` ) { - switch (ev.detail.index) { - case 0: + private async _handleAction(ev: HaDropdownSelectEvent) { + const action = ev.detail.item.value; + + if (action === undefined) { + return; + } + + switch (action) { + case "test": await this._testCondition(); - break; - case 1: + return; + case "duplicate": this._duplicateCondition(); - break; - case 2: + return; + case "copy": this._copyCondition(); - break; - case 3: + return; + case "cut": this._cutCondition(); - break; - case 4: + return; + case "toggle_yaml": this._yamlMode = !this._yamlMode; - break; - case 5: + return; + case "delete": this._delete(); - break; } } @@ -321,9 +319,9 @@ export class HaCardConditionEditor extends LitElement { this._delete(); } - private _delete() { + private _delete = () => { fireEvent(this, "value-changed", { value: null }); - } + }; private _onYamlChange(ev: CustomEvent) { ev.stopPropagation(); @@ -337,7 +335,7 @@ export class HaCardConditionEditor extends LitElement { static styles = [ haStyle, css` - ha-button-menu { + ha-dropdown { --mdc-theme-text-primary-on-background: var(--primary-text-color); } ha-expansion-panel { diff --git a/src/panels/lovelace/editor/conditions/ha-card-conditions-editor.ts b/src/panels/lovelace/editor/conditions/ha-card-conditions-editor.ts index 9a14d230ea..413cb87d48 100644 --- a/src/panels/lovelace/editor/conditions/ha-card-conditions-editor.ts +++ b/src/panels/lovelace/editor/conditions/ha-card-conditions-editor.ts @@ -3,11 +3,11 @@ import deepClone from "deep-clone-simple"; import type { CSSResultGroup, PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; +import { storage } from "../../../../common/decorators/storage"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { stopPropagation } from "../../../../common/dom/stop_propagation"; import "../../../../components/ha-button"; -import "../../../../components/ha-list-item"; -import type { HaSelect } from "../../../../components/ha-select"; +import "../../../../components/ha-dropdown"; +import "../../../../components/ha-dropdown-item"; import "../../../../components/ha-svg-icon"; import type { HomeAssistant } from "../../../../types"; import { ICON_CONDITION } from "../../common/icon-condition"; @@ -27,7 +27,7 @@ import "./types/ha-card-condition-screen"; import "./types/ha-card-condition-state"; import "./types/ha-card-condition-time"; import "./types/ha-card-condition-user"; -import { storage } from "../../../../common/decorators/storage"; +import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown"; const UI_CONDITION = [ "location", @@ -41,8 +41,6 @@ const UI_CONDITION = [ "or", ] as const satisfies readonly Condition["condition"][]; -export const PASTE_VALUE = "__paste__" as const; - @customElement("ha-card-conditions-editor") export class HaCardConditionsEditor extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -107,11 +105,7 @@ export class HaCardConditionsEditor extends LitElement { ` )}
    - + ${this.hass.localize( @@ -120,59 +114,57 @@ export class HaCardConditionsEditor extends LitElement { ${this._clipboard ? html` - + ${this.hass.localize( "ui.panel.lovelace.editor.edit_card.paste_condition" )} - + ` : nothing} ${UI_CONDITION.map( (condition) => html` - + ${this.hass!.localize( `ui.panel.lovelace.editor.condition-editor.condition.${condition}.label` ) || condition} - + ` )} - +
    `; } - private _addCondition(ev: CustomEvent): void { + private _addCondition(ev: HaDropdownSelectEvent) { + const condition = ev.detail.item.value as "paste" | Condition["condition"]; const conditions = [...this.conditions]; - const item = (ev.currentTarget as HaSelect).items[ev.detail.index]; - - if (item.value === PASTE_VALUE && this._clipboard) { - const condition = deepClone(this._clipboard); - conditions.push(condition); - fireEvent(this, "value-changed", { value: conditions }); + if (!condition || (condition === "paste" && !this._clipboard)) { return; } - const condition = item.value as Condition["condition"]; + if (condition === "paste") { + const newCondition = deepClone(this._clipboard); + conditions.push(newCondition); + } else { + const elClass = customElements.get(`ha-card-condition-${condition}`) as + | LovelaceConditionEditorConstructor + | undefined; - const elClass = customElements.get(`ha-card-condition-${condition}`) as - | LovelaceConditionEditorConstructor - | undefined; + conditions.push( + elClass?.defaultConfig ? { ...elClass.defaultConfig } : { condition } + ); + } - conditions.push( - elClass?.defaultConfig - ? { ...elClass.defaultConfig } - : { condition: condition } - ); this._focusLastConditionOnChange = true; fireEvent(this, "value-changed", { value: conditions }); } @@ -210,8 +202,9 @@ export class HaCardConditionsEditor extends LitElement { margin-top: 12px; scroll-margin-top: 48px; } - ha-button-menu { - margin-top: 12px; + ha-dropdown { + display: inline-block; + margin-top: var(--ha-space-3); } `, ]; diff --git a/src/panels/lovelace/editor/config-elements/elements/hui-icon-element-editor.ts b/src/panels/lovelace/editor/config-elements/elements/hui-icon-element-editor.ts index 350327a747..452e665706 100644 --- a/src/panels/lovelace/editor/config-elements/elements/hui-icon-element-editor.ts +++ b/src/panels/lovelace/editor/config-elements/elements/hui-icon-element-editor.ts @@ -8,6 +8,7 @@ import type { SchemaUnion } from "../../../../../components/ha-form/types"; import type { HomeAssistant } from "../../../../../types"; import type { IconElementConfig } from "../../../elements/types"; import type { LovelacePictureElementEditor } from "../../../types"; +import { ACTION_RELATED_CONTEXT } from "../../../components/hui-action-editor"; import { actionConfigStruct } from "../../structs/action-struct"; const iconElementConfigStruct = object({ @@ -38,6 +39,7 @@ const SCHEMA = [ default_action: "more-info", }, }, + context: ACTION_RELATED_CONTEXT, }, { name: "hold_action", @@ -46,6 +48,7 @@ const SCHEMA = [ default_action: "more-info", }, }, + context: ACTION_RELATED_CONTEXT, }, { name: "", @@ -59,6 +62,7 @@ const SCHEMA = [ default_action: "none", }, }, + context: ACTION_RELATED_CONTEXT, }, ], }, diff --git a/src/panels/lovelace/editor/config-elements/elements/hui-image-element-editor.ts b/src/panels/lovelace/editor/config-elements/elements/hui-image-element-editor.ts index ed805d4a5d..d5d98f524f 100644 --- a/src/panels/lovelace/editor/config-elements/elements/hui-image-element-editor.ts +++ b/src/panels/lovelace/editor/config-elements/elements/hui-image-element-editor.ts @@ -18,6 +18,7 @@ import type { SchemaUnion } from "../../../../../components/ha-form/types"; import type { HomeAssistant } from "../../../../../types"; import type { ImageElementConfig } from "../../../elements/types"; import type { LovelacePictureElementEditor } from "../../../types"; +import { ACTION_RELATED_CONTEXT } from "../../../components/hui-action-editor"; import { actionConfigStruct } from "../../structs/action-struct"; const imageElementConfigStruct = object({ @@ -69,6 +70,7 @@ export class HuiImageElementEditor default_action: "more-info", }, }, + context: ACTION_RELATED_CONTEXT, }, { name: "hold_action", @@ -77,6 +79,7 @@ export class HuiImageElementEditor default_action: "more-info", }, }, + context: ACTION_RELATED_CONTEXT, }, { name: "", @@ -90,6 +93,7 @@ export class HuiImageElementEditor default_action: "none", }, }, + context: ACTION_RELATED_CONTEXT, }, ], }, diff --git a/src/panels/lovelace/editor/config-elements/elements/hui-service-button-element-editor.ts b/src/panels/lovelace/editor/config-elements/elements/hui-service-button-element-editor.ts index 9cd2088342..b6c06a48da 100644 --- a/src/panels/lovelace/editor/config-elements/elements/hui-service-button-element-editor.ts +++ b/src/panels/lovelace/editor/config-elements/elements/hui-service-button-element-editor.ts @@ -7,7 +7,7 @@ import "../../../../../components/ha-form/ha-form"; import type { SchemaUnion } from "../../../../../components/ha-form/types"; import "../../../../../components/ha-service-control"; import type { ServiceAction } from "../../../../../data/script"; -import type { HomeAssistant } from "../../../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../../../types"; import type { ServiceButtonElementConfig } from "../../../elements/types"; import type { LovelacePictureElementEditor } from "../../../types"; @@ -78,7 +78,7 @@ export class HuiServiceButtonElementEditor }); } - private _serviceDataChanged(ev: CustomEvent<{ value: ServiceAction }>): void { + private _serviceDataChanged(ev: ValueChangedEvent): void { const config: ServiceButtonElementConfig = { ...this._config!, action: ev.detail.value.action, diff --git a/src/panels/lovelace/editor/config-elements/elements/hui-state-badge-element-editor.ts b/src/panels/lovelace/editor/config-elements/elements/hui-state-badge-element-editor.ts index 4231eba303..f490f52de4 100644 --- a/src/panels/lovelace/editor/config-elements/elements/hui-state-badge-element-editor.ts +++ b/src/panels/lovelace/editor/config-elements/elements/hui-state-badge-element-editor.ts @@ -6,6 +6,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event"; import type { SchemaUnion } from "../../../../../components/ha-form/types"; import type { HomeAssistant } from "../../../../../types"; import "../../../../../components/ha-form/ha-form"; +import { ACTION_RELATED_CONTEXT } from "../../../components/hui-action-editor"; import type { LovelacePictureElementEditor } from "../../../types"; import type { StateBadgeElementConfig } from "../../../elements/types"; import { actionConfigStruct } from "../../structs/action-struct"; @@ -38,6 +39,7 @@ const SCHEMA = [ default_action: "more-info", }, }, + context: ACTION_RELATED_CONTEXT, }, { name: "hold_action", @@ -46,6 +48,7 @@ const SCHEMA = [ default_action: "more-info", }, }, + context: ACTION_RELATED_CONTEXT, }, { name: "", @@ -59,6 +62,7 @@ const SCHEMA = [ default_action: "none", }, }, + context: ACTION_RELATED_CONTEXT, }, ], }, diff --git a/src/panels/lovelace/editor/config-elements/elements/hui-state-icon-element-editor.ts b/src/panels/lovelace/editor/config-elements/elements/hui-state-icon-element-editor.ts index 217d8edac6..053aff12b8 100644 --- a/src/panels/lovelace/editor/config-elements/elements/hui-state-icon-element-editor.ts +++ b/src/panels/lovelace/editor/config-elements/elements/hui-state-icon-element-editor.ts @@ -16,6 +16,7 @@ import type { SchemaUnion } from "../../../../../components/ha-form/types"; import type { HomeAssistant } from "../../../../../types"; import type { StateIconElementConfig } from "../../../elements/types"; import type { LovelacePictureElementEditor } from "../../../types"; +import { ACTION_RELATED_CONTEXT } from "../../../components/hui-action-editor"; import { actionConfigStruct } from "../../structs/action-struct"; const stateIconElementConfigStruct = object({ @@ -48,6 +49,7 @@ const SCHEMA = [ default_action: "more-info", }, }, + context: ACTION_RELATED_CONTEXT, }, { name: "hold_action", @@ -56,6 +58,7 @@ const SCHEMA = [ default_action: "more-info", }, }, + context: ACTION_RELATED_CONTEXT, }, { name: "", @@ -69,6 +72,7 @@ const SCHEMA = [ default_action: "none", }, }, + context: ACTION_RELATED_CONTEXT, }, ], }, diff --git a/src/panels/lovelace/editor/config-elements/elements/hui-state-label-element-editor.ts b/src/panels/lovelace/editor/config-elements/elements/hui-state-label-element-editor.ts index 561419dd9e..86dd0751c3 100644 --- a/src/panels/lovelace/editor/config-elements/elements/hui-state-label-element-editor.ts +++ b/src/panels/lovelace/editor/config-elements/elements/hui-state-label-element-editor.ts @@ -8,6 +8,7 @@ import type { SchemaUnion } from "../../../../../components/ha-form/types"; import type { HomeAssistant } from "../../../../../types"; import type { StateLabelElementConfig } from "../../../elements/types"; import type { LovelacePictureElementEditor } from "../../../types"; +import { ACTION_RELATED_CONTEXT } from "../../../components/hui-action-editor"; import { actionConfigStruct } from "../../structs/action-struct"; const stateLabelElementConfigStruct = object({ @@ -48,6 +49,7 @@ const SCHEMA = [ default_action: "more-info", }, }, + context: ACTION_RELATED_CONTEXT, }, { name: "hold_action", @@ -56,6 +58,7 @@ const SCHEMA = [ default_action: "more-info", }, }, + context: ACTION_RELATED_CONTEXT, }, { name: "", @@ -69,6 +72,7 @@ const SCHEMA = [ default_action: "none", }, }, + context: ACTION_RELATED_CONTEXT, }, ], }, diff --git a/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts index b73ffd946f..f79e5e3856 100644 --- a/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-area-card-editor.ts @@ -39,6 +39,8 @@ import { } from "../../cards/hui-area-card"; import type { AreaCardConfig, AreaCardDisplayType } from "../../cards/types"; import type { LovelaceCardEditor } from "../../types"; +import { actionConfigStruct } from "../structs/action-struct"; +import { ACTION_RELATED_CONTEXT } from "../../components/hui-action-editor"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import type { EditDetailElementEvent, EditSubElementEvent } from "../types"; import { configElementStyle } from "./config-elements-style"; @@ -61,6 +63,8 @@ const cardConfigStruct = assign( features_position: optional(enums(["bottom", "inline"])), aspect_ratio: optional(string()), exclude_entities: optional(array(string())), + tap_action: optional(actionConfigStruct), + image_tap_action: optional(actionConfigStruct), }) ); @@ -187,10 +191,39 @@ export class HuiAreaCardEditor iconPath: mdiGestureTap, schema: [ { - name: "navigation_path", - required: false, - selector: { navigation: {} }, + name: "tap_action", + selector: { + ui_action: { + default_action: "none", + actions: ["navigate", "url", "perform-action", "none"], + }, + }, + context: ACTION_RELATED_CONTEXT, }, + ...(displayType !== "compact" + ? ([ + { + name: "image_tap_action", + selector: { + ui_action: { + default_action: + displayType === "camera" ? "more-info" : "none", + actions: + displayType === "camera" + ? [ + "more-info", + "navigate", + "url", + "perform-action", + "none", + ] + : ["navigate", "url", "perform-action", "none"], + }, + }, + context: ACTION_RELATED_CONTEXT, + }, + ] as const satisfies readonly HaFormSchema[]) + : []), ], }, ] as const satisfies readonly HaFormSchema[] @@ -390,7 +423,10 @@ export class HuiAreaCardEditor const vertical = this._config.vertical && displayType === "compact"; - const featuresSchema = this._featuresSchema(this.hass.localize, vertical); + const featuresSchema = this._featuresSchema( + this.hass.localize, + Boolean(vertical) + ); const data = { camera_view: "auto", @@ -401,6 +437,14 @@ export class HuiAreaCardEditor ...this._config, }; + // Backwards compatibility: convert navigation_path to tap_action for display + if (data.navigation_path && !data.tap_action) { + data.tap_action = { + action: "navigate", + navigation_path: data.navigation_path, + }; + } + // Default features position to bottom and force it to bottom in vertical mode if (!data.features_position || vertical) { data.features_position = "bottom"; @@ -459,10 +503,26 @@ export class HuiAreaCardEditor ...newConfig, }; + // Clean up navigation_path if tap_action is set + if (config.tap_action && config.navigation_path) { + delete config.navigation_path; + } + if (config.display_type !== "camera") { delete config.camera_view; } + // Clean up image_tap_action if compact display type (no image area) + // or if it's more-info but not in camera mode (no entity to show) + if ( + config.image_tap_action && + (config.display_type === "compact" || + (config.display_type !== "camera" && + config.image_tap_action.action === "more-info")) + ) { + delete config.image_tap_action; + } + // Convert content_layout to vertical if (config.content_layout) { config.vertical = config.content_layout === "vertical"; @@ -545,12 +605,13 @@ export class HuiAreaCardEditor case "camera_view": case "content": case "interactions": + case "tap_action": return this.hass!.localize( `ui.panel.lovelace.editor.card.generic.${schema.name}` ); - case "navigation_path": + case "image_tap_action": return this.hass!.localize( - "ui.panel.lovelace.editor.action-editor.navigation_path" + `ui.panel.lovelace.editor.card.area.${schema.name}` ); case "features_position": return this.hass!.localize( diff --git a/src/panels/lovelace/editor/config-elements/hui-area-controls-card-feature-editor.ts b/src/panels/lovelace/editor/config-elements/hui-area-controls-card-feature-editor.ts index 18089a26b7..e0e9ced3a6 100644 --- a/src/panels/lovelace/editor/config-elements/hui-area-controls-card-feature-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-area-controls-card-feature-editor.ts @@ -1,21 +1,28 @@ -import { html, LitElement, nothing } from "lit"; +import { mdiDragHorizontalVariant } from "@mdi/js"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { repeat } from "lit/directives/repeat"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../common/dom/fire_event"; -import type { LocalizeFunc } from "../../../../common/translations/localize"; +import { computeStateName } from "../../../../common/entity/compute_state_name"; +import "../../../../components/ha-area-controls-picker"; import "../../../../components/ha-form/ha-form"; import type { HaFormSchema, SchemaUnion, } from "../../../../components/ha-form/types"; +import "../../../../components/chips/ha-input-chip"; +import "../../../../components/ha-sortable"; +import "../../../../components/ha-svg-icon"; import type { HomeAssistant } from "../../../../types"; import { getAreaControlEntities, MAX_DEFAULT_AREA_CONTROLS, -} from "../../card-features/hui-area-controls-card-feature"; +} from "../../../../data/area/area_controls"; import { - AREA_CONTROLS, + AREA_CONTROL_DOMAINS, type AreaControl, + type AreaControlDomain, type AreaControlsCardFeatureConfig, } from "../../card-features/types"; import type { AreaCardFeatureContext } from "../../cards/hui-area-card"; @@ -40,40 +47,14 @@ export class HuiAreaControlsCardFeatureEditor this._config = config; } - private _schema = memoizeOne( - ( - localize: LocalizeFunc, - customizeControls: boolean, - compatibleControls: AreaControl[] - ) => - [ - { - name: "customize_controls", - selector: { - boolean: {}, - }, - }, - ...(customizeControls - ? ([ - { - name: "controls", - selector: { - select: { - reorder: true, - multiple: true, - options: compatibleControls.map((control) => ({ - value: control, - label: localize( - `ui.panel.lovelace.editor.features.types.area-controls.controls_options.${control}` - ), - })), - }, - }, - }, - ] as const satisfies readonly HaFormSchema[]) - : []), - ] as const satisfies readonly HaFormSchema[] - ); + private _schema = [ + { + name: "customize_controls", + selector: { + boolean: {}, + }, + }, + ] as const satisfies readonly HaFormSchema[]; private _supportedControls = memoizeOne( ( @@ -88,7 +69,7 @@ export class HuiAreaControlsCardFeatureEditor return []; } const controlEntities = getAreaControlEntities( - AREA_CONTROLS as unknown as AreaControl[], + AREA_CONTROL_DOMAINS as unknown as AreaControlDomain[], areaId, excludeEntities, this.hass! @@ -127,23 +108,133 @@ export class HuiAreaControlsCardFeatureEditor customize_controls: this._config.controls !== undefined, }; - const schema = this._schema( - this.hass.localize, - data.customize_controls, - supportedControls + const value = this._config.controls || []; + const excludeValues = value.map((control) => + typeof control === "string" ? control : control.entity_id ); return html` + ${data.customize_controls + ? html` + ${value.length + ? html` + + + ${repeat( + value, + (item) => + typeof item === "string" ? item : item.entity_id, + (item, idx) => { + const label = this._getItemLabel(item); + return html` + + + ${label} + + `; + } + )} + + + ` + : nothing} + + ` + : nothing} `; } + private _getItemLabel(item: AreaControl): string { + if (!this.hass) { + return typeof item === "string" ? item : JSON.stringify(item); + } + + if (typeof item === "string") { + if (AREA_CONTROL_DOMAINS.includes(item as AreaControlDomain)) { + return this.hass.localize( + `ui.panel.lovelace.editor.features.types.area-controls.controls_options.${item}` + ); + } + // Invalid/unknown domain string + return item; + } + + if ("entity_id" in item) { + const entityState = this.hass.states[item.entity_id]; + if (entityState) { + return computeStateName(entityState); + } + return item.entity_id; + } + + return JSON.stringify(item); + } + + private _itemMoved(ev: CustomEvent): void { + ev.stopPropagation(); + const { oldIndex, newIndex } = ev.detail; + const controls = [...(this._config!.controls || [])]; + const item = controls.splice(oldIndex, 1)[0]; + controls.splice(newIndex, 0, item); + this._updateControls(controls); + } + + private _removeItem(ev: CustomEvent): void { + const index = (ev.currentTarget as any).idx; + const controls = [...(this._config!.controls || [])]; + controls.splice(index, 1); + this._updateControls(controls); + } + + private _controlChanged(ev: CustomEvent): void { + ev.stopPropagation(); + const value = ev.detail.value; + if (!value) { + return; + } + // If it's a domain control (in AREA_CONTROL_DOMAINS), save as string for backwards compatibility + // If it's an entity, save in explicit format + const control = AREA_CONTROL_DOMAINS.includes(value as AreaControlDomain) + ? value + : { entity_id: value }; + const controls = [...(this._config!.controls || []), control]; + this._updateControls(controls); + } + + private _updateControls(controls: AreaControl[]): void { + const config = { ...this._config!, controls }; + fireEvent(this, "config-changed", { config }); + } + private _valueChanged(ev: CustomEvent): void { const { customize_controls, ...config } = ev.detail .value as AreaControlsCardFeatureData; @@ -166,10 +257,9 @@ export class HuiAreaControlsCardFeatureEditor } private _computeLabelCallback = ( - schema: SchemaUnion> + schema: SchemaUnion ) => { switch (schema.name) { - case "controls": case "customize_controls": return this.hass!.localize( `ui.panel.lovelace.editor.features.types.area-controls.${schema.name}` @@ -178,6 +268,16 @@ export class HuiAreaControlsCardFeatureEditor return ""; } }; + + static styles = css` + ha-sortable { + display: block; + margin-bottom: var(--ha-space-2); + } + ha-chip-set { + margin-bottom: var(--ha-space-2); + } + `; } declare global { diff --git a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts index e8aaea2ae0..117e510660 100644 --- a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts @@ -14,6 +14,7 @@ import type { HomeAssistant } from "../../../../types"; import { getEntityDefaultButtonAction } from "../../cards/hui-button-card"; import type { ButtonCardConfig } from "../../cards/types"; import type { LovelaceCardEditor } from "../../types"; +import { ACTION_RELATED_CONTEXT } from "../../components/hui-action-editor"; import { actionConfigStruct } from "../structs/action-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { entityNameStruct } from "../structs/entity-name-struct"; @@ -126,6 +127,7 @@ export class HuiButtonCardEditor default_action: getEntityDefaultButtonAction(entityId), }, }, + context: ACTION_RELATED_CONTEXT, }, { name: "hold_action", @@ -134,6 +136,7 @@ export class HuiButtonCardEditor default_action: "more-info", }, }, + context: ACTION_RELATED_CONTEXT, }, { name: "", @@ -147,6 +150,7 @@ export class HuiButtonCardEditor default_action: "none", }, }, + context: ACTION_RELATED_CONTEXT, }, ], }, diff --git a/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts b/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts index 4d149bf9cb..5ba511d34e 100644 --- a/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts @@ -1,3 +1,4 @@ +import "@home-assistant/webawesome/dist/components/divider/divider"; import { mdiDelete, mdiDragHorizontalVariant, @@ -8,10 +9,10 @@ import { LitElement, css, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import { repeat } from "lit/directives/repeat"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { stopPropagation } from "../../../../common/dom/stop_propagation"; import "../../../../components/ha-button"; +import "../../../../components/ha-dropdown"; +import "../../../../components/ha-dropdown-item"; import "../../../../components/ha-icon-button"; -import "../../../../components/ha-list-item"; import "../../../../components/ha-sortable"; import "../../../../components/ha-svg-icon"; import type { CustomCardFeatureEntry } from "../../../../data/lovelace_custom_cards"; @@ -24,6 +25,7 @@ import { import type { HomeAssistant } from "../../../../types"; import { supportsAlarmModesCardFeature } from "../../card-features/hui-alarm-modes-card-feature"; import { supportsAreaControlsCardFeature } from "../../card-features/hui-area-controls-card-feature"; +import { supportsBarGaugeCardFeature } from "../../card-features/hui-bar-gauge-card-feature"; import { supportsButtonCardFeature } from "../../card-features/hui-button-card-feature"; import { supportsClimateFanModesCardFeature } from "../../card-features/hui-climate-fan-modes-card-feature"; import { supportsClimateHvacModesCardFeature } from "../../card-features/hui-climate-hvac-modes-card-feature"; @@ -52,21 +54,21 @@ import { supportsMediaPlayerVolumeButtonsCardFeature } from "../../card-features import { supportsMediaPlayerVolumeSliderCardFeature } from "../../card-features/hui-media-player-volume-slider-card-feature"; import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature"; import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature"; -import { supportsTrendGraphCardFeature } from "../../card-features/hui-trend-graph-card-feature"; import { supportsTargetHumidityCardFeature } from "../../card-features/hui-target-humidity-card-feature"; import { supportsTargetTemperatureCardFeature } from "../../card-features/hui-target-temperature-card-feature"; import { supportsToggleCardFeature } from "../../card-features/hui-toggle-card-feature"; +import { supportsTrendGraphCardFeature } from "../../card-features/hui-trend-graph-card-feature"; import { supportsUpdateActionsCardFeature } from "../../card-features/hui-update-actions-card-feature"; import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature"; import { supportsValveOpenCloseCardFeature } from "../../card-features/hui-valve-open-close-card-feature"; import { supportsValvePositionCardFeature } from "../../card-features/hui-valve-position-card-feature"; -import { supportsBarGaugeCardFeature } from "../../card-features/hui-bar-gauge-card-feature"; import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature"; import type { LovelaceCardFeatureConfig, LovelaceCardFeatureContext, } from "../../card-features/types"; import { getCardFeatureElementClass } from "../../create-element/create-card-feature-element"; +import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown"; export type FeatureType = LovelaceCardFeatureConfig["type"]; @@ -404,45 +406,39 @@ export class HuiCardFeaturesEditor extends LitElement { ${supportedFeaturesType.length > 0 ? html` - + ${this.hass!.localize(`ui.panel.lovelace.editor.features.add`)} ${types.map( (type) => html` - + ${this._getFeatureTypeLabel(type)} - + ` )} ${types.length > 0 && customTypes.length > 0 - ? html`
  • ` + ? html`` : nothing} ${customTypes.map( (type) => html` - + ${this._getFeatureTypeLabel(type)} - + ` )} -
    + ` : nothing} `; } - private async _addFeature(ev: CustomEvent): Promise { - const index = ev.detail.index as number; - - if (index == null) return; - - const value = this._getSupportedFeaturesType()[index]; - if (!value) return; + private async _addFeature(ev: HaDropdownSelectEvent) { + const value = ev.detail.item.value as FeatureType; + if (!value) { + return; + } const elClass = await getCardFeatureElementClass(value); @@ -499,7 +495,9 @@ export class HuiCardFeaturesEditor extends LitElement { display: flex !important; flex-direction: column; } - ha-button-menu { + ha-dropdown { + display: inline-block; + align-self: flex-start; margin-top: var(--ha-space-2); } .feature { diff --git a/src/panels/lovelace/editor/config-elements/hui-clock-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-clock-card-editor.ts index b0078c754b..c02b94f4ce 100644 --- a/src/panels/lovelace/editor/config-elements/hui-clock-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-clock-card-editor.ts @@ -237,33 +237,13 @@ export class HuiClockCardEditor : []), ] as const satisfies readonly HaFormSchema[]) : []), - { - name: "time_zone", - selector: { - select: { - mode: "dropdown", - options: [ - [ - "auto", - localize( - `ui.panel.lovelace.editor.card.clock.time_zones.auto` - ), - ], - ...Object.entries(timezones as Record), - ].map(([key, value]) => ({ - value: key, - label: value, - })), - }, - }, - }, + { name: "time_zone", selector: { timezone: {} } }, ] as const satisfies readonly HaFormSchema[] ); private _data = memoizeOne((config) => ({ clock_style: "digital", clock_size: "small", - time_zone: "auto", time_format: "auto", show_seconds: false, no_background: false, @@ -302,9 +282,6 @@ export class HuiClockCardEditor } private _valueChanged(ev: CustomEvent): void { - if (ev.detail.value.time_zone === "auto") { - delete ev.detail.value.time_zone; - } if (ev.detail.value.time_format === "auto") { delete ev.detail.value.time_format; } diff --git a/src/panels/lovelace/editor/config-elements/hui-distribution-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-distribution-card-editor.ts new file mode 100644 index 0000000000..cdaae02cc5 --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/hui-distribution-card-editor.ts @@ -0,0 +1,218 @@ +import { html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { + array, + assert, + assign, + object, + optional, + string, + union, +} from "superstruct"; +import type { HASSDomEvent } from "../../../../common/dom/fire_event"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { computeDomain } from "../../../../common/entity/compute_domain"; +import "../../../../components/ha-form/ha-form"; +import type { SchemaUnion } from "../../../../components/ha-form/types"; +import type { HaEntityPickerEntityFilterFunc } from "../../../../data/entity/entity"; +import type { HomeAssistant } from "../../../../types"; +import type { DistributionCardConfig } from "../../cards/types"; +import "../../components/hui-entity-editor"; +import type { EntityConfig } from "../../entity-rows/types"; +import type { LovelaceCardEditor } from "../../types"; +import "../hui-sub-element-editor"; +import { processEditorEntities } from "../process-editor-entities"; +import { baseLovelaceCardConfig } from "../structs/base-card-struct"; +import { entityNameStruct } from "../structs/entity-name-struct"; +import type { EditDetailElementEvent, SubElementEditorConfig } from "../types"; + +const distributionEntityConfigStruct = object({ + entity: string(), + name: optional(entityNameStruct), + color: optional(string()), +}); + +const cardConfigStruct = assign( + baseLovelaceCardConfig, + object({ + title: optional(string()), + entities: array(union([string(), distributionEntityConfigStruct])), + }) +); + +const SUB_SCHEMA = [ + { name: "entity", selector: { entity: {} }, required: true }, + { + name: "name", + selector: { entity_name: {} }, + context: { + entity: "entity", + }, + }, + { + name: "color", + selector: { ui_color: {} }, + }, +] as const; + +const SCHEMA = [{ name: "title", selector: { text: {} } }] as const; + +@customElement("hui-distribution-card-editor") +export class HuiDistributionCardEditor + extends LitElement + implements LovelaceCardEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: DistributionCardConfig; + + @state() private _subElementEditorConfig?: SubElementEditorConfig; + + @state() private _configEntities?: EntityConfig[]; + + public setConfig(config: DistributionCardConfig): void { + assert(config, cardConfigStruct); + this._config = config; + this._configEntities = processEditorEntities(config.entities); + } + + private _schema = memoizeOne(() => SCHEMA); + + private _entityFilter = memoizeOne( + ( + entities: EntityConfig[] | undefined, + hass: HomeAssistant + ): HaEntityPickerEntityFilterFunc | undefined => { + // No filtering if no entities yet + if (!entities || entities.length === 0) { + return undefined; + } + + // Get the domain and device_class of the first entity + const firstEntityState = hass.states[entities[0].entity]; + if (!firstEntityState) { + return undefined; + } + + const targetDomain = computeDomain(entities[0].entity); + // Default to "none" if no device_class (Home Assistant pattern) + const targetDeviceClass = + firstEntityState.attributes.device_class || "none"; + + // Create set of already selected entity IDs for fast lookup + const selectedEntityIds = new Set(entities.map((e) => e.entity)); + + // Return filter function that only allows entities with matching domain and device_class + // and excludes already selected entities + return (entity) => { + const entityDomain = computeDomain(entity.entity_id); + const entityDeviceClass = entity.attributes.device_class || "none"; + return ( + entityDomain === targetDomain && + entityDeviceClass === targetDeviceClass && + !selectedEntityIds.has(entity.entity_id) + ); + }; + } + ); + + protected render() { + if (!this.hass || !this._config) { + return nothing; + } + + if (this._subElementEditorConfig) { + return html` + + + `; + } + + const schema = this._schema(); + const entityFilter = this._entityFilter(this._configEntities, this.hass); + + return html` + + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } + + private _entitiesChanged(ev: CustomEvent): void { + const config = { ...this._config!, entities: ev.detail.entities }; + this._configEntities = processEditorEntities(config.entities); + fireEvent(this, "config-changed", { config }); + } + + private _editDetailElement(ev: HASSDomEvent): void { + this._subElementEditorConfig = ev.detail.subElementConfig; + } + + private _goBack(): void { + this._subElementEditorConfig = undefined; + } + + private _handleSubElementChanged(ev: CustomEvent): void { + ev.stopPropagation(); + + const index = this._subElementEditorConfig!.index!; + + const newEntities = this._configEntities!.concat(); + const newConfig = ev.detail.config as EntityConfig; + this._subElementEditorConfig = { + ...this._subElementEditorConfig!, + elementConfig: newConfig, + }; + newEntities[index] = newConfig; + let config = this._config!; + config = { ...config, entities: newEntities }; + this._config = config; + this._configEntities = processEditorEntities(config.entities); + + fireEvent(this, "config-changed", { config }); + } + + private _computeLabelCallback = ( + schema: SchemaUnion> + ) => { + switch (schema.name) { + case "title": + return `${this.hass!.localize( + "ui.panel.lovelace.editor.card.generic.title" + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + default: + return ""; + } + }; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-distribution-card-editor": HuiDistributionCardEditor; + } +} diff --git a/src/panels/lovelace/editor/config-elements/hui-empty-state-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-empty-state-card-editor.ts index e42ea6c46b..0b74aa2db5 100644 --- a/src/panels/lovelace/editor/config-elements/hui-empty-state-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-empty-state-card-editor.ts @@ -1,8 +1,16 @@ -import { mdiGestureTap } from "@mdi/js"; import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; -import { assert, assign, boolean, object, optional, string } from "superstruct"; +import { + array, + assert, + assign, + boolean, + enums, + object, + optional, + string, +} from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; import type { LocalizeFunc } from "../../../../common/translations/localize"; import "../../../../components/ha-form/ha-form"; @@ -16,15 +24,25 @@ import type { LovelaceCardEditor } from "../../types"; import { actionConfigStruct } from "../structs/action-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; +const buttonStruct = object({ + text: string(), + icon: optional(string()), + appearance: optional(enums(["accent", "filled", "outlined", "plain"])), + variant: optional( + enums(["brand", "neutral", "success", "warning", "danger"]) + ), + tap_action: actionConfigStruct, +}); + const cardConfigStruct = assign( baseLovelaceCardConfig, object({ content_only: optional(boolean()), icon: optional(string()), + icon_color: optional(string()), title: optional(string()), content: optional(string()), - action_button_text: optional(string()), - tap_action: optional(actionConfigStruct), + buttons: optional(array(buttonStruct)), }) ); @@ -70,24 +88,66 @@ export class HuiEmptyStateCardEditor }, }, { name: "icon", selector: { icon: {} } }, + { + name: "icon_color", + selector: { + ui_color: {}, + }, + }, { name: "title", selector: { text: {} } }, { name: "content", selector: { text: { multiline: true } } }, { - name: "interactions", - type: "expandable", - flatten: true, - iconPath: mdiGestureTap, - schema: [ - { name: "action_button_text", selector: { text: {} } }, - { - name: "tap_action", - selector: { - ui_action: { - default_action: "none", + name: "buttons", + selector: { + object: { + multiple: true, + label_field: "text", + fields: { + text: { + selector: { text: {} }, + required: true, + }, + icon: { + selector: { icon: {} }, + }, + appearance: { + selector: { + select: { + options: [ + { value: "accent", label: "Accent" }, + { value: "filled", label: "Filled" }, + { value: "outlined", label: "Outlined" }, + { value: "plain", label: "Plain" }, + ], + mode: "dropdown", + }, + }, + }, + variant: { + selector: { + select: { + options: [ + { value: "brand", label: "Brand" }, + { value: "neutral", label: "Neutral" }, + { value: "success", label: "Success" }, + { value: "warning", label: "Warning" }, + { value: "danger", label: "Danger" }, + ], + mode: "dropdown", + }, + }, + }, + tap_action: { + selector: { + ui_action: { + default_action: "none", + }, + }, + required: true, }, }, }, - ], + }, }, ] as const satisfies readonly HaFormSchema[] ); @@ -134,7 +194,7 @@ export class HuiEmptyStateCardEditor switch (schema.name) { case "style": case "content": - case "action_button_text": + case "buttons": return this.hass!.localize( `ui.panel.lovelace.editor.card.empty_state.${schema.name}` ); diff --git a/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts index 6c57c9177c..9042116508 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entity-badge-editor.ts @@ -28,6 +28,7 @@ import { } from "../../badges/hui-entity-badge"; import type { EntityBadgeConfig } from "../../badges/types"; import type { LovelaceBadgeEditor } from "../../types"; +import { ACTION_RELATED_CONTEXT } from "../../components/hui-action-editor"; import "../hui-sub-element-editor"; import { actionConfigStruct } from "../structs/action-struct"; import { baseLovelaceBadgeConfig } from "../structs/base-badge-struct"; @@ -172,6 +173,7 @@ export class HuiEntityBadgeEditor default_action: "more-info", }, }, + context: ACTION_RELATED_CONTEXT, }, { name: "", @@ -185,6 +187,7 @@ export class HuiEntityBadgeEditor default_action: "none" as const, }, }, + context: ACTION_RELATED_CONTEXT, }) ), }, diff --git a/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts index 98a816ef4c..600c6b8434 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts @@ -1,10 +1,26 @@ +import { mdiGestureTap } from "@mdi/js"; import { assert, assign, boolean, object, optional, string } from "superstruct"; import type { LocalizeFunc } from "../../../../common/translations/localize"; import type { HaFormSchema } from "../../../../components/ha-form/types"; import { headerFooterConfigStructs } from "../../header-footer/structs"; import type { LovelaceConfigForm } from "../../types"; +import { actionConfigStruct } from "../structs/action-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { entityNameStruct } from "../structs/entity-name-struct"; +import { + type UiAction, + ACTION_RELATED_CONTEXT, + supportedActions, +} from "../../components/hui-action-editor"; + +const TAP_ACTIONS: UiAction[] = [ + "more-info", + "navigate", + "url", + "perform-action", + "assist", + "none", +]; const struct = assign( baseLovelaceCardConfig, @@ -16,6 +32,11 @@ const struct = assign( unit: optional(string()), theme: optional(string()), state_color: optional(boolean()), + tap_action: optional(supportedActions(actionConfigStruct, TAP_ACTIONS)), + hold_action: optional(supportedActions(actionConfigStruct, TAP_ACTIONS)), + double_tap_action: optional( + supportedActions(actionConfigStruct, TAP_ACTIONS) + ), footer: optional(headerFooterConfigStructs), }) ); @@ -56,18 +77,63 @@ const SCHEMA = [ { name: "state_color", selector: { boolean: {} } }, ], }, + { + name: "interactions", + type: "expandable", + flatten: true, + iconPath: mdiGestureTap, + schema: [ + { + name: "tap_action", + selector: { + ui_action: { + actions: TAP_ACTIONS, + default_action: "more-info", + }, + }, + context: ACTION_RELATED_CONTEXT, + }, + { + name: "", + type: "optional_actions", + flatten: true, + schema: (["hold_action", "double_tap_action"] as const).map( + (action) => ({ + name: action, + selector: { + ui_action: { + actions: TAP_ACTIONS, + default_action: "none" as const, + }, + }, + context: ACTION_RELATED_CONTEXT, + }) + ), + }, + ], + }, ] as HaFormSchema[]; const entityCardConfigForm: LovelaceConfigForm = { schema: SCHEMA, assertConfig: (config) => assert(config, struct), computeLabel: (schema: HaFormSchema, localize: LocalizeFunc) => { - if (schema.name === "theme") { - return `${localize( - "ui.panel.lovelace.editor.card.generic.theme" - )} (${localize("ui.panel.lovelace.editor.card.config.optional")})`; + switch (schema.name) { + case "theme": + return `${localize( + "ui.panel.lovelace.editor.card.generic.theme" + )} (${localize("ui.panel.lovelace.editor.card.config.optional")})`; + case "interactions": + return localize("ui.panel.lovelace.editor.card.generic.interactions"); + case "tap_action": + case "hold_action": + case "double_tap_action": + return `${localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + )} (${localize("ui.panel.lovelace.editor.card.config.optional")})`; + default: + return localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); } - return localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`); }, }; diff --git a/src/panels/lovelace/editor/config-elements/hui-gauge-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-gauge-card-editor.ts index 91ea146be2..b71649327d 100644 --- a/src/panels/lovelace/editor/config-elements/hui-gauge-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-gauge-card-editor.ts @@ -10,7 +10,6 @@ import { number, object, optional, - refine, string, } from "superstruct"; import { fireEvent } from "../../../../common/dom/fire_event"; @@ -20,7 +19,11 @@ import { NON_NUMERIC_ATTRIBUTES } from "../../../../data/entity/entity_attribute import type { HomeAssistant } from "../../../../types"; import { DEFAULT_MAX, DEFAULT_MIN } from "../../cards/hui-gauge-card"; import type { GaugeCardConfig } from "../../cards/types"; -import type { UiAction } from "../../components/hui-action-editor"; +import { + ACTION_RELATED_CONTEXT, + type UiAction, + supportedActions, +} from "../../components/hui-action-editor"; import type { LovelaceCardEditor } from "../../types"; import { actionConfigStruct } from "../structs/action-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; @@ -54,13 +57,11 @@ const cardConfigStruct = assign( theme: optional(string()), needle: optional(boolean()), segments: optional(array(gaugeSegmentStruct)), - tap_action: optional( - refine(actionConfigStruct, TAP_ACTIONS.toString(), (value) => - TAP_ACTIONS.includes(value.action) - ) + tap_action: optional(supportedActions(actionConfigStruct, TAP_ACTIONS)), + hold_action: optional(supportedActions(actionConfigStruct, TAP_ACTIONS)), + double_tap_action: optional( + supportedActions(actionConfigStruct, TAP_ACTIONS) ), - hold_action: optional(actionConfigStruct), - double_tap_action: optional(actionConfigStruct), }) ); @@ -167,6 +168,7 @@ export class HuiGaugeCardEditor default_action: "more-info", }, }, + context: ACTION_RELATED_CONTEXT, }, { name: "", @@ -181,6 +183,7 @@ export class HuiGaugeCardEditor default_action: "none" as const, }, }, + context: ACTION_RELATED_CONTEXT, }) ), }, diff --git a/src/panels/lovelace/editor/config-elements/hui-generic-entity-row-editor.ts b/src/panels/lovelace/editor/config-elements/hui-generic-entity-row-editor.ts index 79b9d95999..3d658a775b 100644 --- a/src/panels/lovelace/editor/config-elements/hui-generic-entity-row-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-generic-entity-row-editor.ts @@ -18,6 +18,7 @@ const SECONDARY_INFO_VALUES = { "last-changed": {}, "last-updated": {}, "last-triggered": { domains: ["automation", "script"] }, + area: {}, position: { domains: ["cover"] }, state: {}, "tilt-position": { domains: ["cover"] }, diff --git a/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts index 641ed7a96a..3dc9cf42a8 100644 --- a/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts @@ -20,6 +20,7 @@ import type { ConfigEntity, GlanceCardConfig } from "../../cards/types"; import "../../components/hui-entity-editor"; import type { EntityConfig } from "../../entity-rows/types"; import type { LovelaceCardEditor } from "../../types"; +import { ACTION_RELATED_CONTEXT } from "../../components/hui-action-editor"; import "../hui-sub-element-editor"; import { processEditorEntities } from "../process-editor-entities"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; @@ -73,6 +74,7 @@ const SUB_SCHEMA = [ default_action: "more-info", }, }, + context: ACTION_RELATED_CONTEXT, }, { name: "", @@ -85,6 +87,7 @@ const SUB_SCHEMA = [ default_action: "none" as const, }, }, + context: ACTION_RELATED_CONTEXT, })), }, ] as const; diff --git a/src/panels/lovelace/editor/config-elements/hui-heading-badges-editor.ts b/src/panels/lovelace/editor/config-elements/hui-heading-badges-editor.ts index 4054d87ea0..3f0d043928 100644 --- a/src/panels/lovelace/editor/config-elements/hui-heading-badges-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-heading-badges-editor.ts @@ -1,21 +1,33 @@ -import "@material/mwc-menu/mwc-menu-surface"; -import { mdiDelete, mdiDragHorizontalVariant, mdiPencil } from "@mdi/js"; +import { + mdiDelete, + mdiDragHorizontalVariant, + mdiPencil, + mdiPlus, +} from "@mdi/js"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; import { repeat } from "lit/directives/repeat"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { preventDefault } from "../../../../common/dom/prevent_default"; -import "../../../../components/entity/ha-entity-picker"; -import type { HaEntityPicker } from "../../../../components/entity/ha-entity-picker"; +import { computeEntityNameList } from "../../../../common/entity/compute_entity_name_display"; +import { computeRTL } from "../../../../common/util/compute_rtl"; +import { nextRender } from "../../../../common/util/render-status"; import "../../../../components/ha-button"; +import "../../../../components/ha-dropdown"; +import "../../../../components/ha-dropdown-item"; import "../../../../components/ha-icon-button"; import "../../../../components/ha-sortable"; import "../../../../components/ha-svg-icon"; import type { HomeAssistant } from "../../../../types"; +import { getHeadingBadgeElementClass } from "../../create-element/create-heading-badge-element"; import type { + ButtonHeadingBadgeConfig, EntityHeadingBadgeConfig, LovelaceHeadingBadgeConfig, } from "../../heading-badges/types"; +import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown"; + +const UI_BADGE_TYPES = ["entity", "button"] as const; declare global { interface HASSDomEvents { @@ -41,8 +53,12 @@ export class HuiHeadingBadgesEditor extends LitElement { return this._badgesKeys.get(badge)!; } - private _createValueChangedHandler(index: number) { - return (ev: CustomEvent) => this._valueChanged(ev, index); + private _getBadgeTypeLabel(type: string): string { + return ( + this.hass.localize( + `ui.panel.lovelace.editor.heading-badges.types.${type}.label` + ) || type + ); } protected render() { @@ -51,120 +67,180 @@ export class HuiHeadingBadgesEditor extends LitElement { } return html` - ${this.badges + ${this.badges?.length ? html` -
    +
    ${repeat( - this.badges, + this.badges.filter(Boolean), (badge) => this._getKey(badge), - (badge, index) => { - const type = badge.type ?? "entity"; - const isEntityBadge = - type === "entity" && "entity" in badge; - const entityBadge = isEntityBadge - ? (badge as EntityHeadingBadgeConfig) - : undefined; - return html` -
    -
    - -
    - ${isEntityBadge && entityBadge - ? html` - - ` - : html` -
    - ${type} -
    - `} - - -
    - `; - } + (badge, index) => this._renderBadgeItem(badge, index) )}
    ` : nothing} -
    - + + + + ${this.hass.localize(`ui.panel.lovelace.editor.heading-badges.add`)} + + ${UI_BADGE_TYPES.map( + (type) => html` + + ${this._getBadgeTypeLabel(type)} + + ` + )} + + `; + } + + private _renderBadgeItem(badge: LovelaceHeadingBadgeConfig, index: number) { + const type = badge.type ?? "entity"; + const entityBadge = badge as EntityHeadingBadgeConfig; + const isWarning = + type === "entity" && + (!entityBadge.entity || !this.hass.states[entityBadge.entity]); + + return html` +
    +
    + +
    + ${type === "entity" + ? this._renderEntityBadge(entityBadge) + : type === "button" + ? this._renderButtonBadge(badge as ButtonHeadingBadgeConfig) + : this._renderUnknownBadge(type)} + +
    `; } - private _entityPicked(ev: CustomEvent): void { - ev.stopPropagation(); - if (!ev.detail.value) { - return; + private _renderEntityBadge(badge: EntityHeadingBadgeConfig) { + const entityId = badge.entity; + const stateObj = entityId ? this.hass.states[entityId] : undefined; + + if (!entityId) { + return html` +
    +
    + ${this._getBadgeTypeLabel("entity")} + ${this.hass.localize( + "ui.panel.lovelace.editor.heading-badges.no_entity" + )} +
    +
    + `; } - const newEntity: LovelaceHeadingBadgeConfig = { - type: "entity", - entity: ev.detail.value, - }; - const newBadges = [...(this.badges || []), newEntity]; - (ev.target as HaEntityPicker).value = undefined; - fireEvent(this, "heading-badges-changed", { badges: newBadges }); + + if (!stateObj) { + return html` +
    +
    + ${entityId} + ${this.hass.localize( + "ui.panel.lovelace.editor.heading-badges.entity_not_found" + )} +
    +
    + `; + } + + const [entityName, deviceName, areaName] = computeEntityNameList( + stateObj, + [{ type: "entity" }, { type: "device" }, { type: "area" }], + this.hass.entities, + this.hass.devices, + this.hass.areas, + this.hass.floors + ); + + const isRTL = computeRTL(this.hass); + + const primary = entityName || deviceName || entityId; + const secondary = [entityName ? deviceName : undefined, areaName] + .filter(Boolean) + .join(isRTL ? " ◂ " : " ▸ "); + + return html` +
    +
    + ${primary} + ${secondary + ? html`${secondary}` + : nothing} +
    +
    + `; } - private _valueChanged(ev: CustomEvent, index: number): void { - ev.stopPropagation(); - const value = ev.detail.value; - const newBadges = [...(this.badges || [])]; + private _renderButtonBadge(badge: ButtonHeadingBadgeConfig) { + return html` +
    +
    + ${this._getBadgeTypeLabel("button")} + ${badge.text + ? html`${badge.text}` + : nothing} +
    +
    + `; + } - if (!value) { - newBadges.splice(index, 1); - } else { - newBadges[index] = { - ...newBadges[index], - entity: value, - }; + private _renderUnknownBadge(type: string) { + return html` +
    +
    + ${type} +
    +
    + `; + } + + private async _addBadge(ev: HaDropdownSelectEvent) { + const type = ev.detail.item.value; + if (!type) { + return; } + const elClass = await getHeadingBadgeElementClass(type); + + let newBadge: LovelaceHeadingBadgeConfig; + if (elClass && elClass.getStubConfig) { + newBadge = elClass.getStubConfig(this.hass); + } else { + newBadge = { type } as LovelaceHeadingBadgeConfig; + } + + const newBadges = [...(this.badges || []), newBadge]; + fireEvent(this, "heading-badges-changed", { badges: newBadges }); + + await nextRender(); + // Open the editor for the new badge + fireEvent(this, "edit-heading-badge", { index: newBadges.length - 1 }); } private _badgeMoved(ev: CustomEvent): void { @@ -177,7 +253,7 @@ export class HuiHeadingBadgesEditor extends LitElement { fireEvent(this, "heading-badges-changed", { badges: newBadges }); } - private _removeEntity(ev: CustomEvent): void { + private _removeBadge(ev: CustomEvent): void { const index = (ev.currentTarget as any).index; const newBadges = [...(this.badges || [])]; @@ -198,11 +274,14 @@ export class HuiHeadingBadgesEditor extends LitElement { display: flex !important; flex-direction: column; } - ha-button { + + ha-dropdown { + display: inline-block; + align-self: flex-start; margin-top: var(--ha-space-2); } - .entities { + .badges { display: flex; flex-direction: column; gap: var(--ha-space-2); @@ -212,6 +291,7 @@ export class HuiHeadingBadgesEditor extends LitElement { display: flex; align-items: center; } + .badge .handle { cursor: move; /* fallback if grab cursor is unsupported */ cursor: grab; @@ -220,13 +300,14 @@ export class HuiHeadingBadgesEditor extends LitElement { padding-inline-start: initial; direction: var(--direction); } + .badge .handle > * { pointer-events: none; } .badge-content { - height: 60px; - font-size: var(--ha-font-size-l); + height: var(--ha-space-12); + font-size: var(--ha-font-size-m); display: flex; align-items: center; justify-content: space-between; @@ -238,15 +319,9 @@ export class HuiHeadingBadgesEditor extends LitElement { flex-direction: column; } - .badge ha-entity-picker { - flex-grow: 1; - min-width: 0; - margin-top: 0; - } - .remove-icon, .edit-icon { - --mdc-icon-button-size: 36px; + --mdc-icon-button-size: var(--ha-space-9); color: var(--secondary-text-color); } @@ -255,24 +330,19 @@ export class HuiHeadingBadgesEditor extends LitElement { color: var(--secondary-text-color); } + .badge.warning { + background-color: var(--ha-color-fill-warning-quiet-resting); + border-radius: var(--ha-border-radius-sm); + overflow: hidden; + } + + .badge.warning .secondary { + color: var(--ha-color-on-warning-normal); + } + li[divider] { border-bottom-color: var(--divider-color); } - - .add-container { - position: relative; - width: 100%; - margin-top: var(--ha-space-2); - } - - mwc-menu-surface { - --mdc-menu-min-width: 100%; - } - - ha-entity-picker { - display: block; - width: 100%; - } `; } diff --git a/src/panels/lovelace/editor/config-elements/hui-heading-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-heading-card-editor.ts index 92f72e1b99..4ffae80e8f 100644 --- a/src/panels/lovelace/editor/config-elements/hui-heading-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-heading-card-editor.ts @@ -25,7 +25,10 @@ import "../../../../components/ha-svg-icon"; import type { HomeAssistant } from "../../../../types"; import { migrateHeadingCardConfig } from "../../cards/hui-heading-card"; import type { HeadingCardConfig } from "../../cards/types"; -import type { UiAction } from "../../components/hui-action-editor"; +import { + ACTION_RELATED_CONTEXT, + type UiAction, +} from "../../components/hui-action-editor"; import type { EntityHeadingBadgeConfig, LovelaceHeadingBadgeConfig, @@ -102,6 +105,7 @@ export class HuiHeadingCardEditor actions, }, }, + context: ACTION_RELATED_CONTEXT, }, ], }, @@ -139,9 +143,7 @@ export class HuiHeadingCardEditor

    - ${this.hass!.localize( - "ui.panel.lovelace.editor.card.heading.entities" - )} + ${this.hass!.localize("ui.panel.lovelace.editor.card.heading.badges")}

    ${this._params.title}` : nothing} - - ${this.hass!.localize( `ui.panel.lovelace.editor.edit_view.edit_${!this._GUImode ? "ui" : "yaml"}` )} - - - + + + ${this.hass.localize( "ui.panel.lovelace.editor.strategy-editor.take_control" )} - - - + + +
    + [ + { + name: "text", + selector: { text: {} }, + }, + { + name: "", + type: "grid", + schema: [ + { + name: "icon", + selector: { icon: {} }, + }, + { + name: "color", + selector: { + ui_color: {}, + }, + }, + ], + }, + { + name: "interactions", + type: "expandable", + flatten: true, + iconPath: mdiGestureTap, + schema: [ + { + name: "tap_action", + selector: { + ui_action: { + default_action: "none", + }, + }, + }, + { + name: "", + type: "optional_actions", + flatten: true, + schema: (["hold_action", "double_tap_action"] as const).map( + (action) => ({ + name: action, + selector: { + ui_action: { + default_action: "none" as const, + }, + }, + }) + ), + }, + ], + }, + ] as const satisfies readonly HaFormSchema[] + ); + + protected render() { + if (!this.hass || !this._config) { + return nothing; + } + + const schema = this._schema(); + + const conditions = this._config.visibility ?? []; + + return html` + + + +

    + ${this.hass!.localize( + "ui.panel.lovelace.editor.card.heading.button_config.visibility" + )} +

    +
    +

    + ${this.hass.localize( + "ui.panel.lovelace.editor.card.heading.button_config.visibility_explanation" + )} +

    + + +
    +
    + `; + } + + private _valueChanged(ev: CustomEvent): void { + ev.stopPropagation(); + if (!this._config || !this.hass) { + return; + } + + const config = ev.detail.value as ButtonHeadingBadgeConfig; + + fireEvent(this, "config-changed", { config }); + } + + private _conditionChanged(ev: CustomEvent): void { + ev.stopPropagation(); + if (!this._config || !this.hass) { + return; + } + + const conditions = ev.detail.value as Condition[]; + + const newConfig: ButtonHeadingBadgeConfig = { + ...this._config, + visibility: conditions, + }; + if (newConfig.visibility?.length === 0) { + delete newConfig.visibility; + } + + fireEvent(this, "config-changed", { config: newConfig }); + } + + private _computeLabelCallback = ( + schema: SchemaUnion> + ) => { + switch (schema.name) { + case "text": + case "color": + return this.hass!.localize( + `ui.panel.lovelace.editor.card.heading.button_config.${schema.name}` + ); + default: + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + } + }; + + static get styles() { + return [ + configElementStyle, + css` + .container { + display: flex; + flex-direction: column; + } + ha-form { + display: block; + margin-bottom: 24px; + } + .intro { + margin: 0; + color: var(--secondary-text-color); + margin-bottom: 8px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-button-heading-badge-editor": HuiButtonHeadingBadgeEditor; + } +} diff --git a/src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts b/src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts index 211c26aff5..5d5bcb749a 100644 --- a/src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts +++ b/src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts @@ -24,6 +24,7 @@ import type { HomeAssistant } from "../../../../types"; import type { Condition } from "../../common/validate-condition"; import type { EntityHeadingBadgeConfig } from "../../heading-badges/types"; import type { LovelaceGenericElementEditor } from "../../types"; +import { ACTION_RELATED_CONTEXT } from "../../components/hui-action-editor"; import "../conditions/ha-card-conditions-editor"; import { configElementStyle } from "../config-elements/config-elements-style"; import { actionConfigStruct } from "../structs/action-struct"; @@ -157,6 +158,7 @@ export class HuiHeadingEntityEditor default_action: "none", }, }, + context: ACTION_RELATED_CONTEXT, }, { name: "", @@ -170,6 +172,7 @@ export class HuiHeadingEntityEditor default_action: "none" as const, }, }, + context: ACTION_RELATED_CONTEXT, }) ), }, diff --git a/src/panels/lovelace/editor/hui-entities-card-row-editor.ts b/src/panels/lovelace/editor/hui-entities-card-row-editor.ts index 1762838a6a..3aa39a83c9 100644 --- a/src/panels/lovelace/editor/hui-entities-card-row-editor.ts +++ b/src/panels/lovelace/editor/hui-entities-card-row-editor.ts @@ -181,7 +181,10 @@ export class HuiEntitiesCardRowEditor extends LitElement { static styles = css` ha-entity-picker { - margin-top: 8px; + margin-top: var(--ha-space-2); + } + ha-sortable ha-entity-picker { + margin-top: 0; } .add-entity { display: block; @@ -194,6 +197,7 @@ export class HuiEntitiesCardRowEditor extends LitElement { .entity { display: flex; align-items: center; + margin-top: var(--ha-space-2); } .entity .handle { diff --git a/src/panels/lovelace/editor/hui-picture-elements-card-row-editor.ts b/src/panels/lovelace/editor/hui-picture-elements-card-row-editor.ts index 0db7f1e74e..d285cf51e0 100644 --- a/src/panels/lovelace/editor/hui-picture-elements-card-row-editor.ts +++ b/src/panels/lovelace/editor/hui-picture-elements-card-row-editor.ts @@ -1,13 +1,18 @@ -import { mdiClose, mdiContentDuplicate, mdiPencil } from "@mdi/js"; +import { + mdiClose, + mdiContentDuplicate, + mdiPencil, + mdiPlaylistPlus, +} from "@mdi/js"; import deepClone from "deep-clone-simple"; import { LitElement, css, html, nothing } from "lit"; -import { customElement, property, query } from "lit/decorators"; +import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; +import "../../../components/ha-button"; +import "../../../components/ha-dropdown"; +import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; +import "../../../components/ha-dropdown-item"; import "../../../components/ha-icon-button"; -import "../../../components/ha-list-item"; -import "../../../components/ha-select"; -import type { HaSelect } from "../../../components/ha-select"; import "../../../components/ha-svg-icon"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import type { HomeAssistant } from "../../../types"; @@ -47,8 +52,6 @@ export class HuiPictureElementsCardRowEditor extends LitElement { @property({ attribute: false }) public elements?: LovelaceElementConfig[]; - @query("ha-select") private _select!: HaSelect; - protected render() { if (!this.elements || !this.hass) { return nothing; @@ -104,26 +107,23 @@ export class HuiPictureElementsCardRowEditor extends LitElement {
    ` )} - + + + + ${this.hass.localize( + "ui.panel.lovelace.editor.card.picture-elements.new_element" + )} + ${elementTypes.map( (element) => html` - ${this.hass?.localize( + + ${this.hass?.localize( `ui.panel.lovelace.editor.card.picture-elements.element_types.${element}` - )} + ) || element} + ` )} - +
    `; } @@ -177,8 +177,8 @@ export class HuiPictureElementsCardRowEditor extends LitElement { return element.title ?? "Unknown type"; } - private async _addElement(ev): Promise { - const value = ev.target!.value; + private async _addElement(ev: HaDropdownSelectEvent): Promise { + const value = ev.detail.item.value; if (value === "") { return; } @@ -191,7 +191,6 @@ export class HuiPictureElementsCardRowEditor extends LitElement { ) ); fireEvent(this, "elements-changed", { elements: newElements }); - this._select.select(-1); } private _removeRow(ev: CustomEvent): void { @@ -269,10 +268,6 @@ export class HuiPictureElementsCardRowEditor extends LitElement { font-size: var(--ha-font-size-s); color: var(--secondary-text-color); } - - ha-select { - width: 100%; - } `; } diff --git a/src/panels/lovelace/editor/lovelace-cards.ts b/src/panels/lovelace/editor/lovelace-cards.ts index ed04beb7f6..c9c2cbb685 100644 --- a/src/panels/lovelace/editor/lovelace-cards.ts +++ b/src/panels/lovelace/editor/lovelace-cards.ts @@ -37,6 +37,10 @@ export const coreCards: Card[] = [ type: "history-graph", showElement: true, }, + { + type: "distribution", + showElement: true, + }, { type: "statistics-graph", showElement: false, diff --git a/src/panels/lovelace/editor/section-editor/hui-dialog-edit-section.ts b/src/panels/lovelace/editor/section-editor/hui-dialog-edit-section.ts index c4b78c4767..c4c53db097 100644 --- a/src/panels/lovelace/editor/section-editor/hui-dialog-edit-section.ts +++ b/src/panels/lovelace/editor/section-editor/hui-dialog-edit-section.ts @@ -1,4 +1,3 @@ -import type { ActionDetail } from "@material/mwc-list"; import { mdiClose, mdiDotsVertical, @@ -12,11 +11,11 @@ import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../../../common/dom/fire_event"; import { stopPropagation } from "../../../../common/dom/stop_propagation"; import "../../../../components/ha-button"; -import "../../../../components/ha-button-menu"; import "../../../../components/ha-dialog"; import "../../../../components/ha-dialog-header"; +import "../../../../components/ha-dropdown"; +import "../../../../components/ha-dropdown-item"; import "../../../../components/ha-icon-button"; -import "../../../../components/ha-list-item"; import "../../../../components/ha-tab-group"; import "../../../../components/ha-tab-group-tab"; import "../../../../components/ha-yaml-editor"; @@ -45,6 +44,7 @@ import { showSelectViewDialog } from "../select-view/show-select-view-dialog"; import "./hui-section-settings-editor"; import "./hui-section-visibility-editor"; import type { EditSectionDialogParams } from "./show-edit-section-dialog"; +import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown"; const TABS = ["tab-settings", "tab-visibility"] as const; @@ -165,38 +165,33 @@ export class HuiDialogEditSection .path=${mdiClose} > ${heading} - - + + ${this.hass.localize( `ui.panel.lovelace.editor.edit_view.edit_${!this._yamlMode ? "yaml" : "ui"}` )} + + - - ${this.hass!.localize( "ui.panel.lovelace.editor.edit_view.move_to_view" )} - - - + + ${!this._yamlMode ? html` @@ -246,14 +241,13 @@ export class HuiDialogEditSection this._currTab = newTab; } - private async _handleAction(ev: CustomEvent) { - ev.stopPropagation(); - ev.preventDefault(); - switch (ev.detail.index) { - case 0: + private async _handleAction(ev: HaDropdownSelectEvent) { + const value = ev.detail.item.value; + switch (value) { + case "toggle-yaml": this._yamlMode = !this._yamlMode; break; - case 1: + case "move-to-view": this._openSelectView(); break; } diff --git a/src/panels/lovelace/editor/select-view/hui-dialog-select-view.ts b/src/panels/lovelace/editor/select-view/hui-dialog-select-view.ts index 5acea78670..f48378cf31 100644 --- a/src/panels/lovelace/editor/select-view/hui-dialog-select-view.ts +++ b/src/panels/lovelace/editor/select-view/hui-dialog-select-view.ts @@ -2,13 +2,11 @@ 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 { stopPropagation } from "../../../../common/dom/stop_propagation"; import "../../../../components/ha-alert"; import "../../../../components/ha-button"; import { createCloseHeading } from "../../../../components/ha-dialog"; import "../../../../components/ha-icon"; import "../../../../components/ha-list"; -import "../../../../components/ha-list-item"; import "../../../../components/ha-radio-list-item"; import "../../../../components/ha-select"; import type { LovelaceConfig } from "../../../../data/lovelace/config/types"; @@ -18,7 +16,7 @@ import type { LovelaceDashboard } from "../../../../data/lovelace/dashboard"; import { fetchDashboards } from "../../../../data/lovelace/dashboard"; import { getDefaultPanelUrlPath } from "../../../../data/panel"; import { haStyleDialog } from "../../../../resources/styles"; -import type { HomeAssistant } from "../../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../../types"; import type { SelectViewDialogParams } from "./show-select-view-dialog"; declare global { @@ -82,29 +80,23 @@ export class HuiDialogSelectView extends LitElement { .disabled=${!this._dashboards.length} .value=${this._urlPath || defaultPanel} @selected=${this._dashboardChanged} - @closed=${stopPropagation} - fixedMenuPosition - naturalMenuWidth + .options=${this._dashboards + .map((dashboard) => ({ + value: dashboard.url_path, + label: `${dashboard.title}${dashboard.id === "lovelace" ? ` (${this.hass.localize("ui.common.default")})` : ""}`, + disabled: dashboard.mode !== "storage", + })) + .sort((a, b) => + a.value === "lovelace" + ? -1 + : b.value === "lovelace" + ? 1 + : a.label.localeCompare(b.label) + )} dialogInitialFocus > - - Default - - ${this._dashboards.map( - (dashboard) => html` - ${dashboard.title} - ` - )} ` - : ""} + : nothing} ${!this._config || (this._config.views || []).length < 1 ? html`${this.hass.localize( @@ -142,7 +134,7 @@ export class HuiDialogSelectView extends LitElement { })} ` - : ""} + : nothing} ) { + let urlPath: string | null = ev.detail.value; if (urlPath === this._urlPath) { return; } diff --git a/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts b/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts index fb98d10bec..2c7c314ed2 100644 --- a/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts +++ b/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts @@ -1,4 +1,3 @@ -import type { ActionDetail } from "@material/mwc-list"; import { mdiClose, mdiDotsVertical, @@ -11,14 +10,14 @@ import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import type { HASSDomEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { navigate } from "../../../../common/navigate"; import { deepEqual } from "../../../../common/util/deep-equal"; import "../../../../components/ha-alert"; import "../../../../components/ha-button"; import "../../../../components/ha-dialog"; import "../../../../components/ha-dialog-header"; -import "../../../../components/ha-list-item"; +import "../../../../components/ha-dropdown"; +import "../../../../components/ha-dropdown-item"; import "../../../../components/ha-spinner"; import "../../../../components/ha-tab-group"; import "../../../../components/ha-tab-group-tab"; @@ -58,6 +57,7 @@ import "./hui-view-background-editor"; import "./hui-view-editor"; import "./hui-view-visibility-editor"; import type { EditViewDialogParams } from "./show-edit-view-dialog"; +import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown"; const TABS = ["tab-settings", "tab-background", "tab-visibility"] as const; @@ -218,38 +218,32 @@ export class HuiDialogEditView extends LitElement { .path=${mdiClose} >

    ${this._viewConfigTitle}

    - - + ${this.hass!.localize( `ui.panel.lovelace.editor.edit_view.edit_${!this._yamlMode ? "yaml" : "ui"}` )} - - - + + + ${this.hass!.localize( "ui.panel.lovelace.editor.edit_view.move_to_dashboard" )} - - + + ${convertToSection ? html` @@ -330,14 +324,18 @@ export class HuiDialogEditView extends LitElement { `; } - private async _handleAction(ev: CustomEvent) { - ev.stopPropagation(); - ev.preventDefault(); - switch (ev.detail.index) { - case 0: + private async _handleAction(ev: HaDropdownSelectEvent) { + const action = ev.detail.item.value; + + if (!action) { + return; + } + + switch (action) { + case "toggle-mode": this._yamlMode = !this._yamlMode; break; - case 1: + case "move-to-dashboard": this._openSelectDashboard(); break; } diff --git a/src/panels/lovelace/editor/view-header/hui-dialog-edit-view-header.ts b/src/panels/lovelace/editor/view-header/hui-dialog-edit-view-header.ts index 14b2cb537a..9f0a0013f3 100644 --- a/src/panels/lovelace/editor/view-header/hui-dialog-edit-view-header.ts +++ b/src/panels/lovelace/editor/view-header/hui-dialog-edit-view-header.ts @@ -1,16 +1,15 @@ -import type { ActionDetail } from "@material/mwc-list"; import { mdiClose, mdiDotsVertical, mdiPlaylistEdit } from "@mdi/js"; import type { CSSResultGroup, PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../../../common/dom/fire_event"; -import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { deepEqual } from "../../../../common/util/deep-equal"; import "../../../../components/ha-button"; import "../../../../components/ha-dialog"; import "../../../../components/ha-dialog-header"; -import "../../../../components/ha-list-item"; +import "../../../../components/ha-dropdown"; +import "../../../../components/ha-dropdown-item"; import "../../../../components/ha-spinner"; import "../../../../components/ha-yaml-editor"; import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; @@ -23,6 +22,7 @@ import { import type { HomeAssistant } from "../../../../types"; import "./hui-view-header-settings-editor"; import type { EditViewHeaderDialogParams } from "./show-edit-view-header-dialog"; +import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown"; @customElement("hui-dialog-edit-view-header") export class HuiDialogEditViewHeader extends LitElement { @@ -113,29 +113,23 @@ export class HuiDialogEditViewHeader extends LitElement { .path=${mdiClose} >

    ${title}

    - - + ${this.hass!.localize( `ui.panel.lovelace.editor.edit_view_header.edit_${!this._yamlMode ? "yaml" : "ui"}` )} - - - + + + ${content} ) { - ev.stopPropagation(); - ev.preventDefault(); - switch (ev.detail.index) { - case 0: - this._yamlMode = !this._yamlMode; - break; + private async _handleAction(ev: HaDropdownSelectEvent) { + const action = ev.detail.item.value; + + if (action === "toggle-mode") { + this._yamlMode = !this._yamlMode; } } diff --git a/src/panels/lovelace/entity-rows/hui-date-entity-row.ts b/src/panels/lovelace/entity-rows/hui-date-entity-row.ts index 0a8554a618..be5718e8fe 100644 --- a/src/panels/lovelace/entity-rows/hui-date-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-date-entity-row.ts @@ -4,7 +4,7 @@ import { customElement, property, state } from "lit/decorators"; import "../../../components/ha-date-input"; import { setDateValue } from "../../../data/date"; import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity"; -import type { HomeAssistant } from "../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../types"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import "../components/hui-generic-entity-row"; import { createEntityNotFoundWarning } from "../components/hui-warning"; @@ -59,7 +59,7 @@ class HuiDateEntityRow extends LitElement implements LovelaceRow { `; } - private _dateChanged(ev: CustomEvent<{ value: string }>): void { + private _dateChanged(ev: ValueChangedEvent): void { if (ev.detail.value) { setDateValue(this.hass!, this._config!.entity, ev.detail.value); } diff --git a/src/panels/lovelace/entity-rows/hui-datetime-entity-row.ts b/src/panels/lovelace/entity-rows/hui-datetime-entity-row.ts index 815942cef3..48eb0cd500 100644 --- a/src/panels/lovelace/entity-rows/hui-datetime-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-datetime-entity-row.ts @@ -6,7 +6,7 @@ import "../../../components/ha-date-input"; import "../../../components/ha-time-input"; import { setDateTimeValue } from "../../../data/datetime"; import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity"; -import type { HomeAssistant } from "../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../types"; import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import "../components/hui-generic-entity-row"; @@ -90,7 +90,7 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { ev.stopPropagation(); } - private _timeChanged(ev: CustomEvent<{ value: string }>): void { + private _timeChanged(ev: ValueChangedEvent): void { if (ev.detail.value) { const stateObj = this.hass!.states[this._config!.entity]; const dateObj = new Date(stateObj.state); @@ -101,7 +101,7 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { } } - private _dateChanged(ev: CustomEvent<{ value: string }>): void { + private _dateChanged(ev: ValueChangedEvent): void { if (ev.detail.value) { const stateObj = this.hass!.states[this._config!.entity]; const dateObj = new Date(stateObj.state); diff --git a/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts index d058126b25..57cafeb18f 100644 --- a/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-input-datetime-entity-row.ts @@ -8,7 +8,7 @@ import { setInputDateTimeValue, stateToIsoDateString, } from "../../../data/input_datetime"; -import type { HomeAssistant } from "../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../types"; import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import "../components/hui-generic-entity-row"; @@ -101,7 +101,7 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { ev.stopPropagation(); } - private _timeChanged(ev: CustomEvent<{ value: string }>): void { + private _timeChanged(ev: ValueChangedEvent): void { const stateObj = this.hass!.states[this._config!.entity]; setInputDateTimeValue( this.hass!, @@ -111,7 +111,7 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { ); } - private _dateChanged(ev: CustomEvent<{ value: string }>): void { + private _dateChanged(ev: ValueChangedEvent): void { const stateObj = this.hass!.states[this._config!.entity]; setInputDateTimeValue( diff --git a/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts b/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts index dfd8f7ec8d..d6dd796943 100644 --- a/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-input-select-entity-row.ts @@ -1,8 +1,7 @@ import type { PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; -import "../../../components/ha-list-item"; +import type { HaSelectSelectEvent } from "../../../components/ha-select"; import "../../../components/ha-select"; import { UNAVAILABLE } from "../../../data/entity/entity"; import { forwardHaptic } from "../../../data/haptics"; @@ -70,17 +69,8 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow { .disabled=${ stateObj.state === UNAVAILABLE /* UNKNOWN state is allowed */ } - naturalMenuWidth @selected=${this._selectedChanged} - @click=${stopPropagation} - @closed=${stopPropagation} > - ${stateObj.attributes.options - ? stateObj.attributes.options.map( - (option) => - html`${option}` - ) - : ""} `; @@ -97,11 +87,11 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow { } `; - private _selectedChanged(ev): void { + private _selectedChanged(ev: HaSelectSelectEvent): void { const stateObj = this.hass!.states[ this._config!.entity ] as InputSelectEntity; - const option = ev.target.value; + const option = ev.detail.value; if ( option === stateObj.state || !stateObj.attributes.options.includes(option) diff --git a/src/panels/lovelace/entity-rows/hui-select-entity-row.ts b/src/panels/lovelace/entity-rows/hui-select-entity-row.ts index 99e3f24cbb..42b7a50239 100644 --- a/src/panels/lovelace/entity-rows/hui-select-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-select-entity-row.ts @@ -1,8 +1,7 @@ import type { PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { stopPropagation } from "../../../common/dom/stop_propagation"; -import "../../../components/ha-list-item"; +import type { HaSelectSelectEvent } from "../../../components/ha-select"; import "../../../components/ha-select"; import { UNAVAILABLE } from "../../../data/entity/entity"; import { forwardHaptic } from "../../../data/haptics"; @@ -22,6 +21,8 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow { @state() private _config?: EntitiesCardEntityConfig; + @state() private _selectedEntityRow?: string; + public setConfig(config: EntitiesCardEntityConfig): void { if (!config || !config.entity) { throw new Error("Entity must be specified"); @@ -65,23 +66,14 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow { > ({ + value: option, + label: this.hass!.formatEntityState(stateObj, option), + }))} .disabled=${stateObj.state === UNAVAILABLE} - naturalMenuWidth - @action=${this._handleAction} - @click=${stopPropagation} - @closed=${stopPropagation} + @selected=${this._handleAction} > - ${stateObj.attributes.options - ? stateObj.attributes.options.map( - (option) => html` - - ${this.hass!.formatEntityState(stateObj, option)} - - ` - ) - : ""} `; @@ -98,10 +90,10 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow { } `; - private _handleAction(ev): void { + private _handleAction(ev: HaSelectSelectEvent): void { const stateObj = this.hass!.states[this._config!.entity] as SelectEntity; - const option = ev.target.value; + const option = ev.detail.value; if ( option === stateObj.state || @@ -120,9 +112,7 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow { setTimeout(() => { const newStateObj = this.hass!.states[this._config!.entity]; if (newStateObj === stateObj) { - const select = this.shadowRoot?.querySelector("ha-select"); - const index = select?.options.indexOf(stateObj.state) ?? -1; - select?.select(index); + this._selectedEntityRow = stateObj.state; } }, 2000) ); diff --git a/src/panels/lovelace/entity-rows/hui-time-entity-row.ts b/src/panels/lovelace/entity-rows/hui-time-entity-row.ts index 97bd1e7fb7..6f9c027cb7 100644 --- a/src/panels/lovelace/entity-rows/hui-time-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-time-entity-row.ts @@ -5,7 +5,7 @@ import "../../../components/ha-date-input"; import "../../../components/ha-time-input"; import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity"; import { setTimeValue } from "../../../data/time"; -import type { HomeAssistant } from "../../../types"; +import type { HomeAssistant, ValueChangedEvent } from "../../../types"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import "../components/hui-generic-entity-row"; import { createEntityNotFoundWarning } from "../components/hui-warning"; @@ -64,7 +64,7 @@ class HuiTimeEntityRow extends LitElement implements LovelaceRow { ev.stopPropagation(); } - private _timeChanged(ev: CustomEvent<{ value: string }>): void { + private _timeChanged(ev: ValueChangedEvent): void { if (ev.detail.value) { const stateObj = this.hass!.states[this._config!.entity]; setTimeValue(this.hass!, stateObj.entity_id, ev.detail.value); diff --git a/src/panels/lovelace/ha-panel-lovelace.ts b/src/panels/lovelace/ha-panel-lovelace.ts index d7fc90b03a..db1c9b8a6e 100644 --- a/src/panels/lovelace/ha-panel-lovelace.ts +++ b/src/panels/lovelace/ha-panel-lovelace.ts @@ -3,6 +3,9 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { navigate } from "../../common/navigate"; +import type { LocalizeFunc } from "../../common/translations/localize"; import { constructUrlCurrentPath } from "../../common/url/construct-url"; import { addSearchParam, @@ -10,15 +13,14 @@ import { } from "../../common/url/search-params"; import { debounce } from "../../common/util/debounce"; import { deepEqual } from "../../common/util/deep-equal"; +import "../../components/ha-button"; import { domainToName } from "../../data/integration"; import { subscribeLovelaceUpdates } from "../../data/lovelace"; import type { LovelaceConfig, - LovelaceDashboardStrategyConfig, LovelaceRawConfig, } from "../../data/lovelace/config/types"; import { - deleteConfig, fetchConfig, isStrategyDashboard, saveConfig, @@ -34,18 +36,13 @@ import { checkLovelaceConfig } from "./common/check-lovelace-config"; import { loadLovelaceResources } from "./common/load-resources"; import { showSaveDialog } from "./editor/show-save-config-dialog"; import "./hui-root"; -import "../../components/ha-button"; import { generateLovelaceDashboardStrategy } from "./strategies/get-strategy"; import type { Lovelace } from "./types"; +import { generateDefaultView } from "./views/default-view"; +import { fetchDashboards } from "../../data/lovelace/dashboard"; (window as any).loadCardHelpers = () => import("./custom-card-helpers"); -const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = { - strategy: { - type: "original-states", - }, -}; - interface LovelacePanelConfig { mode: "yaml" | "storage"; } @@ -61,7 +58,9 @@ declare global { @customElement("ha-panel-lovelace") export class LovelacePanel extends LitElement { - @property({ attribute: false }) public panel?: PanelInfo; + @property({ attribute: false }) public panel?: PanelInfo< + LovelacePanelConfig | undefined + >; @property({ attribute: false }) public hass?: HomeAssistant; @@ -97,11 +96,6 @@ export class LovelacePanel extends LitElement { this.lovelace.rawConfig, this.lovelace.mode ); - } else if (this.lovelace && this.lovelace.mode === "generated") { - // When lovelace is generated, we re-generate each time a user goes - // to the states panel to make sure new entities are shown. - this._panelState = "loading"; - this._regenerateConfig(); } else if (this._fetchConfigOnConnect) { // Config was changed when we were not at the lovelace panel this._fetchConfig(false); @@ -296,15 +290,6 @@ export class LovelacePanel extends LitElement { } }; - private async _regenerateConfig() { - const conf = await generateLovelaceDashboardStrategy( - DEFAULT_CONFIG, - this.hass! - ); - this._setLovelaceConfig(conf, DEFAULT_CONFIG, "generated"); - this._panelState = "loaded"; - } - private async _subscribeUpdates() { this._unsubUpdates = subscribeLovelaceUpdates( this.hass!.connection, @@ -340,7 +325,7 @@ export class LovelacePanel extends LitElement { } public get urlPath() { - return this.panel!.url_path === "lovelace" ? null : this.panel!.url_path; + return this.panel!.url_path; } private _forceFetchConfig() { @@ -352,7 +337,14 @@ export class LovelacePanel extends LitElement { let conf: LovelaceConfig; let rawConf: LovelaceRawConfig | undefined; - let confMode: Lovelace["mode"] = this.panel!.config.mode; + const confMode = this.panel!.config?.mode; + + // If no mode, redirect to /home as there is no "lovelace" dashboard + if (!confMode) { + navigate("/home", { replace: true }); + return; + } + let confProm: Promise | undefined; const preloadWindow = window as WindowWithPreloads; @@ -367,7 +359,6 @@ export class LovelacePanel extends LitElement { (resources) => loadLovelaceResources(resources, this.hass!) ); } - if (this.urlPath !== null || !confProm) { // 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. @@ -384,7 +375,7 @@ export class LovelacePanel extends LitElement { } try { - rawConf = await confProm!; + rawConf = await confProm; // If strategy defined, apply it here. if (isStrategyDashboard(rawConf)) { @@ -404,16 +395,19 @@ export class LovelacePanel extends LitElement { this._errorMsg = err.message; return; } - if (!this.hass?.entities || !this.hass.devices || !this.hass.areas) { - // We need these to generate a dashboard, wait for them - return; + + // If there is no dashboard called "lovelace", redirect to /home + if (this.urlPath === "lovelace") { + const dashboards = await fetchDashboards(this.hass!); + const dashboard = dashboards.find((d) => d.url_path === "lovelace"); + if (!dashboard) { + navigate("/home", { replace: true }); + return; + } } - conf = await generateLovelaceDashboardStrategy( - DEFAULT_CONFIG, - this.hass! - ); - rawConf = DEFAULT_CONFIG; - confMode = "generated"; + // Config not found, create a default one + conf = this._generateDefaultConfig(this.hass!.localize); + rawConf = conf; } finally { this._loading = false; // Ignore updates for another 2 seconds. @@ -458,7 +452,11 @@ export class LovelacePanel extends LitElement { setEditMode: (editMode: boolean) => { // If the dashboard is generated (default dashboard) // Propose to take control of it - if (this.lovelace!.mode === "generated" && editMode) { + if ( + this.lovelace!.mode === "generated" && + editMode && + this.panel?.config + ) { showSaveDialog(this, { lovelace: this.lovelace!, mode: this.panel!.config.mode, @@ -518,19 +516,17 @@ export class LovelacePanel extends LitElement { mode: previousMode, } = this.lovelace!; try { - // Optimistic update - const generatedConf = await generateLovelaceDashboardStrategy( - DEFAULT_CONFIG, - this.hass! + const defaultConfig = this._generateDefaultConfig( + this.hass!.localize ); + // Optimistic update this._updateLovelace({ - config: generatedConf, - rawConfig: DEFAULT_CONFIG, - mode: "generated", - editMode: false, + config: defaultConfig, + rawConfig: defaultConfig, + mode: "storage", }); this._ignoreNextUpdateEvent = true; - await deleteConfig(this.hass!, urlPath); + await saveConfig(this.hass!, urlPath, defaultConfig); } catch (err: any) { // eslint-disable-next-line console.error(err); @@ -547,6 +543,12 @@ export class LovelacePanel extends LitElement { }; } + private _generateDefaultConfig = memoizeOne( + (localize: LocalizeFunc): LovelaceConfig => ({ + views: [generateDefaultView(localize, true)], + }) + ); + private _updateLovelace(props: Partial) { this.lovelace = { ...this.lovelace!, diff --git a/src/panels/lovelace/header-footer/hui-graph-header-footer.ts b/src/panels/lovelace/header-footer/hui-graph-header-footer.ts index 42125d7aec..b289f1a343 100644 --- a/src/panels/lovelace/header-footer/hui-graph-header-footer.ts +++ b/src/panels/lovelace/header-footer/hui-graph-header-footer.ts @@ -3,6 +3,7 @@ import type { PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; +import { fireEvent } from "../../../common/dom/fire_event"; import { computeDomain } from "../../../common/entity/compute_domain"; import "../../../components/ha-spinner"; import { subscribeHistoryStatesTimeWindow } from "../../../data/history"; @@ -126,8 +127,15 @@ export class HuiGraphHeaderFooter `; } + private _handleClick(): void { + fireEvent(this, "hass-more-info", { + entityId: this._config?.entity ?? null, + }); + } + public connectedCallback() { super.connectedCallback(); + this.addEventListener("click", this._handleClick); if (this.hasUpdated && this._config) { this._subscribeHistory(); } @@ -224,6 +232,7 @@ export class HuiGraphHeaderFooter static styles = css` :host { display: block; + cursor: pointer; } ha-spinner { position: absolute; diff --git a/src/panels/lovelace/heading-badges/hui-button-heading-badge.ts b/src/panels/lovelace/heading-badges/hui-button-heading-badge.ts new file mode 100644 index 0000000000..bffb00172c --- /dev/null +++ b/src/panels/lovelace/heading-badges/hui-button-heading-badge.ts @@ -0,0 +1,143 @@ +import { LitElement, css, html, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import { classMap } from "lit/directives/class-map"; +import { computeCssColor } from "../../../common/color/compute-color"; +import "../../../components/ha-control-button"; +import "../../../components/ha-icon"; +import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; +import type { HomeAssistant } from "../../../types"; +import { actionHandler } from "../common/directives/action-handler-directive"; +import { handleAction } from "../common/handle-action"; +import { hasAction } from "../common/has-action"; +import type { + LovelaceHeadingBadge, + LovelaceHeadingBadgeEditor, +} from "../types"; +import type { ButtonHeadingBadgeConfig } from "./types"; + +const DEFAULT_ACTIONS: Pick< + ButtonHeadingBadgeConfig, + "tap_action" | "hold_action" | "double_tap_action" +> = { + tap_action: { action: "none" }, + hold_action: { action: "none" }, + double_tap_action: { action: "none" }, +}; + +@customElement("hui-button-heading-badge") +export class HuiButtonHeadingBadge + extends LitElement + implements LovelaceHeadingBadge +{ + public static async getConfigElement(): Promise { + await import("../editor/heading-badge-editor/hui-button-heading-badge-editor"); + return document.createElement("hui-button-heading-badge-editor"); + } + + public static getStubConfig(): ButtonHeadingBadgeConfig { + return { + type: "button", + icon: "mdi:gesture-tap-button", + }; + } + + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: ButtonHeadingBadgeConfig; + + @property({ type: Boolean }) public preview = false; + + public setConfig(config: ButtonHeadingBadgeConfig): void { + this._config = { + ...DEFAULT_ACTIONS, + ...config, + }; + } + + get hasAction() { + return ( + hasAction(this._config?.tap_action) || + hasAction(this._config?.hold_action) || + hasAction(this._config?.double_tap_action) + ); + } + + private _handleAction(ev: ActionHandlerEvent) { + handleAction(this, this.hass!, this._config!, ev.detail.action!); + } + + protected render() { + if (!this.hass || !this._config) { + return nothing; + } + + const config = this._config; + + const color = config.color ? computeCssColor(config.color) : undefined; + + const style = { "--color": color }; + + return html` + + + ${config.icon + ? html`` + : nothing} + ${config.text + ? html`${config.text}` + : nothing} + + + `; + } + + static styles = css` + ha-control-button { + --control-button-border-radius: var( + --ha-heading-badge-border-radius, + var(--ha-border-radius-pill) + ); + --control-button-padding: 0; + --mdc-icon-size: var(--ha-heading-badge-icon-size, 14px); + width: auto; + height: var(--ha-heading-badge-size, 26px); + min-width: var(--ha-heading-badge-size, 26px); + font-size: var(--ha-font-size-s); + } + ha-control-button.with-text { + --control-button-padding: 0 var(--ha-space-2); + } + ha-control-button.colored { + --control-button-icon-color: var(--color); + --control-button-background-color: var(--color); + --control-button-focus-color: var(--color); + --ha-ripple-color: var(--color); + } + .content { + display: flex; + flex-direction: row; + align-items: center; + white-space: nowrap; + } + .text { + padding: 0 var(--ha-space-1); + line-height: 1; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-button-heading-badge": HuiButtonHeadingBadge; + } +} diff --git a/src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts b/src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts index 2ee2f2ee8b..048f2651e3 100644 --- a/src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts +++ b/src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts @@ -16,6 +16,7 @@ import "../../../state-display/state-display"; import type { HomeAssistant } from "../../../types"; import { actionHandler } from "../common/directives/action-handler-directive"; import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; +import { findEntities } from "../common/find-entities"; import { handleAction } from "../common/handle-action"; import { hasAction } from "../common/has-action"; import { DEFAULT_CONFIG } from "../editor/heading-badge-editor/hui-entity-heading-badge-editor"; @@ -43,6 +44,24 @@ export class HuiEntityHeadingBadge return document.createElement("hui-heading-entity-editor"); } + public static getStubConfig(hass: HomeAssistant): EntityHeadingBadgeConfig { + const includeDomains = ["sensor", "light", "switch"]; + const maxEntities = 1; + const entities = Object.keys(hass.states); + const foundEntities = findEntities( + hass, + maxEntities, + entities, + [], + includeDomains + ); + + return { + type: "entity", + entity: foundEntities[0] || "", + }; + } + @property({ attribute: false }) public hass?: HomeAssistant; @state() private _config?: EntityHeadingBadgeConfig; diff --git a/src/panels/lovelace/heading-badges/types.ts b/src/panels/lovelace/heading-badges/types.ts index 77fc4949ac..6303e9b3d9 100644 --- a/src/panels/lovelace/heading-badges/types.ts +++ b/src/panels/lovelace/heading-badges/types.ts @@ -26,3 +26,13 @@ export interface EntityHeadingBadgeConfig extends LovelaceHeadingBadgeConfig { hold_action?: ActionConfig; double_tap_action?: ActionConfig; } + +export interface ButtonHeadingBadgeConfig extends LovelaceHeadingBadgeConfig { + type: "button"; + text?: string; + icon?: string; + color?: string; + tap_action?: ActionConfig; + hold_action?: ActionConfig; + double_tap_action?: ActionConfig; +} diff --git a/src/panels/lovelace/hui-editor.ts b/src/panels/lovelace/hui-editor.ts index ea29cf696a..4a86bf565b 100644 --- a/src/panels/lovelace/hui-editor.ts +++ b/src/panels/lovelace/hui-editor.ts @@ -198,16 +198,17 @@ class LovelaceFullConfigEditor extends LitElement { } } - private async _removeConfig() { + private async _resetConfig() { try { await this.lovelace!.deleteConfig(); } catch (err: any) { showAlertDialog(this, { text: this.hass.localize( - "ui.panel.lovelace.editor.raw_editor.error_remove", + "ui.panel.lovelace.editor.raw_editor.error_save_yaml", { error: err } ), }); + return; } window.onbeforeunload = null; if (this.closeEditor) { @@ -223,14 +224,14 @@ class LovelaceFullConfigEditor extends LitElement { if (!value) { showConfirmationDialog(this, { title: this.hass.localize( - "ui.panel.lovelace.editor.raw_editor.confirm_delete_config_title" + "ui.panel.lovelace.editor.raw_editor.confirm_reset_config_title" ), text: this.hass.localize( - "ui.panel.lovelace.editor.raw_editor.confirm_delete_config_text" + "ui.panel.lovelace.editor.raw_editor.confirm_reset_config_text" ), - confirmText: this.hass.localize("ui.common.delete"), + confirmText: this.hass.localize("ui.common.reset"), dismissText: this.hass.localize("ui.common.cancel"), - confirm: () => this._removeConfig(), + confirm: () => this._resetConfig(), destructive: true, }); return; diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 8313958693..2959433cef 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -1,4 +1,3 @@ -import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; import { mdiAccount, mdiCodeBraces, @@ -28,7 +27,6 @@ import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { UndoRedoController } from "../../common/controllers/undo-redo-controller"; import { fireEvent } from "../../common/dom/fire_event"; -import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event"; import { goBack, navigate } from "../../common/navigate"; import type { LocalizeKeys } from "../../common/translations/localize"; import { constructUrlCurrentPath } from "../../common/url/construct-url"; @@ -40,19 +38,18 @@ import { import { debounce } from "../../common/util/debounce"; import { afterNextRender } from "../../common/util/render-status"; import "../../components/ha-button"; -import "../../components/ha-button-menu"; +import "../../components/ha-dropdown"; +import "../../components/ha-dropdown-item"; import "../../components/ha-icon"; import "../../components/ha-icon-button"; import "../../components/ha-icon-button-arrow-next"; import "../../components/ha-icon-button-arrow-prev"; -import "../../components/ha-list-item"; import "../../components/ha-menu-button"; import "../../components/ha-svg-icon"; import "../../components/ha-tab-group"; import "../../components/ha-tab-group-tab"; import "../../components/ha-tooltip"; import { createAreaRegistryEntry } from "../../data/area/area_registry"; -import type { LovelacePanelConfig } from "../../data/lovelace"; import type { LovelaceConfig, LovelaceRawConfig, @@ -64,6 +61,7 @@ import { fetchDashboards, updateDashboard, } from "../../data/lovelace/dashboard"; +import { fetchLovelaceInfo } from "../../data/lovelace/resource"; import { getPanelTitle } from "../../data/panel"; import { createPerson } from "../../data/person"; import { showListItemsDialog } from "../../dialogs/dialog-list-items/show-list-items-dialog"; @@ -73,7 +71,6 @@ import { } from "../../dialogs/generic/show-dialog-box"; import { showMoreInfoDialog } from "../../dialogs/more-info/show-ha-more-info-dialog"; import { showQuickBar } from "../../dialogs/quick-bar/show-dialog-quick-bar"; -import { showShortcutsDialog } from "../../dialogs/shortcuts/show-shortcuts-dialog"; import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; import { haStyle } from "../../resources/styles"; import type { HomeAssistant, PanelInfo } from "../../types"; @@ -96,6 +93,7 @@ import "./views/hui-view"; import type { HUIView } from "./views/hui-view"; import "./views/hui-view-background"; import "./views/hui-view-container"; +import type { HaDropdownSelectEvent } from "../../components/ha-dropdown"; interface ActionItem { icon: string; @@ -145,8 +143,16 @@ class HUIRoot extends LitElement { @property({ attribute: false }) public extraActionItems?: ExtraActionItem[]; + @property({ type: Boolean, attribute: "no-edit" }) public noEdit = false; + + @property({ attribute: false }) public backButton = false; + + @property({ attribute: false }) public backPath?: string; + @state() private _curView?: number | "hass-unused-entities"; + @state() private _resourceMode: "yaml" | "storage" = "storage"; + private _configChangedByUndo = false; private _viewCache?: Record; @@ -182,6 +188,7 @@ class HUIRoot extends LitElement { private _renderActionItems(): TemplateResult { const result: TemplateResult[] = []; + if (this._editMode) { result.push( html`) => { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } - extraItem.action(); - }, + overflowAction: extraItem.action, visible: true, overflow: this.narrow, }); @@ -382,10 +386,10 @@ class HUIRoot extends LitElement { const label = [this.hass!.localize(item.key), item.suffix].join(" "); const button = item.subItems ? html` - subItem.visible) .map( (subItem) => html` - + + + ${this.hass!.localize(subItem.key)} - - + ` )} - + ` : html` { - const title = [this.hass!.localize(i.key), i.suffix].join(" "); - const action = i.subItems - ? (e) => { - if (!shouldHandleRequestSelectedEvent(e)) { - return; - } - showListItemsDialog(this, { - title: title, - mode: this.narrow ? "bottom-sheet" : "dialog", - items: i.subItems!.map((si) => ({ - iconPath: si.icon, - label: this.hass!.localize(si.key), - action: si.action, - })), - }); - } - : i.overflowAction; - - listItems.push( - html` - ${title} - - ` - ); - }); result.push(html` - + - ${listItems} - + ${overflowItems.map((i) => { + const title = [this.hass!.localize(i.key), i.suffix].join(" "); + return html` + + ${title} + `; + })} + `); } return html`${result}`; @@ -594,7 +574,7 @@ class HUIRoot extends LitElement {
    ${this._renderActionItems()}
    ` : html` - ${isSubview + ${isSubview || this.backButton ? html` { + this._resourceMode = info.resource_mode; + }); } public connectedCallback(): void { @@ -867,17 +850,11 @@ class HUIRoot extends LitElement { return this.shadowRoot!.getElementById("view") as HTMLDivElement; } - private _handleRefresh(ev: CustomEvent): void { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } + private _handleRefresh = () => { fireEvent(this, "config-refresh"); - } + }; - private _handleReloadResources(ev: CustomEvent): void { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } + private _handleReloadResources = () => { this.hass.callService("lovelace", "reload_resources"); showConfirmationDialog(this, { title: this.hass!.localize( @@ -890,28 +867,11 @@ class HUIRoot extends LitElement { dismissText: this.hass.localize("ui.common.not_now"), confirm: () => location.reload(), }); - } + }; - private _handleShowQuickBar(ev: CustomEvent): void { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } - this._showQuickBar(); - } - - private _showQuickBar(): void { - const params = { - keyboard_shortcut: html`${this.hass.localize("ui.tips.keyboard_shortcut")}`, - }; - - showQuickBar(this, { - hint: this.hass.enableShortcuts - ? this.hass.localize("ui.tips.key_e_tip", params) - : undefined, - }); - } + private _showQuickBar = () => { + showQuickBar(this, { showHint: this.hass.enableShortcuts }); + }; private _goBack(): void { const views = this.lovelace?.config.views ?? []; @@ -920,6 +880,8 @@ class HUIRoot extends LitElement { if (curViewConfig?.back_path != null) { navigate(curViewConfig.back_path, { replace: true }); + } else if (this.backPath) { + navigate(this.backPath, { replace: true }); } else if (history.length > 1) { goBack(); } else if (!views[0].subview) { @@ -929,39 +891,16 @@ class HUIRoot extends LitElement { } } - private _handleAddDevice(ev: CustomEvent): void { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } - this._addDevice(); - } - private _addDevice = async () => { await this.hass.loadFragmentTranslation("config"); - showAddIntegrationDialog(this); + showAddIntegrationDialog(this, { navigateToResult: true }); }; - private _handleCreateAutomation( - ev: CustomEvent - ): void { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } - this._createAutomation(); - } - private _createAutomation = async () => { await this.hass.loadFragmentTranslation("config"); showNewAutomationDialog(this, { mode: "automation" }); }; - private _handleCreateArea(ev: CustomEvent): void { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } - this._createArea(); - } - private _createArea = async () => { await this.hass.loadFragmentTranslation("config"); showAreaRegistryDetailDialog(this, { @@ -987,13 +926,6 @@ class HUIRoot extends LitElement { }); }; - private _handleAddPerson(ev: CustomEvent): void { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } - this._addPerson(); - } - private _addPerson = async () => { await this.hass.loadFragmentTranslation("config"); showPersonDetailDialog(this, { @@ -1017,61 +949,31 @@ class HUIRoot extends LitElement { }); }; - private _handleRawEditor(ev: CustomEvent): void { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } + private _handleRawEditor = () => { this.lovelace!.enableFullEditMode(); - } + }; - private _handleManageDashboards( - ev: CustomEvent - ): void { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } + private _handleManageDashboards = () => { navigate("/config/lovelace/dashboards"); - } + }; - private _handleManageResources(ev: CustomEvent): void { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } + private _handleManageResources = () => { navigate("/config/lovelace/resources"); - } + }; - private _handleUnusedEntities(ev: CustomEvent): void { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } + private _handleUnusedEntities = () => { navigate(`${this.route?.prefix}/hass-unused-entities`); - } + }; - private _handleShowVoiceCommandDialog( - ev: CustomEvent - ): void { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } - this._showVoiceCommandDialog(); - } - - private _showVoiceCommandDialog(): void { + private _showVoiceCommandDialog = () => { showVoiceCommandDialog(this, this.hass, { pipeline_id: "last_used" }); - } + }; private _showMoreInfoDialog(entityId: string): void { showMoreInfoDialog(this, { entityId }); } - private _handleEnableEditMode(ev: CustomEvent): void { - if (!shouldHandleRequestSelectedEvent(ev)) { - return; - } - this._enableEditMode(); - } - - private async _enableEditMode() { + private _enableEditMode = async () => { if (this._yamlMode) { showAlertDialog(this, { text: this.hass!.localize("ui.panel.lovelace.editor.yaml_unsupported"), @@ -1137,7 +1039,7 @@ class HUIRoot extends LitElement { return; } this.lovelace!.setEditMode(true); - } + }; private _editModeDisable(): void { this.lovelace!.setEditMode(false); @@ -1315,11 +1217,6 @@ class HUIRoot extends LitElement { root.appendChild(view); } - private _openShortcutDialog(ev: Event) { - ev.preventDefault(); - showShortcutsDialog(this); - } - private async _applyUndoRedo(item: UndoStackItem) { this._configChangedByUndo = true; try { @@ -1350,6 +1247,33 @@ class HUIRoot extends LitElement { this._undoRedoController.redo(); } + private _handleSubItemSelect(ev: HaDropdownSelectEvent) { + const subItem = (ev.detail?.item as any)?.data as SubActionItem; + if (subItem?.action) { + subItem.action(); + } else if (subItem?.overflowAction) { + subItem.overflowAction(); + } + } + + private _handleOverflowItemSelect(ev: HaDropdownSelectEvent) { + const item = (ev.detail?.item as any)?.data as ActionItem; + if (item?.subItems) { + const title = [this.hass!.localize(item.key), item.suffix].join(" "); + showListItemsDialog(this, { + title: title, + mode: this.narrow ? "bottom-sheet" : "dialog", + items: item.subItems!.map((si) => ({ + iconPath: si.icon, + label: this.hass!.localize(si.key), + action: si.action, + })), + }); + } else if (item?.overflowAction) { + item.overflowAction(); + } + } + static get styles(): CSSResultGroup { return [ haStyle, @@ -1362,7 +1286,6 @@ class HUIRoot extends LitElement { .header { background-color: var(--app-header-background-color); color: var(--app-header-text-color, white); - border-bottom: var(--app-header-border-bottom, none); position: fixed; top: 0; width: calc( @@ -1376,7 +1299,6 @@ class HUIRoot extends LitElement { padding-top: var(--safe-area-inset-top); padding-right: var(--safe-area-inset-right); z-index: 4; - transition: box-shadow 200ms linear; } .narrow .header { width: calc( @@ -1400,6 +1322,7 @@ class HUIRoot extends LitElement { color: var(--app-header-edit-text-color, white); } .toolbar { + border-bottom: var(--app-header-border-bottom, none); height: var(--header-height); display: flex; align-items: center; @@ -1408,11 +1331,14 @@ class HUIRoot extends LitElement { font-weight: var(--ha-font-weight-normal); box-sizing: border-box; } + .edit-mode .toolbar { + border-bottom: none; + } .narrow .toolbar { padding: 0 4px; } .main-title { - margin: var(--margin-title); + margin-inline-start: var(--ha-space-6); line-height: var(--ha-line-height-normal); flex-grow: 1; text-overflow: ellipsis; @@ -1421,8 +1347,7 @@ class HUIRoot extends LitElement { min-width: 0; } .narrow .main-title { - margin: 0; - margin-inline-start: 8px; + margin-inline-start: var(--ha-space-2); } .action-items { white-space: nowrap; @@ -1546,7 +1471,10 @@ class HUIRoot extends LitElement { display: flex; min-height: 100vh; box-sizing: border-box; - padding-top: calc(var(--header-height) + var(--safe-area-inset-top)); + padding-top: calc( + var(--header-height) + var(--safe-area-inset-top) + + var(--view-container-padding-top, 0px) + ); padding-right: var(--safe-area-inset-right); padding-inline-end: var(--safe-area-inset-right); padding-bottom: calc( diff --git a/src/panels/lovelace/sections/hui-grid-section.ts b/src/panels/lovelace/sections/hui-grid-section.ts index c505dd0d6f..786eac3811 100644 --- a/src/panels/lovelace/sections/hui-grid-section.ts +++ b/src/panels/lovelace/sections/hui-grid-section.ts @@ -46,7 +46,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement { @property({ type: Number }) public index?: number; - @property({ attribute: false, type: Number }) public viewIndex?: number; + @property({ attribute: false }) public viewIndex?: number; @property({ attribute: false }) public isStrategy = false; diff --git a/src/panels/lovelace/sections/hui-section.ts b/src/panels/lovelace/sections/hui-section.ts index 358197aa18..e0e572baff 100644 --- a/src/panels/lovelace/sections/hui-section.ts +++ b/src/panels/lovelace/sections/hui-section.ts @@ -50,7 +50,7 @@ export class HuiSection extends ConditionalListenerMixin( @property({ type: Number }) public index!: number; - @property({ attribute: false, type: Number }) public viewIndex!: number; + @property({ attribute: false }) public viewIndex!: number; @state() private _cards: HuiCard[] = []; diff --git a/src/panels/lovelace/special-rows/hui-attribute-row.ts b/src/panels/lovelace/special-rows/hui-attribute-row.ts index 77b4411271..3a144df3d9 100644 --- a/src/panels/lovelace/special-rows/hui-attribute-row.ts +++ b/src/panels/lovelace/special-rows/hui-attribute-row.ts @@ -67,7 +67,8 @@ class HuiAttributeRow extends LitElement implements LovelaceRow { : attribute !== undefined ? html` display.hidden || []) .flat(); - const controls: AreaControl[] = AREA_CONTROLS.filter( + const controls: AreaControlDomain[] = AREA_CONTROL_DOMAINS.filter( (a) => a !== "switch" // Exclude switches control for areas as we don't know what the switches control ); const controlEntities = getAreaControlEntities( diff --git a/src/panels/lovelace/strategies/home/home-area-view-strategy.ts b/src/panels/lovelace/strategies/home/home-area-view-strategy.ts index 78b0a579f1..2ec082c1e5 100644 --- a/src/panels/lovelace/strategies/home/home-area-view-strategy.ts +++ b/src/panels/lovelace/strategies/home/home-area-view-strategy.ts @@ -27,6 +27,7 @@ import { export interface HomeAreaViewStrategyConfig { type: "home-area"; area?: string; + home_panel?: boolean; } @customElement("home-area-view-strategy") @@ -252,11 +253,6 @@ export class HomeAreaViewStrategy extends ReactiveElement { device_class: "battery", }); - const energyFilter = generateEntityFilter(hass, { - domain: "sensor", - device_class: ["energy", "power"], - }); - const primaryFilter = generateEntityFilter(hass, { entity_category: "none", }); @@ -268,7 +264,7 @@ export class HomeAreaViewStrategy extends ReactiveElement { batteryFilter(e) ); const entities = deviceEntities.entities.filter( - (e) => !batteryFilter(e) && !energyFilter(e) && primaryFilter(e) + (e) => !batteryFilter(e) && primaryFilter(e) ); if (entities.length === 0) { @@ -281,7 +277,7 @@ export class HomeAreaViewStrategy extends ReactiveElement { if (device) { heading = computeDeviceName(device) || - hass.localize("ui.panel.lovelace.strategy.home.unamed_device"); + hass.localize("ui.panel.lovelace.strategy.home.unnamed_device"); } else { heading = hass.localize("ui.panel.lovelace.strategy.home.others"); } @@ -364,14 +360,47 @@ export class HomeAreaViewStrategy extends ReactiveElement { cards: [ { type: "empty-state", - icon: "mdi:sofa-outline", + icon: area.icon || "mdi:shape-square-rounded-plus", + icon_color: "primary", content_only: true, title: hass.localize( - "ui.panel.lovelace.strategy.areas.empty_state_title" + "ui.panel.lovelace.strategy.home-area.no_devices_title" ), content: hass.localize( - "ui.panel.lovelace.strategy.areas.empty_state_content" + "ui.panel.lovelace.strategy.home-area.no_devices_content" ), + ...(config.home_panel && hass.user?.is_admin + ? { + buttons: [ + { + icon: "mdi:plus", + text: hass.localize( + "ui.panel.lovelace.strategy.home-area.no_devices_add_device" + ), + appearance: "plain", + variant: "brand", + tap_action: { + action: "fire-dom-event", + home_panel: { + type: "add_integration", + }, + }, + }, + { + icon: "mdi:home-plus", + text: hass.localize( + "ui.panel.lovelace.strategy.home-area.no_devices_assign_device" + ), + appearance: "plain", + variant: "brand", + tap_action: { + action: "navigate", + navigation_path: "/home/other-devices", + }, + }, + ], + } + : {}), } as EmptyStateCardConfig, ], }; diff --git a/src/panels/lovelace/strategies/home/home-dashboard-strategy.ts b/src/panels/lovelace/strategies/home/home-dashboard-strategy.ts index 58cf17279a..c6b5fc3b1c 100644 --- a/src/panels/lovelace/strategies/home/home-dashboard-strategy.ts +++ b/src/panels/lovelace/strategies/home/home-dashboard-strategy.ts @@ -10,11 +10,13 @@ import { HOME_SUMMARIES_ICONS, } from "./helpers/home-summaries"; import type { HomeAreaViewStrategyConfig } from "./home-area-view-strategy"; +import type { HomeOtherDevicesViewStrategyConfig } from "./home-other-devices-view-strategy"; import type { HomeOverviewViewStrategyConfig } from "./home-overview-view-strategy"; export interface HomeDashboardStrategyConfig { type: "home"; favorite_entities?: string[]; + home_panel?: boolean; } @customElement("home-dashboard-strategy") @@ -57,6 +59,7 @@ export class HomeDashboardStrategy extends ReactiveElement { strategy: { type: "home-area", area: area.area_id, + home_panel: config.home_panel, } satisfies HomeAreaViewStrategyConfig, }; }); @@ -77,7 +80,8 @@ export class HomeDashboardStrategy extends ReactiveElement { subview: true, strategy: { type: "home-other-devices", - }, + home_panel: config.home_panel, + } satisfies HomeOtherDevicesViewStrategyConfig, icon: "mdi:devices", } satisfies LovelaceViewRawConfig; @@ -89,6 +93,7 @@ export class HomeDashboardStrategy extends ReactiveElement { strategy: { type: "home-overview", favorite_entities: config.favorite_entities, + home_panel: config.home_panel, } satisfies HomeOverviewViewStrategyConfig, }, ...areaViews, diff --git a/src/panels/lovelace/strategies/home/home-other-devices-view-strategy.ts b/src/panels/lovelace/strategies/home/home-other-devices-view-strategy.ts index 7501ef2a81..9eb2a8d21b 100644 --- a/src/panels/lovelace/strategies/home/home-other-devices-view-strategy.ts +++ b/src/panels/lovelace/strategies/home/home-other-devices-view-strategy.ts @@ -12,17 +12,22 @@ import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/ import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../../types"; import { isHelperDomain } from "../../../config/helpers/const"; -import type { HeadingCardConfig } from "../../cards/types"; +import type { + EmptyStateCardConfig, + EntitiesCardConfig, + HeadingCardConfig, +} from "../../cards/types"; import { OTHER_DEVICES_FILTERS } from "./helpers/other-devices-filters"; export interface HomeOtherDevicesViewStrategyConfig { type: "home-other-devices"; + home_panel?: boolean; } @customElement("home-other-devices-view-strategy") export class HomeOtherDevicesViewStrategy extends ReactiveElement { static async generate( - _config: HomeOtherDevicesViewStrategyConfig, + config: HomeOtherDevicesViewStrategyConfig, hass: HomeAssistant ): Promise { const allEntities = Object.keys(hass.states); @@ -74,16 +79,6 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement { return !isHelperDomain(domain); }); - const batteryFilter = generateEntityFilter(hass, { - domain: "sensor", - device_class: "battery", - }); - - const energyFilter = generateEntityFilter(hass, { - domain: "sensor", - device_class: ["energy", "power"], - }); - const primaryFilter = generateEntityFilter(hass, { entity_category: "none", }); @@ -91,12 +86,7 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement { for (const deviceEntities of devicesEntities) { if (deviceEntities.entities.length === 0) continue; - const batteryEntities = deviceEntities.entities.filter((e) => - batteryFilter(e) - ); - const entities = deviceEntities.entities.filter( - (e) => !batteryFilter(e) && !energyFilter(e) && primaryFilter(e) - ); + const entities = deviceEntities.entities.filter((e) => primaryFilter(e)); if (entities.length === 0) { continue; @@ -108,7 +98,7 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement { if (device) { heading = computeDeviceName(device) || - hass.localize("ui.panel.lovelace.strategy.home.unamed_device"); + hass.localize("ui.panel.lovelace.strategy.home.unnamed_device"); } sections.push({ @@ -125,22 +115,33 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement { } : { action: "none" }, badges: [ - ...batteryEntities.slice(0, 1).map((e) => ({ - entity: e, - type: "entity", - tap_action: { - action: "more-info", - }, - })), + ...(config.home_panel && device && hass.user?.is_admin + ? [ + { + type: "button", + icon: "mdi:home-plus", + text: hass.localize( + "ui.panel.lovelace.strategy.home-other-devices.assign_area" + ), + tap_action: { + action: "fire-dom-event", + home_panel: { + type: "assign_area", + device_id: device.id, + }, + }, + }, + ] + : []), ], } satisfies HeadingCardConfig, - ...entities.map((e) => ({ - type: "tile", - entity: e, - name: { - type: "entity", - }, - })), + { + type: "entities", + entities: entities.map((e) => ({ + entity: e, + name: { type: "entity" }, + })), + } satisfies EntitiesCardConfig, ], }); } @@ -156,7 +157,7 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement { { type: "heading", heading: hass.localize( - "ui.panel.lovelace.strategy.other_devices.helpers" + "ui.panel.lovelace.strategy.home-other-devices.helpers" ), } satisfies HeadingCardConfig, ...helpersEntities.map((e) => ({ @@ -175,7 +176,7 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement { { type: "heading", heading: hass.localize( - "ui.panel.lovelace.strategy.other_devices.entities" + "ui.panel.lovelace.strategy.home-other-devices.entities" ), } satisfies HeadingCardConfig, ...otherEntities.map((e) => ({ @@ -186,6 +187,27 @@ export class HomeOtherDevicesViewStrategy extends ReactiveElement { }); } + // No sections, show empty state + if (sections.length === 0) { + return { + type: "panel", + cards: [ + { + type: "empty-state", + icon: "mdi:check-all", + icon_color: "primary", + content_only: true, + title: hass.localize( + "ui.panel.lovelace.strategy.home-other-devices.all_organized_title" + ), + content: hass.localize( + "ui.panel.lovelace.strategy.home-other-devices.all_organized_content" + ), + } as EmptyStateCardConfig, + ], + }; + } + // Take the full width if there is only one section to avoid narrow header on desktop if (sections.length === 1) { sections[0].column_span = 2; diff --git a/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts b/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts index b92b7d4d1f..f2044cad62 100644 --- a/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts +++ b/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts @@ -19,6 +19,8 @@ import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../../types"; import type { AreaCardConfig, + DiscoveredDevicesCardConfig, + EmptyStateCardConfig, HomeSummaryCard, MarkdownCardConfig, TileCardConfig, @@ -31,6 +33,7 @@ import { OTHER_DEVICES_FILTERS } from "./helpers/other-devices-filters"; export interface HomeOverviewViewStrategyConfig { type: "home-overview"; favorite_entities?: string[]; + home_panel?: boolean; } const computeAreaCard = ( @@ -50,7 +53,10 @@ const computeAreaCard = ( area: areaId, display_type: "compact", sensor_classes: sensorClasses, - navigation_path: path, + tap_action: { + action: "navigate", + navigation_path: path, + }, vertical: true, grid_options: { rows: 2, @@ -189,9 +195,16 @@ export class HomeOverviewViewStrategy extends ReactiveElement { type: "common-controls", limit: maxCommonControls, include_entities: favoriteEntities, - title: hass.localize("ui.panel.lovelace.strategy.home.favorites"), - title_visibilty: [largeScreenCondition], hide_empty: true, + heading: { + type: "heading", + heading: hass.localize("ui.panel.lovelace.strategy.home.favorites"), + heading_style: "title", + visibility: [largeScreenCondition], + grid_options: { + rows: "auto", // Compact style + }, + }, } satisfies CommonControlSectionStrategyConfig, column_span: maxColumns, } as LovelaceStrategySectionConfig; @@ -239,6 +252,11 @@ export class HomeOverviewViewStrategy extends ReactiveElement { // Build summary cards (used in both mobile section and sidebar) const summaryCards: LovelaceCardConfig[] = [ + // Discovered devices card - only visible to admins, hides when empty + { + type: "discovered-devices", + hide_empty: true, + } satisfies DiscoveredDevicesCardConfig, hasLights && ({ type: "home-summary", @@ -290,7 +308,9 @@ export class HomeOverviewViewStrategy extends ReactiveElement { summary: "energy", tap_action: { action: "navigate", - navigation_path: "/energy?historyBack=1", + navigation_path: config.home_panel + ? "/energy?historyBack=1&backPath=/home" + : "/energy?historyBack=1", }, } satisfies HomeSummaryCard), ].filter(Boolean) as LovelaceCardConfig[]; @@ -331,10 +351,69 @@ export class HomeOverviewViewStrategy extends ReactiveElement { sidebarSummaryCards.length > 0 ? { type: "grid", - cards: [summaryHeadingCard, ...sidebarSummaryCards], + cards: [ + { + ...summaryHeadingCard, + grid_options: { rows: "auto" }, // Compact style + }, + ...sidebarSummaryCards, + ], } : undefined; + // No sections, show empty state + if (floorsSections.length === 0) { + return { + type: "panel", + cards: [ + { + type: "empty-state", + icon: "mdi:home-assistant", + icon_color: "primary", + content_only: true, + title: hass.localize( + "ui.panel.lovelace.strategy.home.welcome_title" + ), + content: hass.localize( + "ui.panel.lovelace.strategy.home.welcome_content" + ), + ...(config.home_panel && hass.user?.is_admin + ? { + buttons: [ + { + icon: "mdi:plus", + text: hass.localize( + "ui.panel.lovelace.strategy.home.welcome_add_device" + ), + appearance: "filled", + variant: "brand", + tap_action: { + action: "fire-dom-event", + home_panel: { + type: "add_integration", + }, + }, + }, + { + icon: "mdi:home-edit", + text: hass.localize( + "ui.panel.lovelace.strategy.home.welcome_edit_areas" + ), + appearance: "plain", + variant: "brand", + tap_action: { + action: "navigate", + navigation_path: "/config/areas/dashboard", + }, + }, + ], + } + : {}), + } as EmptyStateCardConfig, + ], + }; + } + const sections = ( [favoritesSection, mobileSummarySection, ...floorsSections] satisfies ( | LovelaceSectionRawConfig diff --git a/src/panels/lovelace/strategies/original-states/original-states-view-strategy.ts b/src/panels/lovelace/strategies/original-states/original-states-view-strategy.ts index c29e8be4ec..b79c38f7b1 100644 --- a/src/panels/lovelace/strategies/original-states/original-states-view-strategy.ts +++ b/src/panels/lovelace/strategies/original-states/original-states-view-strategy.ts @@ -71,6 +71,7 @@ export class OriginalStatesViewStrategy extends ReactiveElement { { type: "empty-state", icon: "mdi:home-assistant", + icon_color: "primary", content_only: true, title: hass.localize( "ui.panel.lovelace.strategy.original-states.empty_state_title" @@ -80,13 +81,19 @@ export class OriginalStatesViewStrategy extends ReactiveElement { ), ...(hass.user?.is_admin ? { - action_button_text: hass.localize( - "ui.panel.lovelace.strategy.original-states.empty_state_action" - ), - tap_action: { - action: "navigate", - navigation_path: "/config/integrations/dashboard", - }, + buttons: [ + { + text: hass.localize( + "ui.panel.lovelace.strategy.original-states.empty_state_action" + ), + appearance: "filled", + variant: "brand", + tap_action: { + action: "navigate", + navigation_path: "/config/integrations/dashboard", + }, + }, + ], } : {}), } as EmptyStateCardConfig, diff --git a/src/panels/lovelace/strategies/usage_prediction/common-controls-section-strategy.ts b/src/panels/lovelace/strategies/usage_prediction/common-controls-section-strategy.ts index 415926e965..7d0de000eb 100644 --- a/src/panels/lovelace/strategies/usage_prediction/common-controls-section-strategy.ts +++ b/src/panels/lovelace/strategies/usage_prediction/common-controls-section-strategy.ts @@ -11,12 +11,16 @@ const DEFAULT_LIMIT = 8; export interface CommonControlSectionStrategyConfig { type: "common-controls"; - title?: string; - icon?: string; limit?: number; exclude_entities?: string[]; include_entities?: string[]; hide_empty?: boolean; + heading?: HeadingCardConfig; + /** @deprecated Use `heading` instead */ + icon?: string; + /** @deprecated Use `heading` instead */ + title?: string; + /** @deprecated Use `heading` instead */ title_visibilty?: Condition[]; } @@ -31,7 +35,9 @@ export class CommonControlsSectionStrategy extends ReactiveElement { cards: [], }; - if (config.title) { + if (config.heading) { + section.cards?.push(config.heading); + } else if (config.title) { section.cards?.push({ type: "heading", heading: config.title, @@ -83,14 +89,6 @@ export class CommonControlsSectionStrategy extends ReactiveElement { ({ type: "tile", entity: entityId, - name: [ - { - type: "device", - }, - { - type: "entity", - }, - ], state_content: ["state", "area_name"], show_entity_picture: true, }) satisfies TileCardConfig diff --git a/src/panels/lovelace/views/default-section.ts b/src/panels/lovelace/views/default-section.ts index af3b96dc52..4fdd084852 100644 --- a/src/panels/lovelace/views/default-section.ts +++ b/src/panels/lovelace/views/default-section.ts @@ -1,13 +1,18 @@ import type { LocalizeFunc } from "../../../common/translations/localize"; -export const generateDefaultSection = (localize: LocalizeFunc) => ({ +export const generateDefaultSection = ( + localize: LocalizeFunc, + includeHeading?: boolean +) => ({ type: "grid", - cards: [ - { - type: "heading", - heading: localize( - "ui.panel.lovelace.editor.section.default_section_title" - ), - }, - ], + cards: includeHeading + ? [ + { + type: "heading", + heading: localize( + "ui.panel.lovelace.editor.section.default_section_title" + ), + }, + ] + : [], }); diff --git a/src/panels/lovelace/views/default-view.ts b/src/panels/lovelace/views/default-view.ts new file mode 100644 index 0000000000..6c3761f254 --- /dev/null +++ b/src/panels/lovelace/views/default-view.ts @@ -0,0 +1,12 @@ +import type { LocalizeFunc } from "../../../common/translations/localize"; +import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; +import { generateDefaultSection } from "./default-section"; + +export const generateDefaultView = ( + localize: LocalizeFunc, + includeHeading?: boolean +) => + ({ + type: "sections", + sections: [generateDefaultSection(localize, includeHeading)], + }) as LovelaceViewConfig; diff --git a/src/panels/lovelace/views/hui-sections-view.ts b/src/panels/lovelace/views/hui-sections-view.ts index 2feb25992a..c1c0914ef8 100644 --- a/src/panels/lovelace/views/hui-sections-view.ts +++ b/src/panels/lovelace/views/hui-sections-view.ts @@ -14,7 +14,6 @@ import "../../../components/ha-sortable"; import "../../../components/ha-svg-icon"; import type { LovelaceViewElement } from "../../../data/lovelace"; import type { LovelaceCardConfig } from "../../../data/lovelace/config/card"; -import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section"; import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; import type { HomeAssistant } from "../../../types"; import type { HuiBadge } from "../badges/hui-badge"; @@ -30,6 +29,7 @@ import { } from "../editor/lovelace-path"; import type { HuiSection } from "../sections/hui-section"; import type { Lovelace } from "../types"; +import { generateDefaultSection } from "./default-section"; import "./hui-view-header"; import "./hui-view-sidebar"; @@ -340,22 +340,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement { `; } - private _defaultSection(includeHeading: boolean): LovelaceSectionConfig { - return { - type: "grid", - cards: includeHeading - ? [ - { - type: "heading", - heading: this.hass!.localize( - "ui.panel.lovelace.editor.section.default_section_title" - ), - }, - ] - : [], - }; - } - private _handleCardAdded(ev) { const { data } = ev.detail; const oldPath = data as LovelaceCardPath; @@ -372,7 +356,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement { const configWithNewSection = addSection( this.lovelace!.config, this.index!, - this._defaultSection(cardConfig.type !== "heading") // If we move a heading card, we don't want to include a heading in the new section + generateDefaultSection(this.hass.localize, cardConfig.type !== "heading") // If we move a heading card, we don't want to include a heading in the new section ); const viewConfig = configWithNewSection.views[ this.index! @@ -397,7 +381,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement { const newConfig = addSection( this.lovelace!.config, this.index!, - this._defaultSection(true) + generateDefaultSection(this.hass.localize, true) ); this.lovelace!.saveConfig(newConfig); } diff --git a/src/panels/lovelace/views/hui-view-header.ts b/src/panels/lovelace/views/hui-view-header.ts index 9559716c0b..ca541879ba 100644 --- a/src/panels/lovelace/views/hui-view-header.ts +++ b/src/panels/lovelace/views/hui-view-header.ts @@ -327,8 +327,8 @@ export class HuiViewHeader extends LitElement { position: relative; display: flex; flex-direction: column; - gap: 24px 8px; - --spacing: 24px; + gap: 16px 8px; + --spacing: 8px; } .layout.has-heading { diff --git a/src/panels/media-browser/ha-bar-media-player.ts b/src/panels/media-browser/ha-bar-media-player.ts index 338f0f2f7d..42d460082e 100644 --- a/src/panels/media-browser/ha-bar-media-player.ts +++ b/src/panels/media-browser/ha-bar-media-player.ts @@ -1,5 +1,3 @@ -import "@material/mwc-linear-progress/mwc-linear-progress"; -import type { LinearProgress } from "@material/mwc-linear-progress/mwc-linear-progress"; import { mdiChevronDown, mdiMonitor, @@ -20,11 +18,16 @@ import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateName } from "../../common/entity/compute_state_name"; import { supportsFeature } from "../../common/entity/supports-feature"; import { debounce } from "../../common/util/debounce"; +import { + startMediaProgressInterval, + stopMediaProgressInterval, +} from "../../common/util/media-progress"; +import { VolumeSliderController } from "../../common/util/volume-slider"; import "../../components/ha-button"; -import "../../components/ha-button-menu"; import "../../components/ha-domain-icon"; +import "../../components/ha-dropdown"; +import "../../components/ha-dropdown-item"; import "../../components/ha-icon-button"; -import "../../components/ha-list-item"; import "../../components/ha-slider"; import "../../components/ha-spinner"; import "../../components/ha-state-icon"; @@ -50,6 +53,7 @@ import type { ResolvedMediaSource } from "../../data/media_source"; import { showAlertDialog } from "../../dialogs/generic/show-dialog-box"; import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../types"; +import type { HaSlider } from "../../components/ha-slider"; import "../lovelace/components/hui-marquee"; import { BrowserMediaPlayer, @@ -70,20 +74,40 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { @property({ type: Boolean, reflect: true }) public narrow = false; - @query("mwc-linear-progress") private _progressBar?: LinearProgress; + @query(".progress-slider") private _progressBar?: HaSlider; @query("#CurrentProgress") private _currentProgress?: HTMLElement; + @query(".volume-slider") private _volumeSlider?: HaSlider; + @state() private _marqueeActive = false; @state() private _newMediaExpected = false; @state() private _browserPlayer?: BrowserMediaPlayer; + private _volumeValue = 0; + private _progressInterval?: number; private _browserPlayerVolume = 0.8; + private _volumeStep = 2; + + private _debouncedVolumeSet = debounce((value: number) => { + this._setVolume(value); + }, 100); + + private _volumeController = new VolumeSliderController({ + getSlider: () => this._volumeSlider, + step: this._volumeStep, + onSetVolume: (value) => this._setVolume(value), + onSetVolumeDebounced: (value) => this._debouncedVolumeSet(value), + onValueUpdated: (value) => { + this._volumeValue = value; + }, + }); + public connectedCallback(): void { super.connectedCallback(); @@ -94,23 +118,20 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { } if ( - !this._progressInterval && this._showProgressBar && - stateObj.state === "playing" + stateObj.state === "playing" && + !this._progressInterval ) { - this._progressInterval = window.setInterval( - () => this._updateProgressBar(), - 1000 + this._progressInterval = startMediaProgressInterval( + this._progressInterval, + () => this._updateProgressBar() ); } } public disconnectedCallback(): void { super.disconnectedCallback(); - if (this._progressInterval) { - clearInterval(this._progressInterval); - this._progressInterval = undefined; - } + this._progressInterval = stopMediaProgressInterval(this._progressInterval); this._tearDownBrowserPlayer(); } @@ -174,7 +195,7 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { const stateObj = this._stateObj; if (!stateObj) { - return this._renderChoosePlayer(stateObj); + return this._renderChoosePlayer(stateObj, this._volumeValue); } const controls: ControlButton[] | undefined = !this.narrow @@ -214,7 +235,6 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { const mediaArt = stateObj.attributes.entity_picture_local || stateObj.attributes.entity_picture; - return html`
    ` + ? html`` : html`
    - +
    ${mediaDuration}
    `} `}
    - ${this._renderChoosePlayer(stateObj)} + ${this._renderChoosePlayer(stateObj, this._volumeValue)} `; } - private _renderChoosePlayer(stateObj: MediaPlayerEntity | undefined) { + private _renderChoosePlayer( + stateObj: MediaPlayerEntity | undefined, + volumeValue: number + ) { const isBrowser = this.entityId === BROWSER_PLAYER; return html`
    @@ -294,26 +348,42 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { stateObj && supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET) ? html` - + - - - + + +
    + ` : "" } - + ${ this.narrow ? html` @@ -342,26 +412,24 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
    ` } - ${this.hass.localize("ui.components.media-browser.web-browser")} - + ${this._mediaPlayerEntities.map( (source) => html` - ${computeStateName(source)} - + ` )} - +
    @@ -401,6 +469,9 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { ) { this._newMediaExpected = false; } + if (changedProps.has("hass")) { + this._updateVolumeValueFromState(this._stateObj); + } } protected updated(changedProps: PropertyValues) { @@ -419,23 +490,25 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { const stateObj = this._stateObj; - this._updateProgressBar(); + if (this.entityId === BROWSER_PLAYER) { + this._updateVolumeValueFromState(stateObj); + } - if ( - !this._progressInterval && - this._showProgressBar && - stateObj?.state === "playing" - ) { - this._progressInterval = window.setInterval( - () => this._updateProgressBar(), - 1000 + this._updateProgressBar(); + this._syncVolumeSlider(); + + if (this._showProgressBar && stateObj?.state === "playing") { + this._progressInterval = startMediaProgressInterval( + this._progressInterval, + () => this._updateProgressBar() ); } else if ( this._progressInterval && (!this._showProgressBar || stateObj?.state !== "playing") ) { - clearInterval(this._progressInterval); - this._progressInterval = undefined; + this._progressInterval = stopMediaProgressInterval( + this._progressInterval + ); } } @@ -489,25 +562,45 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { private _updateProgressBar(): void { const stateObj = this._stateObj; - if (!this._progressBar || !this._currentProgress || !stateObj) { + if (!this._progressBar || !stateObj) { return; } if (!stateObj.attributes.media_duration) { - this._progressBar.progress = 0; - this._currentProgress.innerHTML = ""; + this._progressBar.value = 0; + if (this._currentProgress) { + this._currentProgress.innerHTML = ""; + } return; } const currentProgress = getCurrentProgress(stateObj); - this._progressBar.progress = - currentProgress / stateObj.attributes.media_duration; + this._progressBar.max = stateObj.attributes.media_duration; + this._progressBar.value = currentProgress; if (this._currentProgress) { this._currentProgress.innerHTML = formatMediaTime(currentProgress); } } + private _updateVolumeValueFromState(stateObj?: MediaPlayerEntity): void { + if (!stateObj) { + return; + } + const volumeLevel = stateObj.attributes.volume_level; + if (typeof volumeLevel !== "number" || !Number.isFinite(volumeLevel)) { + return; + } + this._volumeValue = Math.round(volumeLevel * 100); + } + + private _syncVolumeSlider(): void { + if (!this._volumeSlider || this._volumeController.isInteracting) { + return; + } + this._volumeSlider.value = this._volumeValue; + } + private _handleControlClick(e: MouseEvent): void { const action = (e.currentTarget! as HTMLElement).getAttribute("action")!; @@ -526,6 +619,18 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { } } + private _handleMediaSeekChanged(e: Event): void { + if (this.entityId === BROWSER_PLAYER || !this._stateObj) { + return; + } + + const newValue = (e.target as HaSlider).value; + this.hass.callService("media_player", "media_seek", { + entity_id: this._stateObj.entity_id, + seek_position: newValue, + }); + } + private _marqueeMouseOver(): void { if (!this._marqueeActive) { this._marqueeActive = true; @@ -538,20 +643,19 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { } } - private _selectPlayer(ev: CustomEvent): void { - const entityId = (ev.currentTarget as any).player; + private _handlePlayerSelect(ev: CustomEvent): void { + const entityId = (ev.detail.item as any).value; fireEvent(this, "player-picked", { entityId }); } - private async _handleVolumeChange(ev) { - ev.stopPropagation(); - const value = Number(ev.target.value) / 100; + private _setVolume(value: number) { + const volume = value / 100; if (this._browserPlayer) { - this._browserPlayerVolume = value; - this._browserPlayer.setVolume(value); - } else { - await setMediaPlayerVolume(this.hass, this.entityId, value); + this._browserPlayerVolume = volume; + this._browserPlayer.setVolume(volume); + return; } + setMediaPlayerVolume(this.hass, this.entityId, volume); } static styles = css` @@ -565,21 +669,25 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { ); border-top: 1px solid var(--divider-color); margin-right: var(--safe-area-inset-right); + transition: + width var(--ha-animation-duration-normal) ease, + margin-left var(--ha-animation-duration-normal) ease, + margin-right var(--ha-animation-duration-normal) ease; } :host([narrow]) { margin-left: var(--safe-area-inset-left); } - - mwc-linear-progress { - width: 100%; - padding: 0 4px; - --mdc-theme-primary: var(--secondary-text-color); + @media (prefers-reduced-motion: reduce) { + :host { + transition: none; + } } - ha-button-menu ha-button[slot="trigger"] { - line-height: 1; - --mdc-theme-primary: var(--primary-text-color); - --mdc-icon-size: 16px; + ha-slider { + width: 100%; + min-width: 100%; + --ha-slider-thumb-color: var(--primary-color); + --ha-slider-indicator-color: var(--primary-color); } .info { @@ -611,6 +719,7 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { justify-content: flex-end; align-items: center; padding: 16px; + gap: var(--ha-space-2); } .controls { @@ -633,10 +742,35 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { align-items: center; } - mwc-linear-progress[wide] { + .progress > div:first-child { + margin-right: var(--ha-space-2); + } + + .progress > div:last-child { + margin-left: var(--ha-space-2); + } + + .progress ha-slider { margin: 0 4px; } + ha-dropdown.volume-menu::part(menu) { + width: 220px; + max-width: 220px; + overflow: visible; + padding: 15px 15px; + } + + .volume-slider-container { + width: 100%; + } + + @media (pointer: coarse) { + .volume-slider { + pointer-events: none; + } + } + .media-info { text-overflow: ellipsis; white-space: nowrap; @@ -700,15 +834,11 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) { justify-content: flex-end; } - :host([narrow]) mwc-linear-progress { - padding: 0; + :host([narrow]) ha-slider { position: absolute; - top: -4px; + top: -6px; left: 0; - } - - ha-list-item[selected] { - font-weight: var(--ha-font-weight-bold); + right: 0; } `; } diff --git a/src/panels/media-browser/ha-panel-media-browser.ts b/src/panels/media-browser/ha-panel-media-browser.ts index 8272c84205..a50c96961d 100644 --- a/src/panels/media-browser/ha-panel-media-browser.ts +++ b/src/panels/media-browser/ha-panel-media-browser.ts @@ -1,4 +1,3 @@ -import type { ActionDetail } from "@material/mwc-list"; import { mdiAlphaABoxOutline, mdiDotsVertical, @@ -12,9 +11,10 @@ import { storage } from "../../common/decorators/storage"; import type { HASSDomEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event"; import { navigate } from "../../common/navigate"; +import "../../components/ha-dropdown"; +import "../../components/ha-dropdown-item"; import "../../components/ha-icon-button"; import "../../components/ha-icon-button-arrow-prev"; -import "../../components/ha-list-item"; import "../../components/ha-menu-button"; import "../../components/ha-top-app-bar-fixed"; import "../../components/media-player/ha-media-manage-button"; @@ -41,6 +41,7 @@ import type { HomeAssistant, Route } from "../../types"; import "./ha-bar-media-player"; import type { BarMediaPlayer } from "./ha-bar-media-player"; import { showWebBrowserPlayMediaDialog } from "./show-media-player-dialog"; +import type { HaDropdownSelectEvent } from "../../components/ha-dropdown"; const createMediaPanelUrl = (entityId: string, items: MediaPlayerItemId[]) => { let path = `/media-browser/${entityId}`; @@ -119,43 +120,40 @@ class PanelMediaBrowser extends LitElement { .currentItem=${this._currentItem} @media-refresh=${this._refreshMedia} > - + - + ${this.hass.localize("ui.components.media-browser.auto")} - - - + + + ${this.hass.localize("ui.components.media-browser.grid")} - - - + + + ${this.hass.localize("ui.components.media-browser.list")} - - - + + + ) { - switch (ev.detail.index) { - case 0: - this._preferredLayout = "auto"; - break; - case 1: - this._preferredLayout = "grid"; - break; - case 2: - this._preferredLayout = "list"; - break; + private _handleMenuAction(ev: HaDropdownSelectEvent) { + const value = ev.detail.item.value; + + if (["auto", "grid", "list"].includes(value)) { + this._preferredLayout = value as MediaPlayerLayoutType; } } @@ -372,6 +364,10 @@ class PanelMediaBrowser extends LitElement { color: var(--primary-color); } + .selected_menu_item ha-svg-icon { + color: currentColor; + } + ha-bar-media-player { position: fixed; bottom: var(--safe-area-inset-bottom, 0px); diff --git a/src/panels/media-browser/hui-dialog-web-browser-play-media.ts b/src/panels/media-browser/hui-dialog-web-browser-play-media.ts index 79d71d045c..67f3448dfb 100644 --- a/src/panels/media-browser/hui-dialog-web-browser-play-media.ts +++ b/src/panels/media-browser/hui-dialog-web-browser-play-media.ts @@ -2,7 +2,7 @@ import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; -import { createCloseHeading } from "../../components/ha-dialog"; +import "../../components/ha-wa-dialog"; import "../../components/ha-hls-player"; import { haStyleDialog } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; @@ -14,17 +14,24 @@ export class HuiDialogWebBrowserPlayMedia extends LitElement { @state() private _params?: WebBrowserPlayMediaDialogParams; + @state() private _open = false; + public showDialog(params: WebBrowserPlayMediaDialogParams): void { this._params = params; + this._open = true; } - public closeDialog() { - this._params = undefined; + public closeDialog(): void { + this._open = false; + } + + private _dialogClosed(): void { const img = this.renderRoot.querySelector("img"); if (img) { // Unload streaming images so the connection can be closed img.src = ""; } + this._params = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -36,15 +43,13 @@ export class HuiDialogWebBrowserPlayMedia extends LitElement { const mediaType = this._params.sourceType.split("/", 1)[0]; return html` - ${mediaType === "audio" ? html` @@ -88,7 +93,7 @@ export class HuiDialogWebBrowserPlayMedia extends LitElement { : html`${this.hass.localize( "ui.components.media-browser.media_not_supported" )}`} - + `; } @@ -96,13 +101,6 @@ export class HuiDialogWebBrowserPlayMedia extends LitElement { return [ haStyleDialog, css` - @media (min-width: 800px) { - ha-dialog { - --mdc-dialog-max-width: 800px; - --mdc-dialog-min-width: 400px; - } - } - video, audio, img { diff --git a/src/panels/my/ha-panel-my.ts b/src/panels/my/ha-panel-my.ts index 100ca9f438..916fcb2b53 100644 --- a/src/panels/my/ha-panel-my.ts +++ b/src/panels/my/ha-panel-my.ts @@ -26,34 +26,34 @@ export const getMyRedirects = (): Redirects => ({ redirect: "/config/application_credentials", }, developer_assist: { - redirect: "/developer-tools/assist", + redirect: "/config/developer-tools/assist", }, developer_debug: { - redirect: "/developer-tools/debug", + redirect: "/config/developer-tools/debug", }, developer_states: { - redirect: "/developer-tools/state", + redirect: "/config/developer-tools/state", }, developer_services: { - redirect: "/developer-tools/action", + redirect: "/config/developer-tools/action", }, developer_call_service: { - redirect: "/developer-tools/action", + redirect: "/config/developer-tools/action", params: { service: "string", }, }, developer_template: { - redirect: "/developer-tools/template", + redirect: "/config/developer-tools/template", }, developer_events: { - redirect: "/developer-tools/event", + redirect: "/config/developer-tools/event", }, developer_statistics: { - redirect: "/developer-tools/statistics", + redirect: "/config/developer-tools/statistics", }, server_controls: { - redirect: "/developer-tools/yaml", + redirect: "/config/developer-tools/yaml", }, calendar: { component: "calendar", @@ -324,6 +324,36 @@ export const getMyRedirects = (): Redirects => ({ // Moved from Supervisor panel in 2022.5 redirect: "/config/info", }, + supervisor_store: { + redirect: "/config/apps/available", + }, + supervisor_addons: { + redirect: "/config/apps", + }, + supervisor_app: { + redirect: "/config/app", + params: { + app: "string", + }, + optional_params: { + repository_url: "url", + }, + }, + supervisor_addon: { + redirect: "/config/app", + params: { + addon: "string", + }, + optional_params: { + repository_url: "url", + }, + }, + supervisor_add_addon_repository: { + redirect: "/config/apps/available", + params: { + repository_url: "url", + }, + }, hacs_repository: { component: "hacs", redirect: "/hacs/_my_redirect/hacs_repository", @@ -502,13 +532,31 @@ class HaPanelMy extends LitElement { } private _createRedirectUrl(): string { - const params = this._createRedirectParams(); - return `${this._redirect!.redirect}${params}`; + const params = extractSearchParamsObject(); + + // Special case for supervisor_app/supervisor_addon: use path-based URL + // Support both "app" (new) and "addon" (legacy) parameters + if (this._redirect!.redirect === "/config/app") { + const appSlug = params.app || params.addon; + if (appSlug) { + delete params.app; + delete params.addon; + const optionalParams = this._createOptionalParams(params); + return `/config/app/${appSlug}/info${optionalParams}`; + } + } + + const resultParams = this._createRedirectParams(); + return `${this._redirect!.redirect}${resultParams}`; } private _createRedirectParams(): string { const params = extractSearchParamsObject(); - if (!this._redirect!.params && !Object.keys(params).length) { + if ( + !this._redirect!.params && + !this._redirect!.optional_params && + !Object.keys(params).length + ) { return ""; } const resultParams = {}; @@ -521,7 +569,37 @@ class HaPanelMy extends LitElement { } resultParams[key] = params[key]; } - return `?${createSearchParam(resultParams)}`; + for (const [key, type] of Object.entries( + this._redirect!.optional_params || {} + )) { + if (params[key]) { + if (!this._checkParamType(type, params[key])) { + throw Error(); + } + resultParams[key] = params[key]; + } + } + return Object.keys(resultParams).length + ? `?${createSearchParam(resultParams)}` + : ""; + } + + private _createOptionalParams(params: Record): string { + if (!this._redirect!.optional_params) { + return ""; + } + const resultParams = {}; + for (const [key, type] of Object.entries(this._redirect!.optional_params)) { + if (params[key]) { + if (!this._checkParamType(type, params[key])) { + throw Error(); + } + resultParams[key] = params[key]; + } + } + return Object.keys(resultParams).length + ? `?${createSearchParam(resultParams)}` + : ""; } private _checkParamType(type: ParamType, value: string) { diff --git a/src/panels/profile/dialog-ha-mfa-module-setup-flow.ts b/src/panels/profile/dialog-ha-mfa-module-setup-flow.ts index 960d2cce79..d2e52a8b4f 100644 --- a/src/panels/profile/dialog-ha-mfa-module-setup-flow.ts +++ b/src/panels/profile/dialog-ha-mfa-module-setup-flow.ts @@ -1,9 +1,12 @@ import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; +import { ifDefined } from "lit/directives/if-defined"; import { customElement, property, state } from "lit/decorators"; import "../../components/ha-button"; -import "../../components/ha-dialog"; +import "../../components/ha-dialog-footer"; +import "../../components/ha-wa-dialog"; import "../../components/ha-form/ha-form"; +import type { HaFormSchema } from "../../components/ha-form/types"; import "../../components/ha-markdown"; import "../../components/ha-spinner"; import { autocompleteLoginFields } from "../../data/auth"; @@ -28,7 +31,7 @@ class HaMfaModuleSetupFlow extends LitElement { @state() private _loading = false; - @state() private _opened = false; + @state() private _open = false; @state() private _stepData: any = {}; @@ -39,7 +42,7 @@ class HaMfaModuleSetupFlow extends LitElement { public showDialog({ continueFlowId, mfaModuleId, dialogClosedCallback }) { this._instance = instance++; this._dialogClosedCallback = dialogClosedCallback; - this._opened = true; + this._open = true; const fetchStep = continueFlowId ? this.hass.callWS({ @@ -61,22 +64,29 @@ class HaMfaModuleSetupFlow extends LitElement { } public closeDialog() { - // Closed dialog by clicking on the overlay + this._open = false; + } + + private _dialogClosed() { if (this._step) { this._flowDone(); + return; } - this._opened = false; + + this._resetDialogState(); } protected render() { - if (!this._opened) { + if (this._instance === undefined) { return nothing; } return html` -
    ${this._errorMessage @@ -115,6 +125,7 @@ class HaMfaModuleSetupFlow extends LitElement { )} > ` : ""}`}
    - ${this.hass.localize( - ["abort", "create_entry"].includes(this._step?.type || "") - ? "ui.panel.profile.mfa_setup.close" - : "ui.common.cancel" - )} - ${this._step?.type === "form" - ? html`${this.hass.localize( - "ui.panel.profile.mfa_setup.submit" - )}` - : nothing} -
    + + ${this.hass.localize( + ["abort", "create_entry"].includes(this._step?.type || "") + ? "ui.panel.profile.mfa_setup.close" + : "ui.common.cancel" + )} + ${this._step?.type === "form" + ? html`${this.hass.localize( + "ui.panel.profile.mfa_setup.submit" + )}` + : nothing} + + `; } @@ -162,9 +175,6 @@ class HaMfaModuleSetupFlow extends LitElement { .error { color: red; } - ha-dialog { - max-width: 500px; - } ha-markdown { --markdown-svg-background-color: white; --markdown-svg-color: black; @@ -177,9 +187,17 @@ class HaMfaModuleSetupFlow extends LitElement { ha-markdown-element p { text-align: center; } + ha-markdown-element svg { + display: block; + margin: 0 auto; + } ha-markdown-element code { background-color: transparent; } + ha-form { + display: block; + margin-top: var(--ha-space-4); + } ha-markdown-element > *:last-child { margin-bottom: revert; } @@ -206,6 +224,10 @@ class HaMfaModuleSetupFlow extends LitElement { } private _submitStep() { + if (this._isSubmitDisabled()) { + return; + } + this._loading = true; this._errorMessage = undefined; @@ -234,6 +256,62 @@ class HaMfaModuleSetupFlow extends LitElement { ); } + private _isSubmitDisabled() { + return this._loading || this._hasMissingRequiredFields(); + } + + private _hasMissingRequiredFields( + schema: readonly HaFormSchema[] = this._step?.type === "form" + ? this._step.data_schema + : [] + ): boolean { + for (const field of schema) { + if ("schema" in field) { + if (this._hasMissingRequiredFields(field.schema)) { + return true; + } + continue; + } + + if (!field.required) { + continue; + } + + if ( + field.default !== undefined || + field.description?.suggested_value !== undefined + ) { + continue; + } + + if (this._isEmptyValue(this._stepData[field.name])) { + return true; + } + } + + return false; + } + + private _isEmptyValue(value: unknown): boolean { + if (value === undefined || value === null) { + return true; + } + + if (typeof value === "string") { + return value.trim() === ""; + } + + if (Array.isArray(value)) { + return value.length === 0; + } + + if (typeof value === "object") { + return Object.keys(value as Record).length === 0; + } + + return false; + } + private _processStep(step) { if (!step.errors) step.errors = {}; this._step = step; @@ -251,12 +329,15 @@ class HaMfaModuleSetupFlow extends LitElement { this._dialogClosedCallback!({ flowFinished, }); + this._resetDialogState(); + } + private _resetDialogState() { this._errorMessage = undefined; this._step = undefined; this._stepData = {}; this._dialogClosedCallback = undefined; - this.closeDialog(); + this._instance = undefined; } private _computeStepTitle() { diff --git a/src/panels/profile/ha-advanced-mode-row.ts b/src/panels/profile/ha-advanced-mode-row.ts index 9acde53bcb..a3906ac25a 100644 --- a/src/panels/profile/ha-advanced-mode-row.ts +++ b/src/panels/profile/ha-advanced-mode-row.ts @@ -3,7 +3,7 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import "../../components/ha-alert"; import "../../components/ha-card"; -import "../../components/ha-settings-row"; +import "../../components/ha-md-list-item"; import "../../components/ha-switch"; import type { CoreFrontendUserData } from "../../data/frontend"; import { saveFrontendUserData } from "../../data/frontend"; @@ -14,8 +14,6 @@ import type { HomeAssistant } from "../../types"; class AdvancedModeRow extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ type: Boolean }) public narrow = false; - @property({ attribute: false }) public coreUserData?: CoreFrontendUserData; @state() private _error?: string; @@ -25,12 +23,12 @@ class AdvancedModeRow extends LitElement { ${this._error ? html`${this._error}` : nothing} - - - ${this.hass.localize("ui.panel.profile.advanced_mode.title")} - - - ${this.hass.localize("ui.panel.profile.advanced_mode.description")} + + ${this.hass.localize("ui.panel.profile.advanced_mode.title")} + ${this.hass.localize("ui.panel.profile.advanced_mode.description")} ${this.hass.localize("ui.panel.profile.advanced_mode.link_promo")} - - + - + `; } diff --git a/src/panels/profile/ha-enable-shortcuts-row.ts b/src/panels/profile/ha-enable-shortcuts-row.ts index d85d7715b3..f44f8de318 100644 --- a/src/panels/profile/ha-enable-shortcuts-row.ts +++ b/src/panels/profile/ha-enable-shortcuts-row.ts @@ -2,7 +2,7 @@ import type { TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; -import "../../components/ha-settings-row"; +import "../../components/ha-md-list-item"; import "../../components/ha-switch"; import type { HaSwitch } from "../../components/ha-switch"; import type { HomeAssistant } from "../../types"; @@ -11,22 +11,25 @@ import type { HomeAssistant } from "../../types"; class HaEnableShortcutsRow extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ type: Boolean }) public narrow = false; - protected render(): TemplateResult { return html` - - - ${this.hass.localize("ui.panel.profile.enable_shortcuts.header")} - - - ${this.hass.localize("ui.panel.profile.enable_shortcuts.description")} - + + ${this.hass.localize( + "ui.panel.profile.enable_shortcuts.header" + )} + ${this.hass.localize( + "ui.panel.profile.enable_shortcuts.description" + )} - + `; } diff --git a/src/panels/profile/ha-entity-id-picker-row.ts b/src/panels/profile/ha-entity-id-picker-row.ts index 959565acbd..e5bea7a903 100644 --- a/src/panels/profile/ha-entity-id-picker-row.ts +++ b/src/panels/profile/ha-entity-id-picker-row.ts @@ -3,7 +3,7 @@ import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import "../../components/ha-alert"; import "../../components/ha-card"; -import "../../components/ha-settings-row"; +import "../../components/ha-md-list-item"; import "../../components/ha-switch"; import type { CoreFrontendUserData } from "../../data/frontend"; import { saveFrontendUserData } from "../../data/frontend"; @@ -13,32 +13,33 @@ import type { HomeAssistant } from "../../types"; class EntityIdPickerRow extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ type: Boolean }) public narrow = false; - @property({ attribute: false }) public coreUserData?: CoreFrontendUserData; @state() private _error?: string; protected render(): TemplateResult { - return html` - ${this._error + return html`${this._error ? html`${this._error}` : nothing} - - - ${this.hass.localize("ui.panel.profile.entity_id_picker.title")} + ${this.hass.localize( + "ui.panel.profile.entity_id_picker.title" + )} + ${this.hass.localize( + "ui.panel.profile.entity_id_picker.description" + )} - - ${this.hass.localize("ui.panel.profile.entity_id_picker.description")} - - - `; + `; } private async _toggled(ev) { diff --git a/src/panels/profile/ha-force-narrow-row.ts b/src/panels/profile/ha-force-narrow-row.ts index d40ecc4d94..9e2b2bc555 100644 --- a/src/panels/profile/ha-force-narrow-row.ts +++ b/src/panels/profile/ha-force-narrow-row.ts @@ -2,7 +2,7 @@ import type { TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; -import "../../components/ha-settings-row"; +import "../../components/ha-md-list-item"; import "../../components/ha-switch"; import type { HaSwitch } from "../../components/ha-switch"; import type { HomeAssistant } from "../../types"; @@ -11,22 +11,23 @@ import type { HomeAssistant } from "../../types"; class HaForcedNarrowRow extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ type: Boolean }) public narrow = false; - protected render(): TemplateResult { return html` - - - ${this.hass.localize("ui.panel.profile.force_narrow.header")} - - - ${this.hass.localize("ui.panel.profile.force_narrow.description")} - + + ${this.hass.localize("ui.panel.profile.force_narrow.header")} + ${this.hass.localize( + "ui.panel.profile.force_narrow.description" + )} - + `; } diff --git a/src/panels/profile/ha-long-lived-access-token-dialog.ts b/src/panels/profile/ha-long-lived-access-token-dialog.ts index 69db923096..652530a83b 100644 --- a/src/panels/profile/ha-long-lived-access-token-dialog.ts +++ b/src/panels/profile/ha-long-lived-access-token-dialog.ts @@ -1,17 +1,18 @@ -import { mdiContentCopy } from "@mdi/js"; +import { mdiContentCopy, mdiQrcode } from "@mdi/js"; import type { CSSResultGroup, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; -import { createCloseHeading } from "../../components/ha-dialog"; +import { copyToClipboard } from "../../common/util/copy-clipboard"; +import { withViewTransition } from "../../common/util/view-transition"; +import "../../components/ha-alert"; import "../../components/ha-textfield"; import "../../components/ha-button"; -import "../../components/ha-icon-button"; -import { haStyleDialog } from "../../resources/styles"; +import "../../components/ha-dialog-footer"; +import "../../components/ha-svg-icon"; +import "../../components/ha-wa-dialog"; import type { HomeAssistant } from "../../types"; import type { LongLivedAccessTokenDialogParams } from "./show-long-lived-access-token-dialog"; -import type { HaTextField } from "../../components/ha-textfield"; -import { copyToClipboard } from "../../common/util/copy-clipboard"; import { showToast } from "../../util/toast"; const QR_LOGO_URL = "/static/icons/favicon-192x192.png"; @@ -20,81 +21,221 @@ const QR_LOGO_URL = "/static/icons/favicon-192x192.png"; export class HaLongLivedAccessTokenDialog extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _params?: LongLivedAccessTokenDialogParams; - @state() private _qrCode?: TemplateResult; + @state() private _open = false; + + @state() private _renderDialog = false; + + @state() private _name = ""; + + @state() private _token?: string; + + private _createdCallback!: () => void; + + private _existingNames = new Set(); + + @state() private _loading = false; + + @state() private _errorMessage?: string; + public showDialog(params: LongLivedAccessTokenDialogParams): void { - this._params = params; + this._createdCallback = params.createdCallback; + this._existingNames = new Set( + params.existingNames.map((name) => this._normalizeName(name)) + ); + this._renderDialog = true; + this._open = true; } public closeDialog() { - this._params = undefined; + this._open = false; + } + + private _dialogClosed() { + this._open = false; + this._renderDialog = false; + this._name = ""; + this._token = undefined; + this._existingNames = new Set(); + this._errorMessage = undefined; + this._loading = false; this._qrCode = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); } protected render() { - if (!this._params || !this._params.token) { + if (!this._renderDialog) { return nothing; } return html` - -
    - - - -
    - ${this._qrCode - ? this._qrCode - : html` - - ${this.hass.localize( - "ui.panel.profile.long_lived_access_tokens.generate_qr_code" - )} + prevent-scrim-close + @closed=${this._dialogClosed} + > +
    + ${this._errorMessage + ? html`${this._errorMessage}` + : nothing} + ${this._token + ? html` +

    + ${this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.prompt_copy_token" + )} +

    +
    + + + + ${this.hass.localize("ui.common.copy")} - `} -
    +
    +
    + ${this._qrCode + ? this._qrCode + : html` + + + ${this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.generate_qr_code" + )} + + `} +
    + ` + : html` + + `}
    - + + ${this._token + ? nothing + : html` + ${this.hass.localize("ui.common.cancel")} + `} + ${!this._token + ? html` + ${this.hass.localize( + "ui.panel.profile.long_lived_access_tokens.create" + )} + ` + : html` + ${this.hass.localize("ui.common.close")} + `} + + `; } - private async _copyToken(ev): Promise { - const textField = ev.target.parentElement as HaTextField; - await copyToClipboard(textField.value); + private _nameChanged(ev: Event) { + this._name = (ev.currentTarget as HTMLInputElement).value; + this._errorMessage = undefined; + } + + private _isCreateDisabled() { + return this._loading || !this._name.trim() || this._hasDuplicateName(); + } + + private async _createToken(): Promise { + if (this._isCreateDisabled()) { + return; + } + + const name = this._name.trim(); + + this._loading = true; + this._errorMessage = undefined; + + try { + this._token = await this.hass.callWS({ + type: "auth/long_lived_access_token", + lifespan: 3650, + client_name: name, + }); + this._name = name; + this._createdCallback(); + } catch (err: unknown) { + this._errorMessage = err instanceof Error ? err.message : String(err); + } finally { + this._loading = false; + } + } + + private async _copyToken(): Promise { + if (!this._token) { + return; + } + + await copyToClipboard(this._token); showToast(this, { message: this.hass.localize("ui.common.copied_clipboard"), }); } + private _normalizeName(name: string): string { + return name.trim().toLowerCase(); + } + + private _hasDuplicateName(): boolean { + return this._existingNames.has(this._normalizeName(this._name)); + } + private async _generateQR() { + if (!this._token) { + return; + } + const qrcode = await import("qrcode"); - const canvas = await qrcode.toCanvas(this._params!.token, { - width: 180, + const canvas = await qrcode.toCanvas(this._token, { + width: 512, errorCorrectionLevel: "Q", }); const context = canvas.getContext("2d"); @@ -112,35 +253,46 @@ export class HaLongLivedAccessTokenDialog extends LitElement { canvas.height / 3 ); - this._qrCode = html`${this.hass.localize(`; + await withViewTransition(() => { + this._qrCode = html`${this.hass.localize(`; + }); } static get styles(): CSSResultGroup { return [ - haStyleDialog, css` #qr { text-align: center; } + #qr img { + max-width: 90%; + height: auto; + display: block; + margin: 0 auto; + } + .content { + display: grid; + gap: var(--ha-space-4); + } + .token-row { + display: flex; + gap: var(--ha-space-2); + align-items: center; + } + .token-row ha-textfield { + flex: 1; + } + p { + margin: 0; + } ha-textfield { display: block; - --textfield-icon-trailing-padding: 0; - } - ha-textfield > ha-icon-button { - position: relative; - right: -8px; - --mdc-icon-button-size: 36px; - --mdc-icon-size: 20px; - color: var(--secondary-text-color); - inset-inline-start: initial; - inset-inline-end: -8px; - direction: var(--direction); } `, ]; diff --git a/src/panels/profile/ha-long-lived-access-tokens-card.ts b/src/panels/profile/ha-long-lived-access-tokens-card.ts index f6941feb04..cfe95e3543 100644 --- a/src/panels/profile/ha-long-lived-access-tokens-card.ts +++ b/src/panels/profile/ha-long-lived-access-tokens-card.ts @@ -13,7 +13,6 @@ import type { RefreshToken } from "../../data/refresh_token"; import { showAlertDialog, showConfirmationDialog, - showPromptDialog, } from "../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; @@ -26,14 +25,14 @@ class HaLongLivedTokens extends LitElement { @property({ attribute: false }) public refreshTokens?: RefreshToken[]; private _accessTokens = memoizeOne( - (refreshTokens: RefreshToken[]): RefreshToken[] => - refreshTokens - ?.filter((token) => token.type === "long_lived_access_token") + (refreshTokens?: RefreshToken[]): RefreshToken[] => + (refreshTokens ?? []) + .filter((token) => token.type === "long_lived_access_token") .reverse() ); protected render(): TemplateResult { - const accessTokens = this._accessTokens(this.refreshTokens!); + const accessTokens = this._accessTokens(this.refreshTokens); return html` - ${!accessTokens?.length + ${!accessTokens.length ? html`

    ${this.hass.localize( "ui.panel.profile.long_lived_access_tokens.empty_state" )}

    ` - : accessTokens!.map( + : accessTokens.map( (token) => html` ${token.client_name} @@ -98,38 +97,15 @@ class HaLongLivedTokens extends LitElement { `; } - private async _createToken(): Promise { - const name = await showPromptDialog(this, { - text: this.hass.localize( - "ui.panel.profile.long_lived_access_tokens.prompt_name" - ), - inputLabel: this.hass.localize( - "ui.panel.profile.long_lived_access_tokens.name" - ), + private _createToken(): void { + const accessTokens = this._accessTokens(this.refreshTokens); + + showLongLivedAccessTokenDialog(this, { + createdCallback: () => fireEvent(this, "hass-refresh-tokens"), + existingNames: accessTokens + .map((token) => token.client_name) + .filter((name): name is string => Boolean(name)), }); - - if (!name) { - return; - } - - try { - const token = await this.hass.callWS({ - type: "auth/long_lived_access_token", - lifespan: 3650, - client_name: name, - }); - - showLongLivedAccessTokenDialog(this, { token, name }); - - fireEvent(this, "hass-refresh-tokens"); - } catch (err: any) { - showAlertDialog(this, { - title: this.hass.localize( - "ui.panel.profile.long_lived_access_tokens.create_failed" - ), - text: err.message, - }); - } } private async _deleteToken(ev: Event): Promise { diff --git a/src/panels/profile/ha-pick-dashboard-row.ts b/src/panels/profile/ha-pick-dashboard-row.ts index c97dab2efa..862367eeb4 100644 --- a/src/panels/profile/ha-pick-dashboard-row.ts +++ b/src/panels/profile/ha-pick-dashboard-row.ts @@ -1,14 +1,15 @@ -import { mdiViewDashboard } from "@mdi/js"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import "../../components/ha-divider"; +import "../../components/ha-dropdown-item"; import "../../components/ha-icon"; -import "../../components/ha-list-item"; +import type { HaSelectSelectEvent } from "../../components/ha-select"; import "../../components/ha-select"; import "../../components/ha-settings-row"; -import "../../components/ha-svg-icon"; import "../../components/ha-spinner"; +import "../../components/ha-svg-icon"; import { saveFrontendUserData } from "../../data/frontend"; import type { LovelaceDashboard } from "../../data/lovelace/dashboard"; import { fetchDashboards } from "../../data/lovelace/dashboard"; @@ -47,21 +48,16 @@ class HaPickDashboardRow extends LitElement { .label=${this.hass.localize( "ui.panel.profile.dashboard.dropdown_label" )} - .value=${value} + .value=${this._valueLabel(value)} @selected=${this._dashboardChanged} - naturalMenuWidth > - + ${this.hass.localize("ui.panel.profile.dashboard.system")} - + - - - ${this.hass.localize("ui.panel.profile.dashboard.lovelace")} - ${PANEL_DASHBOARDS.map((panel) => { const panelInfo = this.hass.panels[panel] as | PanelInfo @@ -70,13 +66,16 @@ class HaPickDashboardRow extends LitElement { return nothing; } return html` - + ${getPanelTitle(this.hass, panelInfo)} - + `; })} ${this._dashboards.length @@ -90,16 +89,16 @@ class HaPickDashboardRow extends LitElement { return ""; } return html` - ${dashboard.title} - + `; })} ` @@ -117,8 +116,8 @@ class HaPickDashboardRow extends LitElement { this._dashboards = await fetchDashboards(this.hass); } - private _dashboardChanged(ev) { - const value = ev.target.value as string; + private _dashboardChanged(ev: HaSelectSelectEvent): void { + const value = ev.detail.value; if (!value) { return; } @@ -132,6 +131,24 @@ class HaPickDashboardRow extends LitElement { }); } + private _valueLabel = memoizeOne((value: string) => { + if (value === USE_SYSTEM_VALUE) { + return this.hass.localize("ui.panel.profile.dashboard.system"); + } + if (value === "lovelace") { + return this.hass.localize("ui.panel.profile.dashboard.lovelace"); + } + const panelInfo = this.hass.panels[value] as PanelInfo | undefined; + if (panelInfo) { + return getPanelTitle(this.hass, panelInfo); + } + const dashboard = this._dashboards?.find((dash) => dash.url_path === value); + if (dashboard) { + return dashboard.title; + } + return value; + }); + static get styles(): CSSResultGroup { return [ css` @@ -142,6 +159,11 @@ class HaPickDashboardRow extends LitElement { height: 56px; width: 200px; } + + ha-select { + display: block; + width: 100%; + } `, ]; } diff --git a/src/panels/profile/ha-pick-date-format-row.ts b/src/panels/profile/ha-pick-date-format-row.ts index ead7a7cf0f..9f3ec9024a 100644 --- a/src/panels/profile/ha-pick-date-format-row.ts +++ b/src/panels/profile/ha-pick-date-format-row.ts @@ -1,10 +1,10 @@ import type { TemplateResult } from "lit"; -import { html, LitElement } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { formatDateNumeric } from "../../common/datetime/format_date"; import { fireEvent } from "../../common/dom/fire_event"; import "../../components/ha-card"; -import "../../components/ha-list-item"; +import type { HaSelectSelectEvent } from "../../components/ha-select"; import "../../components/ha-select"; import "../../components/ha-settings-row"; import { DateFormat } from "../../data/translation"; @@ -33,33 +33,36 @@ class DateFormatRow extends LitElement { .disabled=${this.hass.locale === undefined} .value=${this.hass.locale.date_format} @selected=${this._handleFormatSelection} - naturalMenuWidth - > - ${Object.values(DateFormat).map((format) => { - const formattedDate = formatDateNumeric( + .options=${Object.values(DateFormat).map((format) => ({ + value: format.toString(), + label: this.hass.localize( + `ui.panel.profile.date_format.formats.${format}` + ), + secondary: formatDateNumeric( date, { ...this.hass.locale, date_format: format, }, this.hass.config - ); - const value = this.hass.localize( - `ui.panel.profile.date_format.formats.${format}` - ); - return html` - ${value} - ${formattedDate} - `; - })} + ), + }))} + > `; } - private async _handleFormatSelection(ev) { - fireEvent(this, "hass-date-format-select", ev.target.value); + private async _handleFormatSelection(ev: HaSelectSelectEvent) { + fireEvent(this, "hass-date-format-select", ev.detail.value); } + + static styles = css` + ha-select { + display: block; + width: 100%; + } + `; } declare global { diff --git a/src/panels/profile/ha-pick-first-weekday-row.ts b/src/panels/profile/ha-pick-first-weekday-row.ts index e793c93151..2fca673498 100644 --- a/src/panels/profile/ha-pick-first-weekday-row.ts +++ b/src/panels/profile/ha-pick-first-weekday-row.ts @@ -1,9 +1,9 @@ import type { TemplateResult } from "lit"; -import { html, LitElement } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { firstWeekday } from "../../common/datetime/first_weekday"; import { fireEvent } from "../../common/dom/fire_event"; -import "../../components/ha-list-item"; +import type { HaSelectSelectEvent } from "../../components/ha-select"; import "../../components/ha-select"; import "../../components/ha-settings-row"; import { FirstWeekday } from "../../data/translation"; @@ -31,43 +31,40 @@ class FirstWeekdayRow extends LitElement { .disabled=${this.hass.locale === undefined} .value=${this.hass.locale.first_weekday} @selected=${this._handleFormatSelection} - naturalMenuWidth - > - ${[ + .options=${[ FirstWeekday.language, FirstWeekday.monday, FirstWeekday.saturday, FirstWeekday.sunday, - ].map((day) => { - const value = this.hass.localize( + ].map((day) => ({ + value: day.toString(), + label: this.hass.localize( `ui.panel.profile.first_weekday.values.${day}` - ); - const twoLine = day === FirstWeekday.language; - return html` - - ${value} - ${twoLine - ? html` - ${this.hass.localize( - `ui.panel.profile.first_weekday.values.${firstWeekday( - this.hass.locale - )}` - )} - ` - : ""} - - `; - })} - + ), + secondary: + day === FirstWeekday.language + ? this.hass.localize( + `ui.panel.profile.first_weekday.values.${firstWeekday( + this.hass.locale + )}` + ) + : undefined, + }))} + > `; } - private async _handleFormatSelection(ev) { - fireEvent(this, "hass-first-weekday-select", ev.target.value); + private async _handleFormatSelection(ev: HaSelectSelectEvent) { + fireEvent(this, "hass-first-weekday-select", ev.detail.value); } + + static styles = css` + ha-select { + display: block; + width: 100%; + } + `; } declare global { diff --git a/src/panels/profile/ha-pick-number-format-row.ts b/src/panels/profile/ha-pick-number-format-row.ts index 356fd6122f..05e2b2612f 100644 --- a/src/panels/profile/ha-pick-number-format-row.ts +++ b/src/panels/profile/ha-pick-number-format-row.ts @@ -1,10 +1,10 @@ import type { TemplateResult } from "lit"; -import { html, LitElement } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import { formatNumber } from "../../common/number/format_number"; import "../../components/ha-card"; -import "../../components/ha-list-item"; +import type { HaSelectSelectEvent } from "../../components/ha-select"; import "../../components/ha-select"; import "../../components/ha-settings-row"; import { NumberFormat } from "../../data/translation"; @@ -32,34 +32,38 @@ class NumberFormatRow extends LitElement { .disabled=${this.hass.locale === undefined} .value=${this.hass.locale.number_format} @selected=${this._handleFormatSelection} - naturalMenuWidth - > - ${Object.values(NumberFormat).map((format) => { - const formattedNumber = formatNumber(1234567.89, { - ...this.hass.locale, - number_format: format, - }); - const value = this.hass.localize( + .options=${Object.values(NumberFormat).map((format) => { + const label = this.hass.localize( `ui.panel.profile.number_format.formats.${format}` ); - const twoLine = value.slice(value.length - 2) !== "89"; // Display explicit number formats on one line - return html` - - ${value} - ${twoLine - ? html`${formattedNumber}` - : ""} - - `; + return { + value: format, + label, + secondary: + label.slice(label.length - 2) !== "89" + ? formatNumber(1234567.89, { + ...this.hass.locale, + number_format: format, + }) + : undefined, + }; })} + > `; } - private async _handleFormatSelection(ev) { - fireEvent(this, "hass-number-format-select", ev.target.value); + private async _handleFormatSelection(ev: HaSelectSelectEvent) { + fireEvent(this, "hass-number-format-select", ev.detail.value); } + + static styles = css` + ha-select { + display: block; + width: 100%; + } + `; } declare global { diff --git a/src/panels/profile/ha-pick-theme-row.ts b/src/panels/profile/ha-pick-theme-row.ts index 0ed5a41f59..d6384de0ca 100644 --- a/src/panels/profile/ha-pick-theme-row.ts +++ b/src/panels/profile/ha-pick-theme-row.ts @@ -3,23 +3,23 @@ import { css, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import { normalizeLuminance } from "../../common/color/palette"; import { fireEvent } from "../../common/dom/fire_event"; -import "../../components/ha-formfield"; -import "../../components/ha-list-item"; -import "../../components/ha-radio"; import "../../components/ha-button"; +import "../../components/ha-formfield"; +import "../../components/ha-radio"; import type { HaRadio } from "../../components/ha-radio"; +import type { HaSelectSelectEvent } from "../../components/ha-select"; import "../../components/ha-select"; import "../../components/ha-settings-row"; import "../../components/ha-textfield"; -import { - DefaultAccentColor, - DefaultPrimaryColor, -} from "../../resources/theme/color/color.globals"; import { saveThemePreferences, subscribeThemePreferences, } from "../../data/theme"; import { SubscribeMixin } from "../../mixins/subscribe-mixin"; +import { + DefaultAccentColor, + DefaultPrimaryColor, +} from "../../resources/theme/color/color.globals"; import type { HomeAssistant, ThemeSettings } from "../../types"; import { documentationUrl } from "../../util/documentation-url"; import { clearSelectedThemeState } from "../../util/ha-pref-storage"; @@ -93,19 +93,18 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) { .disabled=${!hasThemes} .value=${this.hass.selectedTheme?.theme || USE_DEFAULT_THEME} @selected=${this._handleThemeSelection} - naturalMenuWidth + .options=${[ + { + value: USE_DEFAULT_THEME, + label: this.hass.localize("ui.panel.profile.themes.use_default"), + }, + { value: HOME_ASSISTANT_THEME, label: "Home Assistant" }, + ...this._themeNames.map((theme) => ({ + value: theme, + label: theme, + })), + ]} > - - ${this.hass.localize("ui.panel.profile.themes.use_default")} - - - Home Assistant - - ${this._themeNames.map( - (theme) => html` - ${theme} - ` - )} ${curTheme === HOME_ASSISTANT_THEME || @@ -261,8 +260,8 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) { fireEvent(this, "settheme", { dark }); } - private _handleThemeSelection(ev) { - const theme = ev.target.value; + private _handleThemeSelection(ev: HaSelectSelectEvent) { + const theme = ev.detail.value; if (theme === this.hass.selectedTheme?.theme) { return; } @@ -334,6 +333,11 @@ export class HaPickThemeRow extends SubscribeMixin(LitElement) { flex-grow: 1; margin: 0 4px; } + + ha-select { + display: block; + width: 100%; + } `; } diff --git a/src/panels/profile/ha-pick-time-format-row.ts b/src/panels/profile/ha-pick-time-format-row.ts index 17644a7975..835bf54fed 100644 --- a/src/panels/profile/ha-pick-time-format-row.ts +++ b/src/panels/profile/ha-pick-time-format-row.ts @@ -1,10 +1,10 @@ import type { TemplateResult } from "lit"; -import { html, LitElement } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { formatTime } from "../../common/datetime/format_time"; import { fireEvent } from "../../common/dom/fire_event"; import "../../components/ha-card"; -import "../../components/ha-list-item"; +import type { HaSelectSelectEvent } from "../../components/ha-select"; import "../../components/ha-select"; import "../../components/ha-settings-row"; import { TimeFormat } from "../../data/translation"; @@ -33,33 +33,36 @@ class TimeFormatRow extends LitElement { .disabled=${this.hass.locale === undefined} .value=${this.hass.locale.time_format} @selected=${this._handleFormatSelection} - naturalMenuWidth - > - ${Object.values(TimeFormat).map((format) => { - const formattedTime = formatTime( + .options=${Object.values(TimeFormat).map((format) => ({ + value: format.toString(), + label: this.hass.localize( + `ui.panel.profile.time_format.formats.${format}` + ), + secondary: formatTime( date, { ...this.hass.locale, time_format: format, }, this.hass.config - ); - const value = this.hass.localize( - `ui.panel.profile.time_format.formats.${format}` - ); - return html` - ${value} - ${formattedTime} - `; - })} + ), + }))} + > `; } - private async _handleFormatSelection(ev) { - fireEvent(this, "hass-time-format-select", ev.target.value); + private async _handleFormatSelection(ev: HaSelectSelectEvent) { + fireEvent(this, "hass-time-format-select", ev.detail.value); } + + static styles = css` + ha-select { + display: block; + width: 100%; + } + `; } declare global { diff --git a/src/panels/profile/ha-pick-time-zone-row.ts b/src/panels/profile/ha-pick-time-zone-row.ts index a237eb6ec9..0a46e3f1f5 100644 --- a/src/panels/profile/ha-pick-time-zone-row.ts +++ b/src/panels/profile/ha-pick-time-zone-row.ts @@ -1,11 +1,11 @@ import type { TemplateResult } from "lit"; -import { html, LitElement } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { formatDateTimeNumeric } from "../../common/datetime/format_date_time"; import { resolveTimeZone } from "../../common/datetime/resolve-time-zone"; import { fireEvent } from "../../common/dom/fire_event"; import "../../components/ha-card"; -import "../../components/ha-list-item"; +import type { HaSelectSelectEvent } from "../../components/ha-select"; import "../../components/ha-select"; import "../../components/ha-settings-row"; import { TimeZone } from "../../data/translation"; @@ -34,40 +34,42 @@ class TimeZoneRow extends LitElement { .disabled=${this.hass.locale === undefined} .value=${this.hass.locale.time_zone} @selected=${this._handleFormatSelection} - naturalMenuWidth - > - ${Object.values(TimeZone).map((format) => { - const formattedTime = formatDateTimeNumeric( + .options=${Object.values(TimeZone).map((format) => ({ + value: format.toString(), + label: this.hass.localize( + `ui.panel.profile.time_zone.options.${format}`, + { + timezone: resolveTimeZone( + format, + this.hass.config.time_zone + ).replace("_", " "), + } + ), + secondary: formatDateTimeNumeric( date, { ...this.hass.locale, time_zone: format, }, this.hass.config - ); - return html` - ${this.hass.localize( - `ui.panel.profile.time_zone.options.${format}`, - { - timezone: resolveTimeZone( - format, - this.hass.config.time_zone - ).replace("_", " "), - } - )} - ${formattedTime} - `; - })} + ), + }))} + > `; } - private async _handleFormatSelection(ev) { - fireEvent(this, "hass-time-zone-select", ev.target.value); + private async _handleFormatSelection(ev: HaSelectSelectEvent) { + fireEvent(this, "hass-time-zone-select", ev.detail.value); } + + static styles = css` + ha-select { + display: block; + width: 100%; + } + `; } declare global { diff --git a/src/panels/profile/ha-profile-section-general.ts b/src/panels/profile/ha-profile-section-general.ts index b5d99486a9..4348f2a113 100644 --- a/src/panels/profile/ha-profile-section-general.ts +++ b/src/panels/profile/ha-profile-section-general.ts @@ -6,6 +6,9 @@ import { fireEvent } from "../../common/dom/fire_event"; import { nextRender } from "../../common/util/render-status"; import "../../components/ha-button"; import "../../components/ha-card"; +import "../../components/ha-divider"; +import "../../components/ha-md-list"; +import "../../components/ha-md-list-item"; import { isExternal } from "../../data/external"; import type { CoreFrontendUserData } from "../../data/frontend"; import { subscribeFrontendUserData } from "../../data/frontend"; @@ -31,6 +34,7 @@ import "./ha-pick-time-zone-row"; import "./ha-push-notifications-row"; import "./ha-set-suspend-row"; import "./ha-set-vibrate-row"; +import { isComponentLoaded } from "../../common/config/is_component_loaded"; @customElement("ha-profile-section-general") class HaProfileSectionGeneral extends LitElement { @@ -130,6 +134,55 @@ class HaProfileSectionGeneral extends LitElement {
    ${this.hass.localize("ui.panel.profile.user_settings_detail")}
    + + + + + ${this.hass.localize( + "ui.panel.profile.customize_sidebar.header" + )} + ${this.hass.localize( + "ui.panel.profile.customize_sidebar.description" + )} + + ${this.hass.localize( + "ui.panel.profile.customize_sidebar.button" + )} + + + ${this.hass.user!.is_admin + ? html` + + ` + : ""} + ${this.hass.user!.is_admin + ? html` + + ` + : ""} + + - - - - - ${this.hass.localize( - "ui.panel.profile.customize_sidebar.header" - )} - - - ${this.hass.localize( - "ui.panel.profile.customize_sidebar.description" - )} - - - ${this.hass.localize( - "ui.panel.profile.customize_sidebar.button" - )} - - - ${this.hass.user!.is_admin - ? html` - - ` - : ""} - ${this.hass.user!.is_admin - ? html` - - ` - : ""}
    ${this.hass.localize("ui.panel.profile.client_settings_detail")}
    - ${this.hass.dockedSidebar !== "auto" || !this.narrow - ? html` - - ` - : ""} - ${"vibrate" in navigator - ? html` - - ` - : ""} - ${!isExternal - ? html` - - ` - : ""} - - ${!isMobileClient - ? html` - - ` - : ""} + + ${this.hass.dockedSidebar !== "auto" || !this.narrow + ? html` + + ` + : ""} + ${"vibrate" in navigator + ? html` + + ` + : ""} + ${!isExternal && isComponentLoaded(this.hass, "html5.notify") + ? html` + + ` + : ""} + + ${!isMobileClient + ? html` + + ` + : ""} +
    @@ -295,6 +294,12 @@ class HaProfileSectionGeneral extends LitElement { text-align: center; color: var(--secondary-text-color); } + + ha-md-list { + background: none; + padding-top: 0; + padding-bottom: 0; + } `, ]; } diff --git a/src/panels/profile/ha-push-notifications-row.ts b/src/panels/profile/ha-push-notifications-row.ts index dd9abe6ef1..9e4a530e21 100644 --- a/src/panels/profile/ha-push-notifications-row.ts +++ b/src/panels/profile/ha-push-notifications-row.ts @@ -3,7 +3,7 @@ import { LitElement, css, html } from "lit"; import { customElement, property } from "lit/decorators"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { pushSupported } from "../../components/ha-push-notifications-toggle"; -import "../../components/ha-settings-row"; +import "../../components/ha-md-list-item"; import { documentationUrl } from "../../util/documentation-url"; import type { HomeAssistant } from "../../types"; @@ -11,8 +11,6 @@ import type { HomeAssistant } from "../../types"; class HaPushNotificationsRow extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ type: Boolean }) public narrow = false; - protected render(): TemplateResult { const platformLoaded = isComponentLoaded(this.hass, "html5.notify"); let descriptionKey: @@ -30,14 +28,14 @@ class HaPushNotificationsRow extends LitElement { const isDisabled = !platformLoaded || !pushSupported; return html` - - + ${this.hass.localize( "ui.panel.profile.push_notifications.header" )} - - ${this.hass.localize( + ${this.hass.localize( `ui.panel.profile.push_notifications.${descriptionKey}` )} ${this.hass.localize( "ui.panel.profile.push_notifications.link_promo" )} - + > - + `; } diff --git a/src/panels/profile/ha-refresh-tokens-card.ts b/src/panels/profile/ha-refresh-tokens-card.ts index f960a43cc7..dfb8385f05 100644 --- a/src/panels/profile/ha-refresh-tokens-card.ts +++ b/src/panels/profile/ha-refresh-tokens-card.ts @@ -18,10 +18,6 @@ import "../../components/ha-card"; import "../../components/ha-dropdown"; import "../../components/ha-dropdown-item"; import "../../components/ha-icon-button"; -import "../../components/ha-label"; -import "../../components/ha-list-item"; -import "../../components/ha-md-button-menu"; -import "../../components/ha-md-menu-item"; import "../../components/ha-settings-row"; import { deleteAllRefreshTokens } from "../../data/auth"; import type { RefreshToken } from "../../data/refresh_token"; diff --git a/src/panels/profile/ha-set-suspend-row.ts b/src/panels/profile/ha-set-suspend-row.ts index 4cfd25f3ce..2f5776984d 100644 --- a/src/panels/profile/ha-set-suspend-row.ts +++ b/src/panels/profile/ha-set-suspend-row.ts @@ -3,7 +3,7 @@ import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import type { HASSDomEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event"; -import "../../components/ha-settings-row"; +import "../../components/ha-md-list-item"; import "../../components/ha-switch"; import type { HaSwitch } from "../../components/ha-switch"; import type { HomeAssistant } from "../../types"; @@ -25,22 +25,21 @@ declare global { class HaSetSuspendRow extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ type: Boolean }) public narrow = false; - protected render(): TemplateResult { return html` - - - ${this.hass.localize("ui.panel.profile.suspend.header")} - - - ${this.hass.localize("ui.panel.profile.suspend.description")} - + + ${this.hass.localize("ui.panel.profile.suspend.header")} + ${this.hass.localize("ui.panel.profile.suspend.description")} - + `; } diff --git a/src/panels/profile/ha-set-vibrate-row.ts b/src/panels/profile/ha-set-vibrate-row.ts index ae974dde05..2987a7d208 100644 --- a/src/panels/profile/ha-set-vibrate-row.ts +++ b/src/panels/profile/ha-set-vibrate-row.ts @@ -2,7 +2,7 @@ import type { TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; -import "../../components/ha-settings-row"; +import "../../components/ha-md-list-item"; import "../../components/ha-switch"; import type { HaSwitch } from "../../components/ha-switch"; import { forwardHaptic } from "../../data/haptics"; @@ -12,22 +12,21 @@ import type { HomeAssistant } from "../../types"; class HaSetVibrateRow extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ type: Boolean }) public narrow = false; - protected render(): TemplateResult { return html` - - - ${this.hass.localize("ui.panel.profile.vibrate.header")} - - - ${this.hass.localize("ui.panel.profile.vibrate.description")} - + + ${this.hass.localize("ui.panel.profile.vibrate.header")} + ${this.hass.localize("ui.panel.profile.vibrate.description")} - + `; } diff --git a/src/panels/profile/show-long-lived-access-token-dialog.ts b/src/panels/profile/show-long-lived-access-token-dialog.ts index 00ac5a9a53..fd47a877f6 100644 --- a/src/panels/profile/show-long-lived-access-token-dialog.ts +++ b/src/panels/profile/show-long-lived-access-token-dialog.ts @@ -1,8 +1,8 @@ import { fireEvent } from "../../common/dom/fire_event"; export interface LongLivedAccessTokenDialogParams { - token: string; - name: string; + createdCallback: () => void; + existingNames: string[]; } export const showLongLivedAccessTokenDialog = ( diff --git a/src/panels/security/ha-panel-security.ts b/src/panels/security/ha-panel-security.ts index 4d09967172..baeff65736 100644 --- a/src/panels/security/ha-panel-security.ts +++ b/src/panels/security/ha-panel-security.ts @@ -1,6 +1,7 @@ import type { CSSResultGroup, PropertyValues } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; import { goBack } from "../../common/navigate"; import { debounce } from "../../common/util/debounce"; import { deepEqual } from "../../common/util/deep-equal"; @@ -95,7 +96,7 @@ class PanelSecurity extends LitElement { protected render() { return html` -
    +
    ${ this._searchParms.has("historyBack") @@ -175,7 +176,6 @@ class PanelSecurity extends LitElement { .header { background-color: var(--app-header-background-color); color: var(--app-header-text-color, white); - border-bottom: var(--app-header-border-bottom, none); position: fixed; top: 0; width: calc( @@ -186,7 +186,6 @@ class PanelSecurity extends LitElement { ); padding-top: var(--safe-area-inset-top); z-index: 4; - transition: box-shadow 200ms linear; display: flex; flex-direction: row; -webkit-backdrop-filter: var(--app-header-backdrop-filter, none); @@ -220,15 +219,19 @@ class PanelSecurity extends LitElement { padding: 0px 12px; font-weight: var(--ha-font-weight-normal); box-sizing: border-box; + border-bottom: var(--app-header-border-bottom, none); } :host([narrow]) .toolbar { padding: 0 4px; } .main-title { - margin: var(--margin-title); + margin-inline-start: var(--ha-space-6); line-height: var(--ha-line-height-normal); flex-grow: 1; } + .narrow .main-title { + margin-inline-start: var(--ha-space-2); + } hui-view-container { position: relative; display: flex; diff --git a/src/panels/todo/ha-panel-todo.ts b/src/panels/todo/ha-panel-todo.ts index e8346c2ab7..cd4139aea0 100644 --- a/src/panels/todo/ha-panel-todo.ts +++ b/src/panels/todo/ha-panel-todo.ts @@ -24,7 +24,6 @@ import { extractSearchParam, } from "../../common/url/search-params"; import "../../components/ha-button"; -import "../../components/ha-button-menu"; import "../../components/ha-dropdown"; import "../../components/ha-dropdown-item"; import type { HaDropdownItem } from "../../components/ha-dropdown-item"; @@ -51,6 +50,7 @@ import { haStyle } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; import "../lovelace/cards/hui-card"; import { showTodoItemEditDialog } from "./show-dialog-todo-item-editor"; +import type { HaDropdownSelectEvent } from "../../components/ha-dropdown"; @customElement("ha-panel-todo") class PanelTodo extends LitElement { @@ -67,8 +67,6 @@ class PanelTodo extends LitElement { }) private _entityId?: string; - private _headerHeight = 56; - private _showPaneController = new ResizeController(this, { callback: (entries) => entries[0]?.contentRect.width > 750, }); @@ -86,10 +84,6 @@ class PanelTodo extends LitElement { ); this._mql.addListener(this._setIsMobile); this.mobile = this._mql.matches; - const computedStyles = getComputedStyle(this); - this._headerHeight = Number( - computedStyles.getPropertyValue("--header-height").replace("px", "") - ); } public disconnectedCallback() { @@ -116,7 +110,7 @@ class PanelTodo extends LitElement { this._entityId = undefined; } if (!this._entityId) { - this._entityId = getTodoLists(this.hass)[0]?.entity_id; + this._entityId = getTodoLists(this.hass, false)[0]?.entity_id; } } } @@ -153,21 +147,20 @@ class PanelTodo extends LitElement { ? this.hass.states[this._entityId] : undefined; const showPane = this._showPaneController.value ?? !this.narrow; - const listItems = getTodoLists(this.hass).map( + const listItems = getTodoLists(this.hass, false).map( (list) => - html` ${list.name} - ` + ` ); return html`
    ${!showPane - ? html` + ? html`
    ${this._entityId ? entityState ? computeStateName(entityState) : this._entityId - : ""} + : nothing}
    ${listItems} ${this.hass.user?.is_admin - ? html`
  • - - + ? html` + + ${this.hass.localize("ui.panel.todo.create_list")} - ` + ` : nothing} -
    ` + ` : this.hass.localize("panel.todo")}
    ${listItems} @@ -288,10 +269,6 @@ class PanelTodo extends LitElement { `; } - private _handleEntityPicked(ev) { - this._entityId = ev.currentTarget.entityId; - } - private async _addList(): Promise { showConfigFlowDialog(this, { startFlowHandler: "local_todo", @@ -345,7 +322,7 @@ class PanelTodo extends LitElement { } const result = await deleteConfigEntry(this.hass, entryId); - this._entityId = getTodoLists(this.hass)[0]?.entity_id; + this._entityId = getTodoLists(this.hass, false)[0]?.entity_id; if (result.require_restart) { showAlertDialog(this, { @@ -362,7 +339,7 @@ class PanelTodo extends LitElement { showTodoItemEditDialog(this, { entity: this._entityId! }); } - private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) { + private _handleDropdownSelect(ev: HaDropdownSelectEvent) { const action = ev.detail?.item?.value; if (!action) { @@ -382,6 +359,12 @@ class PanelTodo extends LitElement { } } + private _setEntityId(ev: Event) { + const item = ev.currentTarget as HaDropdownItem; + + this._entityId = item.value; + } + static get styles(): CSSResultGroup { return [ haStyle, @@ -401,23 +384,14 @@ class PanelTodo extends LitElement { max-width: 500px; min-width: 0; } - :host([mobile]) .lists { - --mdc-menu-min-width: 100vw; - } - :host(:not([mobile])) .lists ha-list-item { - max-width: calc(100vw - 120px); - } - :host([mobile]) ha-button-menu { - --mdc-shape-medium: 0 0 var(--mdc-shape-medium) - var(--mdc-shape-medium); - } - ha-button-menu { + ha-dropdown { + display: inline-block; max-width: 100%; } - ha-button-menu ha-button { + ha-dropdown ha-button { --ha-font-size-m: var(--ha-font-size-l); } - ha-button-menu ha-button div { + ha-dropdown ha-button div { text-overflow: ellipsis; width: 100%; overflow: hidden; @@ -431,6 +405,10 @@ class PanelTodo extends LitElement { inset-inline-end: calc(16px + var(--safe-area-inset-right, 0px)); inset-inline-start: initial; } + + ha-dropdown.lists ha-dropdown-item { + max-width: 80vw; + } `, ]; } diff --git a/src/resources/codemirror.ts b/src/resources/codemirror.ts index 4cb2261b6d..3e199d79cf 100644 --- a/src/resources/codemirror.ts +++ b/src/resources/codemirror.ts @@ -16,7 +16,9 @@ export { autocompletion } from "@codemirror/autocomplete"; export { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; export { highlightingFor, foldGutter } from "@codemirror/language"; export { + closeSearchPanel, highlightSelectionMatches, + openSearchPanel, search, searchKeymap, } from "@codemirror/search"; diff --git a/src/resources/styles.ts b/src/resources/styles.ts index 047f94b4ad..e9276eb610 100644 --- a/src/resources/styles.ts +++ b/src/resources/styles.ts @@ -34,6 +34,20 @@ export const haStyle = css` margin-inline-end: initial; } + .header { + transition: + box-shadow 200ms linear, + width var(--ha-animation-duration-normal) ease, + padding-left var(--ha-animation-duration-normal) ease, + padding-right var(--ha-animation-duration-normal) ease; + } + + @media (prefers-reduced-motion: reduce) { + .header { + transition: box-shadow 200ms linear; + } + } + h1 { font-family: var(--ha-font-family-heading); -webkit-font-smoothing: var(--ha-font-smoothing); diff --git a/src/resources/theme/color/color.globals.ts b/src/resources/theme/color/color.globals.ts index a25ae8f9fe..be1e2e85bb 100644 --- a/src/resources/theme/color/color.globals.ts +++ b/src/resources/theme/color/color.globals.ts @@ -297,8 +297,9 @@ export const colorStyles = css` --mdc-theme-text-hint-on-background: var(--secondary-text-color); --mdc-theme-text-icon-on-background: var(--secondary-text-color); --mdc-theme-error: var(--error-color); - --app-header-text-color: var(--text-primary-color); - --app-header-background-color: var(--primary-color); + --app-header-text-color: var(--sidebar-text-color); + --app-header-background-color: var(--sidebar-background-color); + --app-header-border-bottom: 1px solid var(--divider-color); --app-theme-color: var(--app-header-background-color); --mdc-checkbox-unchecked-color: rgba(var(--rgb-primary-text-color), 0.54); --mdc-checkbox-disabled-color: var(--disabled-text-color); @@ -351,8 +352,6 @@ export const darkColorStyles = css` --primary-text-color: #e1e1e1; --secondary-text-color: #9b9b9b; --disabled-text-color: #6f6f6f; - --app-header-text-color: #e1e1e1; - --app-header-background-color: #101e24; --switch-unchecked-button-color: #999999; --switch-unchecked-track-color: #9b9b9b; --divider-color: rgba(225, 225, 225, 0.12); diff --git a/src/resources/theme/color/wa.globals.ts b/src/resources/theme/color/wa.globals.ts index 40a173f2dc..8fd8e95c4e 100644 --- a/src/resources/theme/color/wa.globals.ts +++ b/src/resources/theme/color/wa.globals.ts @@ -54,7 +54,7 @@ export const waColorStyles = css` --wa-color-text-quiet: var(--ha-color-text-secondary); - --wa-color-text-normal: var(--ha-color-text-primary); + --wa-color-text-normal: var(--primary-text-color); --wa-color-surface-default: var(--card-background-color); --wa-color-surface-raised: var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)); --wa-panel-border-radius: var(--ha-border-radius-3xl); @@ -64,7 +64,5 @@ export const waColorStyles = css` --wa-focus-ring-color: var(--ha-color-neutral-60); --wa-shadow-l: 4px 8px 12px 0 rgba(0, 0, 0, 0.3); - - --wa-color-text-normal: var(--ha-color-text-primary); } `; diff --git a/src/resources/theme/core.globals.ts b/src/resources/theme/core.globals.ts index 8ecf898cbf..b746d79656 100644 --- a/src/resources/theme/core.globals.ts +++ b/src/resources/theme/core.globals.ts @@ -55,12 +55,16 @@ export const coreStyles = css` --ha-shadow-spread-md: 0; --ha-shadow-spread-lg: 0; - --ha-animation-base-duration: 350ms; + --ha-animation-duration-fast: 150ms; + --ha-animation-duration-normal: 250ms; + --ha-animation-duration-slow: 350ms; } @media (prefers-reduced-motion: reduce) { html { - --ha-animation-base-duration: 0ms; + --ha-animation-duration-fast: 0ms; + --ha-animation-duration-normal: 0ms; + --ha-animation-duration-slow: 0ms; } } `; diff --git a/src/state-control/climate/ha-state-control-climate-humidity.ts b/src/state-control/climate/ha-state-control-climate-humidity.ts index 142372db33..9c7b0c0498 100644 --- a/src/state-control/climate/ha-state-control-climate-humidity.ts +++ b/src/state-control/climate/ha-state-control-climate-humidity.ts @@ -45,7 +45,9 @@ export class HaStateControlClimateHumidity extends LitElement { } } - private _step = 1; + private get _step() { + return this.stateObj.attributes.target_humidity_step ?? 1; + } private get _min() { return this.stateObj.attributes.min_humidity ?? 0; diff --git a/src/state-display/state-display.ts b/src/state-display/state-display.ts index e74ba77528..37a4be3c21 100644 --- a/src/state-display/state-display.ts +++ b/src/state-display/state-display.ts @@ -5,6 +5,7 @@ import { customElement, property } from "lit/decorators"; import { join } from "lit/directives/join"; import { ensureArray } from "../common/array/ensure-array"; import { computeStateDomain } from "../common/entity/compute_state_domain"; +import { STRINGS_SEPARATOR_DOT } from "../common/const"; import "../components/ha-relative-time"; import { isUnavailableState } from "../data/entity/entity"; import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../data/sensor"; @@ -194,7 +195,7 @@ class StateDisplay extends LitElement { return html`${this.hass!.formatEntityState(stateObj)}`; } - return join(values, " · "); + return join(values, STRINGS_SEPARATOR_DOT); } } diff --git a/src/state-summary/state-card-input_number.ts b/src/state-summary/state-card-input_number.ts index c6e1227d47..6b4e26bd07 100644 --- a/src/state-summary/state-card-input_number.ts +++ b/src/state-summary/state-card-input_number.ts @@ -109,6 +109,11 @@ class StateCardInputNumber extends LitElement { width: 100%; max-width: 200px; } + @media (max-width: 450px) { + state-info { + flex-basis: 60%; + } + } `; private async _initialLoad(): Promise { diff --git a/src/state-summary/state-card-input_select.ts b/src/state-summary/state-card-input_select.ts index ca0d063ad1..87a626e0b7 100644 --- a/src/state-summary/state-card-input_select.ts +++ b/src/state-summary/state-card-input_select.ts @@ -1,11 +1,10 @@ import type { TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import { stopPropagation } from "../common/dom/stop_propagation"; import { computeStateName } from "../common/entity/compute_state_name"; import "../components/entity/state-badge"; -import "../components/ha-list-item"; -import "../components/ha-select"; +import "../components/ha-control-select-menu"; +import type { HaDropdownSelectEvent } from "../components/ha-dropdown"; import { UNAVAILABLE } from "../data/entity/entity"; import type { InputSelectEntity } from "../data/input_select"; import { setInputSelectOption } from "../data/input_select"; @@ -18,31 +17,34 @@ class StateCardInputSelect extends LitElement { @property({ attribute: false }) public stateObj!: InputSelectEntity; protected render(): TemplateResult { + const options = this.stateObj.attributes.options.map((option) => ({ + value: option, + label: option, + })); + return html` - - ${this.stateObj.attributes.options.map( - (option) => - html`${option}` - )} - + hide-label + show-arrow + @wa-select=${this._selectedOptionChanged} + > `; } - private async _selectedOptionChanged(ev) { - const option = ev.target.value; - if (option === this.stateObj.state) { + private async _selectedOptionChanged(ev: HaDropdownSelectEvent) { + const option = ev.detail.item?.value; + if ( + !option || + option === this.stateObj.state || + !this.stateObj.attributes.options.includes(option) + ) { return; } await setInputSelectOption(this.hass, this.stateObj.entity_id, option); @@ -51,14 +53,11 @@ class StateCardInputSelect extends LitElement { static styles = css` :host { display: flex; + align-items: center; + gap: var(--ha-space-2); } - state-badge { - float: left; - margin-top: 10px; - } - - ha-select { + ha-control-select-menu { width: 100%; } `; diff --git a/src/state-summary/state-card-select.ts b/src/state-summary/state-card-select.ts index 956454e5f2..0956f280f3 100644 --- a/src/state-summary/state-card-select.ts +++ b/src/state-summary/state-card-select.ts @@ -1,11 +1,10 @@ import type { TemplateResult } from "lit"; import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; -import { stopPropagation } from "../common/dom/stop_propagation"; import { computeStateName } from "../common/entity/compute_state_name"; import "../components/entity/state-badge"; -import "../components/ha-list-item"; -import "../components/ha-select"; +import "../components/ha-control-select-menu"; +import type { HaDropdownSelectEvent } from "../components/ha-dropdown"; import { UNAVAILABLE } from "../data/entity/entity"; import type { SelectEntity } from "../data/select"; import { setSelectOption } from "../data/select"; @@ -18,48 +17,45 @@ class StateCardSelect extends LitElement { @property({ attribute: false }) public stateObj!: SelectEntity; protected render(): TemplateResult { + const options = this.stateObj.attributes.options.map((option) => ({ + value: option, + label: this.hass.formatEntityState(this.stateObj, option), + })); + return html` - - ${this.stateObj.attributes.options.map( - (option) => html` - - ${this.hass.formatEntityState(this.stateObj, option)} - - ` - )} - + hide-label + show-arrow + @wa-select=${this._selectedOptionChanged} + > `; } - private _selectedOptionChanged(ev) { - const option = ev.target.value; - if (option === this.stateObj.state) { + private async _selectedOptionChanged(ev: HaDropdownSelectEvent) { + const option = ev.detail.item?.value; + if ( + !option || + option === this.stateObj.state || + !this.stateObj.attributes.options.includes(option) + ) { return; } - setSelectOption(this.hass, this.stateObj.entity_id, option); + await setSelectOption(this.hass, this.stateObj.entity_id, option); } static styles = css` :host { display: flex; + align-items: center; + gap: var(--ha-space-2); } - state-badge { - float: left; - margin-top: 10px; - } - - ha-select { + ha-control-select-menu { width: 100%; } `; diff --git a/src/state/connection-mixin.ts b/src/state/connection-mixin.ts index f12085ff95..326fad55f3 100644 --- a/src/state/connection-mixin.ts +++ b/src/state/connection-mixin.ts @@ -209,9 +209,22 @@ export const connectionMixin = >( this._loadFragmentTranslations(this.hass?.language, fragment), formatEntityState: (stateObj, state) => (state != null ? state : stateObj.state) ?? "", + formatEntityStateToParts: (stateObj, state) => [ + { + type: "value", + value: (state != null ? state : stateObj.state) ?? "", + }, + ], formatEntityAttributeName: (_stateObj, attribute) => attribute, formatEntityAttributeValue: (stateObj, attribute, value) => value != null ? value : (stateObj.attributes[attribute] ?? ""), + formatEntityAttributeValueToParts: (stateObj, attribute, value) => [ + { + type: "value", + value: + value != null ? value : (stateObj.attributes[attribute] ?? ""), + }, + ], formatEntityName: (stateObj) => computeStateName(stateObj), ...getState(), ...this._pendingHass, diff --git a/src/state/quick-bar-mixin.ts b/src/state/quick-bar-mixin.ts index f0dd705560..4e192d1afc 100644 --- a/src/state/quick-bar-mixin.ts +++ b/src/state/quick-bar-mixin.ts @@ -6,13 +6,17 @@ import { mainWindow } from "../common/dom/get_main_window"; import { ShortcutManager } from "../common/keyboard/shortcuts"; import { extractSearchParamsObject } from "../common/url/search-params"; import type { + QuickBarContextItem, QuickBarParams, QuickBarSection, } from "../dialogs/quick-bar/show-dialog-quick-bar"; -import { showQuickBar } from "../dialogs/quick-bar/show-dialog-quick-bar"; +import { + closeQuickBar, + showQuickBar, +} from "../dialogs/quick-bar/show-dialog-quick-bar"; +import { findRelated, type RelatedResult } from "../data/search"; import { showShortcutsDialog } from "../dialogs/shortcuts/show-shortcuts-dialog"; import { showVoiceCommandDialog } from "../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; -import type { Redirects } from "../panels/my/ha-panel-my"; import type { Constructor, HomeAssistant } from "../types"; import { storeState } from "../util/ha-pref-storage"; import { showToast } from "../util/toast"; @@ -23,11 +27,60 @@ declare global { "hass-quick-bar": QuickBarParams; "hass-quick-bar-trigger": KeyboardEvent; "hass-enable-shortcuts": HomeAssistant["enableShortcuts"]; + "hass-quick-bar-context": QuickBarContextItem | undefined; } } export default >(superClass: T) => class extends superClass { + private _quickBarOpen = false; + + private _quickBarContext?: QuickBarContextItem; + + private _quickBarContextRelated?: RelatedResult; + + private _fetchRelatedMemoized = memoizeOne( + (itemType: QuickBarContextItem["itemType"], itemId: string) => + findRelated(this.hass!, itemType, itemId) + ); + + private _clearQuickBarContext = () => { + this._quickBarContext = undefined; + this._quickBarContextRelated = undefined; + }; + + private _contextMatches = (context?: QuickBarContextItem) => + context?.itemType === this._quickBarContext?.itemType && + context?.itemId === this._quickBarContext?.itemId; + + private _prefetchQuickBarContext = async ( + context?: QuickBarContextItem + ) => { + this._quickBarContextRelated = undefined; + + if (!context) { + return; + } + + try { + const related = await this._fetchRelatedMemoized( + context.itemType, + context.itemId + ); + + if (this._contextMatches(context)) { + this._quickBarContextRelated = related; + } + } catch (err) { + // eslint-disable-next-line no-console + console.warn("Error prefetching quick bar related items", err); + + if (this._contextMatches(context)) { + this._quickBarContextRelated = undefined; + } + } + }; + protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); @@ -36,6 +89,42 @@ export default >(superClass: T) => storeState(this.hass!); }); + this.addEventListener( + "show-dialog", + (ev) => { + if ((ev as CustomEvent).detail.dialogTag === "ha-quick-bar") { + // If quick bar is already open, prevent opening it again + if (this._quickBarOpen) { + ev.stopPropagation(); + ev.preventDefault(); + return; + } + this._quickBarOpen = true; + } + }, + { capture: true } + ); + + this.addEventListener("dialog-closed", (ev) => { + if ((ev as CustomEvent).detail.dialog === "ha-quick-bar") { + this._quickBarOpen = false; + } + }); + + this.addEventListener("hass-quick-bar-context", (ev) => { + this._quickBarContext = + ev.detail && "itemType" in ev.detail && "itemId" in ev.detail + ? ev.detail + : undefined; + this._prefetchQuickBarContext(this._quickBarContext); + }); + + mainWindow.addEventListener( + "location-changed", + this._clearQuickBarContext + ); + mainWindow.addEventListener("popstate", this._clearQuickBarContext); + mainWindow.addEventListener("hass-quick-bar-trigger", (ev) => { switch (ev.detail.key) { case "e": @@ -61,6 +150,15 @@ export default >(superClass: T) => this._registerShortcut(); } + public disconnectedCallback() { + super.disconnectedCallback(); + mainWindow.removeEventListener( + "location-changed", + this._clearQuickBarContext + ); + mainWindow.removeEventListener("popstate", this._clearQuickBarContext); + } + private _registerShortcut() { const shortcutManager = new ShortcutManager(); shortcutManager.add({ @@ -70,7 +168,11 @@ export default >(superClass: T) => m: { handler: (ev) => this._createMyLink(ev) }, a: { handler: (ev) => this._showVoiceCommandDialog(ev) }, d: { handler: (ev) => this._showQuickBar(ev, "device") }, - "$mod+k": { handler: (ev) => this._showQuickBar(ev) }, + "$mod+k": { + handler: (ev) => this._toggleQuickBar(ev), + allowWhenTextSelected: true, + allowInInput: true, + }, // Workaround see https://github.com/jamiebuilds/tinykeys/issues/130 "Shift+?": { handler: (ev) => this._showShortcutDialog(ev) }, // Those are fallbacks for non-latin keyboards that don't have e, c, m keys (qwerty-based shortcuts) @@ -79,7 +181,11 @@ export default >(superClass: T) => KeyM: { handler: (ev) => this._createMyLink(ev) }, KeyA: { handler: (ev) => this._showVoiceCommandDialog(ev) }, KeyD: { handler: (ev) => this._showQuickBar(ev, "device") }, - "$mod+KeyK": { handler: (ev) => this._showQuickBar(ev) }, + "$mod+KeyK": { + handler: (ev) => this._toggleQuickBar(ev), + allowWhenTextSelected: true, + allowInInput: true, + }, }); } @@ -114,7 +220,28 @@ export default >(superClass: T) => } e.preventDefault(); - showQuickBar(this, { mode }); + showQuickBar(this, { + mode, + contextItem: this._quickBarContext, + related: this._quickBarContextRelated, + }); + } + + private _toggleQuickBar(e: KeyboardEvent, mode?: QuickBarSection) { + if (!this._canToggleQuickBar()) { + return; + } + + if (e.defaultPrevented) { + return; + } + e.preventDefault(); + + if (!this._quickBarOpen) { + showQuickBar(this, { mode }); + return; + } + closeQuickBar(); } private _showShortcutDialog(e: KeyboardEvent) { @@ -146,16 +273,8 @@ export default >(superClass: T) => const targetPath = mainWindow.location.pathname; const myParams = new URLSearchParams(); - let redirects: Redirects; - - if (targetPath.startsWith("/hassio")) { - const myPanelSupervisor = - await import("../../hassio/src/hassio-my-redirect"); - redirects = myPanelSupervisor.REDIRECTS; - } else { - const myPanel = await import("../panels/my/ha-panel-my"); - redirects = myPanel.getMyRedirects(); - } + const myPanel = await import("../panels/my/ha-panel-my"); + const redirects = myPanel.getMyRedirects(); for (const [slug, redirect] of Object.entries(redirects)) { if (!targetPath.startsWith(redirect.redirect)) { @@ -173,6 +292,8 @@ export default >(superClass: T) => } if (redirect.redirect === "/config/integrations/integration") { myParams.append("domain", targetPath.split("/")[4]); + } else if (redirect.redirect === "/config/app") { + myParams.append("app", targetPath.split("/")[3]); } else if (redirect.redirect === "/hassio/addon") { myParams.append("addon", targetPath.split("/")[3]); } @@ -194,9 +315,14 @@ export default >(superClass: T) => private _canShowQuickBar(e: KeyboardEvent) { return ( + !this._quickBarOpen && this.hass?.user?.is_admin && this.hass.enableShortcuts && canOverrideAlphanumericInput(e.composedPath()) ); } + + private _canToggleQuickBar() { + return this.hass?.user?.is_admin && this.hass.enableShortcuts; + } }; diff --git a/src/state/state-display-mixin.ts b/src/state/state-display-mixin.ts index dfa3f25e59..f33f7591b9 100644 --- a/src/state/state-display-mixin.ts +++ b/src/state/state-display-mixin.ts @@ -53,8 +53,10 @@ export default >(superClass: T) => { const { formatEntityState, + formatEntityStateToParts, formatEntityAttributeName, formatEntityAttributeValue, + formatEntityAttributeValueToParts, formatEntityName, } = await computeFormatFunctions( this.hass.localize, @@ -68,8 +70,10 @@ export default >(superClass: T) => { ); this._updateHass({ formatEntityState, + formatEntityStateToParts, formatEntityAttributeName, formatEntityAttributeValue, + formatEntityAttributeValueToParts, formatEntityName, }); }; diff --git a/src/state/themes-mixin.ts b/src/state/themes-mixin.ts index 75d449b19b..ef94c3d89e 100644 --- a/src/state/themes-mixin.ts +++ b/src/state/themes-mixin.ts @@ -92,7 +92,7 @@ export default >(superClass: T) => } private _applyTheme(darkPreferred: boolean) { - if (!this.hass) { + if (!this.hass?.config || !this.hass.themes) { return; } diff --git a/src/state/translations-mixin.ts b/src/state/translations-mixin.ts index 2b587acadb..4aca311817 100644 --- a/src/state/translations-mixin.ts +++ b/src/state/translations-mixin.ts @@ -37,21 +37,11 @@ declare global { "hass-language-select": { language: string; }; - "hass-number-format-select": { - number_format: NumberFormat; - }; - "hass-time-format-select": { - time_format: TimeFormat; - }; - "hass-date-format-select": { - date_format: DateFormat; - }; - "hass-time-zone-select": { - time_zone: TimeZone; - }; - "hass-first-weekday-select": { - first_weekday: FirstWeekday; - }; + "hass-number-format-select": NumberFormat; + "hass-time-format-select": TimeFormat; + "hass-date-format-select": DateFormat; + "hass-time-zone-select": TimeZone; + "hass-first-weekday-select": FirstWeekday; "translations-updated": undefined; } } diff --git a/src/translations/en.json b/src/translations/en.json index 64a76f18a4..0218b35c62 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1,5 +1,7 @@ { "panel": { + "demo": "Demo", + "apps": "Apps", "energy": "Energy", "calendar": "Calendar", "config": "Settings", @@ -8,13 +10,12 @@ "logbook": "Activity", "history": "History", "todo": "To-do lists", - "developer_tools": "Developer tools", "media_browser": "Media", "profile": "Profile", "light": "Lights", "security": "Security", "climate": "Climate", - "home": "Home" + "home": "Overview" }, "state": { "default": { @@ -214,6 +215,26 @@ "no_media_playing": "No media playing", "count_media_playing": "{count} {count, plural,\n one {playing}\n other {playing}\n}" }, + "toggle-group": { + "all_off": "All off", + "all_on": "All on", + "some_on": "{on_count} of {total_count} on" + }, + "discovered-devices": { + "title": "Devices discovered", + "count_devices": "{count} {count, plural,\n one {device to add}\n other {devices to add}\n}", + "no_devices": "No devices to add" + }, + "repairs": { + "title": "Repairs", + "count_issues": "{count} {count, plural,\n one {issue}\n other {issues}\n}", + "no_issues": "No issues" + }, + "updates": { + "title": "Updates", + "count_updates": "{count} {count, plural,\n one {update available}\n other {updates available}\n}", + "no_updates": "Up to date" + }, "media_player": { "source": "Source", "sound_mode": "Sound mode", @@ -408,7 +429,7 @@ "cancel": "Cancel", "delete": "Delete", "delete_all": "Delete all", - "download": "[%key:supervisor::backup::download%]", + "download": "Download", "duplicate": "Duplicate", "remove": "Remove", "enable": "Enable", @@ -432,6 +453,7 @@ "edit_item": "Edit {name}", "submit": "Submit", "rename": "Rename", + "reset": "Reset", "search": "[%key:ui::components::data-table::search%]", "ok": "OK", "yes": "Yes", @@ -751,12 +773,24 @@ "config-entry-picker": { "config_entry": "Integration" }, - "coversation-agent-picker": { + "conversation-agent-picker": { "conversation_agent": "Conversation agent", "none": "None" }, "country-picker": { - "country": "Country" + "country": "Country", + "no_match": "No countries found for {term}", + "no_countries": "No countries available" + }, + "currency-picker": { + "currency": "Currency", + "no_match": "No currencies found for {term}", + "no_currencies": "No currencies available" + }, + "timezone-picker": { + "time_zone": "Time zone", + "no_match": "No time zones found for {term}", + "no_timezones": "No time zones available" }, "pipeline-picker": { "pipeline": "Assistant", @@ -807,7 +841,7 @@ "categories": "Categories", "category": "Category", "add_category": "Add category", - "add_new_sugestion": "Add new category ''{name}''", + "add_new_suggestion": "Add new category ''{name}''", "add_new": "Add new category…", "no_categories": "No categories available", "no_match": "No categories found for {term}", @@ -823,7 +857,7 @@ "label-picker": { "label": "Label", "labels": "Labels", - "add_new_sugestion": "Add new label ''{name}''", + "add_new_suggestion": "Add new label ''{name}''", "add_new": "Add new label…", "add": "Add label", "no_labels": "No labels available", @@ -834,7 +868,7 @@ "clear": "Clear", "show_areas": "Show areas", "area": "Area", - "add_new_sugestion": "Add new area ''{name}''", + "add_new_suggestion": "Add new area ''{name}''", "add_new": "Add new area…", "no_areas": "No areas available", "no_match": "No areas found for {term}", @@ -845,7 +879,7 @@ "clear": "Clear", "show_floors": "Show floors", "floor": "Floor", - "add_new_sugestion": "Add new floor ''{name}''", + "add_new_suggestion": "Add new floor ''{name}''", "add_new": "Add new floor…", "no_floors": "No floors available", "no_match": "No floors found for {term}", @@ -870,11 +904,11 @@ "learn_more": "Learn more about statistics", "unknown": "Unknown statistic selected" }, - "addon-picker": { - "addon": "Add-on", + "app-picker": { + "app": "App", "error": { - "no_supervisor": "Add-ons are not supported on your installation.", - "fetch_addons": "There was an error loading add-ons." + "no_supervisor": "Apps are not supported on your installation.", + "fetch_apps": "There was an error loading apps." } }, "mount-picker": { @@ -1290,7 +1324,10 @@ "error": "Error in parsing YAML: {reason}", "error_location": "line: {line}, column: {column}", "enter_fullscreen": "Enter fullscreen", - "exit_fullscreen": "Exit fullscreen" + "exit_fullscreen": "Exit fullscreen", + "find_and_replace": "Find and replace", + "test_on": "Turn on testing", + "test_off": "Turn off testing" }, "state-content-picker": { "state": "State", @@ -1329,7 +1366,12 @@ "error": "Fail!" }, "navigation-picker": { - "add_custom_path": "Add custom path" + "add_custom_path": "Add custom path", + "dashboards": "[%key:ui::panel::config::dashboard::dashboards::main%]", + "related": "Related", + "views": "Views", + "area_settings": "{area} - Settings", + "other_routes": "Other routes" } }, "dialogs": { @@ -1342,50 +1384,51 @@ "navigate_title": "Navigate", "commands": { "reload": { - "all": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::all%]", - "reload": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::reload%]", - "core": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::core%]", - "group": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::group%]", - "automation": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::automation%]", - "script": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::script%]", - "scene": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::scene%]", - "person": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::person%]", - "zone": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::zone%]", - "input_boolean": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_boolean%]", - "input_button": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_button%]", - "input_text": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_text%]", - "input_number": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_number%]", - "input_datetime": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_datetime%]", - "input_select": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::input_select%]", - "template": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::template%]", - "universal": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::universal%]", - "rest": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::rest%]", - "command_line": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::command_line%]", - "filter": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::filter%]", - "statistics": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::statistics%]", - "generic": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::generic%]", - "generic_thermostat": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::generic_thermostat%]", - "homekit": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::homekit%]", - "min_max": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::min_max%]", - "history_stats": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::history_stats%]", - "trend": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::trend%]", - "ping": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::ping%]", - "filesize": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::filesize%]", - "telegram": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::telegram%]", - "smtp": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::smtp%]", - "mqtt": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::mqtt%]", - "rpi_gpio": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::rpi_gpio%]", - "themes": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::themes%]" + "all": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::all%]", + "reload": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::reload%]", + "core": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::core%]", + "group": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::group%]", + "automation": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::automation%]", + "script": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::script%]", + "scene": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::scene%]", + "person": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::person%]", + "zone": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::zone%]", + "input_boolean": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::input_boolean%]", + "input_button": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::input_button%]", + "input_text": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::input_text%]", + "input_number": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::input_number%]", + "input_datetime": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::input_datetime%]", + "input_select": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::input_select%]", + "template": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::template%]", + "universal": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::universal%]", + "rest": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::rest%]", + "command_line": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::command_line%]", + "filter": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::filter%]", + "statistics": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::statistics%]", + "generic": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::generic%]", + "generic_thermostat": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::generic_thermostat%]", + "homekit": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::homekit%]", + "min_max": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::min_max%]", + "history_stats": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::history_stats%]", + "trend": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::trend%]", + "ping": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::ping%]", + "filesize": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::filesize%]", + "telegram": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::telegram%]", + "smtp": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::smtp%]", + "mqtt": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::mqtt%]", + "rpi_gpio": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::rpi_gpio%]", + "themes": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::reloading::themes%]" }, - "server_control": { - "perform_action": "{action} server", - "restart": "[%key:ui::panel::developer-tools::tabs::yaml::section::server_management::restart%]", - "stop": "[%key:ui::panel::developer-tools::tabs::yaml::section::server_management::stop%]" + "home_assistant_control": { + "perform_action": "{action} Home Assistant", + "restart": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::server_management::restart%]", + "stop": "[%key:ui::panel::config::developer-tools::tabs::yaml::section::server_management::stop%]" }, "types": { "reload": "Reload", "navigation": "Navigate", - "server_control": "Server" + "app_settings": "App settings", + "home_assistant_control": "Interrupts all running automations and scripts" }, "navigation": { "logs": "[%key:ui::panel::config::logs::caption%]", @@ -1406,6 +1449,7 @@ "info": "[%key:ui::panel::config::info::caption%]", "network": "[%key:ui::panel::config::network::caption%]", "updates": "[%key:ui::panel::config::updates::caption%]", + "repairs": "[%key:ui::panel::config::repairs::caption%]", "hardware": "[%key:ui::panel::config::hardware::caption%]", "storage": "[%key:ui::panel::config::storage::caption%]", "general": "[%key:ui::panel::config::core::caption%]", @@ -1414,13 +1458,23 @@ "analytics": "[%key:ui::panel::config::analytics::caption%]", "system_health": "[%key:ui::panel::config::system_health::caption%]", "blueprint": "[%key:ui::panel::config::blueprint::caption%]", - "server_control": "[%key:ui::panel::developer-tools::tabs::yaml::title%]", + "server_control": "[%key:ui::panel::config::developer-tools::tabs::yaml::title%]", "system": "[%key:ui::panel::config::dashboard::system::main%]", - "addon_dashboard": "Add-on dashboard", - "addon_store": "Add-on store", - "addon_info": "{addon} info", + "apps": "Apps", + "app_store": "App store", + "app_info": "{app} info", "shortcuts": "[%key:ui::panel::config::info::shortcuts%]", - "labs": "[%key:ui::panel::config::labs::caption%]" + "labs": "[%key:ui::panel::config::labs::caption%]", + "developer-tools": "[%key:ui::panel::config::dashboard::developer_tools::main%]", + "matter": "[%key:ui::panel::config::dashboard::matter::main%]", + "zha": "[%key:ui::panel::config::dashboard::zha::main%]", + "zwave_js": "[%key:ui::panel::config::dashboard::zwave_js::main%]", + "thread": "[%key:ui::panel::config::dashboard::thread::main%]", + "bluetooth": "[%key:ui::panel::config::dashboard::bluetooth::main%]", + "knx": "[%key:ui::panel::config::dashboard::knx::main%]", + "insteon": "[%key:ui::panel::config::dashboard::insteon::main%]", + "voice-assistants": "[%key:ui::panel::config::dashboard::voice_assistants::main%]", + "ai-tasks": "[%key:ui::panel::config::ai_tasks::caption%]" } }, "filter_placeholder": "Search entities", @@ -1467,6 +1521,7 @@ "info": "Information", "related": "Related", "add_entity_to": "Add to", + "attributes": "Attributes", "history": "History", "aggregate": "5-minute aggregated", "logbook": "Activity", @@ -1507,8 +1562,8 @@ "automatic_description_none": "No automatic backup yet.", "manual": "Create manual backup before update", "manual_description": "Includes Home Assistant settings and history.", - "addon": "Keep backup of the last version", - "addon_description": "For easily restoring to version {version}", + "app": "Keep backup of the last version", + "app_description": "For easily restoring to version {version}", "generic": "Create backup" } }, @@ -1611,6 +1666,7 @@ "icon_error": "Icons should be in the format 'prefix:iconname', e.g. 'mdi:home'", "default_code": "Default code", "default_code_error": "Code does not match code format", + "calendar_color": "Calendar color", "entity_id": "Entity ID", "unit_of_measurement": "Unit of measurement", "precipitation_unit": "Precipitation unit", @@ -1798,9 +1854,9 @@ }, "reboot": { "title": "Reboot system", - "description": "Restarts the system running Home Assistant and all add-ons.", + "description": "Restarts the system running Home Assistant and all apps.", "confirm_title": "Reboot system?", - "confirm_description": "This will reboot the complete system which includes Home Assistant and all the add-ons.", + "confirm_description": "This will reboot the complete system which includes Home Assistant and all apps.", "confirm_action": "Reboot", "action_toast": "Rebooting system", "confirm_action_backup": "Wait and reboot", @@ -1808,9 +1864,9 @@ }, "shutdown": { "title": "Shut down system", - "description": "Shuts down the system running Home Assistant and all add-ons.", + "description": "Shuts down the system running Home Assistant and all apps.", "confirm_title": "Shut down system?", - "confirm_description": "This will shut down the complete system which includes Home Assistant and all add-ons.", + "confirm_description": "This will shut down the complete system which includes Home Assistant and all apps.", "confirm_action": "Shut down", "confirm_action_backup": "Wait and shut down", "action_toast": "Shutting down system", @@ -1900,12 +1956,12 @@ "data": "Additional data" }, "template": { - "time": "[%key:ui::panel::developer-tools::tabs::templates::time%]", - "all_listeners": "[%key:ui::panel::developer-tools::tabs::templates::all_listeners%]", - "no_listeners": "[%key:ui::panel::developer-tools::tabs::templates::no_listeners%]", - "listeners": "[%key:ui::panel::developer-tools::tabs::templates::listeners%]", - "entity": "[%key:ui::panel::developer-tools::tabs::templates::entity%]", - "domain": "[%key:ui::panel::developer-tools::tabs::templates::domain%]" + "time": "[%key:ui::panel::config::developer-tools::tabs::templates::time%]", + "all_listeners": "[%key:ui::panel::config::developer-tools::tabs::templates::all_listeners%]", + "no_listeners": "[%key:ui::panel::config::developer-tools::tabs::templates::no_listeners%]", + "listeners": "[%key:ui::panel::config::developer-tools::tabs::templates::listeners%]", + "entity": "[%key:ui::panel::config::developer-tools::tabs::templates::entity%]", + "domain": "[%key:ui::panel::config::developer-tools::tabs::templates::domain%]" } }, "options_flow": { @@ -2011,37 +2067,37 @@ "triggers": "Triggers" }, "unsupported": { - "title": "[%key:supervisor::system::supervisor::unsupported_title%]", - "description": "[%key:supervisor::system::supervisor::unsupported_description%]", + "title": "You are running an unsupported installation", + "description": "Below is a list of issues found with your installation, open the links to learn how you can resolve the issues.", "reasons": { - "apparmor": "[%key:supervisor::system::supervisor::unsupported_reason::apparmor%]", - "content_trust": "[%key:supervisor::system::supervisor::unsupported_reason::content_trust%]", - "dbus": "[%key:supervisor::system::supervisor::unsupported_reason::dbus%]", - "docker_configuration": "[%key:supervisor::system::supervisor::unsupported_reason::docker_configuration%]", - "docker_version": "[%key:supervisor::system::supervisor::unsupported_reason::docker_version%]", - "job_conditions": "[%key:supervisor::system::supervisor::unsupported_reason::job_conditions%]", - "lxc": "[%key:supervisor::system::supervisor::unsupported_reason::lxc%]", - "network_manager": "[%key:supervisor::system::supervisor::unsupported_reason::network_manager%]", - "os": "[%key:supervisor::system::supervisor::unsupported_reason::os%]", - "os_agent": "[%key:supervisor::system::supervisor::unsupported_reason::os_agent%]", - "privileged": "[%key:supervisor::system::supervisor::unsupported_reason::privileged%]", - "software": "[%key:supervisor::system::supervisor::unsupported_reason::software%]", - "source_mods": "[%key:supervisor::system::supervisor::unsupported_reason::source_mods%]", - "systemd": "[%key:supervisor::system::supervisor::unsupported_reason::systemd%]", - "systemd_resolved": "[%key:supervisor::system::supervisor::unsupported_reason::systemd_resolved%]" + "apparmor": "AppArmor is not enabled on the host", + "content_trust": "Content-trust validation is disabled", + "dbus": "DBUS", + "docker_configuration": "Docker configuration", + "docker_version": "Docker version", + "job_conditions": "Ignored job conditions", + "lxc": "LXC", + "network_manager": "Network manager", + "os": "Operating system", + "os_agent": "OS Agent", + "privileged": "Supervisor is not privileged", + "software": "Unsupported software detected", + "source_mods": "Source modifications", + "systemd": "Systemd", + "systemd_resolved": "Systemd-Resolved" } }, "unhealthy": { - "title": "[%key:supervisor::system::supervisor::unhealthy_title%]", - "description": "[%key:supervisor::system::supervisor::unhealthy_description%]", + "title": "Your installation is unhealthy", + "description": "Running an unhealthy installation will cause issues. Below is a list of issues found with your installation, open the links to learn how you can resolve the issues.", "reasons": { - "docker": "[%key:supervisor::system::supervisor::unhealthy_reason::docker%]", - "oserror_bad_message": "[%key:supervisor::system::supervisor::unhealthy_reason::oserror_bad_message%]", - "duplicate_os_installation": "[%key:supervisor::system::supervisor::unhealthy_reason::duplicate_os_installation%]", - "privileged": "[%key:supervisor::system::supervisor::unhealthy_reason::privileged%]", - "supervisor": "[%key:supervisor::system::supervisor::unhealthy_reason::supervisor%]", - "setup": "[%key:supervisor::system::supervisor::unhealthy_reason::setup%]", - "untrusted": "[%key:supervisor::system::supervisor::unhealthy_reason::untrusted%]" + "docker": "The Docker environment is not working properly", + "oserror_bad_message": "Operating System error: Bad message", + "duplicate_os_installation": "Duplicate Home Assistant OS installation", + "privileged": "Supervisor is not privileged", + "supervisor": "Supervisor was not able to update", + "setup": "Setup of the Supervisor failed", + "untrusted": "Detected untrusted content" } }, "join_beta_channel": { @@ -2152,7 +2208,7 @@ "on_any_page": "On any page", "on_pages_with_tables": "On pages with tables", "search": "Quick search", - "search_command": "search command", + "search_command": "search commands", "search_entities": "search entities", "search_devices": "search devices", "search_in_table": "to search in tables" @@ -2255,14 +2311,42 @@ "reset_confirmation": "Are you sure you want to reset the sidebar to its default configuration? This will restore the original order and visibility of all panels." }, "panel": { + "app": { + "error_app_not_installed": "The app is not installed. Please install it first.", + "error_app_no_ingress": "This app does not support ingress.", + "error_app_not_running": "The app is not running. Do you want to start it now?", + "start_app": "Start app", + "app_starting": "The app is starting, this can take some time...", + "error_starting_app": "Error starting the app", + "error_creating_session": "Unable to create an ingress session", + "error_app_not_ready": "The app seems to not be ready, it might still be starting. Do you want to try again?", + "retry": "Retry" + }, "home": { "editor": { - "title": "Edit home page", - "description": "Configure your home page display preferences.", + "title": "Edit Overview page", + "description": "Configure your Overview page display preferences.", "favorite_entities_helper": "Display your favorite entities. Home Assistant will still suggest based on commonly used up to 8 slots.", - "save_failed": "Failed to save home page configuration", + "save_failed": "Failed to save Overview page configuration", "areas_hint": "You can rearrange your floors and areas in the order that best represents your house on the {areas_page}.", "areas_page": "areas page" + }, + "new_overview_dialog": { + "title": "Welcome to your new overview", + "description": "The overview dashboard has been redesigned to give you a better experience managing your smart home.", + "whats_new": "What's new", + "automatic_organization": "Automatic organization", + "automatic_organization_description": "Your devices are now automatically organized by area and floor.", + "favorites": "Favorites", + "favorites_description": "Pin your most used entities to the top for quick access.", + "existing_dashboards": "Your existing dashboards", + "existing_dashboards_description": "Your manual dashboards are still available in the sidebar. Miss the old format? You can create a new dashboard using the \"Overview (legacy)\" template in {dashboard_settings} and set it as default.", + "dashboard_settings": "dashboard settings", + "ok_understood": "OK, understood" + }, + "banner": { + "welcome_message": "Welcome to the new overview dashboard.", + "learn_more": "Learn more" } }, "my": { @@ -2279,6 +2363,13 @@ "config": { "generic": { "headers": { + "entity_id": "Entity ID", + "domain": "Domain", + "area": "Area", + "floor": "Floor", + "category": "Category", + "assistants": "Assistants", + "editable": "Editable", "modified_at": "Modified", "created_at": "Created", "actions": "Actions" @@ -2298,8 +2389,8 @@ "main": "Backup", "secondary": "Generate backups of your Home Assistant configuration" }, - "supervisor": { - "main": "Add-ons", + "apps": { + "main": "Apps", "secondary": "Run extra applications next to Home Assistant" }, "dashboards": { @@ -2334,6 +2425,10 @@ "main": "System", "secondary": "Create backups, check logs, or reboot your system" }, + "developer_tools": { + "main": "Developer tools", + "secondary": "Advanced tools for inspecting and debugging your system" + }, "about": { "main": "About", "secondary": "Version information, credit, and more" @@ -2342,24 +2437,31 @@ "secondary": "Loading..." }, "zwave_js": { + "main": "Z-Wave", "secondary": "Sub-GHz mesh protocol" }, "zha": { + "main": "Zigbee", "secondary": "Low-power mesh network" }, "matter": { + "main": "Matter", "secondary": "Cross-vendor smart home standard" }, "thread": { + "main": "Thread", "secondary": "Mesh network often used for Matter devices" }, "bluetooth": { + "main": "Bluetooth", "secondary": "Local device connectivity" }, "knx": { + "main": "KNX", "secondary": "Building automation standard" }, "insteon": { + "main": "Insteon", "secondary": "Dual-mesh home automation" } }, @@ -2381,7 +2483,7 @@ }, "updates": { "caption": "Updates", - "description": "Manage updates of Home Assistant, add-ons, and devices", + "description": "Manage updates of Home Assistant, apps, and devices", "no_updates": "No updates available", "no_update_entities": { "title": "Unable to check for updates", @@ -2397,8 +2499,8 @@ "more_updates": "Show all updates", "show": "show", "show_skipped": "Show skipped updates", - "join_beta": "[%key:supervisor::system::supervisor::join_beta_action%]", - "leave_beta": "[%key:supervisor::system::supervisor::leave_beta_action%]", + "join_beta": "Join beta channel", + "leave_beta": "Leave beta channel", "skipped": "Skipped", "update_in_progress": "Update in progress", "no_area": "No area" @@ -2458,6 +2560,254 @@ "gen_image_header": "Image generation tasks", "gen_image_description": "Generate images." }, + "apps": { + "caption": "Apps", + "description": "Install and manage apps", + "error_loading": "Error loading apps", + "state": { + "update_available": "Update available", + "installed": "Installed", + "not_installed": "Not installed", + "not_available": "Not available" + }, + "installed": { + "search": "Search apps", + "no_apps": "You don't have any apps installed yet. Click here to browse available apps.", + "add_app": "Install app", + "app_stopped": "App is stopped", + "app_update_available": "Update available", + "app_running": "App is running" + }, + "store": { + "title": "App store", + "check_updates": "Check for updates", + "repositories": "Repositories", + "registries": "Registries", + "missing_apps": "Looking for apps? Enable advanced mode.", + "no_results_found": "No results found in {repository}" + }, + "dialog": { + "repositories": { + "title": "Manage app repositories", + "add": "Add", + "remove": "Remove", + "used": "In use by installed apps", + "no_repositories": "You don't have any app repositories configured" + }, + "registries": { + "title_add": "Add registry", + "title_manage": "Manage registries", + "add_registry": "Add", + "add_new_registry": "Add new registry", + "username": "Username", + "remove": "Remove registry", + "no_registries": "You don't have any registries configured", + "failed_to_add": "Failed to add registry", + "failed_to_remove": "Failed to remove registry", + "registry": "Registry", + "password": "Password" + } + }, + "panel": { + "info": "Info", + "documentation": "Documentation", + "configuration": "Configuration", + "log": "Log" + }, + "dashboard": { + "cpu_usage": "CPU usage", + "ram_usage": "RAM usage", + "app_running": "App is running", + "app_stopped": "App is stopped", + "current_version": "Current version: {version}", + "changelog": "Changelog", + "hostname": "Hostname", + "visit_app_page": "Visit {name} page for more details.", + "start": "Start", + "stop": "Stop", + "restart": "Restart", + "rebuild": "Rebuild", + "uninstall": "Uninstall", + "install": "Install", + "open_web_ui": "Open Web UI", + "failed_to_save": "Failed to save: {error}", + "failed_to_reset": "Failed to reset: {error}", + "failed_to_restart": "Failed to restart {name}", + "protection_mode": { + "title": "Protection mode disabled!", + "content": "Protection mode on this app is disabled! This gives the app full access to the entire system, which is more risky for your system if the app is compromised. Only disable protection mode if you know what you are doing.", + "enable": "Enable" + }, + "system_managed": { + "title": "System managed", + "description": "This app is managed by the system. Settings are automatically configured and changes may be overwritten.", + "take_control": "Take control", + "badge": "System managed" + }, + "option": { + "boot": { + "title": "Start on boot", + "description": "Automatically start this app when Home Assistant starts." + }, + "watchdog": { + "title": "Watchdog", + "description": "Automatically restart this app when it crashes." + }, + "auto_update": { + "title": "Auto update", + "description": "Automatically update this app when a new version is available." + }, + "ingress_panel": { + "title": "Show in sidebar", + "description": "Show a shortcut to this app in the sidebar." + }, + "protected": { + "title": "Protection mode", + "description": "Prevent the app from getting full access to the system." + } + }, + "capability": { + "stages": { + "experimental": "Experimental", + "deprecated": "Deprecated" + }, + "label": { + "rating": "Rating", + "host": "Host", + "hardware": "Hardware access", + "core": "Home Assistant", + "docker": "Docker", + "host_pid": "Host PID", + "apparmor": "AppArmor", + "auth": "Auth API", + "ingress": "Ingress", + "signed": "Signed" + }, + "role": { + "manager": "manager", + "default": "default", + "homeassistant": "homeassistant", + "backup": "backup", + "admin": "admin" + }, + "stage": { + "title": "App stage", + "description": "This app has a specific stage classification." + }, + "rating": { + "title": "Security rating", + "description": "This shows the security rating of the app. Higher is better." + }, + "host_network": { + "title": "Host network", + "description": "This app uses the host network stack." + }, + "full_access": { + "title": "Full hardware access", + "description": "This app has full access to the hardware." + }, + "homeassistant_api": { + "title": "Home Assistant API", + "description": "This app has access to the Home Assistant API." + }, + "hassio_api": { + "title": "Supervisor API", + "description": "This app has access to the Supervisor API." + }, + "docker_api": { + "title": "Docker API", + "description": "This app has access to the Docker API." + }, + "host_pid": { + "title": "Host PID namespace", + "description": "This app has access to the host PID namespace." + }, + "apparmor": { + "title": "AppArmor", + "description": "This shows the AppArmor status of the app." + }, + "auth_api": { + "title": "Auth API", + "description": "This app has access to the Auth API." + }, + "ingress": { + "title": "Ingress", + "description": "This app supports ingress for secure access." + }, + "signed": { + "title": "Signed", + "description": "This app is cryptographically signed." + } + }, + "action_error": { + "install": "Failed to install app", + "start": "Failed to start app", + "stop": "Failed to stop app", + "restart": "Failed to restart app", + "rebuild": "Failed to rebuild app", + "uninstall": "Failed to uninstall app", + "get_changelog": "Failed to get changelog", + "start_invalid_config": "Invalid configuration", + "go_to_config": "Go to configuration", + "validate_config": "Failed to validate app configuration" + }, + "uninstall_dialog": { + "title": "Uninstall {name}?", + "remove_data": "Also remove app data", + "uninstall": "Uninstall" + }, + "restart_dialog": { + "title": "Restart {name}?", + "text": "The app needs to be restarted for the changes to take effect.", + "restart": "Restart" + }, + "update_available": { + "update_name": "Update {name}", + "no_update": "{name} is up to date.", + "description": "{name} {newest_version} is available. You are running {version}.", + "updating": "Updating {name} to {version}...", + "create_backup": { + "app": "Create backup before updating", + "app_description": "Creates a backup of {version} before updating.", + "generic": "Create backup" + } + } + }, + "configuration": { + "no_configuration": "This app has no configuration options.", + "reset_defaults": "Reset to defaults", + "options": { + "header": "Options", + "edit_in_ui": "Edit in UI", + "edit_in_yaml": "Edit in YAML", + "invalid_yaml": "Invalid YAML", + "show_unused_optional": "Show unused optional configuration options" + }, + "network": { + "header": "Network", + "introduction": "Configure the network ports that this app uses.", + "show_disabled": "Show disabled ports", + "reset_defaults": "Reset to defaults" + }, + "audio": { + "header": "Audio", + "input": "Audio input", + "output": "Audio output", + "default": "Default", + "failed_to_load_hardware": "Failed to fetch audio hardware", + "failed_to_save": "Failed to set audio device" + }, + "confirm": { + "reset_options": { + "title": "Reset options?", + "text": "Are you sure you want to reset all options to their default values?" + } + } + }, + "documentation": { + "get_documentation": "Failed to get documentation: {error}" + } + }, "category": { "caption": "Categories", "assign": { @@ -2571,20 +2921,20 @@ "caption": "Backups", "description": "Last backup {relative_time}", "description_no_backup": "Manage backups and restore Home Assistant to a previous state", - "create_backup": "[%key:supervisor::backup::create_backup%]", + "create_backup": "Create backup", "creating_backup": "Backup is currently being created", - "download_backup": "[%key:supervisor::backup::download_backup%]", + "download_backup": "Download backup", "remove_backup": "Delete backup", - "name": "[%key:supervisor::backup::name%]", + "name": "Backup name", "path": "Path", - "size": "[%key:supervisor::backup::size%]", - "created": "[%key:supervisor::backup::created%]", - "no_backups": "[%key:supervisor::backup::no_backups%]", + "size": "Size", + "created": "Created", + "no_backups": "You don't have any backups yet.", "backup_type": "Type", "type": { "manual": "Manual", "automatic": "Automatic", - "addon_update": "Add-on update" + "app_update": "App update" }, "locations": "Locations", "create": { @@ -2786,17 +3136,17 @@ "media": "Media", "media_description": "For example, camera recordings.", "share_folder": "Share folder", - "share_folder_description": "Folder that is often used by add-ons for advanced or older configurations.", - "local_addons": "Local add-ons folder", - "local_addons_description": "Folder that contains the data of your local add-ons.", - "addons": "Add-ons", - "addons_description": "Select what add-ons you want to include.", - "addons_all": "All", - "addons_none": "None", - "addons_custom": "Custom", + "share_folder_description": "Folder that is often used by apps for advanced or older configurations.", + "local_apps": "Local apps folder", + "local_apps_description": "Folder that contains the data of your local apps.", + "apps": "Apps", + "apps_description": "Select what apps you want to include.", + "apps_all": "All", + "apps_none": "None", + "apps_custom": "Custom", "estimated_size": "Estimated backup size", "estimated_size_disclaimer": "This is an approximation based on storage usage and compression. The actual backup size will vary.", - "estimated_size_disclaimer_addons_custom": "This estimate includes all add-ons, and not individual add-on sizes.", + "estimated_size_disclaimer_apps_custom": "This estimate includes all apps, and not individual app sizes.", "estimated_size_loading": "Loading size estimate..." }, "data_picker": { @@ -2804,8 +3154,8 @@ "settings_and_history": "Settings and history", "media": "Media", "share_folder": "Share folder", - "local_addons": "Local add-ons folder", - "addons": "Add-ons", + "local_apps": "Local apps folder", + "apps": "Apps", "ssl": "SSL certificates" }, "schedule": { @@ -2895,9 +3245,12 @@ }, "description": { "create_backup": { - "addon_repositories": "Backing up add-on repositories", - "addons": "Backing up add-ons", - "await_addon_restarts": "Waiting for add-ons to restart", + "addon_repositories": "[%key:ui::panel::config::backup::overview::progress::description::create_backup::app_repositories%]", + "addons": "[%key:ui::panel::config::backup::overview::progress::description::create_backup::apps%]", + "await_addon_restarts": "[%key:ui::panel::config::backup::overview::progress::description::create_backup::await_app_restarts%]", + "app_repositories": "Backing up app repositories", + "apps": "Backing up apps", + "await_app_restarts": "Waiting for apps to restart", "docker_config": "Backing up Docker configuration", "finishing_file": "Finishing backup file", "folders": "Backing up folders", @@ -2905,16 +3258,20 @@ "upload_to_agents": "Uploading to locations" }, "restore_backup": { - "addon_repositories": "Restoring add-on repositories", - "addons": "Restoring add-ons", - "await_addon_restarts": "Waiting for add-ons to restart", + "addon_repositories": "[%key:ui::panel::config::backup::overview::progress::description::restore_backup::app_repositories%]", + "addons": "[%key:ui::panel::config::backup::overview::progress::description::restore_backup::apps%]", + "await_addon_restarts": "[%key:ui::panel::config::backup::overview::progress::description::restore_backup::await_app_restarts%]", + "app_repositories": "Restoring app repositories", + "apps": "Restoring apps", + "await_app_restarts": "Waiting for apps to restart", "await_home_assistant_restart": "Waiting for Home Assistant to restart", "check_home_assistant": "Checking Home Assistant", "docker_config": "Restoring Docker configuration", "download_from_agent": "Downloading from location", "folders": "Restoring folders", "home_assistant": "Restoring Home Assistant", - "remove_delta_addons": "Removing delta add-ons" + "remove_delta_addons": "[%key:ui::panel::config::backup::overview::progress::description::restore_backup::remove_delta_apps%]", + "remove_delta_apps": "Removing delta apps" }, "receive_backup": { "receive_file": "Receiving file", @@ -2931,12 +3288,12 @@ "last_backup_failed_heading": "Last automatic backup failed", "last_backup_failed_description": "The last automatic backup triggered {relative_time} wasn't successful.", "last_backup_failed_locations_description": "The last automatic backup created {relative_time} wasn't stored in all locations.", - "last_backup_failed_addons_description": "The last automatic backup created {relative_time} was not able to backup all add-ons.", + "last_backup_failed_apps_description": "The last automatic backup created {relative_time} was not able to backup all apps.", "last_backup_failed_folders_description": "The last automatic backup created {relative_time} was not able to backup all folders.", - "last_backup_failed_addons_folders_description": "The last automatic backup created {relative_time} wasn't able to backup all add-ons and folders.", - "last_backup_failed_locations_addons_description": "The last automatic backup created {relative_time} wasn't able to backup all add-ons and wasn't stored in all locations.", + "last_backup_failed_apps_folders_description": "The last automatic backup created {relative_time} wasn't able to backup all apps and folders.", + "last_backup_failed_locations_apps_description": "The last automatic backup created {relative_time} wasn't able to backup all apps and wasn't stored in all locations.", "last_backup_failed_locations_folders_description": "The last automatic backup created {relative_time} wasn't able to backup all folders and wasn't stored in all locations.", - "last_backup_failed_locations_addons_folders_description": "The last automatic backup created {relative_time} wasn't able to backup all add-ons and folders and wasn't stored in all locations.", + "last_backup_failed_locations_apps_folders_description": "The last automatic backup created {relative_time} wasn't able to backup all apps and folders and wasn't stored in all locations.", "last_successful_backup_description": "Last successful automatic backup {relative_time} and stored in {count} {count, plural,\n one {location}\n other {locations}\n}.", "no_backup_heading": "No automatic backup available", "no_backup_description": "You have no automatic backups yet.", @@ -2948,7 +3305,7 @@ "title": "My backups", "automatic": "{count} automatic {count, plural,\n one {backup}\n other {backups}\n}", "manual": "{count} manual {count, plural,\n one {backup}\n other {backups}\n}", - "addon_update": "{count} add-on update {count, plural,\n one {backup}\n other {backups}\n}", + "app_update": "{count} app update {count, plural,\n one {backup}\n other {backups}\n}", "total_size": "{size} in total", "show_all": "Show all backups" }, @@ -2971,10 +3328,10 @@ "data": "Home Assistant data that is included", "data_settings_history": "Settings and history", "data_settings_only": "Settings only", - "addons": "Add-ons that are included", - "addons_all": "All add-ons", - "addons_many": "{count} {count, plural,\n one {add-on}\n other {add-ons}\n}", - "addons_none": "No add-ons", + "apps": "Apps that are included", + "apps_all": "All apps", + "apps_many": "{count} {count, plural,\n one {app}\n other {apps}\n}", + "apps_none": "No apps", "locations": "Locations where backup is stored to", "locations_one": "Store in {name}", "locations_many": "Store in {count} off-site {count, plural,\n one {location}\n other {locations}\n}", @@ -3035,9 +3392,9 @@ "title": "Encryption key", "description": "Keep this encryption key in a safe place, as you will need it to access your backup, allowing it to be restored. Download it as an emergency kit file and store it somewhere safe. Encryption keeps your backups private and secure." }, - "addon_update_backup": { - "title": "Add-on update backups", - "description": "Creates a backup of your add-on and its data. That way you can keep around the previous version of the add-on, so you can always roll back to it if needed.", + "app_update_backup": { + "title": "App update backups", + "description": "Creates a backup of your app and its data. That way you can keep around the previous version of the app, so you can always roll back to it if needed.", "local_only": "This backup is only saved on this system.", "retention_description": "Prevent your system from filling up with old versions.", "error_load": "Error loading Supervisor update config: {error}", @@ -3056,7 +3413,7 @@ "error": { "title": "This backup was not created successfully. Some data is missing.", "failed_locations": "Failed locations", - "failed_addons": "Failed add-ons", + "failed_apps": "Failed apps", "failed_folders": "Failed folders" } }, @@ -3121,7 +3478,8 @@ "icon": "Icon", "name": "Name", "last_scanned": "Last scanned", - "write": "Write" + "write": "Write", + "tag_id": "Tag ID" }, "detail": { "new_tag": "New tag", @@ -3138,6 +3496,394 @@ "companion_apps": "Companion apps" } }, + "developer-tools": { + "tabs": { + "assist": { + "title": "Sentences parser", + "description": "Enter sentences and see how they will be parsed by Home Assistant. Each line will be processed as an individual sentence. Intents will not be executed on your instance.", + "parse_sentences": "Parse sentences", + "sentences": "Sentences", + "download_results": "Download results", + "no_match": "No intent matched", + "language": "[%key:ui::components::language-picker::language%]" + }, + "debug": { + "title": "Debug tools", + "debug_connection": { + "title": "Debug connection", + "description": "Observe requests to the server and responses from the server in browser console." + }, + "disable_view_transition": { + "title": "Disable view transitions", + "description": "Disable animated view transitions to prevent browser crash when using browser dev tools." + }, + "entity_diagnostic": { + "title": "Entity diagnostic", + "description": "Select an entity to copy diagnostic details.", + "copy_to_clipboard": "[%key:ui::panel::config::automation::editor::copy_to_clipboard%]" + } + }, + "events": { + "title": "Events", + "description": "Fire an event on the event bus.", + "documentation": "Events documentation", + "type": "Event type", + "data": "Event data (YAML, optional)", + "fire_event": "Fire event", + "event_fired": "Event {name} fired", + "active_listeners": "Active listeners", + "count_listeners": "({count} {count, plural,\n one {listener}\n other {listeners}\n})", + "listen_to_events": "Listen to events", + "filter_events": "Filter events", + "filter_helper": "Only capture events containing the filter string.", + "filter_ignored": "Ignored {count} events.", + "listening_to": "Listening to", + "subscribe_to": "Event to subscribe to", + "start_listening": "Start listening", + "stop_listening": "Stop listening", + "clear_events": "Clear events", + "alert_event_type": "Event type is a mandatory field", + "notification_event_fired": "Event {type} successfully fired!", + "subscribe_failed": "Failed to subscribe to event: {error}" + }, + "actions": { + "title": "Actions", + "description": "The actions dev tool allows you to perform any action available in Home Assistant.", + "call_service": "Perform action", + "response": "Response", + "column_parameter": "Parameter", + "column_description": "Description", + "column_example": "Example", + "fill_example_data": "Fill example data", + "yaml_mode": "Go to YAML mode", + "ui_mode": "Go to UI mode", + "yaml_parameters": "Parameters only available in YAML mode", + "all_parameters": "All available parameters", + "accepts_target": "This action accepts a target, for example: `entity_id: light.bed_light`", + "no_template_ui_support": "The UI does not support templates, you can still use the YAML editor.", + "copy_clipboard_template": "Copy to clipboard as template", + "open_media": "Open media", + "errors": { + "ui": { + "no_action": "No action selected, please select an action", + "invalid_action": "Selected action is invalid, please select a valid action", + "no_target": "This action requires a target, please select a target from the picker", + "missing_required_field": "This action requires field {key}, please enter a valid value for {key}" + }, + "yaml": { + "invalid_yaml": "Action YAML contains syntax errors, please fix the syntax", + "no_action": "No action defined, please define an 'action:' key", + "invalid_action": "Defined action is invalid, please provide an action in the format domain.action", + "no_target": "This action requires a target, please define a target 'entity_id', 'device_id', or 'area_id' under 'target:' or 'data:'", + "missing_required_field": "This action requires field {key}, which must be provided under 'data:'" + } + } + }, + "states": { + "title": "States", + "description1": "Set the current state representation of an entity within Home Assistant.", + "description2": "If the entity belongs to a device, there will be no actual communication with that device.", + "entity": "Entity", + "state": "State", + "attributes": "Attributes", + "state_attributes": "State attributes (YAML, optional)", + "set_state": "Set state", + "current_entities": "Current entity states", + "filter_entities": "Filter entities", + "filter_states": "Filter states", + "filter_attributes": "Filter attributes", + "no_entities": "No entities", + "more_info": "More info", + "alert_entity_field": "Entity is a mandatory field", + "last_updated": "[%key:ui::dialogs::more_info_control::last_updated%]", + "last_changed": "[%key:ui::dialogs::more_info_control::last_changed%]", + "copy_id": "Copy ID to clipboard" + }, + "templates": { + "title": "Template", + "description": "Templates are rendered using the Jinja2 template engine with some Home Assistant specific extensions.", + "editor": "Template editor", + "result": "Result", + "reset": "Reset to demo template", + "confirm_reset": "Do you want to reset your current template back to the demo template?", + "confirm_clear": "Do you want to clear your current template?", + "result_type": "Result type", + "jinja_documentation": "Jinja2 template documentation", + "template_extensions": "Home Assistant template extensions", + "unknown_error_template": "Unknown error rendering template", + "time": "This template updates at the start of each minute.", + "all_listeners": "This template listens for all state changed events.", + "no_listeners": "This template does not listen for any events and will not update automatically.", + "listeners": "This template listens for the following state changed events:", + "entity": "Entity", + "domain": "Domain" + }, + "statistics": { + "title": "Statistics", + "entity": "Entity", + "issue": "Issue", + "no_issue": "No issue", + "issues": { + "units_changed": "The unit of this entity changed from ''{metadata_unit}'' to ''{state_unit}''.", + "mean_type_changed": "The mean type of this entity changed from ''{metadata_mean_type}'' to ''{state_mean_type}''.", + "state_class_removed": "This entity no longer has a state class", + "entity_not_recorded": "This entity is excluded from being recorded.", + "entity_no_longer_recorded": "This entity is no longer being recorded.", + "no_state": "There is no state available for this entity." + }, + "delete_selected": "Delete selected statistics", + "multi_delete": { + "title": "Delete selected statistics", + "info_text": "Do you want to permanently delete the long term statistics {statistic_count, plural,\n one {of this entity}\n other {of {statistic_count} entities}\n} from your database?" + }, + "mean_type": { + "0": "None", + "1": "Arithmetic", + "2": "Circular" + }, + "fix_issue": { + "fix": "Fix issue", + "clearing_failed": "Clearing the statistics failed", + "clearing_timeout_title": "Clearing not done yet", + "clearing_timeout_text": "The clearing of the statistics took longer than expected, it might take longer for the issue to disappear.", + "fix_all": "Fix all", + "info": "Info", + "no_support": { + "title": "Fix issue", + "info_text_1": "Fixing this issue is not supported yet." + }, + "no_state": { + "title": "Entity has no state", + "info_text_1": "''{name}'' ({statistic_id}) has no state at the moment, if this is an orphaned entity, you may want to delete the long term statistics of it from your database.", + "info_text_2": "Do you want to permanently delete the long term statistics of {statistic_id} from your database?" + }, + "entity_not_recorded": { + "title": "Entity is not recorded", + "info_text_1": "State changes of ''{name}'' ({statistic_id}) are not recorded, therefore, we cannot track long term statistics for it.", + "info_text_2": "You probably excluded this entity, or have just included some entities.", + "info_text_3_link": "See the Recorder documentation for more information." + }, + "entity_no_longer_recorded": { + "title": "Entity is no longer recorded", + "info_text_1": "We have generated statistics for ''{name}'' ({statistic_id}) in the past, but state changes of this entity are no longer recorded, therefore, we cannot track long term statistics for it anymore.", + "info_text_2": "You probably excluded this entity, or have just included some entities.", + "info_text_3_link": "See the Recorder documentation for more information.", + "info_text_4": "If you no longer wish to keep the long term statistics recorded in the past, you may delete them now." + }, + "state_class_removed": { + "title": "The entity no longer has a state class", + "info_text_1": "We have generated statistics for ''{name}'' ({statistic_id}) in the past, but it no longer has a state class, therefore, we cannot track long term statistics for it anymore.", + "info_text_2": "Statistics cannot be generated until this entity has a supported state class.", + "info_text_3": "If the state class was previously provided by an integration, this might be a bug. Please report an issue.", + "info_text_4": "If you previously set the state class yourself, please correct it.", + "info_text_4_link": "The different state classes and when to use which can be found in the developer documentation.", + "info_text_5": "If the state class has permanently been removed, you may want to delete the long term statistics of it from your database.", + "info_text_6": "Do you want to permanently delete the long term statistics of {statistic_id} from your database?" + }, + "units_changed": { + "title": "The unit has changed", + "update": "Update the unit of the historic statistic values from ''{metadata_unit}'' to ''{state_unit}'', without converting.", + "clear": "Delete all old statistic data for this entity", + "how_to_fix": "How do you want to fix this issue?", + "info_text_1": "The unit of ''{name}'' ({statistic_id}) changed to ''{current_unit}'' which can't be converted to the previously stored unit, ''{previous_unit}''.", + "info_text_2": "If the historic statistic values have a wrong unit, you can update the units of the old values. The values will not be updated.", + "info_text_3": "Otherwise you can choose to delete all historic statistic values, and start over." + }, + "mean_type_changed": { + "title": "The mean type has changed", + "info_text_1": "The mean type of ''{name}'' ({statistic_id}) changed from ''{metadata_mean_type}'' to ''{state_mean_type}''.", + "info_text_2": "Statistics cannot be generated until the old statistics are deleted, or the mean type matches the old statistics data again.", + "info_text_3": "Do you want to permanently delete the long term statistics of {statistic_id} from your database?" + }, + "adjust_sum": { + "title": "Adjust a statistic", + "no_statistics_found": "No statistics found for this period.", + "info_text_1": "Sometimes the statistics end up being incorrect for a specific point in time. This can mess up your beautiful graphs! Select a time below to find the bad moment and adjust the data.", + "pick_a_time": "Pick a time", + "statistic": "Statistic:", + "start": "Start", + "end": "End", + "new_value": "New value", + "adjust": "Adjust", + "outliers": "Outliers", + "sum_adjusted": "Statistic sum adjusted", + "error_sum_adjusted": "Error adjusting sum: {message}" + } + }, + "adjust_sum": "Adjust sum", + "data_table": { + "select_all_issues": "Select all with issues", + "name": "Name", + "statistic_id": "Statistic ID", + "statistics_unit": "Statistics unit", + "source": "Source", + "issue": "Issue", + "no_statistics": "[%key:ui::components::statistics_charts::no_statistics_found%]" + } + }, + "yaml": { + "title": "YAML", + "section": { + "validation": { + "heading": "Check and restart", + "introduction": "A basic validation of the configuration is automatically done before restarting. The basic validation ensures the YAML configuration doesn't have errors which will prevent Home Assistant or any integration from starting. It's also possible to only do the basic validation check without restarting.", + "check_config": "Check configuration", + "valid": "Configuration will not prevent Home Assistant from starting!", + "invalid": "Configuration invalid!", + "warnings": "Configuration warnings", + "errors": "Configuration errors" + }, + "reloading": { + "all": "All YAML configuration", + "heading": "YAML configuration reloading", + "introduction": "Some parts of Home Assistant can reload without requiring a restart. Selecting one of the options below will unload their current YAML configuration and load the new one.", + "reload": "{domain}", + "core": "Location & customizations", + "group": "Groups, group entities, and notify services", + "automation": "Automations", + "script": "Scripts", + "scene": "Scenes", + "person": "People", + "zone": "Zones", + "input_boolean": "Input booleans", + "input_button": "Input buttons", + "input_text": "Input texts", + "input_number": "Input numbers", + "input_datetime": "Input datetimes", + "input_select": "Input selects", + "template": "Template entities", + "universal": "Universal media player entities", + "rest": "REST entities and notify services", + "command_line": "Command line entities", + "filter": "Filter entities", + "statistics": "Statistics entities", + "generic": "Generic IP camera entities", + "generic_thermostat": "Generic thermostat entities", + "homekit": "HomeKit", + "min_max": "Min/max entities", + "history_stats": "History stats entities", + "trend": "Trend entities", + "ping": "Ping binary sensor entities", + "filesize": "File size entities", + "telegram": "Telegram notify services", + "smtp": "SMTP notify services", + "mqtt": "Manually configured MQTT entities", + "rpi_gpio": "Raspberry Pi GPIO entities", + "timer": "Timers", + "themes": "Themes" + }, + "server_management": { + "heading": "Home Assistant", + "restart": "Restart", + "stop": "Stop", + "confirm_stop": "Are you sure you want to stop Home Assistant?", + "restart_error": "Failed to restart Home Assistant" + } + } + }, + "blueprints": { + "title": "Blueprints", + "paste_invalid_config": "Pasted blueprint is not editable in the visual editor", + "paste_toast_message": "Pasted blueprint from clipboard", + "dialog_pick": { + "header": "Pick a blueprint", + "automation": "Automation", + "script": "Script", + "create_empty_automation": "Create new automation blueprint", + "create_empty_automation_description": "Start with an empty automation blueprint from scratch", + "create_empty_script": "Create new script blueprint", + "create_empty_script_description": "Start with an empty script blueprint from scratch" + }, + "editor": { + "load_blueprints_error_title": "Error loading blueprints", + "load_blueprints_error_text": "There was a problem loading blueprints. Please try again.", + "load_error_not_editable": "Only blueprints in blueprints.yaml are editable.", + "load_error_unknown": "Error loading blueprint ({err_no}).", + "save_error_title": "Error saving blueprint", + "save_error_text": "There was a problem saving the blueprint. Please make sure there are no errors and try again.", + "move_up": "Move up", + "move_down": "Move down", + "edit_ui": "UI editor", + "edit_yaml": "YAML editor", + "metadata": "Metadata", + "error": "There is a problem with the selected blueprint.", + "none_selected": "Please pick a blueprint to edit it.", + "overwrite_existing_title": "You are about to overwrite an existing blueprint", + "overwrite_existing_text": "Reimporting this blueprint will cause your changes to be lost. Are you sure?", + "abandon_changes_title": "You have unsaved changes", + "abandon_changes_text": "Picking a new blueprint will cause your changes to be lost.", + "abandon_changes_confirm_text": "Abandon changes", + "reset_text": "You will lose all of the changes you've made so far.", + "paste_confirm": { + "title": "Pasted blueprint", + "text": "How do you want to paste your blueprint?" + }, + "actions": { + "delete": "Delete", + "duplicate": "Duplicate", + "pick": "Pick a blueprint", + "reset": "Reset", + "edit_ui": "[%key:ui::panel::lovelace::editor::edit_view::edit_ui%]", + "edit_yaml": "[%key:ui::panel::lovelace::editor::edit_view::edit_yaml%]" + }, + "inputs": { + "header": "Inputs", + "section_description": "Inputs are variables that make blueprints generic. The exact value will be determined when an instance of this blueprint is created", + "learn_more": "Learn more about inputs", + "add": "Add new input", + "testing_pass": "Input passes", + "testing_error": "Input did not pass", + "copy": "Copy", + "cut": "Cut", + "rename": "Rename", + "delete_confirm_title": "Delete input", + "delete_confirm_text": "Do you want to delete this input from your blueprint?", + "id": "ID", + "change_id": "Change input ID", + "editor": { + "single": "Input", + "section": "Input section" + }, + "type": { + "new": { + "input": "Input", + "section": "Section", + "type": "Type", + "id": "ID" + }, + "section": { + "name": "Name", + "description": "Description", + "icon": "Icon", + "collapsed": "Collapsed" + }, + "single": { + "name": "Name", + "description": "Description", + "default": "Default value", + "selector": "Selector" + } + } + }, + "name": { + "label": "Name" + }, + "path": { + "label": "Path" + }, + "description": { + "label": "Description" + }, + "author": { + "label": "Author" + }, + "min_version": { + "label": "Minimum version" + } + } + } + } + }, "energy": { "caption": "Energy", "description": "Monitor your energy production and consumption", @@ -3165,8 +3911,15 @@ "delete_power": "Delete power sensor", "power_dialog": { "header": "Configure grid power", + "sensor_type": "Type of power measurement", + "type_standard": "Standard", + "type_inverted": "Inverted", + "type_inverted_description": "Positive values indicate exporting to the grid, negative values indicate importing from the grid.", + "type_two_sensors": "Two sensors", "power_stat": "Power sensor", - "power_helper": "Pick a sensor which measures grid power in either of {unit}. Positive values indicate importing electricity from the grid, negative values indicate exporting electricity to the grid." + "power_helper": "Pick a sensor which measures grid power in either of {unit}. Positive values indicate importing electricity from the grid, negative values indicate exporting electricity to the grid.", + "power_from_grid": "Power from grid", + "power_to_grid": "Power to grid" }, "flow_dialog": { "cost_entity_helper": "Any sensor with a unit of `{currency}/(valid energy unit)` (e.g. `{currency}/Wh` or `{currency}/kWh`) may be used and will be automatically converted.", @@ -3238,7 +3991,15 @@ "energy_into_battery": "Energy charged into the battery", "energy_out_of_battery": "Energy discharged from the battery", "power": "Battery power", - "power_helper": "Pick a sensor which measures the electricity flowing into and out of the battery in either of {unit}. Positive values indicate discharging the battery, negative values indicate charging the battery." + "power_helper": "Pick a sensor which measures the electricity flowing into and out of the battery in either of {unit}. Positive values indicate discharging the battery, negative values indicate charging the battery.", + "sensor_type": "Type of power measurement", + "type_none": "No power sensor", + "type_standard": "Standard", + "type_inverted": "Inverted", + "type_inverted_description": "Positive values indicate charging, negative values indicate discharging.", + "type_two_sensors": "Two sensors", + "power_discharge": "Discharge power", + "power_charge": "Charge power" } }, "gas": { @@ -3346,11 +4107,7 @@ "headers": { "icon": "Icon", "name": "Name", - "entity_id": "Entity ID", - "type": "Type", - "editable": "Editable", - "category": "Category", - "area": "Area" + "type": "Type" }, "create_helper": "Create helper", "no_helpers": "Looks like you don't have any helpers yet!", @@ -3367,8 +4124,8 @@ } }, "core": { - "caption": "General", - "description": "Name, time zone, and locale settings", + "caption": "Home information", + "description": "Name, location, and regional settings", "section": { "core": { "header": "General configuration", @@ -3400,6 +4157,15 @@ "update_units_confirm_title": "The unit of sensors will be updated", "update_units_confirm_text": "The unit system has been changed, and the unit of some sensors like distance and speed will be updated.", "update_units_confirm_update": "Update" + }, + "home_name_card": { + "header": "Home name" + }, + "location_card": { + "header": "Location" + }, + "regional_settings_card": { + "header": "Region" } } } @@ -3560,21 +4326,9 @@ "title": "Webpage", "description": "Integrate a webpage as a dashboard" }, - "areas": { - "title": "Areas (experimental)", - "description": "Display your devices with a view for each area" - }, - "default": { - "title": "Default dashboard", - "description": "Display your devices grouped by area" - }, - "overview": { - "title": "Overview", + "overview-legacy": { + "title": "Overview (Legacy)", "description": "Gives an overview of all your entities and areas they are in" - }, - "home": { - "title": "Home (experimental)", - "description": "Global overview of your home" } }, "search_dashboards": "Search dashboards", @@ -3624,7 +4378,8 @@ "set_default": "Set as default", "remove_default": "Remove as default", "set_default_confirm_title": "Set as default dashboard?", - "set_default_confirm_text": "This dashboard will be shown to all users when opening Home Assistant. Each user can change this in their profile." + "set_default_confirm_text": "This dashboard will be shown to all users when opening Home Assistant.", + "set_default_confirm_note": "Users who have chosen a specific dashboard in their profile will not be affected. They must set it back to \"Auto (use system settings)\" to use this dashboard." } }, "resources": { @@ -3650,7 +4405,10 @@ "confirm_delete_text": "{url} will be permanently deleted.", "refresh_header": "Do you want to refresh?", "refresh_body": "You have to refresh the page to complete the removal. Do you want to refresh now?", - "cant_edit_yaml": "You are using your dashboard in YAML mode, therefore you cannot manage your resources through the UI. Manage them in configuration.yaml.", + "reload_resources": "[%key:ui::panel::lovelace::menu::reload_resources%]", + "reload_refresh_header": "[%key:ui::panel::lovelace::reload_resources::refresh_header%]", + "reload_refresh_body": "[%key:ui::panel::lovelace::reload_resources::refresh_body%]", + "cant_edit_yaml": "Your resources are in YAML mode, therefore you cannot manage them through the UI. Manage them in configuration.yaml.", "detail": { "new_resource": "Add new resource", "edit_resource": "Edit resource", @@ -3801,7 +4559,7 @@ "run_text_pipeline": "Run text pipeline", "run_audio_pipeline": "Run audio pipeline", "run_audio_with_wake": "Run audio pipeline with wake word detection", - "response": "[%key:ui::panel::developer-tools::tabs::actions::response%]", + "response": "[%key:ui::panel::config::developer-tools::tabs::actions::response%]", "send": "Send", "continue_listening": "Continue listening for wake word", "continue_talking": "Continue talking", @@ -3816,10 +4574,6 @@ "headers": { "icon": "Icon", "name": "Name", - "entity_id": "Entity ID", - "area": "Area", - "domain": "Domain", - "assistants": "Assistants", "aliases": "Aliases", "remove": "[%key:ui::common::remove%]" }, @@ -3918,11 +4672,11 @@ } }, "local": { - "title": "Installing add-ons", + "title": "Installing apps", "secondary": "We are preparing your system for local voice processing.", - "failed_title": "Failed to install add-ons", - "failed_secondary": "We were unable to install the add-ons for speech-to-text and text-to-speech automatically for you. Read the documentation to learn how to install them.", - "not_supported_title": "Installation of add-ons is not supported on your system", + "failed_title": "Failed to install apps", + "failed_secondary": "We were unable to install the apps for speech-to-text and text-to-speech automatically for you. Read the documentation to learn how to install them.", + "not_supported_title": "Installation of apps is not supported on your system", "not_supported_secondary": "Your system is not supported to automatically install a local TTS and STT provider. Learn how to set up local TTS and STT providers in the documentation.", "full_local_pipeline": "Full local assistant", "focused_local_pipeline": "Focused local assistant", @@ -3939,7 +4693,7 @@ "creating_pipeline": "Creating assistant" }, "errors": { - "failed_create_entry": "Failed to create entry for {addon}", + "failed_create_entry": "Failed to create entry for {app}", "could_not_find_entities": "Could not find local TTS and STT entities" } }, @@ -3989,8 +4743,6 @@ "trigger": "Trigger", "actions": "Actions", "state": "State", - "category": "Category", - "area": "Area", "icon": "Icon" }, "bulk_action": "Action", @@ -4093,8 +4845,10 @@ "switch_ui_yaml_error": "There are currently YAML errors in the automation, and it cannot be parsed. Switching to UI mode may cause pending changes to be lost. Press cancel to correct any errors before proceeding to prevent loss of pending changes, or continue if you are sure.", "type_automation": "automation", "type_script": "script", + "type_scene": "scene", "type_automation_plural": "[%key:ui::panel::config::blueprint::overview::types_plural::automation%]", "type_script_plural": "[%key:ui::panel::config::blueprint::overview::types_plural::script%]", + "type_scene_plural": "scenes", "new_automation_setup_failed_title": "New {type} setup timed out", "new_automation_setup_failed_text": "Your new {type} was saved, but waiting for it to set up has timed out. This could be due to errors parsing your configuration.yaml, please check the configuration in developer tools. Your {type} will not be visible until this is corrected and {types} are reloaded. Changes to area, category, or labels were not saved and must be reapplied.", "new_automation_setup_keep_waiting": "You may continue to wait for a response from the server, in case it is just taking an unusually long time to process this {type}.", @@ -5008,8 +5762,6 @@ "headers": { "name": "Name", "state": "State", - "category": "Category", - "area": "Area", "icon": "Icon" }, "edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]", @@ -5135,9 +5887,6 @@ "state": "State", "name": "Name", "last_activated": "Last activated", - "category": "Category", - "editable": "[%key:ui::panel::config::helpers::picker::headers::editable%]", - "area": "Area", "icon": "Icon" }, "edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]", @@ -5160,11 +5909,19 @@ "load_error_not_editable": "Only scenes in scenes.yaml are editable.", "load_error_unknown": "Error loading scene ({err_no}).", "save": "Save", + "rename": "Rename", + "missing_name": "Name is required", "unsaved_confirm_title": "Leave editor?", "unsaved_confirm_text": "Unsaved changes will be lost.", "name": "Name", "icon": "Icon", "area": "Area", + "dialog": { + "add_icon": "[%key:ui::panel::config::automation::editor::dialog::add_icon%]", + "add_area": "[%key:ui::panel::config::automation::editor::dialog::add_area%]", + "add_category": "[%key:ui::panel::config::automation::editor::dialog::add_category%]", + "add_labels": "[%key:ui::panel::config::automation::editor::dialog::add_labels%]" + }, "devices": { "header": "Devices", "introduction": "Add the devices that you want to be included in your scene. Set all entities in each device to the state you want for this scene.", @@ -5523,8 +6280,6 @@ "device": "Device", "manufacturer": "Manufacturer", "model": "Model", - "area": "Area", - "floor": "Floor", "integration": "Integration", "battery": "Battery", "disabled_by": "Disabled", @@ -5582,10 +6337,8 @@ "headers": { "state_icon": "State icon", "entity": "Entity", - "entity_id": "Entity ID", "device": "Device", "integration": "Integration", - "area": "Area", "disabled_by": "Disabled by", "status": "Status", "domain": "Domain", @@ -5626,6 +6379,9 @@ } } }, + "domains": { + "caption": "Domains" + }, "person": { "caption": "People", "introduction": "Manage the people Home Assistant recognizes and control their access.", @@ -5697,6 +6453,9 @@ "description": "Manage integrations with services or devices", "integration": "integration", "discovered": "Discovered", + "discovered_devices": "{count, plural,\n one {# discovered device}\n other {# discovered devices}\n}", + "manage_discovered": "Manage discovered devices", + "manage_discovered_description": "Ignore or configure previously discovered devices", "disabled": "Disabled", "available_integrations": "Available integrations", "new_flow": "Set up another instance of {integration}", @@ -6157,7 +6916,7 @@ "network_settings_title": "Network settings", "change_channel": "Change channel", "channel_dialog": { - "title": "Multiprotocol add-on in use", + "title": "Multiprotocol app in use", "text": "Zigbee and Thread share the same adapter and must use the same channel. Change the channel of both networks by reconfiguring multiprotocol from the hardware menu." } }, @@ -6996,8 +7755,8 @@ "title": "Devices", "description": "Generic information about your devices.", "header": "Device analytics", - "info": "Anonymously share data about your devices to help build the Open Home Foundation’s device database. This free, open source resource helps users find useful information about smart home devices. Only device-specific details (like model or manufacturer) are shared — never personally identifying information (like the names you assign).", - "learn_more": "Learn more about the device database and how we process your data", + "info": "Anonymously share data about your devices to help build the Open Home Foundation's device database. This free, open source resource helps users find useful information about smart home devices. Only device-specific details (like model or manufacturer) are shared — never personally identifying information (like the names you assign). Learn more about the device database and how we process your data in our {data_use_statement}, which you accept by opting in.", + "data_use_statement": "Data Use Statement", "alert": { "title": "Important", "content": "Only enable this option if you understand that your device information will be shared." @@ -7009,6 +7768,10 @@ "intro": "Share anonymized information from your installation to help make Home Assistant better and help us convince manufacturers to add local control and privacy-focused features.", "download_device_info": "Preview device analytics" }, + "ai_tasks": { + "caption": "AI tasks", + "description": "Configure AI suggestions and task preferences" + }, "labs": { "caption": "Labs", "custom_integration": "Custom integration", @@ -7114,8 +7877,8 @@ "used": "Used space", "free": "Free space", "system": "System", - "addons_data": "Add-on data", - "addons_config": "Add-on configuration", + "addons_data": "App data", + "addons_config": "App configuration", "media": "Media", "share": "Share", "backup": "Backups", @@ -7241,8 +8004,6 @@ "empty_state_action": "Go to the integrations page" }, "areas": { - "empty_state_title": "No devices", - "empty_state_content": "There are no devices assigned to this area yet. Assign devices to this area to see them here.", "sensors": "Sensors", "sensors_description": "To display temperature and humidity sensors in the overview and in the area view, add a sensor to that area and {edit_the_area} to configure related sensors.", "edit_the_area": "edit the area", @@ -7270,13 +8031,17 @@ "areas": "Areas", "other_areas": "Other areas", "devices": "Devices", - "unamed_device": "Unnamed device", + "unnamed_device": "Unnamed device", "others": "Others", "scenes": "Scenes", "automations": "Automations", "for_you": "For you", "home": "Home", - "favorites": "Favorites" + "favorites": "Favorites", + "welcome_title": "No devices here yet", + "welcome_content": "Add lights, switches, sensors, or other smart home devices to get started.", + "welcome_add_device": "Add new device", + "welcome_edit_areas": "Edit areas" }, "common_controls": { "not_loaded": "Usage Prediction integration is not loaded.", @@ -7284,7 +8049,8 @@ }, "light": { "lights": "Lights", - "other_lights": "Other lights" + "other_lights": "Other lights", + "all_lights": "All lights" }, "security": { "devices": "Devices", @@ -7298,9 +8064,18 @@ "media_players": "Media players", "other_media_players": "Other media players" }, - "other_devices": { + "home-other-devices": { "helpers": "Helpers", - "entities": "Entities" + "entities": "Entities", + "assign_area": "Assign area", + "all_organized_title": "All devices are organized", + "all_organized_content": "There are no unassigned devices left. All devices are organized into areas." + }, + "home-area": { + "no_devices_title": "This is a blank canvas", + "no_devices_content": "Add your smart lights, switches, or sensors to this area to get started.", + "no_devices_add_device": "Add new device", + "no_devices_assign_device": "Assign existing device" } }, "cards": { @@ -7431,7 +8206,8 @@ }, "energy_devices_detail_graph": { "untracked_consumption": "Untracked consumption", - "untracked": "untracked" + "untracked": "untracked", + "other": "Other" }, "carbon_consumed_gauge": { "card_indicates_energy_used": "This card indicates how much of the electricity consumed by your home was generated using non-fossil fuels like solar, wind, and nuclear. The higher, the better!", @@ -7450,6 +8226,13 @@ "compare_previous_period": "Compare previous period" } }, + "distribution": { + "no_entities": "No entities specified", + "domain_mismatch": "All entities must be from the same domain. Found: {domains}", + "device_class_mismatch": "All entities must have the same device class. Found: {classes}", + "no_data": "No data to display. All entities are hidden or have zero values.", + "add_entities": "Add entities to display in the distribution chart" + }, "heading": { "default_heading": "Kitchen" } @@ -7499,7 +8282,9 @@ "create_area_action": "View area", "add_person_success": "Person added", "add_person_action": "View persons", - "add_person": "Add person" + "add_person": "Add person", + "edit_overview": "Edit overview", + "edit_area": "Edit area" }, "reload_resources": { "refresh_header": "Do you want to refresh?", @@ -7528,14 +8313,13 @@ "saved": "Saved", "reload": "Reload", "lovelace_changed": "Your dashboard was updated, do you want to load the updated config in the editor and lose your current changes?", - "confirm_delete_config_title": "Delete dashboard configuration?", - "confirm_delete_config_text": "This dashboard will be permanently deleted. The dashboard will be automatically regenerated to display your areas, devices and entities.", + "confirm_reset_config_title": "Reset dashboard configuration?", + "confirm_reset_config_text": "Your dashboard will be reset to an empty state. You can start fresh and build your dashboard from scratch.", "confirm_unsaved_changes": "You have unsaved changes, are you sure you want to exit?", "confirm_unsaved_comments": "Your configuration might contain comments, these will not be saved. Do you want to continue?", "error_parse_yaml": "Unable to parse YAML: {error}", "error_invalid_config": "Your configuration is not valid: {error}", "error_save_yaml": "Unable to save YAML: {error}", - "error_remove": "Unable to remove configuration: {error}", "resources_moved": "Resources should no longer be added to the dashboard configuration but can be added in the dashboard config panel." }, "edit_lovelace": { @@ -7925,7 +8709,8 @@ "icon": "Area icon", "picture": "Area picture", "camera": "Camera feed" - } + }, + "image_tap_action": "Image tap behavior" }, "calendar": { "name": "Calendar", @@ -7967,7 +8752,8 @@ "tilt-position": "Tilt position", "brightness": "Brightness", "last-updated": "Last updated", - "state": "State" + "state": "State", + "area": "Area" }, "entity_row": { "divider": "Divider", @@ -7988,14 +8774,14 @@ }, "empty_state": { "name": "Empty state", - "description": "The Empty state card displays a centered message with an optional icon and action button.", + "description": "The Empty state card displays a centered message with an optional icon and action buttons.", "style": "Style", "style_options": { "card": "Card", "content-only": "Content only" }, "content": "Content", - "action_text": "Action button text" + "buttons": "Buttons" }, "button": { "name": "Button", @@ -8106,6 +8892,10 @@ "title": "[%key:ui::panel::lovelace::editor::card::grid::title%]", "description": "The Horizontal stack card allows you to stack together multiple cards, so they always sit next to each other in the space of one column." }, + "distribution": { + "name": "Distribution", + "description": "The Distribution card displays multiple numerical entities as a proportional chart with an interactive legend." + }, "humidifier": { "name": "Humidifier", "description": "The Humidifier card gives control of a humidifier entity for humidifying or dehumidifying, allowing you to set its mode and desired humidity.", @@ -8187,7 +8977,7 @@ "title": "Title", "subtitle": "Subtitle" }, - "entities": "Entities", + "badges": "Badges", "entity_config": { "color": "[%key:ui::panel::lovelace::editor::card::tile::color%]", "color_helper": "[%key:ui::panel::lovelace::editor::card::tile::color_helper%]", @@ -8202,6 +8992,12 @@ "state": "[%key:ui::panel::lovelace::editor::badge::entity::displayed_elements_options::state%]" } }, + "button_config": { + "text": "Text", + "color": "Color", + "visibility": "Visibility", + "visibility_explanation": "The button will be shown when ALL conditions below are fulfilled. If no conditions are set, the button will always be shown." + }, "default_heading": "Kitchen" }, "map": { @@ -8217,7 +9013,7 @@ }, "default_zoom": "Default zoom", "source": "Source", - "description": "The Map card that allows you to display entities on a map." + "description": "The Map card allows you to display entities on a map." }, "markdown": { "name": "Markdown", @@ -8479,6 +9275,11 @@ "remove": "Remove entity", "form-label": "Edit entity" }, + "badges": { + "name": "Badges", + "edit": "Edit badge", + "remove": "Remove badge" + }, "features": { "name": "Features", "not_compatible": "Not compatible", @@ -8701,6 +9502,10 @@ "label": "Area controls", "customize_controls": "Customize controls", "controls": "Controls", + "sections": { + "domain": "Domains", + "entity": "Entities" + }, "controls_options": { "light": "Lights", "fan": "Fans", @@ -8729,6 +9534,19 @@ } } }, + "heading-badges": { + "add": "Add badge", + "no_entity": "No entity selected", + "entity_not_found": "Entity not found", + "types": { + "entity": { + "label": "Entity" + }, + "button": { + "label": "Button" + } + } + }, "strategy": { "original-states": { "areas": "Areas to display", @@ -8837,8 +9655,10 @@ }, "current_user": "You are currently logged in as {fullName}.", "is_owner": "You are an owner.", - "user_settings_header": "User settings", - "user_settings_detail": "The following settings are tied to your account and will persist across all sessions and devices.", + "user_preferences_header": "User preferences", + "user_preferences_detail": "The following settings are tied to your account and will persist across all sessions and devices.", + "localization_header": "Localization", + "localization_detail": "These settings are tied to your account and control how dates, times, and numbers are displayed.", "mobile_app_settings": "Mobile app settings", "browser_settings": "Browser settings", "client_settings_detail": "The following settings are local to this client only, and may reset to defaults on logout or when local data is cleared.", @@ -9042,8 +9862,10 @@ "confirm_delete_text": "Are you sure you want to delete the long-lived access token for {name}?", "delete_failed": "Failed to delete the access token.", "create": "Create token", + "created_title": "Token created: {name}", "create_failed": "Failed to create the access token.", "name": "Name", + "name_exists": "A token with this name already exists.", "prompt_name": "Give the token a name", "prompt_copy_token": "Copy your access token. It will not be shown again.", "empty_state": "You have no long-lived access tokens yet.", @@ -9235,390 +10057,6 @@ } } }, - "developer-tools": { - "tabs": { - "assist": { - "title": "Sentences parser", - "description": "Enter sentences and see how they will be parsed by Home Assistant. Each line will be processed as an individual sentence. Intents will not be executed on your instance.", - "parse_sentences": "Parse sentences", - "sentences": "Sentences", - "download_results": "Download results", - "no_match": "No intent matched", - "language": "[%key:ui::components::language-picker::language%]" - }, - "debug": { - "title": "Debug tools", - "debug_connection": { - "title": "Debug connection", - "description": "Observe requests to the server and responses from the server in browser console." - }, - "entity_diagnostic": { - "title": "Entity diagnostic", - "description": "Select an entity to copy diagnostic details.", - "copy_to_clipboard": "[%key:ui::panel::config::automation::editor::copy_to_clipboard%]" - } - }, - "events": { - "title": "Events", - "description": "Fire an event on the event bus.", - "documentation": "Events documentation", - "type": "Event type", - "data": "Event data (YAML, optional)", - "fire_event": "Fire event", - "event_fired": "Event {name} fired", - "active_listeners": "Active listeners", - "count_listeners": "({count} {count, plural,\n one {listener}\n other {listeners}\n})", - "listen_to_events": "Listen to events", - "filter_events": "Filter events", - "filter_helper": "Only capture events containing the filter string.", - "filter_ignored": "Ignored {count} events.", - "listening_to": "Listening to", - "subscribe_to": "Event to subscribe to", - "start_listening": "Start listening", - "stop_listening": "Stop listening", - "clear_events": "Clear events", - "alert_event_type": "Event type is a mandatory field", - "notification_event_fired": "Event {type} successfully fired!", - "subscribe_failed": "Failed to subscribe to event: {error}" - }, - "actions": { - "title": "Actions", - "description": "The actions dev tool allows you to perform any action available in Home Assistant.", - "call_service": "Perform action", - "response": "Response", - "column_parameter": "Parameter", - "column_description": "Description", - "column_example": "Example", - "fill_example_data": "Fill example data", - "yaml_mode": "Go to YAML mode", - "ui_mode": "Go to UI mode", - "yaml_parameters": "Parameters only available in YAML mode", - "all_parameters": "All available parameters", - "accepts_target": "This action accepts a target, for example: `entity_id: light.bed_light`", - "no_template_ui_support": "The UI does not support templates, you can still use the YAML editor.", - "copy_clipboard_template": "Copy to clipboard as template", - "open_media": "Open media", - "errors": { - "ui": { - "no_action": "No action selected, please select an action", - "invalid_action": "Selected action is invalid, please select a valid action", - "no_target": "This action requires a target, please select a target from the picker", - "missing_required_field": "This action requires field {key}, please enter a valid value for {key}" - }, - "yaml": { - "invalid_yaml": "Action YAML contains syntax errors, please fix the syntax", - "no_action": "No action defined, please define an 'action:' key", - "invalid_action": "Defined action is invalid, please provide an action in the format domain.action", - "no_target": "This action requires a target, please define a target 'entity_id', 'device_id', or 'area_id' under 'target:' or 'data:'", - "missing_required_field": "This action requires field {key}, which must be provided under 'data:'" - } - } - }, - "states": { - "title": "States", - "description1": "Set the current state representation of an entity within Home Assistant.", - "description2": "If the entity belongs to a device, there will be no actual communication with that device.", - "entity": "Entity", - "state": "State", - "attributes": "Attributes", - "state_attributes": "State attributes (YAML, optional)", - "set_state": "Set state", - "current_entities": "Current entity states", - "filter_entities": "Filter entities", - "filter_states": "Filter states", - "filter_attributes": "Filter attributes", - "no_entities": "No entities", - "more_info": "More info", - "alert_entity_field": "Entity is a mandatory field", - "last_updated": "[%key:ui::dialogs::more_info_control::last_updated%]", - "last_changed": "[%key:ui::dialogs::more_info_control::last_changed%]", - "copy_id": "Copy ID to clipboard" - }, - "templates": { - "title": "Template", - "description": "Templates are rendered using the Jinja2 template engine with some Home Assistant specific extensions.", - "editor": "Template editor", - "result": "Result", - "reset": "Reset to demo template", - "confirm_reset": "Do you want to reset your current template back to the demo template?", - "confirm_clear": "Do you want to clear your current template?", - "result_type": "Result type", - "jinja_documentation": "Jinja2 template documentation", - "template_extensions": "Home Assistant template extensions", - "unknown_error_template": "Unknown error rendering template", - "time": "This template updates at the start of each minute.", - "all_listeners": "This template listens for all state changed events.", - "no_listeners": "This template does not listen for any events and will not update automatically.", - "listeners": "This template listens for the following state changed events:", - "entity": "Entity", - "domain": "Domain" - }, - "statistics": { - "title": "Statistics", - "entity": "Entity", - "issue": "Issue", - "no_issue": "No issue", - "issues": { - "units_changed": "The unit of this entity changed from ''{metadata_unit}'' to ''{state_unit}''.", - "mean_type_changed": "The mean type of this entity changed from ''{metadata_mean_type}'' to ''{state_mean_type}''.", - "state_class_removed": "This entity no longer has a state class", - "entity_not_recorded": "This entity is excluded from being recorded.", - "entity_no_longer_recorded": "This entity is no longer being recorded.", - "no_state": "There is no state available for this entity." - }, - "delete_selected": "Delete selected statistics", - "multi_delete": { - "title": "Delete selected statistics", - "info_text": "Do you want to permanently delete the long term statistics {statistic_count, plural,\n one {of this entity}\n other {of {statistic_count} entities}\n} from your database?" - }, - "mean_type": { - "0": "None", - "1": "Arithmetic", - "2": "Circular" - }, - "fix_issue": { - "fix": "Fix issue", - "clearing_failed": "Clearing the statistics failed", - "clearing_timeout_title": "Clearing not done yet", - "clearing_timeout_text": "The clearing of the statistics took longer than expected, it might take longer for the issue to disappear.", - "fix_all": "Fix all", - "info": "Info", - "no_support": { - "title": "Fix issue", - "info_text_1": "Fixing this issue is not supported yet." - }, - "no_state": { - "title": "Entity has no state", - "info_text_1": "''{name}'' ({statistic_id}) has no state at the moment, if this is an orphaned entity, you may want to delete the long term statistics of it from your database.", - "info_text_2": "Do you want to permanently delete the long term statistics of {statistic_id} from your database?" - }, - "entity_not_recorded": { - "title": "Entity is not recorded", - "info_text_1": "State changes of ''{name}'' ({statistic_id}) are not recorded, therefore, we cannot track long term statistics for it.", - "info_text_2": "You probably excluded this entity, or have just included some entities.", - "info_text_3_link": "See the Recorder documentation for more information." - }, - "entity_no_longer_recorded": { - "title": "Entity is no longer recorded", - "info_text_1": "We have generated statistics for ''{name}'' ({statistic_id}) in the past, but state changes of this entity are no longer recorded, therefore, we cannot track long term statistics for it anymore.", - "info_text_2": "You probably excluded this entity, or have just included some entities.", - "info_text_3_link": "See the Recorder documentation for more information.", - "info_text_4": "If you no longer wish to keep the long term statistics recorded in the past, you may delete them now." - }, - "state_class_removed": { - "title": "The entity no longer has a state class", - "info_text_1": "We have generated statistics for ''{name}'' ({statistic_id}) in the past, but it no longer has a state class, therefore, we cannot track long term statistics for it anymore.", - "info_text_2": "Statistics cannot be generated until this entity has a supported state class.", - "info_text_3": "If the state class was previously provided by an integration, this might be a bug. Please report an issue.", - "info_text_4": "If you previously set the state class yourself, please correct it.", - "info_text_4_link": "The different state classes and when to use which can be found in the developer documentation.", - "info_text_5": "If the state class has permanently been removed, you may want to delete the long term statistics of it from your database.", - "info_text_6": "Do you want to permanently delete the long term statistics of {statistic_id} from your database?" - }, - "units_changed": { - "title": "The unit has changed", - "update": "Update the unit of the historic statistic values from ''{metadata_unit}'' to ''{state_unit}'', without converting.", - "clear": "Delete all old statistic data for this entity", - "how_to_fix": "How do you want to fix this issue?", - "info_text_1": "The unit of ''{name}'' ({statistic_id}) changed to ''{current_unit}'' which can't be converted to the previously stored unit, ''{previous_unit}''.", - "info_text_2": "If the historic statistic values have a wrong unit, you can update the units of the old values. The values will not be updated.", - "info_text_3": "Otherwise you can choose to delete all historic statistic values, and start over." - }, - "mean_type_changed": { - "title": "The mean type has changed", - "info_text_1": "The mean type of ''{name}'' ({statistic_id}) changed from ''{metadata_mean_type}'' to ''{state_mean_type}''.", - "info_text_2": "Statistics cannot be generated until the old statistics are deleted, or the mean type matches the old statistics data again.", - "info_text_3": "Do you want to permanently delete the long term statistics of {statistic_id} from your database?" - }, - "adjust_sum": { - "title": "Adjust a statistic", - "no_statistics_found": "No statistics found for this period.", - "info_text_1": "Sometimes the statistics end up being incorrect for a specific point in time. This can mess up your beautiful graphs! Select a time below to find the bad moment and adjust the data.", - "pick_a_time": "Pick a time", - "statistic": "Statistic:", - "start": "Start", - "end": "End", - "new_value": "New value", - "adjust": "Adjust", - "outliers": "Outliers", - "sum_adjusted": "Statistic sum adjusted", - "error_sum_adjusted": "Error adjusting sum: {message}" - } - }, - "adjust_sum": "Adjust sum", - "data_table": { - "select_all_issues": "Select all with issues", - "name": "Name", - "statistic_id": "Statistic ID", - "statistics_unit": "Statistics unit", - "source": "Source", - "issue": "Issue", - "no_statistics": "[%key:ui::components::statistics_charts::no_statistics_found%]" - } - }, - "yaml": { - "title": "YAML", - "section": { - "validation": { - "heading": "Check and restart", - "introduction": "A basic validation of the configuration is automatically done before restarting. The basic validation ensures the YAML configuration doesn't have errors which will prevent Home Assistant or any integration from starting. It's also possible to only do the basic validation check without restarting.", - "check_config": "Check configuration", - "valid": "Configuration will not prevent Home Assistant from starting!", - "invalid": "Configuration invalid!", - "warnings": "Configuration warnings", - "errors": "Configuration errors" - }, - "reloading": { - "all": "All YAML configuration", - "heading": "YAML configuration reloading", - "introduction": "Some parts of Home Assistant can reload without requiring a restart. Selecting one of the options below will unload their current YAML configuration and load the new one.", - "reload": "{domain}", - "core": "Location & customizations", - "group": "Groups, group entities, and notify services", - "automation": "Automations", - "script": "Scripts", - "scene": "Scenes", - "person": "People", - "zone": "Zones", - "input_boolean": "Input booleans", - "input_button": "Input buttons", - "input_text": "Input texts", - "input_number": "Input numbers", - "input_datetime": "Input datetimes", - "input_select": "Input selects", - "template": "Template entities", - "universal": "Universal media player entities", - "rest": "REST entities and notify services", - "command_line": "Command line entities", - "filter": "Filter entities", - "statistics": "Statistics entities", - "generic": "Generic IP camera entities", - "generic_thermostat": "Generic thermostat entities", - "homekit": "HomeKit", - "min_max": "Min/max entities", - "history_stats": "History stats entities", - "trend": "Trend entities", - "ping": "Ping binary sensor entities", - "filesize": "File size entities", - "telegram": "Telegram notify services", - "smtp": "SMTP notify services", - "mqtt": "Manually configured MQTT entities", - "rpi_gpio": "Raspberry Pi GPIO entities", - "timer": "Timers", - "themes": "Themes" - }, - "server_management": { - "heading": "Home Assistant", - "restart": "Restart", - "stop": "Stop", - "confirm_stop": "Are you sure you want to stop Home Assistant?", - "restart_error": "Failed to restart Home Assistant" - } - } - }, - "blueprints": { - "title": "Blueprints", - "paste_invalid_config": "Pasted blueprint is not editable in the visual editor", - "paste_toast_message": "Pasted blueprint from clipboard", - "dialog_pick": { - "header": "Pick a blueprint", - "automation": "Automation", - "script": "Script", - "create_empty_automation": "Create new automation blueprint", - "create_empty_automation_description": "Start with an empty automation blueprint from scratch", - "create_empty_script": "Create new script blueprint", - "create_empty_script_description": "Start with an empty script blueprint from scratch" - }, - "editor": { - "load_blueprints_error_title": "Error loading blueprints", - "load_blueprints_error_text": "There was a problem loading blueprints. Please try again.", - "load_error_not_editable": "Only blueprints in blueprints.yaml are editable.", - "load_error_unknown": "Error loading blueprint ({err_no}).", - "save_error_title": "Error saving blueprint", - "save_error_text": "There was a problem saving the blueprint. Please make sure there are no errors and try again.", - "move_up": "Move up", - "move_down": "Move down", - "edit_ui": "UI editor", - "edit_yaml": "YAML editor", - "metadata": "Metadata", - "error": "There is a problem with the selected blueprint.", - "none_selected": "Please pick a blueprint to edit it.", - "overwrite_existing_title": "You are about to overwrite an existing blueprint", - "overwrite_existing_text": "Reimporting this blueprint will cause your changes to be lost. Are you sure?", - "abandon_changes_title": "You have unsaved changes", - "abandon_changes_text": "Picking a new blueprint will cause your changes to be lost.", - "abandon_changes_confirm_text": "Abandon changes", - "reset_text": "You will lose all of the changes you've made so far.", - "paste_confirm": { - "title": "Pasted blueprint", - "text": "How do you want to paste your blueprint?" - }, - "actions": { - "delete": "Delete", - "duplicate": "Duplicate", - "pick": "Pick a blueprint", - "reset": "Reset", - "edit_ui": "[%key:ui::panel::lovelace::editor::edit_view::edit_ui%]", - "edit_yaml": "[%key:ui::panel::lovelace::editor::edit_view::edit_yaml%]" - }, - "inputs": { - "header": "Inputs", - "section_description": "Inputs are variables that make blueprints generic. The exact value will be determined when an instance of this blueprint is created", - "learn_more": "Learn more about inputs", - "add": "Add new input", - "testing_pass": "Input passes", - "testing_error": "Input did not pass", - "copy": "Copy", - "cut": "Cut", - "rename": "Rename", - "delete_confirm_title": "Delete input", - "delete_confirm_text": "Do you want to delete this input from your blueprint?", - "id": "ID", - "change_id": "Change input ID", - "editor": { - "single": "Input", - "section": "Input section" - }, - "type": { - "new": { - "input": "Input", - "section": "Section", - "type": "Type", - "id": "ID" - }, - "section": { - "name": "Name", - "description": "Description", - "icon": "Icon", - "collapsed": "Collapsed" - }, - "single": { - "name": "Name", - "description": "Description", - "default": "Default value", - "selector": "Selector" - } - } - }, - "name": { - "label": "Name" - }, - "path": { - "label": "Path" - }, - "description": { - "label": "Description" - }, - "author": { - "label": "Author" - }, - "min_version": { - "label": "Minimum version" - } - } - } - } - }, "page-onboarding": { "intro": "Are you ready to awaken your home, reclaim your privacy and join a worldwide community of tinkerers?", "back": "Back", @@ -9722,7 +10160,7 @@ "uploading": "[%key:ui::components::file-upload::uploading%]", "details": { "home_assistant_missing": "This backup does not include your Home Assistant configuration, you cannot use it to restore your instance.", - "addons_unsupported": "Your installation method doesn’t support add-ons. If you want to restore these, you have to install Home Assistant OS", + "apps_unsupported": "Your installation method doesn’t support apps. If you want to restore these, you have to install Home Assistant OS", "summary": { "created": "[%key:ui::panel::config::backup::details::summary::created%]", "content": "Content" @@ -9744,11 +10182,11 @@ "settings_and_history": "[%key:ui::panel::config::backup::data_picker::settings_and_history%]", "media": "[%key:ui::panel::config::backup::data_picker::media%]", "share_folder": "[%key:ui::panel::config::backup::data_picker::share_folder%]", - "local_addons": "[%key:ui::panel::config::backup::data_picker::local_addons%]", - "addons": "[%key:ui::panel::config::backup::data_picker::addons%]", + "local_apps": "[%key:ui::panel::config::backup::data_picker::local_apps%]", + "apps": "[%key:ui::panel::config::backup::data_picker::apps%]", "ssl": "[%key:ui::panel::config::backup::data_picker::ssl%]" }, - "restore_no_home_assistant": "[%key:supervisor::backup::restore_no_home_assistant%]", + "restore_no_home_assistant": "Backup does not contain Home Assistant data. To restore Home Assistant you need a backup of Home Assistant Core.", "in_progress": "Restore in progress", "in_progress_description": "The restore process is running in the background. Home Assistant will automatically start again once the restore is complete. Please be patient, this can take a while. Do not close or refresh this page.", "failed": "Restore failed", @@ -9759,27 +10197,27 @@ "upload_drop": "[%key:ui::components::file-upload::secondary%]", "show_log": "Show full log", "hide_log": "Hide full log", - "full_backup": "[%key:supervisor::backup::full_backup%]", - "partial_backup": "[%key:supervisor::backup::partial_backup%]", - "name": "[%key:supervisor::backup::name%]", - "select_type": "[%key:supervisor::backup::select_type%]", - "folders": "[%key:supervisor::backup::folders%]", - "addons": "[%key:supervisor::backup::addons%]", - "password_protection": "[%key:supervisor::backup::password_protection%]", - "password": "[%key:supervisor::backup::password%]", - "confirm_password": "[%key:supervisor::backup::confirm_password%]", - "confirm_restore_partial_backup_title": "[%key:supervisor::backup::confirm_restore_partial_backup_title%]", + "full_backup": "Full backup", + "partial_backup": "Partial backup", + "name": "Backup name", + "select_type": "Select what to restore", + "folders": "Folders", + "apps": "Apps", + "password_protection": "Password protection", + "password": "Backup encryption key", + "confirm_password": "Confirm encryption key", + "confirm_restore_partial_backup_title": "Restore partial backup", "confirm_restore_partial_backup_text": "The backup will be restored. Depending on the size of the backup, this can take up to 45 min. Home Assistant needs to shut down and the restore progress is running in the background. If it succeeds, Home Assistant will automatically start again and you see the login screen. If it fails it will bring you back to the onboarding.", - "confirm_restore_full_backup_title": "[%key:supervisor::backup::confirm_restore_full_backup_title%]", + "confirm_restore_full_backup_title": "Restore full backup", "confirm_restore_full_backup_text": "Depending on the size of the backup, this can take up to 45 minutes. Home Assistant will restart and you will see the login screen when it’s restored.", - "restore": "[%key:supervisor::backup::restore%]", + "restore": "Restore", "close": "[%key:ui::common::close%]", "cancel": "[%key:ui::common::cancel%]", "retry": "Retry", "back": "[%key:ui::common::back%]", - "restore_start_failed": "[%key:supervisor::backup::restore_start_failed%]", - "no_backup_found": "[%key:supervisor::backup::no_backup_found%]", - "unnamed_backup": "[%key:supervisor::backup::unnamed_backup%]", + "restore_start_failed": "Failed to start restore. Unknown error.", + "no_backup_found": "No backup found.", + "unnamed_backup": "Unnamed backup", "cancel_restore": { "title": "Cancel restore process?", "text": "Are you sure you want to cancel the restore process and return to the onboarding?", @@ -9894,7 +10332,8 @@ "key_c_tip": "Press {keyboard_shortcut} 'c' on any page to open the command dialog", "key_e_tip": "Press {keyboard_shortcut} 'e' on any page to open the entity search dialog", "key_m_tip": "Press {keyboard_shortcut} 'm' on any page to get the My Home Assistant link", - "key_a_tip": "Press {keyboard_shortcut} 'a' on any page to open the Assist dialog" + "key_a_tip": "Press {keyboard_shortcut} 'a' on any page to open the Assist dialog", + "key_shortcut_quick_search": "Press {keyboard_shortcut} ''{modifier}+k'' on any page to open quick search" } }, "landing-page": { @@ -9941,594 +10380,5 @@ } } } - }, - "supervisor": { - "addon": { - "failed_to_reset": "Failed to reset add-on configuration, {error}", - "failed_to_save": "Failed to save add-on configuration, {error}", - "state": { - "installed": "Add-on is installed", - "not_installed": "Add-on is not installed", - "not_available": "Add-on is not available on your system" - }, - "panel": { - "configuration": "Configuration", - "documentation": "Documentation", - "info": "Info", - "log": "Log" - }, - "configuration": { - "no_configuration": "This add-on has no configuration.", - "audio": { - "header": "Audio", - "default": "Default", - "input": "Input", - "output": "Output" - }, - "options": { - "header": "Options", - "edit_in_ui": "Edit in UI", - "edit_in_yaml": "Edit in YAML", - "invalid_yaml": "Invalid YAML", - "show_unused_optional": "Show unused optional configuration options" - }, - "network": { - "container": "Container", - "disabled": "Disabled", - "header": "Network", - "show_disabled": "Show disabled ports", - "introduction": "Change the ports on your host that are exposed by the add-on" - } - }, - "dashboard": { - "changelog": "Changelog", - "current_version": "Current version: {version}", - "cpu_usage": "Add-on CPU usage", - "ram_usage": "Add-on RAM usage", - "hostname": "Hostname", - "new_update_available": "{name} {version} is available", - "not_available_arch": "This add-on is not compatible with the processor of your device or the operating system you have installed on your device.", - "not_available_version": "You are running Home Assistant {core_version_installed}, to update to this version of the add-on you need at least version {core_version_needed} of Home Assistant", - "visit_addon_page": "Visit the {name} page for more details.", - "restart": "Restart", - "start": "Start", - "stop": "Stop", - "install": "Install", - "uninstall": "Uninstall", - "rebuild": "Rebuild", - "open_web_ui": "Open web UI", - "protection_mode": { - "title": "Protection mode is disabled!", - "content": "Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on.", - "enable": "[%key:ui::common::enable%]" - }, - "capability": { - "stage": { - "title": "Add-on stage", - "description": "Add-ons can have one of three stages:\n\n{icon_stable} **Stable**: These are add-ons ready to be used in production.\n\n{icon_experimental} **Experimental**: These may contain bugs, and may be unfinished.\n\n{icon_deprecated} **Deprecated**: These add-ons will no longer receive any updates." - }, - "rating": { - "title": "Add-on security rating", - "description": "Home Assistant provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 8. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 8 is the highest score (considered the most secure and lowest risk)." - }, - "host_network": { - "title": "Host network", - "description": "Add-ons usually run in their own isolated network layer, which prevents them from accessing the network of the host operating system. In some cases, this network isolation can limit add-ons in providing their services and therefore, the isolation can be lifted by the add-on author, giving the add-on full access to the network capabilities of the host machine. This gives the add-on more networking capabilities but lowers the security, hence, the security rating of the add-on will be lowered when this option is used by the add-on." - }, - "homeassistant_api": { - "title": "Home Assistant API access", - "description": "This add-on is allowed to access your running Home Assistant instance directly via the Home Assistant API. This mode handles authentication for the add-on as well, which enables an add-on to interact with Home Assistant without the need for additional authentication tokens." - }, - "full_access": { - "title": "Full hardware access", - "description": "This add-on is given full access to the hardware of your system, by request of the add-on author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on." - }, - "hassio_api": { - "title": "Supervisor API access", - "description": "The add-on was given access to the Supervisor API, by request of the add-on author. By default, the add-on can access general version information of your system. When the add-on requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Home Assistant system. This permission is indicated by this badge and will impact the security score of the add-on negatively." - }, - "docker_api": { - "title": "Full Docker access", - "description": "The add-on author has requested the add-on to have management access to the Docker instance running on your system. This mode gives the add-on full access and control to your entire Home Assistant system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on." - }, - "host_pid": { - "title": "Host processes namespace", - "description": "Usually, the processes run by the add-on are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Home Assistant system, which adds security risks and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on." - }, - "apparmor": { - "title": "AppArmor", - "description": "AppArmor ('Application Armor') is a Linux kernel security module that restricts add-on capabilities like network access, raw socket access, and permission to read, write, or execute specific files.\n\nAdd-on authors can provide their security profiles, optimized for the add-on, or request it to be disabled. If AppArmor is disabled, it will raise security risks and therefore, has a negative impact on the security score of the add-on." - }, - "auth_api": { - "title": "Home Assistant authentication", - "description": "An add-on can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log in to applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability." - }, - "ingress": { - "title": "Ingress", - "description": "This add-on is using Ingress to embed its interface securely into Home Assistant." - }, - "signed": { - "title": "Signed", - "description": "This add-on signed and verified with Codenotary Community Attestation Service (CAS)." - }, - "label": { - "core": "Core", - "rating": "rating", - "hardware": "hardware", - "host": "host", - "hassio": "hassio", - "docker": "docker", - "host_pid": "host pid", - "apparmor": "apparmor", - "auth": "auth", - "ingress": "ingress", - "signed": "Signed" - }, - "stages": { - "experimental": "Experimental", - "deprecated": "Deprecated" - }, - "role": { - "manager": "manager", - "default": "default", - "homeassistant": "homeassistant", - "backup": "backup", - "admin": "admin" - } - }, - "option": { - "boot": { - "title": "Start on boot", - "description": "Make the add-on start during a system boot" - }, - "watchdog": { - "title": "Watchdog", - "description": "This will restart the add-on if it crashes" - }, - "auto_update": { - "title": "Automatically update", - "description": "Automatically update the add-on when a new version is available" - }, - "ingress_panel": { - "title": "Add to sidebar", - "description": "Allows users to show or hide the add-on" - }, - "protected": { - "title": "Protection mode", - "description": "Blocks elevated system access from the add-on" - } - }, - "action_error": { - "uninstall": "Failed to uninstall add-on", - "install": "Failed to install add-on", - "stop": "Failed to stop add-on", - "rebuild": "Failed to rebuild add-on", - "restart": "Failed to restart add-on", - "start": "Failed to start add-on", - "go_to_config": "Edit config", - "start_invalid_config": "Go to configuration", - "validate_config": "Failed to validate add-on configuration", - "get_changelog": "Failed to get add-on changelog" - } - }, - "documentation": { - "get_documentation": "Failed to get add-on documentation, {error}" - }, - "logs": { - "get_logs": "Failed to get add-on logs, {error}" - }, - "system_managed": { - "badge": "System-managed", - "title": "This add-on is managed by Home Assistant", - "description": "Manually modifying this configuration may cause the add-on or integration to stop working properly.", - "managed_by": "Managed by", - "take_control": "Take control" - } - }, - "common": { - "cancel": "[%key:ui::common::cancel%]", - "yes": "[%key:ui::common::yes%]", - "no": "[%key:ui::common::no%]", - "add": "[%key:supervisor::dialog::repositories::add%]", - "failed_to_restart_name": "Failed to restart {name}", - "failed_to_update_name": "Failed to update {name}", - "learn_more": "Learn more", - "new_version_available": "New version available", - "newest_version": "Newest version", - "refresh": "[%key:ui::common::refresh%]", - "release_notes": "Release notes", - "reload": "Reload", - "reset_defaults": "Reset to defaults", - "reset_options": "Reset options", - "restart_name": "Restart {name}", - "restart": "Restart", - "running_version": "You are currently running version {version}", - "save": "[%key:ui::common::save%]", - "close": "[%key:ui::common::close%]", - "back": "[%key:ui::common::back%]", - "menu": "[%key:ui::common::menu%]", - "show": "[%key:ui::panel::config::updates::show%]", - "show_more": "Show more information about this", - "update_available": "{count, plural,\n one {Update}\n other {{count} updates}\n} pending", - "update": "Update", - "version": "Version", - "error": { - "unknown": "Unknown error", - "update_failed": "Update failed" - } - }, - "update_available": { - "update_name": "Update {name}", - "open_release_notes": "Open release notes", - "description": "You have {version} installed. Press update to update to version {newest_version}", - "updating": "Updating {name} to version {version}", - "no_update": "No update available for {name}", - "create_backup": { - "addon": "[%key:ui::dialogs::more_info_control::update::create_backup::addon%]", - "addon_description": "[%key:ui::dialogs::more_info_control::update::create_backup::addon_description%]", - "generic": "[%key:ui::dialogs::more_info_control::update::create_backup::generic%]" - } - }, - "confirm": { - "restart": { - "title": "[%key:supervisor::common::restart_name%]", - "text": "Are you sure you want to restart {name}?" - }, - "reset_options": { - "title": "Reset options", - "text": "Are you sure you want to reset all your options?" - } - }, - "dashboard": { - "addon_new_version": "New version available", - "addon_running": "Add-on is running", - "addon_stopped": "Add-on is stopped", - "addons": "Installed add-ons", - "no_addons": "You don't have any add-ons installed yet. Head over to the add-on store to get started!", - "search_addons": "Search add-ons" - }, - "store": { - "missing_addons": "Missing add-ons? Enable advanced mode in your user profile page", - "no_results_found": "No results found in {repository}.", - "registries": "Registries", - "repositories": "Repositories", - "check_updates": "Check for updates" - }, - "panel": { - "addons": "Add-ons", - "dashboard": "Dashboard", - "backups": "Backups", - "store": "Add-on store", - "system": "System" - }, - "my": { - "not_supported": "[%key:ui::panel::my::not_supported%]", - "faq_link": "[%key:ui::panel::my::faq_link%]", - "add_addon_repository_title": "Missing add-on repository", - "add_addon_repository_description": "The add-on ''{addon}'' is a part of the add-on repository ''{repository}'', this repository is missing on your system, do you want to add that now?", - "error": "[%key:ui::panel::my::error%]", - "error_addon_not_found": "Add-on not found", - "error_repository_not_found": "The required repository for this add-on was not found", - "error_addon_not_installed": "The requested add-on is not installed. Please install it first", - "error_addon_no_ingress": "The requested add-on does not support Ingress" - }, - "ingress": { - "error_addon_info": "Unable to fetch add-on info to start Ingress", - "error_addon_not_installed": "The add-on is not installed. Please install it first", - "error_addon_not_supported": "This add-on does not support Ingress", - "error_addon_not_running": "The add-on is not running. Do you want to start it now?", - "start_addon": "Start add-on", - "addon_starting": "The add-on is starting, this can take some time...", - "error_starting_addon": "Error starting the add-on", - "error_creating_session": "Unable to create an Ingress session", - "error_addon_not_ready": "The add-on seems to not be ready, it might still be starting. Do you want to try again?", - "retry": "Retry" - }, - "system": { - "log": { - "log_provider": "Log provider", - "get_logs": "Failed to get {provider} logs, {error}" - }, - "supervisor": { - "cpu_usage": "Supervisor CPU usage", - "ram_usage": "Supervisor RAM usage", - "failed_to_set_option": "Failed to set Supervisor option", - "failed_to_reload": "Failed to reload the Supervisor", - "failed_to_update": "Failed to update the Supervisor", - "unsupported_title": "You are running an unsupported installation", - "unsupported_description": "Below is a list of issues found with your installation, open the links to learn how you can resolve the issues.", - "unhealthy_title": "Your installation is unhealthy", - "unhealthy_description": "Running an unhealthy installation will cause issues. Below is a list of issues found with your installation, open the links to learn how you can resolve the issues.", - "update_supervisor": "Update the Supervisor", - "channel": "Channel", - "leave_beta_action": "Leave beta channel", - "leave_beta_description": "Get stable updates for Home Assistant OS, Core, Supervisor and Frontend", - "join_beta_action": "Join beta channel", - "join_beta_description": "Get beta updates for Home Assistant OS, Core, Supervisor and Frontend", - "share_diagnostics": "Share diagnostics", - "share_diagnostics_description": "Share crash reports and diagnostic information.", - "reload_supervisor": "Reload Supervisor", - "warning": "WARNING", - "search": "Search", - "share_diagonstics_title": "Help improve Home Assistant", - "share_diagonstics_description": "Would you want to automatically share crash reports and diagnostic information when the Supervisor encounters unexpected errors? {line_break} This will allow us to fix the problems, the information is only accessible to the Home Assistant Core team and will not be shared with others.{line_break} The data does not include any private/sensitive information and you can disable this in settings at any time you want.", - "unsupported_reason": { - "apparmor": "AppArmor is not enabled on the host", - "content_trust": "Content-trust validation is disabled", - "dbus": "DBUS", - "docker_configuration": "Docker configuration", - "docker_version": "Docker version", - "job_conditions": "Ignored job conditions", - "lxc": "LXC", - "network_manager": "Network manager", - "os": "Operating system", - "os_agent": "OS Agent", - "privileged": "Supervisor is not privileged", - "software": "Unsupported software detected", - "source_mods": "Source modifications", - "systemd": "Systemd", - "systemd_resolved": "Systemd-Resolved" - }, - "unhealthy_reason": { - "docker": "The Docker environment is not working properly", - "duplicate_os_installation": "Duplicate Home Assistant OS installation", - "oserror_bad_message": "Operating System error: Bad message", - "privileged": "Supervisor is not privileged", - "setup": "Setup of the Supervisor failed", - "supervisor": "Supervisor was not able to update", - "untrusted": "Detected untrusted content" - } - }, - "host": { - "failed_to_get_hardware_list": "Failed to get hardware list", - "failed_to_reboot": "Failed to reboot the host", - "failed_to_shutdown": "Failed to shut down the host", - "failed_to_set_hostname": "Setting hostname failed", - "failed_to_import_from_usb": "Failed to import from USB", - "failed_to_move": "Failed to move data disk", - "used_space": "Used space", - "hostname": "Hostname", - "change_hostname": "Change hostname", - "new_hostname": "Please enter a new hostname:", - "ip_address": "IP address", - "change": "Change", - "operating_system": "Operating system", - "docker_version": "Docker version", - "deployment": "Deployment", - "lifetime_used": "Lifetime used", - "reboot_host": "Reboot host", - "confirm_reboot": "Are you sure you want to reboot the host?", - "confirm_shutdown": "Are you sure you want to shut down the host?", - "shutdown_host": "Shut down host", - "hardware": "Hardware", - "import_from_usb": "Import from USB", - "move_datadisk": "Move data disk" - }, - "core": { - "cpu_usage": "Core CPU usage", - "ram_usage": "Core RAM usage" - } - }, - "backup": { - "search": "[%key:ui::panel::config::backup::picker::search%]", - "loading_backups": "Loading backups. This can take a few seconds.", - "no_backups": "You don't have any backups yet.", - "create_blocked_not_running": "Creating a backup is not possible right now because the system is in \"{state}\" state.", - "restore_blocked_not_running": "Restoring a backup is not possible right now because the system is in \"{state}\" state.", - "delete_selected": "Delete selected backups", - "delete_backup_title": "Delete backups?", - "delete_backup_text": "Do you want to delete {number} {number, plural,\n one {backup}\n other {backups}\n}?", - "delete_backup_confirm": "delete", - "selected": "{number} selected", - "failed_to_delete": "Failed to delete", - "could_not_create": "Could not create backup", - "could_not_restore": "Could not restore backup", - "upload_backup": "Upload backup", - "download_backup": "Download backup", - "create_backup": "Create backup", - "create": "Create", - "location": "Location", - "data_disk": "Data disk", - "created": "Created", - "name": "Backup name", - "type": "Backup type", - "select_type": "Select what to restore", - "full_backup": "Full backup", - "partial_backup": "Partial backup", - "addons": "Add-ons", - "folders": "Folders", - "size": "Size", - "password": "Backup encryption key", - "confirm_password": "Confirm encryption key", - "password_protection": "Password protection", - "enter_password": "Please enter a password.", - "passwords_not_matching": "The passwords do not match", - "backup_already_running": "A backup or restore is already running. Creating a new backup is currently not possible, try again later.", - "confirm_restore_partial_backup_title": "Restore partial backup", - "confirm_restore_partial_backup_text": "The backup will be restored. Depending on the size of the backup, this can take up to 45 min. Home Assistant needs to shut down and the restore progress is running in the background. If it succeeds, Home Assistant will automatically start again.", - "confirm_restore_full_backup_title": "Restore full backup", - "confirm_restore_full_backup_text": "Your entire system will be wiped and the backup will be restored. Depending on the size of the backup, this can take up to 45 min. Home Assistant needs to shut down and the restore progress is running in the background. If it succeeds, Home Assistant will automatically start again.", - "confirm_delete_title": "Delete backup", - "confirm_delete_text": "This backup will be permanently deleted and cannot be restored later.", - "restore": "Restore", - "close": "[%key:ui::common::close%]", - "cancel": "[%key:ui::common::cancel%]", - "delete": "[%key:ui::common::delete%]", - "download": "Download", - "more_actions": "More actions", - "remote_download_title": "Potentially slow download", - "remote_download_text": "You are accessing Home Assistant via remote access. Downloading backups over the Nabu Casa URL will take some time. If you are at home, cancel this dialog and enter your local URL, such as 'http://homeassistant.local:8123'", - "restore_start_failed": "Failed to start restore. Unknown error.", - "no_backup_found": "No backup found.", - "restore_no_home_assistant": "Backup does not contain Home Assistant data. To restore Home Assistant you need a backup of Home Assistant Core.", - "unnamed_backup": "Unnamed backup" - }, - "dialog": { - "network": { - "title": "Network settings", - "connected_to": "Connected to {ssid}", - "scan_ap": "Scan for access points", - "signal_strength": "[%key:ui::panel::config::network::supervisor::signal_strength%]", - "open": "Open", - "wep": "WEP", - "wpa": "WPA-PSK", - "wifi": "[%key:ui::panel::config::network::supervisor::wifi%]", - "wifi_password": "[%key:ui::panel::config::network::supervisor::wifi_password%]", - "warning": "If you are changing the Wi-Fi, IP or gateway addresses, you might lose the connection!", - "static": "Static", - "auto": "Automatic", - "disabled": "Disabled", - "ip_netmask": "IP address/netmask", - "netmask": "Netmask", - "gateway": "Gateway address", - "dns_servers": "DNS servers", - "unsaved": "You have unsaved changes, these will get lost if you change tabs, do you want to continue?", - "failed_to_change": "Failed to change network settings" - }, - "registries": { - "title_add": "Add new container registry", - "title_manage": "Manage container registries", - "registry": "Registry", - "username": "Username", - "password": "Password", - "no_registries": "No registries configured", - "add_registry": "Add registry", - "add_new_registry": "Add new registry", - "remove": "Remove", - "failed_to_add": "Failed to add registry", - "failed_to_remove": "Failed to remove registry" - }, - "repositories": { - "title": "Manage add-on repositories", - "add": "Add", - "remove": "Remove", - "used": "Repository is in use for installed add-ons and can't be removed.", - "no_repositories": "No repositories" - }, - "restart_addon": { - "title": "Restart {name}?", - "text": "To use the new saved configuration this add-on must be restarted.", - "restart": "Restart" - }, - "uninstall_addon": { - "title": "Uninstall {name}?", - "remove_data": "Also permanently delete this add-on's data", - "uninstall": "Uninstall" - }, - "hardware": { - "title": "Hardware", - "search": "Search hardware", - "subsystem": "Subsystem", - "id": "ID", - "attributes": "Attributes", - "device_path": "Device path" - }, - "backup_location": { - "title": "Change default backup location", - "options": { - "default_backup_mount": { - "name": "Default backup location", - "description": "The default location for backups." - } - } - }, - "datadisk_move": { - "title": "[%key:supervisor::system::host::move_datadisk%]", - "description": "You are currently using ''{current_path}'' as data disk. Moving data disks will reboot your device and it's estimated to take {time} minutes. Your Home Assistant installation will not be accessible during this period. Do not disconnect the power during the move!", - "select_device": "Select new data disk", - "no_devices": "No suitable attached devices found", - "moving_desc": "Rebooting and moving data disk. Please have patience", - "moving": "Moving data disk", - "loading_devices": "Loading devices", - "cancel": "[%key:ui::common::cancel%]", - "move": "Move" - } - }, - "ui": { - "components": { - "subpage-data-table": { - "filters": "[%key:ui::components::subpage-data-table::filters%]", - "show_results": "[%key:ui::components::subpage-data-table::show_results%]", - "clear_filter": "[%key:ui::components::subpage-data-table::clear_filter%]", - "close_filter": "[%key:ui::components::subpage-data-table::close_filter%]", - "exit_selection_mode": "[%key:ui::components::subpage-data-table::exit_selection_mode%]", - "enter_selection_mode": "[%key:ui::components::subpage-data-table::enter_selection_mode%]", - "sort_by": "[%key:ui::components::subpage-data-table::sort_by%]", - "group_by": "[%key:ui::components::subpage-data-table::group_by%]", - "dont_group_by": "[%key:ui::components::subpage-data-table::dont_group_by%]", - "collapse_all_groups": "[%key:ui::components::subpage-data-table::collapse_all_groups%]", - "expand_all_groups": "[%key:ui::components::subpage-data-table::expand_all_groups%]", - "select": "[%key:ui::components::subpage-data-table::select%]", - "selected": "[%key:ui::components::subpage-data-table::selected%]", - "select_all": "[%key:ui::components::subpage-data-table::select_all%]", - "select_none": "[%key:ui::components::subpage-data-table::select_none%]", - "settings": "[%key:ui::components::subpage-data-table::settings%]" - }, - "data-table": { - "settings": { - "header": "[%key:ui::components::data-table::settings::header%]", - "hide": "[%key:ui::components::data-table::settings::hide%]", - "show": "[%key:ui::components::data-table::settings::show%]", - "done": "[%key:ui::components::data-table::settings::done%]", - "restore": "[%key:ui::components::data-table::settings::restore%]" - } - } - }, - "panel": { - "config": { - "logs": { - "caption": "[%key:ui::panel::config::logs::caption%]", - "description": "[%key:ui::panel::config::logs::description%]", - "details": "[%key:ui::panel::config::logs::details%]", - "search": "[%key:ui::panel::config::logs::search%]", - "failed_get_logs": "[%key:ui::panel::config::logs::failed_get_logs%]", - "no_issues_search": "[%key:ui::panel::config::logs::no_issues_search%]", - "load_logs": "[%key:ui::panel::config::logs::load_logs%]", - "nr_of_lines": "[%key:ui::panel::config::logs::nr_of_lines%]", - "loading_log": "[%key:ui::panel::config::logs::loading_log%]", - "no_errors": "[%key:ui::panel::config::logs::no_errors%]", - "no_issues": "[%key:ui::panel::config::logs::no_issues%]", - "clear": "[%key:ui::panel::config::logs::clear%]", - "refresh": "[%key:ui::panel::config::logs::refresh%]", - "copy": "[%key:ui::panel::config::logs::copy%]", - "log_provider": "[%key:ui::panel::config::logs::log_provider%]", - "multiple_messages": "[%key:ui::panel::config::logs::multiple_messages%]", - "level": { - "critical": "[%key:ui::panel::config::logs::level::critical%]", - "error": "[%key:ui::panel::config::logs::level::error%]", - "warning": "[%key:ui::panel::config::logs::level::warning%]", - "info": "[%key:ui::panel::config::logs::level::info%]", - "debug": "[%key:ui::panel::config::logs::level::debug%]" - }, - "custom_integration": "[%key:ui::panel::config::logs::custom_integration%]", - "error_from_custom_integration": "[%key:ui::panel::config::logs::error_from_custom_integration%]", - "show_full_logs": "[%key:ui::panel::config::logs::show_full_logs%]", - "select_number_of_lines": "[%key:ui::panel::config::logs::select_number_of_lines%]", - "lines": "[%key:ui::panel::config::logs::lines%]", - "download_logs": "[%key:ui::panel::config::logs::download_logs%]", - "scroll_down_button": "[%key:ui::panel::config::logs::scroll_down_button%]", - "provider_not_found": "[%key:ui::panel::config::logs::provider_not_found%]", - "provider_not_available": "[%key:ui::panel::config::logs::provider_not_available%]", - "haos_boots_title": "[%key:ui::panel::config::logs::haos_boots_title%]", - "show_haos_boots": "[%key:ui::panel::config::logs::show_haos_boots%]", - "hide_haos_boots": "[%key:ui::panel::config::logs::hide_haos_boots%]", - "full_width": "[%key:ui::panel::config::logs::full_width%]", - "wrap_lines": "[%key:ui::panel::config::logs::wrap_lines%]", - "current": "[%key:ui::panel::config::logs::current%]", - "previous": "[%key:ui::panel::config::logs::previous%]", - "startups_ago": "[%key:ui::panel::config::logs::startups_ago%]", - "detail": { - "logger": "[%key:ui::panel::config::logs::detail::logger%]", - "source": "[%key:ui::panel::config::logs::detail::source%]", - "integration": "[%key:ui::panel::config::integrations::integration%]", - "documentation": "[%key:ui::panel::config::logs::detail::documentation%]", - "issues": "[%key:ui::panel::config::logs::detail::issues%]", - "first_occurred": "[%key:ui::panel::config::logs::detail::first_occurred%]", - "last_logged": "[%key:ui::panel::config::logs::detail::last_logged%]" - } - } - } - } - } } } diff --git a/src/types.ts b/src/types.ts index 80f523820e..963b70e779 100644 --- a/src/types.ts +++ b/src/types.ts @@ -37,7 +37,6 @@ declare global { var __VERSION__: string; var __STATIC_PATH__: string; var __BACKWARDS_COMPAT__: boolean; - var __SUPERVISOR__: boolean; var __HASS_URL__: string; /* eslint-enable @typescript-eslint/naming-convention */ @@ -206,6 +205,11 @@ export interface Context { user_id?: string | null; } +export interface ValuePart { + type: "value" | "literal" | "unit"; + value: string; +} + export interface ServiceCallResponse { context: Context; response?: T; @@ -288,11 +292,17 @@ export interface HomeAssistant { ): Promise; loadFragmentTranslation(fragment: string): Promise; formatEntityState(stateObj: HassEntity, state?: string): string; + formatEntityStateToParts(stateObj: HassEntity, state?: string): ValuePart[]; formatEntityAttributeValue( stateObj: HassEntity, attribute: string, value?: any ): string; + formatEntityAttributeValueToParts( + stateObj: HassEntity, + attribute: string, + value?: any + ): ValuePart[]; formatEntityAttributeName(stateObj: HassEntity, attribute: string): string; formatEntityName( stateObj: HassEntity, diff --git a/src/util/file_download.ts b/src/util/file_download.ts index 71d9a896df..1b93a133e5 100644 --- a/src/util/file_download.ts +++ b/src/util/file_download.ts @@ -1,6 +1,3 @@ -import type { HomeAssistant } from "../types"; -import { isIosApp } from "./is_ios"; - export const fileDownload = (href: string, filename = ""): void => { const element = document.createElement("a"); element.target = "_blank"; @@ -11,6 +8,3 @@ export const fileDownload = (href: string, filename = ""): void => { element.dispatchEvent(new MouseEvent("click")); document.body.removeChild(element); }; - -export const downloadFileSupported = (hass: HomeAssistant): boolean => - !isIosApp(hass) || !!hass.auth.external?.config.downloadFileSupported; diff --git a/src/util/launch-screen.ts b/src/util/launch-screen.ts index 1abc5b9e9f..3bcebb5a47 100644 --- a/src/util/launch-screen.ts +++ b/src/util/launch-screen.ts @@ -18,7 +18,7 @@ export const removeLaunchScreen = () => { launchScreenElement.classList.add("removing"); const durationFromCss = getComputedStyle(document.documentElement) - .getPropertyValue("--ha-animation-base-duration") + .getPropertyValue("--ha-animation-duration-slow") .trim(); setTimeout(() => { diff --git a/test/common/datetime/create_duration_data.test.ts b/test/common/datetime/create_duration_data.test.ts index ae6f8a2aa7..e430cf29ca 100644 --- a/test/common/datetime/create_duration_data.test.ts +++ b/test/common/datetime/create_duration_data.test.ts @@ -27,6 +27,12 @@ describe("createDurationData", () => { expect(createDurationData(3600)).toEqual({ seconds: 3600 }); }); + it("should parse decimal seconds correctly", () => { + expect(createDurationData(0.5)).toEqual({ seconds: 0.5 }); + expect(createDurationData(0.2)).toEqual({ seconds: 0.2 }); + expect(createDurationData(1.25)).toEqual({ seconds: 1.25 }); + }); + it("should parse object duration without days correctly", () => { expect(createDurationData({ hours: 1, minutes: 30 })).toEqual({ hours: 1, diff --git a/test/common/entity/compute_state_display.test.ts b/test/common/entity/compute_state_display.test.ts index a218c3534e..8d9f59f207 100644 --- a/test/common/entity/compute_state_display.test.ts +++ b/test/common/entity/compute_state_display.test.ts @@ -678,11 +678,11 @@ describe("computeStateDisplayFromEntityAttributes with numeric device classes", "number.test", { device_class: "monetary", - unit_of_measurement: "$", + unit_of_measurement: "USD", }, "12" ); - expect(result).toBe("12 $"); + expect(result).toBe("$12.00"); }); }); diff --git a/test/common/number/normalize-by-si-prefix.test.ts b/test/common/number/normalize-by-si-prefix.test.ts new file mode 100644 index 0000000000..9b72754f1b --- /dev/null +++ b/test/common/number/normalize-by-si-prefix.test.ts @@ -0,0 +1,54 @@ +import { assert, describe, it } from "vitest"; + +import { normalizeValueBySIPrefix } from "../../../src/common/number/normalize-by-si-prefix"; + +describe("normalizeValueBySIPrefix", () => { + it("Applies kilo prefix (k)", () => { + assert.equal(normalizeValueBySIPrefix(11, "kW"), 11000); + assert.equal(normalizeValueBySIPrefix(2.5, "kWh"), 2500); + }); + + it("Applies mega prefix (M)", () => { + assert.equal(normalizeValueBySIPrefix(3, "MW"), 3_000_000); + }); + + it("Applies giga prefix (G)", () => { + assert.equal(normalizeValueBySIPrefix(1, "GW"), 1_000_000_000); + }); + + it("Applies tera prefix (T)", () => { + assert.equal(normalizeValueBySIPrefix(2, "TW"), 2_000_000_000_000); + }); + + it("Applies milli prefix (m)", () => { + assert.equal(normalizeValueBySIPrefix(500, "mW"), 0.5); + }); + + it("Applies micro prefix (µ micro sign U+00B5)", () => { + assert.equal(normalizeValueBySIPrefix(1000, "\u00B5W"), 0.001); + }); + + it("Applies micro prefix (μ greek mu U+03BC)", () => { + assert.equal(normalizeValueBySIPrefix(1000, "\u03BCW"), 0.001); + }); + + it("Returns value unchanged for single-char units", () => { + assert.equal(normalizeValueBySIPrefix(100, "W"), 100); + assert.equal(normalizeValueBySIPrefix(5, "m"), 5); + assert.equal(normalizeValueBySIPrefix(22, "K"), 22); + }); + + it("Returns value unchanged for undefined unit", () => { + assert.equal(normalizeValueBySIPrefix(42, undefined), 42); + }); + + it("Returns value unchanged for unrecognized prefixes", () => { + assert.equal(normalizeValueBySIPrefix(20, "°C"), 20); + assert.equal(normalizeValueBySIPrefix(50, "dB"), 50); + assert.equal(normalizeValueBySIPrefix(1013, "hPa"), 1013); + }); + + it("Returns value unchanged for empty string", () => { + assert.equal(normalizeValueBySIPrefix(10, ""), 10); + }); +}); diff --git a/test/data/history.test.ts b/test/data/history.test.ts new file mode 100644 index 0000000000..76397a28de --- /dev/null +++ b/test/data/history.test.ts @@ -0,0 +1,110 @@ +import { describe, it, assert, vi } from "vitest"; +import { HistoryStream } from "../../src/data/history"; +import type { HomeAssistant } from "../../src/types"; + +const mockHass = {} as HomeAssistant; + +describe("HistoryStream.processMessage", () => { + it("should delete lc from boundary state when pruning expired history", () => { + const now = Date.now(); + const hoursToShow = 1; + const stream = new HistoryStream(mockHass, hoursToShow); + const purgeBeforePythonTime = (now - 60 * 60 * hoursToShow * 1000) / 1000; + + // Seed combinedHistory with states where lc differs from lu + // (simulating a sensor reporting the same value multiple times) + const oldLc = purgeBeforePythonTime - 3600; // lc is 1 hour before purge time + const oldLu = purgeBeforePythonTime - 10; // lu is 10 seconds before purge time + stream.combinedHistory = { + "sensor.power": [ + { s: "500", a: {}, lc: oldLc, lu: oldLu }, + { s: "500", a: {}, lu: purgeBeforePythonTime + 100 }, + ], + }; + + vi.useFakeTimers(); + vi.setSystemTime(now); + + const result = stream.processMessage({ + states: { + "sensor.power": [{ s: "510", a: {}, lu: purgeBeforePythonTime + 200 }], + }, + }); + + vi.useRealTimers(); + + const boundaryState = result["sensor.power"][0]; + // lc should be deleted so chart uses lu instead of stale lc + assert.equal(boundaryState.lc, undefined); + // lu should be set to approximately purgeBeforePythonTime + assert.closeTo(boundaryState.lu, purgeBeforePythonTime, 1); + // value should be preserved from the expired state + assert.equal(boundaryState.s, "500"); + }); + + it("should handle boundary state without lc correctly", () => { + const now = Date.now(); + const hoursToShow = 1; + const stream = new HistoryStream(mockHass, hoursToShow); + const purgeBeforePythonTime = (now - 60 * 60 * hoursToShow * 1000) / 1000; + + // State without lc (lc equals lu, so lc is omitted) + stream.combinedHistory = { + "sensor.power": [ + { s: "500", a: {}, lu: purgeBeforePythonTime - 10 }, + { s: "510", a: {}, lu: purgeBeforePythonTime + 100 }, + ], + }; + + vi.useFakeTimers(); + vi.setSystemTime(now); + + const result = stream.processMessage({ + states: { + "sensor.power": [{ s: "520", a: {}, lu: purgeBeforePythonTime + 200 }], + }, + }); + + vi.useRealTimers(); + + const boundaryState = result["sensor.power"][0]; + assert.equal(boundaryState.lc, undefined); + assert.closeTo(boundaryState.lu, purgeBeforePythonTime, 1); + assert.equal(boundaryState.s, "500"); + }); + + it("should not modify states when none are expired", () => { + const now = Date.now(); + const hoursToShow = 1; + const stream = new HistoryStream(mockHass, hoursToShow); + const purgeBeforePythonTime = (now - 60 * 60 * hoursToShow * 1000) / 1000; + + // All states are within the time window + stream.combinedHistory = { + "sensor.power": [ + { + s: "500", + a: {}, + lc: purgeBeforePythonTime + 50, + lu: purgeBeforePythonTime + 100, + }, + ], + }; + + vi.useFakeTimers(); + vi.setSystemTime(now); + + const result = stream.processMessage({ + states: { + "sensor.power": [{ s: "510", a: {}, lu: purgeBeforePythonTime + 200 }], + }, + }); + + vi.useRealTimers(); + + // First state should retain its original lc since it wasn't expired + const firstState = result["sensor.power"][0]; + assert.equal(firstState.lc, purgeBeforePythonTime + 50); + assert.equal(firstState.lu, purgeBeforePythonTime + 100); + }); +}); diff --git a/test/panels/lovelace/cards/energy/common/energy-chart-options.test.ts b/test/panels/lovelace/cards/energy/common/energy-chart-options.test.ts index 0f1a7dc53c..7ac7750519 100644 --- a/test/panels/lovelace/cards/energy/common/energy-chart-options.test.ts +++ b/test/panels/lovelace/cards/energy/common/energy-chart-options.test.ts @@ -1,7 +1,12 @@ import { assert, describe, it } from "vitest"; -import type { LineSeriesOption } from "echarts/charts"; +import type { BarSeriesOption, LineSeriesOption } from "echarts/charts"; -import { fillLineGaps } from "../../../../../../src/panels/lovelace/cards/energy/common/energy-chart-options"; +import { + fillDataGapsAndRoundCaps, + fillLineGaps, + getCompareTransform, + getSuggestedMax, +} from "../../../../../../src/panels/lovelace/cards/energy/common/energy-chart-options"; // Helper to get x value from either [x,y] or {value: [x,y]} format function getX(item: any): number { @@ -13,6 +18,76 @@ function getY(item: any): number { return item?.value?.[1] ?? item?.[1]; } +describe("getSuggestedMax", () => { + it("returns end date unchanged for 5minute period", () => { + const end = new Date("2024-03-15T14:37:22.000"); + const result = getSuggestedMax("5minute", end, false); + assert.equal(result.getTime(), end.getTime()); + }); + + it("returns end date unchanged when noRounding is true", () => { + const end = new Date("2024-03-15T14:37:22.000"); + const result = getSuggestedMax("hour", end, true); + assert.equal(result.getTime(), end.getTime()); + }); + + it("rounds down to start of hour for hour period", () => { + const end = new Date("2024-03-15T14:37:22.000"); + const result = getSuggestedMax("hour", end, false); + assert.equal(result.getMinutes(), 0); + assert.equal(result.getSeconds(), 0); + assert.equal(result.getMilliseconds(), 0); + assert.equal(result.getHours(), 14); + }); + + it("rounds down to start of day for day period", () => { + const end = new Date("2024-03-15T14:37:22.000"); + const result = getSuggestedMax("day", end, false); + assert.equal(result.getHours(), 0); + assert.equal(result.getMinutes(), 0); + assert.equal(result.getDate(), 15); + }); + + it("rounds down to start of day for week period", () => { + const end = new Date("2024-03-15T14:37:22.000"); + const result = getSuggestedMax("week", end, false); + assert.equal(result.getHours(), 0); + assert.equal(result.getMinutes(), 0); + assert.equal(result.getDate(), 15); + }); + + it("rounds down to start of month for month period", () => { + const end = new Date("2024-03-15T14:37:22.000"); + const result = getSuggestedMax("month", end, false); + assert.equal(result.getDate(), 1); + assert.equal(result.getHours(), 0); + assert.equal(result.getMonth(), 2); // March = 2 + }); + + it("corrects DST edge case when hour is 0 for day period", () => { + // Simulate a time that lands exactly on midnight (e.g. DST adjustment) + const end = new Date("2024-03-15T00:00:00.000"); + const result = getSuggestedMax("day", end, false); + // Should subtract an hour first, landing on previous day + assert.equal(result.getDate(), 14); + assert.equal(result.getHours(), 0); + }); + + it("does not apply DST correction when hour is nonzero", () => { + const end = new Date("2024-03-15T10:30:00.000"); + const result = getSuggestedMax("day", end, false); + assert.equal(result.getDate(), 15); + assert.equal(result.getHours(), 0); + }); + + it("does not mutate the input date", () => { + const end = new Date("2024-03-15T14:37:22.000"); + const originalTime = end.getTime(); + getSuggestedMax("month", end, false); + assert.equal(end.getTime(), originalTime); + }); +}); + describe("fillLineGaps", () => { it("fills gaps in datasets with missing timestamps", () => { const datasets: LineSeriesOption[] = [ @@ -197,3 +272,264 @@ describe("fillLineGaps", () => { assert.equal(secondItem.itemStyle.color, "red"); }); }); + +// Helper to get bar data item +function getBarItem(dataset: BarSeriesOption, index: number): any { + const dp = dataset.data![index]; + return dp && typeof dp === "object" && "value" in dp ? dp : { value: dp }; +} + +describe("fillDataGapsAndRoundCaps", () => { + it("fills missing buckets with zero values", () => { + // When a dataset has entries at some but not all bucket positions, + // the function splices in zero-value entries to align them + const datasets: BarSeriesOption[] = [ + { + type: "bar", + stack: "a", + data: [ + [1000, 10], + [3000, 30], + ], + }, + { + type: "bar", + stack: "a", + data: [ + [1000, 100], + [2000, 200], + [3000, 300], + ], + }, + ]; + + fillDataGapsAndRoundCaps(datasets); + + // First dataset should now have 3 entries with bucket 2000 filled in + assert.equal(datasets[0].data!.length, 3); + const filled = getBarItem(datasets[0], 1); + assert.equal(filled.value[0], 2000); + assert.equal(filled.value[1], 0); + assert.equal(filled.itemStyle.borderWidth, 0); + }); + + it("sets borderWidth 0 on zero-value existing entries", () => { + const datasets: BarSeriesOption[] = [ + { + type: "bar", + stack: "a", + data: [ + [1000, 0], + [2000, 5], + ], + }, + ]; + + fillDataGapsAndRoundCaps(datasets); + + const zeroItem = getBarItem(datasets[0], 0); + assert.equal(zeroItem.itemStyle.borderWidth, 0); + }); + + it("rounds caps on top positive bar in stack", () => { + const datasets: BarSeriesOption[] = [ + { + type: "bar", + stack: "a", + data: [[1000, 10]], + }, + { + type: "bar", + stack: "a", + data: [[1000, 20]], + }, + ]; + + fillDataGapsAndRoundCaps(datasets); + + // Last dataset (topmost positive) gets rounded top caps + // Iteration is reverse so first positive hit from the end gets the cap + const topItem = getBarItem(datasets[1], 0); + assert.deepEqual(topItem.itemStyle.borderRadius, [4, 4, 0, 0]); + + // Bottom dataset should NOT have rounded caps + const bottomItem = getBarItem(datasets[0], 0); + assert.equal(bottomItem.itemStyle?.borderRadius, undefined); + }); + + it("rounds caps on bottom negative bar in stack", () => { + const datasets: BarSeriesOption[] = [ + { + type: "bar", + stack: "a", + data: [[1000, -10]], + }, + { + type: "bar", + stack: "a", + data: [[1000, -20]], + }, + ]; + + fillDataGapsAndRoundCaps(datasets); + + // Last dataset (bottommost negative) gets rounded bottom caps + const bottomItem = getBarItem(datasets[1], 0); + assert.deepEqual(bottomItem.itemStyle.borderRadius, [0, 0, 4, 4]); + + // First dataset should NOT have rounded caps + const topItem = getBarItem(datasets[0], 0); + assert.equal(topItem.itemStyle?.borderRadius, undefined); + }); + + it("handles different stacks independently", () => { + const datasets: BarSeriesOption[] = [ + { + type: "bar", + stack: "a", + data: [[1000, 10]], + }, + { + type: "bar", + stack: "b", + data: [[1000, 20]], + }, + ]; + + fillDataGapsAndRoundCaps(datasets); + + // Both should get caps since they're in different stacks + const itemA = getBarItem(datasets[0], 0); + assert.deepEqual(itemA.itemStyle.borderRadius, [4, 4, 0, 0]); + + const itemB = getBarItem(datasets[1], 0); + assert.deepEqual(itemB.itemStyle.borderRadius, [4, 4, 0, 0]); + }); + + it("handles object-format data items", () => { + const datasets: BarSeriesOption[] = [ + { + type: "bar", + stack: "a", + data: [{ value: [1000, 10] }], + }, + { + type: "bar", + stack: "a", + data: [{ value: [2000, 20] }], + }, + ]; + + fillDataGapsAndRoundCaps(datasets); + + // Both datasets should now have 2 entries + assert.equal(datasets[0].data!.length, 2); + assert.equal(datasets[1].data!.length, 2); + }); + + it("handles empty datasets", () => { + const datasets: BarSeriesOption[] = [ + { + type: "bar", + stack: "a", + data: [], + }, + ]; + + fillDataGapsAndRoundCaps(datasets); + + assert.equal(datasets[0].data!.length, 0); + }); +}); + +describe("getCompareTransform", () => { + it("returns identity transform when no compareStart", () => { + const start = new Date("2024-03-01"); + const transform = getCompareTransform(start); + + const testDate = new Date("2024-03-15T12:00:00"); + assert.equal(transform(testDate).getTime(), testDate.getTime()); + }); + + it("returns identity transform when compareStart is undefined", () => { + const start = new Date("2024-03-01"); + const transform = getCompareTransform(start, undefined); + + const testDate = new Date("2024-03-15T12:00:00"); + assert.equal(transform(testDate).getTime(), testDate.getTime()); + }); + + it("shifts by years when start is at year boundary", () => { + const start = new Date("2024-01-01T00:00:00"); + const compareStart = new Date("2023-01-01T00:00:00"); + const transform = getCompareTransform(start, compareStart); + + const testDate = new Date("2023-06-15T12:00:00"); + const result = transform(testDate); + // Should shift forward by 1 year + assert.equal(result.getFullYear(), 2024); + assert.equal(result.getMonth(), 5); // June + assert.equal(result.getDate(), 15); + }); + + it("shifts by months when start is at month boundary", () => { + const start = new Date("2024-03-01T00:00:00"); + const compareStart = new Date("2024-01-01T00:00:00"); + const transform = getCompareTransform(start, compareStart); + + const testDate = new Date("2024-01-15T12:00:00"); + const result = transform(testDate); + // Should shift forward by 2 months + assert.equal(result.getMonth(), 2); // March + assert.equal(result.getDate(), 15); + }); + + it("shifts by days when start is at day boundary", () => { + const start = new Date("2024-03-15T00:00:00"); + const compareStart = new Date("2024-03-08T00:00:00"); + const transform = getCompareTransform(start, compareStart); + + const testDate = new Date("2024-03-08T14:30:00"); + const result = transform(testDate); + // Should shift forward by 7 days + assert.equal(result.getDate(), 15); + assert.equal(result.getHours(), 14); + assert.equal(result.getMinutes(), 30); + }); + + it("falls back to millisecond offset for non-aligned starts", () => { + const start = new Date("2024-03-15T10:30:00"); + const compareStart = new Date("2024-03-14T10:30:00"); + const transform = getCompareTransform(start, compareStart); + + const testDate = new Date("2024-03-14T12:00:00"); + const result = transform(testDate); + const expectedOffset = start.getTime() - compareStart.getTime(); + assert.equal(result.getTime(), testDate.getTime() + expectedOffset); + }); + + it("prefers year shift over month shift when both apply", () => { + // Jan 1 is both start-of-year and start-of-month + const start = new Date("2024-01-01T00:00:00"); + const compareStart = new Date("2022-01-01T00:00:00"); + const transform = getCompareTransform(start, compareStart); + + const testDate = new Date("2022-07-01T00:00:00"); + const result = transform(testDate); + // Should shift by 2 years (year check comes first) + assert.equal(result.getFullYear(), 2024); + assert.equal(result.getMonth(), 6); // July + }); + + it("uses month shift when start is at month but not year boundary", () => { + const start = new Date("2024-06-01T00:00:00"); + const compareStart = new Date("2024-03-01T00:00:00"); + const transform = getCompareTransform(start, compareStart); + + const testDate = new Date("2024-03-20T08:00:00"); + const result = transform(testDate); + // Should shift by 3 months + assert.equal(result.getMonth(), 5); // June + assert.equal(result.getDate(), 20); + }); +}); diff --git a/test/panels/lovelace/common/validate-condition.test.ts b/test/panels/lovelace/common/validate-condition.test.ts new file mode 100644 index 0000000000..4b159d0490 --- /dev/null +++ b/test/panels/lovelace/common/validate-condition.test.ts @@ -0,0 +1,111 @@ +import { describe, it, expect } from "vitest"; +import { + checkConditionsMet, + validateConditionalConfig, +} from "../../../../src/panels/lovelace/common/validate-condition"; +import type { HomeAssistant } from "../../../../src/types"; + +const createMockHass = (states: Record = {}) => + ({ + states, + user: { id: "user1" }, + }) as unknown as HomeAssistant; + +describe("validateConditionalConfig", () => { + describe("state condition validation", () => { + it("should return true for valid state condition", () => { + const conditions = [ + { condition: "state", entity: "sensor.test", state: "on" }, + ] as any; + expect(validateConditionalConfig(conditions)).toBe(true); + }); + + it("should return false for state condition without state or state_not", () => { + const conditions = [{ condition: "state", entity: "sensor.test" }] as any; + expect(validateConditionalConfig(conditions)).toBe(false); + }); + }); + + describe("numeric_state condition validation", () => { + it("should return true for valid numeric_state condition", () => { + const conditions = [ + { condition: "numeric_state", entity: "sensor.test", above: 0 }, + ] as any; + expect(validateConditionalConfig(conditions)).toBe(true); + }); + }); +}); + +describe("checkConditionsMet", () => { + describe("state condition evaluation", () => { + it("should return true when state matches", () => { + const hass = createMockHass({ + "sensor.test": { state: "on" }, + }); + const conditions = [ + { condition: "state", entity: "sensor.test", state: "on" }, + ] as any; + expect(checkConditionsMet(conditions, hass)).toBe(true); + }); + + it("should return false when state does not match", () => { + const hass = createMockHass({ + "sensor.test": { state: "off" }, + }); + const conditions = [ + { condition: "state", entity: "sensor.test", state: "on" }, + ] as any; + expect(checkConditionsMet(conditions, hass)).toBe(false); + }); + + it("should return false for condition without state or state_not", () => { + const hass = createMockHass({ + "sensor.test": { state: "on" }, + }); + const conditions = [{ condition: "state", entity: "sensor.test" }] as any; + expect(checkConditionsMet(conditions, hass)).toBe(false); + }); + + it("should not crash with invalid condition type", () => { + const hass = createMockHass({ + "sensor.test": { state: "5" }, + }); + const conditions = [ + { condition: "numeric", entity: "sensor.test", above: 0 }, + ] as any; + // Should not throw - this was the bug + expect(() => checkConditionsMet(conditions, hass)).not.toThrow(); + expect(checkConditionsMet(conditions, hass)).toBe(false); + }); + }); + + describe("numeric_state condition evaluation", () => { + it("should return true when value is above threshold", () => { + const hass = createMockHass({ + "sensor.test": { state: "5" }, + }); + const conditions = [ + { condition: "numeric_state", entity: "sensor.test", above: 0 }, + ] as any; + expect(checkConditionsMet(conditions, hass)).toBe(true); + }); + }); + + describe("legacy conditions", () => { + it("should handle legacy state condition", () => { + const hass = createMockHass({ + "sensor.test": { state: "on" }, + }); + const conditions = [{ entity: "sensor.test", state: "on" }] as any; + expect(checkConditionsMet(conditions, hass)).toBe(true); + }); + + it("should return false for legacy condition without state", () => { + const hass = createMockHass({ + "sensor.test": { state: "on" }, + }); + const conditions = [{ entity: "sensor.test" }] as any; + expect(checkConditionsMet(conditions, hass)).toBe(false); + }); + }); +}); diff --git a/test/vitest.config.ts b/test/vitest.config.ts index 6c2fff36f7..d9c1cd4b80 100644 --- a/test/vitest.config.ts +++ b/test/vitest.config.ts @@ -15,7 +15,6 @@ export default defineConfig({ "src/data/**/*", "src/common/**/*", "src/external_app/**/*", - "src/hassio/**/*", "src/panels/**/*", "src/util/**/*", ], diff --git a/yarn.lock b/yarn.lock index 17dcd08469..d29c99d134 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,10 +5,10 @@ __metadata: version: 8 cacheKey: 10 -"@acemir/cssom@npm:^0.9.28": - version: 0.9.29 - resolution: "@acemir/cssom@npm:0.9.29" - checksum: 10/5f1b1035f18a16dd2a64018121c6caf93b147f1b5860b0a1662ce7f24cdf9574443df8fd880f488149149ea1bb4dfaf504d067e6aeb353fc32eabf9c88e25499 +"@acemir/cssom@npm:^0.9.31": + version: 0.9.31 + resolution: "@acemir/cssom@npm:0.9.31" + checksum: 10/5948336f7f122062d714f4bb519937c42c91c84be348e31b35179f6109efc6753a695701c29f2271d8990f6f728168e933038418d97646cc5a1096099c3455b5 languageName: node linkType: hard @@ -25,7 +25,7 @@ __metadata: languageName: node linkType: hard -"@asamuzakjp/css-color@npm:^4.1.0": +"@asamuzakjp/css-color@npm:^4.1.1": version: 4.1.1 resolution: "@asamuzakjp/css-color@npm:4.1.1" dependencies: @@ -69,57 +69,57 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.28.6": - version: 7.28.6 - resolution: "@babel/code-frame@npm:7.28.6" +"@babel/code-frame@npm:^7.28.6, @babel/code-frame@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/code-frame@npm:7.29.0" dependencies: "@babel/helper-validator-identifier": "npm:^7.28.5" js-tokens: "npm:^4.0.0" picocolors: "npm:^1.1.1" - checksum: 10/93e7ed9e039e3cb661bdb97c26feebafacc6ec13d745881dae5c7e2708f579475daebe7a3b5d23b183bb940b30744f52f4a5bcb65b4df03b79d82fcb38495784 + checksum: 10/199e15ff89007dd30675655eec52481cb245c9fdf4f81e4dc1f866603b0217b57aff25f5ffa0a95bbc8e31eb861695330cd7869ad52cc211aa63016320ef72c5 languageName: node linkType: hard -"@babel/compat-data@npm:^7.27.7, @babel/compat-data@npm:^7.28.6": - version: 7.28.6 - resolution: "@babel/compat-data@npm:7.28.6" - checksum: 10/dc17dfb55711a15f006e34c4610c49b7335fc11b23e192f9e5f625e8ea0f48805e61a57b6b4f5550879332782c93af0b5d6952825fffbb8d4e604b14d698249f +"@babel/compat-data@npm:^7.28.6, @babel/compat-data@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/compat-data@npm:7.29.0" + checksum: 10/7f21beedb930ed8fbf7eabafc60e6e6521c1d905646bf1317a61b2163339157fe797efeb85962bf55136e166b01fd1a6b526a15974b92a8b877d564dcb6c9580 languageName: node linkType: hard -"@babel/core@npm:7.28.6, @babel/core@npm:^7.24.4": - version: 7.28.6 - resolution: "@babel/core@npm:7.28.6" +"@babel/core@npm:7.29.0, @babel/core@npm:^7.24.4": + version: 7.29.0 + resolution: "@babel/core@npm:7.29.0" dependencies: - "@babel/code-frame": "npm:^7.28.6" - "@babel/generator": "npm:^7.28.6" + "@babel/code-frame": "npm:^7.29.0" + "@babel/generator": "npm:^7.29.0" "@babel/helper-compilation-targets": "npm:^7.28.6" "@babel/helper-module-transforms": "npm:^7.28.6" "@babel/helpers": "npm:^7.28.6" - "@babel/parser": "npm:^7.28.6" + "@babel/parser": "npm:^7.29.0" "@babel/template": "npm:^7.28.6" - "@babel/traverse": "npm:^7.28.6" - "@babel/types": "npm:^7.28.6" + "@babel/traverse": "npm:^7.29.0" + "@babel/types": "npm:^7.29.0" "@jridgewell/remapping": "npm:^2.3.5" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 10/1a150a69c547daf13c457be1fdaf1a0935d02b94605e777e049537ec2f279b4bb442ffbe1c2d8ff62c688878b1d5530a5784daf72ece950d1917fb78717f51d2 + checksum: 10/25f4e91688cdfbaf1365831f4f245b436cdaabe63d59389b75752013b8d61819ee4257101b52fc328b0546159fd7d0e74457ed7cf12c365fea54be4fb0a40229 languageName: node linkType: hard -"@babel/generator@npm:^7.28.6": - version: 7.28.6 - resolution: "@babel/generator@npm:7.28.6" +"@babel/generator@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/generator@npm:7.29.0" dependencies: - "@babel/parser": "npm:^7.28.6" - "@babel/types": "npm:^7.28.6" + "@babel/parser": "npm:^7.29.0" + "@babel/types": "npm:^7.29.0" "@jridgewell/gen-mapping": "npm:^0.3.12" "@jridgewell/trace-mapping": "npm:^0.3.28" jsesc: "npm:^3.0.2" - checksum: 10/ef2af927e8e0985d02ec4321a242da761a934e927539147c59fdd544034dc7f0e9846f6bf86209aca7a28aee2243ed0fad668adccd48f96d7d6866215173f9af + checksum: 10/e144a5d3db43207e0909702c60a01928be8751c3df12cb99e94249a618358acd773c99d33c2209a9049142034e13591ba0a7ce938da49d9f7709dc3814020d1e languageName: node linkType: hard @@ -132,7 +132,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.27.1, @babel/helper-compilation-targets@npm:^7.27.2, @babel/helper-compilation-targets@npm:^7.28.6": +"@babel/helper-compilation-targets@npm:^7.27.1, @babel/helper-compilation-targets@npm:^7.28.6": version: 7.28.6 resolution: "@babel/helper-compilation-targets@npm:7.28.6" dependencies: @@ -175,18 +175,18 @@ __metadata: languageName: node linkType: hard -"@babel/helper-define-polyfill-provider@npm:0.6.5, @babel/helper-define-polyfill-provider@npm:^0.6.5": - version: 0.6.5 - resolution: "@babel/helper-define-polyfill-provider@npm:0.6.5" +"@babel/helper-define-polyfill-provider@npm:0.6.6, @babel/helper-define-polyfill-provider@npm:^0.6.5, @babel/helper-define-polyfill-provider@npm:^0.6.6": + version: 0.6.6 + resolution: "@babel/helper-define-polyfill-provider@npm:0.6.6" dependencies: - "@babel/helper-compilation-targets": "npm:^7.27.2" - "@babel/helper-plugin-utils": "npm:^7.27.1" - debug: "npm:^4.4.1" + "@babel/helper-compilation-targets": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" + debug: "npm:^4.4.3" lodash.debounce: "npm:^4.0.8" - resolve: "npm:^1.22.10" + resolve: "npm:^1.22.11" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10/0bdd2d9654d2f650c33976caa1a2afac2c23cf07e83856acdb482423c7bf4542c499ca0bdc723f2961bb36883501f09e9f4fe061ba81c07996daacfba82a6f62 + checksum: 10/1c725c47bafb10ae4527aff6741b44ca49b18bf7005ae4583b15f992783e7c1d7687eab1a5583a373b5494160d46e91e29145280bd850e97d36b8b01bc5fef99 languageName: node linkType: hard @@ -207,7 +207,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.27.1, @babel/helper-module-imports@npm:^7.28.6": +"@babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.28.6": version: 7.28.6 resolution: "@babel/helper-module-imports@npm:7.28.6" dependencies: @@ -217,7 +217,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.27.1, @babel/helper-module-transforms@npm:^7.28.3, @babel/helper-module-transforms@npm:^7.28.6": +"@babel/helper-module-transforms@npm:^7.27.1, @babel/helper-module-transforms@npm:^7.28.6": version: 7.28.6 resolution: "@babel/helper-module-transforms@npm:7.28.6" dependencies: @@ -304,13 +304,13 @@ __metadata: linkType: hard "@babel/helper-wrap-function@npm:^7.27.1": - version: 7.28.3 - resolution: "@babel/helper-wrap-function@npm:7.28.3" + version: 7.28.6 + resolution: "@babel/helper-wrap-function@npm:7.28.6" dependencies: - "@babel/template": "npm:^7.27.2" - "@babel/traverse": "npm:^7.28.3" - "@babel/types": "npm:^7.28.2" - checksum: 10/a5ed5fe7b8d9949d3b4f45ccec0b365018b8e444f6a6d794b4c8291e251e680f5b7c79c49c2170de9d14967c78721f59620ce70c5dac2d53c30628ef971d9dce + "@babel/template": "npm:^7.28.6" + "@babel/traverse": "npm:^7.28.6" + "@babel/types": "npm:^7.28.6" + checksum: 10/d8a895a75399904746f4127db33593a20021fc55d1a5b5dfeb060b87cc13a8dceea91e70a4951bcd376ba9bd8232b0c04bff9a86c1dab83d691e01852c3b5bcd languageName: node linkType: hard @@ -324,14 +324,14 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.23.5, @babel/parser@npm:^7.28.5, @babel/parser@npm:^7.28.6": - version: 7.28.6 - resolution: "@babel/parser@npm:7.28.6" +"@babel/parser@npm:^7.23.5, @babel/parser@npm:^7.28.5, @babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/parser@npm:7.29.0" dependencies: - "@babel/types": "npm:^7.28.6" + "@babel/types": "npm:^7.29.0" bin: parser: ./bin/babel-parser.js - checksum: 10/483a6fb5f9876ec9cbbb98816f2c94f39ae4d1158d35f87e1c4bf19a1f56027c96a1a3962ff0c8c46e8322a6d9e1c80d26b7f9668410df13d5b5769d9447b010 + checksum: 10/b1576dca41074997a33ee740d87b330ae2e647f4b7da9e8d2abd3772b18385d303b0cee962b9b88425e0f30d58358dbb8d63792c1a2d005c823d335f6a029747 languageName: node linkType: hard @@ -448,16 +448,16 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-async-generator-functions@npm:^7.28.6": - version: 7.28.6 - resolution: "@babel/plugin-transform-async-generator-functions@npm:7.28.6" +"@babel/plugin-transform-async-generator-functions@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-async-generator-functions@npm:7.29.0" dependencies: "@babel/helper-plugin-utils": "npm:^7.28.6" "@babel/helper-remap-async-to-generator": "npm:^7.27.1" - "@babel/traverse": "npm:^7.28.6" + "@babel/traverse": "npm:^7.29.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/b3c9e62a90808e8ad0e1608a7fd7169a5bfba3b54f0d8590495e7b0d95b25e882f45062f490e4ab6823bb9758da3619d645c9a536ae77e62cb9abe38400a8c08 + checksum: 10/e2c064a5eb212cbdf14f7c0113e069b845ca0f0ba431c1cc04607d3fc4f3bf1ed70f5c375fe7c61338a45db88bc1a79d270c8d633ce12256e1fce3666c1e6b93 languageName: node linkType: hard @@ -583,15 +583,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:^7.28.6": - version: 7.28.6 - resolution: "@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:7.28.6" +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:7.29.0" dependencies: "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/3f2e2b85199adfdc3297983412c2ecdacc0004bc5ac3263d29909219b8c5afa2ca49e3b6efc11ce67034d5780eef27882a94873444cf27d841d7fa7f01d7dcff + checksum: 10/7fa7b773259a578c9e01c80946f75ecc074520064aa7a87a65db06c7df70766e2fa6be78cda55fa9418a14e30b2b9d595484a46db48074d495d9f877a4276065 languageName: node linkType: hard @@ -733,17 +733,17 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-systemjs@npm:^7.28.5": - version: 7.28.5 - resolution: "@babel/plugin-transform-modules-systemjs@npm:7.28.5" +"@babel/plugin-transform-modules-systemjs@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.29.0" dependencies: - "@babel/helper-module-transforms": "npm:^7.28.3" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-module-transforms": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" "@babel/helper-validator-identifier": "npm:^7.28.5" - "@babel/traverse": "npm:^7.28.5" + "@babel/traverse": "npm:^7.29.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/1b91b4848845eaf6e21663d97a2a6c896553b127deaf3c2e9a2a4f041249277d13ebf71fd42d0ecbc4385e9f76093eff592fe0da0dcf1401b3f38c1615d8c539 + checksum: 10/b3e64728eef02d829510778226da4c06be740fe52e0d45d4aa68b24083096d8ad7df67f2e9e67198b2e85f3237d42bd66f5771f85846f7a746105d05ca2e0cae languageName: node linkType: hard @@ -759,15 +759,15 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.27.1": - version: 7.27.1 - resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.27.1" +"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.29.0" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/a711c92d9753df26cefc1792481e5cbff4fe4f32b383d76b25e36fa865d8023b1b9aa6338cf18f5c0e864c71a7fbe8115e840872ccd61a914d9953849c68de7d + checksum: 10/ed8c27699ca82a6c01cbfd39f3de16b90cfea4f8146a358057f76df290d308a66a8bd2e6734e6a87f68c18576e15d2d70548a84cd474d26fdf256c3f5ae44d8c languageName: node linkType: hard @@ -901,14 +901,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-regenerator@npm:^7.28.6": - version: 7.28.6 - resolution: "@babel/plugin-transform-regenerator@npm:7.28.6" +"@babel/plugin-transform-regenerator@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-regenerator@npm:7.29.0" dependencies: "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/09028ed8ed7f5e3879cbfdcf92a8a730c13b15ce042ef86b29b31cca5a250da641f777dfaf81ab1706fb8cf9252c177f02e882fc7465d3a10b9f385c0bb2ea16 + checksum: 10/c8fa9da74371568c5d34fd7d53de018752550cb10334040ca59e41f34b27f127974bdc5b4d1a1a8e8f3ebcf3cb7f650aa3f2df3b7bf1b7edf67c04493b9e3cb8 languageName: node linkType: hard @@ -935,19 +935,19 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-runtime@npm:7.28.5": - version: 7.28.5 - resolution: "@babel/plugin-transform-runtime@npm:7.28.5" +"@babel/plugin-transform-runtime@npm:7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-runtime@npm:7.29.0" dependencies: - "@babel/helper-module-imports": "npm:^7.27.1" - "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-module-imports": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" babel-plugin-polyfill-corejs2: "npm:^0.4.14" babel-plugin-polyfill-corejs3: "npm:^0.13.0" babel-plugin-polyfill-regenerator: "npm:^0.6.5" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/0d16c90d40dd34f1a981e742ad656ceef619b92d3662ec9ac8d7c8ba79f22bb425c3f9e097333659a4938f03868a53077b1a3aadb7f37504157a0c7af64ec2be + checksum: 10/314cfede923a7fb3aeecf4b282a3090e4a9ae1d84005e9a0365284c5142165a4dccd308455af9013d486a4ad8ada25ccad2fea28c2ec19b086d1ffa0088a69d7 languageName: node linkType: hard @@ -1054,11 +1054,11 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:7.28.6, @babel/preset-env@npm:^7.11.0": - version: 7.28.6 - resolution: "@babel/preset-env@npm:7.28.6" +"@babel/preset-env@npm:7.29.0, @babel/preset-env@npm:^7.11.0": + version: 7.29.0 + resolution: "@babel/preset-env@npm:7.29.0" dependencies: - "@babel/compat-data": "npm:^7.28.6" + "@babel/compat-data": "npm:^7.29.0" "@babel/helper-compilation-targets": "npm:^7.28.6" "@babel/helper-plugin-utils": "npm:^7.28.6" "@babel/helper-validator-option": "npm:^7.27.1" @@ -1072,7 +1072,7 @@ __metadata: "@babel/plugin-syntax-import-attributes": "npm:^7.28.6" "@babel/plugin-syntax-unicode-sets-regex": "npm:^7.18.6" "@babel/plugin-transform-arrow-functions": "npm:^7.27.1" - "@babel/plugin-transform-async-generator-functions": "npm:^7.28.6" + "@babel/plugin-transform-async-generator-functions": "npm:^7.29.0" "@babel/plugin-transform-async-to-generator": "npm:^7.28.6" "@babel/plugin-transform-block-scoped-functions": "npm:^7.27.1" "@babel/plugin-transform-block-scoping": "npm:^7.28.6" @@ -1083,7 +1083,7 @@ __metadata: "@babel/plugin-transform-destructuring": "npm:^7.28.5" "@babel/plugin-transform-dotall-regex": "npm:^7.28.6" "@babel/plugin-transform-duplicate-keys": "npm:^7.27.1" - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "npm:^7.28.6" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "npm:^7.29.0" "@babel/plugin-transform-dynamic-import": "npm:^7.27.1" "@babel/plugin-transform-explicit-resource-management": "npm:^7.28.6" "@babel/plugin-transform-exponentiation-operator": "npm:^7.28.6" @@ -1096,9 +1096,9 @@ __metadata: "@babel/plugin-transform-member-expression-literals": "npm:^7.27.1" "@babel/plugin-transform-modules-amd": "npm:^7.27.1" "@babel/plugin-transform-modules-commonjs": "npm:^7.28.6" - "@babel/plugin-transform-modules-systemjs": "npm:^7.28.5" + "@babel/plugin-transform-modules-systemjs": "npm:^7.29.0" "@babel/plugin-transform-modules-umd": "npm:^7.27.1" - "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.27.1" + "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.29.0" "@babel/plugin-transform-new-target": "npm:^7.27.1" "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.28.6" "@babel/plugin-transform-numeric-separator": "npm:^7.28.6" @@ -1110,7 +1110,7 @@ __metadata: "@babel/plugin-transform-private-methods": "npm:^7.28.6" "@babel/plugin-transform-private-property-in-object": "npm:^7.28.6" "@babel/plugin-transform-property-literals": "npm:^7.27.1" - "@babel/plugin-transform-regenerator": "npm:^7.28.6" + "@babel/plugin-transform-regenerator": "npm:^7.29.0" "@babel/plugin-transform-regexp-modifiers": "npm:^7.28.6" "@babel/plugin-transform-reserved-words": "npm:^7.27.1" "@babel/plugin-transform-shorthand-properties": "npm:^7.27.1" @@ -1123,14 +1123,14 @@ __metadata: "@babel/plugin-transform-unicode-regex": "npm:^7.27.1" "@babel/plugin-transform-unicode-sets-regex": "npm:^7.28.6" "@babel/preset-modules": "npm:0.1.6-no-external-plugins" - babel-plugin-polyfill-corejs2: "npm:^0.4.14" - babel-plugin-polyfill-corejs3: "npm:^0.13.0" - babel-plugin-polyfill-regenerator: "npm:^0.6.5" - core-js-compat: "npm:^3.43.0" + babel-plugin-polyfill-corejs2: "npm:^0.4.15" + babel-plugin-polyfill-corejs3: "npm:^0.14.0" + babel-plugin-polyfill-regenerator: "npm:^0.6.6" + core-js-compat: "npm:^3.48.0" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/ee1b96dec8518436013c4a758003734842e9fed2a3af2013bee7a462289bae4e7bbce99733701164c28a93554be5a9a4c50818fa32335274d19e8b7d3dd53316 + checksum: 10/211b33ec8644636275f61aa273071d8cbc2a6bb28d82ad246e3831a6aa7d96c610a55b5140bcd21be7f71fb04c3aa4a10eb08665fb5505e153cfdd8dbc8c1c1c languageName: node linkType: hard @@ -1154,7 +1154,7 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.27.2, @babel/template@npm:^7.28.6": +"@babel/template@npm:^7.28.6": version: 7.28.6 resolution: "@babel/template@npm:7.28.6" dependencies: @@ -1165,28 +1165,28 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.5, @babel/traverse@npm:^7.28.6": - version: 7.28.6 - resolution: "@babel/traverse@npm:7.28.6" +"@babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.5, @babel/traverse@npm:^7.28.6, @babel/traverse@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/traverse@npm:7.29.0" dependencies: - "@babel/code-frame": "npm:^7.28.6" - "@babel/generator": "npm:^7.28.6" + "@babel/code-frame": "npm:^7.29.0" + "@babel/generator": "npm:^7.29.0" "@babel/helper-globals": "npm:^7.28.0" - "@babel/parser": "npm:^7.28.6" + "@babel/parser": "npm:^7.29.0" "@babel/template": "npm:^7.28.6" - "@babel/types": "npm:^7.28.6" + "@babel/types": "npm:^7.29.0" debug: "npm:^4.3.1" - checksum: 10/dd71efe9412433169b805d5c346a6473e539ce30f605752a0d40a0733feba37259bd72bb4ad2ab591e2eaff1ee56633de160c1e98efdc8f373cf33a4a8660275 + checksum: 10/3a0d0438f1ba9fed4fbe1706ea598a865f9af655a16ca9517ab57bda526e224569ca1b980b473fb68feea5e08deafbbf2cf9febb941f92f2d2533310c3fc4abc languageName: node linkType: hard -"@babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.5, @babel/types@npm:^7.28.6, @babel/types@npm:^7.4.4": - version: 7.28.6 - resolution: "@babel/types@npm:7.28.6" +"@babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.5, @babel/types@npm:^7.28.6, @babel/types@npm:^7.29.0, @babel/types@npm:^7.4.4": + version: 7.29.0 + resolution: "@babel/types@npm:7.29.0" dependencies: "@babel/helper-string-parser": "npm:^7.27.1" "@babel/helper-validator-identifier": "npm:^7.28.5" - checksum: 10/f9c6e52b451065aae5654686ecfc7de2d27dd0fbbc204ee2bd912a71daa359521a32f378981b1cf333ace6c8f86928814452cb9f388a7da59ad468038deb6b5f + checksum: 10/bfc2b211210f3894dcd7e6a33b2d1c32c93495dc1e36b547376aa33441abe551ab4bc1640d4154ee2acd8e46d3bbc925c7224caae02fcaf0e6a771e97fccc661 languageName: node linkType: hard @@ -1197,21 +1197,21 @@ __metadata: languageName: node linkType: hard -"@braintree/sanitize-url@npm:7.1.1": - version: 7.1.1 - resolution: "@braintree/sanitize-url@npm:7.1.1" - checksum: 10/a8a5535c5a0a459ba593a018c554b35493dff004fd09d7147db67243df83bce3d410b89ee7dc2d95cce195b85b877c72f8ca149e1040110a945d193c67293af0 +"@braintree/sanitize-url@npm:7.1.2": + version: 7.1.2 + resolution: "@braintree/sanitize-url@npm:7.1.2" + checksum: 10/d9626ff8f8eb5e192cd055e6e743449c21102c76bb59e405b7028fe56230fa080bfcc80dfb1e21850a6876e75adda9f7b3c888cf0685942bb74da4d2866d6ec3 languageName: node linkType: hard -"@bundle-stats/plugin-webpack-filter@npm:4.21.8": - version: 4.21.8 - resolution: "@bundle-stats/plugin-webpack-filter@npm:4.21.8" +"@bundle-stats/plugin-webpack-filter@npm:4.21.9": + version: 4.21.9 + resolution: "@bundle-stats/plugin-webpack-filter@npm:4.21.9" dependencies: tslib: "npm:2.8.1" peerDependencies: core-js: ^3.0.0 - checksum: 10/909d1ca31880b8f40fdad2fdc60484f086c3167d1654688be1fdd3c280a94ca73ee31dcde5ea118dcc0ee5dfe0dbfedcdec521cff831f13c69baa5af79beabcf + checksum: 10/927bdcc8b4822d2f167f3bf53023a51579268c6aaf5cdafbe4243c30da742e776d955c8fd7a4bad866ff3c6761cdfc5e2beb5f4c76b5080d8b9509cc0e3b159e languageName: node linkType: hard @@ -1227,15 +1227,15 @@ __metadata: languageName: node linkType: hard -"@codemirror/commands@npm:6.10.1": - version: 6.10.1 - resolution: "@codemirror/commands@npm:6.10.1" +"@codemirror/commands@npm:6.10.2": + version: 6.10.2 + resolution: "@codemirror/commands@npm:6.10.2" dependencies: "@codemirror/language": "npm:^6.0.0" "@codemirror/state": "npm:^6.4.0" "@codemirror/view": "npm:^6.27.0" "@lezer/common": "npm:^1.1.0" - checksum: 10/9e305263dc457635fa1c7e5b47756958be5367e38f5bb07a3abfd5966591e2eafd57ea0c5c738b28bb3ab5de64c07a5302ebd49b129ff7e48b225841f66e647f + checksum: 10/cf5cfee8db49911fd190e55dc6f2a15d5c27c875a2d422185ec11c6cdf1eeb2b66ed16d7f8023f1ec0de99b5e43f903629cb6dc1a7cdd64215e804c6d4576a6e languageName: node linkType: hard @@ -1262,35 +1262,35 @@ __metadata: languageName: node linkType: hard -"@codemirror/search@npm:6.5.11": - version: 6.5.11 - resolution: "@codemirror/search@npm:6.5.11" +"@codemirror/search@npm:6.6.0": + version: 6.6.0 + resolution: "@codemirror/search@npm:6.6.0" dependencies: "@codemirror/state": "npm:^6.0.0" - "@codemirror/view": "npm:^6.0.0" + "@codemirror/view": "npm:^6.37.0" crelt: "npm:^1.0.5" - checksum: 10/d057f37cb369460b25625d7eb72f40636bf78ecd140608da53010cf3660f982a9e613826e38d85d87c9c2ff11e45c9482429987bfd4f29cbbd192f1ee3fd2695 + checksum: 10/2947341cf06bde4a682250c245007f70854b7660f7aa29cec2c5cd177fa70deacaadf37c4aa323f669379d32f9912cb958448c93da3dc90b8af5c466ea1cddaf languageName: node linkType: hard -"@codemirror/state@npm:6.5.3, @codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.4.0, @codemirror/state@npm:^6.5.0": - version: 6.5.3 - resolution: "@codemirror/state@npm:6.5.3" +"@codemirror/state@npm:6.5.4, @codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.4.0, @codemirror/state@npm:^6.5.0": + version: 6.5.4 + resolution: "@codemirror/state@npm:6.5.4" dependencies: "@marijn/find-cluster-break": "npm:^1.0.0" - checksum: 10/07dc8e06aa3c78bde36fd584d1e1131a529d244474dd36bffc6ad1033701d6628a02259711692d099b2a482ede015930f20106aa8ebc7b251db6f303bc72caa2 + checksum: 10/0e1b073282f2ba246692ad0238def660d201a886fcdb8de28360db8d7c9ec34fe904b9187709d70ede4fe4e7cf9a2e1700d26fccf22ce8d8bca68e6cdf456b08 languageName: node linkType: hard -"@codemirror/view@npm:6.39.9, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0": - version: 6.39.9 - resolution: "@codemirror/view@npm:6.39.9" +"@codemirror/view@npm:6.39.12, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.37.0": + version: 6.39.12 + resolution: "@codemirror/view@npm:6.39.12" dependencies: "@codemirror/state": "npm:^6.5.0" crelt: "npm:^1.0.6" style-mod: "npm:^4.1.0" w3c-keyname: "npm:^2.2.4" - checksum: 10/9e86b35f31fd4f8b4c2fe608fa6116ddc71261acd842c405de41de1f752268c47ea8e0c400818b4d0481a629e1f773dda9e6f0d24d38ed6a9f6b3d58b2dff669 + checksum: 10/acd476d485914095fe38009bb29c7a6ac4cf3de2d3921172e27fc40639eb5bce0534766fd6e093c94aa60f076f263ce90a670a8052e220244bf4065f95c064bb languageName: node linkType: hard @@ -1333,12 +1333,10 @@ __metadata: languageName: node linkType: hard -"@csstools/css-syntax-patches-for-csstree@npm:1.0.14": - version: 1.0.14 - resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.0.14" - peerDependencies: - postcss: ^8.4 - checksum: 10/c783d5db307552f483d95266452a7765ca138a9e64f12d013c63e960c9c8abbf82c899a34028af1f5ad714e0e94edd97b1aa31784923c1d7d1756d775c3c1d0a +"@csstools/css-syntax-patches-for-csstree@npm:^1.0.21": + version: 1.0.25 + resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.0.25" + checksum: 10/42dfcd164ed6a66eee8dd3fcdbdaa58d032ab8c3bb5ead2453915429367766879b25332e09379a67357fd33742b856160e7c0182c7d90be00f57571b916c18e7 languageName: node linkType: hard @@ -1373,21 +1371,21 @@ __metadata: linkType: hard "@emnapi/core@npm:^1.5.0": - version: 1.7.1 - resolution: "@emnapi/core@npm:1.7.1" + version: 1.8.1 + resolution: "@emnapi/core@npm:1.8.1" dependencies: "@emnapi/wasi-threads": "npm:1.1.0" tslib: "npm:^2.4.0" - checksum: 10/260841f6dd2a7823a964d9de6da3a5e6f565dac8d21a5bd8f6215b87c45c22a4dc371b9ad877961579ee3cca8a76e55e3dd033ae29cba1998999cda6d794bdab + checksum: 10/904ea60c91fc7d8aeb4a8f2c433b8cfb47c50618f2b6f37429fc5093c857c6381c60628a5cfbc3a7b0d75b0a288f21d4ed2d4533e82f92c043801ef255fd6a5c languageName: node linkType: hard "@emnapi/runtime@npm:^1.5.0": - version: 1.7.1 - resolution: "@emnapi/runtime@npm:1.7.1" + version: 1.8.1 + resolution: "@emnapi/runtime@npm:1.8.1" dependencies: tslib: "npm:^2.4.0" - checksum: 10/6fc83f938e3c70e32e84c1fbe5cab6cb9340b8107cee4048384ad5b8f2998a06502b4bed342acaf6e44f473f2c14c4ab1e3fd5083bd7823fc63abfca9eff0175 + checksum: 10/26725e202d4baefdc4a6ba770f703dfc80825a27c27a08c22bac1e1ce6f8f75c47b4fe9424d9b63239463c33ef20b650f08d710da18dfa1164a95e5acb865dba languageName: node linkType: hard @@ -1400,184 +1398,184 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/aix-ppc64@npm:0.27.1" +"@esbuild/aix-ppc64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/aix-ppc64@npm:0.27.2" conditions: os=aix & cpu=ppc64 languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/android-arm64@npm:0.27.1" +"@esbuild/android-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-arm64@npm:0.27.2" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@esbuild/android-arm@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/android-arm@npm:0.27.1" +"@esbuild/android-arm@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-arm@npm:0.27.2" conditions: os=android & cpu=arm languageName: node linkType: hard -"@esbuild/android-x64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/android-x64@npm:0.27.1" +"@esbuild/android-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-x64@npm:0.27.2" conditions: os=android & cpu=x64 languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/darwin-arm64@npm:0.27.1" +"@esbuild/darwin-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/darwin-arm64@npm:0.27.2" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/darwin-x64@npm:0.27.1" +"@esbuild/darwin-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/darwin-x64@npm:0.27.2" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/freebsd-arm64@npm:0.27.1" +"@esbuild/freebsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/freebsd-arm64@npm:0.27.2" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/freebsd-x64@npm:0.27.1" +"@esbuild/freebsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/freebsd-x64@npm:0.27.2" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/linux-arm64@npm:0.27.1" +"@esbuild/linux-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-arm64@npm:0.27.2" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/linux-arm@npm:0.27.1" +"@esbuild/linux-arm@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-arm@npm:0.27.2" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/linux-ia32@npm:0.27.1" +"@esbuild/linux-ia32@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-ia32@npm:0.27.2" conditions: os=linux & cpu=ia32 languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/linux-loong64@npm:0.27.1" +"@esbuild/linux-loong64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-loong64@npm:0.27.2" conditions: os=linux & cpu=loong64 languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/linux-mips64el@npm:0.27.1" +"@esbuild/linux-mips64el@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-mips64el@npm:0.27.2" conditions: os=linux & cpu=mips64el languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/linux-ppc64@npm:0.27.1" +"@esbuild/linux-ppc64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-ppc64@npm:0.27.2" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/linux-riscv64@npm:0.27.1" +"@esbuild/linux-riscv64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-riscv64@npm:0.27.2" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/linux-s390x@npm:0.27.1" +"@esbuild/linux-s390x@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-s390x@npm:0.27.2" conditions: os=linux & cpu=s390x languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/linux-x64@npm:0.27.1" +"@esbuild/linux-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-x64@npm:0.27.2" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/netbsd-arm64@npm:0.27.1" +"@esbuild/netbsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/netbsd-arm64@npm:0.27.2" conditions: os=netbsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/netbsd-x64@npm:0.27.1" +"@esbuild/netbsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/netbsd-x64@npm:0.27.2" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/openbsd-arm64@npm:0.27.1" +"@esbuild/openbsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openbsd-arm64@npm:0.27.2" conditions: os=openbsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/openbsd-x64@npm:0.27.1" +"@esbuild/openbsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openbsd-x64@npm:0.27.2" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/openharmony-arm64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/openharmony-arm64@npm:0.27.1" +"@esbuild/openharmony-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openharmony-arm64@npm:0.27.2" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/sunos-x64@npm:0.27.1" +"@esbuild/sunos-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/sunos-x64@npm:0.27.2" conditions: os=sunos & cpu=x64 languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/win32-arm64@npm:0.27.1" +"@esbuild/win32-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-arm64@npm:0.27.2" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/win32-ia32@npm:0.27.1" +"@esbuild/win32-ia32@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-ia32@npm:0.27.2" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.27.1": - version: 0.27.1 - resolution: "@esbuild/win32-x64@npm:0.27.1" +"@esbuild/win32-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-x64@npm:0.27.2" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -1670,15 +1668,15 @@ __metadata: languageName: node linkType: hard -"@exodus/bytes@npm:^1.6.0": - version: 1.6.0 - resolution: "@exodus/bytes@npm:1.6.0" +"@exodus/bytes@npm:^1.11.0, @exodus/bytes@npm:^1.6.0": + version: 1.11.0 + resolution: "@exodus/bytes@npm:1.11.0" peerDependencies: - "@exodus/crypto": ^1.0.0-rc.4 + "@noble/hashes": ^1.8.0 || ^2.0.0 peerDependenciesMeta: - "@exodus/crypto": + "@noble/hashes": optional: true - checksum: 10/4066bc5f2b7782fabdad4cac707031cbe7c3491bcd38f28e3b5144d687f858e834aaa9b0bcbe9685f1ccfaf4dc747172e82f28cf361e9623448ec80ab755b198 + checksum: 10/a8c815fc15a6f16e05ae2da11b210c3bec7d75c7595478c4b8bb8f3a7bc7fde4b44dc9f160b15c9d7a5b064abd6f38731d7817c132c9da60a4164038601ff54f languageName: node linkType: hard @@ -1708,166 +1706,167 @@ __metadata: languageName: node linkType: hard -"@formatjs/ecma402-abstract@npm:3.0.8": - version: 3.0.8 - resolution: "@formatjs/ecma402-abstract@npm:3.0.8" +"@formatjs/ecma402-abstract@npm:3.1.1": + version: 3.1.1 + resolution: "@formatjs/ecma402-abstract@npm:3.1.1" dependencies: - "@formatjs/fast-memoize": "npm:3.0.3" - "@formatjs/intl-localematcher": "npm:0.7.5" - decimal.js: "npm:^10.4.3" - tslib: "npm:^2.8.0" - checksum: 10/5b8e00790f79c011a6928644baee1409b86353576a91f7a8e9b4562a4a5d486997036ff04cd8451891ab8b7691e683c34cd40d76d4a1751e7df6052d46c54f06 + "@formatjs/fast-memoize": "npm:3.1.0" + "@formatjs/intl-localematcher": "npm:0.8.1" + decimal.js: "npm:^10.6.0" + tslib: "npm:^2.8.1" + checksum: 10/cc494b17601034396e989fe3f2a2b15ed79e05c5d7143269f09ba42db382d216133815901e537fe83dccf80ba32f58d2bcea65f27ba5538402b0e7c25196d426 languageName: node linkType: hard -"@formatjs/fast-memoize@npm:3.0.3": - version: 3.0.3 - resolution: "@formatjs/fast-memoize@npm:3.0.3" +"@formatjs/fast-memoize@npm:3.1.0": + version: 3.1.0 + resolution: "@formatjs/fast-memoize@npm:3.1.0" dependencies: - tslib: "npm:^2.8.0" - checksum: 10/466aa1250eee77897455b6e0282bd178357e326790e18f59e33c77371ebb6099cbf8c520300193ef10ab4909174286ab1d922fa930f02b81dddf08d28eaef1a1 + tslib: "npm:^2.8.1" + checksum: 10/700dfd0a10d20d8cb7427406127f78612a696e4942d38a8f153d82ca213f26bd0616d01a29957f194688887551595455eb0f629c2ce36d964a66ba15815afde9 languageName: node linkType: hard -"@formatjs/icu-messageformat-parser@npm:3.3.0": - version: 3.3.0 - resolution: "@formatjs/icu-messageformat-parser@npm:3.3.0" +"@formatjs/icu-messageformat-parser@npm:3.5.1": + version: 3.5.1 + resolution: "@formatjs/icu-messageformat-parser@npm:3.5.1" dependencies: - "@formatjs/ecma402-abstract": "npm:3.0.8" - "@formatjs/icu-skeleton-parser": "npm:2.0.8" - tslib: "npm:^2.8.0" - checksum: 10/a8f03e9799c9b4f682f6051ea09d3327abd93dbc1ce17b55e07ec45da7d6e3609c1c87a2ad1bfccbd7bdf54c903d98e8bbe06fa282c4dc44263c55b1927f154e + "@formatjs/ecma402-abstract": "npm:3.1.1" + "@formatjs/icu-skeleton-parser": "npm:2.1.1" + tslib: "npm:^2.8.1" + checksum: 10/12ba6ef6c7f6470057ca69f92290a3f341816deea2a010117e29b803c132274e22f191b4306e35b1307c283dda606bf6bdc134a866bfe2b1464356242c2a1d18 languageName: node linkType: hard -"@formatjs/icu-skeleton-parser@npm:2.0.8": - version: 2.0.8 - resolution: "@formatjs/icu-skeleton-parser@npm:2.0.8" +"@formatjs/icu-skeleton-parser@npm:2.1.1": + version: 2.1.1 + resolution: "@formatjs/icu-skeleton-parser@npm:2.1.1" dependencies: - "@formatjs/ecma402-abstract": "npm:3.0.8" - tslib: "npm:^2.8.0" - checksum: 10/9917502129e663da5ec8008a9a27c9032e01477adcf4c71788eb475225f57f4602711f9b8f3a3b669c801b0232e9eb53e58c0fdb11aa81e2339e1416f636be3f + "@formatjs/ecma402-abstract": "npm:3.1.1" + tslib: "npm:^2.8.1" + checksum: 10/4bafb8d04125b452a4ac6a46591b9fb1f0612b9423c9d4df5482ba578d77d20d1b838d5a229913691c0eb660e99fa06053129800693f8cfd011806532436aef4 languageName: node linkType: hard -"@formatjs/intl-datetimeformat@npm:7.1.2": - version: 7.1.2 - resolution: "@formatjs/intl-datetimeformat@npm:7.1.2" +"@formatjs/intl-datetimeformat@npm:7.2.1": + version: 7.2.1 + resolution: "@formatjs/intl-datetimeformat@npm:7.2.1" dependencies: - "@formatjs/ecma402-abstract": "npm:3.0.8" - "@formatjs/intl-localematcher": "npm:0.7.5" - decimal.js: "npm:^10.4.3" - tslib: "npm:^2.8.0" - checksum: 10/e31d35aa8c38b5fd08254726d2d90629629534b5bbc134f2264b60eb1bce36224526112d6ceb2c4eeaed73b29e77c21d3fc8a906b767e28613798dd4ea0819cc + "@formatjs/ecma402-abstract": "npm:3.1.1" + "@formatjs/intl-localematcher": "npm:0.8.1" + decimal.js: "npm:^10.6.0" + tslib: "npm:^2.8.1" + checksum: 10/ed6664a70084780d13db121debb9f337d93b9f17b8690158fcf37684aaccbc9f74366bc3a01a34a3032302b0fc2189fd924492f049869c623d3c94f08c60675e languageName: node linkType: hard -"@formatjs/intl-displaynames@npm:7.1.2": - version: 7.1.2 - resolution: "@formatjs/intl-displaynames@npm:7.1.2" +"@formatjs/intl-displaynames@npm:7.2.1": + version: 7.2.1 + resolution: "@formatjs/intl-displaynames@npm:7.2.1" dependencies: - "@formatjs/ecma402-abstract": "npm:3.0.8" - "@formatjs/intl-localematcher": "npm:0.7.5" - tslib: "npm:^2.8.0" - checksum: 10/8c2a98d8f5fb6361d380beaae107cf58ce9ab963ea291e37081dc896407686160683837259d634e9a46a4a41d6c0bee93403de33c929d91ec320ee7bcfaed20b + "@formatjs/ecma402-abstract": "npm:3.1.1" + "@formatjs/intl-localematcher": "npm:0.8.1" + tslib: "npm:^2.8.1" + checksum: 10/5f72ca3a9838850e13ae3d1baf9779158e6c7265678dc79252026942338eea6f7538150601ef54520901eef492fa66a05d8e8d4b5b8cbe30a0b879fb3500677e languageName: node linkType: hard -"@formatjs/intl-durationformat@npm:0.9.2": - version: 0.9.2 - resolution: "@formatjs/intl-durationformat@npm:0.9.2" +"@formatjs/intl-durationformat@npm:0.10.1": + version: 0.10.1 + resolution: "@formatjs/intl-durationformat@npm:0.10.1" dependencies: - "@formatjs/ecma402-abstract": "npm:3.0.8" - "@formatjs/intl-localematcher": "npm:0.7.5" - tslib: "npm:^2.8.0" - checksum: 10/7c4492640683aa30fbcbd844878e50eb44ef7aa0a415fd6cf10a1b8804cc80324437b4f8121bac2d56d3f4ec4d3c61e14e870852ec6b233a6c5747292fbc3585 + "@formatjs/ecma402-abstract": "npm:3.1.1" + "@formatjs/intl-localematcher": "npm:0.8.1" + tslib: "npm:^2.8.1" + checksum: 10/87387ccc720f8279e5ffacfc63ee49918735686b8a4414190ab570e1bb92433d49276c23ed5b474d144bff2eb6d19e10aa568b2897f8fcbc7afcb0669a92bb52 languageName: node linkType: hard -"@formatjs/intl-enumerator@npm:2.1.2": - version: 2.1.2 - resolution: "@formatjs/intl-enumerator@npm:2.1.2" +"@formatjs/intl-getcanonicallocales@npm:3.2.1": + version: 3.2.1 + resolution: "@formatjs/intl-getcanonicallocales@npm:3.2.1" dependencies: - "@formatjs/ecma402-abstract": "npm:3.0.8" - tslib: "npm:^2.8.0" - checksum: 10/1a551a3a3d4f113d071b503946ea8aaf6b18b69fa746cc6d7e2ca520023794f4f1b82777cd7e9632ea8980473b951c252d52b8375f264e5c151c6cac6a19d6e5 + tslib: "npm:^2.8.1" + checksum: 10/7982699fbcfbc5f4cece8ab6a22bded7b0881901fd56924fa4085b3a01755696e2f6c70a854e34256a8c00fb5b2094bf8264e5ae3a814f326f92a7fd3a4b1682 languageName: node linkType: hard -"@formatjs/intl-getcanonicallocales@npm:3.1.2": - version: 3.1.2 - resolution: "@formatjs/intl-getcanonicallocales@npm:3.1.2" +"@formatjs/intl-listformat@npm:8.2.1": + version: 8.2.1 + resolution: "@formatjs/intl-listformat@npm:8.2.1" dependencies: - tslib: "npm:^2.8.0" - checksum: 10/490f92e9b4c8d887df9afc602fbc857dc47f9e4cca29e73fcb92e35083cdef6091bcacce59f9d8f75334a3010ce41d9ae16143c1b1827550e29d0d01558516b4 + "@formatjs/ecma402-abstract": "npm:3.1.1" + "@formatjs/intl-localematcher": "npm:0.8.1" + tslib: "npm:^2.8.1" + checksum: 10/78548365d7af8280c2cd24d4500242590b4374cc6cde09c65f24752599506a4d8ea3825fa60a6efa552e3fbe45e5aa1a32361081258529bb66f4e1fd4bb03ddc languageName: node linkType: hard -"@formatjs/intl-listformat@npm:8.1.2": - version: 8.1.2 - resolution: "@formatjs/intl-listformat@npm:8.1.2" +"@formatjs/intl-locale@npm:5.2.1": + version: 5.2.1 + resolution: "@formatjs/intl-locale@npm:5.2.1" dependencies: - "@formatjs/ecma402-abstract": "npm:3.0.8" - "@formatjs/intl-localematcher": "npm:0.7.5" - tslib: "npm:^2.8.0" - checksum: 10/ce9f5d821bdd9ec0b973f8869b17fcb84808442a1d43209099a6e4a0c44d5bd2a978c33d61a0929ba6e02aef5bbea2d36129e16c311f433b3cfa6ab7c07f51d2 + "@formatjs/ecma402-abstract": "npm:3.1.1" + "@formatjs/intl-getcanonicallocales": "npm:3.2.1" + "@formatjs/intl-supportedvaluesof": "npm:2.2.1" + tslib: "npm:^2.8.1" + checksum: 10/dce1701ff44006b73037129fa7ea994103407f3a8d8a323633149a0e64153eb2222823355bfc98140763d8e2ba99b841c2756a7bb4694d464ae1c39af47d2107 languageName: node linkType: hard -"@formatjs/intl-locale@npm:5.1.2": - version: 5.1.2 - resolution: "@formatjs/intl-locale@npm:5.1.2" +"@formatjs/intl-localematcher@npm:0.8.1": + version: 0.8.1 + resolution: "@formatjs/intl-localematcher@npm:0.8.1" dependencies: - "@formatjs/ecma402-abstract": "npm:3.0.8" - "@formatjs/intl-enumerator": "npm:2.1.2" - "@formatjs/intl-getcanonicallocales": "npm:3.1.2" - tslib: "npm:^2.8.0" - checksum: 10/85b1257231461e9ccf7ba0b4fc65d97a7a4e4cbb7fdf50da209fe0b91fb56e00f19e3ca8bacf67eb581feda6ceb861112d9cc4624805e19c80e8d0b63535b821 + "@formatjs/fast-memoize": "npm:3.1.0" + tslib: "npm:^2.8.1" + checksum: 10/789cb556a15acfb51ad8150558966e9b9fe65e64b573c16475a51c6abd352488c4dca745b174819d584633073c70147f590b9c45586a202acaeb6942c6bad39f languageName: node linkType: hard -"@formatjs/intl-localematcher@npm:0.7.5": - version: 0.7.5 - resolution: "@formatjs/intl-localematcher@npm:0.7.5" +"@formatjs/intl-numberformat@npm:9.2.2": + version: 9.2.2 + resolution: "@formatjs/intl-numberformat@npm:9.2.2" dependencies: - "@formatjs/fast-memoize": "npm:3.0.3" - tslib: "npm:^2.8.0" - checksum: 10/9899304f23206c6fceb66e25d073278f64b2f02b66b81d5f982f3d5b644e83d81ba74520352289b3db3f8afd932c959a4631784386bbcb74105b8126101ea9ac + "@formatjs/ecma402-abstract": "npm:3.1.1" + "@formatjs/intl-localematcher": "npm:0.8.1" + decimal.js: "npm:^10.6.0" + tslib: "npm:^2.8.1" + checksum: 10/97cc9871d733eca06f5c12d26833aa3fc4672d23f5c3a6a7dbf7a44f4216591d845d3a3ce8170ff8df4b2ef150dccf2374af552f2c81b4bd4d6121c996c24751 languageName: node linkType: hard -"@formatjs/intl-numberformat@npm:9.1.2": - version: 9.1.2 - resolution: "@formatjs/intl-numberformat@npm:9.1.2" +"@formatjs/intl-pluralrules@npm:6.2.2": + version: 6.2.2 + resolution: "@formatjs/intl-pluralrules@npm:6.2.2" dependencies: - "@formatjs/ecma402-abstract": "npm:3.0.8" - "@formatjs/intl-localematcher": "npm:0.7.5" - decimal.js: "npm:^10.4.3" - tslib: "npm:^2.8.0" - checksum: 10/9eef02a74154a9f7c8c9856073bbb588290e0382672da2d6075c003256549dd491657056e86cd5c33916cefb78af8c9a0fd6695e6051e711d4179418e4f427a7 + "@formatjs/ecma402-abstract": "npm:3.1.1" + "@formatjs/intl-localematcher": "npm:0.8.1" + decimal.js: "npm:^10.6.0" + tslib: "npm:^2.8.1" + checksum: 10/71c2844371a427b3d55153b6cf5bcc16b5f0dffeefc048b2fe02a1dabceb062eba4abd39ed154fe11fcfb0e74e3d226c0f82667118606bcda059797894d7d630 languageName: node linkType: hard -"@formatjs/intl-pluralrules@npm:6.1.2": - version: 6.1.2 - resolution: "@formatjs/intl-pluralrules@npm:6.1.2" +"@formatjs/intl-relativetimeformat@npm:12.2.2": + version: 12.2.2 + resolution: "@formatjs/intl-relativetimeformat@npm:12.2.2" dependencies: - "@formatjs/ecma402-abstract": "npm:3.0.8" - "@formatjs/intl-localematcher": "npm:0.7.5" - decimal.js: "npm:^10.4.3" - tslib: "npm:^2.8.0" - checksum: 10/e1f611094eb1d1dafccdf815933062e014ac73c1bfba2ded7654d429d1366be15d77b06f8a00e165d530e6b3488995ef38fb73114e0065edc71eda7cb1eb767c + "@formatjs/ecma402-abstract": "npm:3.1.1" + "@formatjs/intl-localematcher": "npm:0.8.1" + tslib: "npm:^2.8.1" + checksum: 10/958865bcfee4af7f4fb6bdcb88b712a7a9116657fa2d68c3e492ddd52afe7cf70605e7305cccf6c2336acf3c117ed17939d6f814d35b4f2e4bf675ed907e5dc3 languageName: node linkType: hard -"@formatjs/intl-relativetimeformat@npm:12.1.2": - version: 12.1.2 - resolution: "@formatjs/intl-relativetimeformat@npm:12.1.2" +"@formatjs/intl-supportedvaluesof@npm:2.2.1": + version: 2.2.1 + resolution: "@formatjs/intl-supportedvaluesof@npm:2.2.1" dependencies: - "@formatjs/ecma402-abstract": "npm:3.0.8" - "@formatjs/intl-localematcher": "npm:0.7.5" - tslib: "npm:^2.8.0" - checksum: 10/bb324d20baf9e20588425196bef49f6096578bb7b90317b5c356b775e4b36ea8eb39e11565d692bd02b9f20c2d3021f07c559bf58f03817d685ea0c322f851cd + "@formatjs/ecma402-abstract": "npm:3.1.1" + "@formatjs/fast-memoize": "npm:3.1.0" + tslib: "npm:^2.8.1" + checksum: 10/efa90f2e509608cd55ca30ede420323e1b7416ecfffda60acd5984d42a6580f97b756cb29c633370da213ded279dd2403d9a38b49864786f5dd221e57cf0f2b6 languageName: node linkType: hard @@ -1953,9 +1952,9 @@ __metadata: languageName: node linkType: hard -"@home-assistant/webawesome@npm:3.0.0-ha.2": - version: 3.0.0-ha.2 - resolution: "@home-assistant/webawesome@npm:3.0.0-ha.2" +"@home-assistant/webawesome@npm:3.2.1-ha.0": + version: 3.2.1-ha.0 + resolution: "@home-assistant/webawesome@npm:3.2.1-ha.0" dependencies: "@ctrl/tinycolor": "npm:4.1.0" "@floating-ui/dom": "npm:^1.6.13" @@ -1966,7 +1965,7 @@ __metadata: lit: "npm:^3.2.1" nanoid: "npm:^5.1.5" qr-creator: "npm:^1.0.0" - checksum: 10/c94908d88c1e25604d148dfb375b3fb025fc838c9b9ee4aa729b7f111aef6ed45727158923d2d15e2def4b7c74057c2e779b358e90c98e6e0391b3020aa0391c + checksum: 10/27b61807ae41267acb112a5657b956b19c8496a3ccbf19b892a97766d5a59ed39f2c3084b1143c129a592d4685de6c19293d02986d7951056dffdcbde9fe7ce3 languageName: node linkType: hard @@ -2008,12 +2007,12 @@ __metadata: languageName: node linkType: hard -"@isaacs/brace-expansion@npm:^5.0.0": - version: 5.0.0 - resolution: "@isaacs/brace-expansion@npm:5.0.0" +"@isaacs/brace-expansion@npm:^5.0.1": + version: 5.0.1 + resolution: "@isaacs/brace-expansion@npm:5.0.1" dependencies: "@isaacs/balanced-match": "npm:^4.0.1" - checksum: 10/cf3b7f206aff12128214a1df764ac8cdbc517c110db85249b945282407e3dfc5c6e66286383a7c9391a059fc8e6e6a8ca82262fc9d2590bd615376141fbebd2d + checksum: 10/aec226065bc4285436a27379e08cc35bf94ef59f5098ac1c026495c9ba4ab33d851964082d3648d56d63eb90f2642867bd15a3e1b810b98beb1a8c14efce6a94 languageName: node linkType: hard @@ -2294,11 +2293,11 @@ __metadata: linkType: hard "@lezer/lr@npm:^1.0.0": - version: 1.4.5 - resolution: "@lezer/lr@npm:1.4.5" + version: 1.4.7 + resolution: "@lezer/lr@npm:1.4.7" dependencies: "@lezer/common": "npm:^1.0.0" - checksum: 10/f951b71fe7851b0009b8e884fdef4308aa2a9382bb31daaae5fd70d02d039995da17044c676e7587cff50e0a788f34148fef3542aebc8af248c0c3d76bea9826 + checksum: 10/5407e10c8f983eedd8eaace9f2582aac39f7b280cdcf4e396d53ca6c1e654ce1bb2fdbddfbf9a63c8462046be37c8c4da180be7ffaf2d2aa24eb71622f624d85 languageName: node linkType: hard @@ -2321,10 +2320,10 @@ __metadata: languageName: node linkType: hard -"@lit-labs/ssr-dom-shim@npm:^1.4.0, @lit-labs/ssr-dom-shim@npm:^1.5.0": - version: 1.5.0 - resolution: "@lit-labs/ssr-dom-shim@npm:1.5.0" - checksum: 10/7ed5a816ebf8b3d06c155db916793300875c523b9877e2c6be0dbcc034ec8e4cf1b034007c801c0d0a5d352f47c0aa782bcdd19af2240f88f3e2489312f8b8a8 +"@lit-labs/ssr-dom-shim@npm:^1.5.0": + version: 1.5.1 + resolution: "@lit-labs/ssr-dom-shim@npm:1.5.1" + checksum: 10/8b90e101cbaef3aa9d3a16cd90010c2c7c13cc0b3effa8ce166e2c7a57e52e759fb7bb4f800ce5b5dfb1d7cbd8430a0b430bcc8bcb8b54d1417dbf2eb046dd6a languageName: node linkType: hard @@ -2365,10 +2364,10 @@ __metadata: languageName: node linkType: hard -"@lokalise/node-api@npm:15.6.0": - version: 15.6.0 - resolution: "@lokalise/node-api@npm:15.6.0" - checksum: 10/29d4497c34fe090d53bd3ca3f4f1924159d79624fe4706eac6e71b656e55f54b30cd73063e942ea4f4a45714586364fc0e964f1ad4fef4dd7d43af96da19dbb2 +"@lokalise/node-api@npm:15.6.1": + version: 15.6.1 + resolution: "@lokalise/node-api@npm:15.6.1" + checksum: 10/0533046b1271b299d64d1eb9d7561a2d71e8dfead329c9f3fffa7141c069072ce975e4fe1935e5605b5fbbb9abb915b02e3150435b0d852541d7ae01d6ee0026 languageName: node linkType: hard @@ -2904,7 +2903,7 @@ __metadata: languageName: node linkType: hard -"@material/mwc-menu@npm:0.27.0, @material/mwc-menu@npm:^0.27.0": +"@material/mwc-menu@npm:^0.27.0": version: 0.27.0 resolution: "@material/mwc-menu@npm:0.27.0" dependencies: @@ -3705,254 +3704,275 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.53.5" +"@rollup/rollup-android-arm-eabi@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.55.1" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-android-arm64@npm:4.53.5" +"@rollup/rollup-android-arm64@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-android-arm64@npm:4.55.1" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-darwin-arm64@npm:4.53.5" +"@rollup/rollup-darwin-arm64@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-darwin-arm64@npm:4.55.1" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-darwin-x64@npm:4.53.5" +"@rollup/rollup-darwin-x64@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-darwin-x64@npm:4.55.1" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.53.5" +"@rollup/rollup-freebsd-arm64@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.55.1" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-freebsd-x64@npm:4.53.5" +"@rollup/rollup-freebsd-x64@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-freebsd-x64@npm:4.55.1" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.53.5" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.55.1" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.53.5" +"@rollup/rollup-linux-arm-musleabihf@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.55.1" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.53.5" +"@rollup/rollup-linux-arm64-gnu@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.55.1" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.53.5" +"@rollup/rollup-linux-arm64-musl@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.55.1" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-loong64-gnu@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.53.5" +"@rollup/rollup-linux-loong64-gnu@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.55.1" conditions: os=linux & cpu=loong64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-ppc64-gnu@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.53.5" +"@rollup/rollup-linux-loong64-musl@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-loong64-musl@npm:4.55.1" + conditions: os=linux & cpu=loong64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-ppc64-gnu@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.55.1" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.53.5" +"@rollup/rollup-linux-ppc64-musl@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.55.1" + conditions: os=linux & cpu=ppc64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-gnu@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.55.1" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-musl@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.53.5" +"@rollup/rollup-linux-riscv64-musl@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.55.1" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.53.5" +"@rollup/rollup-linux-s390x-gnu@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.55.1" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.53.5" +"@rollup/rollup-linux-x64-gnu@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.55.1" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.53.5" +"@rollup/rollup-linux-x64-musl@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.55.1" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-openharmony-arm64@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-openharmony-arm64@npm:4.53.5" +"@rollup/rollup-openbsd-x64@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-openbsd-x64@npm:4.55.1" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-openharmony-arm64@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.55.1" conditions: os=openharmony & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.53.5" +"@rollup/rollup-win32-arm64-msvc@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.55.1" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.53.5" +"@rollup/rollup-win32-ia32-msvc@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.55.1" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-gnu@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-win32-x64-gnu@npm:4.53.5" +"@rollup/rollup-win32-x64-gnu@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.55.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.53.5": - version: 4.53.5 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.53.5" +"@rollup/rollup-win32-x64-msvc@npm:4.55.1": + version: 4.55.1 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.55.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@rsbuild/plugin-check-syntax@npm:1.5.0": - version: 1.5.0 - resolution: "@rsbuild/plugin-check-syntax@npm:1.5.0" +"@rsbuild/plugin-check-syntax@npm:1.6.1": + version: 1.6.1 + resolution: "@rsbuild/plugin-check-syntax@npm:1.6.1" dependencies: acorn: "npm:^8.15.0" - browserslist-to-es-version: "npm:^1.1.1" + browserslist-to-es-version: "npm:^1.2.0" htmlparser2: "npm:10.0.0" picocolors: "npm:^1.1.1" source-map: "npm:^0.7.6" peerDependencies: - "@rsbuild/core": 1.x + "@rsbuild/core": ^1.0.0 || ^2.0.0-0 peerDependenciesMeta: "@rsbuild/core": optional: true - checksum: 10/69f537d24aa298b9913c1e21378eded13a6deff21e597257bdfeb1ddbe42c711d21d068543190cdd2f6537531fea24feb84cf1637e2492c9fc5d2fcc1f9a53d6 + checksum: 10/9ae6097a0800dd7bd8802dc9517d34d4e48a0d691b185e5a4b48f3830a11e7b40ed02e04db1d00edfe3c2dd8736748b02af3d58743dfaf74193b94248061c0b0 languageName: node linkType: hard -"@rsdoctor/client@npm:1.4.0": - version: 1.4.0 - resolution: "@rsdoctor/client@npm:1.4.0" - checksum: 10/0507957b2fa3dbd5340f49f3a587b5d20d74c420d5eb545009c299f3d8abd2641c17c52223c01bf767aff60aece837e1e092216869bd91bfca5548fa368abbb8 +"@rsdoctor/client@npm:1.5.2": + version: 1.5.2 + resolution: "@rsdoctor/client@npm:1.5.2" + checksum: 10/fb5188a19109294e529c5dedb79fbeda36d03df5dc567583196b68b0baecad0c67bd278f1f3fd915a6fe44870a6d184076a8de38aa717f378765cc41e8d83eac languageName: node linkType: hard -"@rsdoctor/core@npm:1.4.0": - version: 1.4.0 - resolution: "@rsdoctor/core@npm:1.4.0" +"@rsdoctor/core@npm:1.5.2": + version: 1.5.2 + resolution: "@rsdoctor/core@npm:1.5.2" dependencies: - "@rsbuild/plugin-check-syntax": "npm:1.5.0" - "@rsdoctor/graph": "npm:1.4.0" - "@rsdoctor/sdk": "npm:1.4.0" - "@rsdoctor/types": "npm:1.4.0" - "@rsdoctor/utils": "npm:1.4.0" + "@rsbuild/plugin-check-syntax": "npm:1.6.1" + "@rsdoctor/graph": "npm:1.5.2" + "@rsdoctor/sdk": "npm:1.5.2" + "@rsdoctor/types": "npm:1.5.2" + "@rsdoctor/utils": "npm:1.5.2" browserslist-load-config: "npm:^1.0.1" enhanced-resolve: "npm:5.12.0" - es-toolkit: "npm:^1.41.0" + es-toolkit: "npm:^1.43.0" filesize: "npm:^10.1.6" fs-extra: "npm:^11.1.1" semver: "npm:^7.7.3" source-map: "npm:^0.7.6" - checksum: 10/308ba9016e761ecdb1cff1d4bad42d6bf57a8c84223f666072a4175d90c999c5ebf8e8c86051a7cd55b069e2bea39cdefe9f7633c07a1669212901c13609ad3c + checksum: 10/2752ec40b9988f4fcc41cf01612d617e5fa75b68798937c2c71d09488a69b907f3787a2c039c39b32671fb4407bc8577bca71b877c5ef31c4fc1c5674ab9fea2 languageName: node linkType: hard -"@rsdoctor/graph@npm:1.4.0": - version: 1.4.0 - resolution: "@rsdoctor/graph@npm:1.4.0" +"@rsdoctor/graph@npm:1.5.2": + version: 1.5.2 + resolution: "@rsdoctor/graph@npm:1.5.2" dependencies: - "@rsdoctor/types": "npm:1.4.0" - "@rsdoctor/utils": "npm:1.4.0" - es-toolkit: "npm:^1.41.0" + "@rsdoctor/types": "npm:1.5.2" + "@rsdoctor/utils": "npm:1.5.2" + es-toolkit: "npm:^1.43.0" path-browserify: "npm:1.0.1" source-map: "npm:^0.7.6" - checksum: 10/4f05b4e989455f83e50bee293fe6cc86cba75f7e30d92536e4ef02c7dab1a74c0cd75a1c585d5ec7ab63bea50838b8be7500c942986324f9be022774c4983947 + checksum: 10/3cab17675b9a959f5476d15d87d59b700bcf711a8fcfe6b42bbd44d92ff5c6bb85b0c75c16d6eeb8777330f2e38546dd5b6f740f824529bf1fb017149110c513 languageName: node linkType: hard -"@rsdoctor/rspack-plugin@npm:1.4.0": - version: 1.4.0 - resolution: "@rsdoctor/rspack-plugin@npm:1.4.0" +"@rsdoctor/rspack-plugin@npm:1.5.2": + version: 1.5.2 + resolution: "@rsdoctor/rspack-plugin@npm:1.5.2" dependencies: - "@rsdoctor/core": "npm:1.4.0" - "@rsdoctor/graph": "npm:1.4.0" - "@rsdoctor/sdk": "npm:1.4.0" - "@rsdoctor/types": "npm:1.4.0" - "@rsdoctor/utils": "npm:1.4.0" + "@rsdoctor/core": "npm:1.5.2" + "@rsdoctor/graph": "npm:1.5.2" + "@rsdoctor/sdk": "npm:1.5.2" + "@rsdoctor/types": "npm:1.5.2" + "@rsdoctor/utils": "npm:1.5.2" peerDependencies: "@rspack/core": "*" peerDependenciesMeta: "@rspack/core": optional: true - checksum: 10/0e0a1dc9ae003db2ed002b1feea08c3e0c7c7091118606e32bcf3f29a510aae09df22ee525b0832447cc416d29a3541691b67861a09816b892f01dbf5bafe06e + checksum: 10/4316c649c0c55cb3836ad5ff44f5c5975cc1e94af43973d08e2d9a88304f4558825e8297c0c82efbe4be74303a354294b195af8a7c479707d70c81dde3f413b9 languageName: node linkType: hard -"@rsdoctor/sdk@npm:1.4.0": - version: 1.4.0 - resolution: "@rsdoctor/sdk@npm:1.4.0" +"@rsdoctor/sdk@npm:1.5.2": + version: 1.5.2 + resolution: "@rsdoctor/sdk@npm:1.5.2" dependencies: - "@rsdoctor/client": "npm:1.4.0" - "@rsdoctor/graph": "npm:1.4.0" - "@rsdoctor/types": "npm:1.4.0" - "@rsdoctor/utils": "npm:1.4.0" + "@rsdoctor/client": "npm:1.5.2" + "@rsdoctor/graph": "npm:1.5.2" + "@rsdoctor/types": "npm:1.5.2" + "@rsdoctor/utils": "npm:1.5.2" safer-buffer: "npm:2.1.2" socket.io: "npm:4.8.1" tapable: "npm:2.2.3" - checksum: 10/30372447b28d08641670f974a7e8d2bce1704a41005cef004fc64492db25cdd5accd480faa7e9d697988ab34408fd2f0bee3a87d7f96d09232778627e2e50e29 + checksum: 10/c9e0ff49a72ae9ac3fe735cb913876e8fee0e70bb4075fa9bd4813d51a5c6b10235908428e5595af32985c6bf12db5bdf9f5a3fc41978c0aceabd96d185d0111 languageName: node linkType: hard -"@rsdoctor/types@npm:1.4.0": - version: 1.4.0 - resolution: "@rsdoctor/types@npm:1.4.0" +"@rsdoctor/types@npm:1.5.2": + version: 1.5.2 + resolution: "@rsdoctor/types@npm:1.5.2" dependencies: "@types/connect": "npm:3.4.38" "@types/estree": "npm:1.0.5" @@ -3966,16 +3986,16 @@ __metadata: optional: true webpack: optional: true - checksum: 10/8e190b111665ad7a5a6e64f3c8615e945a5624e9a34a6e448a2ffa4eb57bf379df063cf79d7b0ee855930a9e188d4d649ae27659ad33d42453801f8d0c0a168c + checksum: 10/d669ebd62c65b2c34b5d97178cb6bb3f8e96434b406171960da255ef056e5d8f611966726b1c3462f1d3c30c6939d6dde69591104f7e83a44bbc153c78f4f40a languageName: node linkType: hard -"@rsdoctor/utils@npm:1.4.0": - version: 1.4.0 - resolution: "@rsdoctor/utils@npm:1.4.0" +"@rsdoctor/utils@npm:1.5.2": + version: 1.5.2 + resolution: "@rsdoctor/utils@npm:1.5.2" dependencies: "@babel/code-frame": "npm:7.26.2" - "@rsdoctor/types": "npm:1.4.0" + "@rsdoctor/types": "npm:1.5.2" "@types/estree": "npm:1.0.5" acorn: "npm:^8.10.0" acorn-import-attributes: "npm:^1.9.5" @@ -3989,96 +4009,96 @@ __metadata: picocolors: "npm:^1.1.1" rslog: "npm:^1.2.11" strip-ansi: "npm:^6.0.1" - checksum: 10/9caa8cba7d9970d9ec48e3abd167f1033c381805e6dcc32af607f48dc511f45de44b9e5c9b0ae5f20971bca36283b5b1c141cec5efe6bfdce4c5ab2fd7d63869 + checksum: 10/77eb5830affa65dde1ddd69e6b8994776927c892ef569d19166579e64e6a95b391d04fb69ba70a6872a24acfd3a31c67049023c958a410a317bbf22f0907bc5a languageName: node linkType: hard -"@rspack/binding-darwin-arm64@npm:1.7.2": - version: 1.7.2 - resolution: "@rspack/binding-darwin-arm64@npm:1.7.2" +"@rspack/binding-darwin-arm64@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-darwin-arm64@npm:1.7.5" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rspack/binding-darwin-x64@npm:1.7.2": - version: 1.7.2 - resolution: "@rspack/binding-darwin-x64@npm:1.7.2" +"@rspack/binding-darwin-x64@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-darwin-x64@npm:1.7.5" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rspack/binding-linux-arm64-gnu@npm:1.7.2": - version: 1.7.2 - resolution: "@rspack/binding-linux-arm64-gnu@npm:1.7.2" +"@rspack/binding-linux-arm64-gnu@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-linux-arm64-gnu@npm:1.7.5" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rspack/binding-linux-arm64-musl@npm:1.7.2": - version: 1.7.2 - resolution: "@rspack/binding-linux-arm64-musl@npm:1.7.2" +"@rspack/binding-linux-arm64-musl@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-linux-arm64-musl@npm:1.7.5" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rspack/binding-linux-x64-gnu@npm:1.7.2": - version: 1.7.2 - resolution: "@rspack/binding-linux-x64-gnu@npm:1.7.2" +"@rspack/binding-linux-x64-gnu@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-linux-x64-gnu@npm:1.7.5" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rspack/binding-linux-x64-musl@npm:1.7.2": - version: 1.7.2 - resolution: "@rspack/binding-linux-x64-musl@npm:1.7.2" +"@rspack/binding-linux-x64-musl@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-linux-x64-musl@npm:1.7.5" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rspack/binding-wasm32-wasi@npm:1.7.2": - version: 1.7.2 - resolution: "@rspack/binding-wasm32-wasi@npm:1.7.2" +"@rspack/binding-wasm32-wasi@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-wasm32-wasi@npm:1.7.5" dependencies: "@napi-rs/wasm-runtime": "npm:1.0.7" conditions: cpu=wasm32 languageName: node linkType: hard -"@rspack/binding-win32-arm64-msvc@npm:1.7.2": - version: 1.7.2 - resolution: "@rspack/binding-win32-arm64-msvc@npm:1.7.2" +"@rspack/binding-win32-arm64-msvc@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-win32-arm64-msvc@npm:1.7.5" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rspack/binding-win32-ia32-msvc@npm:1.7.2": - version: 1.7.2 - resolution: "@rspack/binding-win32-ia32-msvc@npm:1.7.2" +"@rspack/binding-win32-ia32-msvc@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-win32-ia32-msvc@npm:1.7.5" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rspack/binding-win32-x64-msvc@npm:1.7.2": - version: 1.7.2 - resolution: "@rspack/binding-win32-x64-msvc@npm:1.7.2" +"@rspack/binding-win32-x64-msvc@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding-win32-x64-msvc@npm:1.7.5" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@rspack/binding@npm:1.7.2": - version: 1.7.2 - resolution: "@rspack/binding@npm:1.7.2" +"@rspack/binding@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/binding@npm:1.7.5" dependencies: - "@rspack/binding-darwin-arm64": "npm:1.7.2" - "@rspack/binding-darwin-x64": "npm:1.7.2" - "@rspack/binding-linux-arm64-gnu": "npm:1.7.2" - "@rspack/binding-linux-arm64-musl": "npm:1.7.2" - "@rspack/binding-linux-x64-gnu": "npm:1.7.2" - "@rspack/binding-linux-x64-musl": "npm:1.7.2" - "@rspack/binding-wasm32-wasi": "npm:1.7.2" - "@rspack/binding-win32-arm64-msvc": "npm:1.7.2" - "@rspack/binding-win32-ia32-msvc": "npm:1.7.2" - "@rspack/binding-win32-x64-msvc": "npm:1.7.2" + "@rspack/binding-darwin-arm64": "npm:1.7.5" + "@rspack/binding-darwin-x64": "npm:1.7.5" + "@rspack/binding-linux-arm64-gnu": "npm:1.7.5" + "@rspack/binding-linux-arm64-musl": "npm:1.7.5" + "@rspack/binding-linux-x64-gnu": "npm:1.7.5" + "@rspack/binding-linux-x64-musl": "npm:1.7.5" + "@rspack/binding-wasm32-wasi": "npm:1.7.5" + "@rspack/binding-win32-arm64-msvc": "npm:1.7.5" + "@rspack/binding-win32-ia32-msvc": "npm:1.7.5" + "@rspack/binding-win32-x64-msvc": "npm:1.7.5" dependenciesMeta: "@rspack/binding-darwin-arm64": optional: true @@ -4100,38 +4120,61 @@ __metadata: optional: true "@rspack/binding-win32-x64-msvc": optional: true - checksum: 10/1bee5d8805033d8991773f581c9ae71fd5dafa86ff67fef855d23f79bdac8f34fcb92dbd79783229b23e9a976591e2d70dfc22dba696dafee0c8dc7cda509082 + checksum: 10/3e66805d4dae5f2051f10c5a9126e5bf25926d2a6ccb1c794af2aa49c15e4fdcb9e362bd015a9afef1e788a3272dfe7a28a3c866713badda34579896d736ed4f languageName: node linkType: hard -"@rspack/core@npm:1.7.2": - version: 1.7.2 - resolution: "@rspack/core@npm:1.7.2" +"@rspack/core@npm:1.7.5": + version: 1.7.5 + resolution: "@rspack/core@npm:1.7.5" dependencies: "@module-federation/runtime-tools": "npm:0.22.0" - "@rspack/binding": "npm:1.7.2" + "@rspack/binding": "npm:1.7.5" "@rspack/lite-tapable": "npm:1.1.0" peerDependencies: "@swc/helpers": ">=0.5.1" peerDependenciesMeta: "@swc/helpers": optional: true - checksum: 10/a84d4882d835e62124c9fd5f10e171f1838e5cdaf17ab8f50b0181fd6faefaeb5e28d705e6b6e09be3768d1c58c12e817f951b1c334d3f45cffa56f98d24a159 + checksum: 10/c17d93ef1e7e0728b74bf527150642d9bf072cabb63964ebd32c82da94d6d2f9eac7ff2cc13031bbd376c0c03710899f549e61d2bcfc0a6638a9f3bc8620b7ce languageName: node linkType: hard -"@rspack/dev-server@npm:1.1.5": - version: 1.1.5 - resolution: "@rspack/dev-server@npm:1.1.5" +"@rspack/dev-server@npm:1.2.1": + version: 1.2.1 + resolution: "@rspack/dev-server@npm:1.2.1" dependencies: + "@types/bonjour": "npm:^3.5.13" + "@types/connect-history-api-fallback": "npm:^1.5.4" + "@types/express": "npm:^4.17.25" + "@types/express-serve-static-core": "npm:^4.17.21" + "@types/serve-index": "npm:^1.9.4" + "@types/serve-static": "npm:^1.15.5" + "@types/sockjs": "npm:^0.3.36" + "@types/ws": "npm:^8.5.10" + ansi-html-community: "npm:^0.0.8" + bonjour-service: "npm:^1.2.1" chokidar: "npm:^3.6.0" + colorette: "npm:^2.0.10" + compression: "npm:^1.8.1" + connect-history-api-fallback: "npm:^2.0.0" + express: "npm:^4.22.1" + graceful-fs: "npm:^4.2.6" http-proxy-middleware: "npm:^2.0.9" + ipaddr.js: "npm:^2.1.0" + launch-editor: "npm:^2.6.1" + open: "npm:^10.0.3" p-retry: "npm:^6.2.0" - webpack-dev-server: "npm:5.2.2" + schema-utils: "npm:^4.2.0" + selfsigned: "npm:^2.4.1" + serve-index: "npm:^1.9.1" + sockjs: "npm:^0.3.24" + spdy: "npm:^4.0.2" + webpack-dev-middleware: "npm:^7.4.2" ws: "npm:^8.18.0" peerDependencies: "@rspack/core": "*" - checksum: 10/67a747d998f9a2449cb1c6c5791ffc812c9d99a7219595359ce960063f344fde9f8f2000bbc9633dc490082f69b74a20b8f319697bd19beca65bd108f5dff5e5 + checksum: 10/154808faef8079dc1d6eae1712455864cc7bc1ec686f3020f7117ad3e5f2906940f27ec514eb40230276132371570ecdf6b47f7ab117ad209462bcba7c2b0692 languageName: node linkType: hard @@ -4431,12 +4474,12 @@ __metadata: linkType: hard "@types/chrome@npm:*": - version: 0.1.32 - resolution: "@types/chrome@npm:0.1.32" + version: 0.1.33 + resolution: "@types/chrome@npm:0.1.33" dependencies: "@types/filesystem": "npm:*" "@types/har-format": "npm:*" - checksum: 10/aba2ddd02625f5f8d66c4b382dda0b90eb0dfa67b54de8f903394cafb12d1dd79637367f2f19e148b8d6351664a4284f2d457664eae2fe864ea17a831294f422 + checksum: 10/abe01ebd235ae7ad6b8b4e54e99b0939ef2ae710e663bcdc3dff2c2ff4c99dc77243aebee9a85c18d73883efd407b55d186b61aad95ccffad7e2be2981ac8d66 languageName: node linkType: hard @@ -4534,26 +4577,26 @@ __metadata: linkType: hard "@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^5.0.0": - version: 5.1.0 - resolution: "@types/express-serve-static-core@npm:5.1.0" + version: 5.1.1 + resolution: "@types/express-serve-static-core@npm:5.1.1" dependencies: "@types/node": "npm:*" "@types/qs": "npm:*" "@types/range-parser": "npm:*" "@types/send": "npm:*" - checksum: 10/c0b5b7ebc15b222f51e5705da2b8a5180335bf70927cc83c065784331aa9291984db1bfa4a14f5ba31b538dcb543561d9280046051fa4c9b7256eb971293e735 + checksum: 10/7f3d8cf7e68764c9f3e8f6a12825b69ccf5287347fc1c20b29803d4f08a4abc1153ae11d7258852c61aad50f62ef72d4c1b9c97092b0a90462c3dddec2f6026c languageName: node linkType: hard "@types/express-serve-static-core@npm:^4.17.21, @types/express-serve-static-core@npm:^4.17.33": - version: 4.19.7 - resolution: "@types/express-serve-static-core@npm:4.19.7" + version: 4.19.8 + resolution: "@types/express-serve-static-core@npm:4.19.8" dependencies: "@types/node": "npm:*" "@types/qs": "npm:*" "@types/range-parser": "npm:*" "@types/send": "npm:*" - checksum: 10/a87830df965fb52eec6390accdba918a6f33f3d6cb96853be2cc2f74829a0bc09a29bddd9699127dbc17a170c7eebbe1294a9db9843b5a34dbc768f9ee844c01 + checksum: 10/eb1b832343c0991395c9b10e124dc805921ea7c08efe01222d83912123b8c054119d009e9e55c91af6bdbeeec153c0d35411c9c6d80781bc8c0a43e8b1a84387 languageName: node linkType: hard @@ -4568,7 +4611,7 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:^4.17.21": +"@types/express@npm:^4.17.25": version: 4.17.25 resolution: "@types/express@npm:4.17.25" dependencies: @@ -4707,9 +4750,9 @@ __metadata: linkType: hard "@types/lodash@npm:*": - version: 4.17.21 - resolution: "@types/lodash@npm:4.17.21" - checksum: 10/34920830a3bc82ba619cda05e606fef00c148a69b4f19f770645d2587ccdb8e42ef3ddfc174b7884c0c709fc0a1aeb48f7326da969bad12a1464a03efbbe414c + version: 4.17.23 + resolution: "@types/lodash@npm:4.17.23" + checksum: 10/05935534a44aadef67c2158b2fb4a042a226970088106a40ddc67e4f063783149fe5cf02279d7dd4a1e72c98d9189b9430face659645dbf77270f8c4c3e387f5 languageName: node linkType: hard @@ -4744,11 +4787,11 @@ __metadata: linkType: hard "@types/node@npm:*, @types/node@npm:>=10.0.0": - version: 25.0.2 - resolution: "@types/node@npm:25.0.2" + version: 25.0.9 + resolution: "@types/node@npm:25.0.9" dependencies: undici-types: "npm:~7.16.0" - checksum: 10/8d37061ed51c273551cca3fe36913a448cc5992b687d7c077a5e99eeb3d0f15a8aca81e53ad3f557dc16f4f4b11877832bb4fbb45f3e162066806da969015251 + checksum: 10/2c16f137ed3a952903b7641df5ce16663ad00911dc6a9010156602251420da127bf7dbd32d22aac9eda0cc430381164d55a36240039b7d06ab693b9aad5c3f23 languageName: node linkType: hard @@ -4949,105 +4992,105 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.53.0": - version: 8.53.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.53.0" +"@typescript-eslint/eslint-plugin@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.54.0" dependencies: "@eslint-community/regexpp": "npm:^4.12.2" - "@typescript-eslint/scope-manager": "npm:8.53.0" - "@typescript-eslint/type-utils": "npm:8.53.0" - "@typescript-eslint/utils": "npm:8.53.0" - "@typescript-eslint/visitor-keys": "npm:8.53.0" + "@typescript-eslint/scope-manager": "npm:8.54.0" + "@typescript-eslint/type-utils": "npm:8.54.0" + "@typescript-eslint/utils": "npm:8.54.0" + "@typescript-eslint/visitor-keys": "npm:8.54.0" ignore: "npm:^7.0.5" natural-compare: "npm:^1.4.0" ts-api-utils: "npm:^2.4.0" peerDependencies: - "@typescript-eslint/parser": ^8.53.0 + "@typescript-eslint/parser": ^8.54.0 eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10/2cbfa92d21018d53b33db102500f121cedd67405939a11c20d04a0fdc535412f1e554479a9994a244127a151609fe16ae8bce810749261f243eac13360df1ab1 + checksum: 10/8f1c74ac77d7a84ae3f201bb09cb67271662befed036266af1eaa0653d09b545353441640516c1c86e0a94939887d32f0473c61a642488b14d46533742bfbd1b languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.53.0": - version: 8.53.0 - resolution: "@typescript-eslint/parser@npm:8.53.0" +"@typescript-eslint/parser@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/parser@npm:8.54.0" dependencies: - "@typescript-eslint/scope-manager": "npm:8.53.0" - "@typescript-eslint/types": "npm:8.53.0" - "@typescript-eslint/typescript-estree": "npm:8.53.0" - "@typescript-eslint/visitor-keys": "npm:8.53.0" + "@typescript-eslint/scope-manager": "npm:8.54.0" + "@typescript-eslint/types": "npm:8.54.0" + "@typescript-eslint/typescript-estree": "npm:8.54.0" + "@typescript-eslint/visitor-keys": "npm:8.54.0" debug: "npm:^4.4.3" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10/5337f472aeb3d04041a3c9c9e9d9884e685ba7e4f722ab2963f1054087a62a42946dd0d39993e60506efef0d2a4cc1b0619b34e49261913d6f4d8cdbf3490d56 + checksum: 10/d2e09462c9966ef3deeba71d9e41d1d4876c61eea65888c93a3db6fba48b89a2165459c6519741d40e969da05ed98d3f4c87a7f56c5521ab5699743cc315f6cb languageName: node linkType: hard -"@typescript-eslint/project-service@npm:8.53.0": - version: 8.53.0 - resolution: "@typescript-eslint/project-service@npm:8.53.0" +"@typescript-eslint/project-service@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/project-service@npm:8.54.0" dependencies: - "@typescript-eslint/tsconfig-utils": "npm:^8.53.0" - "@typescript-eslint/types": "npm:^8.53.0" + "@typescript-eslint/tsconfig-utils": "npm:^8.54.0" + "@typescript-eslint/types": "npm:^8.54.0" debug: "npm:^4.4.3" peerDependencies: typescript: ">=4.8.4 <6.0.0" - checksum: 10/2f232f241f57c0f42194a8bcb8c207e4ed4345d7cc097434d394c2904338e64f386903931395ef97cd2cf3ae33d98645f0d6164660d794e33259e2c3978052ff + checksum: 10/93f0483f6bbcf7cf776a53a130f7606f597fba67cf111e1897873bf1531efaa96e4851cfd461da0f0cc93afbdb51e47bcce11cf7dd4fb68b7030c7f9f240b92f languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.53.0": - version: 8.53.0 - resolution: "@typescript-eslint/scope-manager@npm:8.53.0" +"@typescript-eslint/scope-manager@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/scope-manager@npm:8.54.0" dependencies: - "@typescript-eslint/types": "npm:8.53.0" - "@typescript-eslint/visitor-keys": "npm:8.53.0" - checksum: 10/40a651cfc16f9464f92b5a58492207c1f89a1ff98cfedd2d33d1dbe8234ce50c3a543267f1b489f903b001e0abcaf1568e7c9b70c009871c34af6ef3602ac0bf + "@typescript-eslint/types": "npm:8.54.0" + "@typescript-eslint/visitor-keys": "npm:8.54.0" + checksum: 10/3474f3197e8647754393dee62b3145c9de71eaa66c8a68f61c8283aa332141803885db9c96caa6a51f78128ad9ef92f774a90361655e57bd951d5b57eb76f914 languageName: node linkType: hard -"@typescript-eslint/tsconfig-utils@npm:8.53.0, @typescript-eslint/tsconfig-utils@npm:^8.53.0": - version: 8.53.0 - resolution: "@typescript-eslint/tsconfig-utils@npm:8.53.0" +"@typescript-eslint/tsconfig-utils@npm:8.54.0, @typescript-eslint/tsconfig-utils@npm:^8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.54.0" peerDependencies: typescript: ">=4.8.4 <6.0.0" - checksum: 10/91f1f02ec8a3daf7d3dc9e43a847ef834444a6e073e3a4a07a311d898b225124d9c4abb4b48266d821f0ea4225614266084e5157182e7ba7aaecafefbae00c7e + checksum: 10/e9d6b29538716f007919bfcee94f09b7f8e7d2b684ad43d1a3c8d43afb9f0539c7707f84a34f42054e31c8c056b0ccf06575d89e860b4d34632ffefaefafe1fc languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.53.0": - version: 8.53.0 - resolution: "@typescript-eslint/type-utils@npm:8.53.0" +"@typescript-eslint/type-utils@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/type-utils@npm:8.54.0" dependencies: - "@typescript-eslint/types": "npm:8.53.0" - "@typescript-eslint/typescript-estree": "npm:8.53.0" - "@typescript-eslint/utils": "npm:8.53.0" + "@typescript-eslint/types": "npm:8.54.0" + "@typescript-eslint/typescript-estree": "npm:8.54.0" + "@typescript-eslint/utils": "npm:8.54.0" debug: "npm:^4.4.3" ts-api-utils: "npm:^2.4.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10/5be4036b475bbc4bb9a834beefe8114286bbe2dee54c96c65c02d6ceabac3422605802dcbefdbf20ae9ede3c85bf2f650eda2acc7ed1a3bf75f02ed478e7cdd1 + checksum: 10/60e92fb32274abd70165ce6f4187e4cffa55416374c63731d7de8fdcfb7a558b4dd48909ff1ad38ac39d2ea1248ec54d6ce38dbc065fd34529a217fc2450d5b1 languageName: node linkType: hard -"@typescript-eslint/types@npm:8.53.0, @typescript-eslint/types@npm:^8.53.0": - version: 8.53.0 - resolution: "@typescript-eslint/types@npm:8.53.0" - checksum: 10/36ee696a92ed575385b5c1ccc46e3fec9c5d9aa6f3640f8ad0234ed5a763c9ab78c7d3419fd3d462a966f6b95472390b8040055e4e73c75c52671478e90749ff +"@typescript-eslint/types@npm:8.54.0, @typescript-eslint/types@npm:^8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/types@npm:8.54.0" + checksum: 10/c25cc0bdf90fb150cf6ce498897f43fe3adf9e872562159118f34bd91a9bfab5f720cb1a41f3cdf253b2e840145d7d372089b7cef5156624ef31e98d34f91b31 languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.53.0": - version: 8.53.0 - resolution: "@typescript-eslint/typescript-estree@npm:8.53.0" +"@typescript-eslint/typescript-estree@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.54.0" dependencies: - "@typescript-eslint/project-service": "npm:8.53.0" - "@typescript-eslint/tsconfig-utils": "npm:8.53.0" - "@typescript-eslint/types": "npm:8.53.0" - "@typescript-eslint/visitor-keys": "npm:8.53.0" + "@typescript-eslint/project-service": "npm:8.54.0" + "@typescript-eslint/tsconfig-utils": "npm:8.54.0" + "@typescript-eslint/types": "npm:8.54.0" + "@typescript-eslint/visitor-keys": "npm:8.54.0" debug: "npm:^4.4.3" minimatch: "npm:^9.0.5" semver: "npm:^7.7.3" @@ -5055,149 +5098,149 @@ __metadata: ts-api-utils: "npm:^2.4.0" peerDependencies: typescript: ">=4.8.4 <6.0.0" - checksum: 10/bdacb2f3ffde535c3955bbfbd062d2010943f7693034cde4019ccde699e826e7ef91d7e1d2f3652c30584c013924410dae5056417909e8169f1e3d7272636bd9 + checksum: 10/3a545037c6f9319251d3ba44cf7a3216b1372422469e27f7ed3415244ebf42553da1ab4644da42d3f0ae2706a8cad12529ffebcb2e75406f74e3b30b812d010d languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.53.0": - version: 8.53.0 - resolution: "@typescript-eslint/utils@npm:8.53.0" +"@typescript-eslint/utils@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/utils@npm:8.54.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.9.1" - "@typescript-eslint/scope-manager": "npm:8.53.0" - "@typescript-eslint/types": "npm:8.53.0" - "@typescript-eslint/typescript-estree": "npm:8.53.0" + "@typescript-eslint/scope-manager": "npm:8.54.0" + "@typescript-eslint/types": "npm:8.54.0" + "@typescript-eslint/typescript-estree": "npm:8.54.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10/ef123c8531de793d8d4f5fa51076402bfe809481feaee605086986c370c94361c525ec550b2c4c6703cf60e026e87862428c044c763ead3ea9bf9bce8ad79310 + checksum: 10/9f88a2a7ab3e11aa0ff7f99c0e66a0cf2cba10b640def4c64a4f4ef427fecfb22f28dbe5697535915eb01f6507515ac43e45e0ff384bf82856e3420194d9ffdd languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.53.0": - version: 8.53.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.53.0" +"@typescript-eslint/visitor-keys@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.54.0" dependencies: - "@typescript-eslint/types": "npm:8.53.0" + "@typescript-eslint/types": "npm:8.54.0" eslint-visitor-keys: "npm:^4.2.1" - checksum: 10/879e1dfbd002059c0eb59f9660c26eb71a1643622906e4af444dbe5297e95ad210d763b53308b6372b55d85159a161982a8848352706a7d361fd3e17d6ba96d0 + checksum: 10/cca5380ee30250302ee1459e5a0a38de8c16213026dbbff3d167fa7d71d012f31d60ac4483ad45ebd13f2ac963d1ca52dd5f22759a68d4ee57626e421769187a languageName: node linkType: hard -"@vibrant/color@npm:4.0.0, @vibrant/color@npm:^4.0.0": - version: 4.0.0 - resolution: "@vibrant/color@npm:4.0.0" - checksum: 10/deaa1f29140363208f7d210af31320c9cb8fa45f8db08a9d889260b4af7d420031b7bfc53a7731d847fdbb5778a05a7a78cacf56e5f8bfce28db74beed88acfd +"@vibrant/color@npm:4.0.4, @vibrant/color@npm:^4.0.4": + version: 4.0.4 + resolution: "@vibrant/color@npm:4.0.4" + checksum: 10/5b73ef84cd311d5823007eb1f0d6da23e32ea8d732a02e7b6e5bd6c03652c52c67dc6b58c3e5adde196c3d9f7e855f7a8e310f81cdc4251acc61a52ba84266d3 languageName: node linkType: hard -"@vibrant/core@npm:^4.0.0": - version: 4.0.0 - resolution: "@vibrant/core@npm:4.0.0" +"@vibrant/core@npm:^4.0.4": + version: 4.0.4 + resolution: "@vibrant/core@npm:4.0.4" dependencies: - "@vibrant/color": "npm:^4.0.0" - "@vibrant/generator": "npm:^4.0.0" - "@vibrant/image": "npm:^4.0.0" - "@vibrant/quantizer": "npm:^4.0.0" - "@vibrant/worker": "npm:^4.0.0" - checksum: 10/ed88663d56fc47bc8a57c0b338b2e5cb534ac8c8a9a9810167a18d252445587a26f973274e751738a1e11613826083050527e4930b7b0bae8b42dd3d2b0176a4 + "@vibrant/color": "npm:^4.0.4" + "@vibrant/generator": "npm:^4.0.4" + "@vibrant/image": "npm:^4.0.4" + "@vibrant/quantizer": "npm:^4.0.4" + "@vibrant/worker": "npm:^4.0.4" + checksum: 10/07400dba13f4d0c203d1facee0b77c40de5d0e6387a12989297d63bb50fa4d94f04b6a4739bab7a1da0f63f65d7892603ee3664029bb741be94dfaf09b7a846e languageName: node linkType: hard -"@vibrant/generator-default@npm:^4.0.3": - version: 4.0.3 - resolution: "@vibrant/generator-default@npm:4.0.3" +"@vibrant/generator-default@npm:^4.0.4": + version: 4.0.4 + resolution: "@vibrant/generator-default@npm:4.0.4" dependencies: - "@vibrant/color": "npm:^4.0.0" - "@vibrant/generator": "npm:^4.0.0" - checksum: 10/277b579f2fe660fe65bd75aa5b984f23bc91cc5a1fcc922398191b7771f14ee1ac6a162168bb9e199b69af968da98e9621e3e30f9d61bb39670cad1d806ca945 + "@vibrant/color": "npm:^4.0.4" + "@vibrant/generator": "npm:^4.0.4" + checksum: 10/57def9e7b2c06dd420479cbdd5c4545924ed1d29c6633320a133049444071cc93183714f860a2a25202884c432fdef3ad56ab11ca86d272f6313cdf4bb194e31 languageName: node linkType: hard -"@vibrant/generator@npm:^4.0.0": - version: 4.0.0 - resolution: "@vibrant/generator@npm:4.0.0" +"@vibrant/generator@npm:^4.0.4": + version: 4.0.4 + resolution: "@vibrant/generator@npm:4.0.4" dependencies: - "@vibrant/color": "npm:^4.0.0" - "@vibrant/types": "npm:^4.0.0" - checksum: 10/c92cb9fae5b07bf6d987adfb8725f5c916fe37aa91e8a2448d4e65235e324dcd09088e970f1c662dc52dae1218ba753fc55178bf658a015c960c34764b19a3bc + "@vibrant/color": "npm:^4.0.4" + "@vibrant/types": "npm:^4.0.4" + checksum: 10/dd3fd0af3ac4278a08eb76538c84d931b3327d86a8a46089963caf33bddb7daea077991c1ce185d7cc93bcb1ddba1207072c6fc6f3f9777ae2ab3e357286c0d9 languageName: node linkType: hard -"@vibrant/image-browser@npm:^4.0.0": - version: 4.0.0 - resolution: "@vibrant/image-browser@npm:4.0.0" +"@vibrant/image-browser@npm:^4.0.4": + version: 4.0.4 + resolution: "@vibrant/image-browser@npm:4.0.4" dependencies: - "@vibrant/image": "npm:^4.0.0" - checksum: 10/8c7e0ba6e8edd1fd9b63cecad4ee16e2765bc7251b55ba1858ea4a38184cfa18eda39f29a3f3d1c7af80738d98d9cb390c2781f3d89042aed7a3b9d9dbca87fd + "@vibrant/image": "npm:^4.0.4" + checksum: 10/306ab2ee5dcc08960d167cbcbbdd1229c61cff731e06dc9fc80b93162f5d9973e392992234577320253424304964e5abc138cb213873bd31854e4d3d017c64d2 languageName: node linkType: hard -"@vibrant/image-node@npm:^4.0.0": - version: 4.0.0 - resolution: "@vibrant/image-node@npm:4.0.0" +"@vibrant/image-node@npm:^4.0.4": + version: 4.0.4 + resolution: "@vibrant/image-node@npm:4.0.4" dependencies: "@jimp/custom": "npm:^0.22.12" "@jimp/plugin-resize": "npm:^0.22.12" "@jimp/types": "npm:^0.22.12" - "@vibrant/image": "npm:^4.0.0" - checksum: 10/aec3c72c49517c9584a2ca8fb4280c85ea4404bf6bef2691d6df87eedb27e66b0b47bae18dd11b3f6bf3997a699349818fb7a8262303473573370bacfc9f1921 + "@vibrant/image": "npm:^4.0.4" + checksum: 10/6dec4e01ca8d374d293dfb10b57f0751e4b031e7ea8bc13c59afd566862e6e4ae4d547565b6e8f7a0a2eaf238e167251cc470fb772b50a839733c24c8bc1f5e1 languageName: node linkType: hard -"@vibrant/image@npm:^4.0.0": - version: 4.0.0 - resolution: "@vibrant/image@npm:4.0.0" +"@vibrant/image@npm:^4.0.4": + version: 4.0.4 + resolution: "@vibrant/image@npm:4.0.4" dependencies: - "@vibrant/color": "npm:^4.0.0" - checksum: 10/b2eb6a6147fd783e2ea25572a19089ac39c701bf3e94677a059bfb887ee066f8654857e573cc25f5f98de4ad73f916e9af49d465ebfc8d6e443261b37c2f9a4a + "@vibrant/color": "npm:^4.0.4" + checksum: 10/c5f8cd4431a93a96652a3e06ece45e3d352e57518c11af40546e3bc46b131788833190b6579fcdd5e6a6704cab11012c875e5fb0882f0f1322110d6e46021614 languageName: node linkType: hard -"@vibrant/quantizer-mmcq@npm:^4.0.0": - version: 4.0.0 - resolution: "@vibrant/quantizer-mmcq@npm:4.0.0" +"@vibrant/quantizer-mmcq@npm:^4.0.4": + version: 4.0.4 + resolution: "@vibrant/quantizer-mmcq@npm:4.0.4" dependencies: - "@vibrant/color": "npm:^4.0.0" - "@vibrant/image": "npm:^4.0.0" - "@vibrant/quantizer": "npm:^4.0.0" - checksum: 10/bc165c5819b472926bf19a13efc9c2fe85da6b05a9fb4f0c7052a3ee3aa88ba551775a2985ef89cb1fc3431d8831c217f87abb4f6b510d888395ce3fd1c06139 + "@vibrant/color": "npm:^4.0.4" + "@vibrant/image": "npm:^4.0.4" + "@vibrant/quantizer": "npm:^4.0.4" + checksum: 10/c718c349d684164aa17f23250aca1aba8d7a1c5ec51296483d3ca8aae56937e194aa234b656b942b77228f3333bc7bee044a2720e5b49387cd9ee93a836e9ec2 languageName: node linkType: hard -"@vibrant/quantizer@npm:^4.0.0": - version: 4.0.0 - resolution: "@vibrant/quantizer@npm:4.0.0" +"@vibrant/quantizer@npm:^4.0.4": + version: 4.0.4 + resolution: "@vibrant/quantizer@npm:4.0.4" dependencies: - "@vibrant/color": "npm:^4.0.0" - "@vibrant/image": "npm:^4.0.0" - "@vibrant/types": "npm:^4.0.0" - checksum: 10/ec658b17cd1bdcc17624897e73e8f0add8be8dd50bebc3abb7776062d978910355f8320f514431fb128142307510620df74aa2715e01fd653d4d0b02ecc9bd9a + "@vibrant/color": "npm:^4.0.4" + "@vibrant/image": "npm:^4.0.4" + "@vibrant/types": "npm:^4.0.4" + checksum: 10/53ac0471249ad57fb97e95c7085f2acc0676e39d7ac94f95a6d4e03cc2adb27dcec13bd0d22d58422bd0e2e4172c6a3f1e9fb3861e361848a1ff7a4cad81438e languageName: node linkType: hard -"@vibrant/types@npm:^4.0.0": - version: 4.0.0 - resolution: "@vibrant/types@npm:4.0.0" - checksum: 10/7e04b385469a3be1be37d5e43e988fd718a1477c1bf65d79d8ef0cd96d4f2b41941393efc6b10a12f1616fe5738010d5fab9593f8c6e0cf48c26782a6a24246f +"@vibrant/types@npm:^4.0.4": + version: 4.0.4 + resolution: "@vibrant/types@npm:4.0.4" + checksum: 10/249d6112719267a84bc6782fa7cfc065746a98d29b58719c2e647d0c71b064c5c1af3bbce7b233b1d1c78f4f05a887596c0d69f39cd1001e0d3e9d4ea2d5cb0f languageName: node linkType: hard -"@vibrant/worker@npm:^4.0.0": - version: 4.0.0 - resolution: "@vibrant/worker@npm:4.0.0" +"@vibrant/worker@npm:^4.0.4": + version: 4.0.4 + resolution: "@vibrant/worker@npm:4.0.4" dependencies: - "@vibrant/types": "npm:^4.0.0" - checksum: 10/244204f7881507dd37f6b1f4e77db017a8a4a79d688172fe62baf6e8efe61792b33cf66e17f813fe763539236657378a94f183b5b06e85360779c3492d70315c + "@vibrant/types": "npm:^4.0.4" + checksum: 10/5bddebb82b2ab1602cd0821f5ce4354f187809bf7ac61b1459ce3ca9da1301a6ace0c469cabd60390d2e5864244254335c2eb33dac10d6422576f20e8a87d39b languageName: node linkType: hard -"@vitest/coverage-v8@npm:4.0.17": - version: 4.0.17 - resolution: "@vitest/coverage-v8@npm:4.0.17" +"@vitest/coverage-v8@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/coverage-v8@npm:4.0.18" dependencies: "@bcoe/v8-coverage": "npm:^1.0.2" - "@vitest/utils": "npm:4.0.17" + "@vitest/utils": "npm:4.0.18" ast-v8-to-istanbul: "npm:^0.3.10" istanbul-lib-coverage: "npm:^3.2.2" istanbul-lib-report: "npm:^3.0.1" @@ -5207,34 +5250,34 @@ __metadata: std-env: "npm:^3.10.0" tinyrainbow: "npm:^3.0.3" peerDependencies: - "@vitest/browser": 4.0.17 - vitest: 4.0.17 + "@vitest/browser": 4.0.18 + vitest: 4.0.18 peerDependenciesMeta: "@vitest/browser": optional: true - checksum: 10/aab6340670dbf42a5bf4a28b49a4d4c8819e842edac45567bae50af27b9e89264406945e57dd115b833190a6c25ba8f716c2eabaa23d2e249a185e3acc97ec1a + checksum: 10/33bd54aa8ea1c4b0acae77722b34460408325793d4d74159f7d73aedf2d1c4aa940c8666baf31c4b19b3760d68bbc268dd8c9265ebc2088cece428d26568afb4 languageName: node linkType: hard -"@vitest/expect@npm:4.0.17": - version: 4.0.17 - resolution: "@vitest/expect@npm:4.0.17" +"@vitest/expect@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/expect@npm:4.0.18" dependencies: "@standard-schema/spec": "npm:^1.0.0" "@types/chai": "npm:^5.2.2" - "@vitest/spy": "npm:4.0.17" - "@vitest/utils": "npm:4.0.17" + "@vitest/spy": "npm:4.0.18" + "@vitest/utils": "npm:4.0.18" chai: "npm:^6.2.1" tinyrainbow: "npm:^3.0.3" - checksum: 10/f260fefea527aae652be8d71ff188d45f958b7299a4577d1c3ed15bc87e6b20a6abb30ec6419c826259863d8bdbc1122e82cc499fb9eb63aaa43d3a5be1b7f76 + checksum: 10/2115bff1bbcad460ce72032022e4dbcf8572c4b0fe07ca60f5644a8d96dd0dfa112986b5a1a5c5705f4548119b3b829c45d1de0838879211e0d6bb276b4ece73 languageName: node linkType: hard -"@vitest/mocker@npm:4.0.17": - version: 4.0.17 - resolution: "@vitest/mocker@npm:4.0.17" +"@vitest/mocker@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/mocker@npm:4.0.18" dependencies: - "@vitest/spy": "npm:4.0.17" + "@vitest/spy": "npm:4.0.18" estree-walker: "npm:^3.0.3" magic-string: "npm:^0.30.21" peerDependencies: @@ -5245,54 +5288,54 @@ __metadata: optional: true vite: optional: true - checksum: 10/4d938c298dd7e63d23efc56a81e254a8a453b0157b378d4b7af57a40dd2687c24a0e1f2e2499f8d17fe302e6d6d515e67c6a5fbfbff75dee2cfd51c37cf4c7dc + checksum: 10/46f584a4c1180dfb513137bc8db6e2e3b53e141adfe964307297e98321652d86a3f2a52d80cda1f810205bd5fdcab789bb8b52a532e68f175ef1e20be398218d languageName: node linkType: hard -"@vitest/pretty-format@npm:4.0.17": - version: 4.0.17 - resolution: "@vitest/pretty-format@npm:4.0.17" +"@vitest/pretty-format@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/pretty-format@npm:4.0.18" dependencies: tinyrainbow: "npm:^3.0.3" - checksum: 10/e50925f44168b8108a5094e44fd739b7183457c101eb020e88b5556a2f857808d0c9d045113aec83815a20d4aaaf9b7a522a1c651ce111de18daa686891b37a0 + checksum: 10/4cafc7c9853097345bd94e8761bf47c2c04e00d366ac56d79928182787ff83c512c96f1dc2ce9b6aeed4d3a8c23ce12254da203783108d3c096bc398eed2a62d languageName: node linkType: hard -"@vitest/runner@npm:4.0.17": - version: 4.0.17 - resolution: "@vitest/runner@npm:4.0.17" +"@vitest/runner@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/runner@npm:4.0.18" dependencies: - "@vitest/utils": "npm:4.0.17" + "@vitest/utils": "npm:4.0.18" pathe: "npm:^2.0.3" - checksum: 10/75c62ac09b506d2707baad72c9a8ca6addb9bb179548d9ec9af3f7f2303b2e03f4001480c9657325718b15f2997fc39168c027d8d88794c0f8c04800c640c055 + checksum: 10/d7deebf086d7e084f449733ecea6c9c81737a18aafece318cbe7500e45debea00fa9dbf9315fd38aa88550dd5240a791b885ac71665f89b154d71a6c63da5836 languageName: node linkType: hard -"@vitest/snapshot@npm:4.0.17": - version: 4.0.17 - resolution: "@vitest/snapshot@npm:4.0.17" +"@vitest/snapshot@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/snapshot@npm:4.0.18" dependencies: - "@vitest/pretty-format": "npm:4.0.17" + "@vitest/pretty-format": "npm:4.0.18" magic-string: "npm:^0.30.21" pathe: "npm:^2.0.3" - checksum: 10/0cda8970f484bdc5777347cc317f020dc7773ddf0cea996ab5fff453966310c64e9a97854b04998cf0635e8118c12e2235c7a5f921fdfc288dc63dc27c3116d8 + checksum: 10/50aa5fb7fca45c499c145cc2f20e53b8afb0990b53ff4a4e6447dd6f147437edc5316f22e2d82119e154c3cf7c59d44898e7b2faf7ba614ac1051cbe4d662a77 languageName: node linkType: hard -"@vitest/spy@npm:4.0.17": - version: 4.0.17 - resolution: "@vitest/spy@npm:4.0.17" - checksum: 10/23313980c512b00c08a1c64f6ed15dc7c295bb7b09feab571a3cc96536de2f07432109256717f9deb7f1b8c9ba9ac28f7e617cf639654bc564f6ea5a341ad8f4 +"@vitest/spy@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/spy@npm:4.0.18" + checksum: 10/f7b1618ae13790105771dd2a8c973c63c018366fcc69b50f15ce5d12f9ac552efd3c1e6e5ae4ebdb6023d0b8d8f31fef2a0b1b77334284928db45c80c63de456 languageName: node linkType: hard -"@vitest/utils@npm:4.0.17": - version: 4.0.17 - resolution: "@vitest/utils@npm:4.0.17" +"@vitest/utils@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/utils@npm:4.0.18" dependencies: - "@vitest/pretty-format": "npm:4.0.17" + "@vitest/pretty-format": "npm:4.0.18" tinyrainbow: "npm:^3.0.3" - checksum: 10/b8b96f8c2c4fee13f4ef4927e56bbf98c2d4f3a61428d9721c5578c96e2a0953892dfccfad3e0c1a7b3105e3d24f93f826f8338c82c72b9f8bc32b50bc9072a1 + checksum: 10/e8b2ad7bc35b2bc5590f9dc1d1a67644755da416b47ab7099a6f26792903fa0aacb81e6ba99f0f03858d9d3a1d76eeba65150a1a0849690a40817424e749c367 languageName: node linkType: hard @@ -5843,9 +5886,9 @@ __metadata: linkType: hard "axe-core@npm:^4.3.3": - version: 4.11.0 - resolution: "axe-core@npm:4.11.0" - checksum: 10/18254ee95bc328aec9a909b22e4b22e8ff14a21363fdbd1a5227267e66bf1d2fc1425c186e9001759aab5827cf4ee9dc30f7ea57e8200cbf7a1cd555ed21a908 + version: 4.11.1 + resolution: "axe-core@npm:4.11.1" + checksum: 10/bbc8e8959258a229b92fbaa73437050825579815051cac7b0fdbb6752946fea226e403bfeeef3d60d712477bdd4c01afdc8455f27c3d85e4251df88b032b6250 languageName: node linkType: hard @@ -5873,16 +5916,16 @@ __metadata: languageName: node linkType: hard -"babel-plugin-polyfill-corejs2@npm:^0.4.14": - version: 0.4.14 - resolution: "babel-plugin-polyfill-corejs2@npm:0.4.14" +"babel-plugin-polyfill-corejs2@npm:^0.4.14, babel-plugin-polyfill-corejs2@npm:^0.4.15": + version: 0.4.15 + resolution: "babel-plugin-polyfill-corejs2@npm:0.4.15" dependencies: - "@babel/compat-data": "npm:^7.27.7" - "@babel/helper-define-polyfill-provider": "npm:^0.6.5" + "@babel/compat-data": "npm:^7.28.6" + "@babel/helper-define-polyfill-provider": "npm:^0.6.6" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10/8ec00a1b821ccbfcc432630da66e98bc417f5301f4ce665269d50d245a18ad3ce8a8af2a007f28e3defcd555bb8ce65f16b0d4b6d131bd788e2b97d8b8953332 + checksum: 10/e5f8a4e716400b2b5c51f7b3c0eec58da92f1d8cc1c6fe2e32555c98bc594be1de7fa1da373f8e42ab098c33867c4cc2931ce648c92aab7a4f4685417707c438 languageName: node linkType: hard @@ -5898,14 +5941,26 @@ __metadata: languageName: node linkType: hard -"babel-plugin-polyfill-regenerator@npm:^0.6.5": - version: 0.6.5 - resolution: "babel-plugin-polyfill-regenerator@npm:0.6.5" +"babel-plugin-polyfill-corejs3@npm:^0.14.0": + version: 0.14.0 + resolution: "babel-plugin-polyfill-corejs3@npm:0.14.0" dependencies: - "@babel/helper-define-polyfill-provider": "npm:^0.6.5" + "@babel/helper-define-polyfill-provider": "npm:^0.6.6" + core-js-compat: "npm:^3.48.0" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10/ed1932fa9a31e0752fd10ebf48ab9513a654987cab1182890839523cb898559d24ae0578fdc475d9f995390420e64eeaa4b0427045b56949dace3c725bc66dbb + checksum: 10/09c854a3bda9a930fbce4b80d52a24e5b0744fccb0c81bf8f470d62296f197a2afe111b2b9ecb0d8a47068de2f938d14b748295953377e47594b0673d53c9396 + languageName: node + linkType: hard + +"babel-plugin-polyfill-regenerator@npm:^0.6.5, babel-plugin-polyfill-regenerator@npm:^0.6.6": + version: 0.6.6 + resolution: "babel-plugin-polyfill-regenerator@npm:0.6.6" + dependencies: + "@babel/helper-define-polyfill-provider": "npm:^0.6.6" + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: 10/8de7ea32856e75784601cacf8f4e3cbf04ce1fd05d56614b08b7bbe0674d1e59e37ccaa1c7ed16e3b181a63abe5bd43a1ab0e28b8c95618a9ebf0be5e24d6b25 languageName: node linkType: hard @@ -5974,11 +6029,11 @@ __metadata: linkType: hard "baseline-browser-mapping@npm:^2.9.0": - version: 2.9.8 - resolution: "baseline-browser-mapping@npm:2.9.8" + version: 2.9.14 + resolution: "baseline-browser-mapping@npm:2.9.14" bin: baseline-browser-mapping: dist/cli.js - checksum: 10/1c9cf2fb0cc008ba86d5f1bd1f1a771d6f508dbd6124d173549c650734d6d305eba49622e8bc0160beea9ecafcd2f5f104ffdc92f4a815a4926b74a2d7a6d986 + checksum: 10/a329881e5f673c0834843640e9c954c478f643fb983449c99850392e48cf52dfb1dc3de8d81c6a6a2802c86310833accc5e3deb6bef5fb6e329989e28ca5489b languageName: node linkType: hard @@ -6125,12 +6180,12 @@ __metadata: languageName: node linkType: hard -"browserslist-to-es-version@npm:^1.1.1": - version: 1.2.0 - resolution: "browserslist-to-es-version@npm:1.2.0" +"browserslist-to-es-version@npm:^1.2.0": + version: 1.3.0 + resolution: "browserslist-to-es-version@npm:1.3.0" dependencies: - browserslist: "npm:^4.26.2" - checksum: 10/a9af676f4b736fc17cd42cbc98edc735e7f1e775ccb71443afc5ec7184203669f4c6c37ec2d73964c598d7f8fa42ae6ce2ec0e9a9c64fcb1bc297110dbfbd0c9 + browserslist: "npm:^4.28.1" + checksum: 10/31e76428d0c522dbebd099d8d195687782f93e4db8287fa12a9bc6a55c62cdab3205232a0e5c6e7be50593c8188526fdca4ca5984b0028e14ed69232193c5e1d languageName: node linkType: hard @@ -6152,7 +6207,7 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.24.0, browserslist@npm:^4.26.2, browserslist@npm:^4.28.0": +"browserslist@npm:^4.24.0, browserslist@npm:^4.28.1": version: 4.28.1 resolution: "browserslist@npm:4.28.1" dependencies: @@ -6307,16 +6362,16 @@ __metadata: linkType: hard "caniuse-lite@npm:^1.0.30001759": - version: 1.0.30001760 - resolution: "caniuse-lite@npm:1.0.30001760" - checksum: 10/ac5c13d00b946c3ace331d25379ed9d85743b1028aba79ae80402cb128b4b491122585144898fefb836d4d9879c13c717a081ae241a00420cbbc7e1b934ec685 + version: 1.0.30001764 + resolution: "caniuse-lite@npm:1.0.30001764" + checksum: 10/24c6f402902181faa997a6da1cb63410f9376e9e8a33d733121862e7665d200a54d70e551c5626748f78078401c0744496a58d0451fceb8f7fa12498ae12ff20 languageName: node linkType: hard "chai@npm:^6.2.1": - version: 6.2.1 - resolution: "chai@npm:6.2.1" - checksum: 10/f7917749e2468bd3a17ee4769b680e440002960c1294dd11c6d3ad102b5db9ea1a43e3ad9462b7b0f1502e5c845a6e39ce63db9de1def782e44652018c48acb7 + version: 6.2.2 + resolution: "chai@npm:6.2.2" + checksum: 10/13cda42cc40aa46da04a41cf7e5c61df6b6ae0b4e8a8c8b40e04d6947e4d7951377ea8c14f9fa7fe5aaa9e8bd9ba414f11288dc958d4cee6f5221b9436f2778f languageName: node linkType: hard @@ -6623,7 +6678,7 @@ __metadata: languageName: node linkType: hard -"compression@npm:1.8.1, compression@npm:^1.7.4": +"compression@npm:1.8.1, compression@npm:^1.8.1": version: 1.8.1 resolution: "compression@npm:1.8.1" dependencies: @@ -6720,19 +6775,19 @@ __metadata: languageName: node linkType: hard -"core-js-compat@npm:^3.43.0": - version: 3.47.0 - resolution: "core-js-compat@npm:3.47.0" +"core-js-compat@npm:^3.43.0, core-js-compat@npm:^3.48.0": + version: 3.48.0 + resolution: "core-js-compat@npm:3.48.0" dependencies: - browserslist: "npm:^4.28.0" - checksum: 10/8555ac0aede2e61e3b37c50d31a9d63bb59e96ef76194bea0521d2778b24d8b20b45bed7bf2fce9df9856872a1c31e03fec1da101507b4dbaba669693dc95f94 + browserslist: "npm:^4.28.1" + checksum: 10/83c326dcfef5e174fd3f8f33c892c66e06d567ce27f323a1197a6c280c0178fe18d3e9c5fb95b00c18b98d6c53fba5c646def5fedaa77310a4297d16dfbe2029 languageName: node linkType: hard -"core-js@npm:3.47.0": - version: 3.47.0 - resolution: "core-js@npm:3.47.0" - checksum: 10/c02dc6a091c7e6799e3527dc06a428c44bbcff7f8f6ee700ff818b90aa2ebaf1f17b0234146e692811da97cda5a39a6095ecadec9fd1a74b1135103eb0e96cb1 +"core-js@npm:3.48.0": + version: 3.48.0 + resolution: "core-js@npm:3.48.0" + checksum: 10/08bb3cc9b3225b905e72370c18257a14bb5563946d9eb7496799e0ee4f13231768b980ffe98434df7dbd0f8209bd2c19519938a2fa94846b2c82c2d5aa804037 languageName: node linkType: hard @@ -6802,14 +6857,15 @@ __metadata: languageName: node linkType: hard -"cssstyle@npm:^5.3.4": - version: 5.3.4 - resolution: "cssstyle@npm:5.3.4" +"cssstyle@npm:^5.3.7": + version: 5.3.7 + resolution: "cssstyle@npm:5.3.7" dependencies: - "@asamuzakjp/css-color": "npm:^4.1.0" - "@csstools/css-syntax-patches-for-csstree": "npm:1.0.14" + "@asamuzakjp/css-color": "npm:^4.1.1" + "@csstools/css-syntax-patches-for-csstree": "npm:^1.0.21" css-tree: "npm:^3.1.0" - checksum: 10/4eeb85cbaba47c2e4eb9f9a9dea9633e311f1cb706258f582fc8d099edc0b8c0deea93a6943981960d7f920ac0f3bbb5277cc4c355915406c17a95d0049a073d + lru-cache: "npm:^11.2.4" + checksum: 10/bd4469af81f068537dbbce53c4247f192e91202c19abc066b77b4ee7bbf256526bc82471198bec762ac70ea53ce17b8044aec69fd7982d2d0fd9fd7780329e2d languageName: node linkType: hard @@ -6827,13 +6883,13 @@ __metadata: languageName: node linkType: hard -"data-urls@npm:^6.0.0": - version: 6.0.0 - resolution: "data-urls@npm:6.0.0" +"data-urls@npm:^7.0.0": + version: 7.0.0 + resolution: "data-urls@npm:7.0.0" dependencies: - whatwg-mimetype: "npm:^4.0.0" - whatwg-url: "npm:^15.0.0" - checksum: 10/a47f0dde184337c4f168d455aedf0b486fed87b6ca583b4b9ad55d1515f4836b418d4bdc5b5b6fc55e321feb826029586a0d47e1c9a9e7ac4d52a78faceb7fb0 + whatwg-mimetype: "npm:^5.0.0" + whatwg-url: "npm:^16.0.0" + checksum: 10/60f88ded4306aea5d6251c4db100ca272fc026014004d68aad4db495397a73bb39d17a6bd29ed9ab348c88a28f6e97266a1759985df4e12dc8c02bb8544c7731 languageName: node linkType: hard @@ -6886,7 +6942,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.4.1, debug@npm:^4.4.3": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.4.3, debug@npm:~4.4.1": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -6907,7 +6963,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:~4.3.1, debug@npm:~4.3.2, debug@npm:~4.3.4": +"debug@npm:~4.3.2": version: 4.3.7 resolution: "debug@npm:4.3.7" dependencies: @@ -6926,7 +6982,7 @@ __metadata: languageName: node linkType: hard -"decimal.js@npm:^10.4.3, decimal.js@npm:^10.6.0": +"decimal.js@npm:^10.6.0": version: 10.6.0 resolution: "decimal.js@npm:10.6.0" checksum: 10/c0d45842d47c311d11b38ce7ccc911121953d4df3ebb1465d92b31970eb4f6738a065426a06094af59bee4b0d64e42e7c8984abd57b6767c64ea90cf90bb4a69 @@ -7115,9 +7171,9 @@ __metadata: linkType: hard "diff@npm:^8.0.2": - version: 8.0.2 - resolution: "diff@npm:8.0.2" - checksum: 10/82a2120d3418f97822e17a6044ccd4b99a91e26e145e8698353673d7146bd2d092bbebb79c112aae7badc7b9c526f9098cbe342f96174feb6beabdd2587b3c42 + version: 8.0.3 + resolution: "diff@npm:8.0.3" + checksum: 10/52f957e1fa53db4616ff5f4811b92b22b97a160c12a2f86f22debd4181227b0f6751aa8fd711d6a8fcf4618acb13b86bc702e6d9d6d6ed82acfd00c9cb26ace2 languageName: node linkType: hard @@ -7342,8 +7398,8 @@ __metadata: linkType: hard "engine.io@npm:~6.6.0": - version: 6.6.4 - resolution: "engine.io@npm:6.6.4" + version: 6.6.5 + resolution: "engine.io@npm:6.6.5" dependencies: "@types/cors": "npm:^2.8.12" "@types/node": "npm:>=10.0.0" @@ -7351,10 +7407,10 @@ __metadata: base64id: "npm:2.0.0" cookie: "npm:~0.7.2" cors: "npm:~2.8.5" - debug: "npm:~4.3.1" + debug: "npm:~4.4.1" engine.io-parser: "npm:~5.2.1" - ws: "npm:~8.17.1" - checksum: 10/005b43b392d5b4b9bb196d1ae2a8cc1334a7dc70af3cfb50627d257de407ca1afae725fcd8571f9621cd12ed437abaac819c64cf22f09d5ae02b954a7e7bf4f8 + ws: "npm:~8.18.3" + checksum: 10/d48f8c4240185c018c4d5608fa1641dbd640c10dda7ae24cdca57c5e6938e47bead110f1435925822923444590d2b63c7aebe43149fe9978714fee960923a23b languageName: node linkType: hard @@ -7556,7 +7612,7 @@ __metadata: languageName: node linkType: hard -"es-toolkit@npm:^1.41.0": +"es-toolkit@npm:^1.43.0": version: 1.43.0 resolution: "es-toolkit@npm:1.43.0" dependenciesMeta: @@ -7569,35 +7625,35 @@ __metadata: linkType: hard "esbuild@npm:^0.27.0": - version: 0.27.1 - resolution: "esbuild@npm:0.27.1" + version: 0.27.2 + resolution: "esbuild@npm:0.27.2" dependencies: - "@esbuild/aix-ppc64": "npm:0.27.1" - "@esbuild/android-arm": "npm:0.27.1" - "@esbuild/android-arm64": "npm:0.27.1" - "@esbuild/android-x64": "npm:0.27.1" - "@esbuild/darwin-arm64": "npm:0.27.1" - "@esbuild/darwin-x64": "npm:0.27.1" - "@esbuild/freebsd-arm64": "npm:0.27.1" - "@esbuild/freebsd-x64": "npm:0.27.1" - "@esbuild/linux-arm": "npm:0.27.1" - "@esbuild/linux-arm64": "npm:0.27.1" - "@esbuild/linux-ia32": "npm:0.27.1" - "@esbuild/linux-loong64": "npm:0.27.1" - "@esbuild/linux-mips64el": "npm:0.27.1" - "@esbuild/linux-ppc64": "npm:0.27.1" - "@esbuild/linux-riscv64": "npm:0.27.1" - "@esbuild/linux-s390x": "npm:0.27.1" - "@esbuild/linux-x64": "npm:0.27.1" - "@esbuild/netbsd-arm64": "npm:0.27.1" - "@esbuild/netbsd-x64": "npm:0.27.1" - "@esbuild/openbsd-arm64": "npm:0.27.1" - "@esbuild/openbsd-x64": "npm:0.27.1" - "@esbuild/openharmony-arm64": "npm:0.27.1" - "@esbuild/sunos-x64": "npm:0.27.1" - "@esbuild/win32-arm64": "npm:0.27.1" - "@esbuild/win32-ia32": "npm:0.27.1" - "@esbuild/win32-x64": "npm:0.27.1" + "@esbuild/aix-ppc64": "npm:0.27.2" + "@esbuild/android-arm": "npm:0.27.2" + "@esbuild/android-arm64": "npm:0.27.2" + "@esbuild/android-x64": "npm:0.27.2" + "@esbuild/darwin-arm64": "npm:0.27.2" + "@esbuild/darwin-x64": "npm:0.27.2" + "@esbuild/freebsd-arm64": "npm:0.27.2" + "@esbuild/freebsd-x64": "npm:0.27.2" + "@esbuild/linux-arm": "npm:0.27.2" + "@esbuild/linux-arm64": "npm:0.27.2" + "@esbuild/linux-ia32": "npm:0.27.2" + "@esbuild/linux-loong64": "npm:0.27.2" + "@esbuild/linux-mips64el": "npm:0.27.2" + "@esbuild/linux-ppc64": "npm:0.27.2" + "@esbuild/linux-riscv64": "npm:0.27.2" + "@esbuild/linux-s390x": "npm:0.27.2" + "@esbuild/linux-x64": "npm:0.27.2" + "@esbuild/netbsd-arm64": "npm:0.27.2" + "@esbuild/netbsd-x64": "npm:0.27.2" + "@esbuild/openbsd-arm64": "npm:0.27.2" + "@esbuild/openbsd-x64": "npm:0.27.2" + "@esbuild/openharmony-arm64": "npm:0.27.2" + "@esbuild/sunos-x64": "npm:0.27.2" + "@esbuild/win32-arm64": "npm:0.27.2" + "@esbuild/win32-ia32": "npm:0.27.2" + "@esbuild/win32-x64": "npm:0.27.2" dependenciesMeta: "@esbuild/aix-ppc64": optional: true @@ -7653,7 +7709,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 10/534148f01e85ca93ec3a4ae8bef133680f5659e639915cd3a453d6ec9ead94c9a2e9bfd61380301471447e182beb62841cb72e0fa18251cdce3454a2511d7cf4 + checksum: 10/7f1229328b0efc63c4184a61a7eb303df1e99818cc1d9e309fb92600703008e69821e8e984e9e9f54a627da14e0960d561db3a93029482ef96dc82dd267a60c2 languageName: node linkType: hard @@ -7933,11 +7989,11 @@ __metadata: linkType: hard "esquery@npm:^1.5.0": - version: 1.6.0 - resolution: "esquery@npm:1.6.0" + version: 1.7.0 + resolution: "esquery@npm:1.7.0" dependencies: estraverse: "npm:^5.1.0" - checksum: 10/c587fb8ec9ed83f2b1bc97cf2f6854cc30bf784a79d62ba08c6e358bf22280d69aee12827521cf38e69ae9761d23fb7fde593ce315610f85655c139d99b05e5a + checksum: 10/4afaf3089367e1f5885caa116ef386dffd8bfd64da21fd3d0e56e938d2667cfb2e5400ab4a825aa70e799bb3741e5b5d63c0b94d86e2d4cf3095c9e64b2f5a15 languageName: node linkType: hard @@ -8078,7 +8134,7 @@ __metadata: languageName: node linkType: hard -"express@npm:^4.21.2": +"express@npm:^4.22.1": version: 4.22.1 resolution: "express@npm:4.22.1" dependencies: @@ -8227,11 +8283,11 @@ __metadata: linkType: hard "fastq@npm:^1.13.0, fastq@npm:^1.6.0": - version: 1.19.1 - resolution: "fastq@npm:1.19.1" + version: 1.20.1 + resolution: "fastq@npm:1.20.1" dependencies: reusify: "npm:^1.0.4" - checksum: 10/75679dc226316341c4f2a6b618571f51eac96779906faecd8921b984e844d6ae42fabb2df69b1071327d398d5716693ea9c9c8941f64ac9e89ec2032ce59d730 + checksum: 10/ab2fe3a7a108112e7752cfe7fc11683c21e595913a6a593ad0b4415f31dddbfc283775ab66f2c8ccea6ab7cfc116157cbddcfae9798d9de98d08fe0a2c3e97b2 languageName: node linkType: hard @@ -8698,14 +8754,14 @@ __metadata: languageName: node linkType: hard -"glob@npm:13.0.0, glob@npm:^13.0.0": - version: 13.0.0 - resolution: "glob@npm:13.0.0" +"glob@npm:13.0.1, glob@npm:^13.0.0": + version: 13.0.1 + resolution: "glob@npm:13.0.1" dependencies: - minimatch: "npm:^10.1.1" + minimatch: "npm:^10.1.2" minipass: "npm:^7.1.2" path-scurry: "npm:^2.0.0" - checksum: 10/de390721d29ee1c9ea41e40ec2aa0de2cabafa68022e237dc4297665a5e4d650776f2573191984ea1640aba1bf0ea34eddef2d8cbfbfc2ad24b5fb0af41d8846 + checksum: 10/465e8cc269ab88d7415a3906cdc0f4543a2ae54df99207204af5bc28a944396d8d893822f546a8056a78ec714e608ab4f3502532c4d6b9cc5e113adf0fe5109e languageName: node linkType: hard @@ -8747,10 +8803,10 @@ __metadata: languageName: node linkType: hard -"globals@npm:17.0.0": - version: 17.0.0 - resolution: "globals@npm:17.0.0" - checksum: 10/af6213db9bf5f599b8a609061984c4d5fedb23db336228b866eba70819fe13ea3d11ca507b4799e8afc888331d0f7e5760d01507e38cb988fff9ad3a9635b312 +"globals@npm:17.3.0": + version: 17.3.0 + resolution: "globals@npm:17.3.0" + checksum: 10/44ba2b7db93eb6a2531dfba09219845e21f2e724a4f400eb59518b180b7d5bcf7f65580530e3d3023d7dc2bdbacf5d265fd87c393f567deb9a2b0472b51c9d5e languageName: node linkType: hard @@ -8996,45 +9052,45 @@ __metadata: version: 0.0.0-use.local resolution: "home-assistant-frontend@workspace:." dependencies: - "@babel/core": "npm:7.28.6" - "@babel/helper-define-polyfill-provider": "npm:0.6.5" - "@babel/plugin-transform-runtime": "npm:7.28.5" - "@babel/preset-env": "npm:7.28.6" + "@babel/core": "npm:7.29.0" + "@babel/helper-define-polyfill-provider": "npm:0.6.6" + "@babel/plugin-transform-runtime": "npm:7.29.0" + "@babel/preset-env": "npm:7.29.0" "@babel/runtime": "npm:7.28.6" - "@braintree/sanitize-url": "npm:7.1.1" - "@bundle-stats/plugin-webpack-filter": "npm:4.21.8" + "@braintree/sanitize-url": "npm:7.1.2" + "@bundle-stats/plugin-webpack-filter": "npm:4.21.9" "@codemirror/autocomplete": "npm:6.20.0" - "@codemirror/commands": "npm:6.10.1" + "@codemirror/commands": "npm:6.10.2" "@codemirror/language": "npm:6.12.1" "@codemirror/legacy-modes": "npm:6.5.2" - "@codemirror/search": "npm:6.5.11" - "@codemirror/state": "npm:6.5.3" - "@codemirror/view": "npm:6.39.9" + "@codemirror/search": "npm:6.6.0" + "@codemirror/state": "npm:6.5.4" + "@codemirror/view": "npm:6.39.12" "@date-fns/tz": "npm:1.4.1" "@egjs/hammerjs": "npm:2.0.17" - "@formatjs/intl-datetimeformat": "npm:7.1.2" - "@formatjs/intl-displaynames": "npm:7.1.2" - "@formatjs/intl-durationformat": "npm:0.9.2" - "@formatjs/intl-getcanonicallocales": "npm:3.1.2" - "@formatjs/intl-listformat": "npm:8.1.2" - "@formatjs/intl-locale": "npm:5.1.2" - "@formatjs/intl-numberformat": "npm:9.1.2" - "@formatjs/intl-pluralrules": "npm:6.1.2" - "@formatjs/intl-relativetimeformat": "npm:12.1.2" + "@formatjs/intl-datetimeformat": "npm:7.2.1" + "@formatjs/intl-displaynames": "npm:7.2.1" + "@formatjs/intl-durationformat": "npm:0.10.1" + "@formatjs/intl-getcanonicallocales": "npm:3.2.1" + "@formatjs/intl-listformat": "npm:8.2.1" + "@formatjs/intl-locale": "npm:5.2.1" + "@formatjs/intl-numberformat": "npm:9.2.2" + "@formatjs/intl-pluralrules": "npm:6.2.2" + "@formatjs/intl-relativetimeformat": "npm:12.2.2" "@fullcalendar/core": "npm:6.1.20" "@fullcalendar/daygrid": "npm:6.1.20" "@fullcalendar/interaction": "npm:6.1.20" "@fullcalendar/list": "npm:6.1.20" "@fullcalendar/luxon3": "npm:6.1.20" "@fullcalendar/timegrid": "npm:6.1.20" - "@home-assistant/webawesome": "npm:3.0.0-ha.2" + "@home-assistant/webawesome": "npm:3.2.1-ha.0" "@lezer/highlight": "npm:1.2.3" "@lit-labs/motion": "npm:1.1.0" "@lit-labs/observers": "npm:2.1.0" "@lit-labs/virtualizer": "npm:2.1.1" "@lit/context": "npm:1.1.6" "@lit/reactive-element": "npm:2.1.2" - "@lokalise/node-api": "npm:15.6.0" + "@lokalise/node-api": "npm:15.6.1" "@material/chips": "npm:=14.0.0-canary.53b3cad2f.0" "@material/data-table": "npm:=14.0.0-canary.53b3cad2f.0" "@material/mwc-base": "npm:0.27.0" @@ -9047,7 +9103,6 @@ __metadata: "@material/mwc-icon-button": "npm:0.27.0" "@material/mwc-linear-progress": "npm:0.27.0" "@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch" - "@material/mwc-menu": "npm:0.27.0" "@material/mwc-radio": "npm:0.27.0" "@material/mwc-select": "npm:0.27.0" "@material/mwc-snackbar": "npm:0.27.0" @@ -9064,9 +9119,9 @@ __metadata: "@octokit/plugin-retry": "npm:8.0.3" "@octokit/rest": "npm:22.0.1" "@replit/codemirror-indentation-markers": "npm:6.5.3" - "@rsdoctor/rspack-plugin": "npm:1.4.0" - "@rspack/core": "npm:1.7.2" - "@rspack/dev-server": "npm:1.1.5" + "@rsdoctor/rspack-plugin": "npm:1.5.2" + "@rspack/core": "npm:1.7.5" + "@rspack/dev-server": "npm:1.2.1" "@swc/helpers": "npm:0.5.18" "@thomasloven/round-slider": "npm:0.6.0" "@tsparticles/engine": "npm:3.9.1" @@ -9089,8 +9144,8 @@ __metadata: "@types/tar": "npm:6.1.13" "@types/ua-parser-js": "npm:0.7.39" "@types/webspeechapi": "npm:0.0.29" - "@vibrant/color": "npm:4.0.0" - "@vitest/coverage-v8": "npm:4.0.17" + "@vibrant/color": "npm:4.0.4" + "@vitest/coverage-v8": "npm:4.0.18" "@vue/web-component-wrapper": "npm:1.3.0" "@webcomponents/scoped-custom-element-registry": "npm:0.0.10" "@webcomponents/webcomponentsjs": "npm:2.8.0" @@ -9101,7 +9156,7 @@ __metadata: browserslist-useragent-regexp: "npm:4.1.3" color-name: "npm:2.1.0" comlink: "npm:4.4.2" - core-js: "npm:3.47.0" + core-js: "npm:3.48.0" cropperjs: "npm:1.6.2" culori: "npm:4.0.2" date-fns: "npm:4.1.0" @@ -9123,7 +9178,7 @@ __metadata: fancy-log: "npm:2.0.0" fs-extra: "npm:11.3.3" fuse.js: "npm:7.1.0" - glob: "npm:13.0.0" + glob: "npm:13.0.1" google-timezones-json: "npm:1.2.0" gulp: "npm:5.0.1" gulp-brotli: "npm:3.0.0" @@ -9135,9 +9190,9 @@ __metadata: html-minifier-terser: "npm:7.2.0" husky: "npm:9.1.7" idb-keyval: "npm:6.2.2" - intl-messageformat: "npm:11.0.9" + intl-messageformat: "npm:11.1.2" js-yaml: "npm:4.1.1" - jsdom: "npm:27.4.0" + jsdom: "npm:28.0.0" jszip: "npm:3.10.1" leaflet: "npm:1.9.4" leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch" @@ -9152,30 +9207,30 @@ __metadata: map-stream: "npm:0.0.7" marked: "npm:17.0.1" memoize-one: "npm:6.0.0" - node-vibrant: "npm:4.0.3" + node-vibrant: "npm:4.0.4" object-hash: "npm:3.0.0" pinst: "npm:3.0.0" - prettier: "npm:3.7.4" + prettier: "npm:3.8.1" punycode: "npm:2.3.1" qr-scanner: "npm:1.4.2" qrcode: "npm:1.5.4" roboto-fontface: "npm:0.10.0" rrule: "npm:2.8.1" - rspack-manifest-plugin: "npm:5.2.0" + rspack-manifest-plugin: "npm:5.2.1" serve: "npm:14.2.5" sinon: "npm:21.0.1" sortablejs: "patch:sortablejs@npm%3A1.15.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch" stacktrace-js: "npm:2.0.2" superstruct: "npm:2.0.2" - tar: "npm:7.5.2" + tar: "npm:7.5.7" terser-webpack-plugin: "npm:5.3.16" tinykeys: "npm:3.0.0" ts-lit-plugin: "npm:2.0.2" typescript: "npm:5.9.3" - typescript-eslint: "npm:8.53.0" - ua-parser-js: "npm:2.0.8" - vite-tsconfig-paths: "npm:6.0.4" - vitest: "npm:4.0.17" + typescript-eslint: "npm:8.54.0" + ua-parser-js: "npm:2.0.9" + vite-tsconfig-paths: "npm:6.0.5" + vitest: "npm:4.0.18" vue: "npm:2.7.16" vue2-daterange-picker: "npm:0.6.8" webpack-stats-plugin: "npm:1.1.3" @@ -9542,15 +9597,15 @@ __metadata: languageName: node linkType: hard -"intl-messageformat@npm:11.0.9": - version: 11.0.9 - resolution: "intl-messageformat@npm:11.0.9" +"intl-messageformat@npm:11.1.2": + version: 11.1.2 + resolution: "intl-messageformat@npm:11.1.2" dependencies: - "@formatjs/ecma402-abstract": "npm:3.0.8" - "@formatjs/fast-memoize": "npm:3.0.3" - "@formatjs/icu-messageformat-parser": "npm:3.3.0" - tslib: "npm:^2.8.0" - checksum: 10/36481eabd854d623ae9941476cad1dce720efa9c40dbb8cb036b11b1ffda6804689b0cf9da5d2421268cb1138a49f7a0b553fcf8fed03b5a37af283baf326bb4 + "@formatjs/ecma402-abstract": "npm:3.1.1" + "@formatjs/fast-memoize": "npm:3.1.0" + "@formatjs/icu-messageformat-parser": "npm:3.5.1" + tslib: "npm:^2.8.1" + checksum: 10/fff6469d198692d9d62b32075cde445d4e223320d9174411fd2b7cc77e72e871a6dfda31cc44245b29edcf17270bcd7df2f2d029a36cd1cae081ac89be9926d8 languageName: node linkType: hard @@ -10185,15 +10240,15 @@ __metadata: languageName: node linkType: hard -"jsdom@npm:27.4.0": - version: 27.4.0 - resolution: "jsdom@npm:27.4.0" +"jsdom@npm:28.0.0": + version: 28.0.0 + resolution: "jsdom@npm:28.0.0" dependencies: - "@acemir/cssom": "npm:^0.9.28" + "@acemir/cssom": "npm:^0.9.31" "@asamuzakjp/dom-selector": "npm:^6.7.6" - "@exodus/bytes": "npm:^1.6.0" - cssstyle: "npm:^5.3.4" - data-urls: "npm:^6.0.0" + "@exodus/bytes": "npm:^1.11.0" + cssstyle: "npm:^5.3.7" + data-urls: "npm:^7.0.0" decimal.js: "npm:^10.6.0" html-encoding-sniffer: "npm:^6.0.0" http-proxy-agent: "npm:^7.0.2" @@ -10203,18 +10258,18 @@ __metadata: saxes: "npm:^6.0.0" symbol-tree: "npm:^3.2.4" tough-cookie: "npm:^6.0.0" + undici: "npm:^7.20.0" w3c-xmlserializer: "npm:^5.0.0" - webidl-conversions: "npm:^8.0.0" - whatwg-mimetype: "npm:^4.0.0" - whatwg-url: "npm:^15.1.0" - ws: "npm:^8.18.3" + webidl-conversions: "npm:^8.0.1" + whatwg-mimetype: "npm:^5.0.0" + whatwg-url: "npm:^16.0.0" xml-name-validator: "npm:^5.0.0" peerDependencies: canvas: ^3.0.0 peerDependenciesMeta: canvas: optional: true - checksum: 10/7c6db85ab91183b95204648e086cfc09ecee36d9e8fee0bb5d68e27543eca632de0af6d43de461176a7823820543d5c53561778af5f712b1a1cd28bfac084d51 + checksum: 10/c50461190982834446308bbfdfbbb829c6447b1693b1f0dd90fd717960a1d02c326cc90c9a378831d9bf8166702e57dcbf12691ddd7406c13d0a522c7b8971d9 languageName: node linkType: hard @@ -10499,13 +10554,13 @@ __metadata: linkType: hard "lit-element@npm:^4.2.0": - version: 4.2.1 - resolution: "lit-element@npm:4.2.1" + version: 4.2.2 + resolution: "lit-element@npm:4.2.2" dependencies: - "@lit-labs/ssr-dom-shim": "npm:^1.4.0" + "@lit-labs/ssr-dom-shim": "npm:^1.5.0" "@lit/reactive-element": "npm:^2.1.0" lit-html: "npm:^3.3.0" - checksum: 10/0d1d306cb12c3ba840cd9baf376997891ece751220049aa4a3cbd6bab25ba21e30d45012662eddaccccc94fe9930e8a0ef36fb779bf22fbcd2184b7a794fee3d + checksum: 10/268705c5a88c68ceeffb4dc871ffa7a9e5b5405b4907bc408cdc3336ea865b1690b4f4a34155af287200cc0737933c25326ed6816fa2c040b747925717bc21a6 languageName: node linkType: hard @@ -10766,8 +10821,8 @@ __metadata: linkType: hard "memfs@npm:^4.43.1": - version: 4.51.1 - resolution: "memfs@npm:4.51.1" + version: 4.52.0 + resolution: "memfs@npm:4.52.0" dependencies: "@jsonjoy.com/json-pack": "npm:^1.11.0" "@jsonjoy.com/util": "npm:^1.9.0" @@ -10775,7 +10830,7 @@ __metadata: thingies: "npm:^2.5.0" tree-dump: "npm:^1.0.3" tslib: "npm:^2.0.0" - checksum: 10/a2f70cd1b366f910a39bb1a398c03d6f3f0db936376697eca3e176609e9f6acc0ed40fb44d6293dd15eb3f730c3fce536e5024395e3dc92f54374133c96ba1b7 + checksum: 10/057f40d4030948baaf58b8e1c0ff3c7af01e927497422ee98a33dff81c3b9c4d16e1603e5dc6c615871b67122917f7f74343e78692fa8969105e3f450d0cca44 languageName: node linkType: hard @@ -10918,12 +10973,12 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.1.1": - version: 10.1.1 - resolution: "minimatch@npm:10.1.1" +"minimatch@npm:^10.1.2": + version: 10.1.2 + resolution: "minimatch@npm:10.1.2" dependencies: - "@isaacs/brace-expansion": "npm:^5.0.0" - checksum: 10/110f38921ea527022e90f7a5f43721838ac740d0a0c26881c03b57c261354fb9a0430e40b2c56dfcea2ef3c773768f27210d1106f1f2be19cde3eea93f26f45e + "@isaacs/brace-expansion": "npm:^5.0.1" + checksum: 10/6f0ef975463739207144e411bdd54f7205ce38770b162fa3bc4c9be4987a16cb20d0962a82f26c2372598cfba90faa97b327239d303b529b774f17681c163b46 languageName: node linkType: hard @@ -11179,17 +11234,17 @@ __metadata: languageName: node linkType: hard -"node-vibrant@npm:4.0.3": - version: 4.0.3 - resolution: "node-vibrant@npm:4.0.3" +"node-vibrant@npm:4.0.4": + version: 4.0.4 + resolution: "node-vibrant@npm:4.0.4" dependencies: "@types/node": "npm:^18.15.3" - "@vibrant/core": "npm:^4.0.0" - "@vibrant/generator-default": "npm:^4.0.3" - "@vibrant/image-browser": "npm:^4.0.0" - "@vibrant/image-node": "npm:^4.0.0" - "@vibrant/quantizer-mmcq": "npm:^4.0.0" - checksum: 10/a96c0f325a79c39367b0eb7bf4c682bbf36249ff87001385771bf866175278e6079646062eaeb3a2d77368fb2d60a862787ba9e25ea50ac5d875dce5ac521585 + "@vibrant/core": "npm:^4.0.4" + "@vibrant/generator-default": "npm:^4.0.4" + "@vibrant/image-browser": "npm:^4.0.4" + "@vibrant/image-node": "npm:^4.0.4" + "@vibrant/quantizer-mmcq": "npm:^4.0.4" + checksum: 10/8d94820b62adddb6ed9076bc9d587789ee0d1163c78e8e249c75737e5762403ccad350481cbfedba559bc0bf6fed3ccaa903cc6af7e89cb1d09d9406db2260fc languageName: node linkType: hard @@ -11874,12 +11929,12 @@ __metadata: languageName: node linkType: hard -"prettier@npm:3.7.4": - version: 3.7.4 - resolution: "prettier@npm:3.7.4" +"prettier@npm:3.8.1": + version: 3.8.1 + resolution: "prettier@npm:3.8.1" bin: prettier: bin/prettier.cjs - checksum: 10/b4d00ea13baed813cb777c444506632fb10faaef52dea526cacd03085f01f6db11fc969ccebedf05bf7d93c3960900994c6adf1b150e28a31afd5cfe7089b313 + checksum: 10/3da1cf8c1ef9bea828aa618553696c312e951f810bee368f6887109b203f18ee869fe88f66e65f9cf60b7cb1f2eae859892c860a300c062ff8ec69c381fc8dbd languageName: node linkType: hard @@ -12316,7 +12371,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.10, resolve@npm:^1.22.4": +"resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.11, resolve@npm:^1.22.4": version: 1.22.11 resolution: "resolve@npm:1.22.11" dependencies: @@ -12342,7 +12397,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.10#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin": +"resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.22.11#optional!builtin, resolve@patch:resolve@npm%3A^1.22.4#optional!builtin": version: 1.22.11 resolution: "resolve@patch:resolve@npm%3A1.22.11#optional!builtin::version=1.22.11&hash=c3c19d" dependencies: @@ -12428,31 +12483,34 @@ __metadata: linkType: hard "rollup@npm:^4.43.0": - version: 4.53.5 - resolution: "rollup@npm:4.53.5" + version: 4.55.1 + resolution: "rollup@npm:4.55.1" dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.53.5" - "@rollup/rollup-android-arm64": "npm:4.53.5" - "@rollup/rollup-darwin-arm64": "npm:4.53.5" - "@rollup/rollup-darwin-x64": "npm:4.53.5" - "@rollup/rollup-freebsd-arm64": "npm:4.53.5" - "@rollup/rollup-freebsd-x64": "npm:4.53.5" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.53.5" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.53.5" - "@rollup/rollup-linux-arm64-gnu": "npm:4.53.5" - "@rollup/rollup-linux-arm64-musl": "npm:4.53.5" - "@rollup/rollup-linux-loong64-gnu": "npm:4.53.5" - "@rollup/rollup-linux-ppc64-gnu": "npm:4.53.5" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.53.5" - "@rollup/rollup-linux-riscv64-musl": "npm:4.53.5" - "@rollup/rollup-linux-s390x-gnu": "npm:4.53.5" - "@rollup/rollup-linux-x64-gnu": "npm:4.53.5" - "@rollup/rollup-linux-x64-musl": "npm:4.53.5" - "@rollup/rollup-openharmony-arm64": "npm:4.53.5" - "@rollup/rollup-win32-arm64-msvc": "npm:4.53.5" - "@rollup/rollup-win32-ia32-msvc": "npm:4.53.5" - "@rollup/rollup-win32-x64-gnu": "npm:4.53.5" - "@rollup/rollup-win32-x64-msvc": "npm:4.53.5" + "@rollup/rollup-android-arm-eabi": "npm:4.55.1" + "@rollup/rollup-android-arm64": "npm:4.55.1" + "@rollup/rollup-darwin-arm64": "npm:4.55.1" + "@rollup/rollup-darwin-x64": "npm:4.55.1" + "@rollup/rollup-freebsd-arm64": "npm:4.55.1" + "@rollup/rollup-freebsd-x64": "npm:4.55.1" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.55.1" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.55.1" + "@rollup/rollup-linux-arm64-gnu": "npm:4.55.1" + "@rollup/rollup-linux-arm64-musl": "npm:4.55.1" + "@rollup/rollup-linux-loong64-gnu": "npm:4.55.1" + "@rollup/rollup-linux-loong64-musl": "npm:4.55.1" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.55.1" + "@rollup/rollup-linux-ppc64-musl": "npm:4.55.1" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.55.1" + "@rollup/rollup-linux-riscv64-musl": "npm:4.55.1" + "@rollup/rollup-linux-s390x-gnu": "npm:4.55.1" + "@rollup/rollup-linux-x64-gnu": "npm:4.55.1" + "@rollup/rollup-linux-x64-musl": "npm:4.55.1" + "@rollup/rollup-openbsd-x64": "npm:4.55.1" + "@rollup/rollup-openharmony-arm64": "npm:4.55.1" + "@rollup/rollup-win32-arm64-msvc": "npm:4.55.1" + "@rollup/rollup-win32-ia32-msvc": "npm:4.55.1" + "@rollup/rollup-win32-x64-gnu": "npm:4.55.1" + "@rollup/rollup-win32-x64-msvc": "npm:4.55.1" "@types/estree": "npm:1.0.8" fsevents: "npm:~2.3.2" dependenciesMeta: @@ -12478,8 +12536,12 @@ __metadata: optional: true "@rollup/rollup-linux-loong64-gnu": optional: true + "@rollup/rollup-linux-loong64-musl": + optional: true "@rollup/rollup-linux-ppc64-gnu": optional: true + "@rollup/rollup-linux-ppc64-musl": + optional: true "@rollup/rollup-linux-riscv64-gnu": optional: true "@rollup/rollup-linux-riscv64-musl": @@ -12490,6 +12552,8 @@ __metadata: optional: true "@rollup/rollup-linux-x64-musl": optional: true + "@rollup/rollup-openbsd-x64": + optional: true "@rollup/rollup-openharmony-arm64": optional: true "@rollup/rollup-win32-arm64-msvc": @@ -12504,7 +12568,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10/95a9186f6110c3b5428b1048f5766c8a98fbcd5b994e27234ae9be20cc9a81d5c856858c648213ac2bec13d2b5f47396aeba2b3643937b4abc8865f64ddac66b + checksum: 10/392a8bb68ce492d5f8f59d9420b448e76b2550152482a61688617c1c9c52f8f61162478cfe44f2c6a647e82b0a5e7d61e1ac1f2701ca4d48f4acd231df7bd84a languageName: node linkType: hard @@ -12524,17 +12588,17 @@ __metadata: languageName: node linkType: hard -"rspack-manifest-plugin@npm:5.2.0": - version: 5.2.0 - resolution: "rspack-manifest-plugin@npm:5.2.0" +"rspack-manifest-plugin@npm:5.2.1": + version: 5.2.1 + resolution: "rspack-manifest-plugin@npm:5.2.1" dependencies: "@rspack/lite-tapable": "npm:^1.0.1" peerDependencies: - "@rspack/core": 0.x || 1.x + "@rspack/core": ^1.0.0 || ^2.0.0-0 peerDependenciesMeta: "@rspack/core": optional: true - checksum: 10/661774e2f3d41db615b2fb2d14800c85524b33f3fd0b67e4161ca775f9ccbba3ecc8db21cd6e1f878e65a13cd6d718c6d66ad55614137cecb615cfd8d8f1cac9 + checksum: 10/fe0ddf92a881e45859f8dc4991823e3773292cfbd087db0d83d5856785069b1774139116fe0ca0adc02bdca8f7392165a990b104bc0298f4e1824c9d73672b05 languageName: node linkType: hard @@ -12978,22 +13042,22 @@ __metadata: linkType: hard "socket.io-adapter@npm:~2.5.2": - version: 2.5.5 - resolution: "socket.io-adapter@npm:2.5.5" + version: 2.5.6 + resolution: "socket.io-adapter@npm:2.5.6" dependencies: - debug: "npm:~4.3.4" - ws: "npm:~8.17.1" - checksum: 10/e364733a4c34ff1d4a02219e409bd48074fd614b7f5b0568ccfa30dd553252a5b9a41056931306a276891d13ea76a19e2c6f2128a4675c37323f642896874d80 + debug: "npm:~4.4.1" + ws: "npm:~8.18.3" + checksum: 10/2bbefcc6f3d5dedab3105af03091b8863079173ab5610118d0ce94a0cf40fd87956c304f4f06445e361296b1966034be1ff0ba4e87b3c2baec216bbdec43b6e6 languageName: node linkType: hard "socket.io-parser@npm:~4.2.4": - version: 4.2.4 - resolution: "socket.io-parser@npm:4.2.4" + version: 4.2.5 + resolution: "socket.io-parser@npm:4.2.5" dependencies: "@socket.io/component-emitter": "npm:~3.1.0" - debug: "npm:~4.3.1" - checksum: 10/4be500a9ff7e79c50ec25af11048a3ed34b4c003a9500d656786a1e5bceae68421a8394cf3eb0aa9041f85f36c1a9a737617f4aee91a42ab4ce16ffb2aa0c89c + debug: "npm:~4.4.1" + checksum: 10/612b3ba068327cbdca043d07f8d96da587a18e0b3e00f002b6476c22410c891abafc44a3f009abd014f2de42b348032f465a7b19771151728f6361ed116423d2 languageName: node linkType: hard @@ -13562,16 +13626,16 @@ __metadata: languageName: node linkType: hard -"tar@npm:7.5.2, tar@npm:^7.5.2": - version: 7.5.2 - resolution: "tar@npm:7.5.2" +"tar@npm:7.5.7, tar@npm:^7.5.2": + version: 7.5.7 + resolution: "tar@npm:7.5.7" dependencies: "@isaacs/fs-minipass": "npm:^4.0.0" chownr: "npm:^3.0.0" minipass: "npm:^7.1.2" minizlib: "npm:^3.1.0" yallist: "npm:^5.0.0" - checksum: 10/dbad9c9a07863cd1bdf8801d563b3280aa7dd0f4a6cead779ff7516d148dc80b4c04639ba732d47f91f04002f57e8c3c6573a717d649daecaac74ce71daa7ad3 + checksum: 10/0d6938dd32fe5c0f17c8098d92bd9889ee0ed9d11f12381b8146b6e8c87bb5aa49feec7abc42463f0597503d8e89e4c4c0b42bff1a5a38444e918b4878b7fd21 languageName: node linkType: hard @@ -13639,8 +13703,8 @@ __metadata: linkType: hard "terser@npm:^5.15.1, terser@npm:^5.17.4, terser@npm:^5.31.1": - version: 5.44.1 - resolution: "terser@npm:5.44.1" + version: 5.46.0 + resolution: "terser@npm:5.46.0" dependencies: "@jridgewell/source-map": "npm:^0.3.3" acorn: "npm:^8.15.0" @@ -13648,7 +13712,7 @@ __metadata: source-map-support: "npm:~0.5.20" bin: terser: bin/terser - checksum: 10/516ece205b7db778c4eddb287a556423cb776b7ca591b06270e558a76aa2d57c8d71d9c3c4410b276d3426beb03516fff7d96ff8b517e10730a72908810c6e33 + checksum: 10/331e4f5a165d91d16ac6a95b510d4f5ef24679e4bc9e1b4e4182e89b7245f614d24ce0def583e2ca3ca45f82ba810991e0c5b66dd4353a6e0b7082786af6bd35 languageName: node linkType: hard @@ -13958,11 +14022,11 @@ __metadata: linkType: hard "type-fest@npm:^5.2.0": - version: 5.3.1 - resolution: "type-fest@npm:5.3.1" + version: 5.4.1 + resolution: "type-fest@npm:5.4.1" dependencies: tagged-tag: "npm:^1.0.0" - checksum: 10/1015eeae6ba0961b6d7c010f2ca9ad142891e1d1f5e1ff898ee73ec8c82529100bce63ce57ae657b1dc788f7a5f209e600ebc33241b0de433a8c5e7f2018b331 + checksum: 10/be7d4749e1e5cf2e2c9904fa1aaf9da5eef6c47c130881bf93bfd5a670b2ab59c5502466768e42c521281056a2375b1617176a75cf6c52b575f4bbabbd450b21 languageName: node linkType: hard @@ -14029,18 +14093,18 @@ __metadata: languageName: node linkType: hard -"typescript-eslint@npm:8.53.0": - version: 8.53.0 - resolution: "typescript-eslint@npm:8.53.0" +"typescript-eslint@npm:8.54.0": + version: 8.54.0 + resolution: "typescript-eslint@npm:8.54.0" dependencies: - "@typescript-eslint/eslint-plugin": "npm:8.53.0" - "@typescript-eslint/parser": "npm:8.53.0" - "@typescript-eslint/typescript-estree": "npm:8.53.0" - "@typescript-eslint/utils": "npm:8.53.0" + "@typescript-eslint/eslint-plugin": "npm:8.54.0" + "@typescript-eslint/parser": "npm:8.54.0" + "@typescript-eslint/typescript-estree": "npm:8.54.0" + "@typescript-eslint/utils": "npm:8.54.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: ">=4.8.4 <6.0.0" - checksum: 10/b4731161a4fec6ce9110e54407a50733e15b0eb48cb9636906a54068af10f8102a7018b9e5db6264604050955a03d3649a79c4869d43bcce215358a8a8a03f96 + checksum: 10/21b1a27fd44716df8d2c7bac4ebd0caef196a04375fff7919dc817066017b6b8700f1e242bd065a26ac7ce0505b7a588626099e04a28142504ed4f0aae8bffb1 languageName: node linkType: hard @@ -14091,16 +14155,16 @@ __metadata: languageName: node linkType: hard -"ua-parser-js@npm:2.0.8": - version: 2.0.8 - resolution: "ua-parser-js@npm:2.0.8" +"ua-parser-js@npm:2.0.9": + version: 2.0.9 + resolution: "ua-parser-js@npm:2.0.9" dependencies: detect-europe-js: "npm:^0.1.2" is-standalone-pwa: "npm:^0.1.1" ua-is-frozen: "npm:^0.1.2" bin: ua-parser-js: script/cli.js - checksum: 10/862ad2e3b083d8ffbb325d6e7c78924aa86f751829301173457afdf44ff911c95c5478d8a1bd31cf80555dd6aae946b3194d20615a91550794f20dad7ac998b5 + checksum: 10/63e3404e16ade8a7d1b45bf2a871410193ab63620ab02ffa5308a507a984d2698bd74651ce260ac3e0cb9f8df89c71fa55a6beefbe269c12849c6d6cf0974407 languageName: node linkType: hard @@ -14163,6 +14227,13 @@ __metadata: languageName: node linkType: hard +"undici@npm:^7.20.0": + version: 7.20.0 + resolution: "undici@npm:7.20.0" + checksum: 10/09ca3e1255cf05f3c76e6dff2ae760131ea5bba57290b9b184bd94f5167939548e7ea73292c524c25eb91f5a2152623394d4c6124e222d34fcd53ef733c6b156 + languageName: node + linkType: hard + "unicode-canonical-property-names-ecmascript@npm:^2.0.0": version: 2.0.1 resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.1" @@ -14414,24 +14485,20 @@ __metadata: languageName: node linkType: hard -"vite-tsconfig-paths@npm:6.0.4": - version: 6.0.4 - resolution: "vite-tsconfig-paths@npm:6.0.4" +"vite-tsconfig-paths@npm:6.0.5": + version: 6.0.5 + resolution: "vite-tsconfig-paths@npm:6.0.5" dependencies: debug: "npm:^4.1.1" globrex: "npm:^0.1.2" tsconfck: "npm:^3.0.3" - vite: "npm:*" peerDependencies: vite: "*" - peerDependenciesMeta: - vite: - optional: true - checksum: 10/85f871cd5e321f2865972559b01c518664e6e34f9039b630dd77c2f379f8fdc386e15f7237aa5c108d813030c6e9bc8edfbf61687df7684803111a2495edadac + checksum: 10/1c3d38102ed34d057fc602c332bfd059bfedd0b378ee87b1a73eac89e20f6d81ee4bd9639557287e275cae2230f1d9225d2d7d83a2fa355a380cf77568f2cd31 languageName: node linkType: hard -"vite@npm:*, vite@npm:^6.0.0 || ^7.0.0": +"vite@npm:^6.0.0 || ^7.0.0": version: 7.3.1 resolution: "vite@npm:7.3.1" dependencies: @@ -14486,17 +14553,17 @@ __metadata: languageName: node linkType: hard -"vitest@npm:4.0.17": - version: 4.0.17 - resolution: "vitest@npm:4.0.17" +"vitest@npm:4.0.18": + version: 4.0.18 + resolution: "vitest@npm:4.0.18" dependencies: - "@vitest/expect": "npm:4.0.17" - "@vitest/mocker": "npm:4.0.17" - "@vitest/pretty-format": "npm:4.0.17" - "@vitest/runner": "npm:4.0.17" - "@vitest/snapshot": "npm:4.0.17" - "@vitest/spy": "npm:4.0.17" - "@vitest/utils": "npm:4.0.17" + "@vitest/expect": "npm:4.0.18" + "@vitest/mocker": "npm:4.0.18" + "@vitest/pretty-format": "npm:4.0.18" + "@vitest/runner": "npm:4.0.18" + "@vitest/snapshot": "npm:4.0.18" + "@vitest/spy": "npm:4.0.18" + "@vitest/utils": "npm:4.0.18" es-module-lexer: "npm:^1.7.0" expect-type: "npm:^1.2.2" magic-string: "npm:^0.30.21" @@ -14514,10 +14581,10 @@ __metadata: "@edge-runtime/vm": "*" "@opentelemetry/api": ^1.9.0 "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 - "@vitest/browser-playwright": 4.0.17 - "@vitest/browser-preview": 4.0.17 - "@vitest/browser-webdriverio": 4.0.17 - "@vitest/ui": 4.0.17 + "@vitest/browser-playwright": 4.0.18 + "@vitest/browser-preview": 4.0.18 + "@vitest/browser-webdriverio": 4.0.18 + "@vitest/ui": 4.0.18 happy-dom: "*" jsdom: "*" peerDependenciesMeta: @@ -14541,7 +14608,7 @@ __metadata: optional: true bin: vitest: vitest.mjs - checksum: 10/792cf5ecdb2c0c2a61fc7beacec800413dcc5b68ad5e18f74795cdbfe513d58e3b6e437571c728c9992920f52d0640a5264aaf8c3702454b2637ff93451cf567 + checksum: 10/6c6464ebcf3af83546862896fd1b5f10cb6607261bffce39df60033a288b8c1687ae1dd20002b6e4997a7a05303376d1eb58ce20afe63be052529a4378a8c165 languageName: node linkType: hard @@ -14679,10 +14746,10 @@ __metadata: languageName: node linkType: hard -"webidl-conversions@npm:^8.0.0": - version: 8.0.0 - resolution: "webidl-conversions@npm:8.0.0" - checksum: 10/8138d1b291c8f311d93de680653b13b04560aa35d83f9606642e746fca39d7dab9cddd9282ade21774115ea332b8b11f008106b82d4a0125e98a49479381aeee +"webidl-conversions@npm:^8.0.1": + version: 8.0.1 + resolution: "webidl-conversions@npm:8.0.1" + checksum: 10/0f7007311f1fc257a8e406dd236f13b61fb57cf0fddb476aec33457d2d0add2d012d6df0eeb00934399238e3f3b9dad30f59dc6ac83024ae0ebd5a518bf365e8 languageName: node linkType: hard @@ -14705,51 +14772,6 @@ __metadata: languageName: node linkType: hard -"webpack-dev-server@npm:5.2.2": - version: 5.2.2 - resolution: "webpack-dev-server@npm:5.2.2" - dependencies: - "@types/bonjour": "npm:^3.5.13" - "@types/connect-history-api-fallback": "npm:^1.5.4" - "@types/express": "npm:^4.17.21" - "@types/express-serve-static-core": "npm:^4.17.21" - "@types/serve-index": "npm:^1.9.4" - "@types/serve-static": "npm:^1.15.5" - "@types/sockjs": "npm:^0.3.36" - "@types/ws": "npm:^8.5.10" - ansi-html-community: "npm:^0.0.8" - bonjour-service: "npm:^1.2.1" - chokidar: "npm:^3.6.0" - colorette: "npm:^2.0.10" - compression: "npm:^1.7.4" - connect-history-api-fallback: "npm:^2.0.0" - express: "npm:^4.21.2" - graceful-fs: "npm:^4.2.6" - http-proxy-middleware: "npm:^2.0.9" - ipaddr.js: "npm:^2.1.0" - launch-editor: "npm:^2.6.1" - open: "npm:^10.0.3" - p-retry: "npm:^6.2.0" - schema-utils: "npm:^4.2.0" - selfsigned: "npm:^2.4.1" - serve-index: "npm:^1.9.1" - sockjs: "npm:^0.3.24" - spdy: "npm:^4.0.2" - webpack-dev-middleware: "npm:^7.4.2" - ws: "npm:^8.18.0" - peerDependencies: - webpack: ^5.0.0 - peerDependenciesMeta: - webpack: - optional: true - webpack-cli: - optional: true - bin: - webpack-dev-server: bin/webpack-dev-server.js - checksum: 10/59517409cd38c01a875a03b9658f3d20d492b5b8bead9ded4a0f3d33e6857daf2d352fe89f0181dcaea6d0fbe84b0494cb4750a87120fe81cdbb3c32b499451c - languageName: node - linkType: hard - "webpack-stats-plugin@npm:1.1.3": version: 1.1.3 resolution: "webpack-stats-plugin@npm:1.1.3" @@ -14809,20 +14831,21 @@ __metadata: languageName: node linkType: hard -"whatwg-mimetype@npm:^4.0.0": - version: 4.0.0 - resolution: "whatwg-mimetype@npm:4.0.0" - checksum: 10/894a618e2d90bf444b6f309f3ceb6e58cf21b2beaa00c8b333696958c4076f0c7b30b9d33413c9ffff7c5832a0a0c8569e5bb347ef44beded72aeefd0acd62e8 +"whatwg-mimetype@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-mimetype@npm:5.0.0" + checksum: 10/a2d5da445f671ed34010b45283ffb9ba3c68c695b8ec91f7400cfc9149c35eb2bc47bd2c39bbe8e026786b955ace03402ba2e5cfde4955434a3ec3c511a85d0a languageName: node linkType: hard -"whatwg-url@npm:^15.0.0, whatwg-url@npm:^15.1.0": - version: 15.1.0 - resolution: "whatwg-url@npm:15.1.0" +"whatwg-url@npm:^16.0.0": + version: 16.0.0 + resolution: "whatwg-url@npm:16.0.0" dependencies: + "@exodus/bytes": "npm:^1.11.0" tr46: "npm:^6.0.0" - webidl-conversions: "npm:^8.0.0" - checksum: 10/9ae5ce70060f2a9ea73799062af6e796ec2477f44bf1a886953b405700e3ab11d15aa0fe7088c4215f839e56a845d5d1c44584ed292a832837a8c8549c566886 + webidl-conversions: "npm:^8.0.1" + checksum: 10/f57a668adcd98fc21a8da88905ac23da3feba2aea2fb484caab20980504a32c16fd9e3c3313131eacade802505118967631cea3e48e5c3c272ba76521babe355 languageName: node linkType: hard @@ -14901,8 +14924,8 @@ __metadata: linkType: hard "which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.19": - version: 1.1.19 - resolution: "which-typed-array@npm:1.1.19" + version: 1.1.20 + resolution: "which-typed-array@npm:1.1.20" dependencies: available-typed-arrays: "npm:^1.0.7" call-bind: "npm:^1.0.8" @@ -14911,7 +14934,7 @@ __metadata: get-proto: "npm:^1.0.1" gopd: "npm:^1.2.0" has-tostringtag: "npm:^1.0.2" - checksum: 10/12be30fb88567f9863186bee1777f11bea09dd59ed8b3ce4afa7dd5cade75e2f4cc56191a2da165113cc7cf79987ba021dac1e22b5b62aa7e5c56949f2469a68 + checksum: 10/e56da3fc995d330ff012f682476f7883c16b12d36c6717c87c7ca23eb5a5ef957fa89115dacb389b11a9b4e99d5dbe2d12689b4d5d08c050b5aed0eae385b840 languageName: node linkType: hard @@ -15331,7 +15354,22 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.18.0, ws@npm:^8.18.3": +"ws@npm:^8.18.0": + version: 8.19.0 + resolution: "ws@npm:8.19.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/26e4901e93abaf73af9f26a93707c95b4845e91a7a347ec8c569e6e9be7f9df066f6c2b817b2d685544e208207898a750b78461e6e8d810c11a370771450c31b + languageName: node + linkType: hard + +"ws@npm:~8.18.3": version: 8.18.3 resolution: "ws@npm:8.18.3" peerDependencies: @@ -15346,21 +15384,6 @@ __metadata: languageName: node linkType: hard -"ws@npm:~8.17.1": - version: 8.17.1 - resolution: "ws@npm:8.17.1" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10/4264ae92c0b3e59c7e309001e93079b26937aab181835fb7af79f906b22cd33b6196d96556dafb4e985742dd401e99139572242e9847661fdbc96556b9e6902d - languageName: node - linkType: hard - "wsl-utils@npm:^0.1.0": version: 0.1.0 resolution: "wsl-utils@npm:0.1.0"