mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-14 21:27:01 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d3c68973a0 | |||
| 141dce9773 | |||
| d355d9e089 | |||
| ce5a135bd2 | |||
| 8905123249 | |||
| 92e6f69fc7 | |||
| 3fbc11458a | |||
| 8b419646d1 | |||
| 5ca28bdc85 | |||
| efe41b1e1a | |||
| d16aa48f65 | |||
| 99c7a427f1 | |||
| 0ccc582aff | |||
| e32dd78f51 | |||
| c3358e0825 | |||
| 764961b28e | |||
| 897a33963e | |||
| 6eadf2ff15 | |||
| d32f5b6a50 | |||
| a21cf5d995 | |||
| 9aa6cd4154 | |||
| 2024ce0aef | |||
| 908a518f18 |
@@ -58,8 +58,6 @@ jobs:
|
||||
run: yarn run lint:lit --quiet
|
||||
- name: Run prettier
|
||||
run: yarn run lint:prettier
|
||||
- name: Check dependency licenses
|
||||
run: yarn run lint:licenses
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -41,14 +41,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.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@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -62,4 +62,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- master
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# ── Build the demo once and share it across test jobs via artifact ──────────
|
||||
build-demo:
|
||||
name: Build demo
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Build demo
|
||||
run: ./node_modules/.bin/gulp build-demo
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload demo build
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: demo-dist
|
||||
path: demo/dist/
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
|
||||
# ── Build the e2e test app and share it via artifact ────────────────────────
|
||||
build-e2e-test-app:
|
||||
name: Build e2e test app
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Build e2e test app
|
||||
run: ./node_modules/.bin/gulp build-e2e-test-app
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload e2e test app build
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: e2e-test-app-dist
|
||||
path: test/e2e/app/dist/
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
|
||||
# ── Build the gallery and share it via artifact ─────────────────────────────
|
||||
build-gallery:
|
||||
name: Build gallery
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Build gallery
|
||||
run: ./node_modules/.bin/gulp build-gallery
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload gallery build
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: gallery-dist
|
||||
path: gallery/dist/
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
|
||||
# ── Run Playwright tests locally against Chromium ──────────────────────────
|
||||
e2e-local:
|
||||
name: E2E (local Chromium)
|
||||
needs: [build-demo, build-e2e-test-app, build-gallery]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
|
||||
- name: Download demo build
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
with:
|
||||
name: demo-dist
|
||||
path: demo/dist/
|
||||
|
||||
- name: Download e2e test app build
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
with:
|
||||
name: e2e-test-app-dist
|
||||
path: test/e2e/app/dist/
|
||||
|
||||
- name: Download gallery build
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
with:
|
||||
name: gallery-dist
|
||||
path: gallery/dist/
|
||||
|
||||
- name: Run Playwright tests (local)
|
||||
run: yarn test:e2e
|
||||
|
||||
- name: Upload blob report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: always()
|
||||
with:
|
||||
name: blob-report-local
|
||||
path: test/e2e/reports/
|
||||
retention-days: 3
|
||||
|
||||
# ── Run Playwright tests on BrowserStack (real devices + browsers) ─────────
|
||||
# The BrowserStack SDK manages the Local tunnel and uploads results to the
|
||||
# BrowserStack Automate dashboard automatically — no tunnel action needed.
|
||||
e2e-browserstack:
|
||||
name: E2E (BrowserStack)
|
||||
needs: [build-demo, build-e2e-test-app, build-gallery]
|
||||
runs-on: ubuntu-latest
|
||||
environment: browserstack
|
||||
env:
|
||||
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
|
||||
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Download demo build
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
with:
|
||||
name: demo-dist
|
||||
path: demo/dist/
|
||||
|
||||
- name: Download e2e test app build
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
with:
|
||||
name: e2e-test-app-dist
|
||||
path: test/e2e/app/dist/
|
||||
|
||||
- name: Download gallery build
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
with:
|
||||
name: gallery-dist
|
||||
path: gallery/dist/
|
||||
|
||||
- name: Run Playwright tests (BrowserStack)
|
||||
run: yarn test:e2e:browserstack
|
||||
|
||||
# ── Merge local blob reports and post PR comment ───────────────────────────
|
||||
report:
|
||||
name: Report
|
||||
needs: [e2e-local, e2e-browserstack]
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
cache: yarn
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Download blob report (local)
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: blob-report-local
|
||||
path: test/e2e/reports/
|
||||
|
||||
- name: Stage blobs for merge
|
||||
run: node test/e2e/collect-blob-reports.mjs
|
||||
|
||||
- name: Merge blob reports
|
||||
run: npx playwright merge-reports -c test/e2e/playwright.merge.config.ts test/e2e/reports/blob
|
||||
|
||||
- name: Upload merged HTML report
|
||||
id: upload-report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: playwright-report
|
||||
path: test/e2e/reports/combined/
|
||||
retention-days: 14
|
||||
|
||||
- name: Post report link to PR
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||
const body = `## Playwright E2E test report\n\nThe combined HTML report is available as a workflow artifact.\n\n[View workflow run](${runUrl})`;
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body,
|
||||
});
|
||||
@@ -18,6 +18,6 @@ jobs:
|
||||
pull-requests: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@563bf132657a13ded0b01fcb723c5a58cdd824e2 # v7.2.1
|
||||
- uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -54,7 +54,14 @@ src/cast/dev_const.ts
|
||||
# test coverage
|
||||
test/coverage/
|
||||
|
||||
# Playwright e2e output
|
||||
test/e2e/reports/
|
||||
test/e2e/test-results/
|
||||
# E2E test app build output
|
||||
test/e2e/app/dist/
|
||||
|
||||
# AI tooling
|
||||
.claude
|
||||
.cursor
|
||||
.opencode
|
||||
.serena
|
||||
|
||||
+2
-7
@@ -1,16 +1,11 @@
|
||||
approvedGitRepositories:
|
||||
- "**"
|
||||
|
||||
compressionLevel: mixed
|
||||
|
||||
npmMinimalAgeGate: "3d"
|
||||
|
||||
defaultSemverRangePrefix: ""
|
||||
|
||||
enableGlobalCache: false
|
||||
|
||||
enableScripts: true
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
npmMinimalAgeGate: 3d
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.14.1.cjs
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# BrowserStack Automate configuration for Home Assistant frontend e2e tests.
|
||||
# Credentials are read from BROWSERSTACK_USERNAME and BROWSERSTACK_ACCESS_KEY
|
||||
# environment variables set in GitHub Actions (or locally).
|
||||
# See: https://www.browserstack.com/docs/automate/playwright/getting-started/nodejs
|
||||
|
||||
userName: ${BROWSERSTACK_USERNAME}
|
||||
accessKey: ${BROWSERSTACK_ACCESS_KEY}
|
||||
|
||||
projectName: Home Assistant Frontend
|
||||
buildName: e2e tests
|
||||
buildIdentifier: "CI #${BUILD_NUMBER}"
|
||||
|
||||
# ── Platforms ────────────────────────────────────────────────────────────────
|
||||
platforms:
|
||||
- os: Windows
|
||||
osVersion: 11
|
||||
browserName: chrome
|
||||
browserVersion: latest
|
||||
- os: OS X
|
||||
osVersion: Ventura
|
||||
browserName: playwright-firefox
|
||||
browserVersion: latest
|
||||
- deviceName: iPad 6th
|
||||
osVersion: 12
|
||||
browserName: playwright-webkit
|
||||
- deviceName: iPhone 12
|
||||
osVersion: 14
|
||||
browserName: playwright-webkit
|
||||
- deviceName: Samsung Galaxy S23
|
||||
osVersion: 13
|
||||
browserName: chrome
|
||||
realMobile: true
|
||||
|
||||
parallelsPerPlatform: 1
|
||||
|
||||
# ── Local tunnel ─────────────────────────────────────────────────────────────
|
||||
# The SDK manages the BrowserStack Local tunnel automatically.
|
||||
browserstackLocal: true
|
||||
|
||||
framework: playwright
|
||||
|
||||
# Pin to the latest Playwright version BrowserStack supports. Our local
|
||||
# @playwright/test is newer (1.59.x) which BrowserStack does not yet support,
|
||||
# causing a "Malformed endpoint" connection error if left unset.
|
||||
# Update this when BrowserStack adds support for a newer version.
|
||||
# Supported versions: https://www.browserstack.com/docs/automate/playwright/browsers-and-os
|
||||
playwrightVersion: 1.latest
|
||||
|
||||
# ── Debugging ────────────────────────────────────────────────────────────────
|
||||
debug: false
|
||||
networkLogs: false
|
||||
consoleLogs: errors
|
||||
testObservability: true
|
||||
@@ -1,4 +1,3 @@
|
||||
/* global require, module, __dirname, process */
|
||||
const path = require("path");
|
||||
const env = require("./env.cjs");
|
||||
const paths = require("./paths.cjs");
|
||||
@@ -321,4 +320,22 @@ module.exports.config = {
|
||||
isLandingPageBuild: true,
|
||||
};
|
||||
},
|
||||
|
||||
e2eTestApp({ isProdBuild, latestBuild, isStatsBuild }) {
|
||||
return {
|
||||
name: "e2e-test-app" + nameSuffix(latestBuild),
|
||||
entry: {
|
||||
main: path.resolve(paths.e2eTestApp_dir, "src/entrypoint.ts"),
|
||||
},
|
||||
outputPath: outputPath(paths.e2eTestApp_output_root, latestBuild),
|
||||
publicPath: publicPath(latestBuild),
|
||||
defineOverlay: {
|
||||
__VERSION__: JSON.stringify(`E2E-TEST-${env.version()}`),
|
||||
__DEMO__: true,
|
||||
},
|
||||
isProdBuild,
|
||||
latestBuild,
|
||||
isStatsBuild,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// @ts-check
|
||||
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
import rootConfig from "../eslint.config.mjs";
|
||||
|
||||
export default tseslint.config(...rootConfig, {
|
||||
languageOptions: {
|
||||
globals: globals.node,
|
||||
},
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"import-x/no-extraneous-dependencies": "off",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* global module */
|
||||
// Browser-only replacement for core-js/internals/get-built-in-node-module.
|
||||
// The original helper evaluates `Function('return require("...")')()`
|
||||
// when it detects a Node environment, which causes a runtime
|
||||
|
||||
@@ -5,7 +5,6 @@ import "./compress.js";
|
||||
import "./entry-html.js";
|
||||
import "./gather-static.js";
|
||||
import "./gen-icons-json.js";
|
||||
import "./licenses.js";
|
||||
import "./locale-data.js";
|
||||
import "./service-worker.js";
|
||||
import "./translations.js";
|
||||
@@ -37,12 +36,7 @@ gulp.task(
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean",
|
||||
gulp.parallel(
|
||||
"gen-icons-json",
|
||||
"build-translations",
|
||||
"build-locale-data",
|
||||
"gen-licenses"
|
||||
),
|
||||
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||
"copy-static-app",
|
||||
"rspack-prod-app",
|
||||
gulp.parallel("gen-pages-app-prod", "gen-service-worker-app-prod"),
|
||||
|
||||
@@ -45,3 +45,10 @@ gulp.task(
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"clean-e2e-test-app",
|
||||
gulp.parallel("clean-translations", async () =>
|
||||
deleteSync([paths.e2eTestApp_output_root, paths.build_dir])
|
||||
)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import gulp from "gulp";
|
||||
import "./clean.js";
|
||||
import "./entry-html.js";
|
||||
import "./gather-static.js";
|
||||
import "./gen-icons-json.js";
|
||||
import "./translations.js";
|
||||
import "./rspack.js";
|
||||
|
||||
gulp.task(
|
||||
"develop-e2e-test-app",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "development";
|
||||
},
|
||||
"clean-e2e-test-app",
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel(
|
||||
"gen-icons-json",
|
||||
"gen-pages-e2e-test-app-dev",
|
||||
"build-translations",
|
||||
"build-locale-data"
|
||||
),
|
||||
"copy-static-e2e-test-app",
|
||||
"rspack-dev-server-e2e-test-app"
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-e2e-test-app",
|
||||
gulp.series(
|
||||
async function setEnv() {
|
||||
process.env.NODE_ENV = "production";
|
||||
},
|
||||
"clean-e2e-test-app",
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel("gen-icons-json", "build-translations", "build-locale-data"),
|
||||
"copy-static-e2e-test-app",
|
||||
"rspack-prod-e2e-test-app",
|
||||
"gen-pages-e2e-test-app-prod"
|
||||
)
|
||||
);
|
||||
@@ -1,4 +1,3 @@
|
||||
/* global process */
|
||||
// Tasks to generate entry HTML
|
||||
|
||||
import {
|
||||
@@ -26,7 +25,6 @@ const SAFARI_TO_MACOS = {
|
||||
16: [11, 0, 0],
|
||||
17: [12, 0, 0],
|
||||
18: [13, 0, 0],
|
||||
26: [26, 0, 0],
|
||||
};
|
||||
|
||||
const getCommonTemplateVars = () => {
|
||||
@@ -268,3 +266,24 @@ gulp.task(
|
||||
paths.landingPage_output_es5
|
||||
)
|
||||
);
|
||||
|
||||
const E2E_TEST_APP_PAGE_ENTRIES = { "index.html": ["main"] };
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-e2e-test-app-dev",
|
||||
genPagesDevTask(
|
||||
E2E_TEST_APP_PAGE_ENTRIES,
|
||||
paths.e2eTestApp_dir,
|
||||
paths.e2eTestApp_output_root
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"gen-pages-e2e-test-app-prod",
|
||||
genPagesProdTask(
|
||||
E2E_TEST_APP_PAGE_ENTRIES,
|
||||
paths.e2eTestApp_dir,
|
||||
paths.e2eTestApp_output_root,
|
||||
paths.e2eTestApp_output_latest
|
||||
)
|
||||
);
|
||||
|
||||
@@ -201,3 +201,23 @@ gulp.task("copy-static-landing-page", async () => {
|
||||
copyFonts(paths.landingPage_output_static);
|
||||
copyTranslations(paths.landingPage_output_static);
|
||||
});
|
||||
|
||||
gulp.task("copy-static-e2e-test-app", async () => {
|
||||
// Copy app static files (icons, polyfills, etc.)
|
||||
fs.copySync(
|
||||
polyPath("public/static"),
|
||||
path.resolve(paths.e2eTestApp_output_root, "static")
|
||||
);
|
||||
// Copy e2e test app public files (manifest, sw stubs)
|
||||
const e2ePublic = path.resolve(paths.e2eTestApp_dir, "public");
|
||||
if (fs.existsSync(e2ePublic)) {
|
||||
fs.copySync(e2ePublic, paths.e2eTestApp_output_root);
|
||||
}
|
||||
|
||||
copyPolyfills(paths.e2eTestApp_output_static);
|
||||
copyMapPanel(paths.e2eTestApp_output_static);
|
||||
copyFonts(paths.e2eTestApp_output_static);
|
||||
copyTranslations(paths.e2eTestApp_output_static);
|
||||
copyLocaleData(paths.e2eTestApp_output_static);
|
||||
copyMdiIcons(paths.e2eTestApp_output_static);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import "./clean.js";
|
||||
import "./compress.js";
|
||||
import "./demo.js";
|
||||
import "./download-translations.js";
|
||||
import "./e2e-test-app.js";
|
||||
import "./entry-html.js";
|
||||
import "./fetch-nightly-translations.js";
|
||||
import "./gallery.js";
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
// Gulp task to generate third-party license notices.
|
||||
|
||||
import { readFile, access } from "fs/promises";
|
||||
import { generateLicenseFile } from "generate-license-file";
|
||||
import gulp from "gulp";
|
||||
import path from "path";
|
||||
import paths from "../paths.cjs";
|
||||
|
||||
const OUTPUT_FILE = path.join(
|
||||
paths.app_output_static,
|
||||
"third-party-licenses.txt"
|
||||
);
|
||||
|
||||
// The echarts package ships an Apache-2.0 NOTICE file that must be
|
||||
// redistributed alongside the compiled output per Apache License §4(d).
|
||||
const NOTICE_FILES = [
|
||||
path.resolve(paths.root_dir, "node_modules/echarts/NOTICE"),
|
||||
];
|
||||
|
||||
// type-fest ships two license files (MIT for code, CC0 for types).
|
||||
// We use the MIT license since that covers the bundled code.
|
||||
//
|
||||
// Each entry is pinned to a specific version. If a package is updated,
|
||||
// this list must be reviewed and the version updated after verifying
|
||||
// that the new version's license still matches. The build will fail
|
||||
// if the installed version does not match the pinned version.
|
||||
const LICENSE_OVERRIDES = [
|
||||
{
|
||||
// type-fest ships two license files (MIT for code, CC0 for types).
|
||||
// We use the MIT license since that covers the bundled code.
|
||||
packageName: "type-fest",
|
||||
version: "5.6.0",
|
||||
licensePath: path.resolve(
|
||||
paths.root_dir,
|
||||
"node_modules/type-fest/license-mit"
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
gulp.task("gen-licenses", async () => {
|
||||
const licenseOverrides = {};
|
||||
|
||||
for (const { packageName, version, licensePath } of LICENSE_OVERRIDES) {
|
||||
const pkgJsonPath = path.resolve(
|
||||
paths.root_dir,
|
||||
`node_modules/${packageName}/package.json`
|
||||
);
|
||||
|
||||
let packageJSON;
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
packageJSON = JSON.parse(await readFile(pkgJsonPath, "utf-8"));
|
||||
} catch {
|
||||
throw new Error(
|
||||
`package.json for "${packageName}" not found or unreadable at ${pkgJsonPath}`
|
||||
);
|
||||
}
|
||||
|
||||
if (packageJSON.version !== version) {
|
||||
throw new Error(
|
||||
`License override for "${packageName}" is pinned to version ${version}, but found version ${packageJSON.version}. ` +
|
||||
`Please verify the new version's license and update the override in build-scripts/gulp/licenses.js.`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await access(licensePath);
|
||||
} catch {
|
||||
throw new Error(`License file not found or unreadable: ${licensePath}`);
|
||||
}
|
||||
|
||||
licenseOverrides[`${packageName}@${version}`] = licensePath;
|
||||
}
|
||||
|
||||
await generateLicenseFile(
|
||||
path.resolve(paths.root_dir, "package.json"),
|
||||
OUTPUT_FILE,
|
||||
{ append: NOTICE_FILES, replace: licenseOverrides }
|
||||
);
|
||||
});
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
createDemoConfig,
|
||||
createGalleryConfig,
|
||||
createLandingPageConfig,
|
||||
createE2eTestAppConfig,
|
||||
} from "../rspack.cjs";
|
||||
|
||||
const bothBuilds = (createConfigFunc, params) => [
|
||||
@@ -210,3 +211,22 @@ gulp.task("rspack-prod-landing-page", () =>
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("rspack-dev-server-e2e-test-app", () =>
|
||||
runDevServer({
|
||||
compiler: rspack(
|
||||
createE2eTestAppConfig({ isProdBuild: false, latestBuild: true })
|
||||
),
|
||||
contentBase: paths.e2eTestApp_output_root,
|
||||
port: 8095,
|
||||
})
|
||||
);
|
||||
|
||||
gulp.task("rspack-prod-e2e-test-app", () =>
|
||||
prodBuild(
|
||||
bothBuilds(createE2eTestAppConfig, {
|
||||
isProdBuild: true,
|
||||
isStatsBuild: env.isStatsBuild(),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
@@ -50,4 +50,15 @@ module.exports = {
|
||||
),
|
||||
|
||||
translations_src: path.resolve(__dirname, "../src/translations"),
|
||||
|
||||
e2eTestApp_dir: path.resolve(__dirname, "../test/e2e/app"),
|
||||
e2eTestApp_output_root: path.resolve(__dirname, "../test/e2e/app/dist"),
|
||||
e2eTestApp_output_static: path.resolve(
|
||||
__dirname,
|
||||
"../test/e2e/app/dist/static"
|
||||
),
|
||||
e2eTestApp_output_latest: path.resolve(
|
||||
__dirname,
|
||||
"../test/e2e/app/dist/frontend_latest"
|
||||
),
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* global require, module, __dirname */
|
||||
const { existsSync } = require("fs");
|
||||
const path = require("path");
|
||||
const rspack = require("@rspack/core");
|
||||
@@ -338,6 +337,11 @@ const createGalleryConfig = ({ isProdBuild, latestBuild }) =>
|
||||
const createLandingPageConfig = ({ isProdBuild, latestBuild }) =>
|
||||
createRspackConfig(bundle.config.landingPage({ isProdBuild, latestBuild }));
|
||||
|
||||
const createE2eTestAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) =>
|
||||
createRspackConfig(
|
||||
bundle.config.e2eTestApp({ isProdBuild, latestBuild, isStatsBuild })
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
createAppConfig,
|
||||
createDemoConfig,
|
||||
@@ -345,4 +349,5 @@ module.exports = {
|
||||
createGalleryConfig,
|
||||
createRspackConfig,
|
||||
createLandingPageConfig,
|
||||
createE2eTestAppConfig,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockAssist = (hass: MockHomeAssistant) => {
|
||||
// Stub for assist pipeline list — returns empty so developer tools assist
|
||||
// tab loads without errors.
|
||||
hass.mockWS("assist_pipeline/pipeline/list", () => ({
|
||||
pipelines: [],
|
||||
preferred_pipeline: null,
|
||||
}));
|
||||
|
||||
// Stub for assist pipeline run — immediately sends run-end event so
|
||||
// the UI does not hang waiting for a response.
|
||||
hass.mockWS("assist_pipeline/run", (_msg, _hass, onChange) => {
|
||||
if (onChange) {
|
||||
onChange({
|
||||
type: "run-end",
|
||||
});
|
||||
}
|
||||
return null;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockCloud = (hass: MockHomeAssistant) => {
|
||||
// REST mock for cloud status — returns disconnected so config panel loads
|
||||
// without errors but without requiring cloud integration.
|
||||
hass.mockAPI("cloud/status", () => ({
|
||||
logged_in: false,
|
||||
cloud: "disconnected",
|
||||
prefs: {
|
||||
google_enabled: false,
|
||||
alexa_enabled: false,
|
||||
cloudhooks: {},
|
||||
remote_enabled: false,
|
||||
},
|
||||
google_registered: false,
|
||||
alexa_registered: false,
|
||||
remote_domain: null,
|
||||
remote_connected: false,
|
||||
remote_certificate: null,
|
||||
}));
|
||||
};
|
||||
+18
-25
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
addDays,
|
||||
addHours,
|
||||
addMinutes,
|
||||
addMonths,
|
||||
differenceInHours,
|
||||
endOfDay,
|
||||
@@ -13,19 +12,6 @@ import type {
|
||||
} from "../../../src/data/recorder";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
const getNextDate = (
|
||||
currentDate: Date,
|
||||
period: "5minute" | "hour" | "day" | "month"
|
||||
): Date => {
|
||||
return period === "day"
|
||||
? addDays(currentDate, 1)
|
||||
: period === "month"
|
||||
? addMonths(currentDate, 1)
|
||||
: period === "hour"
|
||||
? addHours(currentDate, 1)
|
||||
: addMinutes(currentDate, 5);
|
||||
};
|
||||
|
||||
const generateMeanStatistics = (
|
||||
start: Date,
|
||||
end: Date,
|
||||
@@ -39,10 +25,9 @@ const generateMeanStatistics = (
|
||||
while (end > currentDate && currentDate < now) {
|
||||
const delta = Math.random() * maxDiff;
|
||||
const mean = delta;
|
||||
const nextDate = getNextDate(currentDate, period);
|
||||
statistics.push({
|
||||
start: currentDate.getTime(),
|
||||
end: nextDate.getTime(),
|
||||
end: currentDate.getTime(),
|
||||
mean,
|
||||
min: mean - Math.random() * maxDiff,
|
||||
max: mean + Math.random() * maxDiff,
|
||||
@@ -50,7 +35,12 @@ const generateMeanStatistics = (
|
||||
state: mean,
|
||||
sum: null,
|
||||
});
|
||||
currentDate = nextDate;
|
||||
currentDate =
|
||||
period === "day"
|
||||
? addDays(currentDate, 1)
|
||||
: period === "month"
|
||||
? addMonths(currentDate, 1)
|
||||
: addHours(currentDate, 1);
|
||||
}
|
||||
return statistics;
|
||||
};
|
||||
@@ -68,12 +58,11 @@ const generateSumStatistics = (
|
||||
let sum = initValue;
|
||||
const now = new Date();
|
||||
while (end > currentDate && currentDate < now) {
|
||||
const nextDate = getNextDate(currentDate, period);
|
||||
const add = Math.random() * maxDiff;
|
||||
sum += add;
|
||||
statistics.push({
|
||||
start: currentDate.getTime(),
|
||||
end: nextDate.getTime(),
|
||||
end: currentDate.getTime(),
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
@@ -82,7 +71,12 @@ const generateSumStatistics = (
|
||||
state: initValue + sum,
|
||||
sum,
|
||||
});
|
||||
currentDate = nextDate;
|
||||
currentDate =
|
||||
period === "day"
|
||||
? addDays(currentDate, 1)
|
||||
: period === "month"
|
||||
? addMonths(currentDate, 1)
|
||||
: addHours(currentDate, 1);
|
||||
}
|
||||
return statistics;
|
||||
};
|
||||
@@ -90,7 +84,7 @@ const generateSumStatistics = (
|
||||
const generateCurvedStatistics = (
|
||||
start: Date,
|
||||
end: Date,
|
||||
period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||
_period: "5minute" | "hour" | "day" | "month" = "hour",
|
||||
initValue: number,
|
||||
maxDiff: number,
|
||||
metered: boolean
|
||||
@@ -104,12 +98,11 @@ const generateCurvedStatistics = (
|
||||
let half = false;
|
||||
const now = new Date();
|
||||
while (end > currentDate && currentDate < now) {
|
||||
const nextDate = getNextDate(currentDate, period);
|
||||
const add = i * (Math.random() * maxDiff);
|
||||
sum += add;
|
||||
statistics.push({
|
||||
start: currentDate.getTime(),
|
||||
end: nextDate.getTime(),
|
||||
end: currentDate.getTime(),
|
||||
mean: null,
|
||||
min: null,
|
||||
max: null,
|
||||
@@ -118,7 +111,7 @@ const generateCurvedStatistics = (
|
||||
state: initValue + sum,
|
||||
sum: metered ? sum : null,
|
||||
});
|
||||
currentDate = nextDate;
|
||||
currentDate = addHours(currentDate, 1);
|
||||
if (!half && i > hours / 2) {
|
||||
half = true;
|
||||
}
|
||||
@@ -296,7 +289,7 @@ const statisticsFunctions: Record<
|
||||
end,
|
||||
period,
|
||||
productionFinalVal,
|
||||
0.2
|
||||
2
|
||||
);
|
||||
return [...morning, ...production, ...evening, ...rest];
|
||||
},
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockUpdate = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS("update/list", () => []);
|
||||
};
|
||||
@@ -228,6 +228,12 @@ export default tseslint.config(
|
||||
globals: globals.serviceworker,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["test/e2e/*.mjs"],
|
||||
languageOptions: {
|
||||
globals: globals.node,
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
html,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@material/mwc-drawer";
|
||||
import "@material/mwc-top-app-bar-fixed";
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
+10
-25
@@ -1,3 +1,4 @@
|
||||
import "@material/mwc-drawer";
|
||||
import "@material/mwc-top-app-bar-fixed";
|
||||
import { mdiMenu, mdiSwapHorizontal } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
@@ -6,8 +7,6 @@ import { customElement, query, state } from "lit/decorators";
|
||||
import { dynamicElement } from "../../src/common/dom/dynamic-element-directive";
|
||||
import { setDirectionStyles } from "../../src/common/util/compute_rtl";
|
||||
import "../../src/components/ha-button";
|
||||
import "../../src/components/ha-drawer";
|
||||
import type { HaDrawer } from "../../src/components/ha-drawer";
|
||||
import { HaExpansionPanel } from "../../src/components/ha-expansion-panel";
|
||||
import "../../src/components/ha-icon-button";
|
||||
import "../../src/components/ha-svg-icon";
|
||||
@@ -40,8 +39,8 @@ class HaGallery extends LitElement {
|
||||
@query("notification-manager")
|
||||
private _notifications!: HTMLElementTagNameMap["notification-manager"];
|
||||
|
||||
@query("ha-drawer")
|
||||
private _drawer!: HaDrawer;
|
||||
@query("mwc-drawer")
|
||||
private _drawer!: HTMLElementTagNameMap["mwc-drawer"];
|
||||
|
||||
private _narrow = window.matchMedia("(max-width: 600px)").matches;
|
||||
|
||||
@@ -76,14 +75,15 @@ class HaGallery extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-drawer
|
||||
.direction=${this._rtl ? "rtl" : "ltr"}
|
||||
<mwc-drawer
|
||||
hasHeader
|
||||
.open=${!this._narrow}
|
||||
.type=${this._narrow ? "modal" : "dismissible"}
|
||||
>
|
||||
<div class="drawer-title">Home Assistant Design</div>
|
||||
<span slot="title">Home Assistant Design</span>
|
||||
<!-- <span slot="subtitle">subtitle</span> -->
|
||||
<div class="sidebar">${sidebar}</div>
|
||||
<div slot="appContent" class="app-content">
|
||||
<div slot="appContent">
|
||||
<mwc-top-app-bar-fixed>
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
@@ -144,7 +144,7 @@ class HaGallery extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ha-drawer>
|
||||
</mwc-drawer>
|
||||
<notification-manager
|
||||
.hass=${FAKE_HASS}
|
||||
id="notifications"
|
||||
@@ -226,27 +226,12 @@ class HaGallery extends LitElement {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
--ha-sidebar-width: 256px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
box-sizing: border-box;
|
||||
max-height: calc(100vh - 64px);
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.drawer-title {
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
color: var(--primary-text-color);
|
||||
display: flex;
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
min-height: 64px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.sidebar a {
|
||||
color: var(--primary-text-color);
|
||||
display: block;
|
||||
@@ -270,7 +255,7 @@ class HaGallery extends LitElement {
|
||||
opacity: 0.12;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
div[slot="appContent"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
|
||||
+46
-37
@@ -14,14 +14,22 @@
|
||||
"format:prettier": "prettier . --cache --write",
|
||||
"lint:types": "tsc",
|
||||
"lint:lit": "lit-analyzer \"{.,*}/src/**/*.ts\"",
|
||||
"lint:licenses": "node --no-deprecation script/check-licenses",
|
||||
"lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:types && yarn run lint:lit",
|
||||
"format": "yarn run format:eslint && yarn run format:prettier",
|
||||
"postinstall": "husky",
|
||||
"prepack": "pinst --disable",
|
||||
"postpack": "pinst --enable",
|
||||
"test": "vitest run --config test/vitest.config.ts",
|
||||
"test:coverage": "vitest run --config test/vitest.config.ts --coverage"
|
||||
"test:coverage": "vitest run --config test/vitest.config.ts --coverage",
|
||||
"test:e2e": "node test/e2e/run-suites.mjs demo app gallery",
|
||||
"test:e2e:browserstack": "node test/e2e/run-suites.mjs demo:browserstack app:browserstack gallery:browserstack",
|
||||
"test:e2e:show-report": "yarn playwright show-report test/e2e/reports/combined",
|
||||
"test:e2e:demo": "playwright test --config test/e2e/playwright.demo.config.ts",
|
||||
"test:e2e:demo:browserstack": "browserstack-node-sdk playwright test --config test/e2e/playwright.demo.config.ts",
|
||||
"test:e2e:app": "playwright test --config test/e2e/playwright.app.config.ts",
|
||||
"test:e2e:app:browserstack": "browserstack-node-sdk playwright test --config test/e2e/playwright.app.config.ts",
|
||||
"test:e2e:gallery": "playwright test --config test/e2e/playwright.gallery.config.ts",
|
||||
"test:e2e:gallery:browserstack": "browserstack-node-sdk playwright test --config test/e2e/playwright.gallery.config.ts"
|
||||
},
|
||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||
"license": "Apache-2.0",
|
||||
@@ -29,26 +37,26 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.29.2",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.2",
|
||||
"@codemirror/autocomplete": "6.20.1",
|
||||
"@codemirror/commands": "6.10.3",
|
||||
"@codemirror/lang-jinja": "6.0.1",
|
||||
"@codemirror/lang-yaml": "6.1.3",
|
||||
"@codemirror/language": "6.12.3",
|
||||
"@codemirror/lint": "6.9.6",
|
||||
"@codemirror/lint": "6.9.5",
|
||||
"@codemirror/search": "6.7.0",
|
||||
"@codemirror/state": "6.6.0",
|
||||
"@codemirror/view": "6.42.1",
|
||||
"@codemirror/view": "6.41.1",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.4.2",
|
||||
"@formatjs/intl-displaynames": "7.3.5",
|
||||
"@formatjs/intl-durationformat": "0.10.8",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.6",
|
||||
"@formatjs/intl-listformat": "8.3.5",
|
||||
"@formatjs/intl-locale": "5.3.5",
|
||||
"@formatjs/intl-numberformat": "9.3.5",
|
||||
"@formatjs/intl-pluralrules": "6.3.5",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.5",
|
||||
"@formatjs/intl-datetimeformat": "7.4.1",
|
||||
"@formatjs/intl-displaynames": "7.3.4",
|
||||
"@formatjs/intl-durationformat": "0.10.7",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.5",
|
||||
"@formatjs/intl-listformat": "8.3.4",
|
||||
"@formatjs/intl-locale": "5.3.4",
|
||||
"@formatjs/intl-numberformat": "9.3.4",
|
||||
"@formatjs/intl-pluralrules": "6.3.4",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.4",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
@@ -64,6 +72,7 @@
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/mwc-base": "0.27.0",
|
||||
"@material/mwc-drawer": "0.27.0",
|
||||
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch",
|
||||
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
@@ -80,7 +89,7 @@
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"barcode-detector": "3.1.3",
|
||||
"barcode-detector": "3.1.2",
|
||||
"cally": "0.9.2",
|
||||
"color-name": "2.1.0",
|
||||
"comlink": "4.4.2",
|
||||
@@ -99,7 +108,7 @@
|
||||
"hls.js": "1.6.16",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "11.2.4",
|
||||
"intl-messageformat": "11.2.3",
|
||||
"js-yaml": "4.1.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
@@ -121,28 +130,29 @@
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"weekstart": "2.0.0",
|
||||
"workbox-cacheable-response": "7.4.1",
|
||||
"workbox-core": "7.4.1",
|
||||
"workbox-expiration": "7.4.1",
|
||||
"workbox-precaching": "7.4.1",
|
||||
"workbox-routing": "7.4.1",
|
||||
"workbox-strategies": "7.4.1",
|
||||
"workbox-cacheable-response": "7.4.0",
|
||||
"workbox-core": "7.4.0",
|
||||
"workbox-expiration": "7.4.0",
|
||||
"workbox-precaching": "7.4.0",
|
||||
"workbox-routing": "7.4.0",
|
||||
"workbox-strategies": "7.4.0",
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.8",
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.5",
|
||||
"@babel/preset-env": "7.29.3",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.1",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.60.0",
|
||||
"@lokalise/node-api": "16.0.0",
|
||||
"@lokalise/node-api": "15.7.1",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.11",
|
||||
"@rspack/core": "2.0.2",
|
||||
"@playwright/test": "1.59.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.9",
|
||||
"@rspack/core": "2.0.1",
|
||||
"@rspack/dev-server": "2.0.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
@@ -161,10 +171,11 @@
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "7.0.87",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.1.6",
|
||||
"@vitest/coverage-v8": "4.1.5",
|
||||
"babel-loader": "10.1.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"browserstack-node-sdk": "1.53.2",
|
||||
"del": "8.0.1",
|
||||
"eslint": "10.3.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
@@ -175,8 +186,7 @@
|
||||
"eslint-plugin-unused-imports": "4.4.1",
|
||||
"eslint-plugin-wc": "3.1.0",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.5",
|
||||
"generate-license-file": "4.1.1",
|
||||
"fs-extra": "11.3.4",
|
||||
"glob": "13.0.6",
|
||||
"globals": "17.6.0",
|
||||
"gulp": "5.0.1",
|
||||
@@ -187,8 +197,7 @@
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "29.1.1",
|
||||
"jszip": "3.10.1",
|
||||
"license-checker-rseidelsohn": "4.4.2",
|
||||
"lint-staged": "17.0.4",
|
||||
"lint-staged": "16.4.0",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.18.1",
|
||||
@@ -197,17 +206,17 @@
|
||||
"prettier": "3.8.3",
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"serve": "14.2.6",
|
||||
"sinon": "22.0.0",
|
||||
"tar": "7.5.15",
|
||||
"terser-webpack-plugin": "5.6.0",
|
||||
"sinon": "21.1.2",
|
||||
"tar": "7.5.13",
|
||||
"terser-webpack-plugin": "5.5.0",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.59.3",
|
||||
"typescript-eslint": "8.59.1",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.6",
|
||||
"vitest": "4.1.5",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.4.1#~/.yarn/patches/workbox-build-npm-7.4.1-c84561662c.patch"
|
||||
"workbox-build": "patch:workbox-build@npm%3A7.4.0#~/.yarn/patches/workbox-build-npm-7.4.0-c84561662c.patch"
|
||||
},
|
||||
"resolutions": {
|
||||
"lit": "3.3.2",
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// Checks that all production dependencies use approved open-source licenses.
|
||||
//
|
||||
// To allow a new license type, add its SPDX identifier to ALLOWED_LICENSES.
|
||||
// To allow a specific package that cannot be relicensed (e.g. a dual-license
|
||||
// package where the reported identifier is non-standard), add it to
|
||||
// ALLOWED_PACKAGES with a comment explaining why.
|
||||
|
||||
import { createRequire } from "module";
|
||||
import { fileURLToPath } from "url";
|
||||
import path from "path";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const checker = require("license-checker-rseidelsohn");
|
||||
const root = path.resolve(fileURLToPath(import.meta.url), "../../");
|
||||
|
||||
// Permissive licenses that are compatible with distribution in a compiled wheel.
|
||||
// Copyleft licenses (GPL, LGPL, AGPL, EUPL, etc.) must NOT be added here.
|
||||
const ALLOWED_LICENSES = new Set([
|
||||
"MIT",
|
||||
"MIT*",
|
||||
"ISC",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSD*",
|
||||
"Apache-2.0",
|
||||
"0BSD",
|
||||
"CC0-1.0",
|
||||
"(MIT OR CC0-1.0)",
|
||||
"(MIT AND Zlib)",
|
||||
"Python-2.0", // argparse - Python Software Foundation License (permissive)
|
||||
"Public Domain",
|
||||
"W3C-20150513", // wicg-inert - W3C Software and Document License (permissive)
|
||||
"Unlicense",
|
||||
"CC-BY-4.0",
|
||||
]);
|
||||
|
||||
// Packages whose license identifier is ambiguous or non-standard but have been
|
||||
// manually verified as permissive. Add only when strictly necessary.
|
||||
const ALLOWED_PACKAGES = {
|
||||
// No entries currently needed.
|
||||
};
|
||||
|
||||
checker.init(
|
||||
{
|
||||
start: root,
|
||||
production: true,
|
||||
excludePrivatePackages: true,
|
||||
},
|
||||
(err, packages) => {
|
||||
if (err) {
|
||||
console.error("license-checker failed:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const violations = [];
|
||||
|
||||
for (const [nameAtVersion, info] of Object.entries(packages)) {
|
||||
if (nameAtVersion in ALLOWED_PACKAGES) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const license = info.licenses;
|
||||
|
||||
if (!ALLOWED_LICENSES.has(license)) {
|
||||
violations.push({ package: nameAtVersion, license });
|
||||
}
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error(
|
||||
"The following packages have licenses that are not on the allowlist:"
|
||||
);
|
||||
for (const { package: pkg, license } of violations) {
|
||||
console.error(` ${pkg}: ${license}`);
|
||||
}
|
||||
console.error(`
|
||||
If the license is permissive and appropriate for distribution, add it
|
||||
to ALLOWED_LICENSES in script/check-licenses. If it is a specific
|
||||
package with an ambiguous identifier, add it to ALLOWED_PACKAGES.
|
||||
|
||||
Do NOT add copyleft licenses (GPL, LGPL, AGPL, etc.) to the allowlist.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const count = Object.keys(packages).length;
|
||||
console.log(
|
||||
`License check passed: all ${count} production dependencies use approved licenses.`
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -5,6 +5,7 @@ import { isComponentLoaded } from "./is_component_loaded";
|
||||
|
||||
export const canShowPage = (hass: HomeAssistant, page: PageNavigation) =>
|
||||
(isCore(page) || isLoadedIntegration(hass, page)) &&
|
||||
!hideAdvancedPage(hass, page) &&
|
||||
isNotLoadedIntegration(hass, page);
|
||||
|
||||
export const isLoadedIntegration = (
|
||||
@@ -26,3 +27,8 @@ export const isNotLoadedIntegration = (
|
||||
);
|
||||
|
||||
export const isCore = (page: PageNavigation) => page.core;
|
||||
export const isAdvancedPage = (page: PageNavigation) => page.advancedOnly;
|
||||
export const userWantsAdvanced = (hass: HomeAssistant) =>
|
||||
hass.userData?.showAdvanced;
|
||||
export const hideAdvancedPage = (hass: HomeAssistant, page: PageNavigation) =>
|
||||
isAdvancedPage(page) && !userWantsAdvanced(hass);
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* Walks up the composed tree (jumping shadow roots → their hosts), returning
|
||||
* the ancestor chain top-down. Used to compare two nodes that may live in
|
||||
* different shadow trees — `Node.compareDocumentPosition` only works within a
|
||||
* single root and returns `DOCUMENT_POSITION_DISCONNECTED` otherwise.
|
||||
*/
|
||||
const composedAncestorPath = (node: Node): Node[] => {
|
||||
const path: Node[] = [];
|
||||
let cur: Node | null = node;
|
||||
while (cur) {
|
||||
path.push(cur);
|
||||
const parent = cur.parentNode;
|
||||
if (parent instanceof ShadowRoot) {
|
||||
cur = parent.host;
|
||||
} else if (parent) {
|
||||
cur = parent;
|
||||
} else {
|
||||
const root = cur.getRootNode();
|
||||
cur = root instanceof ShadowRoot ? root.host : null;
|
||||
}
|
||||
}
|
||||
return path.reverse();
|
||||
};
|
||||
|
||||
/**
|
||||
* Document-order comparator that works across shadow boundaries. Suitable as
|
||||
* the `Array.prototype.sort` callback for collections of nodes that may live
|
||||
* in different shadow trees.
|
||||
*/
|
||||
export const compareNodeOrder = (a: Node, b: Node): number => {
|
||||
if (a === b) {
|
||||
return 0;
|
||||
}
|
||||
const pa = composedAncestorPath(a);
|
||||
const pb = composedAncestorPath(b);
|
||||
let i = 0;
|
||||
while (i < pa.length && i < pb.length && pa[i] === pb[i]) {
|
||||
i++;
|
||||
}
|
||||
if (i === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (i === pa.length) {
|
||||
return -1;
|
||||
}
|
||||
if (i === pb.length) {
|
||||
return 1;
|
||||
}
|
||||
// pa[i] and pb[i] are siblings under the LCA, guaranteed same root.
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return pa[i].compareDocumentPosition(pb[i]) & Node.DOCUMENT_POSITION_FOLLOWING
|
||||
? -1
|
||||
: 1;
|
||||
};
|
||||
@@ -1,17 +1,3 @@
|
||||
import {
|
||||
mdiBattery,
|
||||
mdiBattery10,
|
||||
mdiBattery20,
|
||||
mdiBattery30,
|
||||
mdiBattery40,
|
||||
mdiBattery50,
|
||||
mdiBattery60,
|
||||
mdiBattery70,
|
||||
mdiBattery80,
|
||||
mdiBattery90,
|
||||
mdiBatteryAlertVariantOutline,
|
||||
mdiBatteryUnknown,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
const BATTERY_ICONS = {
|
||||
@@ -26,18 +12,6 @@ const BATTERY_ICONS = {
|
||||
90: "mdi:battery-90",
|
||||
100: "mdi:battery",
|
||||
};
|
||||
const BATTERY_ICON_PATHS = {
|
||||
10: mdiBattery10,
|
||||
20: mdiBattery20,
|
||||
30: mdiBattery30,
|
||||
40: mdiBattery40,
|
||||
50: mdiBattery50,
|
||||
60: mdiBattery60,
|
||||
70: mdiBattery70,
|
||||
80: mdiBattery80,
|
||||
90: mdiBattery90,
|
||||
100: mdiBattery,
|
||||
};
|
||||
const BATTERY_CHARGING_ICONS = {
|
||||
10: "mdi:battery-charging-10",
|
||||
20: "mdi:battery-charging-20",
|
||||
@@ -83,15 +57,3 @@ export const batteryLevelIcon = (
|
||||
}
|
||||
return BATTERY_ICONS[batteryRound];
|
||||
};
|
||||
|
||||
export const batteryLevelIconPath = (batteryLevel: number | string): string => {
|
||||
const batteryValue = Number(batteryLevel);
|
||||
if (isNaN(batteryValue)) {
|
||||
return mdiBatteryUnknown;
|
||||
}
|
||||
if (batteryValue <= 5) {
|
||||
return mdiBatteryAlertVariantOutline;
|
||||
}
|
||||
const batteryRound = Math.round(batteryValue / 10) * 10;
|
||||
return BATTERY_ICON_PATHS[batteryRound];
|
||||
};
|
||||
|
||||
@@ -137,10 +137,7 @@ export const computeEntityPickerDisplay = (
|
||||
hass.floors
|
||||
);
|
||||
|
||||
const isRTL = computeRTL(
|
||||
hass.language,
|
||||
hass.translationMetadata.translations
|
||||
);
|
||||
const isRTL = computeRTL(hass);
|
||||
|
||||
const primary = entityName || deviceName || stateObj.entity_id;
|
||||
const secondary =
|
||||
|
||||
@@ -117,6 +117,9 @@ export const generateEntityFilter = (
|
||||
}
|
||||
}
|
||||
if (entityCategories) {
|
||||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
const category = entity?.entity_category || "none";
|
||||
if (!entityCategories.has(category)) {
|
||||
return false;
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import type { LitElement } from "lit";
|
||||
import type { HomeAssistant, Translation } from "../../types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
export function computeRTL(
|
||||
language = "en",
|
||||
translations: Record<string, Translation>
|
||||
) {
|
||||
if (translations[language]) {
|
||||
return translations[language].isRTL || false;
|
||||
export function computeRTL(hass: HomeAssistant) {
|
||||
const lang = hass.language || "en";
|
||||
if (hass.translationMetadata.translations[lang]) {
|
||||
return hass.translationMetadata.translations[lang].isRTL || false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function computeRTLDirection(hass: HomeAssistant) {
|
||||
return emitRTLDirection(
|
||||
computeRTL(hass.language, hass.translationMetadata.translations)
|
||||
);
|
||||
return emitRTLDirection(computeRTL(hass));
|
||||
}
|
||||
|
||||
export function emitRTLDirection(rtl: boolean) {
|
||||
|
||||
@@ -121,7 +121,6 @@ export class HaAutomationRowEventChip extends LitElement {
|
||||
align-items: center;
|
||||
--mdc-icon-size: 16px;
|
||||
line-height: 1;
|
||||
box-shadow: var(--ha-box-shadow-s);
|
||||
}
|
||||
|
||||
button {
|
||||
|
||||
@@ -124,7 +124,6 @@ export class HaAutomationRow extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
@@ -195,6 +194,7 @@ export class HaAutomationRow extends LitElement {
|
||||
}
|
||||
::slotted([slot="event"]) {
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
.icons {
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import type { TooltipPositionCallback } from "echarts/types/dist/shared";
|
||||
|
||||
export const TOOLTIP_GAP_PX = 12;
|
||||
export const TOOLTIP_TOP_OFFSET_PX = 10;
|
||||
|
||||
/**
|
||||
* Pins the tooltip near the top of the chart and offsets it horizontally
|
||||
* from the cursor so it never covers the data point being inspected.
|
||||
* For axis-trigger time-series tooltips where the cursor's Y is uncorrelated
|
||||
* with the displayed content.
|
||||
*/
|
||||
export const sideTooltipPosition: TooltipPositionCallback = (
|
||||
point,
|
||||
_params,
|
||||
dom,
|
||||
_rect,
|
||||
size
|
||||
) => {
|
||||
const [cursorX] = point;
|
||||
const [viewW, viewH] = size.viewSize;
|
||||
const [tipW, tipH] = size.contentSize;
|
||||
|
||||
const rtl =
|
||||
dom instanceof HTMLElement && getComputedStyle(dom).direction === "rtl";
|
||||
|
||||
const rightOfCursor = cursorX + TOOLTIP_GAP_PX;
|
||||
const leftOfCursor = cursorX - TOOLTIP_GAP_PX - tipW;
|
||||
|
||||
let x = rtl ? leftOfCursor : rightOfCursor;
|
||||
const overflowsRight = x + tipW > viewW;
|
||||
const overflowsLeft = x < 0;
|
||||
if (overflowsRight || overflowsLeft) {
|
||||
x = rtl ? rightOfCursor : leftOfCursor;
|
||||
}
|
||||
x = Math.max(0, Math.min(x, viewW - tipW));
|
||||
|
||||
const y = Math.max(0, Math.min(TOOLTIP_TOP_OFFSET_PX, viewH - tipH));
|
||||
|
||||
return [x, y];
|
||||
};
|
||||
@@ -1110,7 +1110,7 @@ export class HaChartBase extends LitElement {
|
||||
private _updateSankeyRoam() {
|
||||
const option = this.chart?.getOption();
|
||||
const sankeySeries = (option?.series as any[])?.filter(
|
||||
(s: any) => s != null && s.type === "sankey"
|
||||
(s: any) => s.type === "sankey"
|
||||
);
|
||||
if (sankeySeries?.length) {
|
||||
this.chart?.setOption({
|
||||
|
||||
@@ -11,7 +11,6 @@ import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { LineChartEntity, LineChartState } from "../../data/history";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import {
|
||||
@@ -294,10 +293,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
(changedProps.has("hass") &&
|
||||
this._hasEntityStatesChanged(changedProps.get("hass")))
|
||||
) {
|
||||
const rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
const rtl = computeRTL(this.hass);
|
||||
let minYAxis: number | ((values: { min: number }) => number) | undefined =
|
||||
this.minYAxis;
|
||||
let maxYAxis: number | ((values: { max: number }) => number) | undefined =
|
||||
@@ -414,7 +410,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
renderMode: "html",
|
||||
position: sideTooltipPosition,
|
||||
position: "bottom",
|
||||
align: "center",
|
||||
confine: true,
|
||||
formatter: this._renderTooltip,
|
||||
},
|
||||
|
||||
@@ -14,7 +14,6 @@ import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { TimelineEntity } from "../../data/history";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import { computeTimelineColor } from "./timeline-color";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import echarts from "../../resources/echarts/echarts";
|
||||
@@ -145,10 +144,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
"ui.components.history_charts.duration"
|
||||
)}: ${millisecondsToDuration(durationInMs)}`;
|
||||
|
||||
const markerLocalized = !computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
const markerLocalized = !computeRTL(this.hass)
|
||||
? marker
|
||||
: `<span style="direction: rtl;display:inline-block;margin-right:4px;margin-inline-end:4px;border-radius:10px;width:10px;height:10px;background-color:${color};"></span>`;
|
||||
|
||||
@@ -171,12 +167,11 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (
|
||||
this.isConnected &&
|
||||
(changedProps.has("startTime") ||
|
||||
changedProps.has("endTime") ||
|
||||
changedProps.has("data") ||
|
||||
this._chartTime <
|
||||
new Date(this.endTime.getTime() - MIN_TIME_BETWEEN_UPDATES))
|
||||
changedProps.has("startTime") ||
|
||||
changedProps.has("endTime") ||
|
||||
changedProps.has("data") ||
|
||||
this._chartTime <
|
||||
new Date(this.endTime.getTime() - MIN_TIME_BETWEEN_UPDATES)
|
||||
) {
|
||||
// If the line is more than 5 minutes old, re-gen it
|
||||
// so the X axis grows even if there is no new data
|
||||
@@ -203,10 +198,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
? Math.max(this.paddingYAxis, this._yWidth)
|
||||
: 0;
|
||||
const labelMargin = 5;
|
||||
const rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
const rtl = computeRTL(this.hass);
|
||||
this._chartOptions = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
@@ -264,7 +256,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
},
|
||||
tooltip: {
|
||||
renderMode: "html",
|
||||
position: sideTooltipPosition,
|
||||
position: "bottom",
|
||||
align: "center",
|
||||
confine: true,
|
||||
formatter: this._renderTooltip,
|
||||
},
|
||||
|
||||
@@ -13,9 +13,7 @@ import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
import { formatDate } from "../../common/datetime/format_date";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import { formatTimeWithSeconds } from "../../common/datetime/format_time";
|
||||
import {
|
||||
formatNumber,
|
||||
getNumberFormatOptions,
|
||||
@@ -39,7 +37,6 @@ import type { HomeAssistant } from "../../types";
|
||||
import { getPeriodicAxisLabelConfig } from "./axis-label";
|
||||
import type { CustomLegendOption } from "./ha-chart-base";
|
||||
import "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import { fillDataGapsAndRoundCaps } from "./round-caps";
|
||||
|
||||
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
||||
@@ -244,8 +241,6 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
private _renderTooltip = (params: any) => {
|
||||
const rendered: Record<string, boolean> = {};
|
||||
const chartIsBar = this.chartType.startsWith("bar");
|
||||
const period = this.period;
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||
: "";
|
||||
@@ -257,67 +252,8 @@ export class StatisticsChart extends LitElement {
|
||||
const statisticId = this._statisticIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
const entry = this.hass.entities[statisticId];
|
||||
let rawValue: string;
|
||||
let rawTime: string;
|
||||
if (chartIsBar) {
|
||||
// For bar charts value is always second value.
|
||||
rawValue = String(param.value[1]);
|
||||
// Time value is third value (un-shifted date) if given, otherwise first value
|
||||
let startTime: Date;
|
||||
let endTime: Date | undefined;
|
||||
if (param.value[2]) {
|
||||
startTime = new Date(param.value[2]);
|
||||
if (param.value[3]) {
|
||||
endTime = new Date(param.value[3]);
|
||||
}
|
||||
} else {
|
||||
startTime = new Date(param.value[0]);
|
||||
}
|
||||
if (
|
||||
period === "year" ||
|
||||
period === "month" ||
|
||||
period === "week" ||
|
||||
period === "day"
|
||||
) {
|
||||
// For year/month/day periods, show only the date
|
||||
rawTime =
|
||||
formatDate(startTime, this.hass.locale, this.hass.config) +
|
||||
(endTime && period !== "day"
|
||||
? ` – ${formatDate(
|
||||
endTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}`
|
||||
: "") +
|
||||
"<br>";
|
||||
} else {
|
||||
// For other time periods, include time in render, and optionally show range
|
||||
// if we have an end time.
|
||||
rawTime =
|
||||
formatDateTimeWithSeconds(
|
||||
startTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
) +
|
||||
(endTime
|
||||
? ` – ${formatTimeWithSeconds(
|
||||
endTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}`
|
||||
: "") +
|
||||
"<br>";
|
||||
}
|
||||
} else {
|
||||
// For lines max series can have 3 values, as the second value is the max-min to form a band
|
||||
rawValue = String(param.value[2] ?? param.value[1]);
|
||||
// Time value is always first value
|
||||
rawTime = `${formatDateTimeWithSeconds(
|
||||
new Date(param.value[0]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)} <br>`;
|
||||
}
|
||||
// max series can have 3 values, as the second value is the max-min to form a band
|
||||
const rawValue = String(param.value[2] ?? param.value[1]);
|
||||
|
||||
const options = getNumberFormatOptions(stateObj, entry) ?? {
|
||||
maximumFractionDigits: 2,
|
||||
@@ -329,7 +265,14 @@ export class StatisticsChart extends LitElement {
|
||||
options
|
||||
)}${unit}`;
|
||||
|
||||
const time = index === 0 ? rawTime : "";
|
||||
const time =
|
||||
index === 0
|
||||
? formatDateTimeWithSeconds(
|
||||
new Date(param.value[0]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
) + "<br>"
|
||||
: "";
|
||||
return `${time}${param.marker} ${param.seriesName}: ${value}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
@@ -425,12 +368,7 @@ export class StatisticsChart extends LitElement {
|
||||
nameTextStyle: {
|
||||
align: "left",
|
||||
},
|
||||
position: computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? "right"
|
||||
: "left",
|
||||
position: computeRTL(this.hass) ? "right" : "left",
|
||||
scale:
|
||||
this.chartType.startsWith("line") ||
|
||||
this.logarithmicScale ||
|
||||
@@ -460,7 +398,8 @@ export class StatisticsChart extends LitElement {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
renderMode: "html",
|
||||
position: sideTooltipPosition,
|
||||
position: "bottom",
|
||||
align: "center",
|
||||
confine: true,
|
||||
formatter: this._renderTooltip,
|
||||
},
|
||||
@@ -567,53 +506,33 @@ export class StatisticsChart extends LitElement {
|
||||
const statDataSets: (LineSeriesOption | BarSeriesOption)[] = [];
|
||||
const statLegendData: typeof legendData = [];
|
||||
|
||||
// Place bars at centre of their specified time range if this is a bar chart
|
||||
// and the period is 5minute or hour.
|
||||
const centerBars =
|
||||
chartType === "bar" &&
|
||||
(this.period === "5minute" || this.period === "hour");
|
||||
|
||||
const pushData = (
|
||||
start: Date, // Data point start time
|
||||
end: Date, // Data point end time
|
||||
limit: Date, // Limit for end time (e.g. now)
|
||||
start: Date,
|
||||
end: Date,
|
||||
dataValues: (number | null)[][]
|
||||
) => {
|
||||
if (!dataValues.length) return;
|
||||
// Limit for time range is lesser of overall limit and data point end
|
||||
limit = end.getTime() < limit.getTime() ? end : limit;
|
||||
if (start.getTime() > limit.getTime()) {
|
||||
if (start > end) {
|
||||
// Drop data points that are after the requested endTime. This could happen if
|
||||
// endTime is "now" and client time is not in sync with server time.
|
||||
return;
|
||||
}
|
||||
statDataSets.forEach((d, i) => {
|
||||
if (chartType === "line") {
|
||||
if (
|
||||
prevEndTime &&
|
||||
prevValues &&
|
||||
prevEndTime.getTime() !== start.getTime()
|
||||
) {
|
||||
// if the end of the previous data doesn't match the start of the current data,
|
||||
// we have to draw a gap so add a value at the end time, and then an empty value.
|
||||
d.data!.push([prevEndTime, ...prevValues[i]!]);
|
||||
d.data!.push([prevEndTime, null]);
|
||||
}
|
||||
d.data!.push([start, ...dataValues[i]!]);
|
||||
} else {
|
||||
let time = start;
|
||||
if (centerBars) {
|
||||
// If centering bars, set the time to the midpoint between start and end instead
|
||||
// of the start time.
|
||||
time = new Date((start.getTime() + end.getTime()) / 2);
|
||||
}
|
||||
// Data value should always be a scalar for bar charts. Pass in
|
||||
// real start time as extra value to allow formatting tooltip.
|
||||
d.data!.push([time, dataValues[i][0]!, start, end]);
|
||||
if (
|
||||
chartType === "line" &&
|
||||
prevEndTime &&
|
||||
prevValues &&
|
||||
prevEndTime.getTime() !== start.getTime()
|
||||
) {
|
||||
// if the end of the previous data doesn't match the start of the current data,
|
||||
// we have to draw a gap so add a value at the end time, and then an empty value.
|
||||
d.data!.push([prevEndTime, ...prevValues[i]!]);
|
||||
d.data!.push([prevEndTime, null]);
|
||||
}
|
||||
d.data!.push([start, ...dataValues[i]!]);
|
||||
});
|
||||
prevValues = dataValues;
|
||||
prevEndTime = limit;
|
||||
prevEndTime = end;
|
||||
};
|
||||
|
||||
let color = colors[statistic_id];
|
||||
@@ -773,7 +692,11 @@ export class StatisticsChart extends LitElement {
|
||||
dataValues.push(val);
|
||||
});
|
||||
if (!this._hiddenStats.has(statistic_id)) {
|
||||
pushData(startDate, endDate, endTime, dataValues);
|
||||
pushData(
|
||||
startDate,
|
||||
endDate.getTime() < endTime.getTime() ? endDate : endTime,
|
||||
dataValues
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@ export class DialogDataTableSettings extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${localize("ui.components.data-table.settings.header")}
|
||||
@closed=${this._dialogClosed}
|
||||
|
||||
@@ -22,14 +22,6 @@ const isOn = (stateObj?: HassEntity) =>
|
||||
!STATES_OFF.includes(stateObj.state) &&
|
||||
!isUnavailableState(stateObj.state);
|
||||
|
||||
/**
|
||||
* @element ha-entity-toggle
|
||||
*
|
||||
* @cssprop --ha-entity-toggle-switch-width - Width of the switch track. Defaults to `38px`.
|
||||
* @cssprop --ha-entity-toggle-switch-size - Height of the switch track. Defaults to `20px`.
|
||||
* @cssprop --ha-entity-toggle-switch-thumb-size - Size of the switch thumb. Defaults to `14px`.
|
||||
*/
|
||||
|
||||
@customElement("ha-entity-toggle")
|
||||
export class HaEntityToggle extends LitElement {
|
||||
// hass is not a property so that we only re-render on stateObj changes
|
||||
@@ -173,9 +165,9 @@ export class HaEntityToggle extends LitElement {
|
||||
white-space: nowrap;
|
||||
}
|
||||
ha-switch {
|
||||
--ha-switch-width: var(--ha-entity-toggle-switch-width, 38px);
|
||||
--ha-switch-size: var(--ha-entity-toggle-switch-size, 20px);
|
||||
--ha-switch-thumb-size: var(--ha-entity-toggle-switch-thumb-size, 14px);
|
||||
--ha-switch-width: 38px;
|
||||
--ha-switch-size: 20px;
|
||||
--ha-switch-thumb-size: 14px;
|
||||
}
|
||||
ha-icon-button {
|
||||
--ha-icon-button-size: 40px;
|
||||
|
||||
@@ -130,6 +130,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
? html`<ha-state-icon
|
||||
.icon=${this.icon}
|
||||
.stateObj=${entityState}
|
||||
.hass=${this.hass}
|
||||
></ha-state-icon>`
|
||||
: ""}
|
||||
${value && !image && !showIcon
|
||||
|
||||
@@ -142,7 +142,6 @@ export class HaStatisticPicker extends LitElement {
|
||||
private async _getStatisticIds() {
|
||||
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
|
||||
this._picker?.requestUpdate();
|
||||
this._valueRenderer = this._makeValueRenderer();
|
||||
}
|
||||
|
||||
private _getItems = () =>
|
||||
@@ -211,10 +210,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(
|
||||
hass.language,
|
||||
hass.translationMetadata.translations
|
||||
);
|
||||
const isRTL = computeRTL(hass);
|
||||
|
||||
const output: StatisticComboBoxItem[] = [];
|
||||
|
||||
@@ -318,7 +314,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _renderValue(value: string) {
|
||||
private _valueRenderer: PickerValueRenderer = (value) => {
|
||||
const statisticId = value;
|
||||
|
||||
const item = this._computeItem(statisticId);
|
||||
@@ -342,13 +338,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _makeValueRenderer(): PickerValueRenderer {
|
||||
return (value) => this._renderValue(value);
|
||||
}
|
||||
|
||||
private _valueRenderer: PickerValueRenderer = this._makeValueRenderer();
|
||||
};
|
||||
|
||||
private _computeItem(statisticId: string): StatisticComboBoxItem {
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
@@ -363,10 +353,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
const primary = entityName || deviceName || statisticId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
|
||||
@@ -98,6 +98,7 @@ export class StateBadge extends LitElement {
|
||||
const domain = stateObj ? computeStateDomain(stateObj) : undefined;
|
||||
|
||||
return html`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
style=${styleMap(this._iconStyle)}
|
||||
data-domain=${ifDefined(domain)}
|
||||
data-state=${ifDefined(stateObj?.state)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { listenMediaQuery } from "../common/dom/media_query";
|
||||
import { internationalizationContext } from "../data/context";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
@@ -81,6 +82,8 @@ export const ADAPTIVE_DIALOG_MEDIA_QUERY =
|
||||
*/
|
||||
@customElement("ha-adaptive-dialog")
|
||||
export class HaAdaptiveDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "aria-labelledby" })
|
||||
public ariaLabelledBy?: string;
|
||||
|
||||
@@ -199,6 +202,7 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
.ariaLabelledBy=${this._defaultAriaLabelledBy}
|
||||
.ariaDescribedBy=${this.ariaDescribedBy}
|
||||
.flexContent=${this.flexContent}
|
||||
.hass=${this.hass}
|
||||
.open=${this.open}
|
||||
.preventScrimClose=${this.preventScrimClose}
|
||||
>
|
||||
@@ -217,6 +221,7 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this.open}
|
||||
.type=${this.type}
|
||||
.width=${this.width}
|
||||
|
||||
@@ -5,10 +5,10 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { Analytics, AnalyticsPreferences } from "../data/analytics";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import "./ha-md-list-item";
|
||||
import "./ha-switch";
|
||||
import type { HaSwitch } from "./ha-switch";
|
||||
import "./ha-tooltip";
|
||||
import "./item/ha-row-item";
|
||||
import type { HaSwitch } from "./ha-switch";
|
||||
|
||||
const ADDITIONAL_PREFERENCES = ["usage", "statistics"] as const;
|
||||
|
||||
@@ -33,7 +33,7 @@ export class HaAnalytics extends LitElement {
|
||||
const baseEnabled = !loading && this.analytics!.preferences.base;
|
||||
|
||||
return html`
|
||||
<ha-row-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.base.title`
|
||||
@@ -52,10 +52,10 @@ export class HaAnalytics extends LitElement {
|
||||
.disabled=${loading}
|
||||
name="base"
|
||||
></ha-switch>
|
||||
</ha-row-item>
|
||||
</ha-md-list-item>
|
||||
${ADDITIONAL_PREFERENCES.map(
|
||||
(preference) => html`
|
||||
<ha-row-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.${preference}.title`
|
||||
@@ -81,10 +81,10 @@ export class HaAnalytics extends LitElement {
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.need_base_enabled`
|
||||
)}
|
||||
</ha-tooltip>`}
|
||||
</ha-row-item>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
)}
|
||||
<ha-row-item>
|
||||
<ha-md-list-item>
|
||||
<span slot="headline"
|
||||
>${this.localize(
|
||||
`ui.panel.${this.translationKeyPanel}.analytics.preferences.diagnostics.title`
|
||||
@@ -103,7 +103,7 @@ export class HaAnalytics extends LitElement {
|
||||
.disabled=${loading}
|
||||
name="diagnostics"
|
||||
></ha-switch>
|
||||
</ha-row-item>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -139,8 +139,10 @@ export class HaAnalytics extends LitElement {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
ha-row-item {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
ha-md-list-item {
|
||||
--md-list-item-leading-space: 0;
|
||||
--md-list-item-trailing-space: 0;
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -184,10 +184,7 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
const allEntityIds = Object.values(controlEntities).flat();
|
||||
const uniqueEntityIds = Array.from(new Set(allEntityIds));
|
||||
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
uniqueEntityIds.forEach((entityId) => {
|
||||
if (isSelected(entityId)) {
|
||||
@@ -264,6 +261,7 @@ export class HaAreaControlsPicker extends LitElement {
|
||||
${item.type === "entity" && item.stateObj
|
||||
? html`<ha-state-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${item.stateObj}
|
||||
></ha-state-icon>`
|
||||
: item.domain
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import "@home-assistant/webawesome/dist/components/drawer/drawer";
|
||||
import type WaDrawer from "@home-assistant/webawesome/dist/components/drawer/drawer";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { css, html, LitElement, type PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
|
||||
import { configContext } from "../data/context";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { isIosApp } from "../util/is_ios";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
|
||||
|
||||
@@ -49,6 +47,8 @@ const SWIPE_LOCKED_CLASSES = new Set(["volume-slider-container", "forecast"]);
|
||||
*/
|
||||
@customElement("ha-bottom-sheet")
|
||||
export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: "aria-labelledby" })
|
||||
public ariaLabelledBy?: string;
|
||||
|
||||
@@ -67,10 +67,6 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@state() private _sliderInteractionActive = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _hassConfig?: ContextType<typeof configContext>;
|
||||
|
||||
@query("#drawer") private _drawer!: HTMLElement;
|
||||
|
||||
@query("#body") private _bodyElement!: HTMLDivElement;
|
||||
@@ -93,24 +89,22 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
await this.updateComplete;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (
|
||||
this._hassConfig?.auth.external &&
|
||||
isIosApp(this._hassConfig.auth.external)
|
||||
) {
|
||||
const element = this.renderRoot.querySelector("[autofocus]");
|
||||
if (element !== null) {
|
||||
if (!element.id) {
|
||||
element.id = "ha-bottom-sheet-autofocus";
|
||||
}
|
||||
this._hassConfig.auth.external.fireMessage({
|
||||
type: "focus_element",
|
||||
payload: {
|
||||
element_id: element.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
// disabled till iOS app fix the "focus_element" implementation
|
||||
// if (this.hass && isIosApp(this.hass.auth.external)) {
|
||||
// const element = this.renderRoot.querySelector("[autofocus]");
|
||||
// if (element !== null) {
|
||||
// if (!element.id) {
|
||||
// element.id = "ha-bottom-sheet-autofocus";
|
||||
// }
|
||||
// this.hass.auth.external?.fireMessage({
|
||||
// type: "focus_element",
|
||||
// payload: {
|
||||
// element_id: element.id,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
(
|
||||
this.renderRoot.querySelector("[autofocus]") as HTMLElement | null
|
||||
)?.focus();
|
||||
|
||||
@@ -27,7 +27,6 @@ import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, ReactiveElement, render } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { consume } from "@lit/context";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
@@ -44,14 +43,7 @@ import type {
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { showToast } from "../util/toast";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import {
|
||||
internationalizationContext,
|
||||
registriesContext,
|
||||
statesContext,
|
||||
labelsContext,
|
||||
configContext,
|
||||
formattersContext,
|
||||
} from "../data/context";
|
||||
import { labelsContext } from "../data/context";
|
||||
import type { LabelRegistryEntry } from "../data/label/label_registry";
|
||||
import "./ha-code-editor-completion-items";
|
||||
import type { CompletionItem } from "./ha-code-editor-completion-items";
|
||||
@@ -86,6 +78,8 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
@property() public mode = "yaml";
|
||||
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
|
||||
@@ -129,29 +123,9 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
@state() private _canCopy = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config?: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n?: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
private _labels?: ContextType<typeof labelsContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: registriesContext, subscribe: true })
|
||||
private _registries?: ContextType<typeof registriesContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: formattersContext, subscribe: true })
|
||||
private _formatters?: ContextType<typeof formattersContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states?: ContextType<typeof statesContext>;
|
||||
private _labels?: LabelRegistryEntry[];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
private _loadedCodeMirror?: typeof import("../resources/codemirror");
|
||||
@@ -215,9 +189,9 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
const line = doc.lineAt(pos);
|
||||
const message = `${
|
||||
err.reason ||
|
||||
this._i18n?.localize("ui.components.yaml-editor.error") ||
|
||||
this.hass?.localize("ui.components.yaml-editor.error") ||
|
||||
"YAML syntax error"
|
||||
}${err.mark ? ` (${this._i18n?.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
|
||||
}${err.mark ? ` (${this.hass?.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
|
||||
diagnostics = [{ from: pos, to: line.to, severity: "error", message }];
|
||||
}
|
||||
this.codemirror.dispatch(
|
||||
@@ -422,8 +396,8 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
this._loadedCodeMirror!.haJinjaHoverSource(
|
||||
view,
|
||||
pos,
|
||||
this._config ? documentationUrl(this._config, "") : undefined,
|
||||
this._hassArgHoverContext()
|
||||
this.hass ? documentationUrl(this.hass, "") : undefined,
|
||||
this.hass ? this._hassArgHoverContext() : undefined
|
||||
),
|
||||
{ hoverTime: 300 }
|
||||
),
|
||||
@@ -434,7 +408,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
const completionSources: CompletionSource[] = [
|
||||
this._loadedCodeMirror.haJinjaCompletionSource,
|
||||
];
|
||||
if (this.autocompleteEntities) {
|
||||
if (this.autocompleteEntities && this.hass) {
|
||||
completionSources.push(this._entityCompletions.bind(this));
|
||||
}
|
||||
if (this.autocompleteIcons) {
|
||||
@@ -473,12 +447,12 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
private _fullscreenLabel(): string {
|
||||
if (this._isFullscreen) {
|
||||
return (
|
||||
this._i18n?.localize("ui.components.yaml-editor.exit_fullscreen") ||
|
||||
this.hass?.localize("ui.components.yaml-editor.exit_fullscreen") ||
|
||||
"Exit fullscreen"
|
||||
);
|
||||
}
|
||||
return (
|
||||
this._i18n?.localize("ui.components.yaml-editor.enter_fullscreen") ||
|
||||
this.hass?.localize("ui.components.yaml-editor.enter_fullscreen") ||
|
||||
"Enter fullscreen"
|
||||
);
|
||||
}
|
||||
@@ -533,7 +507,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
{
|
||||
id: "test",
|
||||
label:
|
||||
this._i18n?.localize(
|
||||
this.hass?.localize(
|
||||
`ui.components.yaml-editor.test_${this.testing ? "off" : "on"}`
|
||||
) || "Test",
|
||||
path: this.testing ? mdiBugOutline : mdiBug,
|
||||
@@ -544,14 +518,14 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
{
|
||||
id: "undo",
|
||||
disabled: !this._canUndo,
|
||||
label: this._i18n?.localize("ui.common.undo") || "Undo",
|
||||
label: this.hass?.localize("ui.common.undo") || "Undo",
|
||||
path: mdiUndo,
|
||||
action: (e: Event) => this._handleUndoClick(e),
|
||||
},
|
||||
{
|
||||
id: "redo",
|
||||
disabled: !this._canRedo,
|
||||
label: this._i18n?.localize("ui.common.redo") || "Redo",
|
||||
label: this.hass?.localize("ui.common.redo") || "Redo",
|
||||
path: mdiRedo,
|
||||
action: (e: Event) => this._handleRedoClick(e),
|
||||
},
|
||||
@@ -559,7 +533,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
id: "copy",
|
||||
disabled: !this._canCopy,
|
||||
label:
|
||||
this._i18n?.localize("ui.components.yaml-editor.copy_to_clipboard") ||
|
||||
this.hass?.localize("ui.components.yaml-editor.copy_to_clipboard") ||
|
||||
"Copy to Clipboard",
|
||||
path: mdiContentCopy,
|
||||
action: (e: Event) => this._handleClipboardClick(e),
|
||||
@@ -567,7 +541,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
{
|
||||
id: "find-replace",
|
||||
label:
|
||||
this._i18n?.localize("ui.components.yaml-editor.find_and_replace") ||
|
||||
this.hass?.localize("ui.components.yaml-editor.find_and_replace") ||
|
||||
"Find and replace",
|
||||
path: mdiFindReplace,
|
||||
action: (e: Event) => this._handleFindReplaceClick(e),
|
||||
@@ -609,7 +583,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
await copyToClipboard(this.value);
|
||||
showToast(this, {
|
||||
message:
|
||||
this._i18n?.localize("ui.common.copied_clipboard") ||
|
||||
this.hass?.localize("ui.common.copied_clipboard") ||
|
||||
"Copied to clipboard",
|
||||
});
|
||||
}
|
||||
@@ -677,11 +651,12 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a HassArgHoverContext from the context objects so that
|
||||
* Builds a HassArgHoverContext from the current hass object so that
|
||||
* haJinjaHoverSource can resolve entity / device / area friendly names
|
||||
* without importing the full HomeAssistant type into the resource file.
|
||||
*/
|
||||
private _hassArgHoverContext(): HassArgHoverContext {
|
||||
const hass = this.hass!;
|
||||
const labelMap: Record<
|
||||
string,
|
||||
{ name: string; description?: string | null }
|
||||
@@ -693,33 +668,27 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
};
|
||||
}
|
||||
return {
|
||||
states: this._states as HassArgHoverContext["states"],
|
||||
devices: this._registries?.devices as HassArgHoverContext["devices"],
|
||||
areas: this._registries?.areas as HassArgHoverContext["areas"],
|
||||
floors: this._registries?.floors as HassArgHoverContext["floors"],
|
||||
entities: this._registries?.entities as HassArgHoverContext["entities"],
|
||||
states: hass.states as HassArgHoverContext["states"],
|
||||
devices: hass.devices as HassArgHoverContext["devices"],
|
||||
areas: hass.areas as HassArgHoverContext["areas"],
|
||||
floors: hass.floors as HassArgHoverContext["floors"],
|
||||
entities: hass.entities as HassArgHoverContext["entities"],
|
||||
labels: labelMap,
|
||||
formatEntityState: (entityId) =>
|
||||
this._formatters!.formatEntityState(this._states![entityId]),
|
||||
hass.formatEntityState(hass.states[entityId]),
|
||||
formatEntityName: (entityId) => {
|
||||
const stateObj = this._states?.[entityId];
|
||||
const stateObj = hass.states[entityId];
|
||||
return (
|
||||
(stateObj?.attributes.friendly_name as string | undefined) ??
|
||||
this._registries?.entities?.[entityId]?.name ??
|
||||
hass.entities[entityId]?.name ??
|
||||
undefined
|
||||
);
|
||||
},
|
||||
formatAttributeName: (entityId, attribute) =>
|
||||
this._formatters!.formatEntityAttributeName(
|
||||
this._states![entityId],
|
||||
attribute
|
||||
),
|
||||
hass.formatEntityAttributeName(hass.states[entityId], attribute),
|
||||
formatAttributeValue: (entityId, attribute) =>
|
||||
this._formatters!.formatEntityAttributeValue(
|
||||
this._states![entityId],
|
||||
attribute
|
||||
),
|
||||
localize: (key) => this._i18n!.localize(key as never),
|
||||
hass.formatEntityAttributeValue(hass.states[entityId], attribute),
|
||||
localize: (key) => hass.localize(key as never),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -729,51 +698,49 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
? completion.apply
|
||||
: completion.label;
|
||||
const context = getEntityContext(
|
||||
this._states![key],
|
||||
this._registries!.entities,
|
||||
this._registries!.devices,
|
||||
this._registries!.areas,
|
||||
this._registries!.floors
|
||||
this.hass!.states[key],
|
||||
this.hass!.entities,
|
||||
this.hass!.devices,
|
||||
this.hass!.areas,
|
||||
this.hass!.floors
|
||||
);
|
||||
|
||||
const completionInfo = document.createElement("div");
|
||||
completionInfo.classList.add("completion-info");
|
||||
|
||||
const formattedState = this._formatters!.formatEntityState(
|
||||
this._states![key]
|
||||
);
|
||||
const formattedState = this.hass!.formatEntityState(this.hass!.states[key]);
|
||||
|
||||
const completionItems: CompletionItem[] = [
|
||||
{
|
||||
label: this._i18n!.localize(
|
||||
label: this.hass!.localize(
|
||||
"ui.components.entity.entity-state-picker.state"
|
||||
),
|
||||
value: formattedState,
|
||||
subValue:
|
||||
// If the state exactly matches the formatted state, don't show the raw state
|
||||
this._states![key].state === formattedState
|
||||
this.hass!.states[key].state === formattedState
|
||||
? undefined
|
||||
: this._states![key].state,
|
||||
: this.hass!.states[key].state,
|
||||
},
|
||||
];
|
||||
|
||||
if (context.device && context.device.name) {
|
||||
completionItems.push({
|
||||
label: this._i18n!.localize("ui.components.device-picker.device"),
|
||||
label: this.hass!.localize("ui.components.device-picker.device"),
|
||||
value: context.device.name,
|
||||
});
|
||||
}
|
||||
|
||||
if (context.area && context.area.name) {
|
||||
completionItems.push({
|
||||
label: this._i18n!.localize("ui.components.area-picker.area"),
|
||||
label: this.hass!.localize("ui.components.area-picker.area"),
|
||||
value: context.area.name,
|
||||
});
|
||||
}
|
||||
|
||||
if (context.floor && context.floor.name) {
|
||||
completionItems.push({
|
||||
label: this._i18n!.localize("ui.components.floor-picker.floor"),
|
||||
label: this.hass!.localize("ui.components.floor-picker.floor"),
|
||||
value: context.floor.name,
|
||||
});
|
||||
}
|
||||
@@ -794,15 +761,15 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
entityId: string,
|
||||
attribute: string
|
||||
): CompletionInfo | null => {
|
||||
if (!this._states || !this._formatters) return null;
|
||||
const stateObj = this._states[entityId];
|
||||
if (!this.hass) return null;
|
||||
const stateObj = this.hass.states[entityId];
|
||||
if (!stateObj) return null;
|
||||
|
||||
const translatedName = this._formatters.formatEntityAttributeName(
|
||||
const translatedName = this.hass.formatEntityAttributeName(
|
||||
stateObj,
|
||||
attribute
|
||||
);
|
||||
const formattedValue = this._formatters.formatEntityAttributeValue(
|
||||
const formattedValue = this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
attribute
|
||||
);
|
||||
@@ -842,9 +809,9 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
completion: Completion
|
||||
): CompletionInfo | Promise<CompletionInfo> | null => {
|
||||
if (
|
||||
this._states &&
|
||||
this.hass &&
|
||||
typeof completion.apply === "string" &&
|
||||
completion.apply in this._states
|
||||
completion.apply in this.hass.states
|
||||
) {
|
||||
return this._renderInfo(completion);
|
||||
}
|
||||
@@ -1053,7 +1020,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
private _statesDotNotationCompletions(
|
||||
context: CompletionContext
|
||||
): CompletionResult | null | undefined {
|
||||
if (!this._states) return undefined;
|
||||
if (!this.hass) return undefined;
|
||||
|
||||
const { state: editorState, pos } = context;
|
||||
const tree = this._loadedCodeMirror!.syntaxTree(editorState);
|
||||
@@ -1162,7 +1129,9 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
case 0: {
|
||||
// states. → offer all unique domains
|
||||
const domains = [
|
||||
...new Set(Object.keys(this._states).map((id) => id.split(".")[0])),
|
||||
...new Set(
|
||||
Object.keys(this.hass.states).map((id) => id.split(".")[0])
|
||||
),
|
||||
].sort();
|
||||
return {
|
||||
from: completionFrom,
|
||||
@@ -1173,7 +1142,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
case 1: {
|
||||
// states.<domain>. → offer entity object_ids for that domain
|
||||
const [domain] = segments;
|
||||
const entities = Object.keys(this._states)
|
||||
const entities = Object.keys(this.hass.states)
|
||||
.filter((id) => id.startsWith(`${domain}.`))
|
||||
.map((id) => id.split(".").slice(1).join("."));
|
||||
if (!entities.length) return { from: completionFrom, options: [] };
|
||||
@@ -1203,7 +1172,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
}
|
||||
// Offer attribute names from the entity's state object
|
||||
const entityId = `${domain}.${entity}`;
|
||||
const entityState = this._states[entityId];
|
||||
const entityState = this.hass.states[entityId];
|
||||
if (!entityState) return { from: completionFrom, options: [] };
|
||||
const attrNames = Object.keys(entityState.attributes).sort();
|
||||
return {
|
||||
@@ -1373,8 +1342,8 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
): CompletionResult {
|
||||
const from = stringNode.from + 1;
|
||||
const empty: CompletionResult = { from, options: [] };
|
||||
if (!entityId || !this._states) return empty;
|
||||
const entityState = this._states[entityId];
|
||||
if (!entityId || !this.hass) return empty;
|
||||
const entityState = this.hass.states[entityId];
|
||||
if (!entityState) return empty;
|
||||
const attrs = Object.keys(entityState.attributes).sort();
|
||||
if (!attrs.length) return empty;
|
||||
@@ -1394,7 +1363,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
from: number;
|
||||
to: number;
|
||||
}): CompletionResult | null {
|
||||
const states = this._getStates(this._states!);
|
||||
const states = this._getStates(this.hass!.states);
|
||||
if (!states?.length) return null;
|
||||
// from is stringNode.from + 1 to skip the opening quote character.
|
||||
const from = stringNode.from + 1;
|
||||
@@ -1428,8 +1397,8 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
from: number;
|
||||
to: number;
|
||||
}): CompletionResult | null {
|
||||
if (!this._registries?.devices) return null;
|
||||
const devices = this._getDevices(this._registries.devices);
|
||||
if (!this.hass?.devices) return null;
|
||||
const devices = this._getDevices(this.hass.devices);
|
||||
if (!devices.length) return null;
|
||||
return {
|
||||
from: stringNode.from + 1,
|
||||
@@ -1457,8 +1426,8 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
from: number;
|
||||
to: number;
|
||||
}): CompletionResult | null {
|
||||
if (!this._registries?.areas) return null;
|
||||
const areas = this._getAreas(this._registries.areas);
|
||||
if (!this.hass?.areas) return null;
|
||||
const areas = this._getAreas(this.hass.areas);
|
||||
if (!areas.length) return null;
|
||||
return {
|
||||
from: stringNode.from + 1,
|
||||
@@ -1486,8 +1455,8 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
from: number;
|
||||
to: number;
|
||||
}): CompletionResult | null {
|
||||
if (!this._registries?.floors) return null;
|
||||
const floors = this._getFloors(this._registries.floors);
|
||||
if (!this.hass?.floors) return null;
|
||||
const floors = this._getFloors(this.hass.floors);
|
||||
if (!floors.length) return null;
|
||||
return {
|
||||
from: stringNode.from + 1,
|
||||
@@ -1587,7 +1556,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
// If cursor is after the entity field, show all entities
|
||||
if (context.pos >= afterField) {
|
||||
const states = this._getStates(this._states!);
|
||||
const states = this._getStates(this.hass!.states);
|
||||
|
||||
if (!states || !states.length) {
|
||||
return null;
|
||||
@@ -1642,7 +1611,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
const afterListMarker = currentLine.from + listItemMatch[0].length;
|
||||
|
||||
if (context.pos >= afterListMarker) {
|
||||
const states = this._getStates(this._states!);
|
||||
const states = this._getStates(this.hass!.states);
|
||||
|
||||
if (!states || !states.length) {
|
||||
return null;
|
||||
@@ -1702,7 +1671,7 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
return null;
|
||||
}
|
||||
|
||||
const states = this._getStates(this._states!);
|
||||
const states = this._getStates(this.hass!.states);
|
||||
|
||||
if (!states || !states.length) {
|
||||
return null;
|
||||
|
||||
+21
-23
@@ -15,10 +15,9 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { withViewTransition } from "../common/util/view-transition";
|
||||
import { configContext, internationalizationContext } from "../data/context";
|
||||
import { internationalizationContext } from "../data/context";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { isIosApp } from "../util/is_ios";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@@ -128,9 +127,10 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n?: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _hassConfig?: ContextType<typeof configContext>;
|
||||
// disabled till iOS app fix the "focus_element" implementation
|
||||
// @state()
|
||||
// @consume({ context: configContext, subscribe: true })
|
||||
// private _hassConfig?: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
private _bodyScrolled = false;
|
||||
@@ -221,24 +221,22 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
await this.updateComplete;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (
|
||||
this._hassConfig?.auth.external &&
|
||||
isIosApp(this._hassConfig.auth.external)
|
||||
) {
|
||||
const element = this.querySelector("[autofocus]");
|
||||
if (element !== null) {
|
||||
if (!element.id) {
|
||||
element.id = "ha-dialog-autofocus";
|
||||
}
|
||||
this._hassConfig.auth.external.fireMessage({
|
||||
type: "focus_element",
|
||||
payload: {
|
||||
element_id: element.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
// disabled till iOS app fix the "focus_element" implementation
|
||||
// if (this._hassConfig?.auth.external && isIosApp(this._hassConfig.auth.external)) {
|
||||
// const element = this.querySelector("[autofocus]");
|
||||
// if (element !== null) {
|
||||
// if (!element.id) {
|
||||
// element.id = "ha-dialog-autofocus";
|
||||
// }
|
||||
// this._hassConfig.auth.external.fireMessage({
|
||||
// type: "focus_element",
|
||||
// payload: {
|
||||
// element_id: element.id,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
+125
-265
@@ -1,115 +1,36 @@
|
||||
import "@home-assistant/webawesome/dist/components/drawer/drawer";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { DrawerBase } from "@material/mwc-drawer/mwc-drawer-base";
|
||||
import { styles } from "@material/mwc-drawer/mwc-drawer.css";
|
||||
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";
|
||||
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"hass-drawer-closed": undefined;
|
||||
"hass-layout-transition": { active: boolean; reason?: string };
|
||||
}
|
||||
interface HTMLElementEventMap {
|
||||
"hass-drawer-closed": HASSDomEvent<HASSDomEvents["hass-drawer-closed"]>;
|
||||
"hass-layout-transition": HASSDomEvent<
|
||||
HASSDomEvents["hass-layout-transition"]
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
const blockingElements = (document as any).$blockingElements;
|
||||
|
||||
@customElement("ha-drawer")
|
||||
export class HaDrawer extends LitElement {
|
||||
private static readonly _SWIPE_AXIS_TOLERANCE = 32;
|
||||
export class HaDrawer extends DrawerBase {
|
||||
@property() public direction: "ltr" | "rtl" = "ltr";
|
||||
|
||||
@property({ reflect: true }) public direction: "ltr" | "rtl" = "ltr";
|
||||
private _mc?: HammerManager;
|
||||
|
||||
@property({ reflect: true }) public type: "" | "dismissible" | "modal" = "";
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public open = false;
|
||||
|
||||
@query("wa-drawer") private _modalDrawer?: HTMLElement;
|
||||
|
||||
@query(".sidebar-shell") private _sidebarShell?: HTMLElement;
|
||||
private _rtlStyle?: HTMLElement;
|
||||
|
||||
private _sidebarTransitionActive = false;
|
||||
|
||||
private _transitionTarget?: HTMLElement;
|
||||
|
||||
private _gestureRecognizer = new SwipeGestureRecognizer({
|
||||
velocitySwipeThreshold: 0.35,
|
||||
});
|
||||
|
||||
private _touchStartY = 0;
|
||||
|
||||
private _touchDeltaY = 0;
|
||||
|
||||
private get _modal() {
|
||||
return this.type === "modal";
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return this._modal
|
||||
? html`
|
||||
<slot name="appContent"></slot>
|
||||
<wa-drawer
|
||||
placement="start"
|
||||
.open=${this.open}
|
||||
light-dismiss
|
||||
without-header
|
||||
@touchstart=${this._handleTouchStart}
|
||||
@wa-after-hide=${this._handleAfterHide}
|
||||
>
|
||||
<slot></slot>
|
||||
</wa-drawer>
|
||||
`
|
||||
: html`
|
||||
<div class="layout">
|
||||
<div class="sidebar-shell">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="app-content">
|
||||
<slot name="appContent"></slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(_: PropertyValues<this>) {
|
||||
this._syncTransitionListeners();
|
||||
|
||||
if (!this.open) {
|
||||
this._resetSwipeTracking();
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this._syncTransitionListeners();
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._removeTransitionListeners();
|
||||
this._unregisterSwipeHandlers();
|
||||
}
|
||||
|
||||
private _handleAfterHide(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this.open = false;
|
||||
fireEvent(this, "hass-drawer-closed");
|
||||
}
|
||||
|
||||
private _closeModalDrawer() {
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
private _handleDrawerTransitionStart = (ev: TransitionEvent) => {
|
||||
if (
|
||||
ev.propertyName !==
|
||||
(this.type === "dismissible" ? "transform" : "width") ||
|
||||
this._sidebarTransitionActive
|
||||
) {
|
||||
if (ev.propertyName !== "width" || this._sidebarTransitionActive) {
|
||||
return;
|
||||
}
|
||||
this._sidebarTransitionActive = true;
|
||||
@@ -120,11 +41,7 @@ export class HaDrawer extends LitElement {
|
||||
};
|
||||
|
||||
private _handleDrawerTransitionEnd = (ev: TransitionEvent) => {
|
||||
if (
|
||||
ev.propertyName !==
|
||||
(this.type === "dismissible" ? "transform" : "width") ||
|
||||
!this._sidebarTransitionActive
|
||||
) {
|
||||
if (ev.propertyName !== "width" || !this._sidebarTransitionActive) {
|
||||
return;
|
||||
}
|
||||
this._sidebarTransitionActive = false;
|
||||
@@ -134,207 +51,150 @@ export class HaDrawer extends LitElement {
|
||||
});
|
||||
};
|
||||
|
||||
private _handleTouchStart = (ev: TouchEvent) => {
|
||||
if (!this._modal || !this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const drawer = this._modalDrawer;
|
||||
const dialog = drawer?.shadowRoot?.querySelector(
|
||||
"dialog"
|
||||
) as HTMLDialogElement | null;
|
||||
|
||||
if (!dialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
const path = ev.composedPath();
|
||||
|
||||
if (!path.includes(dialog)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ev.stopPropagation();
|
||||
this._startSwipeTracking(ev.touches[0].clientX, ev.touches[0].clientY);
|
||||
};
|
||||
|
||||
private _startSwipeTracking(clientX: number, clientY: number) {
|
||||
document.addEventListener("touchmove", this._handleTouchMove, {
|
||||
passive: true,
|
||||
});
|
||||
document.addEventListener("touchend", this._handleTouchEnd);
|
||||
document.addEventListener("touchcancel", this._handleTouchEnd);
|
||||
|
||||
this._touchStartY = clientY;
|
||||
this._touchDeltaY = 0;
|
||||
this._gestureRecognizer.start(clientX);
|
||||
protected createAdapter() {
|
||||
return {
|
||||
...super.createAdapter(),
|
||||
trapFocus: () => {
|
||||
blockingElements.push(this);
|
||||
this.appContent.inert = true;
|
||||
document.body.style.overflow = "hidden";
|
||||
},
|
||||
releaseFocus: () => {
|
||||
blockingElements.remove(this);
|
||||
this.appContent.inert = false;
|
||||
document.body.style.overflow = "";
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private _handleTouchMove = (ev: TouchEvent) => {
|
||||
const currentX = ev.touches[0].clientX;
|
||||
const currentY = ev.touches[0].clientY;
|
||||
this._touchDeltaY = Math.abs(currentY - this._touchStartY);
|
||||
this._gestureRecognizer.move(currentX);
|
||||
};
|
||||
protected updated(changedProps: PropertyValues<this>) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("direction")) {
|
||||
this.mdcRoot.dir = this.direction;
|
||||
if (this.direction === "rtl") {
|
||||
this._rtlStyle = document.createElement("style");
|
||||
this._rtlStyle.innerHTML = `
|
||||
.mdc-drawer--animate {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
.mdc-drawer--opening {
|
||||
transform: translateX(0);
|
||||
}
|
||||
.mdc-drawer--closing {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
`;
|
||||
|
||||
private _handleTouchEnd = () => {
|
||||
this._unregisterSwipeHandlers();
|
||||
|
||||
const result = this._gestureRecognizer.end();
|
||||
const isHorizontalGesture =
|
||||
Math.abs(result.delta) >
|
||||
this._touchDeltaY + HaDrawer._SWIPE_AXIS_TOLERANCE;
|
||||
|
||||
if (!isHorizontalGesture) {
|
||||
this._resetSwipeTracking();
|
||||
return;
|
||||
}
|
||||
|
||||
const drawerDialog = this._modalDrawer?.shadowRoot?.querySelector(
|
||||
'[part="dialog"]'
|
||||
) as HTMLElement | null;
|
||||
const drawerWidth = drawerDialog?.offsetWidth || 0;
|
||||
|
||||
if (result.isSwipe) {
|
||||
const closeByVelocity =
|
||||
this.direction === "rtl"
|
||||
? result.isDownwardSwipe
|
||||
: !result.isDownwardSwipe;
|
||||
|
||||
if (closeByVelocity) {
|
||||
this._closeModalDrawer();
|
||||
this.shadowRoot!.appendChild(this._rtlStyle);
|
||||
} else if (this._rtlStyle) {
|
||||
this.shadowRoot!.removeChild(this._rtlStyle);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const closeByDistance =
|
||||
drawerWidth > 0 &&
|
||||
(this.direction === "rtl"
|
||||
? result.delta > 0 && Math.abs(result.delta) > drawerWidth * 0.5
|
||||
: result.delta < 0 && Math.abs(result.delta) > drawerWidth * 0.5);
|
||||
|
||||
if (closeByDistance) {
|
||||
this._closeModalDrawer();
|
||||
if (changedProps.has("open") && this.open && this.type === "modal") {
|
||||
this._setupSwipe();
|
||||
} else if (this._mc) {
|
||||
this._mc.destroy();
|
||||
this._mc = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
private _unregisterSwipeHandlers() {
|
||||
document.removeEventListener("touchmove", this._handleTouchMove);
|
||||
document.removeEventListener("touchend", this._handleTouchEnd);
|
||||
document.removeEventListener("touchcancel", this._handleTouchEnd);
|
||||
}
|
||||
|
||||
private _resetSwipeTracking() {
|
||||
this._unregisterSwipeHandlers();
|
||||
this._gestureRecognizer.reset();
|
||||
this._touchStartY = 0;
|
||||
this._touchDeltaY = 0;
|
||||
}
|
||||
|
||||
private _syncTransitionListeners() {
|
||||
if (this._transitionTarget === this._sidebarShell) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._removeTransitionListeners();
|
||||
|
||||
if (!this._sidebarShell) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._transitionTarget = this._sidebarShell;
|
||||
this._transitionTarget.addEventListener(
|
||||
protected firstUpdated() {
|
||||
super.firstUpdated();
|
||||
this.mdcRoot?.addEventListener(
|
||||
"transitionstart",
|
||||
this._handleDrawerTransitionStart
|
||||
);
|
||||
this._transitionTarget.addEventListener(
|
||||
this.mdcRoot?.addEventListener(
|
||||
"transitionend",
|
||||
this._handleDrawerTransitionEnd
|
||||
);
|
||||
this._transitionTarget.addEventListener(
|
||||
this.mdcRoot?.addEventListener(
|
||||
"transitioncancel",
|
||||
this._handleDrawerTransitionEnd
|
||||
);
|
||||
}
|
||||
|
||||
private _removeTransitionListeners() {
|
||||
if (!this._transitionTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._transitionTarget.removeEventListener(
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.mdcRoot?.removeEventListener(
|
||||
"transitionstart",
|
||||
this._handleDrawerTransitionStart
|
||||
);
|
||||
this._transitionTarget.removeEventListener(
|
||||
this.mdcRoot?.removeEventListener(
|
||||
"transitionend",
|
||||
this._handleDrawerTransitionEnd
|
||||
);
|
||||
this._transitionTarget.removeEventListener(
|
||||
this.mdcRoot?.removeEventListener(
|
||||
"transitioncancel",
|
||||
this._handleDrawerTransitionEnd
|
||||
);
|
||||
this._transitionTarget = undefined;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
private async _setupSwipe() {
|
||||
const hammer = await import("../resources/hammer");
|
||||
this._mc = new hammer.Manager(document, {
|
||||
touchAction: "pan-y",
|
||||
});
|
||||
this._mc.add(
|
||||
new hammer.Swipe({
|
||||
direction:
|
||||
this.direction === "rtl"
|
||||
? hammer.DIRECTION_RIGHT
|
||||
: hammer.DIRECTION_LEFT,
|
||||
})
|
||||
);
|
||||
this._mc.on("swipeleft swiperight", () => {
|
||||
fireEvent(this, "hass-toggle-menu", { open: false });
|
||||
});
|
||||
}
|
||||
|
||||
.layout {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar-shell {
|
||||
position: fixed;
|
||||
width: var(--ha-sidebar-width);
|
||||
height: 100%;
|
||||
border-inline-end: 1px solid var(--divider-color, rgba(0, 0, 0, 0.12));
|
||||
box-sizing: border-box;
|
||||
transition: width var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
|
||||
.app-content {
|
||||
overflow: unset;
|
||||
min-width: 0;
|
||||
padding-inline-start: var(--ha-sidebar-width);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
padding-inline-start var(--ha-animation-duration-normal) ease,
|
||||
width var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
|
||||
:host([type="dismissible"]) .sidebar-shell {
|
||||
transition: transform var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
|
||||
:host([type="dismissible"]:not([open])) .sidebar-shell {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
:host([type="dismissible"][direction="rtl"]:not([open])) .sidebar-shell {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
:host([type="dismissible"]:not([open])) .app-content {
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
wa-drawer {
|
||||
--size: var(--ha-sidebar-width, 256px);
|
||||
--show-duration: var(--ha-animation-duration-normal);
|
||||
--hide-duration: var(--ha-animation-duration-normal);
|
||||
}
|
||||
|
||||
wa-drawer::part(body) {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
.mdc-drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
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;
|
||||
}
|
||||
.mdc-drawer-app-content {
|
||||
overflow: unset;
|
||||
flex: none;
|
||||
padding-left: var(--mdc-drawer-width);
|
||||
padding-inline-start: var(--mdc-drawer-width);
|
||||
padding-inline-end: initial;
|
||||
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) {
|
||||
/* Use 1ms instead of "none" so the transitionend event still fires.
|
||||
The MDC drawer foundation relies on it to complete the close cycle. */
|
||||
.mdc-drawer,
|
||||
.mdc-drawer-app-content {
|
||||
transition: 1ms;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -39,12 +39,7 @@ export class HaEntitiesDisplayEditor extends LitElement {
|
||||
const items: DisplayItem[] = entities.map((entity) => ({
|
||||
value: entity.entity_id,
|
||||
label: computeStateName(entity),
|
||||
icon: entityIcon(
|
||||
this.hass.entities,
|
||||
this.hass.config,
|
||||
this.hass.connection,
|
||||
entity
|
||||
),
|
||||
icon: entityIcon(this.hass, entity),
|
||||
}));
|
||||
|
||||
const value: DisplayValue = {
|
||||
|
||||
@@ -59,7 +59,6 @@ export class HaExpansionPanel extends LitElement {
|
||||
<slot class="secondary" name="secondary">${this.secondary}</slot>
|
||||
</div>
|
||||
</slot>
|
||||
<slot name="event"></slot>
|
||||
${!this.leftChevron ? chevronIcon : nothing}
|
||||
<slot name="icons"></slot>
|
||||
</div>
|
||||
|
||||
@@ -122,7 +122,11 @@ export class HaFilterEntities extends LitElement {
|
||||
.selected=${this.value?.includes(entity.entity_id) ?? false}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon slot="graphic" .stateObj=${entity}></ha-state-icon>
|
||||
<ha-state-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entity}
|
||||
></ha-state-icon>
|
||||
${computeStateName(entity)}
|
||||
</ha-check-list-item>`;
|
||||
|
||||
|
||||
@@ -137,10 +137,7 @@ export class HaFilterFloorAreas extends LitElement {
|
||||
.selected=${this.value?.areas?.includes(area.area_id) || false}
|
||||
.type=${"areas"}
|
||||
class=${classMap({
|
||||
rtl: computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
),
|
||||
rtl: computeRTL(this.hass),
|
||||
floor: hasFloor,
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import "@home-assistant/webawesome/dist/components/popover/popover";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiPlaylistPlus } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
@@ -14,10 +13,8 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { configContext } from "../data/context";
|
||||
import { PickerMixin } from "../mixins/picker-mixin";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import { isIosApp } from "../util/is_ios";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-button";
|
||||
import "./ha-combo-box-item";
|
||||
@@ -113,9 +110,10 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
|
||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _hassConfig?: ContextType<typeof configContext>;
|
||||
// disabled till iOS app fix the "focus_element" implementation
|
||||
// @state()
|
||||
// @consume({ context: authContext, subscribe: true })
|
||||
// private auth?: ContextType<typeof authContext>;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@@ -321,18 +319,16 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
this._comboBox?.setFieldValue(this._initialFieldValue);
|
||||
this._initialFieldValue = undefined;
|
||||
}
|
||||
if (
|
||||
this._hassConfig?.auth.external &&
|
||||
isIosApp(this._hassConfig.auth.external)
|
||||
) {
|
||||
this._hassConfig.auth.external.fireMessage({
|
||||
type: "focus_element",
|
||||
payload: {
|
||||
element_id: "combo-box",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
// disabled till iOS app fix the "focus_element" implementation
|
||||
// if (this.auth?.external && isIosApp(this.auth.external)) {
|
||||
// this.auth.external.fireMessage({
|
||||
// type: "focus_element",
|
||||
// payload: {
|
||||
// element_id: "combo-box",
|
||||
// },
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
|
||||
this._comboBox?.focus();
|
||||
});
|
||||
|
||||
@@ -166,6 +166,7 @@ export class HaRelatedItems extends LitElement {
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entity}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
@@ -321,6 +322,7 @@ export class HaRelatedItems extends LitElement {
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${group}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
@@ -345,6 +347,7 @@ export class HaRelatedItems extends LitElement {
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${scene}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
@@ -397,6 +400,7 @@ export class HaRelatedItems extends LitElement {
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${automation}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
@@ -448,6 +452,7 @@ export class HaRelatedItems extends LitElement {
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${script}
|
||||
slot="graphic"
|
||||
></ha-state-icon>
|
||||
|
||||
@@ -63,12 +63,7 @@ export class HaSelectBox extends LitElement {
|
||||
const selected = option.value === this.value;
|
||||
|
||||
const isDark = this.hass?.themes.darkMode || false;
|
||||
const isRTL = this.hass
|
||||
? computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
: false;
|
||||
const isRTL = this.hass ? computeRTL(this.hass) : false;
|
||||
|
||||
const imageSrc =
|
||||
typeof option.image === "object"
|
||||
|
||||
@@ -36,15 +36,7 @@ export class HaIconSelector extends LitElement {
|
||||
const placeholder =
|
||||
this.selector.icon?.placeholder ||
|
||||
stateObj?.attributes.icon ||
|
||||
(stateObj &&
|
||||
until(
|
||||
entityIcon(
|
||||
this.hass.entities,
|
||||
this.hass.config,
|
||||
this.hass.connection,
|
||||
stateObj
|
||||
)
|
||||
));
|
||||
(stateObj && until(entityIcon(this.hass, stateObj)));
|
||||
|
||||
return html`
|
||||
<ha-icon-picker
|
||||
@@ -59,7 +51,11 @@ export class HaIconSelector extends LitElement {
|
||||
>
|
||||
${!placeholder && stateObj
|
||||
? html`
|
||||
<ha-state-icon slot="start" .stateObj=${stateObj}></ha-state-icon>
|
||||
<ha-state-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-icon>
|
||||
`
|
||||
: nothing}
|
||||
</ha-icon-picker>
|
||||
|
||||
@@ -188,6 +188,7 @@ export class HaObjectSelector extends LitElement {
|
||||
}
|
||||
|
||||
return html`<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.readonly=${this.disabled}
|
||||
.label=${this.label}
|
||||
.required=${this.required}
|
||||
|
||||
@@ -101,6 +101,7 @@ export class HaTemplateSelector extends LitElement {
|
||||
: nothing}
|
||||
<ha-code-editor
|
||||
mode="jinja2"
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.readOnly=${this.disabled}
|
||||
.placeholder=${this.placeholder || "{{ ... }}"}
|
||||
|
||||
@@ -86,6 +86,9 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: "show-advanced", type: Boolean })
|
||||
public showAdvanced = false;
|
||||
|
||||
@property({ attribute: "show-service-id", type: Boolean })
|
||||
public showServiceId = false;
|
||||
|
||||
@@ -542,6 +545,7 @@ export class HaServiceControl extends LitElement {
|
||||
: ""}
|
||||
${shouldRenderServiceDataYaml
|
||||
? html`<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.service-control.action_data"
|
||||
)}
|
||||
@@ -663,7 +667,10 @@ export class HaServiceControl extends LitElement {
|
||||
? this.hass.services[domain][serviceName].description_placeholders
|
||||
: undefined;
|
||||
|
||||
return dataField.selector
|
||||
return dataField.selector &&
|
||||
(!dataField.advanced ||
|
||||
this.showAdvanced ||
|
||||
(this._value?.data && this._value.data[dataField.key] !== undefined))
|
||||
? html`<ha-settings-row .narrow=${this.narrow}>
|
||||
${!showOptional
|
||||
? hasOptional
|
||||
|
||||
@@ -523,10 +523,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
}
|
||||
|
||||
private _renderUserItem(selectedPanel: string) {
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isSelected = selectedPanel === "profile";
|
||||
|
||||
return html`
|
||||
@@ -564,9 +561,9 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
id="sidebar-external-config"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
|
||||
<span class="item-text" slot="headline">
|
||||
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
||||
</span>
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.localize("ui.sidebar.external_app_configuration")}</span
|
||||
>
|
||||
</ha-list-item-button>
|
||||
${!this.alwaysExpand
|
||||
? this._renderToolTip(
|
||||
@@ -743,7 +740,6 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
--ha-row-item-min-height: var(--ha-space-10);
|
||||
--ha-row-item-padding-block: 0;
|
||||
--ha-row-item-padding-inline: var(--ha-space-3);
|
||||
width: var(--ha-space-12);
|
||||
position: relative;
|
||||
transition: width var(--ha-animation-duration-normal) ease;
|
||||
@@ -844,12 +840,21 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
}
|
||||
|
||||
ha-user-badge {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: var(--ha-space-10);
|
||||
height: var(--ha-space-10);
|
||||
}
|
||||
|
||||
ha-list-item-button.user {
|
||||
--ha-row-item-padding-inline: var(--ha-space-1) 0;
|
||||
--ha-row-item-padding-inline: var(--ha-space-2) var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-list-item-button.user.rtl {
|
||||
--ha-row-item-padding-inline: var(--ha-space-4) var(--ha-space-3);
|
||||
}
|
||||
|
||||
ha-user-badge {
|
||||
flex-shrink: 0;
|
||||
margin-right: calc(var(--ha-space-2) * -1);
|
||||
}
|
||||
|
||||
.spacer {
|
||||
|
||||
@@ -1,46 +1,31 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import {
|
||||
configContext,
|
||||
connectionContext,
|
||||
entitiesContext,
|
||||
} from "../data/context";
|
||||
import {
|
||||
DEFAULT_DOMAIN_ICON,
|
||||
entityIcon,
|
||||
FALLBACK_DOMAIN_ICONS,
|
||||
} from "../data/icons";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-state-icon")
|
||||
export class HaStateIcon extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@property({ attribute: false }) public stateValue?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
protected _config?: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
protected _connection?: ContextType<typeof connectionContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: entitiesContext, subscribe: true })
|
||||
protected _entities?: ContextType<typeof entitiesContext>;
|
||||
|
||||
protected render() {
|
||||
const overrideIcon =
|
||||
this.icon ||
|
||||
(this.stateObj && this._entities?.[this.stateObj.entity_id]?.icon) ||
|
||||
(this.stateObj && this.hass?.entities[this.stateObj.entity_id]?.icon) ||
|
||||
this.stateObj?.attributes.icon;
|
||||
if (overrideIcon) {
|
||||
return html`<ha-icon .icon=${overrideIcon}></ha-icon>`;
|
||||
@@ -48,21 +33,17 @@ export class HaStateIcon extends LitElement {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
if (!this._config || !this._connection || !this._entities) {
|
||||
if (!this.hass) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
const icon = entityIcon(
|
||||
this._entities,
|
||||
this._config.config,
|
||||
this._connection.connection,
|
||||
this.stateObj,
|
||||
this.stateValue
|
||||
).then((icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
const icon = entityIcon(this.hass, this.stateObj, this.stateValue).then(
|
||||
(icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
return this._renderFallback();
|
||||
}
|
||||
return this._renderFallback();
|
||||
});
|
||||
);
|
||||
return html`${until(icon)}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -228,10 +228,6 @@ export class HaSwitch extends Switch {
|
||||
outline: var(--wa-focus-ring);
|
||||
outline-offset: var(--wa-focus-ring-offset);
|
||||
}
|
||||
|
||||
:host(:empty) slot.label {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1136,10 +1136,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
let rtl = false;
|
||||
let showEntityId = false;
|
||||
if (type === "area" || type === "floor") {
|
||||
rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
rtl = computeRTL(this.hass);
|
||||
hasFloor =
|
||||
type === "area" && !!(item as FloorComboBoxItem).area?.floor_id;
|
||||
}
|
||||
|
||||
@@ -3,16 +3,14 @@ import { DEFAULT_SCHEMA, dump, load } from "js-yaml";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import { consume } from "@lit/context";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../common/util/copy-clipboard";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { showToast } from "../util/toast";
|
||||
import "./ha-button";
|
||||
import "./ha-code-editor";
|
||||
import type { HaCodeEditor } from "./ha-code-editor";
|
||||
import { internationalizationContext } from "../data/context";
|
||||
|
||||
const isEmpty = (obj: Record<string, unknown>): boolean => {
|
||||
if (typeof obj !== "object" || obj === null) {
|
||||
@@ -28,6 +26,8 @@ const isEmpty = (obj: Record<string, unknown>): boolean => {
|
||||
|
||||
@customElement("ha-yaml-editor")
|
||||
export class HaYamlEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property({ attribute: false }) public yamlSchema: Schema = DEFAULT_SCHEMA;
|
||||
@@ -59,10 +59,6 @@ export class HaYamlEditor extends LitElement {
|
||||
|
||||
@state() private _yaml = "";
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n?: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@query("ha-code-editor") _codeEditor?: HaCodeEditor;
|
||||
|
||||
public setValue(value): void {
|
||||
@@ -116,6 +112,7 @@ export class HaYamlEditor extends LitElement {
|
||||
? html`<p>${this.label}${this.required ? " *" : ""}</p>`
|
||||
: nothing}
|
||||
<ha-code-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this._yaml}
|
||||
.readOnly=${this.readOnly}
|
||||
.disableFullscreen=${this.disableFullscreen}
|
||||
@@ -135,7 +132,7 @@ export class HaYamlEditor extends LitElement {
|
||||
${this.copyClipboard
|
||||
? html`
|
||||
<ha-button appearance="plain" @click=${this._copyYaml}>
|
||||
${this._i18n!.localize(
|
||||
${this.hass.localize(
|
||||
"ui.components.yaml-editor.copy_to_clipboard"
|
||||
)}
|
||||
</ha-button>
|
||||
@@ -166,7 +163,7 @@ export class HaYamlEditor extends LitElement {
|
||||
// Invalid YAML
|
||||
isValid = false;
|
||||
yamlError = err;
|
||||
errorMsg = `${this._i18n!.localize("ui.components.yaml-editor.error", { reason: err.reason })}${err.mark ? ` (${this._i18n!.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
|
||||
errorMsg = `${this.hass.localize("ui.components.yaml-editor.error", { reason: err.reason })}${err.mark ? ` (${this.hass.localize("ui.components.yaml-editor.error_location", { line: err.mark.line + 1, column: err.mark.column + 1 })})` : ""}`;
|
||||
}
|
||||
} else {
|
||||
parsed = {};
|
||||
@@ -204,7 +201,7 @@ export class HaYamlEditor extends LitElement {
|
||||
if (this.yaml) {
|
||||
await copyToClipboard(this.yaml);
|
||||
showToast(this, {
|
||||
message: this._i18n!.localize("ui.common.copied_clipboard"),
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../list/types";
|
||||
import { HaRowItem } from "./ha-row-item";
|
||||
|
||||
/**
|
||||
@@ -41,12 +39,6 @@ export class HaListItemBase extends HaRowItem {
|
||||
if (!this.hasAttribute("role")) {
|
||||
this.setAttribute("role", this.defaultRole);
|
||||
}
|
||||
fireEvent(this, "ha-list-item-register", { item: this });
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
fireEvent(this, "ha-list-item-unregister", { item: this });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
/**
|
||||
* @element ha-row-item
|
||||
@@ -46,34 +46,13 @@ export class HaRowItem extends LitElement {
|
||||
|
||||
protected readonly _slotController = new HasSlotController(
|
||||
this,
|
||||
"start",
|
||||
"end",
|
||||
"headline",
|
||||
"supporting-text",
|
||||
"content"
|
||||
);
|
||||
|
||||
@state() private _hasStart = false;
|
||||
|
||||
@state() private _hasEnd = false;
|
||||
|
||||
private _onSlotChange(name: "start" | "end") {
|
||||
return (ev: Event) => {
|
||||
const slot = ev.target as HTMLSlotElement;
|
||||
const hasContent = slot
|
||||
.assignedNodes({ flatten: true })
|
||||
.some(
|
||||
(node) =>
|
||||
node.nodeType === Node.ELEMENT_NODE ||
|
||||
(node.nodeType === Node.TEXT_NODE &&
|
||||
(node as Text).textContent?.trim() !== "")
|
||||
);
|
||||
if (name === "start") {
|
||||
this._hasStart = hasContent;
|
||||
} else {
|
||||
this._hasEnd = hasContent;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return this._renderBase(this._renderInner());
|
||||
}
|
||||
@@ -83,20 +62,26 @@ export class HaRowItem extends LitElement {
|
||||
}
|
||||
|
||||
protected _renderInner(): TemplateResult {
|
||||
const hasStart = this._slotController.test("start");
|
||||
const hasEnd = this._slotController.test("end");
|
||||
const hasContent = this._slotController.test("content");
|
||||
|
||||
return html`
|
||||
<div part="start" class="start" ?hidden=${!this._hasStart}>
|
||||
<slot name="start" @slotchange=${this._onSlotChange("start")}></slot>
|
||||
</div>
|
||||
${hasStart
|
||||
? html`<div part="start" class="start">
|
||||
<slot name="start"></slot>
|
||||
</div>`
|
||||
: nothing}
|
||||
<div part="content" class="content">
|
||||
${hasContent
|
||||
? html`<slot name="content"></slot>`
|
||||
: this._renderDefaultContent()}
|
||||
</div>
|
||||
<div part="end" class="end" ?hidden=${!this._hasEnd}>
|
||||
<slot name="end" @slotchange=${this._onSlotChange("end")}></slot>
|
||||
</div>
|
||||
${hasEnd
|
||||
? html`<div part="end" class="end">
|
||||
<slot name="end"></slot>
|
||||
</div>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -130,6 +115,10 @@ export class HaRowItem extends LitElement {
|
||||
color: var(--primary-text-color);
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
--ha-row-item-padding-block: var(--ha-space-3);
|
||||
--ha-row-item-padding-inline: var(--ha-space-4);
|
||||
--ha-row-item-gap: var(--ha-space-4);
|
||||
--ha-row-item-min-height: 48px;
|
||||
}
|
||||
:host([disabled]) {
|
||||
color: var(--disabled-text-color);
|
||||
@@ -140,10 +129,10 @@ export class HaRowItem extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--ha-row-item-gap, var(--ha-space-4));
|
||||
padding-block: var(--ha-row-item-padding-block, var(--ha-space-3));
|
||||
padding-inline: var(--ha-row-item-padding-inline, var(--ha-space-4));
|
||||
min-height: var(--ha-row-item-min-height, 48px);
|
||||
gap: var(--ha-row-item-gap);
|
||||
padding-block: var(--ha-row-item-padding-block);
|
||||
padding-inline: var(--ha-row-item-padding-inline);
|
||||
min-height: var(--ha-row-item-min-height);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.content {
|
||||
@@ -159,10 +148,6 @@ export class HaRowItem extends LitElement {
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.start[hidden],
|
||||
.end[hidden] {
|
||||
display: none;
|
||||
}
|
||||
.headline {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { compareNodeOrder } from "../../common/dom/compare-node-order";
|
||||
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import type { HaListItemBase } from "../item/ha-list-item-base";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HaListItemBase } from "../item/ha-list-item-base";
|
||||
import "./types";
|
||||
import type { HaListItemRegistrationDetail } from "./types";
|
||||
|
||||
/**
|
||||
* @element ha-list-base
|
||||
@@ -14,11 +12,9 @@ import type { HaListItemRegistrationDetail } from "./types";
|
||||
*
|
||||
* @summary
|
||||
* Base list container with roving-tabindex keyboard navigation (ArrowUp/Down,
|
||||
* Home/End, optional Enter/Space activation, optional wrap-focus). Tracks
|
||||
* `HaListItemBase` descendants via the `ha-list-item-register` /
|
||||
* `ha-list-item-unregister` events they fire on connect/disconnect — works
|
||||
* across any nesting depth and shadow boundaries. Subclasses override
|
||||
* `hostRole` and/or `render()` to specialize.
|
||||
* Home/End, optional Enter/Space activation, optional wrap-focus). Discovers
|
||||
* slotted `HaListItemBase` descendants. Subclasses override `hostRole` and/or
|
||||
* `render()` to specialize.
|
||||
*
|
||||
* @slot - List items (`<ha-list-item-*>`).
|
||||
*
|
||||
@@ -72,14 +68,6 @@ export class HaListBase extends LitElement {
|
||||
Space: this._onActivate,
|
||||
});
|
||||
this.addEventListener("focusin", this._onFocusIn);
|
||||
this.addEventListener(
|
||||
"ha-list-item-register",
|
||||
this._onItemRegister as EventListener
|
||||
);
|
||||
this.addEventListener(
|
||||
"ha-list-item-unregister",
|
||||
this._onItemUnregister as EventListener
|
||||
);
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
@@ -87,14 +75,11 @@ export class HaListBase extends LitElement {
|
||||
this._unbindKeys?.();
|
||||
this._unbindKeys = undefined;
|
||||
this.removeEventListener("focusin", this._onFocusIn);
|
||||
this.removeEventListener(
|
||||
"ha-list-item-register",
|
||||
this._onItemRegister as EventListener
|
||||
);
|
||||
this.removeEventListener(
|
||||
"ha-list-item-unregister",
|
||||
this._onItemUnregister as EventListener
|
||||
);
|
||||
}
|
||||
|
||||
public firstUpdated(changed: PropertyValues) {
|
||||
super.firstUpdated(changed);
|
||||
this.updateListItems();
|
||||
}
|
||||
|
||||
public focus(options?: FocusOptions) {
|
||||
@@ -130,14 +115,18 @@ export class HaListBase extends LitElement {
|
||||
this._applyActive(focusItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook called whenever the items array has changed. Subclasses can override
|
||||
* to layer in extra bookkeeping (e.g. selection state sync).
|
||||
*/
|
||||
public updateListItems() {
|
||||
const next = this._discoverListItems();
|
||||
const changed =
|
||||
next.length !== this.items.length ||
|
||||
next.some((it, i) => it !== this.items[i]);
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
this.items = next;
|
||||
this._recomputeFocusableIndexes();
|
||||
if (
|
||||
this._activeItemIndex >= this.items.length ||
|
||||
this._activeItemIndex >= next.length ||
|
||||
!this._hasFocusableItem ||
|
||||
this._activeItemIndex < 0
|
||||
) {
|
||||
@@ -146,32 +135,6 @@ export class HaListBase extends LitElement {
|
||||
this._applyActive(false);
|
||||
}
|
||||
|
||||
private _onItemRegister = (
|
||||
ev: HASSDomEvent<HaListItemRegistrationDetail>
|
||||
) => {
|
||||
ev.stopPropagation();
|
||||
const item = ev.detail.item;
|
||||
if (this.items.includes(item)) {
|
||||
return;
|
||||
}
|
||||
const next = [...this.items, item];
|
||||
next.sort(compareNodeOrder);
|
||||
this.items = next;
|
||||
this.updateListItems();
|
||||
};
|
||||
|
||||
private _onItemUnregister = (
|
||||
ev: HASSDomEvent<HaListItemRegistrationDetail>
|
||||
) => {
|
||||
ev.stopPropagation();
|
||||
const item = ev.detail.item;
|
||||
if (!this.items.includes(item)) {
|
||||
return;
|
||||
}
|
||||
this.items = this.items.filter((it) => it !== item);
|
||||
this.updateListItems();
|
||||
};
|
||||
|
||||
private _recomputeFocusableIndexes() {
|
||||
let first = -1;
|
||||
let last = -1;
|
||||
@@ -188,12 +151,27 @@ export class HaListBase extends LitElement {
|
||||
this._hasFocusableItem = first !== -1;
|
||||
}
|
||||
|
||||
public handleSlotChange = () => {
|
||||
this.updateListItems();
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<div part="base" class="base">
|
||||
<slot></slot>
|
||||
<slot @slotchange=${this.handleSlotChange}></slot>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _discoverListItems(): HaListItemBase[] {
|
||||
const slot =
|
||||
this.renderRoot?.querySelector<HTMLSlotElement>("slot:not([name])");
|
||||
if (!slot) {
|
||||
return [];
|
||||
}
|
||||
return slot
|
||||
.assignedElements({ flatten: true })
|
||||
.filter((el): el is HaListItemBase => el instanceof HaListItemBase);
|
||||
}
|
||||
|
||||
private _isFocusable(index: number): boolean {
|
||||
const item = this.items[index];
|
||||
return !!item && item.interactive && !item.disabled;
|
||||
@@ -292,12 +270,14 @@ export class HaListBase extends LitElement {
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: block;
|
||||
--ha-list-gap: 0;
|
||||
--ha-list-padding: 0;
|
||||
}
|
||||
.base {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-list-gap, 0);
|
||||
padding: var(--ha-list-padding, 0);
|
||||
gap: var(--ha-list-gap);
|
||||
padding: var(--ha-list-padding);
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export class HaListNav extends HaListBase {
|
||||
aria-label=${ifDefined(this.ariaLabel ?? undefined)}
|
||||
>
|
||||
<div part="base" class="base" role="list">
|
||||
<slot></slot>
|
||||
<slot @slotchange=${this.handleSlotChange}></slot>
|
||||
</div>
|
||||
</nav>`;
|
||||
}
|
||||
|
||||
@@ -121,15 +121,15 @@ export class HaListSelectable extends HaListBase {
|
||||
|
||||
public updateListItems() {
|
||||
super.updateListItems();
|
||||
this._syncItemSelectedState(true);
|
||||
this._syncItemSelectedState();
|
||||
}
|
||||
|
||||
private _sortedSelectedIndices(): number[] {
|
||||
return [...this._selectedIndices!].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
private _syncItemSelectedState(reset = false): void {
|
||||
if (!this._selectedIndices || reset) {
|
||||
private _syncItemSelectedState() {
|
||||
if (!this._selectedIndices) {
|
||||
this._selectedIndices = new Set<number>();
|
||||
this.items.forEach((item, i) => {
|
||||
const opt = item as HaListItemOption;
|
||||
|
||||
@@ -11,15 +11,9 @@ export interface HaListActivatedDetail {
|
||||
item: HaListItemBase;
|
||||
}
|
||||
|
||||
export interface HaListItemRegistrationDetail {
|
||||
item: HaListItemBase;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"ha-list-selected": HaListSelectedDetail;
|
||||
"ha-list-activated": HaListActivatedDetail;
|
||||
"ha-list-item-register": HaListItemRegistrationDetail;
|
||||
"ha-list-item-unregister": HaListItemRegistrationDetail;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ class HaEntityMarker extends LitElement {
|
||||
></div>`
|
||||
: this.showIcon && this.entityId
|
||||
? html`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.hass?.states[this.entityId]}
|
||||
></ha-state-icon>`
|
||||
: !this.entityUnit
|
||||
|
||||
@@ -76,7 +76,12 @@ class DialogJoinMediaPlayers extends LitElement {
|
||||
|
||||
const entityId = this._entityId;
|
||||
return html`
|
||||
<ha-dialog .open=${this._open} flexcontent @closed=${this._dialogClosed}>
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
flexcontent
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-dialog-header show-border slot="header">
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
|
||||
@@ -100,6 +100,7 @@ class DialogMediaManage extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
?prevent-scrim-close=${this._uploading || this._deleting}
|
||||
@closed=${this._dialogClosed}
|
||||
|
||||
@@ -77,6 +77,7 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
width="large"
|
||||
flexcontent
|
||||
|
||||
@@ -59,10 +59,7 @@ class HaMediaPlayerToggle extends LitElement {
|
||||
icon = mdiSpeakerPause;
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
const { primary, secondary } = this._computeDisplayData(
|
||||
this.entityId,
|
||||
|
||||
@@ -1,29 +1,15 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import { getDeviceIntegrationLookup } from "../../../data/device/device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../../data/entity/entity";
|
||||
import type { EntitySources } from "../../../data/entity/entity_sources";
|
||||
import { fetchEntitySourcesWithCache } from "../../../data/entity/entity_sources";
|
||||
import type { TargetSelector } from "../../../data/selector";
|
||||
import {
|
||||
filterSelectorDevices,
|
||||
filterSelectorEntities,
|
||||
} from "../../../data/selector";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../../device/ha-device-picker";
|
||||
import "../../ha-adaptive-dialog";
|
||||
import "../../ha-dialog";
|
||||
import "../../ha-dialog-header";
|
||||
import "../../ha-icon-button";
|
||||
import "../../ha-icon-next";
|
||||
import "../../ha-md-list";
|
||||
import "../../ha-md-list-item";
|
||||
import "../../ha-svg-icon";
|
||||
import "../../list/ha-list-base";
|
||||
import "../ha-target-picker-item-row";
|
||||
import type { TargetDetailsDialogParams } from "./show-dialog-target-details";
|
||||
|
||||
@@ -35,12 +21,6 @@ class DialogTargetDetails extends LitElement implements HassDialog {
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@state() private _entitySourcesLoaded = false;
|
||||
|
||||
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
|
||||
|
||||
public showDialog(params: TargetDetailsDialogParams): void {
|
||||
this._params = params;
|
||||
this._opened = true;
|
||||
@@ -54,72 +34,6 @@ class DialogTargetDetails extends LitElement implements HassDialog {
|
||||
private _dialogClosed() {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
this._params = undefined;
|
||||
this._entitySources = undefined;
|
||||
this._entitySourcesLoaded = false;
|
||||
}
|
||||
|
||||
private _hasIntegration(selector: TargetSelector) {
|
||||
return (
|
||||
(selector.target?.entity &&
|
||||
ensureArray(selector.target.entity).some((e) => e.integration)) ||
|
||||
(selector.target?.device &&
|
||||
ensureArray(selector.target.device).some((d) => d.integration))
|
||||
);
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (!changedProperties.has("_params")) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
this._params?.selector &&
|
||||
this._hasIntegration(this._params.selector) &&
|
||||
!this._entitySourcesLoaded
|
||||
) {
|
||||
this._loadEntitySources();
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadEntitySources(): Promise<void> {
|
||||
try {
|
||||
this._entitySources = await fetchEntitySourcesWithCache(this.hass);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to load entity sources for target details", err);
|
||||
} finally {
|
||||
this._entitySourcesLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
const target = this._selectorTarget();
|
||||
if (!target?.entity) {
|
||||
return true;
|
||||
}
|
||||
return ensureArray(target.entity).some((e) =>
|
||||
filterSelectorEntities(e, entity, this._entitySources)
|
||||
);
|
||||
};
|
||||
|
||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||
const target = this._selectorTarget();
|
||||
if (!target?.device) {
|
||||
return true;
|
||||
}
|
||||
const deviceIntegrations = this._entitySources
|
||||
? this._deviceIntegrationLookup(
|
||||
this._entitySources,
|
||||
Object.values(this.hass.entities)
|
||||
)
|
||||
: undefined;
|
||||
return ensureArray(target.device).some((d) =>
|
||||
filterSelectorDevices(d, device, deviceIntegrations)
|
||||
);
|
||||
};
|
||||
|
||||
private _selectorTarget() {
|
||||
return this._params?.selector?.target || null;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -127,86 +41,33 @@ class DialogTargetDetails extends LitElement implements HassDialog {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
let deviceFilter: HaDevicePickerDeviceFilterFunc | undefined;
|
||||
let entityFilter: HaEntityPickerEntityFilterFunc | undefined;
|
||||
let includeDomains: string[] | undefined;
|
||||
let includeDeviceClasses: string[] | undefined;
|
||||
let primaryEntitiesOnly: boolean | undefined;
|
||||
|
||||
if (this._params.selector) {
|
||||
deviceFilter = this._filterDevices;
|
||||
entityFilter = this._filterEntities;
|
||||
primaryEntitiesOnly = this._params.selector.target?.primary_entities_only;
|
||||
} else {
|
||||
deviceFilter = this._params.deviceFilter;
|
||||
entityFilter = this._params.entityFilter;
|
||||
includeDomains = this._params.includeDomains;
|
||||
includeDeviceClasses = this._params.includeDeviceClasses;
|
||||
primaryEntitiesOnly = this._params.primaryEntitiesOnly;
|
||||
}
|
||||
|
||||
const waitingForSources =
|
||||
this._params.selector &&
|
||||
this._hasIntegration(this._params.selector) &&
|
||||
!this._entitySourcesLoaded;
|
||||
|
||||
return html`
|
||||
<ha-adaptive-dialog
|
||||
<ha-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._opened}
|
||||
header-title=${this.hass.localize(
|
||||
"ui.components.target-picker.target_details"
|
||||
)}
|
||||
header-subtitle=${`${this.hass.localize(
|
||||
`ui.components.target-picker.type.${this._params.type}`
|
||||
)}:
|
||||
${this._params.title}`}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div class="type-wrapper">
|
||||
<div class="type-label">
|
||||
${this.hass.localize(
|
||||
`ui.components.target-picker.type.${this._params.type}`
|
||||
)}
|
||||
</div>
|
||||
<ha-list-base
|
||||
.ariaLabel=${`${this.hass.localize(`ui.components.target-picker.type.${this._params.type}`)}: ${this._params.title}`}
|
||||
wrap-focus
|
||||
>
|
||||
${waitingForSources
|
||||
? nothing
|
||||
: html`
|
||||
<ha-target-picker-item-row
|
||||
.hass=${this.hass}
|
||||
.type=${this._params.type}
|
||||
.itemId=${this._params.itemId}
|
||||
.deviceFilter=${deviceFilter}
|
||||
.entityFilter=${entityFilter}
|
||||
.includeDomains=${includeDomains}
|
||||
.includeDeviceClasses=${includeDeviceClasses}
|
||||
.primaryEntitiesOnly=${primaryEntitiesOnly}
|
||||
expand
|
||||
></ha-target-picker-item-row>
|
||||
`}
|
||||
</ha-list-base>
|
||||
</div>
|
||||
</ha-adaptive-dialog>
|
||||
<ha-target-picker-item-row
|
||||
.hass=${this.hass}
|
||||
.type=${this._params.type}
|
||||
.itemId=${this._params.itemId}
|
||||
.deviceFilter=${this._params.deviceFilter}
|
||||
.entityFilter=${this._params.entityFilter}
|
||||
.includeDomains=${this._params.includeDomains}
|
||||
.includeDeviceClasses=${this._params.includeDeviceClasses}
|
||||
.primaryEntitiesOnly=${this._params.primaryEntitiesOnly}
|
||||
expand
|
||||
></ha-target-picker-item-row>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.type-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
border: var(--ha-border-width-sm) solid
|
||||
var(--ha-color-border-neutral-normal);
|
||||
overflow: hidden;
|
||||
}
|
||||
.type-label {
|
||||
background-color: var(--ha-color-surface-low);
|
||||
padding: var(--ha-space-1) var(--ha-space-3);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../../../data/entity/entity";
|
||||
import type { TargetSelector } from "../../../data/selector";
|
||||
import type { TargetType } from "../../../data/target";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../../device/ha-device-picker";
|
||||
|
||||
export type NewBackupType = "automatic" | "manual";
|
||||
|
||||
export interface TargetDetailsDialogParams {
|
||||
title: string;
|
||||
type: TargetType;
|
||||
itemId: string;
|
||||
selector?: TargetSelector;
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
includeDomains?: string[];
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { TargetType, TargetTypeFloorless } from "../../data/target";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker";
|
||||
import "../ha-expansion-panel";
|
||||
import "../list/ha-list-base";
|
||||
import "../ha-md-list";
|
||||
import "./ha-target-picker-item-row";
|
||||
|
||||
@customElement("ha-target-picker-item-group")
|
||||
@@ -66,25 +66,23 @@ export class HaTargetPickerItemGroup extends LitElement {
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<ha-list-base>
|
||||
${Object.entries(this.items).map(([type, items]) =>
|
||||
items
|
||||
? items.map(
|
||||
(item) =>
|
||||
html`<ha-target-picker-item-row
|
||||
.hass=${this.hass}
|
||||
.type=${type as TargetTypeFloorless}
|
||||
.itemId=${item}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.primaryEntitiesOnly=${this.primaryEntitiesOnly}
|
||||
></ha-target-picker-item-row>`
|
||||
)
|
||||
: nothing
|
||||
)}
|
||||
</ha-list-base>
|
||||
${Object.entries(this.items).map(([type, items]) =>
|
||||
items
|
||||
? items.map(
|
||||
(item) =>
|
||||
html`<ha-target-picker-item-row
|
||||
.hass=${this.hass}
|
||||
.type=${type as TargetTypeFloorless}
|
||||
.itemId=${item}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.primaryEntitiesOnly=${this.primaryEntitiesOnly}
|
||||
></ha-target-picker-item-row>`
|
||||
)
|
||||
: nothing
|
||||
)}
|
||||
</ha-expansion-panel>`;
|
||||
}
|
||||
|
||||
@@ -98,7 +96,7 @@ export class HaTargetPickerItemGroup extends LitElement {
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
ha-expansion-panel::part(summary) {
|
||||
background-color: var(--ha-color-surface-low);
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
color: var(--secondary-text-color);
|
||||
@@ -106,6 +104,9 @@ export class HaTargetPickerItemGroup extends LitElement {
|
||||
justify-content: space-between;
|
||||
min-height: unset;
|
||||
}
|
||||
ha-md-list {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiChevronLeft,
|
||||
mdiChevronRight,
|
||||
mdiClose,
|
||||
mdiDevices,
|
||||
mdiHome,
|
||||
mdiLabel,
|
||||
mdiMinusBox,
|
||||
mdiTextureBox,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
type PropertyValues,
|
||||
type TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -48,17 +38,18 @@ import {
|
||||
type ExtractFromTargetResultReferenced,
|
||||
type TargetType,
|
||||
} from "../../data/target";
|
||||
import { showMoreInfoDialog } from "../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import { buttonLinkStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker";
|
||||
import { floorDefaultIconPath } from "../ha-floor-icon";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-md-list";
|
||||
import type { HaMdList } from "../ha-md-list";
|
||||
import "../ha-md-list-item";
|
||||
import type { HaMdListItem } from "../ha-md-list-item";
|
||||
import "../ha-state-icon";
|
||||
import "../ha-svg-icon";
|
||||
import "../item/ha-list-item-base";
|
||||
import "../item/ha-list-item-button";
|
||||
import { showTargetDetailsDialog } from "./dialog/show-dialog-target-details";
|
||||
|
||||
@customElement("ha-target-picker-item-row")
|
||||
@@ -74,9 +65,6 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
@property({ type: Boolean, attribute: "sub-entry", reflect: true })
|
||||
public subEntry = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public subLevel = 0;
|
||||
|
||||
@property({ type: Boolean, attribute: "hide-context" })
|
||||
public hideContext = false;
|
||||
|
||||
@@ -118,6 +106,12 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
_labelRegistry!: LabelRegistryEntry[];
|
||||
|
||||
@query("ha-md-list-item") public item?: HaMdListItem;
|
||||
|
||||
@query("ha-md-list") public list?: HaMdList;
|
||||
|
||||
@query("ha-target-picker-item-row") public itemRow?: HaTargetPickerItemRow;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<this>) {
|
||||
if (!this.subEntry && changedProps.has("itemId")) {
|
||||
this._updateItemData();
|
||||
@@ -143,130 +137,101 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
|
||||
const replaceable = !this.subEntry && !this.expand;
|
||||
|
||||
const content = html`
|
||||
<div class="icon" slot="start">
|
||||
${iconPath
|
||||
? html`<ha-icon .icon=${iconPath}></ha-icon>`
|
||||
: this._iconImg
|
||||
? html`<img
|
||||
alt=${this._domainName || ""}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
src=${this._iconImg}
|
||||
/>`
|
||||
: fallbackIconPath
|
||||
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
|
||||
: this.type === "entity"
|
||||
? html`
|
||||
<ha-state-icon
|
||||
.stateObj=${stateObject ||
|
||||
({
|
||||
entity_id: this.itemId,
|
||||
attributes: {},
|
||||
} as HassEntity)}
|
||||
>
|
||||
</ha-state-icon>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
type=${replaceable ? "button" : "text"}
|
||||
class=${classMap({
|
||||
error: notFound,
|
||||
replaceable,
|
||||
})}
|
||||
@click=${replaceable ? this._replaceItem : undefined}
|
||||
>
|
||||
<div class="icon" slot="start">
|
||||
${this.subEntry
|
||||
? html`
|
||||
<div class="horizontal-line-wrapper">
|
||||
<div class="horizontal-line"></div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${iconPath
|
||||
? html`<ha-icon .icon=${iconPath}></ha-icon>`
|
||||
: this._iconImg
|
||||
? html`<img
|
||||
alt=${this._domainName || ""}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
src=${this._iconImg}
|
||||
/>`
|
||||
: fallbackIconPath
|
||||
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
|
||||
: this.type === "entity"
|
||||
? html`
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObject ||
|
||||
({
|
||||
entity_id: this.itemId,
|
||||
attributes: {},
|
||||
} as HassEntity)}
|
||||
>
|
||||
</ha-state-icon>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
<div slot="headline">${name}</div>
|
||||
${notFound || (context && !this.hideContext)
|
||||
? html`<span slot="supporting-text"
|
||||
>${notFound
|
||||
? this.hass.localize(
|
||||
`ui.components.target-picker.${this.type}_not_found`
|
||||
)
|
||||
: context}</span
|
||||
>`
|
||||
: nothing}
|
||||
${stateObject && this.subEntry
|
||||
? html`<span slot="supporting-text" class="state"
|
||||
>${this.hass.formatEntityState(stateObject)}</span
|
||||
>`
|
||||
: nothing}
|
||||
${!this.subEntry && entries && showEntities
|
||||
? html`
|
||||
<div slot="end" class="summary">
|
||||
${showEntities &&
|
||||
!this.expand &&
|
||||
entries?.referenced_entities.length
|
||||
? html`<button class="main link" @click=${this._openDetails}>
|
||||
${this.hass.localize(
|
||||
"ui.components.target-picker.entities_count",
|
||||
{
|
||||
count: entries?.referenced_entities.length,
|
||||
}
|
||||
)}
|
||||
</button>`
|
||||
: showEntities
|
||||
? html`<span class="main">
|
||||
<div slot="headline">${name}</div>
|
||||
${notFound || (context && !this.hideContext)
|
||||
? html`<span slot="supporting-text"
|
||||
>${notFound
|
||||
? this.hass.localize(
|
||||
`ui.components.target-picker.${this.type}_not_found`
|
||||
)
|
||||
: context}</span
|
||||
>`
|
||||
: nothing}
|
||||
${this._domainName && this.subEntry
|
||||
? html`<span slot="supporting-text" class="domain"
|
||||
>${this._domainName}</span
|
||||
>`
|
||||
: nothing}
|
||||
${!this.subEntry && entries && showEntities
|
||||
? html`
|
||||
<div slot="end" class="summary">
|
||||
${showEntities &&
|
||||
!this.expand &&
|
||||
entries?.referenced_entities.length
|
||||
? html`<button class="main link" @click=${this._openDetails}>
|
||||
${this.hass.localize(
|
||||
"ui.components.target-picker.entities_count",
|
||||
{
|
||||
count: entries?.referenced_entities.length,
|
||||
}
|
||||
)}
|
||||
</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${!this.expand && !this.subEntry
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiClose}
|
||||
slot="end"
|
||||
@click=${this._removeItem}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: this.subEntry && this.type === "entity"
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? mdiChevronLeft
|
||||
: mdiChevronRight}
|
||||
slot="end"
|
||||
></ha-svg-icon>
|
||||
</button>`
|
||||
: showEntities
|
||||
? html`<span class="main">
|
||||
${this.hass.localize(
|
||||
"ui.components.target-picker.entities_count",
|
||||
{
|
||||
count: entries?.referenced_entities.length,
|
||||
}
|
||||
)}
|
||||
</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
|
||||
let item: TemplateResult;
|
||||
|
||||
if (replaceable || (this.subEntry && this.type === "entity")) {
|
||||
item = html`
|
||||
<ha-list-item-button
|
||||
class=${classMap({
|
||||
error: notFound,
|
||||
replaceable,
|
||||
})}
|
||||
@click=${replaceable
|
||||
? this._replaceItem
|
||||
: this.subEntry && this.type === "entity"
|
||||
? this._openMoreInfo
|
||||
: undefined}
|
||||
>
|
||||
${content}
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
} else {
|
||||
item = html`
|
||||
<ha-list-item-base
|
||||
class=${classMap({
|
||||
error: notFound,
|
||||
})}
|
||||
>
|
||||
${content}
|
||||
</ha-list-item-base>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${item}
|
||||
${!this.expand && !this.subEntry
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiClose}
|
||||
slot="end"
|
||||
@click=${this._removeItem}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
${this.expand && entries && entries.referenced_entities
|
||||
? this._renderEntries()
|
||||
: nothing}
|
||||
@@ -276,10 +241,6 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
private _renderEntries() {
|
||||
const entries = this.parentEntries || this._entries;
|
||||
|
||||
if (!entries || entries.referenced_entities.length === 0) {
|
||||
return this._renderEmptyEntries();
|
||||
}
|
||||
|
||||
let nextType: TargetType =
|
||||
this.type === "floor"
|
||||
? "area"
|
||||
@@ -389,64 +350,54 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
) || ([] as string[]),
|
||||
}));
|
||||
|
||||
const nextSubLevel = this.subLevel + 1;
|
||||
|
||||
return html`
|
||||
${rows1.map(
|
||||
(itemId, index) => html`
|
||||
<ha-target-picker-item-row
|
||||
sub-entry
|
||||
.subLevel=${nextSubLevel}
|
||||
style=${`--sub-entry-indent: calc(${nextSubLevel} * var(--ha-space-10));`}
|
||||
.hass=${this.hass}
|
||||
.type=${nextType}
|
||||
.itemId=${itemId}
|
||||
.parentEntries=${rows1Entries?.[index]}
|
||||
.hideContext=${this.hideContext || this.type !== "label"}
|
||||
expand
|
||||
></ha-target-picker-item-row>
|
||||
`
|
||||
)}
|
||||
${deviceRows.map(
|
||||
(itemId, index) => html`
|
||||
<ha-target-picker-item-row
|
||||
sub-entry
|
||||
.subLevel=${nextSubLevel}
|
||||
style=${`--sub-entry-indent: calc(${nextSubLevel} * var(--ha-space-10));`}
|
||||
.hass=${this.hass}
|
||||
type="device"
|
||||
.itemId=${itemId}
|
||||
.parentEntries=${deviceRowsEntries?.[index]}
|
||||
.hideContext=${this.hideContext || this.type !== "label"}
|
||||
expand
|
||||
></ha-target-picker-item-row>
|
||||
`
|
||||
)}
|
||||
${entityRows.map(
|
||||
(itemId) => html`
|
||||
<ha-target-picker-item-row
|
||||
sub-entry
|
||||
.subLevel=${nextSubLevel}
|
||||
style=${`--sub-entry-indent: calc(${nextSubLevel} * var(--ha-space-10));`}
|
||||
.hass=${this.hass}
|
||||
type="entity"
|
||||
.itemId=${itemId}
|
||||
.hideContext=${this.hideContext || this.type !== "label"}
|
||||
></ha-target-picker-item-row>
|
||||
`
|
||||
)}
|
||||
<div class="entries-tree">
|
||||
<div class="line-wrapper">
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
<ha-md-list class="entries">
|
||||
${rows1.map(
|
||||
(itemId, index) => html`
|
||||
<ha-target-picker-item-row
|
||||
sub-entry
|
||||
.hass=${this.hass}
|
||||
.type=${nextType}
|
||||
.itemId=${itemId}
|
||||
.parentEntries=${rows1Entries?.[index]}
|
||||
.hideContext=${this.hideContext || this.type !== "label"}
|
||||
expand
|
||||
></ha-target-picker-item-row>
|
||||
`
|
||||
)}
|
||||
${deviceRows.map(
|
||||
(itemId, index) => html`
|
||||
<ha-target-picker-item-row
|
||||
sub-entry
|
||||
.hass=${this.hass}
|
||||
type="device"
|
||||
.itemId=${itemId}
|
||||
.parentEntries=${deviceRowsEntries?.[index]}
|
||||
.hideContext=${this.hideContext || this.type !== "label"}
|
||||
expand
|
||||
></ha-target-picker-item-row>
|
||||
`
|
||||
)}
|
||||
${entityRows.map(
|
||||
(itemId) => html`
|
||||
<ha-target-picker-item-row
|
||||
sub-entry
|
||||
.hass=${this.hass}
|
||||
type="entity"
|
||||
.itemId=${itemId}
|
||||
.hideContext=${this.hideContext || this.type !== "label"}
|
||||
></ha-target-picker-item-row>
|
||||
`
|
||||
)}
|
||||
</ha-md-list>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderEmptyEntries() {
|
||||
return html`<ha-list-item-base>
|
||||
<ha-svg-icon .path=${mdiMinusBox} slot="start" class="icon"></ha-svg-icon>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize("ui.components.target-picker.no_targets")}</span
|
||||
>
|
||||
</ha-list-item-base>`;
|
||||
}
|
||||
|
||||
private async _updateItemData() {
|
||||
if (this.type === "entity") {
|
||||
this._entries = undefined;
|
||||
@@ -615,14 +566,7 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
const areaName = area ? computeAreaName(area) : undefined;
|
||||
const context = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(
|
||||
computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? " ◂ "
|
||||
: " ▸ "
|
||||
);
|
||||
.join(computeRTL(this.hass) ? " ◂ " : " ▸ ");
|
||||
return {
|
||||
name: entityName || deviceName || item,
|
||||
context,
|
||||
@@ -696,12 +640,6 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _openMoreInfo = () => {
|
||||
showMoreInfoDialog(this, {
|
||||
entityId: this.itemId,
|
||||
});
|
||||
};
|
||||
|
||||
static styles = [
|
||||
buttonLinkStyle,
|
||||
css`
|
||||
@@ -713,6 +651,12 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
--md-list-item-two-line-container-height: 56px;
|
||||
}
|
||||
|
||||
:host([expand]:not([sub-entry])) ha-md-list-item {
|
||||
border: 2px solid var(--ha-color-border-neutral-loud);
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
|
||||
}
|
||||
|
||||
.error {
|
||||
background: var(--ha-color-fill-warning-quiet-resting);
|
||||
}
|
||||
@@ -736,7 +680,6 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
.icon {
|
||||
width: 24px;
|
||||
display: flex;
|
||||
color: var(--ha-color-on-neutral-normal);
|
||||
}
|
||||
|
||||
img {
|
||||
@@ -754,21 +697,53 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
:host([sub-entry]) .summary {
|
||||
margin-inline-start: var(--ha-space-12);
|
||||
margin-right: var(--ha-space-12);
|
||||
}
|
||||
.summary .main {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
:host([expand]) .summary .main {
|
||||
color: var(--ha-color-text-secondary);
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
}
|
||||
.summary .secondary {
|
||||
font-size: var(--ha-font-size-s);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.entries-tree {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.entries-tree .line-wrapper {
|
||||
padding: var(--ha-space-5);
|
||||
}
|
||||
|
||||
.entries-tree .line-wrapper .line {
|
||||
border-left: 2px dashed var(--divider-color);
|
||||
height: calc(100% - 28px);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
:host([sub-entry]) .entries-tree .line-wrapper .line {
|
||||
height: calc(100% - 12px);
|
||||
top: -18px;
|
||||
}
|
||||
|
||||
.entries {
|
||||
padding: 0;
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
|
||||
.horizontal-line-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.horizontal-line-wrapper .horizontal-line {
|
||||
position: absolute;
|
||||
top: 11px;
|
||||
margin-inline-start: -28px;
|
||||
width: 29px;
|
||||
border-top: 2px dashed var(--divider-color);
|
||||
}
|
||||
|
||||
button.link {
|
||||
text-decoration: none;
|
||||
color: var(--primary-color);
|
||||
@@ -779,19 +754,12 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.state {
|
||||
.domain {
|
||||
width: fit-content;
|
||||
font-size: var(--ha-font-size-s);
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
ha-list-item-button::part(end) {
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
:host([sub-entry]) ha-list-item-button::part(base),
|
||||
:host([sub-entry]) ha-list-item-base::part(base) {
|
||||
padding-inline-start: var(--sub-entry-indent);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
padding: var(--ha-space-1);
|
||||
font-family: var(--ha-font-family-code);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -28,6 +28,8 @@ import "../ha-domain-icon";
|
||||
import { floorDefaultIconPath } from "../ha-floor-icon";
|
||||
import "../ha-icon";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-md-list";
|
||||
import "../ha-md-list-item";
|
||||
import "../ha-state-icon";
|
||||
import "../ha-tooltip";
|
||||
|
||||
@@ -74,6 +76,7 @@ export class HaTargetPickerValueChip extends LitElement {
|
||||
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
|
||||
: stateObject
|
||||
? html`<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObject}
|
||||
></ha-state-icon>`
|
||||
: nothing}
|
||||
|
||||
@@ -99,8 +99,7 @@ export class HaTileContainer extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
min-height: var(--row-height, 56px);
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -5,15 +5,19 @@ import { customElement, property } from "lit/decorators";
|
||||
import "../ha-code-editor";
|
||||
import "../ha-icon-button";
|
||||
import type { TraceExtended } from "../../data/trace";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-trace-blueprint-config")
|
||||
export class HaTraceBlueprintConfig extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public trace!: TraceExtended;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-code-editor
|
||||
.value=${dump(this.trace.blueprint_inputs || "").trimRight()}
|
||||
.hass=${this.hass}
|
||||
read-only
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
|
||||
@@ -5,15 +5,19 @@ import { customElement, property } from "lit/decorators";
|
||||
import "../ha-code-editor";
|
||||
import "../ha-icon-button";
|
||||
import type { TraceExtended } from "../../data/trace";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-trace-config")
|
||||
export class HaTraceConfig extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public trace!: TraceExtended;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-code-editor
|
||||
.value=${dump(this.trace.config).trimRight()}
|
||||
.hass=${this.hass}
|
||||
read-only
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import type { Trigger } from "../../data/automation";
|
||||
import { migrateAutomationTrigger } from "../../data/automation";
|
||||
@@ -22,10 +23,9 @@ import "../../panels/logbook/ha-logbook-renderer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-code-editor";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-tab-group";
|
||||
import "../ha-tab-group-tab";
|
||||
import "./hat-logbook-note";
|
||||
import type { NodeInfo } from "./hat-script-graph";
|
||||
import { traceTabStyles } from "./trace-tab-styles";
|
||||
|
||||
const TRACE_PATH_TABS = [
|
||||
"step_config",
|
||||
@@ -66,21 +66,21 @@ export class HaTracePathDetails extends LitElement {
|
||||
${this._renderSelectedTraceInfo()}
|
||||
</div>
|
||||
|
||||
<ha-tab-group @wa-tab-show=${this._handleTabChanged}>
|
||||
<div class="tabs top">
|
||||
${TRACE_PATH_TABS.map(
|
||||
(view) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.active=${this._view === view}
|
||||
.panel=${view}
|
||||
<button
|
||||
.view=${view}
|
||||
class=${classMap({ active: this._view === view })}
|
||||
@click=${this._showTab}
|
||||
>
|
||||
${this.hass!.localize(
|
||||
`ui.panel.config.automation.trace.tabs.${view}`
|
||||
)}
|
||||
</ha-tab-group-tab>
|
||||
</button>
|
||||
`
|
||||
)}
|
||||
</ha-tab-group>
|
||||
</div>
|
||||
${this._view === "step_config"
|
||||
? this._renderSelectedConfig()
|
||||
: this._view === "changed_variables"
|
||||
@@ -271,6 +271,7 @@ export class HaTracePathDetails extends LitElement {
|
||||
return config
|
||||
? html`<ha-code-editor
|
||||
.value=${dump(config).trimEnd()}
|
||||
.hass=${this.hass}
|
||||
read-only
|
||||
dir="ltr"
|
||||
></ha-code-editor>`
|
||||
@@ -307,11 +308,7 @@ export class HaTracePathDetails extends LitElement {
|
||||
? this.hass!.localize(
|
||||
"ui.panel.config.automation.trace.path.no_variables_changed"
|
||||
)
|
||||
: html`<ha-code-editor
|
||||
read-only
|
||||
dir="ltr"
|
||||
.value=${dump(trace.changed_variables).trimEnd()}
|
||||
></ha-code-editor>`}
|
||||
: html`<pre>${dump(trace.changed_variables).trimEnd()}</pre>`}
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
@@ -386,12 +383,13 @@ export class HaTracePathDetails extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _handleTabChanged(ev: CustomEvent) {
|
||||
this._view = ev.detail.name as typeof this._view;
|
||||
private _showTab(ev) {
|
||||
this._view = ev.target.view;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
traceTabStyles,
|
||||
css`
|
||||
.padded-box {
|
||||
margin: 16px;
|
||||
@@ -408,16 +406,6 @@ export class HaTracePathDetails extends LitElement {
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
ha-tab-group {
|
||||
background-color: var(--primary-background-color);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
ha-tab-group-tab::part(base) {
|
||||
padding: 2px 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import { toggleAttribute } from "../../common/dom/toggle_attribute";
|
||||
import { fullEntitiesContext } from "../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { LogbookEntry } from "../../data/logbook";
|
||||
import { localizeTriggerDescription } from "../../data/logbook";
|
||||
import type {
|
||||
ChooseAction,
|
||||
IfAction,
|
||||
@@ -333,10 +332,7 @@ class ActionRenderer {
|
||||
: "other",
|
||||
alias: triggerStep.changed_variables.trigger?.alias,
|
||||
triggeredPath: triggerStep.path === "trigger" ? "manual" : "trigger",
|
||||
trigger: localizeTriggerDescription(
|
||||
this.hass.localize,
|
||||
this.trace.trigger
|
||||
),
|
||||
trigger: this.trace.trigger,
|
||||
time: formatDateTimeWithSeconds(
|
||||
new Date(triggerStep.timestamp),
|
||||
this.hass.locale,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { css } from "lit";
|
||||
|
||||
export const traceTabStyles = css`
|
||||
.tabs {
|
||||
background-color: var(--primary-background-color);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
display: flex;
|
||||
padding-left: 4px;
|
||||
padding-inline-start: 4px;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
|
||||
.tabs.top {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.tabs > * {
|
||||
padding: 2px 16px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
bottom: -1px;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
user-select: none;
|
||||
background: none;
|
||||
color: var(--primary-text-color);
|
||||
outline: none;
|
||||
transition: background 15ms linear;
|
||||
}
|
||||
|
||||
.tabs > *.active {
|
||||
border-bottom-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.tabs > *:focus,
|
||||
.tabs > *:hover {
|
||||
background: var(--secondary-background-color);
|
||||
}
|
||||
`;
|
||||
@@ -1,5 +1,4 @@
|
||||
import type {
|
||||
Connection,
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
HassServiceTarget,
|
||||
@@ -585,19 +584,6 @@ export const testCondition = (
|
||||
variables,
|
||||
});
|
||||
|
||||
export const subscribeCondition = (
|
||||
connection: Connection,
|
||||
onChange: (result: {
|
||||
result?: boolean;
|
||||
error?: string | { code: string; message: string };
|
||||
}) => void,
|
||||
condition: Condition
|
||||
) =>
|
||||
connection.subscribeMessage(onChange, {
|
||||
type: "subscribe_condition",
|
||||
condition,
|
||||
});
|
||||
|
||||
export interface AutomationClipboard {
|
||||
trigger?: Trigger;
|
||||
condition?: Condition;
|
||||
|
||||
@@ -5,7 +5,6 @@ import type {
|
||||
HomeAssistantApi,
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantConnection,
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantInternationalization,
|
||||
HomeAssistantRegistries,
|
||||
HomeAssistantUI,
|
||||
@@ -64,14 +63,6 @@ export const uiContext = createContext<HomeAssistantUI>("hassUi");
|
||||
*/
|
||||
export const configContext = createContext<HomeAssistantConfig>("hassConfig");
|
||||
|
||||
/**
|
||||
* Entity formatting functions: `formatEntityState`, `formatEntityStateToParts`,
|
||||
* `formatEntityAttributeValue`, `formatEntityAttributeValueToParts`,
|
||||
* `formatEntityAttributeName`, and `formatEntityName`.
|
||||
*/
|
||||
export const formattersContext =
|
||||
createContext<HomeAssistantFormatters>("hassFormatters");
|
||||
|
||||
/**
|
||||
* Map of all entities in the entity registry, keyed by entity ID.
|
||||
*/
|
||||
|
||||
@@ -3,7 +3,6 @@ import type {
|
||||
HomeAssistantApi,
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantConnection,
|
||||
HomeAssistantFormatters,
|
||||
HomeAssistantInternationalization,
|
||||
HomeAssistantRegistries,
|
||||
HomeAssistantUI,
|
||||
@@ -157,32 +156,6 @@ const updateConfig = (
|
||||
return value;
|
||||
};
|
||||
|
||||
const updateFormatters = (
|
||||
hass: HomeAssistant,
|
||||
value?: HomeAssistantFormatters
|
||||
): HomeAssistantFormatters => {
|
||||
if (
|
||||
!value ||
|
||||
value.formatEntityState !== hass.formatEntityState ||
|
||||
value.formatEntityStateToParts !== hass.formatEntityStateToParts ||
|
||||
value.formatEntityAttributeValue !== hass.formatEntityAttributeValue ||
|
||||
value.formatEntityAttributeValueToParts !==
|
||||
hass.formatEntityAttributeValueToParts ||
|
||||
value.formatEntityAttributeName !== hass.formatEntityAttributeName ||
|
||||
value.formatEntityName !== hass.formatEntityName
|
||||
) {
|
||||
return {
|
||||
formatEntityState: hass.formatEntityState,
|
||||
formatEntityStateToParts: hass.formatEntityStateToParts,
|
||||
formatEntityAttributeValue: hass.formatEntityAttributeValue,
|
||||
formatEntityAttributeValueToParts: hass.formatEntityAttributeValueToParts,
|
||||
formatEntityAttributeName: hass.formatEntityAttributeName,
|
||||
formatEntityName: hass.formatEntityName,
|
||||
};
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export const updateHassGroups = {
|
||||
registries: updateRegistries,
|
||||
internationalization: updateInternationalization,
|
||||
@@ -190,5 +163,4 @@ export const updateHassGroups = {
|
||||
connection: updateConnection,
|
||||
ui: updateUi,
|
||||
config: updateConfig,
|
||||
formatters: updateFormatters,
|
||||
};
|
||||
|
||||
@@ -164,7 +164,6 @@ export interface BatterySourceTypeEnergyPreference {
|
||||
stat_energy_to: string;
|
||||
stat_rate?: string; // always available if power_config is set
|
||||
power_config?: PowerConfig;
|
||||
stat_soc?: string;
|
||||
}
|
||||
export interface GasSourceTypeEnergyPreference {
|
||||
type: "gas";
|
||||
@@ -182,8 +181,6 @@ export interface GasSourceTypeEnergyPreference {
|
||||
entity_energy_price: string | null;
|
||||
number_energy_price: number | null;
|
||||
unit_of_measurement?: string | null;
|
||||
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface WaterSourceTypeEnergyPreference {
|
||||
@@ -202,8 +199,6 @@ export interface WaterSourceTypeEnergyPreference {
|
||||
entity_energy_price: string | null;
|
||||
number_energy_price: number | null;
|
||||
unit_of_measurement?: string | null;
|
||||
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export type EnergySource =
|
||||
|
||||
@@ -96,10 +96,7 @@ export const getEntities = (
|
||||
|
||||
const domainName = domainToName(hass.localize, computeDomain(entityId));
|
||||
|
||||
const isRTL = computeRTL(
|
||||
hass.language,
|
||||
hass.translationMetadata.translations
|
||||
);
|
||||
const isRTL = computeRTL(hass);
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
|
||||
+9
-26
@@ -456,13 +456,11 @@ const getIconFromTranslations = (
|
||||
};
|
||||
|
||||
export const entityIcon = async (
|
||||
entities: HomeAssistant["entities"],
|
||||
hassConfig: HomeAssistant["config"],
|
||||
hassConnection: Connection,
|
||||
hass: HomeAssistant,
|
||||
stateObj: HassEntity,
|
||||
state?: string
|
||||
) => {
|
||||
const entry = entities?.[stateObj.entity_id] as
|
||||
const entry = hass.entities?.[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
if (entry?.icon) {
|
||||
@@ -470,14 +468,7 @@ export const entityIcon = async (
|
||||
}
|
||||
const domain = computeStateDomain(stateObj);
|
||||
|
||||
return getEntityIcon(
|
||||
hassConfig,
|
||||
hassConnection,
|
||||
domain,
|
||||
stateObj,
|
||||
state,
|
||||
entry
|
||||
);
|
||||
return getEntityIcon(hass, domain, stateObj, state, entry);
|
||||
};
|
||||
|
||||
export const entryIcon = async (
|
||||
@@ -489,19 +480,11 @@ export const entryIcon = async (
|
||||
}
|
||||
const stateObj = hass.states[entry.entity_id] as HassEntity | undefined;
|
||||
const domain = computeDomain(entry.entity_id);
|
||||
return getEntityIcon(
|
||||
hass.config,
|
||||
hass.connection,
|
||||
domain,
|
||||
stateObj,
|
||||
undefined,
|
||||
entry
|
||||
);
|
||||
return getEntityIcon(hass, domain, stateObj, undefined, entry);
|
||||
};
|
||||
|
||||
const getEntityIcon = async (
|
||||
hassConfig: HomeAssistant["config"],
|
||||
hassConnection: Connection,
|
||||
hass: HomeAssistant,
|
||||
domain: string,
|
||||
stateObj?: HassEntity,
|
||||
stateValue?: string,
|
||||
@@ -515,8 +498,8 @@ const getEntityIcon = async (
|
||||
let icon: string | undefined;
|
||||
if (translation_key && platform) {
|
||||
const platformIcons = await getPlatformIcons(
|
||||
hassConfig,
|
||||
hassConnection,
|
||||
hass.config,
|
||||
hass.connection,
|
||||
platform
|
||||
);
|
||||
if (platformIcons) {
|
||||
@@ -532,8 +515,8 @@ const getEntityIcon = async (
|
||||
|
||||
if (!icon) {
|
||||
const entityComponentIcons = await getComponentIcons(
|
||||
hassConnection,
|
||||
hassConfig,
|
||||
hass.connection,
|
||||
hass.config,
|
||||
domain
|
||||
);
|
||||
if (entityComponentIcons) {
|
||||
|
||||
@@ -195,49 +195,6 @@ export const localizeTriggerSource = (
|
||||
return source;
|
||||
};
|
||||
|
||||
// Mapping from a phrase key to the bare-phrase translation key (without the
|
||||
// "triggered by" prefix), used by localizeTriggerDescription below.
|
||||
const triggerDescriptionKeys: Record<
|
||||
TriggerPhraseKeys,
|
||||
| "numeric_state_of"
|
||||
| "state_of"
|
||||
| "event"
|
||||
| "time"
|
||||
| "time_pattern"
|
||||
| "homeassistant_stopping"
|
||||
| "homeassistant_starting"
|
||||
> = {
|
||||
triggered_by_numeric_state_of: "numeric_state_of",
|
||||
triggered_by_state_of: "state_of",
|
||||
triggered_by_event: "event",
|
||||
triggered_by_time_pattern: "time_pattern",
|
||||
triggered_by_time: "time",
|
||||
triggered_by_homeassistant_stopping: "homeassistant_stopping",
|
||||
triggered_by_homeassistant_starting: "homeassistant_starting",
|
||||
};
|
||||
|
||||
// Like localizeTriggerSource, but returns just the bare localized trigger
|
||||
// description (without the "triggered by" prefix). Used where the surrounding
|
||||
// template already supplies its own "triggered by" wording.
|
||||
export const localizeTriggerDescription = (
|
||||
localize: LocalizeFunc,
|
||||
source: string
|
||||
) => {
|
||||
for (const triggerPhraseKey of Object.keys(
|
||||
triggerPhrases
|
||||
) as TriggerPhraseKeys[]) {
|
||||
const phrase = triggerPhrases[triggerPhraseKey];
|
||||
if (source.startsWith(phrase)) {
|
||||
const bareKey = triggerDescriptionKeys[triggerPhraseKey];
|
||||
return source.replace(
|
||||
phrase,
|
||||
`${localize(`ui.components.logbook.${bareKey}`)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return source;
|
||||
};
|
||||
|
||||
export const localizeStateMessage = (
|
||||
hass: HomeAssistant,
|
||||
localize: LocalizeFunc,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user