Compare commits
211 Commits
20240404.0
...
dev
Author | SHA1 | Date |
---|---|---|
renovate[bot] | 174f1991b1 | |
renovate[bot] | e15495a626 | |
renovate[bot] | a8a9a797cb | |
karwosts | 914dbc1e28 | |
Jan-Philipp Benecke | 111816f08a | |
renovate[bot] | 1b4534890c | |
renovate[bot] | ed6542469d | |
renovate[bot] | 3774a3d6ba | |
Bram Kragten | bfa293ae3a | |
renovate[bot] | 9264adb799 | |
renovate[bot] | 829ea4a9e4 | |
Paul Bottein | be26f8bc24 | |
renovate[bot] | c864b34a9a | |
Cody C | 099ea61a94 | |
karwosts | 3ebe6027be | |
karwosts | f5f2a5ad5b | |
Paulus Schoutsen | d046700d06 | |
dependabot[bot] | 2ad84b2832 | |
dependabot[bot] | f9ccb9fc72 | |
renovate[bot] | 6d3940db1e | |
renovate[bot] | 20d174431d | |
renovate[bot] | 1900710e06 | |
renovate[bot] | ed86a48e1c | |
renovate[bot] | d2bdb52926 | |
G Johansson | 9c57c9f151 | |
karwosts | 9e9cb15a42 | |
renovate[bot] | 6421a9443d | |
Paulus Schoutsen | f2b43ddad8 | |
Yosi Levy | e55b59d9b7 | |
Paul Bottein | 4a77359a06 | |
renovate[bot] | 505d7b6ddb | |
Steve Repsher | 79cdc43699 | |
renovate[bot] | 8ff9823cd7 | |
Paul Bottein | 3488c60818 | |
Yosi Levy | 43a422cdca | |
Douwe | 11f2bef05c | |
karwosts | ff9f331287 | |
Steve Repsher | cdf64ccdaa | |
Simon Lamon | 8b220acca2 | |
Paul Bottein | 8fdb7fa1d5 | |
Paulus Schoutsen | 008c842431 | |
Paul Bottein | bc41de0d9c | |
renovate[bot] | 7310c9cf6d | |
Steve Repsher | 84b436c08e | |
renovate[bot] | 1925a47bdc | |
renovate[bot] | 438a426458 | |
karwosts | f923deb71d | |
renovate[bot] | e79bc71ab7 | |
karwosts | 11b0990d2b | |
Simon Lamon | 870cb0c65f | |
Paul Bottein | deda2009f8 | |
renovate[bot] | b2797ab8da | |
renovate[bot] | 644dcb0381 | |
Bram Kragten | c65f4f7a6e | |
Bram Kragten | 68a79490dc | |
Paul Bottein | 6febe8552e | |
Bram Kragten | f611f23f6f | |
Bram Kragten | 627e06663b | |
Paul Bottein | ab01633069 | |
Bram Kragten | 17dcc90638 | |
Paul Bottein | d0df029ff1 | |
Paul Bottein | 86a7e69812 | |
Adam Kapos | af9417f2a6 | |
Paul Bottein | 7120ad99b9 | |
Adam Kapos | 334c245b65 | |
Nicooow | bcb72d83b8 | |
karwosts | c99e0e846b | |
J. Nick Koston | ec3f63e8a3 | |
karwosts | 1bc33a30ec | |
krazos | 8cca233b7c | |
karwosts | a78608bfb4 | |
Bram Kragten | 1a797b3415 | |
Bram Kragten | 2b27a4da2b | |
Bram Kragten | 1df92fa863 | |
Bram Kragten | cdde85315a | |
Paul Bottein | dc67f9faf4 | |
dependabot[bot] | 3ad1be50a2 | |
dependabot[bot] | 8aadfe7d28 | |
renovate[bot] | cff54b73a4 | |
Philip Allgaier | b54cfeb0c0 | |
renovate[bot] | cefe612b11 | |
renovate[bot] | 4bc874b497 | |
Philip Allgaier | f3abaa8e02 | |
Philip Allgaier | 21a563fe98 | |
Paul Bottein | 35d6c638ab | |
Bram Kragten | 68f8239708 | |
renovate[bot] | 0db64cca0b | |
renovate[bot] | accfda5f4b | |
Philip Allgaier | c97c20f57d | |
Philip Allgaier | 2725d0191d | |
renovate[bot] | 852cc62398 | |
David F. Mulcahey | 654e3ce437 | |
Bram Kragten | 20a3a00aec | |
Bram Kragten | 22b927d666 | |
Philip Allgaier | 709d6be2e3 | |
Bram Kragten | fbda9ca418 | |
Paul Bottein | 4e97e3763e | |
Bram Kragten | 4c9c52d27d | |
Bram Kragten | 87bcd3e471 | |
Paul Bottein | 7e9b01b56d | |
karwosts | 713763fc21 | |
renovate[bot] | 5b7ab1bfcb | |
Bram Kragten | 4b0d19b615 | |
Bram Kragten | 90e5d259af | |
Bram Kragten | af3a331f57 | |
Bram Kragten | 67c60a4aa8 | |
Bram Kragten | 62de16bb8e | |
Marc Geurts | d9b71e754d | |
Matthias Alphart | 5fc950f09f | |
J. Nick Koston | 0725c7b160 | |
Steve Repsher | 469dbbcccc | |
Bram Kragten | ffdd661b1f | |
Bram Kragten | 81922f5a3e | |
renovate[bot] | 7e25366897 | |
Bram Kragten | 8ab61b5468 | |
Bram Kragten | 8239f6dd60 | |
Bram Kragten | 45dce18e4d | |
Bram Kragten | a428ad0655 | |
Bram Kragten | 1b54d51e4a | |
Bram Kragten | eb1354d229 | |
Bram Kragten | 4d21f9e80c | |
Paul Bottein | 62f46baacf | |
Bram Kragten | a3090796d2 | |
renovate[bot] | c34c5d64f9 | |
Bram Kragten | 66228f5858 | |
Bram Kragten | ac378cfe6d | |
Nicooow | 7ecf8b755e | |
Matthias Alphart | 141107f1f3 | |
karwosts | b5277dee53 | |
Steve Repsher | 4b593c1c96 | |
dependabot[bot] | 50ce1b94c8 | |
dependabot[bot] | 8bf27a83ec | |
renovate[bot] | 389f0d3d23 | |
karwosts | b966601e6a | |
renovate[bot] | f2a0881821 | |
renovate[bot] | 50a49eae43 | |
renovate[bot] | 1c04561004 | |
renovate[bot] | b80d94d260 | |
karwosts | 87012e23e7 | |
Simon Lamon | f39758b103 | |
G Johansson | 697bbf428e | |
renovate[bot] | c7444a2605 | |
karwosts | 3a5f4d33d2 | |
renovate[bot] | c3dc62523b | |
renovate[bot] | 424622061a | |
renovate[bot] | a3b021b11d | |
renovate[bot] | b60ad8b143 | |
J. Nick Koston | e376efc579 | |
renovate[bot] | 382035a1d4 | |
renovate[bot] | 542e22fe0e | |
karwosts | af37d57779 | |
renovate[bot] | fbef0b0186 | |
renovate[bot] | 9e67d6add8 | |
Simon Lamon | 25c702ad2b | |
Bram Kragten | 6516597c93 | |
renovate[bot] | 1df9c38a8c | |
renovate[bot] | bd7217145a | |
renovate[bot] | 569fef38a4 | |
Thomas Steiner | f21c89cf1a | |
renovate[bot] | 02cc418969 | |
Cougar | 4faba159c0 | |
Adam Kapos | 29816e6c5e | |
karwosts | 5317a11c39 | |
Simon Lamon | 27c53b3241 | |
renovate[bot] | 919befa961 | |
renovate[bot] | f9c02ed099 | |
karwosts | b35c325f43 | |
karwosts | b82f1128fe | |
karwosts | 178feb7330 | |
karwosts | 0118a5bf4c | |
renovate[bot] | e0087bd142 | |
renovate[bot] | c2d3e7900e | |
Charles Garwood | fb8312110b | |
renovate[bot] | 16de57342e | |
renovate[bot] | ad6e041c04 | |
Steve Repsher | e22e3e88a0 | |
Steve Repsher | dc8a50965c | |
Bram Kragten | 1914de7ddf | |
Simon Lamon | 2e505cfb1f | |
Bram Kragten | ab49aca815 | |
Bram Kragten | c96968e476 | |
Simon Lamon | 8f050516ec | |
Simon Lamon | 27d2b244a4 | |
Bram Kragten | be2f2c6271 | |
Bram Kragten | 8dc2797b16 | |
renovate[bot] | 7ca8dabc44 | |
Bram Kragten | baeb55e217 | |
Simon Lamon | a8502fcc11 | |
Paulus Schoutsen | 9f5bc5b196 | |
Bruno Pantaleão Gonçalves | 7556ab9506 | |
Adam Kapos | bf176ac314 | |
Adam Kapos | 9903e22eaa | |
Simon Lamon | 1e0f7d9629 | |
Charles Garwood | e8a140af44 | |
Bram Kragten | b091d4f298 | |
renovate[bot] | 35cf3063cb | |
renovate[bot] | 7141ef17be | |
renovate[bot] | be2c68c0bb | |
renovate[bot] | 7c944d3767 | |
renovate[bot] | 1d4f02df2e | |
renovate[bot] | 2007a74a20 | |
renovate[bot] | 8c0839ad57 | |
renovate[bot] | 516b9a54c4 | |
renovate[bot] | 0d3e730c9c | |
renovate[bot] | c7a87d02b2 | |
Bram Kragten | dd082c204b | |
renovate[bot] | c4af3d1579 | |
Bram Kragten | 10eadbcbbb | |
Bram Kragten | 17141824f7 | |
Bram Kragten | 4cfd6c010f | |
Bram Kragten | daa9024bff |
|
@ -115,6 +115,7 @@
|
|||
}
|
||||
],
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"lit/attribute-names": "warn",
|
||||
"lit/attribute-value-entities": "off",
|
||||
"lit/no-template-map": "off",
|
||||
"lit/no-native-attributes": "warn",
|
||||
|
|
|
@ -21,7 +21,7 @@ jobs:
|
|||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.5
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
|
@ -57,7 +57,7 @@ jobs:
|
|||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.5
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.5
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
|
@ -58,7 +58,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.5
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
|
@ -76,7 +76,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.5
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
|
@ -89,7 +89,7 @@ jobs:
|
|||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
with:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
|
@ -100,7 +100,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.5
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
|
@ -113,7 +113,7 @@ jobs:
|
|||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
with:
|
||||
name: supervisor-bundle-stats
|
||||
path: build/stats/*.json
|
||||
|
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.5
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.5
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
|
@ -58,7 +58,7 @@ jobs:
|
|||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.5
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.5
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
|
|
|
@ -21,7 +21,7 @@ jobs:
|
|||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.5
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
|
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.5
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v5
|
||||
|
@ -57,14 +57,14 @@ jobs:
|
|||
run: tar -czvf translations.tar.gz translations
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.5
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
|
@ -55,7 +55,7 @@ jobs:
|
|||
script/release
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@v2.0.4
|
||||
uses: softprops/action-gh-release@v2.0.5
|
||||
with:
|
||||
files: |
|
||||
dist/*.whl
|
||||
|
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.5
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -6,4 +6,4 @@ enableGlobalCache: false
|
|||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.1.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.2.2.cjs
|
||||
|
|
|
@ -3,6 +3,8 @@ const env = require("./env.cjs");
|
|||
const paths = require("./paths.cjs");
|
||||
const { dependencies } = require("../package.json");
|
||||
|
||||
const BABEL_PLUGINS = path.join(__dirname, "babel-plugins");
|
||||
|
||||
// GitHub base URL to use for production source maps
|
||||
// Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version
|
||||
module.exports.sourceMapURL = () => {
|
||||
|
@ -100,22 +102,12 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
|||
],
|
||||
plugins: [
|
||||
[
|
||||
path.resolve(
|
||||
paths.polymer_dir,
|
||||
"build-scripts/babel-plugins/inline-constants-plugin.cjs"
|
||||
),
|
||||
path.join(BABEL_PLUGINS, "inline-constants-plugin.cjs"),
|
||||
{
|
||||
modules: ["@mdi/js"],
|
||||
ignoreModuleNotFound: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
path.resolve(
|
||||
paths.polymer_dir,
|
||||
"build-scripts/babel-plugins/custom-polyfill-plugin.js"
|
||||
),
|
||||
{ method: "usage-global" },
|
||||
],
|
||||
// Minify template literals for production
|
||||
isProdBuild && [
|
||||
"template-html-minifier",
|
||||
|
@ -153,6 +145,17 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
|||
],
|
||||
sourceMaps: !isTestBuild,
|
||||
overrides: [
|
||||
{
|
||||
// Add plugin to inject various polyfills, excluding the polyfills
|
||||
// themselves to prevent self- injection.
|
||||
plugins: [
|
||||
[
|
||||
path.join(BABEL_PLUGINS, "custom-polyfill-plugin.js"),
|
||||
{ method: "usage-global" },
|
||||
],
|
||||
],
|
||||
exclude: /\/node_modules\/(?:unfetch|proxy-polyfill)\//,
|
||||
},
|
||||
{
|
||||
// Use unambiguous for dependencies so that require() is correctly injected into CommonJS files
|
||||
// Exclusions are needed in some cases where ES modules have no static imports or exports, such as polyfills
|
||||
|
|
|
@ -9,7 +9,7 @@ import gulp from "gulp";
|
|||
import jszip from "jszip";
|
||||
import path from "path";
|
||||
import process from "process";
|
||||
import tar from "tar";
|
||||
import { extract } from "tar";
|
||||
|
||||
const MAX_AGE = 24; // hours
|
||||
const OWNER = "home-assistant";
|
||||
|
@ -156,7 +156,7 @@ gulp.task("fetch-nightly-translations", async function () {
|
|||
console.log("Unpacking downloaded translations...");
|
||||
const zip = await jszip.loadAsync(downloadResponse.data);
|
||||
await deleteCurrent;
|
||||
const extractStream = zip.file(/.*/)[0].nodeStream().pipe(tar.extract());
|
||||
const extractStream = zip.file(/.*/)[0].nodeStream().pipe(extract());
|
||||
await new Promise((resolve, reject) => {
|
||||
extractStream.on("close", resolve).on("error", reject);
|
||||
});
|
||||
|
|
|
@ -1,92 +1,111 @@
|
|||
import { createHash } from "crypto";
|
||||
import { deleteSync } from "del";
|
||||
import { mkdirSync, readdirSync, readFileSync, renameSync } from "fs";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { deleteAsync } from "del";
|
||||
import { glob } from "glob";
|
||||
import gulp from "gulp";
|
||||
import flatmap from "gulp-flatmap";
|
||||
import transform from "gulp-json-transform";
|
||||
import merge from "gulp-merge-json";
|
||||
import rename from "gulp-rename";
|
||||
import path from "path";
|
||||
import vinylBuffer from "vinyl-buffer";
|
||||
import source from "vinyl-source-stream";
|
||||
import merge from "lodash.merge";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, readFile } from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
import { PassThrough, Transform } from "node:stream";
|
||||
import { finished } from "node:stream/promises";
|
||||
import env from "../env.cjs";
|
||||
import paths from "../paths.cjs";
|
||||
import { mapFiles } from "../util.cjs";
|
||||
import "./fetch-nightly-translations.js";
|
||||
|
||||
const inFrontendDir = "translations/frontend";
|
||||
const inBackendDir = "translations/backend";
|
||||
const workDir = "build/translations";
|
||||
const fullDir = workDir + "/full";
|
||||
const coreDir = workDir + "/core";
|
||||
const outDir = workDir + "/output";
|
||||
const outDir = join(workDir, "output");
|
||||
const EN_SRC = join(paths.translations_src, "en.json");
|
||||
|
||||
let mergeBackend = false;
|
||||
|
||||
gulp.task(
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel((done) => {
|
||||
gulp.parallel(async () => {
|
||||
mergeBackend = true;
|
||||
done();
|
||||
}, "allow-setup-fetch-nightly-translations")
|
||||
);
|
||||
|
||||
// Panel translations which should be split from the core translations.
|
||||
const TRANSLATION_FRAGMENTS = Object.keys(
|
||||
JSON.parse(
|
||||
readFileSync(
|
||||
path.resolve(paths.polymer_dir, "src/translations/en.json"),
|
||||
"utf-8"
|
||||
)
|
||||
).ui.panel
|
||||
);
|
||||
// Transform stream to apply a function on Vinyl JSON files (buffer mode only).
|
||||
// The provided function can either return a new object, or an array of
|
||||
// [object, subdirectory] pairs for fragmentizing the JSON.
|
||||
class CustomJSON extends Transform {
|
||||
constructor(func, reviver = null) {
|
||||
super({ objectMode: true });
|
||||
this._func = func;
|
||||
this._reviver = reviver;
|
||||
}
|
||||
|
||||
function recursiveFlatten(prefix, data) {
|
||||
let output = {};
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (typeof data[key] === "object") {
|
||||
output = {
|
||||
...output,
|
||||
...recursiveFlatten(prefix + key + ".", data[key]),
|
||||
};
|
||||
async _transform(file, _, callback) {
|
||||
try {
|
||||
let obj = JSON.parse(file.contents.toString(), this._reviver);
|
||||
if (this._func) obj = this._func(obj, file.path);
|
||||
for (const [outObj, dir] of Array.isArray(obj) ? obj : [[obj, ""]]) {
|
||||
const outFile = file.clone({ contents: false });
|
||||
outFile.contents = Buffer.from(JSON.stringify(outObj));
|
||||
outFile.dirname += `/${dir}`;
|
||||
this.push(outFile);
|
||||
}
|
||||
callback(null);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transform stream to merge Vinyl JSON files (buffer mode only).
|
||||
class MergeJSON extends Transform {
|
||||
_objects = [];
|
||||
|
||||
constructor(stem, startObj = {}, reviver = null) {
|
||||
super({ objectMode: true, allowHalfOpen: false });
|
||||
this._stem = stem;
|
||||
this._startObj = structuredClone(startObj);
|
||||
this._reviver = reviver;
|
||||
}
|
||||
|
||||
async _transform(file, _, callback) {
|
||||
try {
|
||||
this._objects.push(JSON.parse(file.contents.toString(), this._reviver));
|
||||
if (!this._outFile) this._outFile = file.clone({ contents: false });
|
||||
callback(null);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
|
||||
async _flush(callback) {
|
||||
try {
|
||||
const mergedObj = merge(this._startObj, ...this._objects);
|
||||
this._outFile.contents = Buffer.from(JSON.stringify(mergedObj));
|
||||
this._outFile.stem = this._stem;
|
||||
callback(null, this._outFile);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utility to flatten object keys to single level using separator
|
||||
const flatten = (data, prefix = "", sep = ".") => {
|
||||
const output = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (typeof value === "object") {
|
||||
Object.assign(output, flatten(value, prefix + key + sep, sep));
|
||||
} else {
|
||||
output[prefix + key] = data[key];
|
||||
output[prefix + key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
};
|
||||
|
||||
function flatten(data) {
|
||||
return recursiveFlatten("", data);
|
||||
}
|
||||
|
||||
function emptyFilter(data) {
|
||||
const newData = {};
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (data[key]) {
|
||||
if (typeof data[key] === "object") {
|
||||
newData[key] = emptyFilter(data[key]);
|
||||
} else {
|
||||
newData[key] = data[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
return newData;
|
||||
}
|
||||
|
||||
function recursiveEmpty(data) {
|
||||
const newData = {};
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (data[key]) {
|
||||
if (typeof data[key] === "object") {
|
||||
newData[key] = recursiveEmpty(data[key]);
|
||||
} else {
|
||||
newData[key] = "TRANSLATED";
|
||||
}
|
||||
}
|
||||
});
|
||||
return newData;
|
||||
}
|
||||
// Filter functions that can be passed directly to JSON.parse()
|
||||
const emptyReviver = (_key, value) => value || undefined;
|
||||
const testReviver = (_key, value) =>
|
||||
value && typeof value === "string" ? "TRANSLATED" : value;
|
||||
|
||||
/**
|
||||
* Replace Lokalise key placeholders with their actual values.
|
||||
|
@ -95,60 +114,44 @@ function recursiveEmpty(data) {
|
|||
* be included in src/translations/en.json, but still be usable while
|
||||
* developing locally.
|
||||
*
|
||||
* @link https://docs.lokalise.co/article/KO5SZWLLsy-key-referencing
|
||||
* @link https://docs.lokalise.com/en/articles/1400528-key-referencing
|
||||
*/
|
||||
const re_key_reference = /\[%key:([^%]+)%\]/;
|
||||
function lokaliseTransform(data, original, file) {
|
||||
const KEY_REFERENCE = /\[%key:([^%]+)%\]/;
|
||||
const lokaliseTransform = (data, path, original = data) => {
|
||||
const output = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value instanceof Object) {
|
||||
output[key] = lokaliseTransform(value, original, file);
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (typeof value === "object") {
|
||||
output[key] = lokaliseTransform(value, path, original);
|
||||
} else {
|
||||
output[key] = value.replace(re_key_reference, (_match, lokalise_key) => {
|
||||
output[key] = value.replace(KEY_REFERENCE, (_match, lokalise_key) => {
|
||||
const replace = lokalise_key.split("::").reduce((tr, k) => {
|
||||
if (!tr) {
|
||||
throw Error(
|
||||
`Invalid key placeholder ${lokalise_key} in ${file.path}`
|
||||
);
|
||||
throw Error(`Invalid key placeholder ${lokalise_key} in ${path}`);
|
||||
}
|
||||
return tr[k];
|
||||
}, original);
|
||||
if (typeof replace !== "string") {
|
||||
throw Error(
|
||||
`Invalid key placeholder ${lokalise_key} in ${file.path}`
|
||||
);
|
||||
throw Error(`Invalid key placeholder ${lokalise_key} in ${path}`);
|
||||
}
|
||||
return replace;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
};
|
||||
|
||||
gulp.task("clean-translations", async () => deleteSync([workDir]));
|
||||
gulp.task("clean-translations", () => deleteAsync([workDir]));
|
||||
|
||||
gulp.task("ensure-translations-build-dir", async () => {
|
||||
mkdirSync(workDir, { recursive: true });
|
||||
});
|
||||
const makeWorkDir = () => mkdir(workDir, { recursive: true });
|
||||
|
||||
gulp.task("create-test-metadata", () =>
|
||||
env.isProdBuild()
|
||||
? Promise.resolve()
|
||||
: writeFile(
|
||||
workDir + "/testMetadata.json",
|
||||
JSON.stringify({ test: { nativeName: "Test" } })
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("create-test-translation", () =>
|
||||
const createTestTranslation = () =>
|
||||
env.isProdBuild()
|
||||
? Promise.resolve()
|
||||
: gulp
|
||||
.src(path.join(paths.translations_src, "en.json"))
|
||||
.pipe(transform((data, _file) => recursiveEmpty(data)))
|
||||
.src(EN_SRC)
|
||||
.pipe(new CustomJSON(null, testReviver))
|
||||
.pipe(rename("test.json"))
|
||||
.pipe(gulp.dest(workDir))
|
||||
);
|
||||
.pipe(gulp.dest(workDir));
|
||||
|
||||
/**
|
||||
* This task will build a master translation file, to be used as the base for
|
||||
|
@ -159,279 +162,164 @@ gulp.task("create-test-translation", () =>
|
|||
* project is buildable immediately after merging new translation keys, since
|
||||
* the Lokalise update to translations/en.json will not happen immediately.
|
||||
*/
|
||||
gulp.task("build-master-translation", () => {
|
||||
const src = [path.join(paths.translations_src, "en.json")];
|
||||
const createMasterTranslation = () =>
|
||||
gulp
|
||||
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])])
|
||||
.pipe(new CustomJSON(lokaliseTransform))
|
||||
.pipe(new MergeJSON("en"))
|
||||
.pipe(gulp.dest(workDir));
|
||||
|
||||
if (mergeBackend) {
|
||||
src.push(path.join(inBackendDir, "en.json"));
|
||||
const FRAGMENTS = ["base"];
|
||||
|
||||
const toggleSupervisorFragment = async () => {
|
||||
FRAGMENTS[0] = "supervisor";
|
||||
};
|
||||
|
||||
const panelFragment = (fragment) =>
|
||||
fragment !== "base" && fragment !== "supervisor";
|
||||
|
||||
const HASHES = new Map();
|
||||
|
||||
const createTranslations = async () => {
|
||||
// Parse and store the master to avoid repeating this for each locale, then
|
||||
// add the panel fragments when processing the app.
|
||||
const enMaster = JSON.parse(await readFile(`${workDir}/en.json`, "utf-8"));
|
||||
if (FRAGMENTS[0] === "base") {
|
||||
FRAGMENTS.push(...Object.keys(enMaster.ui.panel));
|
||||
}
|
||||
|
||||
return gulp
|
||||
.src(src)
|
||||
.pipe(transform((data, file) => lokaliseTransform(data, data, file)))
|
||||
// The downstream pipeline is setup first. It hashes the merged data for
|
||||
// each locale, then fragmentizes and flattens the data for final output.
|
||||
const translationFiles = await glob([
|
||||
`${inFrontendDir}/!(en).json`,
|
||||
...(env.isProdBuild() ? [] : [`${workDir}/test.json`]),
|
||||
]);
|
||||
const hashStream = new Transform({
|
||||
objectMode: true,
|
||||
transform: async (file, _, callback) => {
|
||||
const hash = env.isProdBuild()
|
||||
? createHash("md5").update(file.contents).digest("hex")
|
||||
: "dev";
|
||||
HASHES.set(file.stem, hash);
|
||||
file.stem += `-${hash}`;
|
||||
callback(null, file);
|
||||
},
|
||||
}).setMaxListeners(translationFiles.length + 1);
|
||||
const fragmentsStream = hashStream
|
||||
.pipe(
|
||||
merge({
|
||||
fileName: "en.json",
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(fullDir));
|
||||
});
|
||||
|
||||
gulp.task("build-merged-translations", () =>
|
||||
gulp
|
||||
.src([
|
||||
inFrontendDir + "/*.json",
|
||||
"!" + inFrontendDir + "/en.json",
|
||||
...(env.isProdBuild() ? [] : [workDir + "/test.json"]),
|
||||
])
|
||||
.pipe(transform((data, file) => lokaliseTransform(data, data, file)))
|
||||
.pipe(
|
||||
flatmap((stream, file) => {
|
||||
// For each language generate a merged json file. It begins with the master
|
||||
// translation as a failsafe for untranslated strings, and merges all parent
|
||||
// tags into one file for each specific subtag
|
||||
//
|
||||
// TODO: This is a naive interpretation of BCP47 that should be improved.
|
||||
// Will be OK for now as long as we don't have anything more complicated
|
||||
// than a base translation + region.
|
||||
const tr = path.basename(file.history[0], ".json");
|
||||
const subtags = tr.split("-");
|
||||
const src = [fullDir + "/en.json"];
|
||||
for (let i = 1; i <= subtags.length; i++) {
|
||||
const lang = subtags.slice(0, i).join("-");
|
||||
if (lang === "test") {
|
||||
src.push(workDir + "/test.json");
|
||||
} else if (lang !== "en") {
|
||||
src.push(inFrontendDir + "/" + lang + ".json");
|
||||
if (mergeBackend) {
|
||||
src.push(inBackendDir + "/" + lang + ".json");
|
||||
}
|
||||
new CustomJSON((data) =>
|
||||
FRAGMENTS.map((fragment) => {
|
||||
switch (fragment) {
|
||||
case "base":
|
||||
// Remove the panels and supervisor to create the base translations
|
||||
return [
|
||||
flatten({
|
||||
...data,
|
||||
ui: { ...data.ui, panel: undefined },
|
||||
supervisor: undefined,
|
||||
}),
|
||||
"",
|
||||
];
|
||||
case "supervisor":
|
||||
// Supervisor key is at the top level
|
||||
return [flatten(data.supervisor), ""];
|
||||
default:
|
||||
// Create a fragment with only the given panel
|
||||
return [
|
||||
flatten(data.ui.panel[fragment], `ui.panel.${fragment}.`),
|
||||
fragment,
|
||||
];
|
||||
}
|
||||
}
|
||||
return gulp
|
||||
.src(src, { allowEmpty: true })
|
||||
.pipe(transform((data) => emptyFilter(data)))
|
||||
.pipe(
|
||||
merge({
|
||||
fileName: tr + ".json",
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(fullDir));
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
let taskName;
|
||||
|
||||
const splitTasks = [];
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
taskName = "build-translation-fragment-" + fragment;
|
||||
gulp.task(taskName, () =>
|
||||
// Return only the translations for this fragment.
|
||||
gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(
|
||||
transform((data) => ({
|
||||
ui: {
|
||||
panel: {
|
||||
[fragment]: data.ui.panel[fragment],
|
||||
},
|
||||
},
|
||||
}))
|
||||
)
|
||||
.pipe(gulp.dest(workDir + "/" + fragment))
|
||||
);
|
||||
splitTasks.push(taskName);
|
||||
});
|
||||
|
||||
taskName = "build-translation-core";
|
||||
gulp.task(taskName, () =>
|
||||
// Remove the fragment translations from the core translation.
|
||||
gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(
|
||||
transform((data, _file) => {
|
||||
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||
delete data.ui.panel[fragment];
|
||||
});
|
||||
delete data.supervisor;
|
||||
return data;
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(coreDir))
|
||||
);
|
||||
|
||||
splitTasks.push(taskName);
|
||||
|
||||
gulp.task("build-flattened-translations", () =>
|
||||
// Flatten the split versions of our translations, and move them into outDir
|
||||
gulp
|
||||
.src(
|
||||
TRANSLATION_FRAGMENTS.map(
|
||||
(fragment) => workDir + "/" + fragment + "/*.json"
|
||||
).concat(coreDir + "/*.json"),
|
||||
{ base: workDir }
|
||||
)
|
||||
.pipe(
|
||||
transform((data) =>
|
||||
// Polymer.AppLocalizeBehavior requires flattened json
|
||||
flatten(data)
|
||||
})
|
||||
)
|
||||
)
|
||||
.pipe(
|
||||
rename((filePath) => {
|
||||
if (filePath.dirname === "core") {
|
||||
filePath.dirname = "";
|
||||
.pipe(gulp.dest(outDir));
|
||||
|
||||
// Send the English master downstream first, then for each other locale
|
||||
// generate merged JSON data to continue piping. It begins with the master
|
||||
// translation as a failsafe for untranslated strings, and merges all parent
|
||||
// tags into one file for each specific subtag
|
||||
//
|
||||
// TODO: This is a naive interpretation of BCP47 that should be improved.
|
||||
// Will be OK for now as long as we don't have anything more complicated
|
||||
// than a base translation + region.
|
||||
gulp
|
||||
.src(`${workDir}/en.json`)
|
||||
.pipe(new PassThrough({ objectMode: true }))
|
||||
.pipe(hashStream, { end: false });
|
||||
const mergesFinished = [];
|
||||
for (const translationFile of translationFiles) {
|
||||
const locale = basename(translationFile, ".json");
|
||||
const subtags = locale.split("-");
|
||||
const mergeFiles = [];
|
||||
for (let i = 1; i <= subtags.length; i++) {
|
||||
const lang = subtags.slice(0, i).join("-");
|
||||
if (lang === "test") {
|
||||
mergeFiles.push(`${workDir}/test.json`);
|
||||
} else if (lang !== "en") {
|
||||
mergeFiles.push(`${inFrontendDir}/${lang}.json`);
|
||||
if (mergeBackend) {
|
||||
mergeFiles.push(`${inBackendDir}/${lang}.json`);
|
||||
}
|
||||
// In dev we create the file with the fake hash in the filename
|
||||
if (!env.isProdBuild()) {
|
||||
filePath.basename += "-dev";
|
||||
}
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(outDir))
|
||||
);
|
||||
|
||||
const fingerprints = {};
|
||||
|
||||
gulp.task("build-translation-fingerprints", () => {
|
||||
// Fingerprint full file of each language
|
||||
const files = readdirSync(fullDir);
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
fingerprints[files[i].split(".")[0]] = {
|
||||
// In dev we create fake hashes
|
||||
hash: env.isProdBuild()
|
||||
? createHash("md5")
|
||||
.update(readFileSync(path.join(fullDir, files[i]), "utf-8"))
|
||||
.digest("hex")
|
||||
: "dev",
|
||||
};
|
||||
}
|
||||
|
||||
// In dev we create the file with the fake hash in the filename
|
||||
if (env.isProdBuild()) {
|
||||
mapFiles(outDir, ".json", (filename) => {
|
||||
const parsed = path.parse(filename);
|
||||
|
||||
// nl.json -> nl-<hash>.json
|
||||
if (!(parsed.name in fingerprints)) {
|
||||
throw new Error(`Unable to find hash for ${filename}`);
|
||||
}
|
||||
|
||||
renameSync(
|
||||
filename,
|
||||
`${parsed.dir}/${parsed.name}-${fingerprints[parsed.name].hash}${
|
||||
parsed.ext
|
||||
}`
|
||||
);
|
||||
});
|
||||
}
|
||||
const mergeStream = gulp
|
||||
.src(mergeFiles, { allowEmpty: true })
|
||||
.pipe(new MergeJSON(locale, enMaster, emptyReviver));
|
||||
mergesFinished.push(finished(mergeStream));
|
||||
mergeStream.pipe(hashStream, { end: false });
|
||||
}
|
||||
|
||||
const stream = source("translationFingerprints.json");
|
||||
stream.write(JSON.stringify(fingerprints));
|
||||
process.nextTick(() => stream.end());
|
||||
return stream.pipe(vinylBuffer()).pipe(gulp.dest(workDir));
|
||||
});
|
||||
// Wait for all merges to finish, then it's safe to end writing to the
|
||||
// downstream pipeline and wait for all fragments to finish writing.
|
||||
await Promise.all(mergesFinished);
|
||||
hashStream.end();
|
||||
await finished(fragmentsStream);
|
||||
};
|
||||
|
||||
gulp.task("build-translation-fragment-supervisor", () =>
|
||||
const writeTranslationMetaData = () =>
|
||||
gulp
|
||||
.src(fullDir + "/*.json")
|
||||
.pipe(transform((data) => data.supervisor))
|
||||
.src([`${paths.translations_src}/translationMetadata.json`])
|
||||
.pipe(
|
||||
rename((filePath) => {
|
||||
// In dev we create the file with the fake hash in the filename
|
||||
new CustomJSON((meta) => {
|
||||
// Add the test translation in development.
|
||||
if (!env.isProdBuild()) {
|
||||
filePath.basename += "-dev";
|
||||
meta.test = { nativeName: "Test" };
|
||||
}
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(workDir + "/supervisor"))
|
||||
);
|
||||
|
||||
gulp.task("build-translation-flatten-supervisor", () =>
|
||||
gulp
|
||||
.src(workDir + "/supervisor/*.json")
|
||||
.pipe(
|
||||
transform((data) =>
|
||||
// Polymer.AppLocalizeBehavior requires flattened json
|
||||
flatten(data)
|
||||
)
|
||||
)
|
||||
.pipe(gulp.dest(outDir))
|
||||
);
|
||||
|
||||
gulp.task("build-translation-write-metadata", () =>
|
||||
gulp
|
||||
.src([
|
||||
path.join(paths.translations_src, "translationMetadata.json"),
|
||||
...(env.isProdBuild() ? [] : [workDir + "/testMetadata.json"]),
|
||||
workDir + "/translationFingerprints.json",
|
||||
])
|
||||
.pipe(merge({}))
|
||||
.pipe(
|
||||
transform((data) => {
|
||||
const newData = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
// Filter out translations without native name.
|
||||
if (value.nativeName) {
|
||||
newData[key] = value;
|
||||
} else {
|
||||
// Filter out locales without a native name, and add the hashes.
|
||||
for (const locale of Object.keys(meta)) {
|
||||
if (!meta[locale].nativeName) {
|
||||
meta[locale] = undefined;
|
||||
console.warn(
|
||||
`Skipping language ${key}. Native name was not translated.`
|
||||
`Skipping locale ${locale} because native name is not translated.`
|
||||
);
|
||||
} else {
|
||||
meta[locale].hash = HASHES.get(locale);
|
||||
}
|
||||
});
|
||||
return newData;
|
||||
}
|
||||
return {
|
||||
fragments: FRAGMENTS.filter(panelFragment),
|
||||
translations: meta,
|
||||
};
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
transform((data) => ({
|
||||
fragments: TRANSLATION_FRAGMENTS,
|
||||
translations: data,
|
||||
}))
|
||||
)
|
||||
.pipe(rename("translationMetadata.json"))
|
||||
.pipe(gulp.dest(workDir))
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"create-translations",
|
||||
gulp.series(
|
||||
gulp.parallel("create-test-metadata", "create-test-translation"),
|
||||
"build-master-translation",
|
||||
"build-merged-translations",
|
||||
gulp.parallel(...splitTasks),
|
||||
"build-flattened-translations"
|
||||
)
|
||||
);
|
||||
.pipe(gulp.dest(workDir));
|
||||
|
||||
gulp.task(
|
||||
"build-translations",
|
||||
gulp.series(
|
||||
gulp.parallel(
|
||||
"fetch-nightly-translations",
|
||||
gulp.series("clean-translations", "ensure-translations-build-dir")
|
||||
gulp.series("clean-translations", makeWorkDir)
|
||||
),
|
||||
"create-translations",
|
||||
"build-translation-fingerprints",
|
||||
"build-translation-write-metadata"
|
||||
createTestTranslation,
|
||||
createMasterTranslation,
|
||||
createTranslations,
|
||||
writeTranslationMetaData
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-supervisor-translations",
|
||||
gulp.series(
|
||||
gulp.parallel(
|
||||
"fetch-nightly-translations",
|
||||
gulp.series("clean-translations", "ensure-translations-build-dir")
|
||||
),
|
||||
gulp.parallel("create-test-metadata", "create-test-translation"),
|
||||
"build-master-translation",
|
||||
"build-merged-translations",
|
||||
"build-translation-fragment-supervisor",
|
||||
"build-translation-flatten-supervisor",
|
||||
"build-translation-fingerprints",
|
||||
"build-translation-write-metadata"
|
||||
)
|
||||
gulp.series(toggleSupervisorFragment, "build-translations")
|
||||
);
|
||||
|
|
|
@ -99,7 +99,7 @@ gulp.task("webpack-watch-app", () => {
|
|||
).watch({ poll: isWsl }, doneHandler());
|
||||
gulp.watch(
|
||||
path.join(paths.translations_src, "en.json"),
|
||||
gulp.series("create-translations", "copy-translations-app")
|
||||
gulp.series("build-translations", "copy-translations-app")
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
// Helper function to map recursively over files in a folder and it's subfolders
|
||||
module.exports.mapFiles = function mapFiles(startPath, filter, mapFunc) {
|
||||
const files = fs.readdirSync(startPath);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const filename = path.join(startPath, files[i]);
|
||||
const stat = fs.lstatSync(filename);
|
||||
if (stat.isDirectory()) {
|
||||
mapFiles(filename, filter, mapFunc);
|
||||
} else if (filename.indexOf(filter) >= 0) {
|
||||
mapFunc(filename);
|
||||
}
|
||||
}
|
||||
};
|
|
@ -10,6 +10,7 @@ const WebpackBar = require("webpackbar");
|
|||
const {
|
||||
TransformAsyncModulesPlugin,
|
||||
} = require("transform-async-modules-webpack-plugin");
|
||||
const { dependencies } = require("../package.json");
|
||||
const paths = require("./paths.cjs");
|
||||
const bundle = require("./bundle.cjs");
|
||||
|
||||
|
@ -156,7 +157,10 @@ const createWebpackConfig = ({
|
|||
transform: (stats) => JSON.stringify(filterStats(stats)),
|
||||
}),
|
||||
!latestBuild &&
|
||||
new TransformAsyncModulesPlugin({ browserslistEnv: "legacy" }),
|
||||
new TransformAsyncModulesPlugin({
|
||||
browserslistEnv: "legacy",
|
||||
runtime: { version: dependencies["@babel/runtime"] },
|
||||
}),
|
||||
].filter(Boolean),
|
||||
resolve: {
|
||||
extensions: [".ts", ".js", ".json"],
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
import { selectedDemoConfig } from "./configs/demo-configs";
|
||||
import { mockAreaRegistry } from "./stubs/area_registry";
|
||||
import { mockAuth } from "./stubs/auth";
|
||||
import { mockConfigEntries } from "./stubs/config_entries";
|
||||
import { mockEnergy } from "./stubs/energy";
|
||||
|
@ -23,10 +24,10 @@ import { mockLovelace } from "./stubs/lovelace";
|
|||
import { mockMediaPlayer } from "./stubs/media_player";
|
||||
import { mockPersistentNotification } from "./stubs/persistent_notification";
|
||||
import { mockRecorder } from "./stubs/recorder";
|
||||
import { mockTodo } from "./stubs/todo";
|
||||
import { mockSensor } from "./stubs/sensor";
|
||||
import { mockSystemLog } from "./stubs/system_log";
|
||||
import { mockTemplate } from "./stubs/template";
|
||||
import { mockTodo } from "./stubs/todo";
|
||||
import { mockTranslations } from "./stubs/translations";
|
||||
|
||||
@customElement("ha-demo")
|
||||
|
@ -62,6 +63,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
|||
mockEnergy(hass);
|
||||
mockPersistentNotification(hass);
|
||||
mockConfigEntries(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockEntityRegistry(hass, [
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { format, startOfToday, startOfTomorrow } from "date-fns/esm";
|
||||
import { format, startOfToday, startOfTomorrow } from "date-fns";
|
||||
import {
|
||||
EnergyInfo,
|
||||
EnergyPreferences,
|
||||
|
|
|
@ -161,12 +161,14 @@ const LABELS: LabelRegistryEntry[] = [
|
|||
name: "Energy",
|
||||
icon: null,
|
||||
color: "yellow",
|
||||
description: null,
|
||||
},
|
||||
{
|
||||
label_id: "entertainment",
|
||||
name: "Entertainment",
|
||||
icon: "mdi:popcorn",
|
||||
color: "blue",
|
||||
description: null,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
|||
import { customElement, query } from "lit/decorators";
|
||||
import { CoverEntityFeature } from "../../../../src/data/cover";
|
||||
import { LightColorMode } from "../../../../src/data/light";
|
||||
import { LockEntityFeature } from "../../../../src/data/lock";
|
||||
import { VacuumEntityFeature } from "../../../../src/data/vacuum";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
|
@ -20,6 +21,11 @@ const ENTITIES = [
|
|||
getEntity("light", "unavailable", "unavailable", {
|
||||
friendly_name: "Unavailable entity",
|
||||
}),
|
||||
getEntity("lock", "front_door", "locked", {
|
||||
friendly_name: "Front Door Lock",
|
||||
device_class: "lock",
|
||||
supported_features: LockEntityFeature.OPEN,
|
||||
}),
|
||||
getEntity("climate", "thermostat", "heat", {
|
||||
current_temperature: 73,
|
||||
min_temp: 45,
|
||||
|
@ -138,6 +144,24 @@ const CONFIGS = [
|
|||
- type: "color-temp"
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Lock commands feature",
|
||||
config: `
|
||||
- type: tile
|
||||
entity: lock.front_door
|
||||
features:
|
||||
- type: "lock-commands"
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Lock open door feature",
|
||||
config: `
|
||||
- type: tile
|
||||
entity: lock.front_door
|
||||
features:
|
||||
- type: "lock-open-door"
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Vacuum commands feature",
|
||||
config: `
|
||||
|
|
|
@ -36,6 +36,8 @@ const createConfigEntry = (
|
|||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
reason: null,
|
||||
error_reason_translation_key: null,
|
||||
error_reason_translation_placeholders: null,
|
||||
...override,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import { globIterate } from "glob";
|
||||
import { availableParallelism } from "node:os";
|
||||
|
||||
process.env.UV_THREADPOOL_SIZE = availableParallelism();
|
||||
|
||||
const gulpImports = [];
|
||||
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import { mdiStorePlus, mdiUpdate } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { mdiRefresh, mdiStorePlus } from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/ha-fab";
|
||||
import { reloadHassioAddons } from "../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import { supervisorTabs } from "../hassio-tabs";
|
||||
import "./hassio-addons";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import { reloadHassioAddons } from "../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
|
||||
@customElement("hassio-dashboard")
|
||||
class HassioDashboard extends LitElement {
|
||||
|
@ -43,7 +43,7 @@ class HassioDashboard extends LitElement {
|
|||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
@click=${this._handleCheckUpdates}
|
||||
.path=${mdiUpdate}
|
||||
.path=${mdiRefresh}
|
||||
.label=${this.supervisor.localize("store.check_updates")}
|
||||
></ha-icon-button>
|
||||
<hassio-addons
|
||||
|
|
109
package.json
109
package.json
|
@ -25,15 +25,15 @@
|
|||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.24.1",
|
||||
"@babel/runtime": "7.24.5",
|
||||
"@braintree/sanitize-url": "7.0.1",
|
||||
"@codemirror/autocomplete": "6.15.0",
|
||||
"@codemirror/commands": "6.3.3",
|
||||
"@codemirror/autocomplete": "6.16.0",
|
||||
"@codemirror/commands": "6.5.0",
|
||||
"@codemirror/language": "6.10.1",
|
||||
"@codemirror/legacy-modes": "6.3.3",
|
||||
"@codemirror/legacy-modes": "6.4.0",
|
||||
"@codemirror/search": "6.5.6",
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@codemirror/view": "6.26.1",
|
||||
"@codemirror/view": "6.26.3",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.12.3",
|
||||
"@formatjs/intl-displaynames": "6.6.6",
|
||||
|
@ -70,7 +70,6 @@
|
|||
"@material/mwc-list": "0.27.0",
|
||||
"@material/mwc-menu": "0.27.0",
|
||||
"@material/mwc-radio": "0.27.0",
|
||||
"@material/mwc-ripple": "0.27.0",
|
||||
"@material/mwc-select": "0.27.0",
|
||||
"@material/mwc-snackbar": "0.27.0",
|
||||
"@material/mwc-switch": "0.27.0",
|
||||
|
@ -81,7 +80,7 @@
|
|||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/web": "=1.3.0",
|
||||
"@material/web": "1.4.1",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@polymer/paper-item": "3.0.1",
|
||||
|
@ -89,8 +88,8 @@
|
|||
"@polymer/paper-tabs": "3.1.0",
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.3.10",
|
||||
"@vaadin/vaadin-themable-mixin": "24.3.10",
|
||||
"@vaadin/combo-box": "24.3.13",
|
||||
"@vaadin/vaadin-themable-mixin": "24.3.13",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
|
@ -101,25 +100,25 @@
|
|||
"chart.js": "4.4.2",
|
||||
"color-name": "2.0.0",
|
||||
"comlink": "4.4.1",
|
||||
"core-js": "3.36.1",
|
||||
"cropperjs": "1.6.1",
|
||||
"date-fns": "2.30.0",
|
||||
"date-fns-tz": "2.0.1",
|
||||
"core-js": "3.37.1",
|
||||
"cropperjs": "1.6.2",
|
||||
"date-fns": "3.6.0",
|
||||
"date-fns-tz": "3.1.3",
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"element-internals-polyfill": "1.3.10",
|
||||
"element-internals-polyfill": "1.3.11",
|
||||
"fuse.js": "7.0.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||
"home-assistant-js-websocket": "9.2.1",
|
||||
"home-assistant-js-websocket": "9.3.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.5.11",
|
||||
"intl-messageformat": "10.5.12",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "1.0.4",
|
||||
"lit": "2.8.0",
|
||||
"luxon": "3.4.4",
|
||||
"marked": "12.0.1",
|
||||
"marked": "12.0.2",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "0.3.2",
|
||||
|
@ -141,27 +140,27 @@
|
|||
"vue": "2.7.16",
|
||||
"vue2-daterange-picker": "0.6.8",
|
||||
"weekstart": "2.0.0",
|
||||
"workbox-cacheable-response": "7.0.0",
|
||||
"workbox-core": "7.0.0",
|
||||
"workbox-expiration": "7.0.0",
|
||||
"workbox-precaching": "7.0.0",
|
||||
"workbox-routing": "7.0.0",
|
||||
"workbox-strategies": "7.0.0",
|
||||
"workbox-cacheable-response": "7.1.0",
|
||||
"workbox-core": "7.1.0",
|
||||
"workbox-expiration": "7.1.0",
|
||||
"workbox-precaching": "7.1.0",
|
||||
"workbox-routing": "7.1.0",
|
||||
"workbox-strategies": "7.1.0",
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.24.3",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.1",
|
||||
"@babel/core": "7.24.5",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.2",
|
||||
"@babel/plugin-proposal-decorators": "7.24.1",
|
||||
"@babel/plugin-transform-runtime": "7.24.3",
|
||||
"@babel/preset-env": "7.24.3",
|
||||
"@babel/preset-env": "7.24.5",
|
||||
"@babel/preset-typescript": "7.24.1",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.12.2",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@lokalise/node-api": "12.3.0",
|
||||
"@octokit/auth-oauth-device": "7.0.1",
|
||||
"@octokit/plugin-retry": "7.0.3",
|
||||
"@octokit/rest": "20.0.2",
|
||||
"@lokalise/node-api": "12.5.0",
|
||||
"@octokit/auth-oauth-device": "7.1.1",
|
||||
"@octokit/plugin-retry": "7.1.1",
|
||||
"@octokit/rest": "20.1.1",
|
||||
"@open-wc/dev-server-hmr": "0.1.4",
|
||||
"@rollup/plugin-babel": "6.0.4",
|
||||
"@rollup/plugin-commonjs": "25.0.7",
|
||||
|
@ -169,29 +168,30 @@
|
|||
"@rollup/plugin-node-resolve": "15.2.3",
|
||||
"@rollup/plugin-replace": "5.0.5",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.13",
|
||||
"@types/chromecast-caf-sender": "1.0.9",
|
||||
"@types/color-name": "1.1.3",
|
||||
"@types/chromecast-caf-receiver": "6.0.14",
|
||||
"@types/chromecast-caf-sender": "1.0.10",
|
||||
"@types/color-name": "1.1.4",
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/html-minifier-terser": "7.0.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/leaflet": "1.9.8",
|
||||
"@types/leaflet": "1.9.12",
|
||||
"@types/leaflet-draw": "1.0.11",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/mocha": "10.0.6",
|
||||
"@types/qrcode": "1.5.5",
|
||||
"@types/serve-handler": "6.1.4",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
"@types/tar": "6.1.11",
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "7.4.0",
|
||||
"@typescript-eslint/parser": "7.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.9.0",
|
||||
"@typescript-eslint/parser": "7.9.0",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@web/dev-server-rollup": "0.4.1",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"chai": "5.1.0",
|
||||
"chai": "5.1.1",
|
||||
"del": "7.1.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
|
@ -200,17 +200,15 @@
|
|||
"eslint-import-resolver-webpack": "0.13.8",
|
||||
"eslint-plugin-disable": "2.0.3",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-lit": "1.11.0",
|
||||
"eslint-plugin-lit": "1.13.0",
|
||||
"eslint-plugin-lit-a11y": "4.1.2",
|
||||
"eslint-plugin-unused-imports": "3.1.0",
|
||||
"eslint-plugin-wc": "2.0.4",
|
||||
"eslint-plugin-unused-imports": "3.2.0",
|
||||
"eslint-plugin-wc": "2.1.0",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"glob": "10.3.10",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-flatmap": "1.0.2",
|
||||
"glob": "10.3.15",
|
||||
"gulp": "5.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-merge-json": "2.2.1",
|
||||
"gulp-rename": "2.0.0",
|
||||
"gulp-zopfli-green": "6.0.1",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
|
@ -219,10 +217,11 @@
|
|||
"jszip": "3.10.1",
|
||||
"lint-staged": "15.2.2",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
"magic-string": "0.30.8",
|
||||
"magic-string": "0.30.10",
|
||||
"map-stream": "0.0.7",
|
||||
"mocha": "10.3.0",
|
||||
"mocha": "10.4.0",
|
||||
"object-hash": "3.0.0",
|
||||
"open": "10.1.0",
|
||||
"pinst": "3.0.0",
|
||||
|
@ -232,23 +231,21 @@
|
|||
"rollup-plugin-terser": "7.0.2",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"serve-handler": "6.1.5",
|
||||
"sinon": "17.0.1",
|
||||
"sinon": "18.0.0",
|
||||
"source-map-url": "0.4.1",
|
||||
"systemjs": "6.14.3",
|
||||
"tar": "6.2.1",
|
||||
"systemjs": "6.15.1",
|
||||
"tar": "7.1.0",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"transform-async-modules-webpack-plugin": "1.0.4",
|
||||
"transform-async-modules-webpack-plugin": "1.1.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.4.3",
|
||||
"vinyl-buffer": "1.0.1",
|
||||
"vinyl-source-stream": "2.0.0",
|
||||
"typescript": "5.4.5",
|
||||
"webpack": "5.91.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.0.4",
|
||||
"webpack-manifest-plugin": "5.0.0",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "6.0.1",
|
||||
"workbox-build": "7.0.0"
|
||||
"workbox-build": "7.1.0"
|
||||
},
|
||||
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
|
||||
"resolutions": {
|
||||
|
@ -260,5 +257,5 @@
|
|||
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
|
||||
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.1.1"
|
||||
"packageManager": "yarn@4.2.2"
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20240404.0"
|
||||
version = "20240501.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
|
|
@ -40,6 +40,11 @@
|
|||
"matchPackageNames": ["tsparticles-engine"],
|
||||
"matchPackagePrefixes": ["tsparticles-preset-"]
|
||||
},
|
||||
{
|
||||
"description": "Group date-fns with dependent timezone package",
|
||||
"groupName": "date-fns",
|
||||
"matchPackageNames": ["date-fns", "date-fns-tz"]
|
||||
},
|
||||
{
|
||||
"description": "Group and temporarily disable WDS packages",
|
||||
"groupName": "Web Dev Server",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { utcToZonedTime, zonedTimeToUtc } from "date-fns-tz";
|
||||
import { toZonedTime, fromZonedTime } from "date-fns-tz";
|
||||
import { HassConfig } from "home-assistant-js-websocket";
|
||||
import { FrontendLocaleData, TimeZone } from "../../data/translation";
|
||||
|
||||
|
@ -8,10 +8,10 @@ const calcZonedDate = (
|
|||
fn: (date: Date, options?: any) => Date | number | boolean,
|
||||
options?
|
||||
) => {
|
||||
const inputZoned = utcToZonedTime(date, tz);
|
||||
const inputZoned = toZonedTime(date, tz);
|
||||
const fnZoned = fn(inputZoned, options);
|
||||
if (fnZoned instanceof Date) {
|
||||
return zonedTimeToUtc(fnZoned, tz) as Date;
|
||||
return fromZonedTime(fnZoned, tz) as Date;
|
||||
}
|
||||
return fnZoned;
|
||||
};
|
||||
|
@ -51,6 +51,6 @@ export const calcDateDifferenceProperty = (
|
|||
locale,
|
||||
config,
|
||||
locale.time_zone === TimeZone.server
|
||||
? utcToZonedTime(startDate, config.time_zone)
|
||||
? toZonedTime(startDate, config.time_zone)
|
||||
: startDate
|
||||
);
|
||||
|
|
|
@ -61,11 +61,8 @@ export const applyThemesOnElement = (
|
|||
const accentColor = themeSettings?.accentColor;
|
||||
|
||||
if (darkMode && primaryColor) {
|
||||
themeRules["app-header-background-color"] = hexBlend(
|
||||
primaryColor,
|
||||
"#121212",
|
||||
8
|
||||
);
|
||||
themeRules["app-theme-color"] = hexBlend(primaryColor, "#121212", 8);
|
||||
themeRules["app-header-background-color"] = themeRules["app-theme-color"];
|
||||
}
|
||||
|
||||
if (primaryColor) {
|
||||
|
|
|
@ -187,11 +187,14 @@ export const computeStateDisplayFromEntityAttributes = (
|
|||
if (
|
||||
[
|
||||
"button",
|
||||
"conversation",
|
||||
"event",
|
||||
"image",
|
||||
"input_button",
|
||||
"notify",
|
||||
"scene",
|
||||
"stt",
|
||||
"tag",
|
||||
"tts",
|
||||
"wake_word",
|
||||
].includes(domain) ||
|
||||
|
|
|
@ -28,7 +28,15 @@ export const FIXED_DOMAIN_STATES = {
|
|||
input_button: [],
|
||||
lawn_mower: ["error", "paused", "mowing", "docked"],
|
||||
light: ["on", "off"],
|
||||
lock: ["jammed", "locked", "locking", "unlocked", "unlocking"],
|
||||
lock: [
|
||||
"jammed",
|
||||
"locked",
|
||||
"locking",
|
||||
"unlocked",
|
||||
"unlocking",
|
||||
"opening",
|
||||
"open",
|
||||
],
|
||||
media_player: [
|
||||
"off",
|
||||
"on",
|
||||
|
|
|
@ -2,6 +2,7 @@ import IntlMessageFormat from "intl-messageformat";
|
|||
import type { HTMLTemplateResult } from "lit";
|
||||
import { polyfillLocaleData } from "../../resources/locale-data-polyfill";
|
||||
import { Resources, TranslationDict } from "../../types";
|
||||
import { fireEvent } from "../dom/fire_event";
|
||||
|
||||
// Exclude some patterns from key type checking for now
|
||||
// These are intended to be removed as errors are fixed
|
||||
|
@ -81,7 +82,9 @@ export interface FormatsType {
|
|||
*/
|
||||
|
||||
export const computeLocalize = async <Keys extends string = LocalizeKeys>(
|
||||
cache: any,
|
||||
cache: HTMLElement & {
|
||||
_localizationCache?: Record<string, IntlMessageFormat>;
|
||||
},
|
||||
language: string,
|
||||
resources: Resources,
|
||||
formats?: FormatsType
|
||||
|
@ -107,7 +110,7 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>(
|
|||
}
|
||||
|
||||
const messageKey = key + translatedValue;
|
||||
let translatedMessage = cache._localizationCache[messageKey] as
|
||||
let translatedMessage = cache._localizationCache![messageKey] as
|
||||
| IntlMessageFormat
|
||||
| undefined;
|
||||
|
||||
|
@ -121,7 +124,7 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>(
|
|||
} catch (err: any) {
|
||||
return "Translation error: " + err.message;
|
||||
}
|
||||
cache._localizationCache[messageKey] = translatedMessage;
|
||||
cache._localizationCache![messageKey] = translatedMessage;
|
||||
}
|
||||
|
||||
let argObject = {};
|
||||
|
@ -137,6 +140,12 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>(
|
|||
try {
|
||||
return translatedMessage.format<string>(argObject) as string;
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Translation error", key, language, err);
|
||||
fireEvent(cache, "write_log", {
|
||||
level: "error",
|
||||
message: `Failed to format translation for key '${key}' in language '${language}'. ${err}`,
|
||||
});
|
||||
return "Translation " + err;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
export const hasRejectedItems = <T = any>(results: PromiseSettledResult<T>[]) =>
|
||||
results.some((result) => result.status === "rejected");
|
||||
|
||||
export const rejectedItems = <T = any>(
|
||||
results: PromiseSettledResult<T>[]
|
||||
): PromiseRejectedResult[] =>
|
||||
results.filter(
|
||||
(result) => result.status === "rejected"
|
||||
) as PromiseRejectedResult[];
|
|
@ -1,4 +1,4 @@
|
|||
import { differenceInDays, differenceInWeeks, startOfWeek } from "date-fns/esm";
|
||||
import { differenceInDays, differenceInWeeks, startOfWeek } from "date-fns";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import { firstWeekdayIndex } from "../datetime/first_weekday";
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ import {
|
|||
endOfMonth,
|
||||
endOfQuarter,
|
||||
endOfYear,
|
||||
} from "date-fns/esm";
|
||||
} from "date-fns";
|
||||
import {
|
||||
formatDate,
|
||||
formatDateMonth,
|
||||
|
|
|
@ -20,6 +20,7 @@ export class HaInputChip extends MdInputChip {
|
|||
0.15
|
||||
);
|
||||
--ha-input-chip-selected-container-opacity: 1;
|
||||
--md-input-chip-label-text-font: Roboto, sans-serif;
|
||||
}
|
||||
/** Set the size of mdc icons **/
|
||||
::slotted([slot="icon"]) {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { mdiArrowDown, mdiArrowUp } from "@mdi/js";
|
||||
import { mdiArrowDown, mdiArrowUp, mdiChevronUp } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import {
|
||||
customElement,
|
||||
|
@ -22,7 +22,9 @@ import { styleMap } from "lit/directives/style-map";
|
|||
import memoizeOne from "memoize-one";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { groupBy } from "../../common/util/group-by";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
|
@ -32,17 +34,6 @@ import type { HaCheckbox } from "../ha-checkbox";
|
|||
import "../ha-svg-icon";
|
||||
import "../search-input";
|
||||
import { filterData, sortData } from "./sort-filter";
|
||||
import { groupBy } from "../../common/util/group-by";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"selection-changed": SelectionChangedEvent;
|
||||
"row-click": RowClickedEvent;
|
||||
"sorting-changed": SortingChangedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export interface RowClickedEvent {
|
||||
id: string;
|
||||
|
@ -52,6 +43,10 @@ export interface SelectionChangedEvent {
|
|||
value: string[];
|
||||
}
|
||||
|
||||
export interface CollapsedChangedEvent {
|
||||
value: string[];
|
||||
}
|
||||
|
||||
export interface SortingChangedEvent {
|
||||
column: string;
|
||||
direction: SortingDirection;
|
||||
|
@ -142,10 +137,14 @@ export class HaDataTable extends LitElement {
|
|||
|
||||
@property() public groupColumn?: string;
|
||||
|
||||
@property({ attribute: false }) public groupOrder?: string[];
|
||||
|
||||
@property() public sortColumn?: string;
|
||||
|
||||
@property() public sortDirection: SortingDirection = null;
|
||||
|
||||
@property({ attribute: false }) public initialCollapsedGroups?: string[];
|
||||
|
||||
@state() private _filterable = false;
|
||||
|
||||
@state() private _filter = "";
|
||||
|
@ -158,6 +157,8 @@ export class HaDataTable extends LitElement {
|
|||
|
||||
@state() private _items: DataTableRowData[] = [];
|
||||
|
||||
@state() private _collapsedGroups: string[] = [];
|
||||
|
||||
private _checkableRowsCount?: number;
|
||||
|
||||
private _checkedRows: string[] = [];
|
||||
|
@ -213,17 +214,19 @@ export class HaDataTable extends LitElement {
|
|||
(column) => column.filterable
|
||||
);
|
||||
|
||||
for (const columnId in this.columns) {
|
||||
if (this.columns[columnId].direction) {
|
||||
this.sortDirection = this.columns[columnId].direction!;
|
||||
this.sortColumn = columnId;
|
||||
if (!this.sortColumn) {
|
||||
for (const columnId in this.columns) {
|
||||
if (this.columns[columnId].direction) {
|
||||
this.sortDirection = this.columns[columnId].direction!;
|
||||
this.sortColumn = columnId;
|
||||
|
||||
fireEvent(this, "sorting-changed", {
|
||||
column: columnId,
|
||||
direction: this.sortDirection,
|
||||
});
|
||||
fireEvent(this, "sorting-changed", {
|
||||
column: columnId,
|
||||
direction: this.sortDirection,
|
||||
});
|
||||
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -248,13 +251,23 @@ export class HaDataTable extends LitElement {
|
|||
).length;
|
||||
}
|
||||
|
||||
if (!this.hasUpdated && this.initialCollapsedGroups) {
|
||||
this._collapsedGroups = this.initialCollapsedGroups;
|
||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
||||
} else if (properties.has("groupColumn")) {
|
||||
this._collapsedGroups = [];
|
||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
||||
}
|
||||
|
||||
if (
|
||||
properties.has("data") ||
|
||||
properties.has("columns") ||
|
||||
properties.has("_filter") ||
|
||||
properties.has("sortColumn") ||
|
||||
properties.has("sortDirection") ||
|
||||
properties.has("groupColumn")
|
||||
properties.has("groupColumn") ||
|
||||
properties.has("groupOrder") ||
|
||||
properties.has("_collapsedGroups")
|
||||
) {
|
||||
this._sortFilterData();
|
||||
}
|
||||
|
@ -447,6 +460,8 @@ export class HaDataTable extends LitElement {
|
|||
}
|
||||
return html`
|
||||
<div
|
||||
@mouseover=${this._setTitle}
|
||||
@focus=${this._setTitle}
|
||||
role=${column.main ? "rowheader" : "cell"}
|
||||
class="mdc-data-table__cell ${classMap({
|
||||
"mdc-data-table__cell--flex": column.type === "flex",
|
||||
|
@ -514,11 +529,7 @@ export class HaDataTable extends LitElement {
|
|||
}
|
||||
|
||||
if (this.appendRow || this.hasFab || this.groupColumn) {
|
||||
const items = [...data];
|
||||
|
||||
if (this.appendRow) {
|
||||
items.push({ append: true, content: this.appendRow });
|
||||
}
|
||||
let items = [...data];
|
||||
|
||||
if (this.groupColumn) {
|
||||
const grouped = groupBy(items, (item) => item[this.groupColumn!]);
|
||||
|
@ -530,45 +541,66 @@ export class HaDataTable extends LitElement {
|
|||
const sorted: {
|
||||
[key: string]: DataTableRowData[];
|
||||
} = Object.keys(grouped)
|
||||
.sort((a, b) =>
|
||||
stringCompare(
|
||||
.sort((a, b) => {
|
||||
const orderA = this.groupOrder?.indexOf(a) ?? -1;
|
||||
const orderB = this.groupOrder?.indexOf(b) ?? -1;
|
||||
if (orderA !== orderB) {
|
||||
if (orderA === -1) {
|
||||
return 1;
|
||||
}
|
||||
if (orderB === -1) {
|
||||
return -1;
|
||||
}
|
||||
return orderA - orderB;
|
||||
}
|
||||
return stringCompare(
|
||||
["", "-", "—"].includes(a) ? "zzz" : a,
|
||||
["", "-", "—"].includes(b) ? "zzz" : b,
|
||||
this.hass.locale.language
|
||||
)
|
||||
)
|
||||
);
|
||||
})
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = grouped[key];
|
||||
return obj;
|
||||
}, {});
|
||||
const groupedItems: DataTableRowData[] = [];
|
||||
Object.entries(sorted).forEach(([groupName, rows]) => {
|
||||
if (
|
||||
groupName !== UNDEFINED_GROUP_KEY ||
|
||||
Object.keys(sorted).length > 1
|
||||
) {
|
||||
groupedItems.push({
|
||||
append: true,
|
||||
content: html`<div
|
||||
class="mdc-data-table__cell group-header"
|
||||
role="cell"
|
||||
groupedItems.push({
|
||||
append: true,
|
||||
content: html`<div
|
||||
class="mdc-data-table__cell group-header"
|
||||
role="cell"
|
||||
.group=${groupName}
|
||||
@click=${this._collapseGroup}
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiChevronUp}
|
||||
class=${this._collapsedGroups.includes(groupName)
|
||||
? "collapsed"
|
||||
: ""}
|
||||
>
|
||||
${groupName === UNDEFINED_GROUP_KEY ? "" : groupName || ""}
|
||||
</div>`,
|
||||
});
|
||||
</ha-icon-button>
|
||||
${groupName === UNDEFINED_GROUP_KEY
|
||||
? this.hass.localize("ui.components.data-table.ungrouped")
|
||||
: groupName || ""}
|
||||
</div>`,
|
||||
});
|
||||
if (!this._collapsedGroups.includes(groupName)) {
|
||||
groupedItems.push(...rows);
|
||||
}
|
||||
|
||||
groupedItems.push(...rows);
|
||||
});
|
||||
items = groupedItems;
|
||||
}
|
||||
|
||||
this._items = groupedItems;
|
||||
} else {
|
||||
this._items = items;
|
||||
if (this.appendRow) {
|
||||
items.push({ append: true, content: this.appendRow });
|
||||
}
|
||||
|
||||
if (this.hasFab) {
|
||||
this._items = [...this._items, { empty: true }];
|
||||
items.push({ empty: true });
|
||||
}
|
||||
|
||||
this._items = items;
|
||||
} else {
|
||||
this._items = data;
|
||||
}
|
||||
|
@ -649,6 +681,13 @@ export class HaDataTable extends LitElement {
|
|||
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
|
||||
};
|
||||
|
||||
private _setTitle(ev: Event) {
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
if (target.scrollWidth > target.offsetWidth) {
|
||||
target.setAttribute("title", target.innerText);
|
||||
}
|
||||
}
|
||||
|
||||
private _checkedRowsChanged() {
|
||||
// force scroller to update, change it's items
|
||||
if (this._items.length) {
|
||||
|
@ -679,6 +718,18 @@ export class HaDataTable extends LitElement {
|
|||
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
|
||||
}
|
||||
|
||||
private _collapseGroup = (ev: Event) => {
|
||||
const groupName = (ev.currentTarget as any).group;
|
||||
if (this._collapsedGroups.includes(groupName)) {
|
||||
this._collapsedGroups = this._collapsedGroups.filter(
|
||||
(grp) => grp !== groupName
|
||||
);
|
||||
} else {
|
||||
this._collapsedGroups = [...this._collapsedGroups, groupName];
|
||||
}
|
||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
||||
};
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
|
@ -931,8 +982,22 @@ export class HaDataTable extends LitElement {
|
|||
|
||||
.group-header {
|
||||
padding-top: 12px;
|
||||
padding-left: 12px;
|
||||
padding-inline-start: 12px;
|
||||
padding-inline-end: initial;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.group-header ha-icon-button {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.group-header ha-icon-button.collapsed {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
:host {
|
||||
|
@ -1031,4 +1096,12 @@ declare global {
|
|||
interface HTMLElementTagNameMap {
|
||||
"ha-data-table": HaDataTable;
|
||||
}
|
||||
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"selection-changed": SelectionChangedEvent;
|
||||
"row-click": RowClickedEvent;
|
||||
"sorting-changed": SortingChangedEvent;
|
||||
"collapsed-changed": CollapsedChangedEvent;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,6 +76,8 @@ class HaEntitiesPickerLight extends LitElement {
|
|||
@property({ attribute: false })
|
||||
public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
|
||||
@property({ type: Array }) public createDomains?: string[];
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
|
@ -103,6 +105,7 @@ class HaEntitiesPickerLight extends LitElement {
|
|||
.value=${entityId}
|
||||
.label=${this.pickedEntityLabel}
|
||||
.disabled=${this.disabled}
|
||||
.createDomains=${this.createDomains}
|
||||
@value-changed=${this._entityChanged}
|
||||
></ha-entity-picker>
|
||||
</div>
|
||||
|
@ -122,6 +125,7 @@ class HaEntitiesPickerLight extends LitElement {
|
|||
.label=${this.pickEntityLabel}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.createDomains=${this.createDomains}
|
||||
.required=${this.required && !currentEntities.length}
|
||||
@value-changed=${this._addEntity}
|
||||
></ha-entity-picker>
|
||||
|
|
|
@ -18,6 +18,12 @@ import "../ha-icon-button";
|
|||
import "../ha-svg-icon";
|
||||
import "./state-badge";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import {
|
||||
isHelperDomain,
|
||||
HelperDomain,
|
||||
} from "../../panels/config/helpers/const";
|
||||
|
||||
interface HassEntityWithCachedName extends HassEntity, ScorableTextItem {
|
||||
friendly_name: string;
|
||||
|
@ -25,6 +31,8 @@ interface HassEntityWithCachedName extends HassEntity, ScorableTextItem {
|
|||
|
||||
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
||||
|
||||
const CREATE_ID = "___create-new-entity___";
|
||||
|
||||
@customElement("ha-entity-picker")
|
||||
export class HaEntityPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
@ -44,6 +52,8 @@ export class HaEntityPicker extends LitElement {
|
|||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Array }) public createDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show entities from specific domains.
|
||||
* @type {Array}
|
||||
|
@ -130,7 +140,11 @@ export class HaEntityPicker extends LitElement {
|
|||
></state-badge>`
|
||||
: ""}
|
||||
<span>${item.friendly_name}</span>
|
||||
<span slot="secondary">${item.entity_id}</span>
|
||||
<span slot="secondary"
|
||||
>${item.entity_id.startsWith(CREATE_ID)
|
||||
? this.hass.localize("ui.components.entity.entity-picker.new_entity")
|
||||
: item.entity_id}</span
|
||||
>
|
||||
</ha-list-item>`;
|
||||
|
||||
private _getStates = memoizeOne(
|
||||
|
@ -143,7 +157,8 @@ export class HaEntityPicker extends LitElement {
|
|||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
|
||||
includeEntities: this["includeEntities"],
|
||||
excludeEntities: this["excludeEntities"]
|
||||
excludeEntities: this["excludeEntities"],
|
||||
createDomains: this["createDomains"]
|
||||
): HassEntityWithCachedName[] => {
|
||||
let states: HassEntityWithCachedName[] = [];
|
||||
|
||||
|
@ -152,6 +167,34 @@ export class HaEntityPicker extends LitElement {
|
|||
}
|
||||
let entityIds = Object.keys(hass.states);
|
||||
|
||||
const createItems = createDomains?.length
|
||||
? createDomains.map((domain) => {
|
||||
const newFriendlyName = hass.localize(
|
||||
"ui.components.entity.entity-picker.create_helper",
|
||||
{
|
||||
domain: isHelperDomain(domain)
|
||||
? hass.localize(
|
||||
`ui.panel.config.helpers.types.${domain as HelperDomain}`
|
||||
)
|
||||
: domainToName(hass.localize, domain),
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
entity_id: CREATE_ID + domain,
|
||||
state: "on",
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
context: { id: "", user_id: null, parent_id: null },
|
||||
friendly_name: newFriendlyName,
|
||||
attributes: {
|
||||
icon: "mdi:plus",
|
||||
},
|
||||
strings: [domain, newFriendlyName],
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
if (!entityIds.length) {
|
||||
return [
|
||||
{
|
||||
|
@ -171,6 +214,7 @@ export class HaEntityPicker extends LitElement {
|
|||
},
|
||||
strings: [],
|
||||
},
|
||||
...createItems,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -281,9 +325,14 @@ export class HaEntityPicker extends LitElement {
|
|||
},
|
||||
strings: [],
|
||||
},
|
||||
...createItems,
|
||||
];
|
||||
}
|
||||
|
||||
if (createItems?.length) {
|
||||
states.push(...createItems);
|
||||
}
|
||||
|
||||
return states;
|
||||
}
|
||||
);
|
||||
|
@ -310,13 +359,18 @@ export class HaEntityPicker extends LitElement {
|
|||
this.includeDeviceClasses,
|
||||
this.includeUnitOfMeasurement,
|
||||
this.includeEntities,
|
||||
this.excludeEntities
|
||||
this.excludeEntities,
|
||||
this.createDomains
|
||||
);
|
||||
if (this._initedStates) {
|
||||
this.comboBox.filteredItems = this._states;
|
||||
}
|
||||
this._initedStates = true;
|
||||
}
|
||||
|
||||
if (changedProps.has("createDomains") && this.createDomains?.length) {
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
@ -354,6 +408,18 @@ export class HaEntityPicker extends LitElement {
|
|||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (newValue && newValue.startsWith(CREATE_ID)) {
|
||||
const domain = newValue.substring(CREATE_ID.length);
|
||||
showHelperDetailDialog(this, {
|
||||
domain,
|
||||
dialogClosedCallback: (item) => {
|
||||
if (item.entityId) this._setValue(item.entityId);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue !== this._value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ export class HaCard extends LitElement {
|
|||
--ha-card-background,
|
||||
var(--card-background-color, white)
|
||||
);
|
||||
-webkit-backdrop-filter: var(--ha-card-backdrop-filter, none);
|
||||
backdrop-filter: var(--ha-card-backdrop-filter, none);
|
||||
box-shadow: var(--ha-card-box-shadow, none);
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import "element-internals-polyfill";
|
||||
import { MdCircularProgress } from "@material/web/progress/circular-progress";
|
||||
import { CSSResult, PropertyValues, css } from "lit";
|
||||
import { PropertyValues, css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-circular-progress")
|
||||
|
@ -32,17 +32,15 @@ export class HaCircularProgress extends MdCircularProgress {
|
|||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-primary: var(--primary-color);
|
||||
--md-circular-progress-size: 48px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-primary: var(--primary-color);
|
||||
--md-circular-progress-size: 48px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
import { Ripple } from "@material/mwc-ripple";
|
||||
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
queryAsync,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import "./ha-ripple";
|
||||
|
||||
@customElement("ha-control-button")
|
||||
export class HaControlButton extends LitElement {
|
||||
|
@ -16,10 +9,6 @@ export class HaControlButton extends LitElement {
|
|||
|
||||
@property() public label?: string;
|
||||
|
||||
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
|
||||
|
||||
@state() private _shouldRenderRipple = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<button
|
||||
|
@ -28,54 +17,13 @@ export class HaControlButton extends LitElement {
|
|||
aria-label=${ifDefined(this.label)}
|
||||
title=${ifDefined(this.label)}
|
||||
.disabled=${Boolean(this.disabled)}
|
||||
@focus=${this.handleRippleFocus}
|
||||
@blur=${this.handleRippleBlur}
|
||||
@mousedown=${this.handleRippleActivate}
|
||||
@mouseup=${this.handleRippleDeactivate}
|
||||
@mouseenter=${this.handleRippleMouseEnter}
|
||||
@mouseleave=${this.handleRippleMouseLeave}
|
||||
@touchstart=${this.handleRippleActivate}
|
||||
@touchend=${this.handleRippleDeactivate}
|
||||
@touchcancel=${this.handleRippleDeactivate}
|
||||
>
|
||||
<slot></slot>
|
||||
${this._shouldRenderRipple && !this.disabled
|
||||
? html`<mwc-ripple></mwc-ripple>`
|
||||
: ""}
|
||||
<ha-ripple .disabled=${this.disabled}></ha-ripple>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
|
||||
this._shouldRenderRipple = true;
|
||||
return this._ripple;
|
||||
});
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private handleRippleActivate(evt?: Event) {
|
||||
this._rippleHandlers.startPress(evt);
|
||||
}
|
||||
|
||||
private handleRippleDeactivate() {
|
||||
this._rippleHandlers.endPress();
|
||||
}
|
||||
|
||||
private handleRippleMouseEnter() {
|
||||
this._rippleHandlers.startHover();
|
||||
}
|
||||
|
||||
private handleRippleMouseLeave() {
|
||||
this._rippleHandlers.endHover();
|
||||
}
|
||||
|
||||
private handleRippleFocus() {
|
||||
this._rippleHandlers.startFocus();
|
||||
}
|
||||
|
||||
private handleRippleBlur() {
|
||||
this._rippleHandlers.endFocus();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
|
@ -86,6 +34,7 @@ export class HaControlButton extends LitElement {
|
|||
--control-button-border-radius: 10px;
|
||||
--control-button-padding: 8px;
|
||||
--mdc-icon-size: 20px;
|
||||
--ha-ripple-color: var(--secondary-text-color);
|
||||
color: var(--primary-text-color);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
@ -113,12 +62,14 @@ export class HaControlButton extends LitElement {
|
|||
outline: none;
|
||||
overflow: hidden;
|
||||
background: none;
|
||||
--mdc-ripple-color: var(--control-button-background-color);
|
||||
/* For safari border-radius overflow */
|
||||
z-index: 0;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
.button:focus-visible {
|
||||
--control-button-background-opacity: 0.4;
|
||||
}
|
||||
.button::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
|
|
@ -1,22 +1,14 @@
|
|||
import { Ripple } from "@material/mwc-ripple";
|
||||
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
|
||||
import { SelectBase } from "@material/mwc-select/mwc-select-base";
|
||||
import { mdiMenuDown } from "@mdi/js";
|
||||
import { css, html, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
query,
|
||||
queryAsync,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
import "./ha-icon";
|
||||
import type { HaIcon } from "./ha-icon";
|
||||
import "./ha-ripple";
|
||||
import "./ha-svg-icon";
|
||||
import type { HaSvgIcon } from "./ha-svg-icon";
|
||||
|
||||
|
@ -32,10 +24,6 @@ export class HaControlSelectMenu extends SelectBase {
|
|||
@property({ type: Boolean, attribute: "hide-label" })
|
||||
public hideLabel = false;
|
||||
|
||||
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
|
||||
|
||||
@state() private _shouldRenderRipple = false;
|
||||
|
||||
public override render() {
|
||||
const classes = {
|
||||
"select-disabled": this.disabled,
|
||||
|
@ -69,17 +57,10 @@ export class HaControlSelectMenu extends SelectBase {
|
|||
aria-labelledby=${ifDefined(labelledby)}
|
||||
aria-label=${ifDefined(labelAttribute)}
|
||||
aria-required=${this.required}
|
||||
@click=${this.onClick}
|
||||
@focus=${this.onFocus}
|
||||
@blur=${this.onBlur}
|
||||
@click=${this.onClick}
|
||||
@keydown=${this.onKeydown}
|
||||
@mousedown=${this.handleRippleActivate}
|
||||
@mouseup=${this.handleRippleDeactivate}
|
||||
@mouseenter=${this.handleRippleMouseEnter}
|
||||
@mouseleave=${this.handleRippleMouseLeave}
|
||||
@touchstart=${this.handleRippleActivate}
|
||||
@touchend=${this.handleRippleDeactivate}
|
||||
@touchcancel=${this.handleRippleDeactivate}
|
||||
>
|
||||
${this.renderIcon()}
|
||||
<div class="content">
|
||||
|
@ -91,9 +72,7 @@ export class HaControlSelectMenu extends SelectBase {
|
|||
: nothing}
|
||||
</div>
|
||||
${this.renderArrow()}
|
||||
${this._shouldRenderRipple && !this.disabled
|
||||
? html` <mwc-ripple></mwc-ripple> `
|
||||
: nothing}
|
||||
<ha-ripple .disabled=${this.disabled}></ha-ripple>
|
||||
</div>
|
||||
${this.renderMenu()}
|
||||
</div>
|
||||
|
@ -135,46 +114,6 @@ export class HaControlSelectMenu extends SelectBase {
|
|||
`;
|
||||
}
|
||||
|
||||
protected onFocus() {
|
||||
this.handleRippleFocus();
|
||||
super.onFocus();
|
||||
}
|
||||
|
||||
protected onBlur() {
|
||||
this.handleRippleBlur();
|
||||
super.onBlur();
|
||||
}
|
||||
|
||||
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
|
||||
this._shouldRenderRipple = true;
|
||||
return this._ripple;
|
||||
});
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private handleRippleActivate(evt?: Event) {
|
||||
this._rippleHandlers.startPress(evt);
|
||||
}
|
||||
|
||||
private handleRippleDeactivate() {
|
||||
this._rippleHandlers.endPress();
|
||||
}
|
||||
|
||||
private handleRippleMouseEnter() {
|
||||
this._rippleHandlers.startHover();
|
||||
}
|
||||
|
||||
private handleRippleMouseLeave() {
|
||||
this._rippleHandlers.endHover();
|
||||
}
|
||||
|
||||
private handleRippleFocus() {
|
||||
this._rippleHandlers.startFocus();
|
||||
}
|
||||
|
||||
private handleRippleBlur() {
|
||||
this._rippleHandlers.endFocus();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("translations-updated", this._translationsUpdated);
|
||||
|
@ -204,6 +143,7 @@ export class HaControlSelectMenu extends SelectBase {
|
|||
--control-select-menu-height: 48px;
|
||||
--control-select-menu-padding: 6px 10px;
|
||||
--mdc-icon-size: 20px;
|
||||
--ha-ripple-color: var(--secondary-text-color);
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
width: auto;
|
||||
|
@ -224,7 +164,6 @@ export class HaControlSelectMenu extends SelectBase {
|
|||
outline: none;
|
||||
overflow: hidden;
|
||||
background: none;
|
||||
--mdc-ripple-color: var(--control-select-menu-background-color);
|
||||
/* For safari border-radius overflow */
|
||||
z-index: 0;
|
||||
transition: color 180ms ease-in-out;
|
||||
|
@ -264,6 +203,10 @@ export class HaControlSelectMenu extends SelectBase {
|
|||
letter-spacing: inherit;
|
||||
}
|
||||
|
||||
.select-anchor:focus-visible {
|
||||
--control-select-menu-background-opacity: 0.4;
|
||||
}
|
||||
|
||||
.select-anchor::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
|
|
@ -67,6 +67,9 @@ export class HaControlSlider extends LitElement {
|
|||
@property({ attribute: "tooltip-mode" })
|
||||
public tooltipMode: TooltipMode = "interaction";
|
||||
|
||||
@property({ attribute: "touch-action" })
|
||||
public touchAction?: string;
|
||||
|
||||
@property({ type: Number })
|
||||
public value?: number;
|
||||
|
||||
|
@ -152,7 +155,7 @@ export class HaControlSlider extends LitElement {
|
|||
setupListeners() {
|
||||
if (this.slider && !this._mc) {
|
||||
this._mc = new Manager(this.slider, {
|
||||
touchAction: this.vertical ? "pan-x" : "pan-y",
|
||||
touchAction: this.touchAction ?? (this.vertical ? "pan-x" : "pan-y"),
|
||||
});
|
||||
this._mc.add(
|
||||
new Pan({
|
||||
|
|
|
@ -33,6 +33,9 @@ export class HaControlSwitch extends LitElement {
|
|||
// SVG icon path (if you need a non SVG icon instead, use the provided off icon slot to pass an <ha-icon slot="icon-off"> in)
|
||||
@property({ type: String }) pathOff?: string;
|
||||
|
||||
@property({ attribute: "touch-action" })
|
||||
public touchAction?: string;
|
||||
|
||||
private _mc?: HammerManager;
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
|
@ -73,7 +76,7 @@ export class HaControlSwitch extends LitElement {
|
|||
setupListeners() {
|
||||
if (this.switch && !this._mc) {
|
||||
this._mc = new Manager(this.switch, {
|
||||
touchAction: this.vertical ? "pan-x" : "pan-y",
|
||||
touchAction: this.touchAction ?? (this.vertical ? "pan-x" : "pan-y"),
|
||||
});
|
||||
this._mc.add(
|
||||
new Swipe({
|
||||
|
|
|
@ -19,6 +19,7 @@ import { HomeAssistant } from "../types";
|
|||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
import { getExtendedEntityRegistryEntry } from "../data/entity_registry";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
|
@ -107,13 +108,23 @@ export class HaConversationAgentPicker extends LitElement {
|
|||
}
|
||||
|
||||
private async _maybeFetchConfigEntry() {
|
||||
if (!this.value || this.value === "homeassistant") {
|
||||
if (!this.value || !(this.value in this.hass.entities)) {
|
||||
this._configEntry = undefined;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const regEntry = await getExtendedEntityRegistryEntry(
|
||||
this.hass,
|
||||
this.value
|
||||
);
|
||||
|
||||
if (!regEntry.config_entry_id) {
|
||||
this._configEntry = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
this._configEntry = (
|
||||
await getConfigEntry(this.hass, this.value)
|
||||
await getConfigEntry(this.hass, regEntry.config_entry_id)
|
||||
).config_entry;
|
||||
} catch (err) {
|
||||
this._configEntry = undefined;
|
||||
|
|
|
@ -75,8 +75,14 @@ export class HaDialog extends DialogBase {
|
|||
var(--divider-color)
|
||||
);
|
||||
z-index: var(--dialog-z-index, 8);
|
||||
-webkit-backdrop-filter: var(--dialog-backdrop-filter, none);
|
||||
backdrop-filter: var(--dialog-backdrop-filter, none);
|
||||
-webkit-backdrop-filter: var(
|
||||
--ha-dialog-scrim-backdrop-filter,
|
||||
var(--dialog-backdrop-filter, none)
|
||||
);
|
||||
backdrop-filter: var(
|
||||
--ha-dialog-scrim-backdrop-filter,
|
||||
var(--dialog-backdrop-filter, none)
|
||||
);
|
||||
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
|
||||
--mdc-typography-headline6-font-weight: 400;
|
||||
--mdc-typography-headline6-font-size: 1.574rem;
|
||||
|
@ -119,6 +125,12 @@ export class HaDialog extends DialogBase {
|
|||
margin-top: var(--dialog-surface-margin-top);
|
||||
min-height: var(--mdc-dialog-min-height, auto);
|
||||
border-radius: var(--ha-dialog-border-radius, 28px);
|
||||
-webkit-backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
|
||||
backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
|
||||
background: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
}
|
||||
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
||||
display: flex;
|
||||
|
|
|
@ -69,7 +69,7 @@ export class HaFilterDevices extends LitElement {
|
|||
@value-changed=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
<mwc-list class="ha-scrollbar">
|
||||
<mwc-list class="ha-scrollbar" multi>
|
||||
<lit-virtualizer
|
||||
.items=${this._devices(
|
||||
this.hass.devices,
|
||||
|
@ -94,7 +94,7 @@ export class HaFilterDevices extends LitElement {
|
|||
? nothing
|
||||
: html`<ha-check-list-item
|
||||
.value=${device.id}
|
||||
.selected=${this.value?.includes(device.id)}
|
||||
.selected=${this.value?.includes(device.id) ?? false}
|
||||
>
|
||||
${computeDeviceName(device, this.hass)}
|
||||
</ha-check-list-item>`;
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { domainToName } from "../data/integration";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-domain-icon";
|
||||
import "./search-input-outlined";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
|
||||
@customElement("ha-filter-domains")
|
||||
export class HaFilterDomains extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
leftChevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.headers.domain"
|
||||
)}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
.path=${mdiFilterVariantRemove}
|
||||
@click=${this._clearFilter}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`<search-input-outlined
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
<mwc-list
|
||||
class="ha-scrollbar"
|
||||
@click=${this._handleItemClick}
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._domains(this.hass.states, this._filter),
|
||||
(i) => i,
|
||||
(domain) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${domain}
|
||||
.selected=${(this.value || []).includes(domain)}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-domain-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
brandFallback
|
||||
></ha-domain-icon>
|
||||
${domainToName(this.hass.localize, domain)}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</mwc-list> `
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private _domains = memoizeOne((states, filter) => {
|
||||
const domains = new Set<string>();
|
||||
Object.keys(states).forEach((entityId) => {
|
||||
domains.add(computeDomain(entityId));
|
||||
});
|
||||
|
||||
return Array.from(domains.values())
|
||||
.filter(
|
||||
(entry) =>
|
||||
!filter ||
|
||||
entry.toLowerCase().includes(filter) ||
|
||||
domainToName(this.hass.localize, entry).toLowerCase().includes(filter)
|
||||
)
|
||||
.sort((a, b) => stringCompare(a, b, this.hass.locale.language));
|
||||
});
|
||||
|
||||
protected updated(changed) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _handleItemClick(ev) {
|
||||
const listItem = ev.target.closest("ha-check-list-item");
|
||||
const value = listItem?.value;
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
if (this.value?.includes(value)) {
|
||||
this.value = this.value?.filter((val) => val !== value);
|
||||
} else {
|
||||
this.value = [...(this.value || []), value];
|
||||
}
|
||||
|
||||
listItem.selected = this.value.includes(value);
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: this.value,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _clearFilter(ev) {
|
||||
ev.preventDefault();
|
||||
this.value = undefined;
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: undefined,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value.toLowerCase();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: 0;
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header ha-icon-button {
|
||||
margin-inline-start: initial;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
background-color: var(--primary-color);
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
search-input-outlined {
|
||||
display: block;
|
||||
padding: 0 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-filter-domains": HaFilterDomains;
|
||||
}
|
||||
}
|
|
@ -71,7 +71,7 @@ export class HaFilterEntities extends LitElement {
|
|||
@value-changed=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
<mwc-list class="ha-scrollbar">
|
||||
<mwc-list class="ha-scrollbar" multi>
|
||||
<lit-virtualizer
|
||||
.items=${this._entities(
|
||||
this.hass.states,
|
||||
|
@ -108,7 +108,7 @@ export class HaFilterEntities extends LitElement {
|
|||
? nothing
|
||||
: html`<ha-check-list-item
|
||||
.value=${entity.entity_id}
|
||||
.selected=${this.value?.includes(entity.entity_id)}
|
||||
.selected=${this.value?.includes(entity.entity_id) ?? false}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
|
|
|
@ -55,7 +55,11 @@ export class HaFilterIntegrations extends LitElement {
|
|||
@value-changed=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
<mwc-list class="ha-scrollbar" @click=${this._handleItemClick}>
|
||||
<mwc-list
|
||||
class="ha-scrollbar"
|
||||
@click=${this._handleItemClick}
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._integrations(this._manifests, this._filter, this.value),
|
||||
(i) => i.domain,
|
||||
|
|
|
@ -62,8 +62,8 @@ export class HaFilterStates extends LitElement {
|
|||
(item) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${item.value}
|
||||
.selected=${this.value?.includes(item.value)}
|
||||
.graphic=${hasIcon ? "icon" : undefined}
|
||||
.selected=${this.value?.includes(item.value) ?? false}
|
||||
.graphic=${hasIcon ? "icon" : null}
|
||||
>
|
||||
${item.icon
|
||||
? html`<ha-icon
|
||||
|
|
|
@ -71,6 +71,10 @@ export const computeInitialHaFormData = (
|
|||
if (selector.country?.countries?.length) {
|
||||
data[field.name] = selector.country.countries[0];
|
||||
}
|
||||
} else if ("language" in selector) {
|
||||
if (selector.language?.languages?.length) {
|
||||
data[field.name] = selector.language.languages[0];
|
||||
}
|
||||
} else if ("duration" in selector) {
|
||||
data[field.name] = {
|
||||
hours: 0,
|
||||
|
@ -93,7 +97,9 @@ export const computeInitialHaFormData = (
|
|||
) {
|
||||
data[field.name] = {};
|
||||
} else {
|
||||
throw new Error("Selector not supported in initial form data");
|
||||
throw new Error(
|
||||
`Selector ${Object.keys(selector)[0]} not supported in initial form data`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,13 +1,29 @@
|
|||
import { FormfieldBase } from "@material/mwc-formfield/mwc-formfield-base";
|
||||
import { styles } from "@material/mwc-formfield/mwc-formfield.css";
|
||||
import { css } from "lit";
|
||||
import { css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-formfield")
|
||||
export class HaFormfield extends FormfieldBase {
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
protected override render() {
|
||||
const classes = {
|
||||
"mdc-form-field--align-end": this.alignEnd,
|
||||
"mdc-form-field--space-between": this.spaceBetween,
|
||||
"mdc-form-field--nowrap": this.nowrap,
|
||||
};
|
||||
|
||||
return html` <div class="mdc-form-field ${classMap(classes)}">
|
||||
<slot></slot>
|
||||
<label class="mdc-label" @click=${this._labelClick}
|
||||
><slot name="label">${this.label}</slot></label
|
||||
>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected _labelClick() {
|
||||
const input = this.input as HTMLInputElement | undefined;
|
||||
if (!input) return;
|
||||
|
@ -39,6 +55,9 @@ export class HaFormfield extends FormfieldBase {
|
|||
margin-inline-end: 10px;
|
||||
margin-inline-start: inline;
|
||||
}
|
||||
.mdc-form-field {
|
||||
align-items: var(--ha-formfield-align-items, center);
|
||||
}
|
||||
.mdc-form-field > label {
|
||||
direction: var(--direction);
|
||||
margin-inline-start: 0;
|
||||
|
|
|
@ -302,6 +302,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
|||
name: this.hass.localize("ui.components.label-picker.no_match"),
|
||||
icon: null,
|
||||
color: null,
|
||||
description: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -315,6 +316,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
|||
name: this.hass.localize("ui.components.label-picker.add_new"),
|
||||
icon: "mdi:plus",
|
||||
color: null,
|
||||
description: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "@material/web/ripple/ripple";
|
||||
|
||||
@customElement("ha-label")
|
||||
class HaLabel extends LitElement {
|
||||
|
@ -11,7 +10,6 @@ class HaLabel extends LitElement {
|
|||
<span class="content">
|
||||
<slot name="icon"></slot>
|
||||
<slot></slot>
|
||||
<md-ripple></md-ripple>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
@ -27,7 +25,6 @@ class HaLabel extends LitElement {
|
|||
0.15
|
||||
);
|
||||
--ha-label-background-opacity: 1;
|
||||
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
|
|
|
@ -1,25 +1,23 @@
|
|||
import { customElement } from "lit/decorators";
|
||||
import "element-internals-polyfill";
|
||||
import { MdListItem } from "@material/web/list/list-item";
|
||||
import { CSSResult, css } from "lit";
|
||||
import { css } from "lit";
|
||||
|
||||
@customElement("ha-list-item-new")
|
||||
export class HaListItemNew extends MdListItem {
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
...MdListItem.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-secondary: var(--secondary-text-color);
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-secondary: var(--secondary-text-color);
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
import { customElement } from "lit/decorators";
|
||||
import "element-internals-polyfill";
|
||||
import { MdList } from "@material/web/list/list";
|
||||
import { CSSResult, css } from "lit";
|
||||
import { css } from "lit";
|
||||
|
||||
@customElement("ha-list-new")
|
||||
export class HaListNew extends MdList {
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
...MdList.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { MdMenuItem } from "@material/web/menu/menu-item";
|
||||
import "element-internals-polyfill";
|
||||
import { CSSResult, css } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-menu-item")
|
||||
export class HaMenuItem extends MdMenuItem {
|
||||
static override styles: CSSResult[] = [
|
||||
...MdMenuItem.styles,
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
|
@ -25,6 +25,7 @@ export class HaMenuItem extends MdMenuItem {
|
|||
|
||||
--md-sys-color-on-primary-container: var(--primary-text-color);
|
||||
--md-sys-color-on-secondary-container: var(--primary-text-color);
|
||||
--md-menu-item-label-text-font: Roboto, sans-serif;
|
||||
}
|
||||
:host(.warning) {
|
||||
--md-menu-item-label-text-color: var(--error-color);
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { customElement } from "lit/decorators";
|
||||
import "element-internals-polyfill";
|
||||
import { CSSResult, css } from "lit";
|
||||
import { css } from "lit";
|
||||
import { MdMenu } from "@material/web/menu/menu";
|
||||
|
||||
@customElement("ha-menu")
|
||||
export class HaMenu extends MdMenu {
|
||||
static override styles: CSSResult[] = [
|
||||
...MdMenu.styles,
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-surface-container: var(--card-background-color);
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import { AttachableController } from "@material/web/internal/controller/attachable-controller";
|
||||
import { MdRipple } from "@material/web/ripple/ripple";
|
||||
import "element-internals-polyfill";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-ripple")
|
||||
export class HaRipple extends MdRipple {
|
||||
private readonly attachableTouchController = new AttachableController(
|
||||
this,
|
||||
this.onTouchControlChange.bind(this)
|
||||
);
|
||||
|
||||
attach(control: HTMLElement) {
|
||||
super.attach(control);
|
||||
this.attachableTouchController.attach(control);
|
||||
}
|
||||
|
||||
detach() {
|
||||
super.detach();
|
||||
this.attachableTouchController.detach();
|
||||
}
|
||||
|
||||
private _handleTouchEnd = () => {
|
||||
if (!this.disabled) {
|
||||
// @ts-ignore
|
||||
super.endPressAnimation();
|
||||
}
|
||||
};
|
||||
|
||||
private onTouchControlChange(
|
||||
prev: HTMLElement | null,
|
||||
next: HTMLElement | null
|
||||
) {
|
||||
// Add touchend event to clean ripple on touch devices using action handler
|
||||
prev?.removeEventListener("touchend", this._handleTouchEnd);
|
||||
next?.addEventListener("touchend", this._handleTouchEnd);
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-ripple-hover-opacity: var(--ha-ripple-hover-opacity, 0.08);
|
||||
--md-ripple-pressed-opacity: var(--ha-ripple-pressed-opacity, 0.12);
|
||||
--md-ripple-hover-color: var(
|
||||
--ha-ripple-hover-color,
|
||||
var(--ha-ripple-color, var(--secondary-text-color))
|
||||
);
|
||||
--md-ripple-pressed-color: var(
|
||||
--ha-ripple-pressed-color,
|
||||
var(--ha-ripple-color, var(--secondary-text-color))
|
||||
);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-ripple": HaRipple;
|
||||
}
|
||||
}
|
|
@ -8,7 +8,10 @@ import {
|
|||
fetchEntitySourcesWithCache,
|
||||
} from "../../data/entity_sources";
|
||||
import type { EntitySelector } from "../../data/selector";
|
||||
import { filterSelectorEntities } from "../../data/selector";
|
||||
import {
|
||||
filterSelectorEntities,
|
||||
computeCreateDomains,
|
||||
} from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entities-picker";
|
||||
import "../entity/ha-entity-picker";
|
||||
|
@ -31,6 +34,8 @@ export class HaEntitySelector extends LitElement {
|
|||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@state() private _createDomains: string[] | undefined;
|
||||
|
||||
private _hasIntegration(selector: EntitySelector) {
|
||||
return (
|
||||
selector.entity?.filter &&
|
||||
|
@ -64,6 +69,7 @@ export class HaEntitySelector extends LitElement {
|
|||
.includeEntities=${this.selector.entity?.include_entities}
|
||||
.excludeEntities=${this.selector.entity?.exclude_entities}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.createDomains=${this._createDomains}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
allow-custom-entity
|
||||
|
@ -79,6 +85,7 @@ export class HaEntitySelector extends LitElement {
|
|||
.includeEntities=${this.selector.entity.include_entities}
|
||||
.excludeEntities=${this.selector.entity.exclude_entities}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.createDomains=${this._createDomains}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-entities-picker>
|
||||
|
@ -96,6 +103,9 @@ export class HaEntitySelector extends LitElement {
|
|||
this._entitySources = sources;
|
||||
});
|
||||
}
|
||||
if (changedProps.has("selector")) {
|
||||
this._createDomains = computeCreateDomains(this.selector);
|
||||
}
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
|
|
|
@ -7,8 +7,10 @@ import type {
|
|||
LocationSelectorValue,
|
||||
} from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { SchemaUnion } from "../ha-form/types";
|
||||
import type { MarkerLocation } from "../map/ha-locations-editor";
|
||||
import "../map/ha-locations-editor";
|
||||
import "../ha-form/ha-form";
|
||||
|
||||
@customElement("ha-selector-location")
|
||||
export class HaLocationSelector extends LitElement {
|
||||
|
@ -24,6 +26,49 @@ export class HaLocationSelector extends LitElement {
|
|||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(radius?: boolean, radius_readonly?: boolean) =>
|
||||
[
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "latitude",
|
||||
required: true,
|
||||
selector: { number: { step: "any" } },
|
||||
},
|
||||
{
|
||||
name: "longitude",
|
||||
required: true,
|
||||
selector: { number: { step: "any" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
...(radius
|
||||
? [
|
||||
{
|
||||
name: "radius",
|
||||
required: true,
|
||||
default: 1000,
|
||||
disabled: !!radius_readonly,
|
||||
selector: { number: { min: 0, step: 1, mode: "box" } as const },
|
||||
} as const,
|
||||
]
|
||||
: []),
|
||||
] as const
|
||||
);
|
||||
|
||||
protected willUpdate() {
|
||||
if (!this.value) {
|
||||
this.value = {
|
||||
latitude: this.hass.config.latitude,
|
||||
longitude: this.hass.config.longitude,
|
||||
radius: this.selector.location?.radius ? 1000 : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<p>${this.label ? this.label : ""}</p>
|
||||
|
@ -35,6 +80,17 @@ export class HaLocationSelector extends LitElement {
|
|||
@location-updated=${this._locationChanged}
|
||||
@radius-updated=${this._radiusChanged}
|
||||
></ha-locations-editor>
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.schema=${this._schema(
|
||||
this.selector.location?.radius,
|
||||
this.selector.location?.radius_readonly
|
||||
)}
|
||||
.data=${this.value}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -66,7 +122,8 @@ export class HaLocationSelector extends LitElement {
|
|||
? "mdi:map-marker-radius"
|
||||
: "mdi:map-marker",
|
||||
location_editable: true,
|
||||
radius_editable: true,
|
||||
radius_editable:
|
||||
!!selector.location?.radius && !selector.location?.radius_readonly,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -80,14 +137,39 @@ export class HaLocationSelector extends LitElement {
|
|||
}
|
||||
|
||||
private _radiusChanged(ev: CustomEvent) {
|
||||
const radius = ev.detail.radius;
|
||||
const radius = Math.round(ev.detail.radius);
|
||||
fireEvent(this, "value-changed", { value: { ...this.value, radius } });
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
const radius = Math.round(ev.detail.value.radius);
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
latitude: value.latitude,
|
||||
longitude: value.longitude,
|
||||
...(this.selector.location?.radius &&
|
||||
!this.selector.location?.radius_readonly
|
||||
? {
|
||||
radius,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _computeLabel = (
|
||||
entry: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string =>
|
||||
this.hass.localize(`ui.components.selectors.location.${entry.name}`);
|
||||
|
||||
static styles = css`
|
||||
ha-locations-editor {
|
||||
display: block;
|
||||
height: 400px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
|
|
|
@ -278,6 +278,14 @@ export class HaSelectSelector extends LitElement {
|
|||
|
||||
private _valueChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
|
||||
if (ev.detail?.index === -1 && this.value !== undefined) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const value = ev.detail?.value || ev.target.value;
|
||||
if (this.disabled || value === undefined || value === (this.value ?? "")) {
|
||||
return;
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
filterSelectorDevices,
|
||||
filterSelectorEntities,
|
||||
TargetSelector,
|
||||
computeCreateDomains,
|
||||
} from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-target-picker";
|
||||
|
@ -42,6 +43,8 @@ export class HaTargetSelector extends LitElement {
|
|||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@state() private _createDomains: string[] | undefined;
|
||||
|
||||
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
|
||||
|
||||
private _hasIntegration(selector: TargetSelector) {
|
||||
|
@ -68,6 +71,9 @@ export class HaTargetSelector extends LitElement {
|
|||
this._entitySources = sources;
|
||||
});
|
||||
}
|
||||
if (changedProperties.has("selector")) {
|
||||
this._createDomains = computeCreateDomains(this.selector);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
|
@ -82,6 +88,7 @@ export class HaTargetSelector extends LitElement {
|
|||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.disabled=${this.disabled}
|
||||
.createDomains=${this._createDomains}
|
||||
></ha-target-picker>`;
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
expandFloorTarget,
|
||||
expandLabelTarget,
|
||||
Selector,
|
||||
TargetSelector,
|
||||
} from "../data/selector";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
|
@ -363,6 +364,11 @@ export class HaServiceControl extends LitElement {
|
|||
return false;
|
||||
}
|
||||
|
||||
private _targetSelector = memoizeOne(
|
||||
(targetSelector: TargetSelector | null | undefined) =>
|
||||
targetSelector ? { target: { ...targetSelector } } : { target: {} }
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const serviceData = this._getServiceInfo(
|
||||
this._value?.service,
|
||||
|
@ -401,157 +407,151 @@ export class HaServiceControl extends LitElement {
|
|||
)) ||
|
||||
serviceData?.description;
|
||||
|
||||
return html`
|
||||
${this.hidePicker
|
||||
? nothing
|
||||
: html`<ha-service-picker
|
||||
return html`${this.hidePicker
|
||||
? nothing
|
||||
: html`<ha-service-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._value?.service}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._serviceChanged}
|
||||
></ha-service-picker>`}
|
||||
${this.hideDescription
|
||||
? nothing
|
||||
: html`
|
||||
<div class="description">
|
||||
${description ? html`<p>${description}</p>` : ""}
|
||||
${this._manifest
|
||||
? html` <a
|
||||
href=${this._manifest.is_built_in
|
||||
? documentationUrl(
|
||||
this.hass,
|
||||
`/integrations/${this._manifest.domain}`
|
||||
)
|
||||
: this._manifest.documentation}
|
||||
title=${this.hass.localize(
|
||||
"ui.components.service-control.integration_doc"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiHelpCircle}
|
||||
class="help-icon"
|
||||
></ha-icon-button>
|
||||
</a>`
|
||||
: nothing}
|
||||
</div>
|
||||
`}
|
||||
${serviceData && "target" in serviceData
|
||||
? html`<ha-settings-row .narrow=${this.narrow}>
|
||||
${hasOptional
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
: ""}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize("ui.components.service-control.target")}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
"ui.components.service-control.target_description"
|
||||
)}</span
|
||||
><ha-selector
|
||||
.hass=${this.hass}
|
||||
.value=${this._value?.service}
|
||||
.selector=${this._targetSelector(
|
||||
serviceData.target as TargetSelector
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._serviceChanged}
|
||||
></ha-service-picker>`}
|
||||
${this.hideDescription
|
||||
? nothing
|
||||
: html`
|
||||
<div class="description">
|
||||
${description ? html`<p>${description}</p>` : ""}
|
||||
${this._manifest
|
||||
? html` <a
|
||||
href=${this._manifest.is_built_in
|
||||
? documentationUrl(
|
||||
this.hass,
|
||||
`/integrations/${this._manifest.domain}`
|
||||
)
|
||||
: this._manifest.documentation}
|
||||
title=${this.hass.localize(
|
||||
"ui.components.service-control.integration_doc"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiHelpCircle}
|
||||
class="help-icon"
|
||||
></ha-icon-button>
|
||||
</a>`
|
||||
: nothing}
|
||||
</div>
|
||||
`}
|
||||
${serviceData && "target" in serviceData
|
||||
? html`<ha-settings-row .narrow=${this.narrow}>
|
||||
${hasOptional
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
: ""}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.components.service-control.target"
|
||||
)}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
"ui.components.service-control.target_description"
|
||||
)}</span
|
||||
><ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${serviceData.target
|
||||
? { target: serviceData.target }
|
||||
: { target: {} }}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._targetChanged}
|
||||
.value=${this._value?.target}
|
||||
></ha-selector
|
||||
></ha-settings-row>`
|
||||
: entityId
|
||||
? html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.value=${this._value?.data?.entity_id}
|
||||
.label=${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.fields.entity_id.description`
|
||||
) || entityId.description}
|
||||
@value-changed=${this._entityPicked}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>`
|
||||
: ""}
|
||||
${shouldRenderServiceDataYaml
|
||||
? html`<ha-yaml-editor
|
||||
@value-changed=${this._targetChanged}
|
||||
.value=${this._value?.target}
|
||||
></ha-selector
|
||||
></ha-settings-row>`
|
||||
: entityId
|
||||
? html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize("ui.components.service-control.data")}
|
||||
.name=${"data"}
|
||||
.readOnly=${this.disabled}
|
||||
.defaultValue=${this._value?.data}
|
||||
@value-changed=${this._dataChanged}
|
||||
></ha-yaml-editor>`
|
||||
: filteredFields?.map((dataField) => {
|
||||
const selector = dataField?.selector ?? { text: undefined };
|
||||
const type = Object.keys(selector)[0];
|
||||
const enhancedSelector = [
|
||||
"action",
|
||||
"condition",
|
||||
"trigger",
|
||||
].includes(type)
|
||||
? {
|
||||
[type]: {
|
||||
...selector[type],
|
||||
path: [dataField.key],
|
||||
},
|
||||
}
|
||||
: selector;
|
||||
.disabled=${this.disabled}
|
||||
.value=${this._value?.data?.entity_id}
|
||||
.label=${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.fields.entity_id.description`
|
||||
) || entityId.description}
|
||||
@value-changed=${this._entityPicked}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>`
|
||||
: ""}
|
||||
${shouldRenderServiceDataYaml
|
||||
? html`<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize("ui.components.service-control.data")}
|
||||
.name=${"data"}
|
||||
.readOnly=${this.disabled}
|
||||
.defaultValue=${this._value?.data}
|
||||
@value-changed=${this._dataChanged}
|
||||
></ha-yaml-editor>`
|
||||
: filteredFields?.map((dataField) => {
|
||||
const selector = dataField?.selector ?? { text: undefined };
|
||||
const type = Object.keys(selector)[0];
|
||||
const enhancedSelector = ["action", "condition", "trigger"].includes(
|
||||
type
|
||||
)
|
||||
? {
|
||||
[type]: {
|
||||
...selector[type],
|
||||
path: [dataField.key],
|
||||
},
|
||||
}
|
||||
: selector;
|
||||
|
||||
const showOptional = showOptionalToggle(dataField);
|
||||
const showOptional = showOptionalToggle(dataField);
|
||||
|
||||
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
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
: ""
|
||||
: html`<ha-checkbox
|
||||
.key=${dataField.key}
|
||||
.checked=${this._checkedKeys.has(dataField.key) ||
|
||||
(this._value?.data &&
|
||||
this._value.data[dataField.key] !== undefined)}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._checkboxChanged}
|
||||
slot="prefix"
|
||||
></ha-checkbox>`}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.name`
|
||||
) ||
|
||||
dataField.name ||
|
||||
dataField.key}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
|
||||
) || dataField?.description}</span
|
||||
>
|
||||
<ha-selector
|
||||
.disabled=${this.disabled ||
|
||||
(showOptional &&
|
||||
!this._checkedKeys.has(dataField.key) &&
|
||||
(!this._value?.data ||
|
||||
this._value.data[dataField.key] === undefined))}
|
||||
.hass=${this.hass}
|
||||
.selector=${enhancedSelector}
|
||||
.key=${dataField.key}
|
||||
@value-changed=${this._serviceDataChanged}
|
||||
.value=${this._value?.data
|
||||
? this._value.data[dataField.key]
|
||||
: undefined}
|
||||
.placeholder=${dataField.default}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
@item-moved=${this._itemMoved}
|
||||
></ha-selector>
|
||||
</ha-settings-row>`
|
||||
: "";
|
||||
})}
|
||||
`;
|
||||
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
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
: ""
|
||||
: html`<ha-checkbox
|
||||
.key=${dataField.key}
|
||||
.checked=${this._checkedKeys.has(dataField.key) ||
|
||||
(this._value?.data &&
|
||||
this._value.data[dataField.key] !== undefined)}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._checkboxChanged}
|
||||
slot="prefix"
|
||||
></ha-checkbox>`}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.name`
|
||||
) ||
|
||||
dataField.name ||
|
||||
dataField.key}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
|
||||
) || dataField?.description}</span
|
||||
>
|
||||
<ha-selector
|
||||
.disabled=${this.disabled ||
|
||||
(showOptional &&
|
||||
!this._checkedKeys.has(dataField.key) &&
|
||||
(!this._value?.data ||
|
||||
this._value.data[dataField.key] === undefined))}
|
||||
.hass=${this.hass}
|
||||
.selector=${enhancedSelector}
|
||||
.key=${dataField.key}
|
||||
@value-changed=${this._serviceDataChanged}
|
||||
.value=${this._value?.data
|
||||
? this._value.data[dataField.key]
|
||||
: undefined}
|
||||
.placeholder=${dataField.default}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
@item-moved=${this._itemMoved}
|
||||
></ha-selector>
|
||||
</ha-settings-row>`
|
||||
: "";
|
||||
})} `;
|
||||
}
|
||||
|
||||
private _localizeValueCallback = (key: string) => {
|
||||
|
|
|
@ -8,6 +8,9 @@ export class HaSettingsRow extends LitElement {
|
|||
@property({ type: Boolean, attribute: "three-line" })
|
||||
public threeLine = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "wrap-heading", reflect: true })
|
||||
public wrapHeading = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="prefix-wrap">
|
||||
|
@ -51,7 +54,7 @@ export class HaSettingsRow extends LitElement {
|
|||
.body[three-line] {
|
||||
min-height: var(--paper-item-body-three-line-min-height, 88px);
|
||||
}
|
||||
.body > * {
|
||||
:host(:not([wrap-heading])) body > * {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { customElement } from "lit/decorators";
|
||||
import "element-internals-polyfill";
|
||||
import { MdSlider } from "@material/web/slider/slider";
|
||||
import { CSSResult, css } from "lit";
|
||||
import { css } from "lit";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
|
||||
@customElement("ha-slider")
|
||||
|
@ -11,8 +11,8 @@ export class HaSlider extends MdSlider {
|
|||
this.dir = mainWindow.document.dir;
|
||||
}
|
||||
|
||||
static override styles: CSSResult[] = [
|
||||
...MdSlider.styles,
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-primary: var(--primary-color);
|
||||
|
|
|
@ -82,7 +82,7 @@ export class HaSortable extends LitElement {
|
|||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._shouldBeDestroy = false;
|
||||
if (this.hasUpdated) {
|
||||
if (this.hasUpdated && !this.disabled) {
|
||||
this._createSortable();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import { customElement } from "lit/decorators";
|
||||
import "element-internals-polyfill";
|
||||
import { CSSResult, css } from "lit";
|
||||
import { css } from "lit";
|
||||
import { MdSubMenu } from "@material/web/menu/sub-menu";
|
||||
|
||||
@customElement("ha-sub-menu")
|
||||
// @ts-expect-error
|
||||
export class HaSubMenu extends MdSubMenu {
|
||||
static override styles: CSSResult[] = [
|
||||
MdSubMenu.styles,
|
||||
async show() {
|
||||
super.show();
|
||||
this.menu.hasOverflow = false;
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
|
|
|
@ -1,15 +1,7 @@
|
|||
import type { Ripple } from "@material/mwc-ripple";
|
||||
import "@material/mwc-ripple/mwc-ripple";
|
||||
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
queryAsync,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import "./ha-ripple";
|
||||
|
||||
@customElement("ha-tab")
|
||||
export class HaTab extends LitElement {
|
||||
|
@ -19,10 +11,6 @@ export class HaTab extends LitElement {
|
|||
|
||||
@property() public name?: string;
|
||||
|
||||
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
|
||||
|
||||
@state() private _shouldRenderRipple = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div
|
||||
|
@ -30,60 +18,21 @@ export class HaTab extends LitElement {
|
|||
role="tab"
|
||||
aria-selected=${this.active}
|
||||
aria-label=${ifDefined(this.name)}
|
||||
@focus=${this.handleRippleFocus}
|
||||
@blur=${this.handleRippleBlur}
|
||||
@mousedown=${this.handleRippleActivate}
|
||||
@mouseup=${this.handleRippleDeactivate}
|
||||
@mouseenter=${this.handleRippleMouseEnter}
|
||||
@mouseleave=${this.handleRippleMouseLeave}
|
||||
@touchstart=${this.handleRippleActivate}
|
||||
@touchend=${this.handleRippleDeactivate}
|
||||
@touchcancel=${this.handleRippleDeactivate}
|
||||
@keydown=${this._handleKeyDown}
|
||||
>
|
||||
${this.narrow ? html`<slot name="icon"></slot>` : ""}
|
||||
<span class="name">${this.name}</span>
|
||||
${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""}
|
||||
<ha-ripple></ha-ripple>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
|
||||
this._shouldRenderRipple = true;
|
||||
return this._ripple;
|
||||
});
|
||||
|
||||
private _handleKeyDown(ev: KeyboardEvent): void {
|
||||
if (ev.key === "Enter") {
|
||||
(ev.target as HTMLElement).click();
|
||||
}
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private handleRippleActivate(evt?: Event) {
|
||||
this._rippleHandlers.startPress(evt);
|
||||
}
|
||||
|
||||
private handleRippleDeactivate() {
|
||||
this._rippleHandlers.endPress();
|
||||
}
|
||||
|
||||
private handleRippleMouseEnter() {
|
||||
this._rippleHandlers.startHover();
|
||||
}
|
||||
|
||||
private handleRippleMouseLeave() {
|
||||
this._rippleHandlers.endHover();
|
||||
}
|
||||
|
||||
private handleRippleFocus() {
|
||||
this._rippleHandlers.startFocus();
|
||||
}
|
||||
|
||||
private handleRippleBlur() {
|
||||
this._rippleHandlers.endFocus();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
div {
|
||||
|
@ -126,6 +75,15 @@ export class HaTab extends LitElement {
|
|||
:host([narrow]) div {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
div:focus-visible:before {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: "";
|
||||
inset: 0;
|
||||
background-color: var(--secondary-text-color);
|
||||
opacity: 0.08;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,6 +65,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Array }) public createDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only targets with entities from specific domains.
|
||||
* @type {Array}
|
||||
|
@ -468,6 +470,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeEntities=${ensureArray(this.value?.entity_id)}
|
||||
.createDomains=${this.createDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
@click=${this._preventDefault}
|
||||
allow-custom-entity
|
||||
|
|
|
@ -19,7 +19,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
|||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ThemeMode } from "../../types";
|
||||
import "../ha-input-helper-text";
|
||||
import "./ha-map";
|
||||
import type { HaMap } from "./ha-map";
|
||||
|
@ -61,7 +61,8 @@ export class HaLocationsEditor extends LitElement {
|
|||
|
||||
@property({ type: Number }) public zoom = 16;
|
||||
|
||||
@property({ type: Boolean }) public darkMode = false;
|
||||
@property({ attribute: "theme-mode", type: String })
|
||||
public themeMode: ThemeMode = "auto";
|
||||
|
||||
@state() private _locationMarkers?: Record<string, Marker | Circle>;
|
||||
|
||||
|
@ -133,7 +134,7 @@ export class HaLocationsEditor extends LitElement {
|
|||
.layers=${this._getLayers(this._circles, this._locationMarkers)}
|
||||
.zoom=${this.zoom}
|
||||
.autoFit=${this.autoFit}
|
||||
?darkMode=${this.darkMode}
|
||||
.themeMode=${this.themeMode}
|
||||
></ha-map>
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
|
|
|
@ -1,32 +1,32 @@
|
|||
import { isToday } from "date-fns";
|
||||
import type {
|
||||
Circle,
|
||||
CircleMarker,
|
||||
LatLngTuple,
|
||||
LatLngExpression,
|
||||
LatLngTuple,
|
||||
Layer,
|
||||
Map,
|
||||
Marker,
|
||||
Polyline,
|
||||
} from "leaflet";
|
||||
import { isToday } from "date-fns";
|
||||
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
|
||||
import { CSSResultGroup, PropertyValues, ReactiveElement, css } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { formatDateTime } from "../../common/datetime/format_date_time";
|
||||
import {
|
||||
formatTimeWeekday,
|
||||
formatTimeWithSeconds,
|
||||
} from "../../common/datetime/format_time";
|
||||
import {
|
||||
LeafletModuleType,
|
||||
setupLeafletMap,
|
||||
} from "../../common/dom/setup-leaflet-map";
|
||||
import {
|
||||
formatTimeWithSeconds,
|
||||
formatTimeWeekday,
|
||||
} from "../../common/datetime/format_time";
|
||||
import { formatDateTime } from "../../common/datetime/format_date_time";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { loadPolyfillIfNeeded } from "../../resources/resize-observer.polyfill";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { HomeAssistant, ThemeMode } from "../../types";
|
||||
import { isTouch } from "../../util/is_touch";
|
||||
import "../ha-icon-button";
|
||||
import "./ha-entity-marker";
|
||||
import { isTouch } from "../../util/is_touch";
|
||||
|
||||
const getEntityId = (entity: string | HaMapEntity): string =>
|
||||
typeof entity === "string" ? entity : entity.entity_id;
|
||||
|
@ -69,7 +69,8 @@ export class HaMap extends ReactiveElement {
|
|||
|
||||
@property({ type: Boolean }) public fitZones = false;
|
||||
|
||||
@property({ type: Boolean }) public darkMode = false;
|
||||
@property({ attribute: "theme-mode", type: String })
|
||||
public themeMode: ThemeMode = "auto";
|
||||
|
||||
@property({ type: Number }) public zoom = 14;
|
||||
|
||||
|
@ -154,7 +155,7 @@ export class HaMap extends ReactiveElement {
|
|||
}
|
||||
|
||||
if (
|
||||
!changedProps.has("darkMode") &&
|
||||
!changedProps.has("themeMode") &&
|
||||
(!changedProps.has("hass") ||
|
||||
(oldHass && oldHass.themes?.darkMode === this.hass.themes?.darkMode))
|
||||
) {
|
||||
|
@ -163,12 +164,18 @@ export class HaMap extends ReactiveElement {
|
|||
this._updateMapStyle();
|
||||
}
|
||||
|
||||
private get _darkMode() {
|
||||
return (
|
||||
this.themeMode === "dark" ||
|
||||
(this.themeMode === "auto" && Boolean(this.hass.themes.darkMode))
|
||||
);
|
||||
}
|
||||
|
||||
private _updateMapStyle(): void {
|
||||
const darkMode = this.darkMode || (this.hass.themes.darkMode ?? false);
|
||||
const forcedDark = this.darkMode;
|
||||
const map = this.renderRoot.querySelector("#map");
|
||||
map!.classList.toggle("dark", darkMode);
|
||||
map!.classList.toggle("forced-dark", forcedDark);
|
||||
map!.classList.toggle("dark", this._darkMode);
|
||||
map!.classList.toggle("forced-dark", this.themeMode === "dark");
|
||||
map!.classList.toggle("forced-light", this.themeMode === "light");
|
||||
}
|
||||
|
||||
private async _loadMap(): Promise<void> {
|
||||
|
@ -398,8 +405,7 @@ export class HaMap extends ReactiveElement {
|
|||
"--dark-primary-color"
|
||||
);
|
||||
|
||||
const className =
|
||||
this.darkMode || this.hass.themes.darkMode ? "dark" : "light";
|
||||
const className = this._darkMode ? "dark" : "light";
|
||||
|
||||
for (const entity of this.entities) {
|
||||
const stateObj = hass.states[getEntityId(entity)];
|
||||
|
@ -543,27 +549,30 @@ export class HaMap extends ReactiveElement {
|
|||
background: #090909;
|
||||
}
|
||||
#map.forced-dark {
|
||||
color: #ffffff;
|
||||
--map-filter: invert(0.9) hue-rotate(170deg) brightness(1.5)
|
||||
contrast(1.2) saturate(0.3);
|
||||
}
|
||||
#map.forced-light {
|
||||
background: #ffffff;
|
||||
color: #000000;
|
||||
--map-filter: invert(0);
|
||||
}
|
||||
#map:active {
|
||||
cursor: grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
.light {
|
||||
color: #000000;
|
||||
}
|
||||
.dark {
|
||||
color: #ffffff;
|
||||
}
|
||||
.leaflet-tile-pane {
|
||||
filter: var(--map-filter);
|
||||
}
|
||||
.dark .leaflet-bar a {
|
||||
background-color: var(--card-background-color, #1c1c1c);
|
||||
background-color: #1c1c1c;
|
||||
color: #ffffff;
|
||||
}
|
||||
.dark .leaflet-bar a:hover {
|
||||
background-color: #313131;
|
||||
}
|
||||
.leaflet-marker-draggable {
|
||||
cursor: move !important;
|
||||
}
|
||||
|
|
|
@ -797,6 +797,7 @@ export class HaAutomationTracer extends LitElement {
|
|||
description: html`${this.hass.localize(
|
||||
`ui.panel.config.automation.trace.messages.${message}`,
|
||||
{
|
||||
reason: this.trace.script_execution,
|
||||
time: renderFinishedAt(),
|
||||
executiontime: renderRuntime(),
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
|
||||
export const FORMAT_TEXT = "text";
|
||||
export const FORMAT_NUMBER = "number";
|
||||
|
@ -96,3 +97,9 @@ export const ALARM_MODES: Record<AlarmMode, AlarmConfig> = {
|
|||
path: mdiShieldOff,
|
||||
},
|
||||
};
|
||||
|
||||
export const supportedAlarmModes = (stateObj: AlarmControlPanelEntity) =>
|
||||
(Object.keys(ALARM_MODES) as AlarmMode[]).filter((mode) => {
|
||||
const feature = ALARM_MODES[mode].feature;
|
||||
return !feature || supportsFeature(stateObj, feature);
|
||||
});
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { EntityFilter } from "../common/entity/entity_filter";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
type StrictConnectionMode = "disabled" | "guard_page" | "drop_connection";
|
||||
|
||||
interface CloudStatusNotLoggedIn {
|
||||
logged_in: false;
|
||||
cloud: "disconnected" | "connecting" | "connected";
|
||||
|
@ -19,6 +21,7 @@ export interface CloudPreferences {
|
|||
alexa_enabled: boolean;
|
||||
remote_enabled: boolean;
|
||||
remote_allow_remote_enable: boolean;
|
||||
strict_connection: StrictConnectionMode;
|
||||
google_secure_devices_pin: string | undefined;
|
||||
cloudhooks: { [webhookId: string]: CloudWebhook };
|
||||
alexa_report_state: boolean;
|
||||
|
@ -141,6 +144,7 @@ export const updateCloudPref = (
|
|||
google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"];
|
||||
tts_default_voice?: CloudPreferences["tts_default_voice"];
|
||||
remote_allow_remote_enable?: CloudPreferences["remote_allow_remote_enable"];
|
||||
strict_connection?: CloudPreferences["strict_connection"];
|
||||
}
|
||||
) =>
|
||||
hass.callWS({
|
||||
|
|
|
@ -23,6 +23,8 @@ export interface ConfigEntry {
|
|||
pref_disable_polling: boolean;
|
||||
disabled_by: "user" | null;
|
||||
reason: string | null;
|
||||
error_reason_translation_key: string | null;
|
||||
error_reason_translation_placeholders: Record<string, string> | null;
|
||||
}
|
||||
|
||||
export type ConfigEntryMutableParams = Partial<
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
startOfDay,
|
||||
isFirstDayOfMonth,
|
||||
isLastDayOfMonth,
|
||||
} from "date-fns/esm";
|
||||
} from "date-fns";
|
||||
import { Collection, getCollection } from "home-assistant-js-websocket";
|
||||
import {
|
||||
calcDate,
|
||||
|
@ -95,6 +95,7 @@ export type EnergySolarForecasts = {
|
|||
export interface DeviceConsumptionEnergyPreference {
|
||||
// This is an ever increasing value
|
||||
stat_consumption: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface FlowFromGridSourceEnergyPreference {
|
||||
|
|
|
@ -422,7 +422,8 @@ export const computeHistory = (
|
|||
entityIds: string[],
|
||||
localize: LocalizeFunc,
|
||||
sensorNumericalDeviceClasses: string[],
|
||||
splitDeviceClasses = false
|
||||
splitDeviceClasses = false,
|
||||
forceNumeric = false
|
||||
): HistoryResult => {
|
||||
const lineChartDevices: { [unit: string]: HistoryStates } = {};
|
||||
const timelineDevices: TimelineEntity[] = [];
|
||||
|
@ -468,6 +469,7 @@ export const computeHistory = (
|
|||
let unit: string | undefined;
|
||||
|
||||
const isNumeric =
|
||||
forceNumeric ||
|
||||
isNumericFromDomain(domain) ||
|
||||
(currentState != null &&
|
||||
isNumericFromAttributes(currentState.attributes)) ||
|
||||
|
|
|
@ -44,6 +44,7 @@ export interface IntegrationManifest {
|
|||
| "local_polling"
|
||||
| "local_push";
|
||||
single_config_entry?: boolean;
|
||||
version?: string;
|
||||
}
|
||||
export interface IntegrationSetup {
|
||||
domain: string;
|
||||
|
|
|
@ -9,12 +9,14 @@ export interface LabelRegistryEntry {
|
|||
name: string;
|
||||
icon: string | null;
|
||||
color: string | null;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
export interface LabelRegistryEntryMutableParams {
|
||||
name: string;
|
||||
icon?: string | null;
|
||||
color?: string | null;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export const fetchLabelRegistry = (conn: Connection) =>
|
||||
|
|
|
@ -5,9 +5,7 @@ import {
|
|||
import { getExtendedEntityRegistryEntry } from "./entity_registry";
|
||||
import { showEnterCodeDialog } from "../dialogs/enter-code/show-enter-code-dialog";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export const FORMAT_TEXT = "text";
|
||||
export const FORMAT_NUMBER = "number";
|
||||
import { UNAVAILABLE } from "./entity";
|
||||
|
||||
export const enum LockEntityFeature {
|
||||
OPEN = 1,
|
||||
|
@ -24,6 +22,33 @@ export interface LockEntity extends HassEntityBase {
|
|||
|
||||
type ProtectedLockService = "lock" | "unlock" | "open";
|
||||
|
||||
export function isLocked(stateObj: LockEntity) {
|
||||
return stateObj.state === "locked";
|
||||
}
|
||||
|
||||
export function isUnlocking(stateObj: LockEntity) {
|
||||
return stateObj.state === "unlocking";
|
||||
}
|
||||
|
||||
export function isLocking(stateObj: LockEntity) {
|
||||
return stateObj.state === "locking";
|
||||
}
|
||||
|
||||
export function isJammed(stateObj: LockEntity) {
|
||||
return stateObj.state === "jammed";
|
||||
}
|
||||
|
||||
export function isAvailable(stateObj: LockEntity) {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return false;
|
||||
}
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
return (
|
||||
assumedState ||
|
||||
(!isLocking(stateObj) && !isUnlocking(stateObj) && !isJammed(stateObj))
|
||||
);
|
||||
}
|
||||
|
||||
export const callProtectedLockService = async (
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
|
|
|
@ -358,6 +358,10 @@ export const localizeStateMessage = (
|
|||
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_locking`);
|
||||
case "unlocking":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_unlocking`);
|
||||
case "opening":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_opening`);
|
||||
case "open":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.is_opened`);
|
||||
case "locked":
|
||||
return localize(`${LOGBOOK_LOCALIZE_PATH}.was_locked`);
|
||||
case "jammed":
|
||||
|
|
|
@ -93,6 +93,7 @@ export type ScriptConfig = ManualScriptConfig | BlueprintScriptConfig;
|
|||
|
||||
export interface ManualScriptConfig {
|
||||
alias: string;
|
||||
description?: string;
|
||||
sequence: Action | Action[];
|
||||
icon?: string;
|
||||
mode?: (typeof MODES)[number];
|
||||
|
|
|
@ -89,7 +89,8 @@ const tryDescribeAction = <T extends ActionType>(
|
|||
const config = action as ActionTypes["service"];
|
||||
|
||||
const targets: string[] = [];
|
||||
if (config.target) {
|
||||
const targetOrData = config.target || config.data;
|
||||
if (targetOrData) {
|
||||
for (const [key, name] of Object.entries({
|
||||
area_id: "areas",
|
||||
device_id: "devices",
|
||||
|
@ -97,12 +98,10 @@ const tryDescribeAction = <T extends ActionType>(
|
|||
floor_id: "floors",
|
||||
label_id: "labels",
|
||||
})) {
|
||||
if (!(key in config.target)) {
|
||||
if (!(key in targetOrData)) {
|
||||
continue;
|
||||
}
|
||||
const keyConf: string[] = Array.isArray(config.target[key])
|
||||
? config.target[key]
|
||||
: [config.target[key]];
|
||||
const keyConf: string[] = ensureArray(targetOrData[key]) || [];
|
||||
|
||||
for (const targetThing of keyConf) {
|
||||
if (isTemplate(targetThing)) {
|
||||
|
@ -195,8 +194,12 @@ const tryDescribeAction = <T extends ActionType>(
|
|||
(config.service && isTemplate(config.service))
|
||||
) {
|
||||
return hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.service_based_on_template`,
|
||||
{ targets: formatListWithAnds(hass.locale, targets) }
|
||||
targets.length
|
||||
? `${actionTranslationBaseKey}.service.description.service_based_on_template`
|
||||
: `${actionTranslationBaseKey}.service.description.service_based_on_template_no_targets`,
|
||||
{
|
||||
targets: formatListWithAnds(hass.locale, targets),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -208,7 +211,9 @@ const tryDescribeAction = <T extends ActionType>(
|
|||
|
||||
if (config.metadata) {
|
||||
return hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.service_name`,
|
||||
targets.length
|
||||
? `${actionTranslationBaseKey}.service.description.service_name`
|
||||
: `${actionTranslationBaseKey}.service.description.service_name_no_targets`,
|
||||
{
|
||||
domain: domainToName(hass.localize, domain),
|
||||
name: service || config.service,
|
||||
|
@ -218,7 +223,9 @@ const tryDescribeAction = <T extends ActionType>(
|
|||
}
|
||||
|
||||
return hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.service_based_on_name`,
|
||||
targets.length
|
||||
? `${actionTranslationBaseKey}.service.description.service_based_on_name`
|
||||
: `${actionTranslationBaseKey}.service.description.service_based_on_name_no_targets`,
|
||||
{
|
||||
name: service
|
||||
? `${domainToName(hass.localize, domain)}: ${service}`
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
EntityRegistryEntry,
|
||||
} from "./entity_registry";
|
||||
import { EntitySources } from "./entity_sources";
|
||||
import { isHelperDomain } from "../panels/config/helpers/const";
|
||||
|
||||
export type Selector =
|
||||
| ActionSelector
|
||||
|
@ -270,7 +271,11 @@ export interface LanguageSelector {
|
|||
}
|
||||
|
||||
export interface LocationSelector {
|
||||
location: { radius?: boolean; icon?: string } | null;
|
||||
location: {
|
||||
radius?: boolean;
|
||||
radius_readonly?: boolean;
|
||||
icon?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface LocationSelectorValue {
|
||||
|
@ -817,3 +822,34 @@ export const handleLegacyDeviceSelector = (
|
|||
device: rest,
|
||||
};
|
||||
};
|
||||
|
||||
export const computeCreateDomains = (
|
||||
selector: EntitySelector | TargetSelector
|
||||
): undefined | string[] => {
|
||||
let entityFilters: EntitySelectorFilter[] | undefined;
|
||||
|
||||
if ("target" in selector) {
|
||||
entityFilters = ensureArray(selector.target?.entity);
|
||||
} else if ("entity" in selector) {
|
||||
if (selector.entity?.include_entities) {
|
||||
return undefined;
|
||||
}
|
||||
entityFilters = ensureArray(selector.entity?.filter);
|
||||
}
|
||||
if (!entityFilters) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const createDomains = entityFilters.flatMap((entityFilter) =>
|
||||
!entityFilter.integration &&
|
||||
!entityFilter.device_class &&
|
||||
!entityFilter.supported_features &&
|
||||
entityFilter.domain
|
||||
? ensureArray(entityFilter.domain).filter((domain) =>
|
||||
isHelperDomain(domain)
|
||||
)
|
||||
: []
|
||||
);
|
||||
|
||||
return [...new Set(createDomains)];
|
||||
};
|
||||
|
|
|
@ -118,6 +118,10 @@ export const checkForEntityUpdates = async (
|
|||
return;
|
||||
}
|
||||
|
||||
showToast(element, {
|
||||
message: hass.localize("ui.panel.config.updates.checking_updates"),
|
||||
});
|
||||
|
||||
let updated = 0;
|
||||
|
||||
const unsubscribeEvents = await hass.connection.subscribeEvents<HassEvent>(
|
||||
|
|
|
@ -11,6 +11,11 @@ export interface Zone {
|
|||
radius?: number;
|
||||
}
|
||||
|
||||
export interface HomeZoneMutableParams {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface ZoneMutableParams {
|
||||
name: string;
|
||||
icon?: string;
|
||||
|
|
|
@ -52,7 +52,7 @@ export const showConfigFlowDialog = (
|
|||
? html`
|
||||
<ha-markdown allowsvg breaks .content=${description}></ha-markdown>
|
||||
`
|
||||
: "";
|
||||
: step.reason;
|
||||
},
|
||||
|
||||
renderShowFormStepHeader(hass, step) {
|
||||
|
|
|
@ -34,7 +34,7 @@ export interface FlowConfig {
|
|||
renderAbortDescription(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepAbort
|
||||
): TemplateResult | "";
|
||||
): TemplateResult | string;
|
||||
|
||||
renderShowFormStepHeader(
|
||||
hass: HomeAssistant,
|
||||
|
|
|
@ -65,7 +65,7 @@ export const showOptionsFlowDialog = (
|
|||
.content=${description}
|
||||
></ha-markdown>
|
||||
`
|
||||
: "";
|
||||
: step.reason;
|
||||
},
|
||||
|
||||
renderShowFormStepHeader(hass, step) {
|
||||
|
|
|
@ -78,6 +78,7 @@ class LightColorTempPicker extends LitElement {
|
|||
|
||||
return html`
|
||||
<ha-control-slider
|
||||
touch-action="none"
|
||||
inverted
|
||||
vertical
|
||||
.value=${this._ctPickerValue}
|
||||
|
|
|
@ -16,21 +16,23 @@ class MoreInfoCounter extends LitElement {
|
|||
return nothing;
|
||||
}
|
||||
|
||||
const disabled = isUnavailableState(this.stateObj!.state);
|
||||
const disabled = isUnavailableState(this.stateObj.state);
|
||||
|
||||
return html`
|
||||
<div class="actions">
|
||||
<mwc-button
|
||||
.action=${"increment"}
|
||||
@click=${this._handleActionClick}
|
||||
.disabled=${disabled}
|
||||
.disabled=${disabled ||
|
||||
Number(this.stateObj.state) === this.stateObj.attributes.maximum}
|
||||
>
|
||||
${this.hass!.localize("ui.card.counter.actions.increment")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
.action=${"decrement"}
|
||||
@click=${this._handleActionClick}
|
||||
.disabled=${disabled}
|
||||
.disabled=${disabled ||
|
||||
Number(this.stateObj.state) === this.stateObj.attributes.minimum}
|
||||
>
|
||||
${this.hass!.localize("ui.card.counter.actions.decrement")}
|
||||
</mwc-button>
|
||||
|
|
|
@ -189,6 +189,7 @@ class MoreInfoLawnMower extends LitElement {
|
|||
.flex-horizontal {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.space-around {
|
||||
justify-content: space-around;
|
||||
|
|
|
@ -9,11 +9,12 @@ import "../../../components/ha-control-button";
|
|||
import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-outlined-icon-button";
|
||||
import "../../../components/ha-state-icon";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import {
|
||||
LockEntity,
|
||||
LockEntityFeature,
|
||||
callProtectedLockService,
|
||||
isAvailable,
|
||||
isJammed,
|
||||
} from "../../../data/lock";
|
||||
import "../../../state-control/lock/ha-state-control-lock-toggle";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
@ -85,15 +86,13 @@ class MoreInfoLock extends LitElement {
|
|||
"--state-color": color,
|
||||
};
|
||||
|
||||
const isJammed = this.stateObj.state === "jammed";
|
||||
|
||||
return html`
|
||||
<ha-more-info-state-header
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-more-info-state-header>
|
||||
<div class="controls" style=${styleMap(style)}>
|
||||
${this.stateObj.state === "jammed"
|
||||
${isJammed(this.stateObj)
|
||||
? html`
|
||||
<div class="status">
|
||||
<span></span>
|
||||
|
@ -125,7 +124,7 @@ class MoreInfoLock extends LitElement {
|
|||
`
|
||||
: html`
|
||||
<ha-control-button
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
.disabled=${!isAvailable(this.stateObj)}
|
||||
class="open-button ${this._buttonState}"
|
||||
@click=${this._open}
|
||||
>
|
||||
|
@ -139,7 +138,7 @@ class MoreInfoLock extends LitElement {
|
|||
: nothing}
|
||||
</div>
|
||||
<div>
|
||||
${isJammed
|
||||
${isJammed(this.stateObj)
|
||||
? html`
|
||||
<ha-control-button-group class="jammed">
|
||||
<ha-control-button @click=${this._unlock}>
|
||||
|
|
|
@ -19,6 +19,7 @@ import "../../../components/ha-select";
|
|||
import "../../../components/ha-slider";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
|
||||
import { isUnavailableState } from "../../../data/entity";
|
||||
import {
|
||||
MediaPickedEvent,
|
||||
MediaPlayerEntity,
|
||||
|
@ -62,7 +63,8 @@ class MoreInfoMediaPlayer extends LitElement {
|
|||
`
|
||||
)}
|
||||
</div>
|
||||
${supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
|
||||
${!isUnavailableState(stateObj.state) &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
|
||||
? html`
|
||||
<mwc-button
|
||||
.label=${this.hass.localize(
|
||||
|
|
|
@ -142,7 +142,7 @@ class MoreInfoVacuum extends LitElement {
|
|||
"ui.dialogs.more_info_control.vacuum.commands"
|
||||
)}
|
||||
</div>
|
||||
<div class="flex-horizontal">
|
||||
<div class="flex-horizontal space-around">
|
||||
${VACUUM_COMMANDS.filter((item) =>
|
||||
item.isVisible(stateObj)
|
||||
).map(
|
||||
|
@ -327,6 +327,9 @@ class MoreInfoVacuum extends LitElement {
|
|||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.space-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { startOfYesterday, subHours } from "date-fns/esm";
|
||||
import { startOfYesterday, subHours } from "date-fns";
|
||||
import { LitElement, PropertyValues, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue