Compare commits

..

1 Commits

Author SHA1 Message Date
Bram Kragten
29a103e884 Prevent wrap of menu items 2024-04-02 15:09:08 +02:00
333 changed files with 8152 additions and 12651 deletions

View File

@@ -115,7 +115,6 @@
} }
], ],
"unused-imports/no-unused-imports": "error", "unused-imports/no-unused-imports": "error",
"lit/attribute-names": "warn",
"lit/attribute-value-entities": "off", "lit/attribute-value-entities": "off",
"lit/no-template-map": "off", "lit/no-template-map": "off",
"lit/no-native-attributes": "warn", "lit/no-native-attributes": "warn",

View File

@@ -21,7 +21,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
with: with:
ref: dev ref: dev
@@ -57,7 +57,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
with: with:
ref: master ref: master

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2
with: with:
@@ -58,7 +58,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2
with: with:
@@ -76,7 +76,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2
with: with:
@@ -89,7 +89,7 @@ jobs:
env: env:
IS_TEST: "true" IS_TEST: "true"
- name: Upload bundle stats - name: Upload bundle stats
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.1
with: with:
name: frontend-bundle-stats name: frontend-bundle-stats
path: build/stats/*.json path: build/stats/*.json
@@ -100,7 +100,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2
with: with:
@@ -113,7 +113,7 @@ jobs:
env: env:
IS_TEST: "true" IS_TEST: "true"
- name: Upload bundle stats - name: Upload bundle stats
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.1
with: with:
name: supervisor-bundle-stats name: supervisor-bundle-stats
path: build/stats/*.json path: build/stats/*.json

View File

@@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
with: with:
# We must fetch at least the immediate parents so that if this is # We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head. # a pull request then we can checkout the head.

View File

@@ -22,7 +22,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
with: with:
ref: dev ref: dev
@@ -58,7 +58,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
with: with:
ref: master ref: master

View File

@@ -16,7 +16,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2

View File

@@ -21,7 +21,7 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview') if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2

View File

@@ -20,7 +20,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5 uses: actions/setup-python@v5
@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.1
with: with:
name: wheels name: wheels
path: dist/home_assistant_frontend*.whl path: dist/home_assistant_frontend*.whl
if-no-files-found: error if-no-files-found: error
- name: Upload translations - name: Upload translations
uses: actions/upload-artifact@v4.3.3 uses: actions/upload-artifact@v4.3.1
with: with:
name: translations name: translations
path: translations.tar.gz path: translations.tar.gz

View File

@@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Verify version - name: Verify version
uses: home-assistant/actions/helpers/verify-version@master uses: home-assistant/actions/helpers/verify-version@master
@@ -55,7 +55,7 @@ jobs:
script/release script/release
- name: Upload release assets - name: Upload release assets
uses: softprops/action-gh-release@v2.0.5 uses: softprops/action-gh-release@v2.0.4
with: with:
files: | files: |
dist/*.whl dist/*.whl

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.2
- name: Upload Translations - name: Upload Translations
run: | run: |

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.2.2.cjs yarnPath: .yarn/releases/yarn-4.1.1.cjs

View File

@@ -1,23 +1,7 @@
import defineProvider from "@babel/helper-define-polyfill-provider"; import defineProvider from "@babel/helper-define-polyfill-provider";
import { join } from "node:path";
import paths from "../paths.cjs";
const POLYFILL_DIR = join(paths.polymer_dir, "src/resources/polyfills");
// List of polyfill keys with supported browser targets for the functionality // List of polyfill keys with supported browser targets for the functionality
const PolyfillSupport = { const PolyfillSupport = {
// Note states and shadowRoot properties should be supported.
"element-internals": {
android: 90,
chrome: 90,
edge: 90,
firefox: 126,
ios: 17.4,
opera: 76,
opera_mobile: 64,
safari: 17.4,
samsung: 15.0,
},
fetch: { fetch: {
android: 42, android: 42,
chrome: 42, chrome: 42,
@@ -40,36 +24,16 @@ const PolyfillSupport = {
safari: 10.0, safari: 10.0,
samsung: 5.0, samsung: 5.0,
}, },
"resize-observer": {
android: 64,
chrome: 64,
edge: 79,
firefox: 69,
ios: 13.4,
opera: 51,
opera_mobile: 47,
safari: 13.1,
samsung: 9.0,
},
}; };
// Map of global variables and/or instance and static properties to the // Map of global variables and/or instance and static properties to the
// corresponding polyfill key and actual module to import // corresponding polyfill key and actual module to import
const polyfillMap = { const polyfillMap = {
global: { global: {
fetch: { key: "fetch", module: "unfetch/polyfill" },
Proxy: { key: "proxy", module: "proxy-polyfill" }, Proxy: { key: "proxy", module: "proxy-polyfill" },
ResizeObserver: { fetch: { key: "fetch", module: "unfetch/polyfill" },
key: "resize-observer",
module: join(POLYFILL_DIR, "resize-observer.ts"),
},
},
instance: {
attachInternals: {
key: "element-internals",
module: "element-internals-polyfill",
},
}, },
instance: {},
static: {}, static: {},
}; };
@@ -85,9 +49,7 @@ export default defineProvider(
if (polyfill && shouldInjectPolyfill(polyfill.desc.key)) { if (polyfill && shouldInjectPolyfill(polyfill.desc.key)) {
debug(polyfill.desc.key); debug(polyfill.desc.key);
utils.injectGlobalImport(polyfill.desc.module); utils.injectGlobalImport(polyfill.desc.module);
return true;
} }
return false;
}, },
}; };
} }

View File

@@ -3,8 +3,6 @@ const env = require("./env.cjs");
const paths = require("./paths.cjs"); const paths = require("./paths.cjs");
const { dependencies } = require("../package.json"); const { dependencies } = require("../package.json");
const BABEL_PLUGINS = path.join(__dirname, "babel-plugins");
// GitHub base URL to use for production source maps // 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 // Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version
module.exports.sourceMapURL = () => { module.exports.sourceMapURL = () => {
@@ -102,12 +100,22 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
], ],
plugins: [ plugins: [
[ [
path.join(BABEL_PLUGINS, "inline-constants-plugin.cjs"), path.resolve(
paths.polymer_dir,
"build-scripts/babel-plugins/inline-constants-plugin.cjs"
),
{ {
modules: ["@mdi/js"], modules: ["@mdi/js"],
ignoreModuleNotFound: true, ignoreModuleNotFound: true,
}, },
], ],
[
path.resolve(
paths.polymer_dir,
"build-scripts/babel-plugins/custom-polyfill-plugin.js"
),
{ method: "usage-global" },
],
// Minify template literals for production // Minify template literals for production
isProdBuild && [ isProdBuild && [
"template-html-minifier", "template-html-minifier",
@@ -145,26 +153,6 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
], ],
sourceMaps: !isTestBuild, sourceMaps: !isTestBuild,
overrides: [ 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: [
path.join(paths.polymer_dir, "src/resources/polyfills"),
...[
"@lit-labs/virtualizer/polyfills",
"@webcomponents/scoped-custom-element-registry",
"element-internals-polyfill",
"proxy-polyfill",
"unfetch",
].map((p) => new RegExp(`/node_modules/${p}/`)),
],
},
{ {
// Use unambiguous for dependencies so that require() is correctly injected into CommonJS files // 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 // Exclusions are needed in some cases where ES modules have no static imports or exports, such as polyfills

View File

@@ -9,7 +9,7 @@ import gulp from "gulp";
import jszip from "jszip"; import jszip from "jszip";
import path from "path"; import path from "path";
import process from "process"; import process from "process";
import { extract } from "tar"; import tar from "tar";
const MAX_AGE = 24; // hours const MAX_AGE = 24; // hours
const OWNER = "home-assistant"; const OWNER = "home-assistant";
@@ -156,7 +156,7 @@ gulp.task("fetch-nightly-translations", async function () {
console.log("Unpacking downloaded translations..."); console.log("Unpacking downloaded translations...");
const zip = await jszip.loadAsync(downloadResponse.data); const zip = await jszip.loadAsync(downloadResponse.data);
await deleteCurrent; await deleteCurrent;
const extractStream = zip.file(/.*/)[0].nodeStream().pipe(extract()); const extractStream = zip.file(/.*/)[0].nodeStream().pipe(tar.extract());
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
extractStream.on("close", resolve).on("error", reject); extractStream.on("close", resolve).on("error", reject);
}); });

View File

@@ -1,111 +1,92 @@
/* eslint-disable max-classes-per-file */ import { createHash } from "crypto";
import { deleteSync } from "del";
import { deleteAsync } from "del"; import { mkdirSync, readdirSync, readFileSync, renameSync } from "fs";
import { glob } from "glob"; import { writeFile } from "node:fs/promises";
import gulp from "gulp"; 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 rename from "gulp-rename";
import merge from "lodash.merge"; import path from "path";
import { createHash } from "node:crypto"; import vinylBuffer from "vinyl-buffer";
import { mkdir, readFile } from "node:fs/promises"; import source from "vinyl-source-stream";
import { basename, join } from "node:path";
import { PassThrough, Transform } from "node:stream";
import { finished } from "node:stream/promises";
import env from "../env.cjs"; import env from "../env.cjs";
import paths from "../paths.cjs"; import paths from "../paths.cjs";
import { mapFiles } from "../util.cjs";
import "./fetch-nightly-translations.js"; import "./fetch-nightly-translations.js";
const inFrontendDir = "translations/frontend"; const inFrontendDir = "translations/frontend";
const inBackendDir = "translations/backend"; const inBackendDir = "translations/backend";
const workDir = "build/translations"; const workDir = "build/translations";
const outDir = join(workDir, "output"); const fullDir = workDir + "/full";
const EN_SRC = join(paths.translations_src, "en.json"); const coreDir = workDir + "/core";
const outDir = workDir + "/output";
let mergeBackend = false; let mergeBackend = false;
gulp.task( gulp.task(
"translations-enable-merge-backend", "translations-enable-merge-backend",
gulp.parallel(async () => { gulp.parallel((done) => {
mergeBackend = true; mergeBackend = true;
done();
}, "allow-setup-fetch-nightly-translations") }, "allow-setup-fetch-nightly-translations")
); );
// Transform stream to apply a function on Vinyl JSON files (buffer mode only). // Panel translations which should be split from the core translations.
// The provided function can either return a new object, or an array of const TRANSLATION_FRAGMENTS = Object.keys(
// [object, subdirectory] pairs for fragmentizing the JSON. JSON.parse(
class CustomJSON extends Transform { readFileSync(
constructor(func, reviver = null) { path.resolve(paths.polymer_dir, "src/translations/en.json"),
super({ objectMode: true }); "utf-8"
this._func = func; )
this._reviver = reviver; ).ui.panel
} );
async _transform(file, _, callback) { function recursiveFlatten(prefix, data) {
try { let output = {};
let obj = JSON.parse(file.contents.toString(), this._reviver); Object.keys(data).forEach((key) => {
if (this._func) obj = this._func(obj, file.path); if (typeof data[key] === "object") {
for (const [outObj, dir] of Array.isArray(obj) ? obj : [[obj, ""]]) { output = {
const outFile = file.clone({ contents: false }); ...output,
outFile.contents = Buffer.from(JSON.stringify(outObj)); ...recursiveFlatten(prefix + key + ".", data[key]),
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 { } else {
output[prefix + key] = value; output[prefix + key] = data[key];
} }
} });
return output; return output;
}; }
// Filter functions that can be passed directly to JSON.parse() function flatten(data) {
const emptyReviver = (_key, value) => value || undefined; return recursiveFlatten("", data);
const testReviver = (_key, value) => }
value && typeof value === "string" ? "TRANSLATED" : value;
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;
}
/** /**
* Replace Lokalise key placeholders with their actual values. * Replace Lokalise key placeholders with their actual values.
@@ -114,44 +95,60 @@ const testReviver = (_key, value) =>
* be included in src/translations/en.json, but still be usable while * be included in src/translations/en.json, but still be usable while
* developing locally. * developing locally.
* *
* @link https://docs.lokalise.com/en/articles/1400528-key-referencing * @link https://docs.lokalise.co/article/KO5SZWLLsy-key-referencing
*/ */
const KEY_REFERENCE = /\[%key:([^%]+)%\]/; const re_key_reference = /\[%key:([^%]+)%\]/;
const lokaliseTransform = (data, path, original = data) => { function lokaliseTransform(data, original, file) {
const output = {}; const output = {};
for (const [key, value] of Object.entries(data)) { Object.entries(data).forEach(([key, value]) => {
if (typeof value === "object") { if (value instanceof Object) {
output[key] = lokaliseTransform(value, path, original); output[key] = lokaliseTransform(value, original, file);
} else { } else {
output[key] = value.replace(KEY_REFERENCE, (_match, lokalise_key) => { output[key] = value.replace(re_key_reference, (_match, lokalise_key) => {
const replace = lokalise_key.split("::").reduce((tr, k) => { const replace = lokalise_key.split("::").reduce((tr, k) => {
if (!tr) { if (!tr) {
throw Error(`Invalid key placeholder ${lokalise_key} in ${path}`); throw Error(
`Invalid key placeholder ${lokalise_key} in ${file.path}`
);
} }
return tr[k]; return tr[k];
}, original); }, original);
if (typeof replace !== "string") { if (typeof replace !== "string") {
throw Error(`Invalid key placeholder ${lokalise_key} in ${path}`); throw Error(
`Invalid key placeholder ${lokalise_key} in ${file.path}`
);
} }
return replace; return replace;
}); });
} }
} });
return output; return output;
}; }
gulp.task("clean-translations", () => deleteAsync([workDir])); gulp.task("clean-translations", async () => deleteSync([workDir]));
const makeWorkDir = () => mkdir(workDir, { recursive: true }); gulp.task("ensure-translations-build-dir", async () => {
mkdirSync(workDir, { recursive: true });
});
const createTestTranslation = () => gulp.task("create-test-metadata", () =>
env.isProdBuild()
? Promise.resolve()
: writeFile(
workDir + "/testMetadata.json",
JSON.stringify({ test: { nativeName: "Test" } })
)
);
gulp.task("create-test-translation", () =>
env.isProdBuild() env.isProdBuild()
? Promise.resolve() ? Promise.resolve()
: gulp : gulp
.src(EN_SRC) .src(path.join(paths.translations_src, "en.json"))
.pipe(new CustomJSON(null, testReviver)) .pipe(transform((data, _file) => recursiveEmpty(data)))
.pipe(rename("test.json")) .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 * This task will build a master translation file, to be used as the base for
@@ -162,164 +159,279 @@ const createTestTranslation = () =>
* project is buildable immediately after merging new translation keys, since * project is buildable immediately after merging new translation keys, since
* the Lokalise update to translations/en.json will not happen immediately. * the Lokalise update to translations/en.json will not happen immediately.
*/ */
const createMasterTranslation = () => gulp.task("build-master-translation", () => {
gulp const src = [path.join(paths.translations_src, "en.json")];
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])])
.pipe(new CustomJSON(lokaliseTransform))
.pipe(new MergeJSON("en"))
.pipe(gulp.dest(workDir));
const FRAGMENTS = ["base"]; if (mergeBackend) {
src.push(path.join(inBackendDir, "en.json"));
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));
} }
// The downstream pipeline is setup first. It hashes the merged data for return gulp
// each locale, then fragmentizes and flattens the data for final output. .src(src)
const translationFiles = await glob([ .pipe(transform((data, file) => lokaliseTransform(data, data, file)))
`${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( .pipe(
new CustomJSON((data) => merge({
FRAGMENTS.map((fragment) => { fileName: "en.json",
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,
];
}
})
)
)
.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`);
}
}
}
const mergeStream = gulp
.src(mergeFiles, { allowEmpty: true })
.pipe(new MergeJSON(locale, enMaster, emptyReviver));
mergesFinished.push(finished(mergeStream));
mergeStream.pipe(hashStream, { end: false });
}
// 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);
};
const writeTranslationMetaData = () =>
gulp
.src([`${paths.translations_src}/translationMetadata.json`])
.pipe(
new CustomJSON((meta) => {
// Add the test translation in development.
if (!env.isProdBuild()) {
meta.test = { nativeName: "Test" };
}
// 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 locale ${locale} because native name is not translated.`
);
} else {
meta[locale].hash = HASHES.get(locale);
}
}
return {
fragments: FRAGMENTS.filter(panelFragment),
translations: meta,
};
}) })
) )
.pipe(gulp.dest(workDir)); .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");
}
}
}
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 = "";
}
// 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 stream = source("translationFingerprints.json");
stream.write(JSON.stringify(fingerprints));
process.nextTick(() => stream.end());
return stream.pipe(vinylBuffer()).pipe(gulp.dest(workDir));
});
gulp.task("build-translation-fragment-supervisor", () =>
gulp
.src(fullDir + "/*.json")
.pipe(transform((data) => data.supervisor))
.pipe(
rename((filePath) => {
// In dev we create the file with the fake hash in the filename
if (!env.isProdBuild()) {
filePath.basename += "-dev";
}
})
)
.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 {
console.warn(
`Skipping language ${key}. Native name was not translated.`
);
}
});
return newData;
})
)
.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"
)
);
gulp.task( gulp.task(
"build-translations", "build-translations",
gulp.series( gulp.series(
gulp.parallel( gulp.parallel(
"fetch-nightly-translations", "fetch-nightly-translations",
gulp.series("clean-translations", makeWorkDir) gulp.series("clean-translations", "ensure-translations-build-dir")
), ),
createTestTranslation, "create-translations",
createMasterTranslation, "build-translation-fingerprints",
createTranslations, "build-translation-write-metadata"
writeTranslationMetaData
) )
); );
gulp.task( gulp.task(
"build-supervisor-translations", "build-supervisor-translations",
gulp.series(toggleSupervisorFragment, "build-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"
)
); );

View File

@@ -99,7 +99,7 @@ gulp.task("webpack-watch-app", () => {
).watch({ poll: isWsl }, doneHandler()); ).watch({ poll: isWsl }, doneHandler());
gulp.watch( gulp.watch(
path.join(paths.translations_src, "en.json"), path.join(paths.translations_src, "en.json"),
gulp.series("build-translations", "copy-translations-app") gulp.series("create-translations", "copy-translations-app")
); );
}); });

16
build-scripts/util.cjs Normal file
View File

@@ -0,0 +1,16 @@
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);
}
}
};

View File

@@ -10,7 +10,6 @@ const WebpackBar = require("webpackbar");
const { const {
TransformAsyncModulesPlugin, TransformAsyncModulesPlugin,
} = require("transform-async-modules-webpack-plugin"); } = require("transform-async-modules-webpack-plugin");
const { dependencies } = require("../package.json");
const paths = require("./paths.cjs"); const paths = require("./paths.cjs");
const bundle = require("./bundle.cjs"); const bundle = require("./bundle.cjs");
@@ -157,15 +156,11 @@ const createWebpackConfig = ({
transform: (stats) => JSON.stringify(filterStats(stats)), transform: (stats) => JSON.stringify(filterStats(stats)),
}), }),
!latestBuild && !latestBuild &&
new TransformAsyncModulesPlugin({ new TransformAsyncModulesPlugin({ browserslistEnv: "legacy" }),
browserslistEnv: "legacy",
runtime: { version: dependencies["@babel/runtime"] },
}),
].filter(Boolean), ].filter(Boolean),
resolve: { resolve: {
extensions: [".ts", ".js", ".json"], extensions: [".ts", ".js", ".json"],
alias: { alias: {
"lit/static-html$": "lit/static-html.js",
"lit/decorators$": "lit/decorators.js", "lit/decorators$": "lit/decorators.js",
"lit/directive$": "lit/directive.js", "lit/directive$": "lit/directive.js",
"lit/directives/until$": "lit/directives/until.js", "lit/directives/until$": "lit/directives/until.js",

View File

@@ -10,7 +10,6 @@ import {
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant"; import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
import { HomeAssistant } from "../../src/types"; import { HomeAssistant } from "../../src/types";
import { selectedDemoConfig } from "./configs/demo-configs"; import { selectedDemoConfig } from "./configs/demo-configs";
import { mockAreaRegistry } from "./stubs/area_registry";
import { mockAuth } from "./stubs/auth"; import { mockAuth } from "./stubs/auth";
import { mockConfigEntries } from "./stubs/config_entries"; import { mockConfigEntries } from "./stubs/config_entries";
import { mockEnergy } from "./stubs/energy"; import { mockEnergy } from "./stubs/energy";
@@ -24,10 +23,10 @@ import { mockLovelace } from "./stubs/lovelace";
import { mockMediaPlayer } from "./stubs/media_player"; import { mockMediaPlayer } from "./stubs/media_player";
import { mockPersistentNotification } from "./stubs/persistent_notification"; import { mockPersistentNotification } from "./stubs/persistent_notification";
import { mockRecorder } from "./stubs/recorder"; import { mockRecorder } from "./stubs/recorder";
import { mockTodo } from "./stubs/todo";
import { mockSensor } from "./stubs/sensor"; import { mockSensor } from "./stubs/sensor";
import { mockSystemLog } from "./stubs/system_log"; import { mockSystemLog } from "./stubs/system_log";
import { mockTemplate } from "./stubs/template"; import { mockTemplate } from "./stubs/template";
import { mockTodo } from "./stubs/todo";
import { mockTranslations } from "./stubs/translations"; import { mockTranslations } from "./stubs/translations";
@customElement("ha-demo") @customElement("ha-demo")
@@ -63,7 +62,6 @@ export class HaDemo extends HomeAssistantAppEl {
mockEnergy(hass); mockEnergy(hass);
mockPersistentNotification(hass); mockPersistentNotification(hass);
mockConfigEntries(hass); mockConfigEntries(hass);
mockAreaRegistry(hass);
mockEntityRegistry(hass, [ mockEntityRegistry(hass, [
{ {
config_entry_id: "co2signal", config_entry_id: "co2signal",

View File

@@ -1,4 +1,4 @@
import { format, startOfToday, startOfTomorrow } from "date-fns"; import { format, startOfToday, startOfTomorrow } from "date-fns/esm";
import { import {
EnergyInfo, EnergyInfo,
EnergyPreferences, EnergyPreferences,

View File

@@ -64,12 +64,6 @@ const ACTIONS = [
entity_id: "input_boolean.toggle_4", entity_id: "input_boolean.toggle_4",
}, },
}, },
{
sequence: [
{ scene: "scene.kitchen_morning" },
{ service: "light.turn_off", target: { entity_id: "light.kitchen" } },
],
},
{ {
parallel: [ parallel: [
{ scene: "scene.kitchen_morning" }, { scene: "scene.kitchen_morning" },
@@ -142,7 +136,7 @@ export class DemoAutomationDescribeAction extends LitElement {
<div class="action"> <div class="action">
<span> <span>
${this._action ${this._action
? describeAction(this.hass, [], [], [], this._action) ? describeAction(this.hass, [], this._action)
: "<invalid YAML>"} : "<invalid YAML>"}
</span> </span>
<ha-yaml-editor <ha-yaml-editor
@@ -155,7 +149,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${ACTIONS.map( ${ACTIONS.map(
(conf) => html` (conf) => html`
<div class="action"> <div class="action">
<span>${describeAction(this.hass, [], [], [], conf as any)}</span> <span>${describeAction(this.hass, [], conf as any)}</span>
<pre>${dump(conf)}</pre> <pre>${dump(conf)}</pre>
</div> </div>
` `

View File

@@ -20,7 +20,6 @@ import { HaWaitForTriggerAction } from "../../../../src/panels/config/automation
import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template"; import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template";
import { Action } from "../../../../src/data/script"; import { Action } from "../../../../src/data/script";
import { HaConditionAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-condition"; import { HaConditionAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-condition";
import { HaSequenceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-sequence";
import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel"; import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel";
import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if"; import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if";
import { HaStopAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-stop"; import { HaStopAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-stop";
@@ -40,7 +39,6 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [
{ name: "If-Then", actions: [HaIfAction.defaultConfig] }, { name: "If-Then", actions: [HaIfAction.defaultConfig] },
{ name: "Choose", actions: [HaChooseAction.defaultConfig] }, { name: "Choose", actions: [HaChooseAction.defaultConfig] },
{ name: "Variables", actions: [{ variables: { hello: "1" } }] }, { name: "Variables", actions: [{ variables: { hello: "1" } }] },
{ name: "Sequence", actions: [HaSequenceAction.defaultConfig] },
{ name: "Parallel", actions: [HaParallelAction.defaultConfig] }, { name: "Parallel", actions: [HaParallelAction.defaultConfig] },
{ name: "Stop", actions: [HaStopAction.defaultConfig] }, { name: "Stop", actions: [HaStopAction.defaultConfig] },
]; ];

View File

@@ -187,7 +187,7 @@ export class DemoHaControlSelect extends LitElement {
--mdc-icon-size: 24px; --mdc-icon-size: 24px;
--control-select-color: var(--state-fan-active-color); --control-select-color: var(--state-fan-active-color);
--control-select-thickness: 130px; --control-select-thickness: 130px;
--control-select-border-radius: 36px; --control-select-border-radius: 48px;
} }
.vertical-selects { .vertical-selects {
height: 300px; height: 300px;

View File

@@ -151,7 +151,7 @@ export class DemoHaBarSlider extends LitElement {
--control-slider-background: #ffcf4c; --control-slider-background: #ffcf4c;
--control-slider-background-opacity: 0.2; --control-slider-background-opacity: 0.2;
--control-slider-thickness: 130px; --control-slider-thickness: 130px;
--control-slider-border-radius: 36px; --control-slider-border-radius: 48px;
} }
.vertical-sliders { .vertical-sliders {
height: 300px; height: 300px;

View File

@@ -118,7 +118,7 @@ export class DemoHaControlSwitch extends LitElement {
--control-switch-on-color: var(--green-color); --control-switch-on-color: var(--green-color);
--control-switch-off-color: var(--red-color); --control-switch-off-color: var(--red-color);
--control-switch-thickness: 130px; --control-switch-thickness: 130px;
--control-switch-border-radius: 36px; --control-switch-border-radius: 48px;
--control-switch-padding: 6px; --control-switch-padding: 6px;
--mdc-icon-size: 24px; --mdc-icon-size: 24px;
} }

View File

@@ -161,14 +161,12 @@ const LABELS: LabelRegistryEntry[] = [
name: "Energy", name: "Energy",
icon: null, icon: null,
color: "yellow", color: "yellow",
description: null,
}, },
{ {
label_id: "entertainment", label_id: "entertainment",
name: "Entertainment", name: "Entertainment",
icon: "mdi:popcorn", icon: "mdi:popcorn",
color: "blue", color: "blue",
description: null,
}, },
]; ];

View File

@@ -2,7 +2,6 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, query } from "lit/decorators"; import { customElement, query } from "lit/decorators";
import { CoverEntityFeature } from "../../../../src/data/cover"; import { CoverEntityFeature } from "../../../../src/data/cover";
import { LightColorMode } from "../../../../src/data/light"; import { LightColorMode } from "../../../../src/data/light";
import { LockEntityFeature } from "../../../../src/data/lock";
import { VacuumEntityFeature } from "../../../../src/data/vacuum"; import { VacuumEntityFeature } from "../../../../src/data/vacuum";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass"; import { provideHass } from "../../../../src/fake_data/provide_hass";
@@ -21,11 +20,6 @@ const ENTITIES = [
getEntity("light", "unavailable", "unavailable", { getEntity("light", "unavailable", "unavailable", {
friendly_name: "Unavailable entity", 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", { getEntity("climate", "thermostat", "heat", {
current_temperature: 73, current_temperature: 73,
min_temp: 45, min_temp: 45,
@@ -144,24 +138,6 @@ const CONFIGS = [
- type: "color-temp" - 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", heading: "Vacuum commands feature",
config: ` config: `

View File

@@ -368,7 +368,6 @@ export class DemoEntityState extends LitElement {
hass.localize, hass.localize,
entry.stateObj, entry.stateObj,
hass.locale, hass.locale,
[], // numericDeviceClasses
hass.config, hass.config,
hass.entities hass.entities
)}`, )}`,

View File

@@ -36,8 +36,6 @@ const createConfigEntry = (
pref_disable_new_entities: false, pref_disable_new_entities: false,
pref_disable_polling: false, pref_disable_polling: false,
reason: null, reason: null,
error_reason_translation_key: null,
error_reason_translation_placeholders: null,
...override, ...override,
}); });

View File

@@ -1,7 +1,4 @@
import { globIterate } from "glob"; import { globIterate } from "glob";
import { availableParallelism } from "node:os";
process.env.UV_THREADPOOL_SIZE = availableParallelism();
const gulpImports = []; const gulpImports = [];

View File

@@ -1,19 +1,19 @@
import { mdiRefresh, mdiStorePlus } from "@mdi/js"; import { mdiStorePlus, mdiUpdate } from "@mdi/js";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version"; import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-fab"; 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 { 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 "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
import { supervisorTabs } from "../hassio-tabs"; import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addons"; 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") @customElement("hassio-dashboard")
class HassioDashboard extends LitElement { class HassioDashboard extends LitElement {
@@ -43,7 +43,7 @@ class HassioDashboard extends LitElement {
<ha-icon-button <ha-icon-button
slot="toolbar-icon" slot="toolbar-icon"
@click=${this._handleCheckUpdates} @click=${this._handleCheckUpdates}
.path=${mdiRefresh} .path=${mdiUpdate}
.label=${this.supervisor.localize("store.check_updates")} .label=${this.supervisor.localize("store.check_updates")}
></ha-icon-button> ></ha-icon-button>
<hassio-addons <hassio-addons

View File

@@ -25,30 +25,30 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@babel/runtime": "7.24.5", "@babel/runtime": "7.24.1",
"@braintree/sanitize-url": "7.0.2", "@braintree/sanitize-url": "7.0.1",
"@codemirror/autocomplete": "6.16.0", "@codemirror/autocomplete": "6.15.0",
"@codemirror/commands": "6.5.0", "@codemirror/commands": "6.3.3",
"@codemirror/language": "6.10.1", "@codemirror/language": "6.10.1",
"@codemirror/legacy-modes": "6.4.0", "@codemirror/legacy-modes": "6.3.3",
"@codemirror/search": "6.5.6", "@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1", "@codemirror/state": "6.4.1",
"@codemirror/view": "6.26.3", "@codemirror/view": "6.26.1",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.5", "@formatjs/intl-datetimeformat": "6.12.3",
"@formatjs/intl-displaynames": "6.6.8", "@formatjs/intl-displaynames": "6.6.6",
"@formatjs/intl-getcanonicallocales": "2.3.0", "@formatjs/intl-getcanonicallocales": "2.3.0",
"@formatjs/intl-listformat": "7.5.7", "@formatjs/intl-listformat": "7.5.5",
"@formatjs/intl-locale": "4.0.0", "@formatjs/intl-locale": "3.4.5",
"@formatjs/intl-numberformat": "8.10.3", "@formatjs/intl-numberformat": "8.10.1",
"@formatjs/intl-pluralrules": "5.2.14", "@formatjs/intl-pluralrules": "5.2.12",
"@formatjs/intl-relativetimeformat": "11.2.14", "@formatjs/intl-relativetimeformat": "11.2.12",
"@fullcalendar/core": "6.1.13", "@fullcalendar/core": "6.1.11",
"@fullcalendar/daygrid": "6.1.13", "@fullcalendar/daygrid": "6.1.11",
"@fullcalendar/interaction": "6.1.13", "@fullcalendar/interaction": "6.1.11",
"@fullcalendar/list": "6.1.13", "@fullcalendar/list": "6.1.11",
"@fullcalendar/luxon3": "6.1.13", "@fullcalendar/luxon3": "6.1.11",
"@fullcalendar/timegrid": "6.1.13", "@fullcalendar/timegrid": "6.1.11",
"@lezer/highlight": "1.2.0", "@lezer/highlight": "1.2.0",
"@lit-labs/context": "0.4.1", "@lit-labs/context": "0.4.1",
"@lit-labs/motion": "1.0.7", "@lit-labs/motion": "1.0.7",
@@ -70,6 +70,7 @@
"@material/mwc-list": "0.27.0", "@material/mwc-list": "0.27.0",
"@material/mwc-menu": "0.27.0", "@material/mwc-menu": "0.27.0",
"@material/mwc-radio": "0.27.0", "@material/mwc-radio": "0.27.0",
"@material/mwc-ripple": "0.27.0",
"@material/mwc-select": "0.27.0", "@material/mwc-select": "0.27.0",
"@material/mwc-snackbar": "0.27.0", "@material/mwc-snackbar": "0.27.0",
"@material/mwc-switch": "0.27.0", "@material/mwc-switch": "0.27.0",
@@ -80,7 +81,7 @@
"@material/mwc-top-app-bar": "0.27.0", "@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0", "@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0", "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "1.4.1", "@material/web": "=1.3.0",
"@mdi/js": "7.4.47", "@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47", "@mdi/svg": "7.4.47",
"@polymer/paper-item": "3.0.1", "@polymer/paper-item": "3.0.1",
@@ -88,8 +89,8 @@
"@polymer/paper-tabs": "3.1.0", "@polymer/paper-tabs": "3.1.0",
"@polymer/polymer": "3.5.1", "@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0", "@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.3.13", "@vaadin/combo-box": "24.3.10",
"@vaadin/vaadin-themable-mixin": "24.3.13", "@vaadin/vaadin-themable-mixin": "24.3.10",
"@vibrant/color": "3.2.1-alpha.1", "@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1", "@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -97,28 +98,28 @@
"@webcomponents/scoped-custom-element-registry": "0.0.9", "@webcomponents/scoped-custom-element-registry": "0.0.9",
"@webcomponents/webcomponentsjs": "2.8.0", "@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1", "app-datepicker": "5.1.1",
"chart.js": "4.4.3", "chart.js": "4.4.2",
"color-name": "2.0.0", "color-name": "2.0.0",
"comlink": "4.4.1", "comlink": "4.4.1",
"core-js": "3.37.1", "core-js": "3.36.1",
"cropperjs": "1.6.2", "cropperjs": "1.6.1",
"date-fns": "3.6.0", "date-fns": "2.30.0",
"date-fns-tz": "3.1.3", "date-fns-tz": "2.0.1",
"deep-clone-simple": "1.1.1", "deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
"element-internals-polyfill": "1.3.11", "element-internals-polyfill": "1.3.10",
"fuse.js": "7.0.0", "fuse.js": "7.0.0",
"google-timezones-json": "1.2.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", "hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.3.0", "home-assistant-js-websocket": "9.2.1",
"idb-keyval": "6.2.1", "idb-keyval": "6.2.1",
"intl-messageformat": "10.5.14", "intl-messageformat": "10.5.11",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"leaflet": "1.9.4", "leaflet": "1.9.4",
"leaflet-draw": "1.0.4", "leaflet-draw": "1.0.4",
"lit": "2.8.0", "lit": "2.8.0",
"luxon": "3.4.4", "luxon": "3.4.4",
"marked": "12.0.2", "marked": "12.0.1",
"memoize-one": "6.0.0", "memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1", "node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2", "proxy-polyfill": "0.3.2",
@@ -140,58 +141,57 @@
"vue": "2.7.16", "vue": "2.7.16",
"vue2-daterange-picker": "0.6.8", "vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0", "weekstart": "2.0.0",
"workbox-cacheable-response": "7.1.0", "workbox-cacheable-response": "7.0.0",
"workbox-core": "7.1.0", "workbox-core": "7.0.0",
"workbox-expiration": "7.1.0", "workbox-expiration": "7.0.0",
"workbox-precaching": "7.1.0", "workbox-precaching": "7.0.0",
"workbox-routing": "7.1.0", "workbox-routing": "7.0.0",
"workbox-strategies": "7.1.0", "workbox-strategies": "7.0.0",
"xss": "1.0.15" "xss": "1.0.15"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.24.5", "@babel/core": "7.24.3",
"@babel/helper-define-polyfill-provider": "0.6.2", "@babel/helper-define-polyfill-provider": "0.6.1",
"@babel/plugin-proposal-decorators": "7.24.1", "@babel/plugin-proposal-decorators": "7.24.1",
"@babel/plugin-transform-runtime": "7.24.3", "@babel/plugin-transform-runtime": "7.24.3",
"@babel/preset-env": "7.24.5", "@babel/preset-env": "7.24.3",
"@babel/preset-typescript": "7.24.1", "@babel/preset-typescript": "7.24.1",
"@bundle-stats/plugin-webpack-filter": "4.12.2", "@bundle-stats/plugin-webpack-filter": "4.12.2",
"@koa/cors": "5.0.0", "@koa/cors": "5.0.0",
"@lokalise/node-api": "12.5.0", "@lokalise/node-api": "12.3.0",
"@octokit/auth-oauth-device": "7.1.1", "@octokit/auth-oauth-device": "7.0.1",
"@octokit/plugin-retry": "7.1.1", "@octokit/plugin-retry": "7.0.3",
"@octokit/rest": "20.1.1", "@octokit/rest": "20.0.2",
"@open-wc/dev-server-hmr": "0.1.4", "@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.4", "@rollup/plugin-babel": "6.0.4",
"@rollup/plugin-commonjs": "25.0.8", "@rollup/plugin-commonjs": "25.0.7",
"@rollup/plugin-json": "6.1.0", "@rollup/plugin-json": "6.1.0",
"@rollup/plugin-node-resolve": "15.2.3", "@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-replace": "5.0.5", "@rollup/plugin-replace": "5.0.5",
"@types/babel__plugin-transform-runtime": "7.9.5", "@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.14", "@types/chromecast-caf-receiver": "6.0.13",
"@types/chromecast-caf-sender": "1.0.10", "@types/chromecast-caf-sender": "1.0.9",
"@types/color-name": "1.1.4", "@types/color-name": "1.1.3",
"@types/glob": "8.1.0", "@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.2", "@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.12", "@types/leaflet": "1.9.8",
"@types/leaflet-draw": "1.0.11", "@types/leaflet-draw": "1.0.11",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.4.2", "@types/luxon": "3.4.2",
"@types/mocha": "10.0.6", "@types/mocha": "10.0.6",
"@types/qrcode": "1.5.5", "@types/qrcode": "1.5.5",
"@types/serve-handler": "6.1.4", "@types/serve-handler": "6.1.4",
"@types/sortablejs": "1.15.8", "@types/sortablejs": "1.15.8",
"@types/tar": "6.1.13", "@types/tar": "6.1.11",
"@types/ua-parser-js": "0.7.39", "@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29", "@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "7.10.0", "@typescript-eslint/eslint-plugin": "7.4.0",
"@typescript-eslint/parser": "7.10.0", "@typescript-eslint/parser": "7.4.0",
"@web/dev-server": "0.1.38", "@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1", "@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3", "babel-loader": "9.1.3",
"babel-plugin-template-html-minifier": "4.1.0", "babel-plugin-template-html-minifier": "4.1.0",
"chai": "5.1.1", "chai": "5.1.0",
"del": "7.1.0", "del": "7.1.0",
"eslint": "8.57.0", "eslint": "8.57.0",
"eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-base": "15.0.0",
@@ -200,28 +200,29 @@
"eslint-import-resolver-webpack": "0.13.8", "eslint-import-resolver-webpack": "0.13.8",
"eslint-plugin-disable": "2.0.3", "eslint-plugin-disable": "2.0.3",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.29.1",
"eslint-plugin-lit": "1.13.0", "eslint-plugin-lit": "1.11.0",
"eslint-plugin-lit-a11y": "4.1.2", "eslint-plugin-lit-a11y": "4.1.2",
"eslint-plugin-unused-imports": "3.2.0", "eslint-plugin-unused-imports": "3.1.0",
"eslint-plugin-wc": "2.1.0", "eslint-plugin-wc": "2.0.4",
"fancy-log": "2.0.0", "fancy-log": "2.0.0",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"glob": "10.4.1", "glob": "10.3.10",
"gulp": "5.0.0", "gulp": "4.0.2",
"gulp-flatmap": "1.0.2",
"gulp-json-transform": "0.5.0", "gulp-json-transform": "0.5.0",
"gulp-merge-json": "2.2.1",
"gulp-rename": "2.0.0", "gulp-rename": "2.0.0",
"gulp-zopfli-green": "6.0.1", "gulp-zopfli-green": "6.0.1",
"html-minifier-terser": "7.2.0", "html-minifier-terser": "7.2.0",
"husky": "9.0.11", "husky": "9.0.11",
"instant-mocha": "1.5.2", "instant-mocha": "1.5.2",
"jszip": "3.10.1", "jszip": "3.10.1",
"lint-staged": "15.2.4", "lint-staged": "15.2.2",
"lit-analyzer": "2.0.3", "lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0", "lodash.template": "4.5.0",
"magic-string": "0.30.10", "magic-string": "0.30.8",
"map-stream": "0.0.7", "map-stream": "0.0.7",
"mocha": "10.4.0", "mocha": "10.3.0",
"object-hash": "3.0.0", "object-hash": "3.0.0",
"open": "10.1.0", "open": "10.1.0",
"pinst": "3.0.0", "pinst": "3.0.0",
@@ -231,21 +232,23 @@
"rollup-plugin-terser": "7.0.2", "rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.12.0", "rollup-plugin-visualizer": "5.12.0",
"serve-handler": "6.1.5", "serve-handler": "6.1.5",
"sinon": "18.0.0", "sinon": "17.0.1",
"source-map-url": "0.4.1", "source-map-url": "0.4.1",
"systemjs": "6.15.1", "systemjs": "6.14.3",
"tar": "7.1.0", "tar": "6.2.1",
"terser-webpack-plugin": "5.3.10", "terser-webpack-plugin": "5.3.10",
"transform-async-modules-webpack-plugin": "1.1.1", "transform-async-modules-webpack-plugin": "1.0.4",
"ts-lit-plugin": "2.0.2", "ts-lit-plugin": "2.0.2",
"typescript": "5.4.5", "typescript": "5.4.3",
"vinyl-buffer": "1.0.1",
"vinyl-source-stream": "2.0.0",
"webpack": "5.91.0", "webpack": "5.91.0",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
"webpack-dev-server": "5.0.4", "webpack-dev-server": "5.0.4",
"webpack-manifest-plugin": "5.0.0", "webpack-manifest-plugin": "5.0.0",
"webpack-stats-plugin": "1.1.3", "webpack-stats-plugin": "1.1.3",
"webpackbar": "6.0.1", "webpackbar": "6.0.1",
"workbox-build": "7.1.0" "workbox-build": "7.0.0"
}, },
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch", "_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
"resolutions": { "resolutions": {
@@ -257,5 +260,5 @@
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch", "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" "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.2.2" "packageManager": "yarn@4.1.1"
} }

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20240501.0" version = "20240402.0"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@@ -40,11 +40,6 @@
"matchPackageNames": ["tsparticles-engine"], "matchPackageNames": ["tsparticles-engine"],
"matchPackagePrefixes": ["tsparticles-preset-"] "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", "description": "Group and temporarily disable WDS packages",
"groupName": "Web Dev Server", "groupName": "Web Dev Server",

View File

@@ -1,4 +1,4 @@
import { toZonedTime, fromZonedTime } from "date-fns-tz"; import { utcToZonedTime, zonedTimeToUtc } from "date-fns-tz";
import { HassConfig } from "home-assistant-js-websocket"; import { HassConfig } from "home-assistant-js-websocket";
import { FrontendLocaleData, TimeZone } from "../../data/translation"; import { FrontendLocaleData, TimeZone } from "../../data/translation";
@@ -8,10 +8,10 @@ const calcZonedDate = (
fn: (date: Date, options?: any) => Date | number | boolean, fn: (date: Date, options?: any) => Date | number | boolean,
options? options?
) => { ) => {
const inputZoned = toZonedTime(date, tz); const inputZoned = utcToZonedTime(date, tz);
const fnZoned = fn(inputZoned, options); const fnZoned = fn(inputZoned, options);
if (fnZoned instanceof Date) { if (fnZoned instanceof Date) {
return fromZonedTime(fnZoned, tz) as Date; return zonedTimeToUtc(fnZoned, tz) as Date;
} }
return fnZoned; return fnZoned;
}; };
@@ -51,6 +51,6 @@ export const calcDateDifferenceProperty = (
locale, locale,
config, config,
locale.time_zone === TimeZone.server locale.time_zone === TimeZone.server
? toZonedTime(startDate, config.time_zone) ? utcToZonedTime(startDate, config.time_zone)
: startDate : startDate
); );

View File

@@ -1,5 +1,3 @@
export type MediaQueriesListener = () => void;
/** /**
* Attach a media query. Listener is called right away and when it matches. * Attach a media query. Listener is called right away and when it matches.
* @param mediaQuery media query to match. * @param mediaQuery media query to match.
@@ -9,7 +7,7 @@ export type MediaQueriesListener = () => void;
export const listenMediaQuery = ( export const listenMediaQuery = (
mediaQuery: string, mediaQuery: string,
matchesChanged: (matches: boolean) => void matchesChanged: (matches: boolean) => void
): MediaQueriesListener => { ) => {
const mql = matchMedia(mediaQuery); const mql = matchMedia(mediaQuery);
const listener = (e) => matchesChanged(e.matches); const listener = (e) => matchesChanged(e.matches);
mql.addListener(listener); mql.addListener(listener);

View File

@@ -19,11 +19,28 @@ import { blankBeforeUnit } from "../translations/blank_before_unit";
import { LocalizeFunc } from "../translations/localize"; import { LocalizeFunc } from "../translations/localize";
import { computeDomain } from "./compute_domain"; import { computeDomain } from "./compute_domain";
export const computeStateDisplaySingleEntity = (
localize: LocalizeFunc,
stateObj: HassEntity,
locale: FrontendLocaleData,
config: HassConfig,
entity: EntityRegistryDisplayEntry | undefined,
state?: string
): string =>
computeStateDisplayFromEntityAttributes(
localize,
locale,
config,
entity,
stateObj.entity_id,
stateObj.attributes,
state !== undefined ? state : stateObj.state
);
export const computeStateDisplay = ( export const computeStateDisplay = (
localize: LocalizeFunc, localize: LocalizeFunc,
stateObj: HassEntity, stateObj: HassEntity,
locale: FrontendLocaleData, locale: FrontendLocaleData,
sensorNumericDeviceClasses: string[],
config: HassConfig, config: HassConfig,
entities: HomeAssistant["entities"], entities: HomeAssistant["entities"],
state?: string state?: string
@@ -35,7 +52,6 @@ export const computeStateDisplay = (
return computeStateDisplayFromEntityAttributes( return computeStateDisplayFromEntityAttributes(
localize, localize,
locale, locale,
sensorNumericDeviceClasses,
config, config,
entity, entity,
stateObj.entity_id, stateObj.entity_id,
@@ -47,7 +63,6 @@ export const computeStateDisplay = (
export const computeStateDisplayFromEntityAttributes = ( export const computeStateDisplayFromEntityAttributes = (
localize: LocalizeFunc, localize: LocalizeFunc,
locale: FrontendLocaleData, locale: FrontendLocaleData,
sensorNumericDeviceClasses: string[],
config: HassConfig, config: HassConfig,
entity: EntityRegistryDisplayEntry | undefined, entity: EntityRegistryDisplayEntry | undefined,
entityId: string, entityId: string,
@@ -58,15 +73,8 @@ export const computeStateDisplayFromEntityAttributes = (
return localize(`state.default.${state}`); return localize(`state.default.${state}`);
} }
const domain = computeDomain(entityId);
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber` // Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
if ( if (isNumericFromAttributes(attributes)) {
isNumericFromAttributes(
attributes,
domain === "sensor" ? sensorNumericDeviceClasses : []
)
) {
// state is duration // state is duration
if ( if (
attributes.device_class === "duration" && attributes.device_class === "duration" &&
@@ -112,6 +120,8 @@ export const computeStateDisplayFromEntityAttributes = (
return value; return value;
} }
const domain = computeDomain(entityId);
if (domain === "datetime") { if (domain === "datetime") {
const time = new Date(state); const time = new Date(state);
return formatDateTime(time, locale, config); return formatDateTime(time, locale, config);
@@ -177,14 +187,11 @@ export const computeStateDisplayFromEntityAttributes = (
if ( if (
[ [
"button", "button",
"conversation",
"event", "event",
"image", "image",
"input_button", "input_button",
"notify",
"scene", "scene",
"stt", "stt",
"tag",
"tts", "tts",
"wake_word", "wake_word",
].includes(domain) || ].includes(domain) ||

View File

@@ -28,15 +28,7 @@ export const FIXED_DOMAIN_STATES = {
input_button: [], input_button: [],
lawn_mower: ["error", "paused", "mowing", "docked"], lawn_mower: ["error", "paused", "mowing", "docked"],
light: ["on", "off"], light: ["on", "off"],
lock: [ lock: ["jammed", "locked", "locking", "unlocked", "unlocking"],
"jammed",
"locked",
"locking",
"unlocked",
"unlocking",
"opening",
"open",
],
media_player: [ media_player: [
"off", "off",
"on", "on",

View File

@@ -14,12 +14,8 @@ export const isNumericState = (stateObj: HassEntity): boolean =>
isNumericFromAttributes(stateObj.attributes); isNumericFromAttributes(stateObj.attributes);
export const isNumericFromAttributes = ( export const isNumericFromAttributes = (
attributes: HassEntityAttributeBase, attributes: HassEntityAttributeBase
numericDeviceClasses?: string[] ): boolean => !!attributes.unit_of_measurement || !!attributes.state_class;
): boolean =>
!!attributes.unit_of_measurement ||
!!attributes.state_class ||
(numericDeviceClasses || []).includes(attributes.device_class || "");
export const numberFormatToLocale = ( export const numberFormatToLocale = (
localeOptions: FrontendLocaleData localeOptions: FrontendLocaleData

View File

@@ -21,8 +21,7 @@ export const computeFormatFunctions = async (
localize: LocalizeFunc, localize: LocalizeFunc,
locale: FrontendLocaleData, locale: FrontendLocaleData,
config: HassConfig, config: HassConfig,
entities: HomeAssistant["entities"], entities: HomeAssistant["entities"]
sensorNumericDeviceClasses: string[]
): Promise<{ ): Promise<{
formatEntityState: FormatEntityStateFunc; formatEntityState: FormatEntityStateFunc;
formatEntityAttributeValue: FormatEntityAttributeValueFunc; formatEntityAttributeValue: FormatEntityAttributeValueFunc;
@@ -36,15 +35,7 @@ export const computeFormatFunctions = async (
return { return {
formatEntityState: (stateObj, state) => formatEntityState: (stateObj, state) =>
computeStateDisplay( computeStateDisplay(localize, stateObj, locale, config, entities, state),
localize,
stateObj,
locale,
sensorNumericDeviceClasses,
config,
entities,
state
),
formatEntityAttributeValue: (stateObj, attribute, value) => formatEntityAttributeValue: (stateObj, attribute, value) =>
computeAttributeValueDisplay( computeAttributeValueDisplay(
localize, localize,

View File

@@ -2,7 +2,6 @@ import IntlMessageFormat from "intl-messageformat";
import type { HTMLTemplateResult } from "lit"; import type { HTMLTemplateResult } from "lit";
import { polyfillLocaleData } from "../../resources/locale-data-polyfill"; import { polyfillLocaleData } from "../../resources/locale-data-polyfill";
import { Resources, TranslationDict } from "../../types"; import { Resources, TranslationDict } from "../../types";
import { fireEvent } from "../dom/fire_event";
// Exclude some patterns from key type checking for now // Exclude some patterns from key type checking for now
// These are intended to be removed as errors are fixed // These are intended to be removed as errors are fixed
@@ -82,9 +81,7 @@ export interface FormatsType {
*/ */
export const computeLocalize = async <Keys extends string = LocalizeKeys>( export const computeLocalize = async <Keys extends string = LocalizeKeys>(
cache: HTMLElement & { cache: any,
_localizationCache?: Record<string, IntlMessageFormat>;
},
language: string, language: string,
resources: Resources, resources: Resources,
formats?: FormatsType formats?: FormatsType
@@ -110,7 +107,7 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>(
} }
const messageKey = key + translatedValue; const messageKey = key + translatedValue;
let translatedMessage = cache._localizationCache![messageKey] as let translatedMessage = cache._localizationCache[messageKey] as
| IntlMessageFormat | IntlMessageFormat
| undefined; | undefined;
@@ -124,7 +121,7 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>(
} catch (err: any) { } catch (err: any) {
return "Translation error: " + err.message; return "Translation error: " + err.message;
} }
cache._localizationCache![messageKey] = translatedMessage; cache._localizationCache[messageKey] = translatedMessage;
} }
let argObject = {}; let argObject = {};
@@ -140,12 +137,6 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>(
try { try {
return translatedMessage.format<string>(argObject) as string; return translatedMessage.format<string>(argObject) as string;
} catch (err: any) { } 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; return "Translation " + err;
} }
}; };

View File

@@ -1,9 +0,0 @@
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[];

View File

@@ -1,4 +1,4 @@
import { differenceInDays, differenceInWeeks, startOfWeek } from "date-fns"; import { differenceInDays, differenceInWeeks, startOfWeek } from "date-fns/esm";
import { FrontendLocaleData } from "../../data/translation"; import { FrontendLocaleData } from "../../data/translation";
import { firstWeekdayIndex } from "../datetime/first_weekday"; import { firstWeekdayIndex } from "../datetime/first_weekday";

View File

@@ -34,7 +34,7 @@ import {
endOfMonth, endOfMonth,
endOfQuarter, endOfQuarter,
endOfYear, endOfYear,
} from "date-fns"; } from "date-fns/esm";
import { import {
formatDate, formatDate,
formatDateMonth, formatDateMonth,

View File

@@ -313,38 +313,31 @@ export class HaChartBase extends LitElement {
`; `;
} }
private _loading = false;
private async _setupChart() { private async _setupChart() {
if (this._loading) return;
const ctx: CanvasRenderingContext2D = this.renderRoot const ctx: CanvasRenderingContext2D = this.renderRoot
.querySelector("canvas")! .querySelector("canvas")!
.getContext("2d")!; .getContext("2d")!;
this._loading = true;
try {
const ChartConstructor = (await import("../../resources/chartjs")).Chart;
const computedStyles = getComputedStyle(this); const ChartConstructor = (await import("../../resources/chartjs")).Chart;
ChartConstructor.defaults.borderColor = const computedStyles = getComputedStyle(this);
computedStyles.getPropertyValue("--divider-color");
ChartConstructor.defaults.color = computedStyles.getPropertyValue(
"--secondary-text-color"
);
ChartConstructor.defaults.font.family =
computedStyles.getPropertyValue("--mdc-typography-body1-font-family") ||
computedStyles.getPropertyValue("--mdc-typography-font-family") ||
"Roboto, Noto, sans-serif";
this.chart = new ChartConstructor(ctx, { ChartConstructor.defaults.borderColor =
type: this.chartType, computedStyles.getPropertyValue("--divider-color");
data: this.data, ChartConstructor.defaults.color = computedStyles.getPropertyValue(
options: this._createOptions(), "--secondary-text-color"
plugins: this._createPlugins(), );
}); ChartConstructor.defaults.font.family =
} finally { computedStyles.getPropertyValue("--mdc-typography-body1-font-family") ||
this._loading = false; computedStyles.getPropertyValue("--mdc-typography-font-family") ||
} "Roboto, Noto, sans-serif";
this.chart = new ChartConstructor(ctx, {
type: this.chartType,
data: this.data,
options: this._createOptions(),
plugins: this._createPlugins(),
});
} }
private _createOptions() { private _createOptions() {

View File

@@ -1,3 +1,4 @@
import "element-internals-polyfill";
import { MdAssistChip } from "@material/web/chips/assist-chip"; import { MdAssistChip } from "@material/web/chips/assist-chip";
import { css, html } from "lit"; import { css, html } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
@@ -44,8 +45,8 @@ export class HaAssistChip extends MdAssistChip {
margin-inline-start: var(--_icon-label-space); margin-inline-start: var(--_icon-label-space);
} }
::before { ::before {
background: var(--ha-assist-chip-container-color, transparent); background: var(--ha-assist-chip-container-color);
opacity: var(--ha-assist-chip-container-opacity, 1); opacity: var(--ha-assist-chip-container-opacity);
} }
:where(.active)::before { :where(.active)::before {
background: var(--ha-assist-chip-active-container-color); background: var(--ha-assist-chip-active-container-color);

View File

@@ -1,3 +1,4 @@
import "element-internals-polyfill";
import { MdChipSet } from "@material/web/chips/chip-set"; import { MdChipSet } from "@material/web/chips/chip-set";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";

View File

@@ -1,3 +1,4 @@
import "element-internals-polyfill";
import { MdFilterChip } from "@material/web/chips/filter-chip"; import { MdFilterChip } from "@material/web/chips/filter-chip";
import { css, html } from "lit"; import { css, html } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";

View File

@@ -1,3 +1,4 @@
import "element-internals-polyfill";
import { MdInputChip } from "@material/web/chips/input-chip"; import { MdInputChip } from "@material/web/chips/input-chip";
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
@@ -19,7 +20,6 @@ export class HaInputChip extends MdInputChip {
0.15 0.15
); );
--ha-input-chip-selected-container-opacity: 1; --ha-input-chip-selected-container-opacity: 1;
--md-input-chip-label-text-font: Roboto, sans-serif;
} }
/** Set the size of mdc icons **/ /** Set the size of mdc icons **/
::slotted([slot="icon"]) { ::slotted([slot="icon"]) {

View File

@@ -1,13 +1,13 @@
import { mdiArrowDown, mdiArrowUp, mdiChevronUp } from "@mdi/js"; import { mdiArrowDown, mdiArrowUp } from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import { import {
css,
CSSResultGroup, CSSResultGroup,
html,
LitElement, LitElement,
nothing,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
css,
html,
nothing,
} from "lit"; } from "lit";
import { import {
customElement, customElement,
@@ -22,9 +22,7 @@ import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { restoreScroll } from "../../common/decorators/restore-scroll"; import { restoreScroll } from "../../common/decorators/restore-scroll";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { stringCompare } from "../../common/string/compare";
import { debounce } from "../../common/util/debounce"; import { debounce } from "../../common/util/debounce";
import { groupBy } from "../../common/util/group-by";
import { nextRender } from "../../common/util/render-status"; import { nextRender } from "../../common/util/render-status";
import { haStyleScrollbar } from "../../resources/styles"; import { haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer"; import { loadVirtualizer } from "../../resources/virtualizer";
@@ -34,6 +32,16 @@ import type { HaCheckbox } from "../ha-checkbox";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "../search-input"; import "../search-input";
import { filterData, sortData } from "./sort-filter"; import { filterData, sortData } from "./sort-filter";
import { groupBy } from "../../common/util/group-by";
declare global {
// for fire event
interface HASSDomEvents {
"selection-changed": SelectionChangedEvent;
"row-click": RowClickedEvent;
"sorting-changed": SortingChangedEvent;
}
}
export interface RowClickedEvent { export interface RowClickedEvent {
id: string; id: string;
@@ -43,10 +51,6 @@ export interface SelectionChangedEvent {
value: string[]; value: string[];
} }
export interface CollapsedChangedEvent {
value: string[];
}
export interface SortingChangedEvent { export interface SortingChangedEvent {
column: string; column: string;
direction: SortingDirection; direction: SortingDirection;
@@ -137,14 +141,10 @@ export class HaDataTable extends LitElement {
@property() public groupColumn?: string; @property() public groupColumn?: string;
@property({ attribute: false }) public groupOrder?: string[];
@property() public sortColumn?: string; @property() public sortColumn?: string;
@property() public sortDirection: SortingDirection = null; @property() public sortDirection: SortingDirection = null;
@property({ attribute: false }) public initialCollapsedGroups?: string[];
@state() private _filterable = false; @state() private _filterable = false;
@state() private _filter = ""; @state() private _filter = "";
@@ -157,8 +157,6 @@ export class HaDataTable extends LitElement {
@state() private _items: DataTableRowData[] = []; @state() private _items: DataTableRowData[] = [];
@state() private _collapsedGroups: string[] = [];
private _checkableRowsCount?: number; private _checkableRowsCount?: number;
private _checkedRows: string[] = []; private _checkedRows: string[] = [];
@@ -214,19 +212,17 @@ export class HaDataTable extends LitElement {
(column) => column.filterable (column) => column.filterable
); );
if (!this.sortColumn) { for (const columnId in this.columns) {
for (const columnId in this.columns) { if (this.columns[columnId].direction) {
if (this.columns[columnId].direction) { this.sortDirection = this.columns[columnId].direction!;
this.sortDirection = this.columns[columnId].direction!; this.sortColumn = columnId;
this.sortColumn = columnId;
fireEvent(this, "sorting-changed", { fireEvent(this, "sorting-changed", {
column: columnId, column: columnId,
direction: this.sortDirection, direction: this.sortDirection,
}); });
break; break;
}
} }
} }
@@ -251,23 +247,13 @@ export class HaDataTable extends LitElement {
).length; ).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 ( if (
properties.has("data") || properties.has("data") ||
properties.has("columns") || properties.has("columns") ||
properties.has("_filter") || properties.has("_filter") ||
properties.has("sortColumn") || properties.has("sortColumn") ||
properties.has("sortDirection") || properties.has("sortDirection") ||
properties.has("groupColumn") || properties.has("groupColumn")
properties.has("groupOrder") ||
properties.has("_collapsedGroups")
) { ) {
this._sortFilterData(); this._sortFilterData();
} }
@@ -460,8 +446,6 @@ export class HaDataTable extends LitElement {
} }
return html` return html`
<div <div
@mouseover=${this._setTitle}
@focus=${this._setTitle}
role=${column.main ? "rowheader" : "cell"} role=${column.main ? "rowheader" : "cell"}
class="mdc-data-table__cell ${classMap({ class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--flex": column.type === "flex", "mdc-data-table__cell--flex": column.type === "flex",
@@ -529,7 +513,11 @@ export class HaDataTable extends LitElement {
} }
if (this.appendRow || this.hasFab || this.groupColumn) { if (this.appendRow || this.hasFab || this.groupColumn) {
let items = [...data]; const items = [...data];
if (this.appendRow) {
items.push({ append: true, content: this.appendRow });
}
if (this.groupColumn) { if (this.groupColumn) {
const grouped = groupBy(items, (item) => item[this.groupColumn!]); const grouped = groupBy(items, (item) => item[this.groupColumn!]);
@@ -541,66 +529,39 @@ export class HaDataTable extends LitElement {
const sorted: { const sorted: {
[key: string]: DataTableRowData[]; [key: string]: DataTableRowData[];
} = Object.keys(grouped) } = Object.keys(grouped)
.sort((a, b) => { .sort()
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) => { .reduce((obj, key) => {
obj[key] = grouped[key]; obj[key] = grouped[key];
return obj; return obj;
}, {}); }, {});
const groupedItems: DataTableRowData[] = []; const groupedItems: DataTableRowData[] = [];
Object.entries(sorted).forEach(([groupName, rows]) => { Object.entries(sorted).forEach(([groupName, rows]) => {
groupedItems.push({ if (
append: true, groupName !== UNDEFINED_GROUP_KEY ||
content: html`<div Object.keys(sorted).length > 1
class="mdc-data-table__cell group-header" ) {
role="cell" groupedItems.push({
.group=${groupName} append: true,
@click=${this._collapseGroup} content: html`<div
> class="mdc-data-table__cell group-header"
<ha-icon-button role="cell"
.path=${mdiChevronUp}
class=${this._collapsedGroups.includes(groupName)
? "collapsed"
: ""}
> >
</ha-icon-button> ${groupName === UNDEFINED_GROUP_KEY ? "" : groupName || ""}
${groupName === UNDEFINED_GROUP_KEY </div>`,
? this.hass.localize("ui.components.data-table.ungrouped") });
: groupName || ""}
</div>`,
});
if (!this._collapsedGroups.includes(groupName)) {
groupedItems.push(...rows);
} }
});
items = groupedItems;
}
if (this.appendRow) { groupedItems.push(...rows);
items.push({ append: true, content: this.appendRow }); });
this._items = groupedItems;
} else {
this._items = items;
} }
if (this.hasFab) { if (this.hasFab) {
items.push({ empty: true }); this._items = [...this._items, { empty: true }];
} }
this._items = items;
} else { } else {
this._items = data; this._items = data;
} }
@@ -681,13 +642,6 @@ export class HaDataTable extends LitElement {
fireEvent(this, "row-click", { id: rowId }, { bubbles: false }); 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() { private _checkedRowsChanged() {
// force scroller to update, change it's items // force scroller to update, change it's items
if (this._items.length) { if (this._items.length) {
@@ -718,18 +672,6 @@ export class HaDataTable extends LitElement {
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop; 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 { static get styles(): CSSResultGroup {
return [ return [
haStyleScrollbar, haStyleScrollbar,
@@ -982,22 +924,8 @@ export class HaDataTable extends LitElement {
.group-header { .group-header {
padding-top: 12px; padding-top: 12px;
padding-left: 12px;
padding-inline-start: 12px;
padding-inline-end: initial;
width: 100%; width: 100%;
font-weight: 500; 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 { :host {
@@ -1096,12 +1024,4 @@ declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-data-table": HaDataTable; "ha-data-table": HaDataTable;
} }
// for fire event
interface HASSDomEvents {
"selection-changed": SelectionChangedEvent;
"row-click": RowClickedEvent;
"sorting-changed": SortingChangedEvent;
"collapsed-changed": CollapsedChangedEvent;
}
} }

View File

@@ -11,10 +11,10 @@ import {
} from "../common/datetime/localize_date"; } from "../common/datetime/localize_date";
import { mainWindow } from "../common/dom/get_main_window"; import { mainWindow } from "../common/dom/get_main_window";
// Set the current date to the left picker instead of the right picker because the right is hidden
const CustomDateRangePicker = Vue.extend({ const CustomDateRangePicker = Vue.extend({
mixins: [DateRangePicker], mixins: [DateRangePicker],
methods: { methods: {
// Set the current date to the left picker instead of the right picker because the right is hidden
selectMonthDate() { selectMonthDate() {
const dt: Date = this.end || new Date(); const dt: Date = this.end || new Date();
// @ts-ignore // @ts-ignore
@@ -23,33 +23,6 @@ const CustomDateRangePicker = Vue.extend({
month: dt.getMonth() + 1, month: dt.getMonth() + 1,
}); });
}, },
// Fix the start/end date calculation when selecting a date range. The
// original code keeps track of the first clicked date (in_selection) but it
// never sets it to either the start or end date variables, so if the
// in_selection date is between the start and end date that were set by the
// hover the selection will enter a broken state that's counter-intuitive
// when hovering between weeks and leads to a random date when selecting a
// range across months. This bug doesn't seem to be present on v0.6.7 of the
// lib
hoverDate(value: Date) {
if (this.readonly) return;
if (this.in_selection) {
const pickA = this.in_selection as Date;
const pickB = value;
this.start = this.normalizeDatetime(
Math.min(pickA.valueOf(), pickB.valueOf()),
this.start
);
this.end = this.normalizeDatetime(
Math.max(pickA.valueOf(), pickB.valueOf()),
this.end
);
}
this.$emit("hover-date", value);
},
}, },
}); });

View File

@@ -76,8 +76,6 @@ class HaEntitiesPickerLight extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public entityFilter?: HaEntityPickerEntityFilterFunc; public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Array }) public createDomains?: string[];
protected render() { protected render() {
if (!this.hass) { if (!this.hass) {
return nothing; return nothing;
@@ -105,7 +103,6 @@ class HaEntitiesPickerLight extends LitElement {
.value=${entityId} .value=${entityId}
.label=${this.pickedEntityLabel} .label=${this.pickedEntityLabel}
.disabled=${this.disabled} .disabled=${this.disabled}
.createDomains=${this.createDomains}
@value-changed=${this._entityChanged} @value-changed=${this._entityChanged}
></ha-entity-picker> ></ha-entity-picker>
</div> </div>
@@ -125,7 +122,6 @@ class HaEntitiesPickerLight extends LitElement {
.label=${this.pickEntityLabel} .label=${this.pickEntityLabel}
.helper=${this.helper} .helper=${this.helper}
.disabled=${this.disabled} .disabled=${this.disabled}
.createDomains=${this.createDomains}
.required=${this.required && !currentEntities.length} .required=${this.required && !currentEntities.length}
@value-changed=${this._addEntity} @value-changed=${this._addEntity}
></ha-entity-picker> ></ha-entity-picker>

View File

@@ -18,12 +18,6 @@ import "../ha-icon-button";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "./state-badge"; import "./state-badge";
import { caseInsensitiveStringCompare } from "../../common/string/compare"; 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 { interface HassEntityWithCachedName extends HassEntity, ScorableTextItem {
friendly_name: string; friendly_name: string;
@@ -31,8 +25,6 @@ interface HassEntityWithCachedName extends HassEntity, ScorableTextItem {
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean; export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
const CREATE_ID = "___create-new-entity___";
@customElement("ha-entity-picker") @customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement { export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -52,8 +44,6 @@ export class HaEntityPicker extends LitElement {
@property() public helper?: string; @property() public helper?: string;
@property({ type: Array }) public createDomains?: string[];
/** /**
* Show entities from specific domains. * Show entities from specific domains.
* @type {Array} * @type {Array}
@@ -140,11 +130,7 @@ export class HaEntityPicker extends LitElement {
></state-badge>` ></state-badge>`
: ""} : ""}
<span>${item.friendly_name}</span> <span>${item.friendly_name}</span>
<span slot="secondary" <span slot="secondary">${item.entity_id}</span>
>${item.entity_id.startsWith(CREATE_ID)
? this.hass.localize("ui.components.entity.entity-picker.new_entity")
: item.entity_id}</span
>
</ha-list-item>`; </ha-list-item>`;
private _getStates = memoizeOne( private _getStates = memoizeOne(
@@ -157,8 +143,7 @@ export class HaEntityPicker extends LitElement {
includeDeviceClasses: this["includeDeviceClasses"], includeDeviceClasses: this["includeDeviceClasses"],
includeUnitOfMeasurement: this["includeUnitOfMeasurement"], includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
includeEntities: this["includeEntities"], includeEntities: this["includeEntities"],
excludeEntities: this["excludeEntities"], excludeEntities: this["excludeEntities"]
createDomains: this["createDomains"]
): HassEntityWithCachedName[] => { ): HassEntityWithCachedName[] => {
let states: HassEntityWithCachedName[] = []; let states: HassEntityWithCachedName[] = [];
@@ -167,34 +152,6 @@ export class HaEntityPicker extends LitElement {
} }
let entityIds = Object.keys(hass.states); 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) { if (!entityIds.length) {
return [ return [
{ {
@@ -214,7 +171,6 @@ export class HaEntityPicker extends LitElement {
}, },
strings: [], strings: [],
}, },
...createItems,
]; ];
} }
@@ -325,14 +281,9 @@ export class HaEntityPicker extends LitElement {
}, },
strings: [], strings: [],
}, },
...createItems,
]; ];
} }
if (createItems?.length) {
states.push(...createItems);
}
return states; return states;
} }
); );
@@ -359,18 +310,13 @@ export class HaEntityPicker extends LitElement {
this.includeDeviceClasses, this.includeDeviceClasses,
this.includeUnitOfMeasurement, this.includeUnitOfMeasurement,
this.includeEntities, this.includeEntities,
this.excludeEntities, this.excludeEntities
this.createDomains
); );
if (this._initedStates) { if (this._initedStates) {
this.comboBox.filteredItems = this._states; this.comboBox.filteredItems = this._states;
} }
this._initedStates = true; this._initedStates = true;
} }
if (changedProps.has("createDomains") && this.createDomains?.length) {
this.hass.loadFragmentTranslation("config");
}
} }
protected render(): TemplateResult { protected render(): TemplateResult {
@@ -405,21 +351,9 @@ export class HaEntityPicker extends LitElement {
this._opened = ev.detail.value; this._opened = ev.detail.value;
} }
private _valueChanged(ev: ValueChangedEvent<string | undefined>) { private _valueChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation(); ev.stopPropagation();
const newValue = ev.detail.value?.trim(); 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) { if (newValue !== this._value) {
this._setValue(newValue); this._setValue(newValue);
} }
@@ -427,13 +361,13 @@ export class HaEntityPicker extends LitElement {
private _filterChanged(ev: CustomEvent): void { private _filterChanged(ev: CustomEvent): void {
const target = ev.target as HaComboBox; const target = ev.target as HaComboBox;
const filterString = ev.detail.value.trim().toLowerCase(); const filterString = ev.detail.value.toLowerCase();
target.filteredItems = filterString.length target.filteredItems = filterString.length
? fuzzyFilterSort<HassEntityWithCachedName>(filterString, this._states) ? fuzzyFilterSort<HassEntityWithCachedName>(filterString, this._states)
: this._states; : this._states;
} }
private _setValue(value: string | undefined) { private _setValue(value: string) {
this.value = value; this.value = value;
setTimeout(() => { setTimeout(() => {
fireEvent(this, "value-changed", { value }); fireEvent(this, "value-changed", { value });

View File

@@ -1,9 +1,8 @@
import { mdiTextureBox } from "@mdi/js"; import { mdiTextureBox } from "@mdi/js";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit"; import { LitElement, PropertyValues, TemplateResult, html } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
@@ -12,7 +11,6 @@ import {
ScorableTextItem, ScorableTextItem,
fuzzyFilterSort, fuzzyFilterSort,
} from "../common/string/filter/sequence-matching"; } from "../common/string/filter/sequence-matching";
import { computeRTL } from "../common/util/compute_rtl";
import { AreaRegistryEntry } from "../data/area_registry"; import { AreaRegistryEntry } from "../data/area_registry";
import { import {
DeviceEntityDisplayLookup, DeviceEntityDisplayLookup,
@@ -34,7 +32,6 @@ import "./ha-floor-icon";
import "./ha-icon-button"; import "./ha-icon-button";
import "./ha-list-item"; import "./ha-list-item";
import "./ha-svg-icon"; import "./ha-svg-icon";
import "./ha-tree-indicator";
type ScorableAreaFloorEntry = ScorableTextItem & FloorAreaEntry; type ScorableAreaFloorEntry = ScorableTextItem & FloorAreaEntry;
@@ -44,11 +41,28 @@ interface FloorAreaEntry {
icon: string | null; icon: string | null;
strings: string[]; strings: string[];
type: "floor" | "area"; type: "floor" | "area";
level: number | null;
hasFloor?: boolean; hasFloor?: boolean;
lastArea?: boolean; level: number | null;
} }
const rowRenderer: ComboBoxLitRenderer<FloorAreaEntry> = (item) =>
html`<ha-list-item
graphic="icon"
style=${item.type === "area" && item.hasFloor
? "--mdc-list-side-padding-left: 48px;"
: ""}
>
${item.type === "floor"
? html`<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>`
: item.icon
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon
slot="graphic"
.path=${mdiTextureBox}
></ha-svg-icon>`}
${item.name}
</ha-list-item>`;
@customElement("ha-area-floor-picker") @customElement("ha-area-floor-picker")
export class HaAreaFloorPicker extends SubscribeMixin(LitElement) { export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -137,44 +151,6 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
await this.comboBox?.focus(); await this.comboBox?.focus();
} }
private _rowRenderer: ComboBoxLitRenderer<FloorAreaEntry> = (item) => {
const rtl = computeRTL(this.hass);
return html`
<ha-list-item
graphic="icon"
style=${item.type === "area" && item.hasFloor
? rtl
? "--mdc-list-side-padding-right: 48px;"
: "--mdc-list-side-padding-left: 48px;"
: ""}
>
${item.type === "area" && item.hasFloor
? html`<ha-tree-indicator
style=${styleMap({
width: "48px",
position: "absolute",
top: "0px",
left: rtl ? undefined : "8px",
right: rtl ? "8px" : undefined,
transform: rtl ? "scaleX(-1)" : "",
})}
.end=${item.lastArea}
slot="graphic"
></ha-tree-indicator>`
: nothing}
${item.type === "floor"
? html`<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>`
: item.icon
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
: html`<ha-svg-icon
slot="graphic"
.path=${mdiTextureBox}
></ha-svg-icon>`}
${item.name}
</ha-list-item>
`;
};
private _getAreas = memoizeOne( private _getAreas = memoizeOne(
( (
floors: FloorRegistryEntry[], floors: FloorRegistryEntry[],
@@ -388,7 +364,7 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
}); });
} }
output.push( output.push(
...floorAreas.map((area, index, array) => ({ ...floorAreas.map((area) => ({
id: area.area_id, id: area.area_id,
type: "area" as const, type: "area" as const,
name: area.name, name: area.name,
@@ -396,7 +372,6 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
strings: [area.area_id, ...area.aliases, area.name], strings: [area.area_id, ...area.aliases, area.name],
hasFloor: true, hasFloor: true,
level: null, level: null,
lastArea: index === array.length - 1,
})) }))
); );
}); });
@@ -470,7 +445,7 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
.placeholder=${this.placeholder .placeholder=${this.placeholder
? this.hass.areas[this.placeholder]?.name ? this.hass.areas[this.placeholder]?.name
: undefined} : undefined}
.renderer=${this._rowRenderer} .renderer=${rowRenderer}
@filter-changed=${this._filterChanged} @filter-changed=${this._filterChanged}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@value-changed=${this._areaChanged} @value-changed=${this._areaChanged}

View File

@@ -428,8 +428,6 @@ export class HaAreaPicker extends LitElement {
(ev.target as any).value = this._value; (ev.target as any).value = this._value;
this.hass.loadFragmentTranslation("config");
showAreaRegistryDetailDialog(this, { showAreaRegistryDetailDialog(this, {
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "", suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
createEntry: async (values) => { createEntry: async (values) => {

View File

@@ -14,8 +14,6 @@ export class HaCard extends LitElement {
--ha-card-background, --ha-card-background,
var(--card-background-color, white) 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-shadow: var(--ha-card-box-shadow, none);
box-sizing: border-box; box-sizing: border-box;
border-radius: var(--ha-card-border-radius, 12px); border-radius: var(--ha-card-border-radius, 12px);

View File

@@ -1,5 +1,6 @@
import "element-internals-polyfill";
import { MdCircularProgress } from "@material/web/progress/circular-progress"; import { MdCircularProgress } from "@material/web/progress/circular-progress";
import { PropertyValues, css } from "lit"; import { CSSResult, PropertyValues, css } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
@customElement("ha-circular-progress") @customElement("ha-circular-progress")
@@ -31,15 +32,17 @@ export class HaCircularProgress extends MdCircularProgress {
} }
} }
static override styles = [ static get styles(): CSSResult[] {
...super.styles, return [
css` ...super.styles,
:host { css`
--md-sys-color-primary: var(--primary-color); :host {
--md-circular-progress-size: 48px; --md-sys-color-primary: var(--primary-color);
} --md-circular-progress-size: 48px;
`, }
]; `,
];
}
} }
declare global { declare global {

View File

@@ -1,7 +1,14 @@
import { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import {
customElement,
eventOptions,
property,
queryAsync,
state,
} from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import "./ha-ripple";
@customElement("ha-control-button") @customElement("ha-control-button")
export class HaControlButton extends LitElement { export class HaControlButton extends LitElement {
@@ -9,6 +16,10 @@ export class HaControlButton extends LitElement {
@property() public label?: string; @property() public label?: string;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<button <button
@@ -17,13 +28,54 @@ export class HaControlButton extends LitElement {
aria-label=${ifDefined(this.label)} aria-label=${ifDefined(this.label)}
title=${ifDefined(this.label)} title=${ifDefined(this.label)}
.disabled=${Boolean(this.disabled)} .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> <slot></slot>
<ha-ripple .disabled=${this.disabled}></ha-ripple> ${this._shouldRenderRipple && !this.disabled
? html`<mwc-ripple></mwc-ripple>`
: ""}
</button> </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 { static get styles(): CSSResultGroup {
return css` return css`
:host { :host {
@@ -34,7 +86,6 @@ export class HaControlButton extends LitElement {
--control-button-border-radius: 10px; --control-button-border-radius: 10px;
--control-button-padding: 8px; --control-button-padding: 8px;
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
--ha-ripple-color: var(--secondary-text-color);
color: var(--primary-text-color); color: var(--primary-text-color);
width: 40px; width: 40px;
height: 40px; height: 40px;
@@ -62,14 +113,12 @@ export class HaControlButton extends LitElement {
outline: none; outline: none;
overflow: hidden; overflow: hidden;
background: none; background: none;
--mdc-ripple-color: var(--control-button-background-color);
/* For safari border-radius overflow */ /* For safari border-radius overflow */
z-index: 0; z-index: 0;
font-size: inherit; font-size: inherit;
color: inherit; color: inherit;
} }
.button:focus-visible {
--control-button-background-opacity: 0.4;
}
.button::before { .button::before {
content: ""; content: "";
position: absolute; position: absolute;

View File

@@ -1,14 +1,22 @@
import { Ripple } from "@material/mwc-ripple";
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { SelectBase } from "@material/mwc-select/mwc-select-base"; import { SelectBase } from "@material/mwc-select/mwc-select-base";
import { mdiMenuDown } from "@mdi/js"; import { mdiMenuDown } from "@mdi/js";
import { css, html, nothing } from "lit"; import { css, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators"; import {
customElement,
eventOptions,
property,
query,
queryAsync,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { debounce } from "../common/util/debounce"; import { debounce } from "../common/util/debounce";
import { nextRender } from "../common/util/render-status"; import { nextRender } from "../common/util/render-status";
import "./ha-icon"; import "./ha-icon";
import type { HaIcon } from "./ha-icon"; import type { HaIcon } from "./ha-icon";
import "./ha-ripple";
import "./ha-svg-icon"; import "./ha-svg-icon";
import type { HaSvgIcon } from "./ha-svg-icon"; import type { HaSvgIcon } from "./ha-svg-icon";
@@ -24,6 +32,10 @@ export class HaControlSelectMenu extends SelectBase {
@property({ type: Boolean, attribute: "hide-label" }) @property({ type: Boolean, attribute: "hide-label" })
public hideLabel = false; public hideLabel = false;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
public override render() { public override render() {
const classes = { const classes = {
"select-disabled": this.disabled, "select-disabled": this.disabled,
@@ -57,10 +69,17 @@ export class HaControlSelectMenu extends SelectBase {
aria-labelledby=${ifDefined(labelledby)} aria-labelledby=${ifDefined(labelledby)}
aria-label=${ifDefined(labelAttribute)} aria-label=${ifDefined(labelAttribute)}
aria-required=${this.required} aria-required=${this.required}
@click=${this.onClick}
@focus=${this.onFocus} @focus=${this.onFocus}
@blur=${this.onBlur} @blur=${this.onBlur}
@click=${this.onClick}
@keydown=${this.onKeydown} @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()} ${this.renderIcon()}
<div class="content"> <div class="content">
@@ -72,7 +91,9 @@ export class HaControlSelectMenu extends SelectBase {
: nothing} : nothing}
</div> </div>
${this.renderArrow()} ${this.renderArrow()}
<ha-ripple .disabled=${this.disabled}></ha-ripple> ${this._shouldRenderRipple && !this.disabled
? html` <mwc-ripple></mwc-ripple> `
: nothing}
</div> </div>
${this.renderMenu()} ${this.renderMenu()}
</div> </div>
@@ -83,7 +104,7 @@ export class HaControlSelectMenu extends SelectBase {
if (!this.showArrow) return nothing; if (!this.showArrow) return nothing;
return html` return html`
<div class="icon arrow"> <div class="icon">
<ha-svg-icon .path=${mdiMenuDown}></ha-svg-icon> <ha-svg-icon .path=${mdiMenuDown}></ha-svg-icon>
</div> </div>
`; `;
@@ -114,6 +135,46 @@ 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() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
window.addEventListener("translations-updated", this._translationsUpdated); window.addEventListener("translations-updated", this._translationsUpdated);
@@ -143,7 +204,6 @@ export class HaControlSelectMenu extends SelectBase {
--control-select-menu-height: 48px; --control-select-menu-height: 48px;
--control-select-menu-padding: 6px 10px; --control-select-menu-padding: 6px 10px;
--mdc-icon-size: 20px; --mdc-icon-size: 20px;
--ha-ripple-color: var(--secondary-text-color);
font-size: 14px; font-size: 14px;
line-height: 1.4; line-height: 1.4;
width: auto; width: auto;
@@ -164,6 +224,7 @@ export class HaControlSelectMenu extends SelectBase {
outline: none; outline: none;
overflow: hidden; overflow: hidden;
background: none; background: none;
--mdc-ripple-color: var(--control-select-menu-background-color);
/* For safari border-radius overflow */ /* For safari border-radius overflow */
z-index: 0; z-index: 0;
transition: color 180ms ease-in-out; transition: color 180ms ease-in-out;
@@ -179,8 +240,7 @@ export class HaControlSelectMenu extends SelectBase {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
justify-content: center; justify-content: center;
width: 0; flex: 1;
flex-grow: 1;
overflow: hidden; overflow: hidden;
} }
@@ -193,13 +253,6 @@ export class HaControlSelectMenu extends SelectBase {
margin: auto; margin: auto;
} }
.arrow {
margin-left: -10px;
margin-inline-end: initial;
margin-inline-start: -10px;
direction: var(--direction);
}
.label { .label {
font-size: 0.85em; font-size: 0.85em;
letter-spacing: 0.4px; letter-spacing: 0.4px;
@@ -211,10 +264,6 @@ export class HaControlSelectMenu extends SelectBase {
letter-spacing: inherit; letter-spacing: inherit;
} }
.select-anchor:focus-visible {
--control-select-menu-background-opacity: 0.4;
}
.select-anchor::before { .select-anchor::before {
content: ""; content: "";
position: absolute; position: absolute;

View File

@@ -67,9 +67,6 @@ export class HaControlSlider extends LitElement {
@property({ attribute: "tooltip-mode" }) @property({ attribute: "tooltip-mode" })
public tooltipMode: TooltipMode = "interaction"; public tooltipMode: TooltipMode = "interaction";
@property({ attribute: "touch-action" })
public touchAction?: string;
@property({ type: Number }) @property({ type: Number })
public value?: number; public value?: number;
@@ -155,7 +152,7 @@ export class HaControlSlider extends LitElement {
setupListeners() { setupListeners() {
if (this.slider && !this._mc) { if (this.slider && !this._mc) {
this._mc = new Manager(this.slider, { this._mc = new Manager(this.slider, {
touchAction: this.touchAction ?? (this.vertical ? "pan-x" : "pan-y"), touchAction: this.vertical ? "pan-x" : "pan-y",
}); });
this._mc.add( this._mc.add(
new Pan({ new Pan({

View File

@@ -33,9 +33,6 @@ 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) // 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({ type: String }) pathOff?: string;
@property({ attribute: "touch-action" })
public touchAction?: string;
private _mc?: HammerManager; private _mc?: HammerManager;
protected firstUpdated(changedProperties: PropertyValues): void { protected firstUpdated(changedProperties: PropertyValues): void {
@@ -76,7 +73,7 @@ export class HaControlSwitch extends LitElement {
setupListeners() { setupListeners() {
if (this.switch && !this._mc) { if (this.switch && !this._mc) {
this._mc = new Manager(this.switch, { this._mc = new Manager(this.switch, {
touchAction: this.touchAction ?? (this.vertical ? "pan-x" : "pan-y"), touchAction: this.vertical ? "pan-x" : "pan-y",
}); });
this._mc.add( this._mc.add(
new Swipe({ new Swipe({

View File

@@ -19,7 +19,6 @@ import { HomeAssistant } from "../types";
import "./ha-list-item"; import "./ha-list-item";
import "./ha-select"; import "./ha-select";
import type { HaSelect } from "./ha-select"; import type { HaSelect } from "./ha-select";
import { getExtendedEntityRegistryEntry } from "../data/entity_registry";
const NONE = "__NONE_OPTION__"; const NONE = "__NONE_OPTION__";
@@ -108,23 +107,13 @@ export class HaConversationAgentPicker extends LitElement {
} }
private async _maybeFetchConfigEntry() { private async _maybeFetchConfigEntry() {
if (!this.value || !(this.value in this.hass.entities)) { if (!this.value || this.value === "homeassistant") {
this._configEntry = undefined; this._configEntry = undefined;
return; return;
} }
try { try {
const regEntry = await getExtendedEntityRegistryEntry(
this.hass,
this.value
);
if (!regEntry.config_entry_id) {
this._configEntry = undefined;
return;
}
this._configEntry = ( this._configEntry = (
await getConfigEntry(this.hass, regEntry.config_entry_id) await getConfigEntry(this.hass, this.value)
).config_entry; ).config_entry;
} catch (err) { } catch (err) {
this._configEntry = undefined; this._configEntry = undefined;

View File

@@ -75,14 +75,8 @@ export class HaDialog extends DialogBase {
var(--divider-color) var(--divider-color)
); );
z-index: var(--dialog-z-index, 8); z-index: var(--dialog-z-index, 8);
-webkit-backdrop-filter: var( -webkit-backdrop-filter: var(--dialog-backdrop-filter, none);
--ha-dialog-scrim-backdrop-filter, backdrop-filter: var(--dialog-backdrop-filter, none);
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-dialog-box-shadow: var(--dialog-box-shadow, none);
--mdc-typography-headline6-font-weight: 400; --mdc-typography-headline6-font-weight: 400;
--mdc-typography-headline6-font-size: 1.574rem; --mdc-typography-headline6-font-size: 1.574rem;
@@ -125,12 +119,6 @@ export class HaDialog extends DialogBase {
margin-top: var(--dialog-surface-margin-top); margin-top: var(--dialog-surface-margin-top);
min-height: var(--mdc-dialog-min-height, auto); min-height: var(--mdc-dialog-min-height, auto);
border-radius: var(--ha-dialog-border-radius, 28px); 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 { :host([flexContent]) .mdc-dialog .mdc-dialog__content {
display: flex; display: flex;

View File

@@ -1,13 +1,12 @@
import { SelectedDetail } from "@material/mwc-list"; import { SelectedDetail } from "@material/mwc-list";
import "@material/mwc-menu/mwc-menu-surface"; import "@material/mwc-menu/mwc-menu-surface";
import { mdiFilterVariantRemove } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { Blueprints, fetchBlueprints } from "../data/blueprint";
import { findRelated, RelatedResult } from "../data/search"; import { findRelated, RelatedResult } from "../data/search";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { haStyleScrollbar } from "../resources/styles";
import { Blueprints, fetchBlueprints } from "../data/blueprint";
@customElement("ha-filter-blueprints") @customElement("ha-filter-blueprints")
export class HaFilterBlueprints extends LitElement { export class HaFilterBlueprints extends LitElement {
@@ -36,11 +35,7 @@ export class HaFilterBlueprints extends LitElement {
<div slot="header" class="header"> <div slot="header" class="header">
${this.hass.localize("ui.panel.config.blueprint.caption")} ${this.hass.localize("ui.panel.config.blueprint.caption")}
${this.value?.length ${this.value?.length
? html`<div class="badge">${this.value?.length}</div> ? html`<div class="badge">${this.value?.length}</div>`
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing} : nothing}
</div> </div>
${this._blueprints && this._shouldRender ${this._blueprints && this._shouldRender
@@ -133,15 +128,6 @@ export class HaFilterBlueprints extends LitElement {
}); });
} }
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyleScrollbar, haStyleScrollbar,
@@ -161,10 +147,6 @@ export class HaFilterBlueprints extends LitElement {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge { .badge {
display: inline-block; display: inline-block;
margin-left: 8px; margin-left: 8px;

View File

@@ -2,7 +2,6 @@ import { ActionDetail, SelectedDetail } from "@material/mwc-list";
import { import {
mdiDelete, mdiDelete,
mdiDotsVertical, mdiDotsVertical,
mdiFilterVariantRemove,
mdiPencil, mdiPencil,
mdiPlus, mdiPlus,
mdiTag, mdiTag,
@@ -69,11 +68,7 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
<div slot="header" class="header"> <div slot="header" class="header">
${this.hass.localize("ui.panel.config.category.caption")} ${this.hass.localize("ui.panel.config.category.caption")}
${this.value?.length ${this.value?.length
? html`<div class="badge">${this.value?.length}</div> ? html`<div class="badge">${this.value?.length}</div>`
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing} : nothing}
</div> </div>
${this._shouldRender ${this._shouldRender
@@ -259,15 +254,6 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
}); });
} }
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyleScrollbar, haStyleScrollbar,
@@ -288,10 +274,6 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge { .badge {
display: inline-block; display: inline-block;
margin-left: 8px; margin-left: 8px;

View File

@@ -1,4 +1,3 @@
import { mdiFilterVariantRemove } from "@mdi/js";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -14,11 +13,10 @@ import { stringCompare } from "../common/string/compare";
import { computeDeviceName } from "../data/device_registry"; import { computeDeviceName } from "../data/device_registry";
import { findRelated, RelatedResult } from "../data/search"; import { findRelated, RelatedResult } from "../data/search";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import { loadVirtualizer } from "../resources/virtualizer";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-check-list-item";
import "./ha-expansion-panel"; import "./ha-expansion-panel";
import "./search-input-outlined"; import "./ha-check-list-item";
import { loadVirtualizer } from "../resources/virtualizer";
@customElement("ha-filter-devices") @customElement("ha-filter-devices")
export class HaFilterDevices extends LitElement { export class HaFilterDevices extends LitElement {
@@ -34,8 +32,6 @@ export class HaFilterDevices extends LitElement {
@state() private _shouldRender = false; @state() private _shouldRender = false;
@state() private _filter?: string;
public willUpdate(properties: PropertyValues) { public willUpdate(properties: PropertyValues) {
super.willUpdate(properties); super.willUpdate(properties);
@@ -55,33 +51,19 @@ export class HaFilterDevices extends LitElement {
<div slot="header" class="header"> <div slot="header" class="header">
${this.hass.localize("ui.panel.config.devices.caption")} ${this.hass.localize("ui.panel.config.devices.caption")}
${this.value?.length ${this.value?.length
? html`<div class="badge">${this.value?.length}</div> ? html`<div class="badge">${this.value?.length}</div>`
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing} : nothing}
</div> </div>
${this._shouldRender ${this._shouldRender
? html`<search-input-outlined ? html`<mwc-list class="ha-scrollbar">
.hass=${this.hass} <lit-virtualizer
.filter=${this._filter} .items=${this._devices(this.hass.devices, this.value)}
@value-changed=${this._handleSearchChange} .keyFunction=${this._keyFunction}
.renderItem=${this._renderItem}
@click=${this._handleItemClick}
> >
</search-input-outlined> </lit-virtualizer>
<mwc-list class="ha-scrollbar" multi> </mwc-list>`
<lit-virtualizer
.items=${this._devices(
this.hass.devices,
this._filter || "",
this.value
)}
.keyFunction=${this._keyFunction}
.renderItem=${this._renderItem}
@click=${this._handleItemClick}
>
</lit-virtualizer>
</mwc-list>`
: nothing} : nothing}
</ha-expansion-panel> </ha-expansion-panel>
`; `;
@@ -90,14 +72,12 @@ export class HaFilterDevices extends LitElement {
private _keyFunction = (device) => device?.id; private _keyFunction = (device) => device?.id;
private _renderItem = (device) => private _renderItem = (device) =>
!device html`<ha-check-list-item
? nothing .value=${device.id}
: html`<ha-check-list-item .selected=${this.value?.includes(device.id)}
.value=${device.id} >
.selected=${this.value?.includes(device.id) ?? false} ${computeDeviceName(device, this.hass)}
> </ha-check-list-item>`;
${computeDeviceName(device, this.hass)}
</ha-check-list-item>`;
private _handleItemClick(ev) { private _handleItemClick(ev) {
const listItem = ev.target.closest("ha-check-list-item"); const listItem = ev.target.closest("ha-check-list-item");
@@ -119,7 +99,7 @@ export class HaFilterDevices extends LitElement {
setTimeout(() => { setTimeout(() => {
if (!this.expanded) return; if (!this.expanded) return;
this.renderRoot.querySelector("mwc-list")!.style.height = this.renderRoot.querySelector("mwc-list")!.style.height =
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input `${this.clientHeight - 49}px`;
}, 300); }, 300);
} }
} }
@@ -132,28 +112,16 @@ export class HaFilterDevices extends LitElement {
this.expanded = ev.detail.expanded; this.expanded = ev.detail.expanded;
} }
private _handleSearchChange(ev: CustomEvent) { private _devices = memoizeOne((devices: HomeAssistant["devices"], _value) => {
this._filter = ev.detail.value.toLowerCase(); const values = Object.values(devices);
} return values.sort((a, b) =>
stringCompare(
private _devices = memoizeOne( a.name_by_user || a.name || "",
(devices: HomeAssistant["devices"], filter: string, _value) => { b.name_by_user || b.name || "",
const values = Object.values(devices); this.hass.locale.language
return values )
.filter( );
(device) => });
!filter ||
computeDeviceName(device, this.hass).toLowerCase().includes(filter)
)
.sort((a, b) =>
stringCompare(
computeDeviceName(a, this.hass),
computeDeviceName(b, this.hass),
this.hass.locale.language
)
);
}
);
private async _findRelated() { private async _findRelated() {
const relatedPromises: Promise<RelatedResult>[] = []; const relatedPromises: Promise<RelatedResult>[] = [];
@@ -190,15 +158,6 @@ export class HaFilterDevices extends LitElement {
}); });
} }
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyleScrollbar, haStyleScrollbar,
@@ -219,10 +178,6 @@ export class HaFilterDevices extends LitElement {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge { .badge {
display: inline-block; display: inline-block;
margin-left: 8px; margin-left: 8px;
@@ -242,10 +197,6 @@ export class HaFilterDevices extends LitElement {
ha-check-list-item { ha-check-list-item {
width: 100%; width: 100%;
} }
search-input-outlined {
display: block;
padding: 0 8px;
}
`, `,
]; ];
} }

View File

@@ -1,204 +0,0 @@
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;
}
}

View File

@@ -1,4 +1,3 @@
import { mdiFilterVariantRemove } from "@mdi/js";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@@ -15,11 +14,10 @@ import { computeStateName } from "../common/entity/compute_state_name";
import { stringCompare } from "../common/string/compare"; import { stringCompare } from "../common/string/compare";
import { findRelated, RelatedResult } from "../data/search"; import { findRelated, RelatedResult } from "../data/search";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import { loadVirtualizer } from "../resources/virtualizer";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-check-list-item";
import "./ha-state-icon"; import "./ha-state-icon";
import "./search-input-outlined"; import "./ha-check-list-item";
import { loadVirtualizer } from "../resources/virtualizer";
@customElement("ha-filter-entities") @customElement("ha-filter-entities")
export class HaFilterEntities extends LitElement { export class HaFilterEntities extends LitElement {
@@ -35,8 +33,6 @@ export class HaFilterEntities extends LitElement {
@state() private _shouldRender = false; @state() private _shouldRender = false;
@state() private _filter?: string;
public willUpdate(properties: PropertyValues) { public willUpdate(properties: PropertyValues) {
super.willUpdate(properties); super.willUpdate(properties);
@@ -56,27 +52,16 @@ export class HaFilterEntities extends LitElement {
<div slot="header" class="header"> <div slot="header" class="header">
${this.hass.localize("ui.panel.config.entities.caption")} ${this.hass.localize("ui.panel.config.entities.caption")}
${this.value?.length ${this.value?.length
? html`<div class="badge">${this.value?.length}</div> ? html`<div class="badge">${this.value?.length}</div>`
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing} : nothing}
</div> </div>
${this._shouldRender ${this._shouldRender
? html` ? html`
<search-input-outlined <mwc-list class="ha-scrollbar">
.hass=${this.hass}
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
>
</search-input-outlined>
<mwc-list class="ha-scrollbar" multi>
<lit-virtualizer <lit-virtualizer
.items=${this._entities( .items=${this._entities(
this.hass.states, this.hass.states,
this.type, this.type,
this._filter || "",
this.value this.value
)} )}
.keyFunction=${this._keyFunction} .keyFunction=${this._keyFunction}
@@ -96,7 +81,7 @@ export class HaFilterEntities extends LitElement {
setTimeout(() => { setTimeout(() => {
if (!this.expanded) return; if (!this.expanded) return;
this.renderRoot.querySelector("mwc-list")!.style.height = this.renderRoot.querySelector("mwc-list")!.style.height =
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input `${this.clientHeight - 49}px`;
}, 300); }, 300);
} }
} }
@@ -104,20 +89,18 @@ export class HaFilterEntities extends LitElement {
private _keyFunction = (entity) => entity?.entity_id; private _keyFunction = (entity) => entity?.entity_id;
private _renderItem = (entity) => private _renderItem = (entity) =>
!entity html`<ha-check-list-item
? nothing .value=${entity.entity_id}
: html`<ha-check-list-item .selected=${this.value?.includes(entity.entity_id)}
.value=${entity.entity_id} graphic="icon"
.selected=${this.value?.includes(entity.entity_id) ?? false} >
graphic="icon" <ha-state-icon
> slot="graphic"
<ha-state-icon .hass=${this.hass}
slot="graphic" .stateObj=${entity}
.hass=${this.hass} ></ha-state-icon>
.stateObj=${entity} ${computeStateName(entity)}
></ha-state-icon> </ha-check-list-item>`;
${computeStateName(entity)}
</ha-check-list-item>`;
private _handleItemClick(ev) { private _handleItemClick(ev) {
const listItem = ev.target.closest("ha-check-list-item"); const listItem = ev.target.closest("ha-check-list-item");
@@ -142,27 +125,12 @@ export class HaFilterEntities extends LitElement {
this.expanded = ev.detail.expanded; this.expanded = ev.detail.expanded;
} }
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value.toLowerCase();
}
private _entities = memoizeOne( private _entities = memoizeOne(
( (states: HomeAssistant["states"], type: this["type"], _value) => {
states: HomeAssistant["states"],
type: this["type"],
filter: string,
_value
) => {
const values = Object.values(states); const values = Object.values(states);
return values return values
.filter( .filter(
(entityState) => (entityState) => !type || computeStateDomain(entityState) !== type
(!type || computeStateDomain(entityState) !== type) &&
(!filter ||
entityState.entity_id.toLowerCase().includes(filter) ||
entityState.attributes.friendly_name
?.toLowerCase()
.includes(filter))
) )
.sort((a, b) => .sort((a, b) =>
stringCompare( stringCompare(
@@ -209,15 +177,6 @@ export class HaFilterEntities extends LitElement {
}); });
} }
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyleScrollbar, haStyleScrollbar,
@@ -237,10 +196,6 @@ export class HaFilterEntities extends LitElement {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge { .badge {
display: inline-block; display: inline-block;
margin-left: 8px; margin-left: 8px;
@@ -261,10 +216,6 @@ export class HaFilterEntities extends LitElement {
--mdc-list-item-graphic-margin: 16px; --mdc-list-item-graphic-margin: 16px;
width: 100%; width: 100%;
} }
search-input-outlined {
display: block;
padding: 0 8px;
}
`, `,
]; ];
} }

View File

@@ -1,19 +1,17 @@
import "@material/mwc-menu/mwc-menu-surface"; import "@material/mwc-menu/mwc-menu-surface";
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js"; import { mdiTextureBox } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { computeRTL } from "../common/util/compute_rtl";
import { import {
FloorRegistryEntry, FloorRegistryEntry,
getFloorAreaLookup, getFloorAreaLookup,
subscribeFloorRegistry, subscribeFloorRegistry,
} from "../data/floor_registry"; } from "../data/floor_registry";
import { RelatedResult, findRelated } from "../data/search"; import { findRelated, RelatedResult } from "../data/search";
import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
@@ -21,7 +19,6 @@ import "./ha-check-list-item";
import "./ha-floor-icon"; import "./ha-floor-icon";
import "./ha-icon"; import "./ha-icon";
import "./ha-svg-icon"; import "./ha-svg-icon";
import "./ha-tree-indicator";
@customElement("ha-filter-floor-areas") @customElement("ha-filter-floor-areas")
export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
@@ -56,13 +53,9 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.areas.caption")} ${this.hass.localize("ui.panel.config.areas.caption")}
${this.value?.areas?.length || this.value?.floors?.length ${this.value?.areas?.length || this.value?.floors?.length
? html`<div class="badge"> ? html`<div class="badge">
${(this.value?.areas?.length || 0) + ${(this.value?.areas?.length || 0) +
(this.value?.floors?.length || 0)} (this.value?.floors?.length || 0)}
</div> </div>`
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing} : nothing}
</div> </div>
${this._shouldRender ${this._shouldRender
@@ -89,10 +82,8 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
</ha-check-list-item> </ha-check-list-item>
${repeat( ${repeat(
floor.areas, floor.areas,
(area, index) => (area) => area.area_id,
`${area.area_id}${index === floor.areas.length - 1 ? "___last" : ""}`, (area) => this._renderArea(area)
(area, index) =>
this._renderArea(area, index === floor.areas.length - 1)
)} )}
` `
)} )}
@@ -108,37 +99,23 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
`; `;
} }
private _renderArea(area, last: boolean = false) { private _renderArea(area) {
const hasFloor = !!area.floor_id; return html`<ha-check-list-item
return html` .value=${area.area_id}
<ha-check-list-item .selected=${this.value?.areas?.includes(area.area_id) || false}
.value=${area.area_id} .type=${"areas"}
.selected=${this.value?.areas?.includes(area.area_id) || false} graphic="icon"
.type=${"areas"} class=${area.floor_id ? "floor" : ""}
graphic="icon" @request-selected=${this._handleItemClick}
@request-selected=${this._handleItemClick} >
class=${classMap({ ${area.icon
rtl: computeRTL(this.hass), ? html`<ha-icon slot="graphic" .icon=${area.icon}></ha-icon>`
floor: hasFloor, : html`<ha-svg-icon
})} slot="graphic"
> .path=${mdiTextureBox}
${hasFloor ></ha-svg-icon>`}
? html` ${area.name}
<ha-tree-indicator </ha-check-list-item>`;
.end=${last}
slot="graphic"
></ha-tree-indicator>
`
: nothing}
${area.icon
? html`<ha-icon slot="graphic" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="graphic"
.path=${mdiTextureBox}
></ha-svg-icon>`}
${area.name}
</ha-check-list-item>
`;
} }
private _handleItemClick(ev) { private _handleItemClick(ev) {
@@ -261,15 +238,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
}); });
} }
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyleScrollbar, haStyleScrollbar,
@@ -289,10 +257,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge { .badge {
display: inline-block; display: inline-block;
margin-left: 8px; margin-left: 8px;
@@ -313,26 +277,9 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
--mdc-list-item-graphic-margin: 16px; --mdc-list-item-graphic-margin: 16px;
} }
.floor { .floor {
padding-left: 48px; padding-left: 32px;
padding-inline-start: 48px; padding-inline-start: 32px;
padding-inline-end: 16px;
} }
ha-tree-indicator {
width: 56px;
position: absolute;
top: 0px;
left: 0px;
}
.rtl ha-tree-indicator {
right: 0px;
left: initial;
transform: scaleX(-1);
}
.subdir {
margin-inline-end: 8px;
opacity: .6;
}
.
`, `,
]; ];
} }

View File

@@ -1,4 +1,4 @@
import { mdiFilterVariantRemove } from "@mdi/js"; import { SelectedDetail } from "@material/mwc-list";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
@@ -12,7 +12,6 @@ import {
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-domain-icon"; import "./ha-domain-icon";
import "./search-input-outlined";
@customElement("ha-filter-integrations") @customElement("ha-filter-integrations")
export class HaFilterIntegrations extends LitElement { export class HaFilterIntegrations extends LitElement {
@@ -28,8 +27,6 @@ export class HaFilterIntegrations extends LitElement {
@state() private _shouldRender = false; @state() private _shouldRender = false;
@state() private _filter?: string;
protected render() { protected render() {
return html` return html`
<ha-expansion-panel <ha-expansion-panel
@@ -41,27 +38,18 @@ export class HaFilterIntegrations extends LitElement {
<div slot="header" class="header"> <div slot="header" class="header">
${this.hass.localize("ui.panel.config.integrations.caption")} ${this.hass.localize("ui.panel.config.integrations.caption")}
${this.value?.length ${this.value?.length
? html`<div class="badge">${this.value?.length}</div> ? html`<div class="badge">${this.value?.length}</div>`
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing} : nothing}
</div> </div>
${this._manifests && this._shouldRender ${this._manifests && this._shouldRender
? html`<search-input-outlined ? html`
.hass=${this.hass}
.filter=${this._filter}
@value-changed=${this._handleSearchChange}
>
</search-input-outlined>
<mwc-list <mwc-list
class="ha-scrollbar" @selected=${this._integrationsSelected}
@click=${this._handleItemClick}
multi multi
class="ha-scrollbar"
> >
${repeat( ${repeat(
this._integrations(this._manifests, this._filter, this.value), this._integrations(this._manifests, this.value),
(i) => i.domain, (i) => i.domain,
(integration) => (integration) =>
html`<ha-check-list-item html`<ha-check-list-item
@@ -80,7 +68,8 @@ export class HaFilterIntegrations extends LitElement {
${integration.name || integration.domain} ${integration.name || integration.domain}
</ha-check-list-item>` </ha-check-list-item>`
)} )}
</mwc-list> ` </mwc-list>
`
: nothing} : nothing}
</ha-expansion-panel> </ha-expansion-panel>
`; `;
@@ -91,7 +80,7 @@ export class HaFilterIntegrations extends LitElement {
setTimeout(() => { setTimeout(() => {
if (!this.expanded) return; if (!this.expanded) return;
this.renderRoot.querySelector("mwc-list")!.style.height = this.renderRoot.querySelector("mwc-list")!.style.height =
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input `${this.clientHeight - 49}px`;
}, 300); }, 300);
} }
} }
@@ -109,17 +98,12 @@ export class HaFilterIntegrations extends LitElement {
} }
private _integrations = memoizeOne( private _integrations = memoizeOne(
(manifest: IntegrationManifest[], filter: string | undefined, _value) => (manifest: IntegrationManifest[], _value) =>
manifest manifest
.filter( .filter(
(mnfst) => (mnfst) =>
(!mnfst.integration_type || !mnfst.integration_type ||
!["entity", "system", "hardware"].includes( !["entity", "system", "hardware"].includes(mnfst.integration_type)
mnfst.integration_type
)) &&
(!filter ||
mnfst.name.toLowerCase().includes(filter) ||
mnfst.domain.toLowerCase().includes(filter))
) )
.sort((a, b) => .sort((a, b) =>
stringCompare( stringCompare(
@@ -130,38 +114,34 @@ export class HaFilterIntegrations extends LitElement {
) )
); );
private _handleItemClick(ev) { private async _integrationsSelected(
const listItem = ev.target.closest("ha-check-list-item"); ev: CustomEvent<SelectedDetail<Set<number>>>
const value = listItem?.value; ) {
if (!value) { const integrations = this._integrations(this._manifests!, this.value);
if (!ev.detail.index.size) {
fireEvent(this, "data-table-filter-changed", {
value: [],
items: undefined,
});
this.value = [];
return; return;
} }
if (this.value?.includes(value)) {
this.value = this.value?.filter((val) => val !== value); const value: string[] = [];
} else {
this.value = [...(this.value || []), value]; for (const index of ev.detail.index) {
const domain = integrations[index].domain;
value.push(domain);
} }
listItem.selected = this.value?.includes(value); this.value = value;
fireEvent(this, "data-table-filter-changed", { fireEvent(this, "data-table-filter-changed", {
value: this.value, value,
items: undefined, 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 { static get styles(): CSSResultGroup {
return [ return [
haStyleScrollbar, haStyleScrollbar,
@@ -181,10 +161,6 @@ export class HaFilterIntegrations extends LitElement {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge { .badge {
display: inline-block; display: inline-block;
margin-left: 8px; margin-left: 8px;
@@ -201,10 +177,6 @@ export class HaFilterIntegrations extends LitElement {
padding: 0px 2px; padding: 0px 2px;
color: var(--text-primary-color); color: var(--text-primary-color);
} }
search-input-outlined {
display: block;
padding: 0 8px;
}
`, `,
]; ];
} }

View File

@@ -1,18 +1,19 @@
import { SelectedDetail } from "@material/mwc-list"; import { SelectedDetail } from "@material/mwc-list";
import "@material/mwc-menu/mwc-menu-surface"; import "@material/mwc-menu/mwc-menu-surface";
import { mdiCog, mdiFilterVariantRemove } from "@mdi/js"; import { mdiPlus } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import { computeCssColor } from "../common/color/compute-color"; import { computeCssColor } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { navigate } from "../common/navigate";
import { import {
LabelRegistryEntry, LabelRegistryEntry,
createLabelRegistryEntry,
subscribeLabelRegistry, subscribeLabelRegistry,
} from "../data/label_registry"; } from "../data/label_registry";
import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-check-list-item"; import "./ha-check-list-item";
@@ -53,11 +54,7 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
<div slot="header" class="header"> <div slot="header" class="header">
${this.hass.localize("ui.panel.config.labels.caption")} ${this.hass.localize("ui.panel.config.labels.caption")}
${this.value?.length ${this.value?.length
? html`<div class="badge">${this.value?.length}</div> ? html`<div class="badge">${this.value?.length}</div>`
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing} : nothing}
</div> </div>
${this._shouldRender ${this._shouldRender
@@ -98,11 +95,11 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
${this.expanded ${this.expanded
? html`<ha-list-item ? html`<ha-list-item
graphic="icon" graphic="icon"
@click=${this._manageLabels} @click=${this._addLabel}
class="add" class="add"
> >
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon> <ha-svg-icon slot="graphic" .path=${mdiPlus}></ha-svg-icon>
${this.hass.localize("ui.panel.config.labels.manage_labels")} ${this.hass.localize("ui.panel.config.labels.add_label")}
</ha-list-item>` </ha-list-item>`
: nothing} : nothing}
`; `;
@@ -118,8 +115,10 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
} }
} }
private _manageLabels() { private _addLabel() {
navigate("/config/labels"); showLabelDetailDialog(this, {
createEntry: (values) => createLabelRegistryEntry(this.hass, values),
});
} }
private _expandedWillChange(ev) { private _expandedWillChange(ev) {
@@ -154,15 +153,6 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
}); });
} }
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyleScrollbar, haStyleScrollbar,
@@ -183,10 +173,6 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge { .badge {
display: inline-block; display: inline-block;
margin-left: 8px; margin-left: 8px;

View File

@@ -1,12 +1,11 @@
import { SelectedDetail } from "@material/mwc-list"; import { SelectedDetail } from "@material/mwc-list";
import { mdiFilterVariantRemove } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./ha-check-list-item";
import "./ha-expansion-panel"; import "./ha-expansion-panel";
import "./ha-check-list-item";
import "./ha-icon"; import "./ha-icon";
@customElement("ha-filter-states") @customElement("ha-filter-states")
@@ -44,11 +43,7 @@ export class HaFilterStates extends LitElement {
<div slot="header" class="header"> <div slot="header" class="header">
${this.label} ${this.label}
${this.value?.length ${this.value?.length
? html`<div class="badge">${this.value?.length}</div> ? html`<div class="badge">${this.value?.length}</div>`
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing} : nothing}
</div> </div>
${this._shouldRender ${this._shouldRender
@@ -62,8 +57,8 @@ export class HaFilterStates extends LitElement {
(item) => (item) =>
html`<ha-check-list-item html`<ha-check-list-item
.value=${item.value} .value=${item.value}
.selected=${this.value?.includes(item.value) ?? false} .selected=${this.value?.includes(item.value)}
.graphic=${hasIcon ? "icon" : null} .graphic=${hasIcon ? "icon" : undefined}
> >
${item.icon ${item.icon
? html`<ha-icon ? html`<ha-icon
@@ -123,15 +118,6 @@ export class HaFilterStates extends LitElement {
}); });
} }
private _clearFilter(ev) {
ev.preventDefault();
this.value = undefined;
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyleScrollbar, haStyleScrollbar,
@@ -151,10 +137,6 @@ export class HaFilterStates extends LitElement {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge { .badge {
display: inline-block; display: inline-block;
margin-left: 8px; margin-left: 8px;

View File

@@ -10,10 +10,7 @@ import {
ScorableTextItem, ScorableTextItem,
fuzzyFilterSort, fuzzyFilterSort,
} from "../common/string/filter/sequence-matching"; } from "../common/string/filter/sequence-matching";
import { import { AreaRegistryEntry } from "../data/area_registry";
AreaRegistryEntry,
updateAreaRegistryEntry,
} from "../data/area_registry";
import { import {
DeviceEntityDisplayLookup, DeviceEntityDisplayLookup,
DeviceRegistryEntry, DeviceRegistryEntry,
@@ -440,18 +437,11 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
(ev.target as any).value = this._value; (ev.target as any).value = this._value;
this.hass.loadFragmentTranslation("config");
showFloorRegistryDetailDialog(this, { showFloorRegistryDetailDialog(this, {
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "", suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
createEntry: async (values, addedAreas) => { createEntry: async (values) => {
try { try {
const floor = await createFloorRegistryEntry(this.hass, values); const floor = await createFloorRegistryEntry(this.hass, values);
addedAreas.forEach((areaId) => {
updateAreaRegistryEntry(this.hass, areaId, {
floor_id: floor.floor_id,
});
});
const floors = [...this._floors!, floor]; const floors = [...this._floors!, floor];
this.comboBox.filteredItems = this._getFloors( this.comboBox.filteredItems = this._getFloors(
floors, floors,

View File

@@ -71,10 +71,6 @@ export const computeInitialHaFormData = (
if (selector.country?.countries?.length) { if (selector.country?.countries?.length) {
data[field.name] = selector.country.countries[0]; 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) { } else if ("duration" in selector) {
data[field.name] = { data[field.name] = {
hours: 0, hours: 0,
@@ -97,9 +93,7 @@ export const computeInitialHaFormData = (
) { ) {
data[field.name] = {}; data[field.name] = {};
} else { } else {
throw new Error( throw new Error("Selector not supported in initial form data");
`Selector ${Object.keys(selector)[0]} not supported in initial form data`
);
} }
} }
}); });

View File

@@ -1,29 +1,13 @@
import { FormfieldBase } from "@material/mwc-formfield/mwc-formfield-base"; import { FormfieldBase } from "@material/mwc-formfield/mwc-formfield-base";
import { styles } from "@material/mwc-formfield/mwc-formfield.css"; import { styles } from "@material/mwc-formfield/mwc-formfield.css";
import { css, html } from "lit"; import { css } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-formfield") @customElement("ha-formfield")
export class HaFormfield extends FormfieldBase { export class HaFormfield extends FormfieldBase {
@property({ type: Boolean, reflect: true }) public disabled = false; @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() { protected _labelClick() {
const input = this.input as HTMLInputElement | undefined; const input = this.input as HTMLInputElement | undefined;
if (!input) return; if (!input) return;
@@ -55,9 +39,6 @@ export class HaFormfield extends FormfieldBase {
margin-inline-end: 10px; margin-inline-end: 10px;
margin-inline-start: inline; margin-inline-start: inline;
} }
.mdc-form-field {
align-items: var(--ha-formfield-align-items, center);
}
.mdc-form-field > label { .mdc-form-field > label {
direction: var(--direction); direction: var(--direction);
margin-inline-start: 0; margin-inline-start: 0;

View File

@@ -302,7 +302,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
name: this.hass.localize("ui.components.label-picker.no_match"), name: this.hass.localize("ui.components.label-picker.no_match"),
icon: null, icon: null,
color: null, color: null,
description: null,
}, },
]; ];
} }
@@ -316,7 +315,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
name: this.hass.localize("ui.components.label-picker.add_new"), name: this.hass.localize("ui.components.label-picker.add_new"),
icon: "mdi:plus", icon: "mdi:plus",
color: null, color: null,
description: null,
}, },
]; ];
} }
@@ -447,8 +445,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
(ev.target as any).value = this._value; (ev.target as any).value = this._value;
this.hass.loadFragmentTranslation("config");
showLabelDetailDialog(this, { showLabelDetailDialog(this, {
entry: undefined, entry: undefined,
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "", suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",

View File

@@ -1,5 +1,6 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "@material/web/ripple/ripple";
@customElement("ha-label") @customElement("ha-label")
class HaLabel extends LitElement { class HaLabel extends LitElement {
@@ -10,6 +11,7 @@ class HaLabel extends LitElement {
<span class="content"> <span class="content">
<slot name="icon"></slot> <slot name="icon"></slot>
<slot></slot> <slot></slot>
<md-ripple></md-ripple>
</span> </span>
`; `;
} }
@@ -25,6 +27,7 @@ class HaLabel extends LitElement {
0.15 0.15
); );
--ha-label-background-opacity: 1; --ha-label-background-opacity: 1;
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;
display: inline-flex; display: inline-flex;

View File

@@ -2,10 +2,8 @@ import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, TemplateResult, css, html, nothing } from "lit"; import { LitElement, TemplateResult, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../common/color/compute-color"; import { computeCssColor } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare";
import { import {
LabelRegistryEntry, LabelRegistryEntry,
subscribeLabelRegistry, subscribeLabelRegistry,
@@ -19,6 +17,7 @@ import "./chips/ha-input-chip";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-label-picker"; import "./ha-label-picker";
import type { HaLabelPicker } from "./ha-label-picker"; import type { HaLabelPicker } from "./ha-label-picker";
import { stringCompare } from "../common/string/compare";
@customElement("ha-labels-picker") @customElement("ha-labels-picker")
export class HaLabelsPicker extends SubscribeMixin(LitElement) { export class HaLabelsPicker extends SubscribeMixin(LitElement) {
@@ -103,35 +102,25 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
]; ];
} }
private _sortedLabels = memoizeOne(
(
value: string[] | undefined,
labels: { [id: string]: LabelRegistryEntry } | undefined,
language: string
) =>
value
?.map((id) => labels?.[id])
.sort((a, b) => stringCompare(a?.name || "", b?.name || "", language))
);
protected render(): TemplateResult { protected render(): TemplateResult {
const labels = this._sortedLabels( const labels = this.value
this.value, ?.map((id) => this._labels?.[id])
this._labels, .sort((a, b) =>
this.hass.locale.language stringCompare(a?.name || "", b?.name || "", this.hass.locale.language)
); );
return html` return html`
${labels?.length ${labels?.length
? html`<ha-chip-set> ? html`<ha-chip-set>
${repeat( ${repeat(
labels, labels,
(label) => label?.label_id, (label) => label?.label_id,
(label) => { (label, idx) => {
const color = label?.color const color = label?.color
? computeCssColor(label.color) ? computeCssColor(label.color)
: undefined; : undefined;
return html` return html`
<ha-input-chip <ha-input-chip
.idx=${idx}
.item=${label} .item=${label}
@remove=${this._removeItem} @remove=${this._removeItem}
@click=${this._openDetail} @click=${this._openDetail}
@@ -172,12 +161,12 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
} }
private _removeItem(ev) { private _removeItem(ev) {
const label = ev.currentTarget.item; this._value.splice(ev.target.idx, 1);
this._setValue(this._value.filter((id) => id !== label.label_id)); this._setValue([...this._value]);
} }
private _openDetail(ev) { private _openDetail(ev) {
const label = ev.currentTarget.item; const label = ev.target.item;
showLabelDetailDialog(this, { showLabelDetailDialog(this, {
entry: label, entry: label,
updateEntry: async (values) => { updateEntry: async (values) => {

View File

@@ -1,22 +1,25 @@
import { MdListItem } from "@material/web/list/list-item";
import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "element-internals-polyfill";
import { MdListItem } from "@material/web/list/list-item";
import { CSSResult, css } from "lit";
@customElement("ha-list-item-new") @customElement("ha-list-item-new")
export class HaListItemNew extends MdListItem { export class HaListItemNew extends MdListItem {
static override styles = [ static get styles(): CSSResult[] {
...super.styles, return [
css` ...MdListItem.styles,
:host { css`
--ha-icon-display: block; :host {
--md-sys-color-primary: var(--primary-text-color); --ha-icon-display: block;
--md-sys-color-secondary: var(--secondary-text-color); --md-sys-color-primary: var(--primary-text-color);
--md-sys-color-surface: var(--card-background-color); --md-sys-color-secondary: var(--secondary-text-color);
--md-sys-color-on-surface: var(--primary-text-color); --md-sys-color-surface: var(--card-background-color);
--md-sys-color-on-surface-variant: var(--secondary-text-color); --md-sys-color-on-surface: var(--primary-text-color);
} --md-sys-color-on-surface-variant: var(--secondary-text-color);
`, }
]; `,
];
}
} }
declare global { declare global {

View File

@@ -1,17 +1,20 @@
import { MdList } from "@material/web/list/list";
import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "element-internals-polyfill";
import { MdList } from "@material/web/list/list";
import { CSSResult, css } from "lit";
@customElement("ha-list-new") @customElement("ha-list-new")
export class HaListNew extends MdList { export class HaListNew extends MdList {
static override styles = [ static get styles(): CSSResult[] {
...super.styles, return [
css` ...MdList.styles,
:host { css`
--md-sys-color-surface: var(--card-background-color); :host {
} --md-sys-color-surface: var(--card-background-color);
`, }
]; `,
];
}
} }
declare global { declare global {

View File

@@ -1,11 +1,12 @@
import { MdMenuItem } from "@material/web/menu/menu-item";
import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "element-internals-polyfill";
import { CSSResult, css } from "lit";
import { MdMenuItem } from "@material/web/menu/menu-item";
@customElement("ha-menu-item") @customElement("ha-menu-item")
export class HaMenuItem extends MdMenuItem { export class HaMenuItem extends MdMenuItem {
static override styles = [ static override styles: CSSResult[] = [
...super.styles, ...MdMenuItem.styles,
css` css`
:host { :host {
--ha-icon-display: block; --ha-icon-display: block;
@@ -24,7 +25,6 @@ export class HaMenuItem extends MdMenuItem {
--md-sys-color-on-primary-container: var(--primary-text-color); --md-sys-color-on-primary-container: var(--primary-text-color);
--md-sys-color-on-secondary-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) { :host(.warning) {
--md-menu-item-label-text-color: var(--error-color); --md-menu-item-label-text-color: var(--error-color);

View File

@@ -1,11 +1,12 @@
import { MdMenu } from "@material/web/menu/menu";
import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "element-internals-polyfill";
import { CSSResult, css } from "lit";
import { MdMenu } from "@material/web/menu/menu";
@customElement("ha-menu") @customElement("ha-menu")
export class HaMenu extends MdMenu { export class HaMenu extends MdMenu {
static override styles = [ static override styles: CSSResult[] = [
...super.styles, ...MdMenu.styles,
css` css`
:host { :host {
--md-sys-color-surface-container: var(--card-background-color); --md-sys-color-surface-container: var(--card-background-color);

View File

@@ -1,6 +1,7 @@
import { MdOutlinedButton } from "@material/web/button/outlined-button";
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "element-internals-polyfill";
import { MdOutlinedButton } from "@material/web/button/outlined-button";
@customElement("ha-outlined-button") @customElement("ha-outlined-button")
export class HaOutlinedButton extends MdOutlinedButton { export class HaOutlinedButton extends MdOutlinedButton {

View File

@@ -1,39 +0,0 @@
import { MdOutlinedField } from "@material/web/field/outlined-field";
import { css } from "lit";
import { customElement } from "lit/decorators";
import { literal } from "lit/static-html";
@customElement("ha-outlined-field")
export class HaOutlinedField extends MdOutlinedField {
protected readonly fieldTag = literal`ha-outlined-field`;
static override styles = [
...super.styles,
css`
.container::before {
display: block;
content: "";
position: absolute;
inset: 0;
background-color: var(--ha-outlined-field-container-color, transparent);
opacity: var(--ha-outlined-field-container-opacity, 1);
border-start-start-radius: var(--_container-shape-start-start);
border-start-end-radius: var(--_container-shape-start-end);
border-end-start-radius: var(--_container-shape-end-start);
border-end-end-radius: var(--_container-shape-end-end);
}
.with-start .start {
margin-inline-end: var(--ha-outlined-field-start-margin, 4px);
}
.with-end .end {
margin-inline-start: var(--ha-outlined-field-end-margin, 4px);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-outlined-field": HaOutlinedField;
}
}

View File

@@ -1,6 +1,7 @@
import { MdOutlinedIconButton } from "@material/web/iconbutton/outlined-icon-button";
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "element-internals-polyfill";
import { MdOutlinedIconButton } from "@material/web/iconbutton/outlined-icon-button";
@customElement("ha-outlined-icon-button") @customElement("ha-outlined-icon-button")
export class HaOutlinedIconButton extends MdOutlinedIconButton { export class HaOutlinedIconButton extends MdOutlinedIconButton {

View File

@@ -1,13 +1,10 @@
import { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field"; import { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field";
import "element-internals-polyfill";
import { css } from "lit"; import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { literal } from "lit/static-html";
import "./ha-outlined-field";
@customElement("ha-outlined-text-field") @customElement("ha-outlined-text-field")
export class HaOutlinedTextField extends MdOutlinedTextField { export class HaOutlinedTextField extends MdOutlinedTextField {
protected readonly fieldTag = literal`ha-outlined-field`;
static override styles = [ static override styles = [
...super.styles, ...super.styles,
css` css`
@@ -28,8 +25,6 @@ export class HaOutlinedTextField extends MdOutlinedTextField {
--md-outlined-field-container-shape-end-end: 10px; --md-outlined-field-container-shape-end-end: 10px;
--md-outlined-field-container-shape-end-start: 10px; --md-outlined-field-container-shape-end-start: 10px;
--md-outlined-field-focus-outline-width: 1px; --md-outlined-field-focus-outline-width: 1px;
--ha-outlined-field-start-margin: -4px;
--ha-outlined-field-end-margin: -4px;
--mdc-icon-size: var(--md-input-chip-icon-size, 18px); --mdc-icon-size: var(--md-input-chip-icon-size, 18px);
} }
.input { .input {

View File

@@ -1,62 +0,0 @@
import { AttachableController } from "@material/web/internal/controller/attachable-controller";
import { MdRipple } from "@material/web/ripple/ripple";
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;
}
}

View File

@@ -8,10 +8,7 @@ import {
fetchEntitySourcesWithCache, fetchEntitySourcesWithCache,
} from "../../data/entity_sources"; } from "../../data/entity_sources";
import type { EntitySelector } from "../../data/selector"; import type { EntitySelector } from "../../data/selector";
import { import { filterSelectorEntities } from "../../data/selector";
filterSelectorEntities,
computeCreateDomains,
} from "../../data/selector";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../entity/ha-entities-picker"; import "../entity/ha-entities-picker";
import "../entity/ha-entity-picker"; import "../entity/ha-entity-picker";
@@ -34,8 +31,6 @@ export class HaEntitySelector extends LitElement {
@property({ type: Boolean }) public required = true; @property({ type: Boolean }) public required = true;
@state() private _createDomains: string[] | undefined;
private _hasIntegration(selector: EntitySelector) { private _hasIntegration(selector: EntitySelector) {
return ( return (
selector.entity?.filter && selector.entity?.filter &&
@@ -69,7 +64,6 @@ export class HaEntitySelector extends LitElement {
.includeEntities=${this.selector.entity?.include_entities} .includeEntities=${this.selector.entity?.include_entities}
.excludeEntities=${this.selector.entity?.exclude_entities} .excludeEntities=${this.selector.entity?.exclude_entities}
.entityFilter=${this._filterEntities} .entityFilter=${this._filterEntities}
.createDomains=${this._createDomains}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}
allow-custom-entity allow-custom-entity
@@ -85,7 +79,6 @@ export class HaEntitySelector extends LitElement {
.includeEntities=${this.selector.entity.include_entities} .includeEntities=${this.selector.entity.include_entities}
.excludeEntities=${this.selector.entity.exclude_entities} .excludeEntities=${this.selector.entity.exclude_entities}
.entityFilter=${this._filterEntities} .entityFilter=${this._filterEntities}
.createDomains=${this._createDomains}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}
></ha-entities-picker> ></ha-entities-picker>
@@ -103,9 +96,6 @@ export class HaEntitySelector extends LitElement {
this._entitySources = sources; this._entitySources = sources;
}); });
} }
if (changedProps.has("selector")) {
this._createDomains = computeCreateDomains(this.selector);
}
} }
private _filterEntities = (entity: HassEntity): boolean => { private _filterEntities = (entity: HassEntity): boolean => {

View File

@@ -30,7 +30,6 @@ export class HaLabelSelector extends LitElement {
if (this.selector.label.multiple) { if (this.selector.label.multiple) {
return html` return html`
<ha-labels-picker <ha-labels-picker
no-add
.hass=${this.hass} .hass=${this.hass}
.value=${ensureArray(this.value ?? [])} .value=${ensureArray(this.value ?? [])}
.disabled=${this.disabled} .disabled=${this.disabled}
@@ -42,7 +41,6 @@ export class HaLabelSelector extends LitElement {
} }
return html` return html`
<ha-label-picker <ha-label-picker
no-add
.hass=${this.hass} .hass=${this.hass}
.value=${this.value} .value=${this.value}
.disabled=${this.disabled} .disabled=${this.disabled}

View File

@@ -7,10 +7,8 @@ import type {
LocationSelectorValue, LocationSelectorValue,
} from "../../data/selector"; } from "../../data/selector";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import type { SchemaUnion } from "../ha-form/types";
import type { MarkerLocation } from "../map/ha-locations-editor"; import type { MarkerLocation } from "../map/ha-locations-editor";
import "../map/ha-locations-editor"; import "../map/ha-locations-editor";
import "../ha-form/ha-form";
@customElement("ha-selector-location") @customElement("ha-selector-location")
export class HaLocationSelector extends LitElement { export class HaLocationSelector extends LitElement {
@@ -26,49 +24,6 @@ export class HaLocationSelector extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false; @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() { protected render() {
return html` return html`
<p>${this.label ? this.label : ""}</p> <p>${this.label ? this.label : ""}</p>
@@ -80,17 +35,6 @@ export class HaLocationSelector extends LitElement {
@location-updated=${this._locationChanged} @location-updated=${this._locationChanged}
@radius-updated=${this._radiusChanged} @radius-updated=${this._radiusChanged}
></ha-locations-editor> ></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>
`; `;
} }
@@ -122,8 +66,7 @@ export class HaLocationSelector extends LitElement {
? "mdi:map-marker-radius" ? "mdi:map-marker-radius"
: "mdi:map-marker", : "mdi:map-marker",
location_editable: true, location_editable: true,
radius_editable: radius_editable: true,
!!selector.location?.radius && !selector.location?.radius_readonly,
}, },
]; ];
} }
@@ -137,39 +80,14 @@ export class HaLocationSelector extends LitElement {
} }
private _radiusChanged(ev: CustomEvent) { private _radiusChanged(ev: CustomEvent) {
const radius = Math.round(ev.detail.radius); const radius = ev.detail.radius;
fireEvent(this, "value-changed", { value: { ...this.value, 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` static styles = css`
ha-locations-editor { ha-locations-editor {
display: block; display: block;
height: 400px; height: 400px;
margin-bottom: 16px;
} }
p { p {
margin-top: 0; margin-top: 0;

View File

@@ -278,14 +278,6 @@ export class HaSelectSelector extends LitElement {
private _valueChanged(ev) { private _valueChanged(ev) {
ev.stopPropagation(); 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; const value = ev.detail?.value || ev.target.value;
if (this.disabled || value === undefined || value === (this.value ?? "")) { if (this.disabled || value === undefined || value === (this.value ?? "")) {
return; return;

View File

@@ -22,7 +22,6 @@ import {
filterSelectorDevices, filterSelectorDevices,
filterSelectorEntities, filterSelectorEntities,
TargetSelector, TargetSelector,
computeCreateDomains,
} from "../../data/selector"; } from "../../data/selector";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../ha-target-picker"; import "../ha-target-picker";
@@ -43,8 +42,6 @@ export class HaTargetSelector extends LitElement {
@state() private _entitySources?: EntitySources; @state() private _entitySources?: EntitySources;
@state() private _createDomains: string[] | undefined;
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup); private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
private _hasIntegration(selector: TargetSelector) { private _hasIntegration(selector: TargetSelector) {
@@ -71,9 +68,6 @@ export class HaTargetSelector extends LitElement {
this._entitySources = sources; this._entitySources = sources;
}); });
} }
if (changedProperties.has("selector")) {
this._createDomains = computeCreateDomains(this.selector);
}
} }
protected render() { protected render() {
@@ -88,7 +82,6 @@ export class HaTargetSelector extends LitElement {
.deviceFilter=${this._filterDevices} .deviceFilter=${this._filterDevices}
.entityFilter=${this._filterEntities} .entityFilter=${this._filterEntities}
.disabled=${this.disabled} .disabled=${this.disabled}
.createDomains=${this._createDomains}
></ha-target-picker>`; ></ha-target-picker>`;
} }

View File

@@ -33,7 +33,6 @@ import {
expandFloorTarget, expandFloorTarget,
expandLabelTarget, expandLabelTarget,
Selector, Selector,
TargetSelector,
} from "../data/selector"; } from "../data/selector";
import { HomeAssistant, ValueChangedEvent } from "../types"; import { HomeAssistant, ValueChangedEvent } from "../types";
import { documentationUrl } from "../util/documentation-url"; import { documentationUrl } from "../util/documentation-url";
@@ -364,11 +363,6 @@ export class HaServiceControl extends LitElement {
return false; return false;
} }
private _targetSelector = memoizeOne(
(targetSelector: TargetSelector | null | undefined) =>
targetSelector ? { target: { ...targetSelector } } : { target: {} }
);
protected render() { protected render() {
const serviceData = this._getServiceInfo( const serviceData = this._getServiceInfo(
this._value?.service, this._value?.service,
@@ -407,151 +401,157 @@ export class HaServiceControl extends LitElement {
)) || )) ||
serviceData?.description; serviceData?.description;
return html`${this.hidePicker return html`
? nothing ${this.hidePicker
: html`<ha-service-picker ? nothing
.hass=${this.hass} : html`<ha-service-picker
.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} .hass=${this.hass}
.selector=${this._targetSelector( .value=${this._value?.service}
serviceData.target as TargetSelector
)}
.disabled=${this.disabled} .disabled=${this.disabled}
@value-changed=${this._targetChanged} @value-changed=${this._serviceChanged}
.value=${this._value?.target} ></ha-service-picker>`}
></ha-selector ${this.hideDescription
></ha-settings-row>` ? nothing
: entityId : html`
? html`<ha-entity-picker <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
.hass=${this.hass} .hass=${this.hass}
.disabled=${this.disabled} .label=${this.hass.localize("ui.components.service-control.data")}
.value=${this._value?.data?.entity_id} .name=${"data"}
.label=${this.hass.localize( .readOnly=${this.disabled}
`component.${domain}.services.${serviceName}.fields.entity_id.description` .defaultValue=${this._value?.data}
) || entityId.description} @value-changed=${this._dataChanged}
@value-changed=${this._entityPicked} ></ha-yaml-editor>`
allow-custom-entity : filteredFields?.map((dataField) => {
></ha-entity-picker>` const selector = dataField?.selector ?? { text: undefined };
: ""} const type = Object.keys(selector)[0];
${shouldRenderServiceDataYaml const enhancedSelector = [
? html`<ha-yaml-editor "action",
.hass=${this.hass} "condition",
.label=${this.hass.localize("ui.components.service-control.data")} "trigger",
.name=${"data"} ].includes(type)
.readOnly=${this.disabled} ? {
.defaultValue=${this._value?.data} [type]: {
@value-changed=${this._dataChanged} ...selector[type],
></ha-yaml-editor>` path: [dataField.key],
: filteredFields?.map((dataField) => { },
const selector = dataField?.selector ?? { text: undefined }; }
const type = Object.keys(selector)[0]; : selector;
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 && return dataField.selector &&
(!dataField.advanced || (!dataField.advanced ||
this.showAdvanced || this.showAdvanced ||
(this._value?.data && (this._value?.data &&
this._value.data[dataField.key] !== undefined)) this._value.data[dataField.key] !== undefined))
? html`<ha-settings-row .narrow=${this.narrow}> ? html`<ha-settings-row .narrow=${this.narrow}>
${!showOptional ${!showOptional
? hasOptional ? hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>` ? html`<div slot="prefix" class="checkbox-spacer"></div>`
: "" : ""
: html`<ha-checkbox : html`<ha-checkbox
.key=${dataField.key} .key=${dataField.key}
.checked=${this._checkedKeys.has(dataField.key) || .checked=${this._checkedKeys.has(dataField.key) ||
(this._value?.data && (this._value?.data &&
this._value.data[dataField.key] !== undefined)} this._value.data[dataField.key] !== undefined)}
.disabled=${this.disabled} .disabled=${this.disabled}
@change=${this._checkboxChanged} @change=${this._checkboxChanged}
slot="prefix" slot="prefix"
></ha-checkbox>`} ></ha-checkbox>`}
<span slot="heading" <span slot="heading"
>${this.hass.localize( >${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.${dataField.key}.name` `component.${domain}.services.${serviceName}.fields.${dataField.key}.name`
) || ) ||
dataField.name || dataField.name ||
dataField.key}</span dataField.key}</span
> >
<span slot="description" <span slot="description"
>${this.hass.localize( >${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description` `component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
) || dataField?.description}</span ) || dataField?.description}</span
> >
<ha-selector <ha-selector
.disabled=${this.disabled || .disabled=${this.disabled ||
(showOptional && (showOptional &&
!this._checkedKeys.has(dataField.key) && !this._checkedKeys.has(dataField.key) &&
(!this._value?.data || (!this._value?.data ||
this._value.data[dataField.key] === undefined))} this._value.data[dataField.key] === undefined))}
.hass=${this.hass} .hass=${this.hass}
.selector=${enhancedSelector} .selector=${enhancedSelector}
.key=${dataField.key} .key=${dataField.key}
@value-changed=${this._serviceDataChanged} @value-changed=${this._serviceDataChanged}
.value=${this._value?.data .value=${this._value?.data
? this._value.data[dataField.key] ? this._value.data[dataField.key]
: undefined} : undefined}
.placeholder=${dataField.default} .placeholder=${dataField.default}
.localizeValue=${this._localizeValueCallback} .localizeValue=${this._localizeValueCallback}
@item-moved=${this._itemMoved} @item-moved=${this._itemMoved}
></ha-selector> ></ha-selector>
</ha-settings-row>` </ha-settings-row>`
: ""; : "";
})} `; })}
`;
} }
private _localizeValueCallback = (key: string) => { private _localizeValueCallback = (key: string) => {

View File

@@ -8,9 +8,6 @@ export class HaSettingsRow extends LitElement {
@property({ type: Boolean, attribute: "three-line" }) @property({ type: Boolean, attribute: "three-line" })
public threeLine = false; public threeLine = false;
@property({ type: Boolean, attribute: "wrap-heading", reflect: true })
public wrapHeading = false;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div class="prefix-wrap"> <div class="prefix-wrap">
@@ -54,7 +51,7 @@ export class HaSettingsRow extends LitElement {
.body[three-line] { .body[three-line] {
min-height: var(--paper-item-body-three-line-min-height, 88px); min-height: var(--paper-item-body-three-line-min-height, 88px);
} }
:host(:not([wrap-heading])) body > * { .body > * {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;

View File

@@ -1,6 +1,7 @@
import { MdSlider } from "@material/web/slider/slider";
import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "element-internals-polyfill";
import { MdSlider } from "@material/web/slider/slider";
import { CSSResult, css } from "lit";
import { mainWindow } from "../common/dom/get_main_window"; import { mainWindow } from "../common/dom/get_main_window";
@customElement("ha-slider") @customElement("ha-slider")
@@ -10,8 +11,8 @@ export class HaSlider extends MdSlider {
this.dir = mainWindow.document.dir; this.dir = mainWindow.document.dir;
} }
static override styles = [ static override styles: CSSResult[] = [
...super.styles, ...MdSlider.styles,
css` css`
:host { :host {
--md-sys-color-primary: var(--primary-color); --md-sys-color-primary: var(--primary-color);

View File

@@ -82,7 +82,7 @@ export class HaSortable extends LitElement {
public connectedCallback() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this._shouldBeDestroy = false; this._shouldBeDestroy = false;
if (this.hasUpdated && !this.disabled) { if (this.hasUpdated) {
this._createSortable(); this._createSortable();
} }
} }

View File

@@ -1,16 +1,13 @@
import { MdSubMenu } from "@material/web/menu/sub-menu";
import { css } from "lit";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import "element-internals-polyfill";
import { CSSResult, css } from "lit";
import { MdSubMenu } from "@material/web/menu/sub-menu";
@customElement("ha-sub-menu") @customElement("ha-sub-menu")
// @ts-expect-error
export class HaSubMenu extends MdSubMenu { export class HaSubMenu extends MdSubMenu {
async show() { static override styles: CSSResult[] = [
super.show(); MdSubMenu.styles,
this.menu.hasOverflow = false;
}
static override styles = [
...super.styles,
css` css`
:host { :host {
--ha-icon-display: block; --ha-icon-display: block;

View File

@@ -1,7 +1,15 @@
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 { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; import {
customElement,
eventOptions,
property,
queryAsync,
state,
} from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import "./ha-ripple";
@customElement("ha-tab") @customElement("ha-tab")
export class HaTab extends LitElement { export class HaTab extends LitElement {
@@ -11,6 +19,10 @@ export class HaTab extends LitElement {
@property() public name?: string; @property() public name?: string;
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
@state() private _shouldRenderRipple = false;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div <div
@@ -18,21 +30,60 @@ export class HaTab extends LitElement {
role="tab" role="tab"
aria-selected=${this.active} aria-selected=${this.active}
aria-label=${ifDefined(this.name)} 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} @keydown=${this._handleKeyDown}
> >
${this.narrow ? html`<slot name="icon"></slot>` : ""} ${this.narrow ? html`<slot name="icon"></slot>` : ""}
<span class="name">${this.name}</span> <span class="name">${this.name}</span>
<ha-ripple></ha-ripple> ${this._shouldRenderRipple ? html`<mwc-ripple></mwc-ripple>` : ""}
</div> </div>
`; `;
} }
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
this._shouldRenderRipple = true;
return this._ripple;
});
private _handleKeyDown(ev: KeyboardEvent): void { private _handleKeyDown(ev: KeyboardEvent): void {
if (ev.key === "Enter") { if (ev.key === "Enter") {
(ev.target as HTMLElement).click(); (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 { static get styles(): CSSResultGroup {
return css` return css`
div { div {
@@ -75,15 +126,6 @@ export class HaTab extends LitElement {
:host([narrow]) div { :host([narrow]) div {
padding: 0 4px; padding: 0 4px;
} }
div:focus-visible:before {
position: absolute;
display: block;
content: "";
inset: 0;
background-color: var(--secondary-text-color);
opacity: 0.08;
}
`; `;
} }
} }

View File

@@ -65,8 +65,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property() public helper?: string; @property() public helper?: string;
@property({ type: Array }) public createDomains?: string[];
/** /**
* Show only targets with entities from specific domains. * Show only targets with entities from specific domains.
* @type {Array} * @type {Array}
@@ -470,7 +468,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
.includeDeviceClasses=${this.includeDeviceClasses} .includeDeviceClasses=${this.includeDeviceClasses}
.includeDomains=${this.includeDomains} .includeDomains=${this.includeDomains}
.excludeEntities=${ensureArray(this.value?.entity_id)} .excludeEntities=${ensureArray(this.value?.entity_id)}
.createDomains=${this.createDomains}
@value-changed=${this._targetPicked} @value-changed=${this._targetPicked}
@click=${this._preventDefault} @click=${this._preventDefault}
allow-custom-entity allow-custom-entity

Some files were not shown because too many files have changed in this diff Show More