mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-19 07:16:39 +00:00
20240424.0 (#20602)
This commit is contained in:
commit
8712adbf8d
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@ -57,7 +57,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
12
.github/workflows/ci.yaml
vendored
12
.github/workflows/ci.yaml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
@ -58,7 +58,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
@ -76,7 +76,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
@ -89,7 +89,7 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
uses: actions/upload-artifact@v4.3.2
|
||||
with:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
@ -100,7 +100,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
@ -113,7 +113,7 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
uses: actions/upload-artifact@v4.3.2
|
||||
with:
|
||||
name: supervisor-bundle-stats
|
||||
path: build/stats/*.json
|
||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@ -22,7 +22,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@ -58,7 +58,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
|
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
|
6
.github/workflows/nightly.yaml
vendored
6
.github/workflows/nightly.yaml
vendored
@ -20,7 +20,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v5
|
||||
@ -57,14 +57,14 @@ jobs:
|
||||
run: tar -czvf translations.tar.gz translations
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
uses: actions/upload-artifact@v4.3.2
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
uses: actions/upload-artifact@v4.3.2
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
@ -9,7 +9,7 @@ import gulp from "gulp";
|
||||
import jszip from "jszip";
|
||||
import path from "path";
|
||||
import process from "process";
|
||||
import tar from "tar";
|
||||
import { extract } from "tar";
|
||||
|
||||
const MAX_AGE = 24; // hours
|
||||
const OWNER = "home-assistant";
|
||||
@ -156,7 +156,7 @@ gulp.task("fetch-nightly-translations", async function () {
|
||||
console.log("Unpacking downloaded translations...");
|
||||
const zip = await jszip.loadAsync(downloadResponse.data);
|
||||
await deleteCurrent;
|
||||
const extractStream = zip.file(/.*/)[0].nodeStream().pipe(tar.extract());
|
||||
const extractStream = zip.file(/.*/)[0].nodeStream().pipe(extract());
|
||||
await new Promise((resolve, reject) => {
|
||||
extractStream.on("close", resolve).on("error", reject);
|
||||
});
|
||||
|
@ -1,92 +1,76 @@
|
||||
import { createHash } from "crypto";
|
||||
import { deleteSync } from "del";
|
||||
import { mkdirSync, readdirSync, readFileSync, renameSync } from "fs";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { deleteAsync } from "del";
|
||||
import { glob } from "glob";
|
||||
import gulp from "gulp";
|
||||
import flatmap from "gulp-flatmap";
|
||||
import transform from "gulp-json-transform";
|
||||
import merge from "gulp-merge-json";
|
||||
import rename from "gulp-rename";
|
||||
import path from "path";
|
||||
import vinylBuffer from "vinyl-buffer";
|
||||
import source from "vinyl-source-stream";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, readFile } from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
import { Transform } from "node:stream";
|
||||
import { finished } from "node:stream/promises";
|
||||
import env from "../env.cjs";
|
||||
import paths from "../paths.cjs";
|
||||
import { mapFiles } from "../util.cjs";
|
||||
import "./fetch-nightly-translations.js";
|
||||
|
||||
const inFrontendDir = "translations/frontend";
|
||||
const inBackendDir = "translations/backend";
|
||||
const workDir = "build/translations";
|
||||
const fullDir = workDir + "/full";
|
||||
const coreDir = workDir + "/core";
|
||||
const outDir = workDir + "/output";
|
||||
const outDir = join(workDir, "output");
|
||||
const EN_SRC = join(paths.translations_src, "en.json");
|
||||
|
||||
let mergeBackend = false;
|
||||
|
||||
gulp.task(
|
||||
"translations-enable-merge-backend",
|
||||
gulp.parallel((done) => {
|
||||
gulp.parallel(async () => {
|
||||
mergeBackend = true;
|
||||
done();
|
||||
}, "allow-setup-fetch-nightly-translations")
|
||||
);
|
||||
|
||||
// Panel translations which should be split from the core translations.
|
||||
const TRANSLATION_FRAGMENTS = Object.keys(
|
||||
JSON.parse(
|
||||
readFileSync(
|
||||
path.resolve(paths.polymer_dir, "src/translations/en.json"),
|
||||
"utf-8"
|
||||
)
|
||||
).ui.panel
|
||||
);
|
||||
|
||||
function recursiveFlatten(prefix, data) {
|
||||
let output = {};
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (typeof data[key] === "object") {
|
||||
output = {
|
||||
...output,
|
||||
...recursiveFlatten(prefix + key + ".", data[key]),
|
||||
};
|
||||
} else {
|
||||
output[prefix + key] = data[key];
|
||||
// Transform stream to apply a function on Vinyl JSON files (buffer mode only).
|
||||
// The provided function can either return a new object, or an array of
|
||||
// [object, subdirectory] pairs for fragmentizing the JSON.
|
||||
class CustomJSON extends Transform {
|
||||
constructor(func, reviver = null) {
|
||||
super({ objectMode: true });
|
||||
this._func = func;
|
||||
this._reviver = reviver;
|
||||
}
|
||||
|
||||
async _transform(file, _, callback) {
|
||||
try {
|
||||
let obj = JSON.parse(file.contents.toString(), this._reviver);
|
||||
if (this._func) obj = this._func(obj, file.path);
|
||||
for (const [outObj, dir] of Array.isArray(obj) ? obj : [[obj, ""]]) {
|
||||
const outFile = file.clone({ contents: false });
|
||||
outFile.contents = Buffer.from(JSON.stringify(outObj));
|
||||
outFile.dirname += `/${dir}`;
|
||||
this.push(outFile);
|
||||
}
|
||||
callback(null);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utility to flatten object keys to single level using separator
|
||||
const flatten = (data, prefix = "", sep = ".") => {
|
||||
const output = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (typeof value === "object") {
|
||||
Object.assign(output, flatten(value, prefix + key + sep, sep));
|
||||
} else {
|
||||
output[prefix + key] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
return output;
|
||||
}
|
||||
};
|
||||
|
||||
function flatten(data) {
|
||||
return recursiveFlatten("", data);
|
||||
}
|
||||
|
||||
function emptyFilter(data) {
|
||||
const newData = {};
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (data[key]) {
|
||||
if (typeof data[key] === "object") {
|
||||
newData[key] = emptyFilter(data[key]);
|
||||
} else {
|
||||
newData[key] = data[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
return newData;
|
||||
}
|
||||
|
||||
function recursiveEmpty(data) {
|
||||
const newData = {};
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (data[key]) {
|
||||
if (typeof data[key] === "object") {
|
||||
newData[key] = recursiveEmpty(data[key]);
|
||||
} else {
|
||||
newData[key] = "TRANSLATED";
|
||||
}
|
||||
}
|
||||
});
|
||||
return newData;
|
||||
}
|
||||
// Filter functions that can be passed directly to JSON.parse()
|
||||
const emptyReviver = (_key, value) => value || undefined;
|
||||
const testReviver = (_key, value) =>
|
||||
value && typeof value === "string" ? "TRANSLATED" : value;
|
||||
|
||||
/**
|
||||
* Replace Lokalise key placeholders with their actual values.
|
||||
@ -95,60 +79,44 @@ function recursiveEmpty(data) {
|
||||
* be included in src/translations/en.json, but still be usable while
|
||||
* developing locally.
|
||||
*
|
||||
* @link https://docs.lokalise.co/article/KO5SZWLLsy-key-referencing
|
||||
* @link https://docs.lokalise.com/en/articles/1400528-key-referencing
|
||||
*/
|
||||
const re_key_reference = /\[%key:([^%]+)%\]/;
|
||||
function lokaliseTransform(data, original, file) {
|
||||
const KEY_REFERENCE = /\[%key:([^%]+)%\]/;
|
||||
const lokaliseTransform = (data, path, original = data) => {
|
||||
const output = {};
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (value instanceof Object) {
|
||||
output[key] = lokaliseTransform(value, original, file);
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (typeof value === "object") {
|
||||
output[key] = lokaliseTransform(value, path, original);
|
||||
} else {
|
||||
output[key] = value.replace(re_key_reference, (_match, lokalise_key) => {
|
||||
output[key] = value.replace(KEY_REFERENCE, (_match, lokalise_key) => {
|
||||
const replace = lokalise_key.split("::").reduce((tr, k) => {
|
||||
if (!tr) {
|
||||
throw Error(
|
||||
`Invalid key placeholder ${lokalise_key} in ${file.path}`
|
||||
);
|
||||
throw Error(`Invalid key placeholder ${lokalise_key} in ${path}`);
|
||||
}
|
||||
return tr[k];
|
||||
}, original);
|
||||
if (typeof replace !== "string") {
|
||||
throw Error(
|
||||
`Invalid key placeholder ${lokalise_key} in ${file.path}`
|
||||
);
|
||||
throw Error(`Invalid key placeholder ${lokalise_key} in ${path}`);
|
||||
}
|
||||
return replace;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
};
|
||||
|
||||
gulp.task("clean-translations", async () => deleteSync([workDir]));
|
||||
gulp.task("clean-translations", () => deleteAsync([workDir]));
|
||||
|
||||
gulp.task("ensure-translations-build-dir", async () => {
|
||||
mkdirSync(workDir, { recursive: true });
|
||||
});
|
||||
const makeWorkDir = () => mkdir(workDir, { recursive: true });
|
||||
|
||||
gulp.task("create-test-metadata", () =>
|
||||
env.isProdBuild()
|
||||
? Promise.resolve()
|
||||
: writeFile(
|
||||
workDir + "/testMetadata.json",
|
||||
JSON.stringify({ test: { nativeName: "Test" } })
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task("create-test-translation", () =>
|
||||
const createTestTranslation = () =>
|
||||
env.isProdBuild()
|
||||
? Promise.resolve()
|
||||
: gulp
|
||||
.src(path.join(paths.translations_src, "en.json"))
|
||||
.pipe(transform((data, _file) => recursiveEmpty(data)))
|
||||
.src(EN_SRC)
|
||||
.pipe(new CustomJSON(null, testReviver))
|
||||
.pipe(rename("test.json"))
|
||||
.pipe(gulp.dest(workDir))
|
||||
);
|
||||
.pipe(gulp.dest(workDir));
|
||||
|
||||
/**
|
||||
* This task will build a master translation file, to be used as the base for
|
||||
@ -159,279 +127,171 @@ gulp.task("create-test-translation", () =>
|
||||
* project is buildable immediately after merging new translation keys, since
|
||||
* the Lokalise update to translations/en.json will not happen immediately.
|
||||
*/
|
||||
gulp.task("build-master-translation", () => {
|
||||
const src = [path.join(paths.translations_src, "en.json")];
|
||||
|
||||
if (mergeBackend) {
|
||||
src.push(path.join(inBackendDir, "en.json"));
|
||||
}
|
||||
|
||||
return gulp
|
||||
.src(src)
|
||||
.pipe(transform((data, file) => lokaliseTransform(data, data, file)))
|
||||
const createMasterTranslation = () =>
|
||||
gulp
|
||||
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])])
|
||||
.pipe(new CustomJSON(lokaliseTransform))
|
||||
.pipe(
|
||||
merge({
|
||||
fileName: "en.json",
|
||||
jsonSpace: undefined,
|
||||
})
|
||||
)
|
||||
.pipe(gulp.dest(fullDir));
|
||||
});
|
||||
.pipe(gulp.dest(workDir));
|
||||
|
||||
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)))
|
||||
const FRAGMENTS = ["base"];
|
||||
|
||||
const toggleSupervisorFragment = async () => {
|
||||
FRAGMENTS[0] = "supervisor";
|
||||
};
|
||||
|
||||
const panelFragment = (fragment) =>
|
||||
fragment !== "base" && fragment !== "supervisor";
|
||||
|
||||
const HASHES = new Map();
|
||||
|
||||
const createTranslations = async () => {
|
||||
// Parse and store the master to avoid repeating this for each locale, then
|
||||
// add the panel fragments when processing the app.
|
||||
const enMaster = JSON.parse(await readFile(`${workDir}/en.json`, "utf-8"));
|
||||
if (FRAGMENTS[0] === "base") {
|
||||
FRAGMENTS.push(...Object.keys(enMaster.ui.panel));
|
||||
}
|
||||
|
||||
// The downstream pipeline is setup first. It hashes the merged data for
|
||||
// each locale, then fragmentizes and flattens the data for final output.
|
||||
const translationFiles = await glob([
|
||||
`${inFrontendDir}/!(en).json`,
|
||||
...(env.isProdBuild() ? [] : [`${workDir}/test.json`]),
|
||||
]);
|
||||
const hashStream = new Transform({
|
||||
objectMode: true,
|
||||
transform: async (file, _, callback) => {
|
||||
const hash = env.isProdBuild()
|
||||
? createHash("md5").update(file.contents).digest("hex")
|
||||
: "dev";
|
||||
HASHES.set(file.stem, hash);
|
||||
file.stem += `-${hash}`;
|
||||
callback(null, file);
|
||||
},
|
||||
}).setMaxListeners(translationFiles.length + 1);
|
||||
const fragmentsStream = hashStream
|
||||
.pipe(
|
||||
flatmap((stream, file) => {
|
||||
// For each language generate a merged json file. It begins with the master
|
||||
new CustomJSON((data) =>
|
||||
FRAGMENTS.map((fragment) => {
|
||||
switch (fragment) {
|
||||
case "base":
|
||||
// Remove the panels and supervisor to create the base translations
|
||||
return [
|
||||
flatten({
|
||||
...data,
|
||||
ui: { ...data.ui, panel: undefined },
|
||||
supervisor: undefined,
|
||||
}),
|
||||
"",
|
||||
];
|
||||
case "supervisor":
|
||||
// Supervisor key is at the top level
|
||||
return [flatten(data.supervisor), ""];
|
||||
default:
|
||||
// Create a fragment with only the given panel
|
||||
return [
|
||||
flatten(data.ui.panel[fragment], `ui.panel.${fragment}.`),
|
||||
fragment,
|
||||
];
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
.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.
|
||||
const tr = path.basename(file.history[0], ".json");
|
||||
const subtags = tr.split("-");
|
||||
const src = [fullDir + "/en.json"];
|
||||
gulp.src(`${workDir}/en.json`).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") {
|
||||
src.push(workDir + "/test.json");
|
||||
mergeFiles.push(`${workDir}/test.json`);
|
||||
} else if (lang !== "en") {
|
||||
src.push(inFrontendDir + "/" + lang + ".json");
|
||||
mergeFiles.push(`${inFrontendDir}/${lang}.json`);
|
||||
if (mergeBackend) {
|
||||
src.push(inBackendDir + "/" + lang + ".json");
|
||||
mergeFiles.push(`${inBackendDir}/${lang}.json`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return gulp
|
||||
.src(src, { allowEmpty: true })
|
||||
.pipe(transform((data) => emptyFilter(data)))
|
||||
.pipe(
|
||||
const mergeStream = gulp.src(mergeFiles, { allowEmpty: true }).pipe(
|
||||
merge({
|
||||
fileName: tr + ".json",
|
||||
fileName: `${locale}.json`,
|
||||
startObj: enMaster,
|
||||
jsonReviver: emptyReviver,
|
||||
jsonSpace: undefined,
|
||||
})
|
||||
)
|
||||
.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 = "";
|
||||
mergesFinished.push(finished(mergeStream));
|
||||
mergeStream.pipe(hashStream, { end: false });
|
||||
}
|
||||
// In dev we create the file with the fake hash in the filename
|
||||
|
||||
// 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()) {
|
||||
filePath.basename += "-dev";
|
||||
meta.test = { nativeName: "Test" };
|
||||
}
|
||||
})
|
||||
)
|
||||
.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 {
|
||||
// Filter out locales without a native name, and add the hashes.
|
||||
for (const locale of Object.keys(meta)) {
|
||||
if (!meta[locale].nativeName) {
|
||||
meta[locale] = undefined;
|
||||
console.warn(
|
||||
`Skipping language ${key}. Native name was not translated.`
|
||||
`Skipping locale ${locale} because native name is not translated.`
|
||||
);
|
||||
} else {
|
||||
meta[locale].hash = HASHES.get(locale);
|
||||
}
|
||||
});
|
||||
return newData;
|
||||
}
|
||||
return {
|
||||
fragments: FRAGMENTS.filter(panelFragment),
|
||||
translations: meta,
|
||||
};
|
||||
})
|
||||
)
|
||||
.pipe(
|
||||
transform((data) => ({
|
||||
fragments: TRANSLATION_FRAGMENTS,
|
||||
translations: data,
|
||||
}))
|
||||
)
|
||||
.pipe(rename("translationMetadata.json"))
|
||||
.pipe(gulp.dest(workDir))
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"create-translations",
|
||||
gulp.series(
|
||||
gulp.parallel("create-test-metadata", "create-test-translation"),
|
||||
"build-master-translation",
|
||||
"build-merged-translations",
|
||||
gulp.parallel(...splitTasks),
|
||||
"build-flattened-translations"
|
||||
)
|
||||
);
|
||||
.pipe(gulp.dest(workDir));
|
||||
|
||||
gulp.task(
|
||||
"build-translations",
|
||||
gulp.series(
|
||||
gulp.parallel(
|
||||
"fetch-nightly-translations",
|
||||
gulp.series("clean-translations", "ensure-translations-build-dir")
|
||||
gulp.series("clean-translations", makeWorkDir)
|
||||
),
|
||||
"create-translations",
|
||||
"build-translation-fingerprints",
|
||||
"build-translation-write-metadata"
|
||||
createTestTranslation,
|
||||
createMasterTranslation,
|
||||
createTranslations,
|
||||
writeTranslationMetaData
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task(
|
||||
"build-supervisor-translations",
|
||||
gulp.series(
|
||||
gulp.parallel(
|
||||
"fetch-nightly-translations",
|
||||
gulp.series("clean-translations", "ensure-translations-build-dir")
|
||||
),
|
||||
gulp.parallel("create-test-metadata", "create-test-translation"),
|
||||
"build-master-translation",
|
||||
"build-merged-translations",
|
||||
"build-translation-fragment-supervisor",
|
||||
"build-translation-flatten-supervisor",
|
||||
"build-translation-fingerprints",
|
||||
"build-translation-write-metadata"
|
||||
)
|
||||
gulp.series(toggleSupervisorFragment, "build-translations")
|
||||
);
|
||||
|
@ -99,7 +99,7 @@ gulp.task("webpack-watch-app", () => {
|
||||
).watch({ poll: isWsl }, doneHandler());
|
||||
gulp.watch(
|
||||
path.join(paths.translations_src, "en.json"),
|
||||
gulp.series("create-translations", "copy-translations-app")
|
||||
gulp.series("build-translations", "copy-translations-app")
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1,16 +0,0 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
// Helper function to map recursively over files in a folder and it's subfolders
|
||||
module.exports.mapFiles = function mapFiles(startPath, filter, mapFunc) {
|
||||
const files = fs.readdirSync(startPath);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const filename = path.join(startPath, files[i]);
|
||||
const stat = fs.lstatSync(filename);
|
||||
if (stat.isDirectory()) {
|
||||
mapFiles(filename, filter, mapFunc);
|
||||
} else if (filename.indexOf(filter) >= 0) {
|
||||
mapFunc(filename);
|
||||
}
|
||||
}
|
||||
};
|
@ -10,6 +10,7 @@ const WebpackBar = require("webpackbar");
|
||||
const {
|
||||
TransformAsyncModulesPlugin,
|
||||
} = require("transform-async-modules-webpack-plugin");
|
||||
const { dependencies } = require("../package.json");
|
||||
const paths = require("./paths.cjs");
|
||||
const bundle = require("./bundle.cjs");
|
||||
|
||||
@ -156,7 +157,10 @@ const createWebpackConfig = ({
|
||||
transform: (stats) => JSON.stringify(filterStats(stats)),
|
||||
}),
|
||||
!latestBuild &&
|
||||
new TransformAsyncModulesPlugin({ browserslistEnv: "legacy" }),
|
||||
new TransformAsyncModulesPlugin({
|
||||
browserslistEnv: "legacy",
|
||||
runtime: { version: dependencies["@babel/runtime"] },
|
||||
}),
|
||||
].filter(Boolean),
|
||||
resolve: {
|
||||
extensions: [".ts", ".js", ".json"],
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { format, startOfToday, startOfTomorrow } from "date-fns/esm";
|
||||
import { format, startOfToday, startOfTomorrow } from "date-fns";
|
||||
import {
|
||||
EnergyInfo,
|
||||
EnergyPreferences,
|
||||
|
@ -161,12 +161,14 @@ const LABELS: LabelRegistryEntry[] = [
|
||||
name: "Energy",
|
||||
icon: null,
|
||||
color: "yellow",
|
||||
description: null,
|
||||
},
|
||||
{
|
||||
label_id: "entertainment",
|
||||
name: "Entertainment",
|
||||
icon: "mdi:popcorn",
|
||||
color: "blue",
|
||||
description: null,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -2,6 +2,7 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, query } from "lit/decorators";
|
||||
import { CoverEntityFeature } from "../../../../src/data/cover";
|
||||
import { LightColorMode } from "../../../../src/data/light";
|
||||
import { LockEntityFeature } from "../../../../src/data/lock";
|
||||
import { VacuumEntityFeature } from "../../../../src/data/vacuum";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
@ -20,6 +21,11 @@ const ENTITIES = [
|
||||
getEntity("light", "unavailable", "unavailable", {
|
||||
friendly_name: "Unavailable entity",
|
||||
}),
|
||||
getEntity("lock", "front_door", "locked", {
|
||||
friendly_name: "Front Door Lock",
|
||||
device_class: "lock",
|
||||
supported_features: LockEntityFeature.OPEN,
|
||||
}),
|
||||
getEntity("climate", "thermostat", "heat", {
|
||||
current_temperature: 73,
|
||||
min_temp: 45,
|
||||
@ -138,6 +144,24 @@ const CONFIGS = [
|
||||
- type: "color-temp"
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Lock commands feature",
|
||||
config: `
|
||||
- type: tile
|
||||
entity: lock.front_door
|
||||
features:
|
||||
- type: "lock-commands"
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Lock open door feature",
|
||||
config: `
|
||||
- type: tile
|
||||
entity: lock.front_door
|
||||
features:
|
||||
- type: "lock-open-door"
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Vacuum commands feature",
|
||||
config: `
|
||||
|
@ -36,6 +36,8 @@ const createConfigEntry = (
|
||||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
reason: null,
|
||||
error_reason_translation_key: null,
|
||||
error_reason_translation_placeholders: null,
|
||||
...override,
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { globIterate } from "glob";
|
||||
import { availableParallelism } from "node:os";
|
||||
|
||||
process.env.UV_THREADPOOL_SIZE = availableParallelism();
|
||||
|
||||
const gulpImports = [];
|
||||
|
||||
|
69
package.json
69
package.json
@ -25,15 +25,15 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.24.1",
|
||||
"@babel/runtime": "7.24.4",
|
||||
"@braintree/sanitize-url": "7.0.1",
|
||||
"@codemirror/autocomplete": "6.15.0",
|
||||
"@codemirror/commands": "6.3.3",
|
||||
"@codemirror/autocomplete": "6.16.0",
|
||||
"@codemirror/commands": "6.5.0",
|
||||
"@codemirror/language": "6.10.1",
|
||||
"@codemirror/legacy-modes": "6.3.3",
|
||||
"@codemirror/legacy-modes": "6.4.0",
|
||||
"@codemirror/search": "6.5.6",
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@codemirror/view": "6.26.1",
|
||||
"@codemirror/view": "6.26.3",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.12.3",
|
||||
"@formatjs/intl-displaynames": "6.6.6",
|
||||
@ -81,7 +81,7 @@
|
||||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/web": "=1.3.0",
|
||||
"@material/web": "1.4.1",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@polymer/paper-item": "3.0.1",
|
||||
@ -89,8 +89,8 @@
|
||||
"@polymer/paper-tabs": "3.1.0",
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.3.10",
|
||||
"@vaadin/vaadin-themable-mixin": "24.3.10",
|
||||
"@vaadin/combo-box": "24.3.11",
|
||||
"@vaadin/vaadin-themable-mixin": "24.3.11",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@ -101,17 +101,17 @@
|
||||
"chart.js": "4.4.2",
|
||||
"color-name": "2.0.0",
|
||||
"comlink": "4.4.1",
|
||||
"core-js": "3.36.1",
|
||||
"core-js": "3.37.0",
|
||||
"cropperjs": "1.6.1",
|
||||
"date-fns": "2.30.0",
|
||||
"date-fns-tz": "2.0.1",
|
||||
"date-fns": "3.6.0",
|
||||
"date-fns-tz": "3.1.3",
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"element-internals-polyfill": "1.3.10",
|
||||
"element-internals-polyfill": "1.3.11",
|
||||
"fuse.js": "7.0.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||
"home-assistant-js-websocket": "9.2.1",
|
||||
"home-assistant-js-websocket": "9.3.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.5.11",
|
||||
"js-yaml": "4.1.0",
|
||||
@ -119,7 +119,7 @@
|
||||
"leaflet-draw": "1.0.4",
|
||||
"lit": "2.8.0",
|
||||
"luxon": "3.4.4",
|
||||
"marked": "12.0.1",
|
||||
"marked": "12.0.2",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "0.3.2",
|
||||
@ -150,18 +150,18 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.24.3",
|
||||
"@babel/core": "7.24.4",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.1",
|
||||
"@babel/plugin-proposal-decorators": "7.24.1",
|
||||
"@babel/plugin-transform-runtime": "7.24.3",
|
||||
"@babel/preset-env": "7.24.3",
|
||||
"@babel/preset-env": "7.24.4",
|
||||
"@babel/preset-typescript": "7.24.1",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.12.2",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@lokalise/node-api": "12.3.0",
|
||||
"@octokit/auth-oauth-device": "7.0.1",
|
||||
"@octokit/plugin-retry": "7.0.3",
|
||||
"@octokit/rest": "20.0.2",
|
||||
"@lokalise/node-api": "12.4.0",
|
||||
"@octokit/auth-oauth-device": "7.1.1",
|
||||
"@octokit/plugin-retry": "7.1.0",
|
||||
"@octokit/rest": "20.1.0",
|
||||
"@open-wc/dev-server-hmr": "0.1.4",
|
||||
"@rollup/plugin-babel": "6.0.4",
|
||||
"@rollup/plugin-commonjs": "25.0.7",
|
||||
@ -169,24 +169,24 @@
|
||||
"@rollup/plugin-node-resolve": "15.2.3",
|
||||
"@rollup/plugin-replace": "5.0.5",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.13",
|
||||
"@types/chromecast-caf-receiver": "6.0.14",
|
||||
"@types/chromecast-caf-sender": "1.0.9",
|
||||
"@types/color-name": "1.1.3",
|
||||
"@types/color-name": "1.1.4",
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/html-minifier-terser": "7.0.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/leaflet": "1.9.8",
|
||||
"@types/leaflet": "1.9.11",
|
||||
"@types/leaflet-draw": "1.0.11",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/mocha": "10.0.6",
|
||||
"@types/qrcode": "1.5.5",
|
||||
"@types/serve-handler": "6.1.4",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
"@types/tar": "6.1.11",
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "7.4.0",
|
||||
"@typescript-eslint/parser": "7.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.7.0",
|
||||
"@typescript-eslint/parser": "7.7.0",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@web/dev-server-rollup": "0.4.1",
|
||||
"babel-loader": "9.1.3",
|
||||
@ -203,12 +203,11 @@
|
||||
"eslint-plugin-lit": "1.11.0",
|
||||
"eslint-plugin-lit-a11y": "4.1.2",
|
||||
"eslint-plugin-unused-imports": "3.1.0",
|
||||
"eslint-plugin-wc": "2.0.4",
|
||||
"eslint-plugin-wc": "2.1.0",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"glob": "10.3.10",
|
||||
"glob": "10.3.12",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-flatmap": "1.0.2",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-merge-json": "2.2.1",
|
||||
"gulp-rename": "2.0.0",
|
||||
@ -220,9 +219,9 @@
|
||||
"lint-staged": "15.2.2",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.template": "4.5.0",
|
||||
"magic-string": "0.30.8",
|
||||
"magic-string": "0.30.10",
|
||||
"map-stream": "0.0.7",
|
||||
"mocha": "10.3.0",
|
||||
"mocha": "10.4.0",
|
||||
"object-hash": "3.0.0",
|
||||
"open": "10.1.0",
|
||||
"pinst": "3.0.0",
|
||||
@ -235,13 +234,11 @@
|
||||
"sinon": "17.0.1",
|
||||
"source-map-url": "0.4.1",
|
||||
"systemjs": "6.14.3",
|
||||
"tar": "6.2.1",
|
||||
"tar": "7.0.1",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"transform-async-modules-webpack-plugin": "1.0.4",
|
||||
"transform-async-modules-webpack-plugin": "1.1.0",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.4.3",
|
||||
"vinyl-buffer": "1.0.1",
|
||||
"vinyl-source-stream": "2.0.0",
|
||||
"typescript": "5.4.5",
|
||||
"webpack": "5.91.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.0.4",
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20240404.2"
|
||||
version = "20240424.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@ -40,6 +40,11 @@
|
||||
"matchPackageNames": ["tsparticles-engine"],
|
||||
"matchPackagePrefixes": ["tsparticles-preset-"]
|
||||
},
|
||||
{
|
||||
"description": "Group date-fns with dependent timezone package",
|
||||
"groupName": "date-fns",
|
||||
"matchPackageNames": ["date-fns", "date-fns-tz"]
|
||||
},
|
||||
{
|
||||
"description": "Group and temporarily disable WDS packages",
|
||||
"groupName": "Web Dev Server",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { utcToZonedTime, zonedTimeToUtc } from "date-fns-tz";
|
||||
import { toZonedTime, fromZonedTime } from "date-fns-tz";
|
||||
import { HassConfig } from "home-assistant-js-websocket";
|
||||
import { FrontendLocaleData, TimeZone } from "../../data/translation";
|
||||
|
||||
@ -8,10 +8,10 @@ const calcZonedDate = (
|
||||
fn: (date: Date, options?: any) => Date | number | boolean,
|
||||
options?
|
||||
) => {
|
||||
const inputZoned = utcToZonedTime(date, tz);
|
||||
const inputZoned = toZonedTime(date, tz);
|
||||
const fnZoned = fn(inputZoned, options);
|
||||
if (fnZoned instanceof Date) {
|
||||
return zonedTimeToUtc(fnZoned, tz) as Date;
|
||||
return fromZonedTime(fnZoned, tz) as Date;
|
||||
}
|
||||
return fnZoned;
|
||||
};
|
||||
@ -51,6 +51,6 @@ export const calcDateDifferenceProperty = (
|
||||
locale,
|
||||
config,
|
||||
locale.time_zone === TimeZone.server
|
||||
? utcToZonedTime(startDate, config.time_zone)
|
||||
? toZonedTime(startDate, config.time_zone)
|
||||
: startDate
|
||||
);
|
||||
|
@ -187,11 +187,14 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
if (
|
||||
[
|
||||
"button",
|
||||
"conversation",
|
||||
"event",
|
||||
"image",
|
||||
"input_button",
|
||||
"notify",
|
||||
"scene",
|
||||
"stt",
|
||||
"tag",
|
||||
"tts",
|
||||
"wake_word",
|
||||
].includes(domain) ||
|
||||
|
@ -2,6 +2,7 @@ import IntlMessageFormat from "intl-messageformat";
|
||||
import type { HTMLTemplateResult } from "lit";
|
||||
import { polyfillLocaleData } from "../../resources/locale-data-polyfill";
|
||||
import { Resources, TranslationDict } from "../../types";
|
||||
import { fireEvent } from "../dom/fire_event";
|
||||
|
||||
// Exclude some patterns from key type checking for now
|
||||
// These are intended to be removed as errors are fixed
|
||||
@ -81,7 +82,9 @@ export interface FormatsType {
|
||||
*/
|
||||
|
||||
export const computeLocalize = async <Keys extends string = LocalizeKeys>(
|
||||
cache: any,
|
||||
cache: HTMLElement & {
|
||||
_localizationCache?: Record<string, IntlMessageFormat>;
|
||||
},
|
||||
language: string,
|
||||
resources: Resources,
|
||||
formats?: FormatsType
|
||||
@ -107,7 +110,7 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>(
|
||||
}
|
||||
|
||||
const messageKey = key + translatedValue;
|
||||
let translatedMessage = cache._localizationCache[messageKey] as
|
||||
let translatedMessage = cache._localizationCache![messageKey] as
|
||||
| IntlMessageFormat
|
||||
| undefined;
|
||||
|
||||
@ -121,7 +124,7 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>(
|
||||
} catch (err: any) {
|
||||
return "Translation error: " + err.message;
|
||||
}
|
||||
cache._localizationCache[messageKey] = translatedMessage;
|
||||
cache._localizationCache![messageKey] = translatedMessage;
|
||||
}
|
||||
|
||||
let argObject = {};
|
||||
@ -137,6 +140,12 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>(
|
||||
try {
|
||||
return translatedMessage.format<string>(argObject) as string;
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Translation error", key, language, err);
|
||||
fireEvent(cache, "write_log", {
|
||||
level: "error",
|
||||
message: `Failed to format translation for key '${key}' in language '${language}'. ${err}`,
|
||||
});
|
||||
return "Translation " + err;
|
||||
}
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { differenceInDays, differenceInWeeks, startOfWeek } from "date-fns/esm";
|
||||
import { differenceInDays, differenceInWeeks, startOfWeek } from "date-fns";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import { firstWeekdayIndex } from "../datetime/first_weekday";
|
||||
|
||||
|
@ -34,7 +34,7 @@ import {
|
||||
endOfMonth,
|
||||
endOfQuarter,
|
||||
endOfYear,
|
||||
} from "date-fns/esm";
|
||||
} from "date-fns";
|
||||
import {
|
||||
formatDate,
|
||||
formatDateMonth,
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { mdiArrowDown, mdiArrowUp } from "@mdi/js";
|
||||
import { mdiArrowDown, mdiArrowUp, mdiChevronDown } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import {
|
||||
customElement,
|
||||
@ -22,7 +22,9 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { groupBy } from "../../common/util/group-by";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
@ -32,17 +34,6 @@ import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-svg-icon";
|
||||
import "../search-input";
|
||||
import { filterData, sortData } from "./sort-filter";
|
||||
import { groupBy } from "../../common/util/group-by";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"selection-changed": SelectionChangedEvent;
|
||||
"row-click": RowClickedEvent;
|
||||
"sorting-changed": SortingChangedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export interface RowClickedEvent {
|
||||
id: string;
|
||||
@ -52,6 +43,10 @@ export interface SelectionChangedEvent {
|
||||
value: string[];
|
||||
}
|
||||
|
||||
export interface CollapsedChangedEvent {
|
||||
value: string[];
|
||||
}
|
||||
|
||||
export interface SortingChangedEvent {
|
||||
column: string;
|
||||
direction: SortingDirection;
|
||||
@ -142,10 +137,14 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@property() public groupColumn?: string;
|
||||
|
||||
@property({ attribute: false }) public groupOrder?: string[];
|
||||
|
||||
@property() public sortColumn?: string;
|
||||
|
||||
@property() public sortDirection: SortingDirection = null;
|
||||
|
||||
@property({ attribute: false }) public initialCollapsedGroups?: string[];
|
||||
|
||||
@state() private _filterable = false;
|
||||
|
||||
@state() private _filter = "";
|
||||
@ -158,6 +157,8 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@state() private _items: DataTableRowData[] = [];
|
||||
|
||||
@state() private _collapsedGroups: string[] = [];
|
||||
|
||||
private _checkableRowsCount?: number;
|
||||
|
||||
private _checkedRows: string[] = [];
|
||||
@ -213,6 +214,7 @@ export class HaDataTable extends LitElement {
|
||||
(column) => column.filterable
|
||||
);
|
||||
|
||||
if (!this.sortColumn) {
|
||||
for (const columnId in this.columns) {
|
||||
if (this.columns[columnId].direction) {
|
||||
this.sortDirection = this.columns[columnId].direction!;
|
||||
@ -226,6 +228,7 @@ export class HaDataTable extends LitElement {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const clonedColumns: DataTableColumnContainer = deepClone(this.columns);
|
||||
Object.values(clonedColumns).forEach(
|
||||
@ -248,13 +251,23 @@ export class HaDataTable extends LitElement {
|
||||
).length;
|
||||
}
|
||||
|
||||
if (!this.hasUpdated && this.initialCollapsedGroups) {
|
||||
this._collapsedGroups = this.initialCollapsedGroups;
|
||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
||||
} else if (properties.has("groupColumn")) {
|
||||
this._collapsedGroups = [];
|
||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
||||
}
|
||||
|
||||
if (
|
||||
properties.has("data") ||
|
||||
properties.has("columns") ||
|
||||
properties.has("_filter") ||
|
||||
properties.has("sortColumn") ||
|
||||
properties.has("sortDirection") ||
|
||||
properties.has("groupColumn")
|
||||
properties.has("groupColumn") ||
|
||||
properties.has("groupOrder") ||
|
||||
properties.has("_collapsedGroups")
|
||||
) {
|
||||
this._sortFilterData();
|
||||
}
|
||||
@ -447,6 +460,8 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
return html`
|
||||
<div
|
||||
@mouseover=${this._setTitle}
|
||||
@focus=${this._setTitle}
|
||||
role=${column.main ? "rowheader" : "cell"}
|
||||
class="mdc-data-table__cell ${classMap({
|
||||
"mdc-data-table__cell--flex": column.type === "flex",
|
||||
@ -514,11 +529,7 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
if (this.appendRow || this.hasFab || this.groupColumn) {
|
||||
const items = [...data];
|
||||
|
||||
if (this.appendRow) {
|
||||
items.push({ append: true, content: this.appendRow });
|
||||
}
|
||||
let items = [...data];
|
||||
|
||||
if (this.groupColumn) {
|
||||
const grouped = groupBy(items, (item) => item[this.groupColumn!]);
|
||||
@ -530,13 +541,24 @@ export class HaDataTable extends LitElement {
|
||||
const sorted: {
|
||||
[key: string]: DataTableRowData[];
|
||||
} = Object.keys(grouped)
|
||||
.sort((a, b) =>
|
||||
stringCompare(
|
||||
.sort((a, b) => {
|
||||
const orderA = this.groupOrder?.indexOf(a) ?? -1;
|
||||
const orderB = this.groupOrder?.indexOf(b) ?? -1;
|
||||
if (orderA !== orderB) {
|
||||
if (orderA === -1) {
|
||||
return 1;
|
||||
}
|
||||
if (orderB === -1) {
|
||||
return -1;
|
||||
}
|
||||
return orderA - orderB;
|
||||
}
|
||||
return stringCompare(
|
||||
["", "-", "—"].includes(a) ? "zzz" : a,
|
||||
["", "-", "—"].includes(b) ? "zzz" : b,
|
||||
this.hass.locale.language
|
||||
)
|
||||
)
|
||||
);
|
||||
})
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = grouped[key];
|
||||
return obj;
|
||||
@ -552,23 +574,39 @@ export class HaDataTable extends LitElement {
|
||||
content: html`<div
|
||||
class="mdc-data-table__cell group-header"
|
||||
role="cell"
|
||||
.group=${groupName}
|
||||
@click=${this._collapseGroup}
|
||||
>
|
||||
${groupName === UNDEFINED_GROUP_KEY ? "" : groupName || ""}
|
||||
<ha-icon-button
|
||||
.path=${mdiChevronDown}
|
||||
class=${this._collapsedGroups.includes(groupName)
|
||||
? "collapsed"
|
||||
: ""}
|
||||
>
|
||||
</ha-icon-button>
|
||||
${groupName === UNDEFINED_GROUP_KEY
|
||||
? this.hass.localize("ui.components.data-table.ungrouped")
|
||||
: groupName || ""}
|
||||
</div>`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!this._collapsedGroups.includes(groupName)) {
|
||||
groupedItems.push(...rows);
|
||||
}
|
||||
});
|
||||
|
||||
this._items = groupedItems;
|
||||
} else {
|
||||
this._items = items;
|
||||
items = groupedItems;
|
||||
}
|
||||
|
||||
if (this.appendRow) {
|
||||
items.push({ append: true, content: this.appendRow });
|
||||
}
|
||||
|
||||
if (this.hasFab) {
|
||||
this._items = [...this._items, { empty: true }];
|
||||
items.push({ empty: true });
|
||||
}
|
||||
|
||||
this._items = items;
|
||||
} else {
|
||||
this._items = data;
|
||||
}
|
||||
@ -649,6 +687,13 @@ export class HaDataTable extends LitElement {
|
||||
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
|
||||
};
|
||||
|
||||
private _setTitle(ev: Event) {
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
if (target.scrollWidth > target.offsetWidth) {
|
||||
target.setAttribute("title", target.innerText);
|
||||
}
|
||||
}
|
||||
|
||||
private _checkedRowsChanged() {
|
||||
// force scroller to update, change it's items
|
||||
if (this._items.length) {
|
||||
@ -679,6 +724,18 @@ export class HaDataTable extends LitElement {
|
||||
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
|
||||
}
|
||||
|
||||
private _collapseGroup = (ev: Event) => {
|
||||
const groupName = (ev.currentTarget as any).group;
|
||||
if (this._collapsedGroups.includes(groupName)) {
|
||||
this._collapsedGroups = this._collapsedGroups.filter(
|
||||
(grp) => grp !== groupName
|
||||
);
|
||||
} else {
|
||||
this._collapsedGroups = [...this._collapsedGroups, groupName];
|
||||
}
|
||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
||||
};
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
@ -931,8 +988,21 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
.group-header {
|
||||
padding-top: 12px;
|
||||
padding-left: 12px;
|
||||
padding-inline-start: 12px;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.group-header ha-icon-button {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.group-header ha-icon-button.collapsed {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
:host {
|
||||
@ -1031,4 +1101,12 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-data-table": HaDataTable;
|
||||
}
|
||||
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"selection-changed": SelectionChangedEvent;
|
||||
"row-click": RowClickedEvent;
|
||||
"sorting-changed": SortingChangedEvent;
|
||||
"collapsed-changed": CollapsedChangedEvent;
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,12 @@ import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "./state-badge";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import {
|
||||
isHelperDomain,
|
||||
HelperDomain,
|
||||
} from "../../panels/config/helpers/const";
|
||||
|
||||
interface HassEntityWithCachedName extends HassEntity, ScorableTextItem {
|
||||
friendly_name: string;
|
||||
@ -25,6 +31,8 @@ interface HassEntityWithCachedName extends HassEntity, ScorableTextItem {
|
||||
|
||||
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
||||
|
||||
const CREATE_ID = "___create-new-entity___";
|
||||
|
||||
@customElement("ha-entity-picker")
|
||||
export class HaEntityPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@ -44,6 +52,8 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Array }) public createDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show entities from specific domains.
|
||||
* @type {Array}
|
||||
@ -130,7 +140,11 @@ export class HaEntityPicker extends LitElement {
|
||||
></state-badge>`
|
||||
: ""}
|
||||
<span>${item.friendly_name}</span>
|
||||
<span slot="secondary">${item.entity_id}</span>
|
||||
<span slot="secondary"
|
||||
>${item.entity_id.startsWith(CREATE_ID)
|
||||
? this.hass.localize("ui.components.entity.entity-picker.new_entity")
|
||||
: item.entity_id}</span
|
||||
>
|
||||
</ha-list-item>`;
|
||||
|
||||
private _getStates = memoizeOne(
|
||||
@ -143,7 +157,8 @@ export class HaEntityPicker extends LitElement {
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
|
||||
includeEntities: this["includeEntities"],
|
||||
excludeEntities: this["excludeEntities"]
|
||||
excludeEntities: this["excludeEntities"],
|
||||
createDomains: this["createDomains"]
|
||||
): HassEntityWithCachedName[] => {
|
||||
let states: HassEntityWithCachedName[] = [];
|
||||
|
||||
@ -152,6 +167,34 @@ export class HaEntityPicker extends LitElement {
|
||||
}
|
||||
let entityIds = Object.keys(hass.states);
|
||||
|
||||
const createItems = createDomains?.length
|
||||
? createDomains.map((domain) => {
|
||||
const newFriendlyName = hass.localize(
|
||||
"ui.components.entity.entity-picker.create_helper",
|
||||
{
|
||||
domain: isHelperDomain(domain)
|
||||
? hass.localize(
|
||||
`ui.panel.config.helpers.types.${domain as HelperDomain}`
|
||||
)
|
||||
: domainToName(hass.localize, domain),
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
entity_id: CREATE_ID + domain,
|
||||
state: "on",
|
||||
last_changed: "",
|
||||
last_updated: "",
|
||||
context: { id: "", user_id: null, parent_id: null },
|
||||
friendly_name: newFriendlyName,
|
||||
attributes: {
|
||||
icon: "mdi:plus",
|
||||
},
|
||||
strings: [domain, newFriendlyName],
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
if (!entityIds.length) {
|
||||
return [
|
||||
{
|
||||
@ -171,6 +214,7 @@ export class HaEntityPicker extends LitElement {
|
||||
},
|
||||
strings: [],
|
||||
},
|
||||
...createItems,
|
||||
];
|
||||
}
|
||||
|
||||
@ -281,9 +325,14 @@ export class HaEntityPicker extends LitElement {
|
||||
},
|
||||
strings: [],
|
||||
},
|
||||
...createItems,
|
||||
];
|
||||
}
|
||||
|
||||
if (createItems?.length) {
|
||||
states.push(...createItems);
|
||||
}
|
||||
|
||||
return states;
|
||||
}
|
||||
);
|
||||
@ -310,13 +359,18 @@ export class HaEntityPicker extends LitElement {
|
||||
this.includeDeviceClasses,
|
||||
this.includeUnitOfMeasurement,
|
||||
this.includeEntities,
|
||||
this.excludeEntities
|
||||
this.excludeEntities,
|
||||
this.createDomains
|
||||
);
|
||||
if (this._initedStates) {
|
||||
this.comboBox.filteredItems = this._states;
|
||||
}
|
||||
this._initedStates = true;
|
||||
}
|
||||
|
||||
if (changedProps.has("createDomains") && this.createDomains?.length) {
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@ -354,6 +408,18 @@ export class HaEntityPicker extends LitElement {
|
||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (newValue && newValue.startsWith(CREATE_ID)) {
|
||||
const domain = newValue.substring(CREATE_ID.length);
|
||||
showHelperDetailDialog(this, {
|
||||
domain,
|
||||
dialogClosedCallback: (item) => {
|
||||
if (item.entityId) this._setValue(item.entityId);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue !== this._value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
|
@ -14,6 +14,8 @@ export class HaCard extends LitElement {
|
||||
--ha-card-background,
|
||||
var(--card-background-color, white)
|
||||
);
|
||||
-webkit-backdrop-filter: var(--ha-card-backdrop-filter, none);
|
||||
backdrop-filter: var(--ha-card-backdrop-filter, none);
|
||||
box-shadow: var(--ha-card-box-shadow, none);
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import "element-internals-polyfill";
|
||||
import { MdCircularProgress } from "@material/web/progress/circular-progress";
|
||||
import { CSSResult, PropertyValues, css } from "lit";
|
||||
import { PropertyValues, css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-circular-progress")
|
||||
@ -32,8 +32,7 @@ export class HaCircularProgress extends MdCircularProgress {
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
@ -42,7 +41,6 @@ export class HaCircularProgress extends MdCircularProgress {
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -67,6 +67,9 @@ export class HaControlSlider extends LitElement {
|
||||
@property({ attribute: "tooltip-mode" })
|
||||
public tooltipMode: TooltipMode = "interaction";
|
||||
|
||||
@property({ attribute: "touch-action" })
|
||||
public touchAction?: string;
|
||||
|
||||
@property({ type: Number })
|
||||
public value?: number;
|
||||
|
||||
@ -152,7 +155,7 @@ export class HaControlSlider extends LitElement {
|
||||
setupListeners() {
|
||||
if (this.slider && !this._mc) {
|
||||
this._mc = new Manager(this.slider, {
|
||||
touchAction: this.vertical ? "pan-x" : "pan-y",
|
||||
touchAction: this.touchAction ?? (this.vertical ? "pan-x" : "pan-y"),
|
||||
});
|
||||
this._mc.add(
|
||||
new Pan({
|
||||
|
@ -33,6 +33,9 @@ export class HaControlSwitch extends LitElement {
|
||||
// SVG icon path (if you need a non SVG icon instead, use the provided off icon slot to pass an <ha-icon slot="icon-off"> in)
|
||||
@property({ type: String }) pathOff?: string;
|
||||
|
||||
@property({ attribute: "touch-action" })
|
||||
public touchAction?: string;
|
||||
|
||||
private _mc?: HammerManager;
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
@ -73,7 +76,7 @@ export class HaControlSwitch extends LitElement {
|
||||
setupListeners() {
|
||||
if (this.switch && !this._mc) {
|
||||
this._mc = new Manager(this.switch, {
|
||||
touchAction: this.vertical ? "pan-x" : "pan-y",
|
||||
touchAction: this.touchAction ?? (this.vertical ? "pan-x" : "pan-y"),
|
||||
});
|
||||
this._mc.add(
|
||||
new Swipe({
|
||||
|
@ -75,8 +75,14 @@ export class HaDialog extends DialogBase {
|
||||
var(--divider-color)
|
||||
);
|
||||
z-index: var(--dialog-z-index, 8);
|
||||
-webkit-backdrop-filter: var(--dialog-backdrop-filter, none);
|
||||
backdrop-filter: var(--dialog-backdrop-filter, none);
|
||||
-webkit-backdrop-filter: var(
|
||||
--ha-dialog-scrim-backdrop-filter,
|
||||
var(--dialog-backdrop-filter, none)
|
||||
);
|
||||
backdrop-filter: var(
|
||||
--ha-dialog-scrim-backdrop-filter,
|
||||
var(--dialog-backdrop-filter, none)
|
||||
);
|
||||
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
|
||||
--mdc-typography-headline6-font-weight: 400;
|
||||
--mdc-typography-headline6-font-size: 1.574rem;
|
||||
@ -119,6 +125,8 @@ export class HaDialog extends DialogBase {
|
||||
margin-top: var(--dialog-surface-margin-top);
|
||||
min-height: var(--mdc-dialog-min-height, auto);
|
||||
border-radius: var(--ha-dialog-border-radius, 28px);
|
||||
-webkit-backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
|
||||
backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
|
||||
}
|
||||
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
||||
display: flex;
|
||||
|
@ -94,7 +94,7 @@ export class HaFilterDevices extends LitElement {
|
||||
? nothing
|
||||
: html`<ha-check-list-item
|
||||
.value=${device.id}
|
||||
.selected=${this.value?.includes(device.id)}
|
||||
.selected=${this.value?.includes(device.id) ?? false}
|
||||
>
|
||||
${computeDeviceName(device, this.hass)}
|
||||
</ha-check-list-item>`;
|
||||
|
@ -108,7 +108,7 @@ export class HaFilterEntities extends LitElement {
|
||||
? nothing
|
||||
: html`<ha-check-list-item
|
||||
.value=${entity.entity_id}
|
||||
.selected=${this.value?.includes(entity.entity_id)}
|
||||
.selected=${this.value?.includes(entity.entity_id) ?? false}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
|
@ -62,8 +62,8 @@ export class HaFilterStates extends LitElement {
|
||||
(item) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${item.value}
|
||||
.selected=${this.value?.includes(item.value)}
|
||||
.graphic=${hasIcon ? "icon" : undefined}
|
||||
.selected=${this.value?.includes(item.value) ?? false}
|
||||
.graphic=${hasIcon ? "icon" : null}
|
||||
>
|
||||
${item.icon
|
||||
? html`<ha-icon
|
||||
|
@ -302,6 +302,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
name: this.hass.localize("ui.components.label-picker.no_match"),
|
||||
icon: null,
|
||||
color: null,
|
||||
description: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -315,6 +316,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
name: this.hass.localize("ui.components.label-picker.add_new"),
|
||||
icon: "mdi:plus",
|
||||
color: null,
|
||||
description: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import "element-internals-polyfill";
|
||||
import { MdListItem } from "@material/web/list/list-item";
|
||||
import { CSSResult, css } from "lit";
|
||||
import { css } from "lit";
|
||||
|
||||
@customElement("ha-list-item-new")
|
||||
export class HaListItemNew extends MdListItem {
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
...MdListItem.styles,
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
@ -19,7 +18,6 @@ export class HaListItemNew extends MdListItem {
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -1,20 +1,18 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import "element-internals-polyfill";
|
||||
import { MdList } from "@material/web/list/list";
|
||||
import { CSSResult, css } from "lit";
|
||||
import { css } from "lit";
|
||||
|
||||
@customElement("ha-list-new")
|
||||
export class HaListNew extends MdList {
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
...MdList.styles,
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { MdMenuItem } from "@material/web/menu/menu-item";
|
||||
import "element-internals-polyfill";
|
||||
import { CSSResult, css } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-menu-item")
|
||||
export class HaMenuItem extends MdMenuItem {
|
||||
static override styles: CSSResult[] = [
|
||||
...MdMenuItem.styles,
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import "element-internals-polyfill";
|
||||
import { CSSResult, css } from "lit";
|
||||
import { css } from "lit";
|
||||
import { MdMenu } from "@material/web/menu/menu";
|
||||
|
||||
@customElement("ha-menu")
|
||||
export class HaMenu extends MdMenu {
|
||||
static override styles: CSSResult[] = [
|
||||
...MdMenu.styles,
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-surface-container: var(--card-background-color);
|
||||
|
@ -7,8 +7,10 @@ import type {
|
||||
LocationSelectorValue,
|
||||
} from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { SchemaUnion } from "../ha-form/types";
|
||||
import type { MarkerLocation } from "../map/ha-locations-editor";
|
||||
import "../map/ha-locations-editor";
|
||||
import "../ha-form/ha-form";
|
||||
|
||||
@customElement("ha-selector-location")
|
||||
export class HaLocationSelector extends LitElement {
|
||||
@ -24,6 +26,49 @@ export class HaLocationSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(radius?: boolean, radius_readonly?: boolean) =>
|
||||
[
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "latitude",
|
||||
required: true,
|
||||
selector: { number: { step: "any" } },
|
||||
},
|
||||
{
|
||||
name: "longitude",
|
||||
required: true,
|
||||
selector: { number: { step: "any" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
...(radius
|
||||
? [
|
||||
{
|
||||
name: "radius",
|
||||
required: true,
|
||||
default: 1000,
|
||||
disabled: !!radius_readonly,
|
||||
selector: { number: { min: 0, step: 1, mode: "box" } as const },
|
||||
} as const,
|
||||
]
|
||||
: []),
|
||||
] as const
|
||||
);
|
||||
|
||||
protected willUpdate() {
|
||||
if (!this.value) {
|
||||
this.value = {
|
||||
latitude: this.hass.config.latitude,
|
||||
longitude: this.hass.config.longitude,
|
||||
radius: this.selector.location?.radius ? 1000 : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<p>${this.label ? this.label : ""}</p>
|
||||
@ -35,6 +80,17 @@ export class HaLocationSelector extends LitElement {
|
||||
@location-updated=${this._locationChanged}
|
||||
@radius-updated=${this._radiusChanged}
|
||||
></ha-locations-editor>
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.schema=${this._schema(
|
||||
this.selector.location?.radius,
|
||||
this.selector.location?.radius_readonly
|
||||
)}
|
||||
.data=${this.value}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -66,7 +122,8 @@ export class HaLocationSelector extends LitElement {
|
||||
? "mdi:map-marker-radius"
|
||||
: "mdi:map-marker",
|
||||
location_editable: true,
|
||||
radius_editable: true,
|
||||
radius_editable:
|
||||
!!selector.location?.radius && !selector.location?.radius_readonly,
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -80,14 +137,39 @@ export class HaLocationSelector extends LitElement {
|
||||
}
|
||||
|
||||
private _radiusChanged(ev: CustomEvent) {
|
||||
const radius = ev.detail.radius;
|
||||
const radius = Math.round(ev.detail.radius);
|
||||
fireEvent(this, "value-changed", { value: { ...this.value, radius } });
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
const radius = Math.round(ev.detail.value.radius);
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
latitude: value.latitude,
|
||||
longitude: value.longitude,
|
||||
...(this.selector.location?.radius &&
|
||||
!this.selector.location?.radius_readonly
|
||||
? {
|
||||
radius,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _computeLabel = (
|
||||
entry: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string =>
|
||||
this.hass.localize(`ui.components.selectors.location.${entry.name}`);
|
||||
|
||||
static styles = css`
|
||||
ha-locations-editor {
|
||||
display: block;
|
||||
height: 400px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
|
@ -82,6 +82,7 @@ export class HaTargetSelector extends LitElement {
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.disabled=${this.disabled}
|
||||
.createDomains=${this.selector.target?.create_domains}
|
||||
></ha-target-picker>`;
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,7 @@ import {
|
||||
expandFloorTarget,
|
||||
expandLabelTarget,
|
||||
Selector,
|
||||
TargetSelector,
|
||||
} from "../data/selector";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
@ -43,6 +44,7 @@ import "./ha-service-picker";
|
||||
import "./ha-settings-row";
|
||||
import "./ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "./ha-yaml-editor";
|
||||
import { isHelperDomain } from "../panels/config/helpers/const";
|
||||
|
||||
const attributeFilter = (values: any[], attribute: any) => {
|
||||
if (typeof attribute === "object") {
|
||||
@ -363,6 +365,15 @@ export class HaServiceControl extends LitElement {
|
||||
return false;
|
||||
}
|
||||
|
||||
private _targetSelector = memoizeOne(
|
||||
(targetSelector: TargetSelector | null | undefined, domain?: string) => {
|
||||
const create_domains = isHelperDomain(domain) ? [domain] : undefined;
|
||||
return targetSelector
|
||||
? { target: { ...targetSelector, create_domains } }
|
||||
: { target: { create_domains } };
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const serviceData = this._getServiceInfo(
|
||||
this._value?.service,
|
||||
@ -401,8 +412,7 @@ export class HaServiceControl extends LitElement {
|
||||
)) ||
|
||||
serviceData?.description;
|
||||
|
||||
return html`
|
||||
${this.hidePicker
|
||||
return html`${this.hidePicker
|
||||
? nothing
|
||||
: html`<ha-service-picker
|
||||
.hass=${this.hass}
|
||||
@ -443,9 +453,7 @@ export class HaServiceControl extends LitElement {
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
: ""}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.components.service-control.target"
|
||||
)}</span
|
||||
>${this.hass.localize("ui.components.service-control.target")}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
@ -453,9 +461,10 @@ export class HaServiceControl extends LitElement {
|
||||
)}</span
|
||||
><ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${serviceData.target
|
||||
? { target: serviceData.target }
|
||||
: { target: {} }}
|
||||
.selector=${this._targetSelector(
|
||||
serviceData.target as TargetSelector,
|
||||
domain
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._targetChanged}
|
||||
.value=${this._value?.target}
|
||||
@ -485,11 +494,9 @@ export class HaServiceControl extends LitElement {
|
||||
: filteredFields?.map((dataField) => {
|
||||
const selector = dataField?.selector ?? { text: undefined };
|
||||
const type = Object.keys(selector)[0];
|
||||
const enhancedSelector = [
|
||||
"action",
|
||||
"condition",
|
||||
"trigger",
|
||||
].includes(type)
|
||||
const enhancedSelector = ["action", "condition", "trigger"].includes(
|
||||
type
|
||||
)
|
||||
? {
|
||||
[type]: {
|
||||
...selector[type],
|
||||
@ -550,8 +557,7 @@ export class HaServiceControl extends LitElement {
|
||||
></ha-selector>
|
||||
</ha-settings-row>`
|
||||
: "";
|
||||
})}
|
||||
`;
|
||||
})} `;
|
||||
}
|
||||
|
||||
private _localizeValueCallback = (key: string) => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import "element-internals-polyfill";
|
||||
import { MdSlider } from "@material/web/slider/slider";
|
||||
import { CSSResult, css } from "lit";
|
||||
import { css } from "lit";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
|
||||
@customElement("ha-slider")
|
||||
@ -11,8 +11,8 @@ export class HaSlider extends MdSlider {
|
||||
this.dir = mainWindow.document.dir;
|
||||
}
|
||||
|
||||
static override styles: CSSResult[] = [
|
||||
...MdSlider.styles,
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-primary: var(--primary-color);
|
||||
|
@ -82,7 +82,7 @@ export class HaSortable extends LitElement {
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._shouldBeDestroy = false;
|
||||
if (this.hasUpdated) {
|
||||
if (this.hasUpdated && !this.disabled) {
|
||||
this._createSortable();
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,17 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import "element-internals-polyfill";
|
||||
import { CSSResult, css } from "lit";
|
||||
import { css } from "lit";
|
||||
import { MdSubMenu } from "@material/web/menu/sub-menu";
|
||||
|
||||
@customElement("ha-sub-menu")
|
||||
// @ts-expect-error
|
||||
export class HaSubMenu extends MdSubMenu {
|
||||
async show() {
|
||||
super.show();
|
||||
this.menu.hasOverflow = false;
|
||||
}
|
||||
|
||||
static override styles: CSSResult[] = [
|
||||
MdSubMenu.styles,
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
|
@ -65,6 +65,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Array }) public createDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only targets with entities from specific domains.
|
||||
* @type {Array}
|
||||
@ -468,6 +470,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeEntities=${ensureArray(this.value?.entity_id)}
|
||||
.createDomains=${this.createDomains}
|
||||
@value-changed=${this._targetPicked}
|
||||
@click=${this._preventDefault}
|
||||
allow-custom-entity
|
||||
|
@ -797,6 +797,7 @@ export class HaAutomationTracer extends LitElement {
|
||||
description: html`${this.hass.localize(
|
||||
`ui.panel.config.automation.trace.messages.${message}`,
|
||||
{
|
||||
reason: this.trace.script_execution,
|
||||
time: renderFinishedAt(),
|
||||
executiontime: renderRuntime(),
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { EntityFilter } from "../common/entity/entity_filter";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
type StrictConnectionMode = "disabled" | "guard_page" | "drop_connection";
|
||||
|
||||
interface CloudStatusNotLoggedIn {
|
||||
logged_in: false;
|
||||
cloud: "disconnected" | "connecting" | "connected";
|
||||
@ -19,6 +21,7 @@ export interface CloudPreferences {
|
||||
alexa_enabled: boolean;
|
||||
remote_enabled: boolean;
|
||||
remote_allow_remote_enable: boolean;
|
||||
strict_connection: StrictConnectionMode;
|
||||
google_secure_devices_pin: string | undefined;
|
||||
cloudhooks: { [webhookId: string]: CloudWebhook };
|
||||
alexa_report_state: boolean;
|
||||
@ -141,6 +144,7 @@ export const updateCloudPref = (
|
||||
google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"];
|
||||
tts_default_voice?: CloudPreferences["tts_default_voice"];
|
||||
remote_allow_remote_enable?: CloudPreferences["remote_allow_remote_enable"];
|
||||
strict_connection?: CloudPreferences["strict_connection"];
|
||||
}
|
||||
) =>
|
||||
hass.callWS({
|
||||
|
@ -23,6 +23,8 @@ export interface ConfigEntry {
|
||||
pref_disable_polling: boolean;
|
||||
disabled_by: "user" | null;
|
||||
reason: string | null;
|
||||
error_reason_translation_key: string | null;
|
||||
error_reason_translation_placeholders: Record<string, string> | null;
|
||||
}
|
||||
|
||||
export type ConfigEntryMutableParams = Partial<
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
startOfDay,
|
||||
isFirstDayOfMonth,
|
||||
isLastDayOfMonth,
|
||||
} from "date-fns/esm";
|
||||
} from "date-fns";
|
||||
import { Collection, getCollection } from "home-assistant-js-websocket";
|
||||
import {
|
||||
calcDate,
|
||||
@ -95,6 +95,7 @@ export type EnergySolarForecasts = {
|
||||
export interface DeviceConsumptionEnergyPreference {
|
||||
// This is an ever increasing value
|
||||
stat_consumption: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface FlowFromGridSourceEnergyPreference {
|
||||
|
@ -422,7 +422,8 @@ export const computeHistory = (
|
||||
entityIds: string[],
|
||||
localize: LocalizeFunc,
|
||||
sensorNumericalDeviceClasses: string[],
|
||||
splitDeviceClasses = false
|
||||
splitDeviceClasses = false,
|
||||
forceNumeric = false
|
||||
): HistoryResult => {
|
||||
const lineChartDevices: { [unit: string]: HistoryStates } = {};
|
||||
const timelineDevices: TimelineEntity[] = [];
|
||||
@ -468,6 +469,7 @@ export const computeHistory = (
|
||||
let unit: string | undefined;
|
||||
|
||||
const isNumeric =
|
||||
forceNumeric ||
|
||||
isNumericFromDomain(domain) ||
|
||||
(currentState != null &&
|
||||
isNumericFromAttributes(currentState.attributes)) ||
|
||||
|
@ -9,12 +9,14 @@ export interface LabelRegistryEntry {
|
||||
name: string;
|
||||
icon: string | null;
|
||||
color: string | null;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
export interface LabelRegistryEntryMutableParams {
|
||||
name: string;
|
||||
icon?: string | null;
|
||||
color?: string | null;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export const fetchLabelRegistry = (conn: Connection) =>
|
||||
|
@ -5,9 +5,7 @@ import {
|
||||
import { getExtendedEntityRegistryEntry } from "./entity_registry";
|
||||
import { showEnterCodeDialog } from "../dialogs/enter-code/show-enter-code-dialog";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export const FORMAT_TEXT = "text";
|
||||
export const FORMAT_NUMBER = "number";
|
||||
import { UNAVAILABLE } from "./entity";
|
||||
|
||||
export const enum LockEntityFeature {
|
||||
OPEN = 1,
|
||||
@ -24,6 +22,33 @@ export interface LockEntity extends HassEntityBase {
|
||||
|
||||
type ProtectedLockService = "lock" | "unlock" | "open";
|
||||
|
||||
export function isLocked(stateObj: LockEntity) {
|
||||
return stateObj.state === "locked";
|
||||
}
|
||||
|
||||
export function isUnlocking(stateObj: LockEntity) {
|
||||
return stateObj.state === "unlocking";
|
||||
}
|
||||
|
||||
export function isLocking(stateObj: LockEntity) {
|
||||
return stateObj.state === "locking";
|
||||
}
|
||||
|
||||
export function isJammed(stateObj: LockEntity) {
|
||||
return stateObj.state === "jammed";
|
||||
}
|
||||
|
||||
export function isAvailable(stateObj: LockEntity) {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return false;
|
||||
}
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
return (
|
||||
assumedState ||
|
||||
(!isLocking(stateObj) && !isUnlocking(stateObj) && !isJammed(stateObj))
|
||||
);
|
||||
}
|
||||
|
||||
export const callProtectedLockService = async (
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
|
@ -270,7 +270,11 @@ export interface LanguageSelector {
|
||||
}
|
||||
|
||||
export interface LocationSelector {
|
||||
location: { radius?: boolean; icon?: string } | null;
|
||||
location: {
|
||||
radius?: boolean;
|
||||
radius_readonly?: boolean;
|
||||
icon?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface LocationSelectorValue {
|
||||
@ -401,6 +405,7 @@ export interface TargetSelector {
|
||||
target: {
|
||||
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
|
||||
device?: DeviceSelectorFilter | readonly DeviceSelectorFilter[];
|
||||
create_domains?: string[];
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,11 @@ export interface Zone {
|
||||
radius?: number;
|
||||
}
|
||||
|
||||
export interface HomeZoneMutableParams {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface ZoneMutableParams {
|
||||
name: string;
|
||||
icon?: string;
|
||||
|
@ -78,6 +78,7 @@ class LightColorTempPicker extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-control-slider
|
||||
touch-action="none"
|
||||
inverted
|
||||
vertical
|
||||
.value=${this._ctPickerValue}
|
||||
|
@ -9,11 +9,12 @@ import "../../../components/ha-control-button";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-outlined-icon-button";
|
||||
import "../../../components/ha-state-icon";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import {
|
||||
LockEntity,
|
||||
LockEntityFeature,
|
||||
callProtectedLockService,
|
||||
isAvailable,
|
||||
isJammed,
|
||||
} from "../../../data/lock";
|
||||
import "../../../state-control/lock/ha-state-control-lock-toggle";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
@ -85,15 +86,13 @@ class MoreInfoLock extends LitElement {
|
||||
"--state-color": color,
|
||||
};
|
||||
|
||||
const isJammed = this.stateObj.state === "jammed";
|
||||
|
||||
return html`
|
||||
<ha-more-info-state-header
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-more-info-state-header>
|
||||
<div class="controls" style=${styleMap(style)}>
|
||||
${this.stateObj.state === "jammed"
|
||||
${isJammed(this.stateObj)
|
||||
? html`
|
||||
<div class="status">
|
||||
<span></span>
|
||||
@ -125,7 +124,7 @@ class MoreInfoLock extends LitElement {
|
||||
`
|
||||
: html`
|
||||
<ha-control-button
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
.disabled=${!isAvailable(this.stateObj)}
|
||||
class="open-button ${this._buttonState}"
|
||||
@click=${this._open}
|
||||
>
|
||||
@ -139,7 +138,7 @@ class MoreInfoLock extends LitElement {
|
||||
: nothing}
|
||||
</div>
|
||||
<div>
|
||||
${isJammed
|
||||
${isJammed(this.stateObj)
|
||||
? html`
|
||||
<ha-control-button-group class="jammed">
|
||||
<ha-control-button @click=${this._unlock}>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { startOfYesterday, subHours } from "date-fns/esm";
|
||||
import { startOfYesterday, subHours } from "date-fns";
|
||||
import { LitElement, PropertyValues, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { startOfYesterday } from "date-fns/esm";
|
||||
import { startOfYesterday } from "date-fns";
|
||||
import { css, html, LitElement, PropertyValues, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
|
@ -9,7 +9,19 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { showAutomationEditor } from "../data/automation";
|
||||
import { HomeAssistantMain } from "../layouts/home-assistant-main";
|
||||
import type { EMIncomingMessageCommands } from "./external_messaging";
|
||||
import type {
|
||||
EMIncomingMessageBarCodeScanAborted,
|
||||
EMIncomingMessageBarCodeScanResult,
|
||||
EMIncomingMessageCommands,
|
||||
} from "./external_messaging";
|
||||
|
||||
const barCodeListeners = new Set<
|
||||
(
|
||||
msg:
|
||||
| EMIncomingMessageBarCodeScanResult
|
||||
| EMIncomingMessageBarCodeScanAborted
|
||||
) => boolean
|
||||
>();
|
||||
|
||||
export const attachExternalToApp = (hassMainEl: HomeAssistantMain) => {
|
||||
window.addEventListener("haptic", (ev) =>
|
||||
@ -24,6 +36,19 @@ export const attachExternalToApp = (hassMainEl: HomeAssistantMain) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const addExternalBarCodeListener = (
|
||||
listener: (
|
||||
msg:
|
||||
| EMIncomingMessageBarCodeScanResult
|
||||
| EMIncomingMessageBarCodeScanAborted
|
||||
) => boolean
|
||||
) => {
|
||||
barCodeListeners.add(listener);
|
||||
return () => {
|
||||
barCodeListeners.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
const handleExternalMessage = (
|
||||
hassMainEl: HomeAssistantMain,
|
||||
msg: EMIncomingMessageCommands
|
||||
@ -88,6 +113,22 @@ const handleExternalMessage = (
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else if (msg.command === "bar_code/scan_result") {
|
||||
barCodeListeners.forEach((listener) => listener(msg));
|
||||
bus.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else if (msg.command === "bar_code/aborted") {
|
||||
barCodeListeners.forEach((listener) => listener(msg));
|
||||
bus.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
@ -37,9 +37,11 @@ interface EMOutgoingMessageConfigGet extends EMMessage {
|
||||
|
||||
interface EMOutgoingMessageBarCodeScan extends EMMessage {
|
||||
type: "bar_code/scan";
|
||||
payload: {
|
||||
title: string;
|
||||
description: string;
|
||||
alternative_option_label?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageBarCodeClose extends EMMessage {
|
||||
@ -48,7 +50,9 @@ interface EMOutgoingMessageBarCodeClose extends EMMessage {
|
||||
|
||||
interface EMOutgoingMessageBarCodeNotify extends EMMessage {
|
||||
type: "bar_code/notify";
|
||||
payload: {
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageMatterCommission extends EMMessage {
|
||||
|
@ -41,14 +41,6 @@ import type { HomeAssistant, Route } from "../types";
|
||||
import "./hass-tabs-subpage";
|
||||
import type { PageNavigation } from "./hass-tabs-subpage";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"search-changed": { value: string };
|
||||
"clear-filter": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("hass-tabs-subpage-data-table")
|
||||
export class HaTabsSubpageDataTable extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@ -63,6 +55,8 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "main-page" }) public mainPage = false;
|
||||
|
||||
@property({ attribute: false }) public initialCollapsedGroups: string[] = [];
|
||||
|
||||
/**
|
||||
* Object with the columns.
|
||||
* @type {Object}
|
||||
@ -166,8 +160,15 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public showFilters = false;
|
||||
|
||||
@property({ attribute: false }) public initialSorting?: {
|
||||
column: string;
|
||||
direction: SortingDirection;
|
||||
};
|
||||
|
||||
@property() public initialGroupColumn?: string;
|
||||
|
||||
@property({ attribute: false }) public groupOrder?: string[];
|
||||
|
||||
@state() private _sortColumn?: string;
|
||||
|
||||
@state() private _sortDirection: SortingDirection = null;
|
||||
@ -190,9 +191,16 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
this._dataTable.clearSelection();
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
protected willUpdate() {
|
||||
if (this.hasUpdated) {
|
||||
return;
|
||||
}
|
||||
if (this.initialGroupColumn) {
|
||||
this._groupColumn = this.initialGroupColumn;
|
||||
this._setGroupColumn(this.initialGroupColumn);
|
||||
}
|
||||
if (this.initialSorting) {
|
||||
this._sortColumn = this.initialSorting.column;
|
||||
this._sortDirection = this.initialSorting.direction;
|
||||
}
|
||||
}
|
||||
|
||||
@ -418,6 +426,8 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
.sortColumn=${this._sortColumn}
|
||||
.sortDirection=${this._sortDirection}
|
||||
.groupColumn=${this._groupColumn}
|
||||
.groupOrder=${this.groupOrder}
|
||||
.initialCollapsedGroups=${this.initialCollapsedGroups}
|
||||
>
|
||||
${!this.narrow
|
||||
? html`
|
||||
@ -496,9 +506,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
${this.showFilters && !showPane
|
||||
? html`<ha-dialog
|
||||
open
|
||||
.heading=${localize("ui.components.subpage-data-table.filters", {
|
||||
number: this.data.length,
|
||||
})}
|
||||
.heading=${localize("ui.components.subpage-data-table.filters")}
|
||||
>
|
||||
<ha-dialog-header slot="heading">
|
||||
<ha-icon-button
|
||||
@ -510,9 +518,7 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
)}
|
||||
></ha-icon-button>
|
||||
<span slot="title"
|
||||
>${localize("ui.components.subpage-data-table.filters", {
|
||||
number: this.data.length,
|
||||
})}</span
|
||||
>${localize("ui.components.subpage-data-table.filters")}</span
|
||||
>
|
||||
${this.filters
|
||||
? html`<ha-icon-button
|
||||
@ -564,10 +570,20 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
this._sortDirection = null;
|
||||
}
|
||||
this._sortColumn = this._sortDirection === null ? undefined : columnId;
|
||||
|
||||
fireEvent(this, "sorting-changed", {
|
||||
column: columnId,
|
||||
direction: this._sortDirection,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleGroupBy(ev) {
|
||||
this._groupColumn = ev.currentTarget.value;
|
||||
this._setGroupColumn(ev.currentTarget.value);
|
||||
}
|
||||
|
||||
private _setGroupColumn(columnId: string) {
|
||||
this._groupColumn = columnId;
|
||||
fireEvent(this, "grouping-changed", { value: columnId });
|
||||
}
|
||||
|
||||
private _enableSelectMode() {
|
||||
@ -823,4 +839,11 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hass-tabs-subpage-data-table": HaTabsSubpageDataTable;
|
||||
}
|
||||
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"search-changed": { value: string };
|
||||
"grouping-changed": { value: string };
|
||||
"clear-filter": undefined;
|
||||
}
|
||||
}
|
||||
|
@ -169,10 +169,6 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
// @ts-ignore
|
||||
this._loadHassTranslations(this.hass!.language, "entity");
|
||||
|
||||
// Backwards compatibility for custom integrations
|
||||
// @ts-ignore
|
||||
this._loadHassTranslations(this.hass!.language, "state");
|
||||
|
||||
document.addEventListener(
|
||||
"visibilitychange",
|
||||
() => this._checkVisibility(),
|
||||
|
@ -1,7 +1,7 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiCalendarClock } from "@mdi/js";
|
||||
import { toDate } from "date-fns-tz";
|
||||
import { addDays, isSameDay } from "date-fns/esm";
|
||||
import { addDays, isSameDay } from "date-fns";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { formatDate } from "../../common/datetime/format_date";
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
addMilliseconds,
|
||||
differenceInMilliseconds,
|
||||
startOfHour,
|
||||
} from "date-fns/esm";
|
||||
} from "date-fns";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
|
@ -511,6 +511,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
mdiToggleSwitchOffOutline,
|
||||
mdiTransitConnection,
|
||||
} from "@mdi/js";
|
||||
import { differenceInDays } from "date-fns/esm";
|
||||
import { differenceInDays } from "date-fns";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
@ -37,15 +37,21 @@ import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
import "../../../components/chips/ha-assist-chip";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
@ -105,10 +111,6 @@ import { showCategoryRegistryDetailDialog } from "../category/show-dialog-catego
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import { showNewAutomationDialog } from "./show-dialog-new-automation";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
|
||||
type AutomationItem = AutomationEntity & {
|
||||
name: string;
|
||||
@ -156,6 +158,19 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _overflowAutomation?: AutomationItem;
|
||||
|
||||
@storage({ key: "automation-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "automation-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({
|
||||
key: "automation-table-collapsed",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@query("#overflow-menu") private _overflowMenu!: HaMenu;
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
@ -424,9 +439,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</div></ha-menu-item
|
||||
>`;
|
||||
|
||||
const labelsInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 700) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
|
||||
const automations = this._automations(
|
||||
this.automations,
|
||||
this._entityReg,
|
||||
@ -468,7 +485,12 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
this.hass.localize,
|
||||
this.hass.locale
|
||||
)}
|
||||
initialGroupColumn="category"
|
||||
.initialGroupColumn=${this._activeGrouping || "category"}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
.data=${automations}
|
||||
.empty=${!this.automations.length}
|
||||
@row-click=${this._handleRowClicked}
|
||||
@ -1236,6 +1258,18 @@ ${rejected
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
this._activeGrouping = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCollapseChanged(ev: CustomEvent) {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@ -6,6 +6,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/device/ha-device-picker";
|
||||
import "../../../../../components/device/ha-device-trigger-picker";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import { computeInitialHaFormData } from "../../../../../components/ha-form/compute-initial-ha-form-data";
|
||||
import { fullEntitiesContext } from "../../../../../data/context";
|
||||
import {
|
||||
deviceAutomationsEqual,
|
||||
@ -44,7 +45,9 @@ export class HaDeviceTrigger extends LitElement {
|
||||
|
||||
private _extraFieldsData = memoizeOne(
|
||||
(trigger: DeviceTrigger, capabilities: DeviceCapabilities) => {
|
||||
const extraFieldsData: Record<string, any> = {};
|
||||
const extraFieldsData = computeInitialHaFormData(
|
||||
capabilities.extra_fields
|
||||
);
|
||||
capabilities.extra_fields.forEach((item) => {
|
||||
if (trigger[item.name] !== undefined) {
|
||||
extraFieldsData![item.name] = trigger[item.name];
|
||||
|
@ -24,6 +24,7 @@ import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
import "../../../components/ha-button";
|
||||
@ -54,6 +55,7 @@ import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showAddBlueprintDialog } from "./show-dialog-import-blueprint";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
|
||||
type BlueprintMetaDataPath = BlueprintMetaData & {
|
||||
path: string;
|
||||
@ -92,8 +94,24 @@ class HaBlueprintOverview extends LitElement {
|
||||
Blueprints
|
||||
>;
|
||||
|
||||
@storage({ key: "blueprint-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "blueprint-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({
|
||||
key: "blueprint-table-collapsed",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
private _processedBlueprints = memoizeOne(
|
||||
(blueprints: Record<string, Blueprints>): BlueprintMetaDataPath[] => {
|
||||
(
|
||||
blueprints: Record<string, Blueprints>,
|
||||
localize: LocalizeFunc
|
||||
): BlueprintMetaDataPath[] => {
|
||||
const result: any[] = [];
|
||||
Object.entries(blueprints).forEach(([type, typeBlueprints]) =>
|
||||
Object.entries(typeBlueprints).forEach(([path, blueprint]) => {
|
||||
@ -101,6 +119,9 @@ class HaBlueprintOverview extends LitElement {
|
||||
result.push({
|
||||
name: blueprint.error,
|
||||
type,
|
||||
translated_type: localize(
|
||||
`ui.panel.config.blueprint.overview.types.${type as "automation" | "script"}`
|
||||
),
|
||||
error: true,
|
||||
path,
|
||||
fullpath: `${type}/${path}`,
|
||||
@ -109,6 +130,9 @@ class HaBlueprintOverview extends LitElement {
|
||||
result.push({
|
||||
...blueprint.metadata,
|
||||
type,
|
||||
translated_type: localize(
|
||||
`ui.panel.config.blueprint.overview.types.${type as "automation" | "script"}`
|
||||
),
|
||||
error: false,
|
||||
path,
|
||||
fullpath: `${type}/${path}`,
|
||||
@ -140,14 +164,11 @@ class HaBlueprintOverview extends LitElement {
|
||||
`
|
||||
: undefined,
|
||||
},
|
||||
type: {
|
||||
translated_type: {
|
||||
title: localize("ui.panel.config.blueprint.overview.headers.type"),
|
||||
template: (blueprint) =>
|
||||
html`${this.hass.localize(
|
||||
`ui.panel.config.blueprint.overview.types.${blueprint.type}`
|
||||
)}`,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
hidden: narrow,
|
||||
direction: "asc",
|
||||
width: "10%",
|
||||
@ -256,7 +277,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
this.hass.language,
|
||||
this.hass.localize
|
||||
)}
|
||||
.data=${this._processedBlueprints(this.blueprints)}
|
||||
.data=${this._processedBlueprints(this.blueprints, this.hass.localize)}
|
||||
id="fullpath"
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.config.blueprint.overview.no_blueprints"
|
||||
@ -281,6 +302,12 @@ class HaBlueprintOverview extends LitElement {
|
||||
>
|
||||
</a>
|
||||
</div>`}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
@ -341,9 +368,10 @@ class HaBlueprintOverview extends LitElement {
|
||||
}
|
||||
|
||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const blueprint = this._processedBlueprints(this.blueprints).find(
|
||||
(b) => b.fullpath === ev.detail.id
|
||||
)!;
|
||||
const blueprint = this._processedBlueprints(
|
||||
this.blueprints,
|
||||
this.hass.localize
|
||||
).find((b) => b.fullpath === ev.detail.id)!;
|
||||
if (blueprint.error) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.blueprint.overview.error", {
|
||||
@ -502,6 +530,18 @@ class HaBlueprintOverview extends LitElement {
|
||||
fireEvent(this, "reload-blueprints");
|
||||
};
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
this._activeGrouping = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCollapseChanged(ev: CustomEvent) {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return haStyle;
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import { showCloudCertificateDialog } from "../dialog-cloud-certificate/show-dialog-cloud-certificate";
|
||||
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
|
||||
|
||||
@customElement("cloud-remote-pref")
|
||||
export class CloudRemotePref extends LitElement {
|
||||
@ -33,7 +34,7 @@ export class CloudRemotePref extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const { remote_enabled, remote_allow_remote_enable } =
|
||||
const { remote_enabled, remote_allow_remote_enable, strict_connection } =
|
||||
this.cloudStatus.prefs;
|
||||
|
||||
const {
|
||||
@ -153,6 +154,61 @@ export class CloudRemotePref extends LitElement {
|
||||
@change=${this._toggleAllowRemoteEnabledChanged}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
<ha-settings-row>
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection"
|
||||
)}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_secondary"
|
||||
)}</span
|
||||
>
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_mode"
|
||||
)}
|
||||
@selected=${this._setStrictConnectionMode}
|
||||
naturalMenuWidth
|
||||
.value=${strict_connection}
|
||||
>
|
||||
<ha-list-item value="disabled">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_modes.disabled"
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="guard_page">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_modes.guard_page"
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="drop_connection">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_modes.drop_connection"
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-select>
|
||||
</ha-settings-row>
|
||||
${strict_connection !== "disabled"
|
||||
? html` <ha-settings-row>
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_link"
|
||||
)}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_link_secondary"
|
||||
)}</span
|
||||
>
|
||||
<ha-button @click=${this._createLoginUrl}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_create_link"
|
||||
)}</ha-button
|
||||
>
|
||||
</ha-settings-row>`
|
||||
: nothing}
|
||||
<ha-settings-row>
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
@ -223,6 +279,18 @@ export class CloudRemotePref extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _setStrictConnectionMode(ev) {
|
||||
const mode = ev.target.value;
|
||||
try {
|
||||
await updateCloudPref(this.hass, {
|
||||
strict_connection: mode,
|
||||
});
|
||||
fireEvent(this, "ha-refresh-cloud-status");
|
||||
} catch (err: any) {
|
||||
alert(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
private async _copyURL(ev): Promise<void> {
|
||||
const url = ev.currentTarget.url;
|
||||
await copyToClipboard(url);
|
||||
@ -231,6 +299,40 @@ export class CloudRemotePref extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private async _createLoginUrl() {
|
||||
try {
|
||||
const result = await this.hass.callService(
|
||||
"cloud",
|
||||
"create_temporary_strict_connection_url",
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
true
|
||||
);
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_link"
|
||||
),
|
||||
text: html`${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_link_created_message"
|
||||
)}
|
||||
<pre>${result.response.url}</pre>
|
||||
<ha-button
|
||||
.url=${result.response.url}
|
||||
@click=${this._copyURL}
|
||||
unelevated
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_copy_link"
|
||||
)}
|
||||
</ha-button>`,
|
||||
});
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, { text: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.preparing {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { UNIT_C } from "../../../common/const";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
@ -22,8 +21,6 @@ import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../../components/ha-textfield";
|
||||
import "../../../components/ha-timezone-picker";
|
||||
import "../../../components/map/ha-locations-editor";
|
||||
import type { MarkerLocation } from "../../../components/map/ha-locations-editor";
|
||||
import { ConfigUpdateValues, saveCoreConfig } from "../../../data/core";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
@ -242,19 +239,7 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
>
|
||||
</ha-language-picker>
|
||||
</div>
|
||||
${this.narrow
|
||||
? html`
|
||||
<ha-locations-editor
|
||||
.hass=${this.hass}
|
||||
.locations=${this._markerLocation(
|
||||
this.hass.config.latitude,
|
||||
this.hass.config.longitude,
|
||||
this._location
|
||||
)}
|
||||
@location-updated=${this._locationChanged}
|
||||
></ha-locations-editor>
|
||||
`
|
||||
: html`
|
||||
|
||||
<ha-settings-row>
|
||||
<div slot="heading">
|
||||
${this.hass.localize(
|
||||
@ -270,7 +255,6 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
>${this.hass.localize("ui.common.edit")}</mwc-button
|
||||
>
|
||||
</ha-settings-row>
|
||||
`}
|
||||
<div class="card-actions">
|
||||
<ha-progress-button @click=${this._updateEntry}>
|
||||
${this.hass!.localize("ui.panel.config.zone.detail.update")}
|
||||
@ -319,10 +303,6 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
this._updateUnits = (ev.target as HaCheckbox).checked;
|
||||
}
|
||||
|
||||
private _locationChanged(ev: CustomEvent) {
|
||||
this._location = ev.detail.location;
|
||||
}
|
||||
|
||||
private async _updateEntry(ev: CustomEvent) {
|
||||
const button = ev.target as HaProgressButton;
|
||||
if (button.progress) {
|
||||
@ -381,21 +361,6 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _markerLocation = memoizeOne(
|
||||
(
|
||||
lat: number,
|
||||
lng: number,
|
||||
location?: [number, number]
|
||||
): MarkerLocation[] => [
|
||||
{
|
||||
id: "location",
|
||||
latitude: location ? location[0] : lat,
|
||||
longitude: location ? location[1] : lng,
|
||||
location_editable: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
private _editLocation() {
|
||||
navigate("/config/zone/edit/zone.home");
|
||||
}
|
||||
@ -441,11 +406,6 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
margin-top: 8px;
|
||||
display: inline-block;
|
||||
}
|
||||
ha-locations-editor {
|
||||
display: block;
|
||||
height: 400px;
|
||||
padding: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -82,11 +82,11 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.updates.show_skipped")}
|
||||
</ha-check-list-item>
|
||||
${this._supervisorInfo?.channel !== "dev"
|
||||
${this._supervisorInfo && this._supervisorInfo.channel !== "dev"
|
||||
? html`
|
||||
<li divider role="separator"></li>
|
||||
<mwc-list-item @request-selected=${this._toggleBeta}>
|
||||
${this._supervisorInfo?.channel === "stable"
|
||||
${this._supervisorInfo.channel === "stable"
|
||||
? this.hass.localize("ui.panel.config.updates.join_beta")
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.updates.leave_beta"
|
||||
|
@ -93,10 +93,19 @@ const randomTip = (hass: HomeAssistant, narrow: boolean) => {
|
||||
weight: 2,
|
||||
narrow: true,
|
||||
},
|
||||
{ content: hass.localize("ui.tips.key_c_hint"), weight: 1, narrow: false },
|
||||
{ content: hass.localize("ui.tips.key_m_hint"), weight: 1, narrow: false },
|
||||
];
|
||||
|
||||
if (hass?.enableShortcuts) {
|
||||
tips.push(
|
||||
{
|
||||
content: hass.localize("ui.tips.key_c_hint"),
|
||||
weight: 1,
|
||||
narrow: false,
|
||||
},
|
||||
{ content: hass.localize("ui.tips.key_m_hint"), weight: 1, narrow: false }
|
||||
);
|
||||
}
|
||||
|
||||
if (narrow) {
|
||||
tips = tips.filter((tip) => tip.narrow);
|
||||
}
|
||||
@ -310,7 +319,9 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
private _showQuickBar(): void {
|
||||
showQuickBar(this, {
|
||||
commandMode: true,
|
||||
hint: this.hass.localize("ui.dialogs.quick-bar.key_c_hint"),
|
||||
hint: this.hass.enableShortcuts
|
||||
? this.hass.localize("ui.dialogs.quick-bar.key_c_hint")
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,12 @@
|
||||
import { consume } from "@lit-labs/context";
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import { mdiChevronRight, mdiMenuDown, mdiPlus } from "@mdi/js";
|
||||
import {
|
||||
mdiChevronRight,
|
||||
mdiDotsVertical,
|
||||
mdiMenuDown,
|
||||
mdiPlus,
|
||||
mdiTextureBox,
|
||||
} from "@mdi/js";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
@ -10,10 +16,12 @@ import {
|
||||
nothing,
|
||||
} from "lit";
|
||||
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import {
|
||||
@ -22,10 +30,15 @@ import {
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/entity/ha-battery-icon";
|
||||
@ -41,6 +54,7 @@ import "../../../components/ha-filter-states";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { ConfigEntry, sortConfigEntries } from "../../../data/config_entries";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import {
|
||||
@ -65,15 +79,12 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "../integrations/ha-integration-overflow-menu";
|
||||
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
import { showAlertDialog } from "../../lovelace/custom-card-helpers";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
|
||||
interface DeviceRowData extends DeviceRegistryEntry {
|
||||
device?: DeviceRowData;
|
||||
@ -117,6 +128,19 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
@state()
|
||||
_labels!: LabelRegistryEntry[];
|
||||
|
||||
@storage({ key: "devices-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "devices-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({ key: "devices-table-collapsed", state: false, subscribe: false })
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
});
|
||||
|
||||
private _ignoreLocationChange = false;
|
||||
|
||||
public connectedCallback() {
|
||||
@ -549,6 +573,41 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
this._labels
|
||||
);
|
||||
|
||||
const areasInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 700) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
|
||||
const areaItems = html`${Object.values(this.hass.areas).map(
|
||||
(area) =>
|
||||
html`<ha-menu-item
|
||||
.value=${area.area_id}
|
||||
@click=${this._handleBulkArea}
|
||||
>
|
||||
${area.icon
|
||||
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
<div slot="headline">${area.name}</div>
|
||||
</ha-menu-item>`
|
||||
)}
|
||||
<ha-menu-item .value=${null} @click=${this._handleBulkArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-menu-item>
|
||||
<md-divider role="separator" tabindex="-1"></md-divider>
|
||||
<ha-menu-item @click=${this._bulkCreateArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-menu-item>`;
|
||||
|
||||
const labelItems = html`${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((deviceId) =>
|
||||
@ -614,8 +673,14 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@clear-filter=${this._clearFilter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
@row-click=${this._handleRowClicked}
|
||||
clickable
|
||||
hasFab
|
||||
@ -696,9 +761,29 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${labelItems}
|
||||
</ha-button-menu-new>`
|
||||
: html` <ha-button-menu-new has-overflow slot="selection-bar"
|
||||
><ha-assist-chip
|
||||
</ha-button-menu-new>
|
||||
|
||||
${areasInOverflow
|
||||
? nothing
|
||||
: html`<ha-button-menu-new slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${areaItems}
|
||||
</ha-button-menu-new>`}`
|
||||
: nothing}
|
||||
${this.narrow || areasInOverflow
|
||||
? html`<ha-button-menu-new has-overflow slot="selection-bar">
|
||||
${this.narrow
|
||||
? html`<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
@ -708,8 +793,14 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
<ha-sub-menu>
|
||||
</ha-assist-chip>`
|
||||
: html`<ha-icon-button
|
||||
.path=${mdiDotsVertical}
|
||||
.label=${"ui.panel.config.automation.picker.bulk_action"}
|
||||
slot="trigger"
|
||||
></ha-icon-button>`}
|
||||
${this.narrow
|
||||
? html` <ha-sub-menu>
|
||||
<ha-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
@ -722,8 +813,24 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
></ha-svg-icon>
|
||||
</ha-menu-item>
|
||||
<ha-menu slot="menu">${labelItems}</ha-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing}
|
||||
<ha-sub-menu>
|
||||
<ha-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-menu-item>
|
||||
<ha-menu slot="menu">${areaItems}</ha-menu>
|
||||
</ha-sub-menu>
|
||||
</ha-button-menu-new>`}
|
||||
</ha-button-menu-new>`
|
||||
: nothing}
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
@ -809,6 +916,46 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
this._selected = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _handleBulkArea(ev) {
|
||||
const area = ev.currentTarget.value;
|
||||
this._bulkAddArea(area);
|
||||
}
|
||||
|
||||
private async _bulkAddArea(area: string) {
|
||||
const promises: Promise<DeviceRegistryEntry>[] = [];
|
||||
this._selected.forEach((deviceId) => {
|
||||
promises.push(
|
||||
updateDeviceRegistryEntry(this.hass, deviceId, {
|
||||
area_id: area,
|
||||
})
|
||||
);
|
||||
});
|
||||
const result = await Promise.allSettled(promises);
|
||||
if (hasRejectedItems(result)) {
|
||||
const rejected = rejectedItems(result);
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.common.multiselect.failed", {
|
||||
number: rejected.length,
|
||||
}),
|
||||
text: html`<pre>
|
||||
${rejected
|
||||
.map((r) => r.reason.message || r.reason.code || r.reason)
|
||||
.join("\r\n")}</pre
|
||||
>`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _bulkCreateArea() {
|
||||
showAreaRegistryDetailDialog(this, {
|
||||
createEntry: async (values) => {
|
||||
const area = await createAreaRegistryEntry(this.hass, values);
|
||||
this._bulkAddArea(area.area_id);
|
||||
return area;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleBulkLabel(ev) {
|
||||
const label = ev.currentTarget.value;
|
||||
const action = ev.currentTarget.action;
|
||||
@ -855,9 +1002,24 @@ ${rejected
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
this._activeGrouping = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCollapseChanged(ev: CustomEvent) {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
hass-tabs-subpage-data-table {
|
||||
--data-table-row-height: 60px;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiDelete, mdiDevices } from "@mdi/js";
|
||||
import { mdiDelete, mdiDevices, mdiPencil } from "@mdi/js";
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
@ -83,18 +83,24 @@ export class EnergyDeviceSettings extends LitElement {
|
||||
${this.preferences.device_consumption.map((device) => {
|
||||
const entityState = this.hass.states[device.stat_consumption];
|
||||
return html`
|
||||
<div class="row">
|
||||
<div class="row" .device=${device}>
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entityState}
|
||||
></ha-state-icon>
|
||||
<span class="content"
|
||||
>${getStatisticLabel(
|
||||
>${device.name ||
|
||||
getStatisticLabel(
|
||||
this.hass,
|
||||
device.stat_consumption,
|
||||
this.statsMetadata?.[device.stat_consumption]
|
||||
)}</span
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.edit")}
|
||||
@click=${this._editDevice}
|
||||
.path=${mdiPencil}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.common.delete")}
|
||||
@click=${this._deleteDevice}
|
||||
@ -117,6 +123,24 @@ export class EnergyDeviceSettings extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _editDevice(ev) {
|
||||
const origDevice: DeviceConsumptionEnergyPreference =
|
||||
ev.currentTarget.closest(".row").device;
|
||||
showEnergySettingsDeviceDialog(this, {
|
||||
device: { ...origDevice },
|
||||
device_consumptions: this.preferences
|
||||
.device_consumption as DeviceConsumptionEnergyPreference[],
|
||||
saveCallback: async (newDevice) => {
|
||||
await this._savePreferences({
|
||||
...this.preferences,
|
||||
device_consumption: this.preferences.device_consumption.map((d) =>
|
||||
d === origDevice ? newDevice : d
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _addDevice() {
|
||||
showEnergySettingsDeviceDialog(this, {
|
||||
device_consumptions: this.preferences
|
||||
|
@ -44,9 +44,10 @@ export class DialogEnergyDeviceSettings
|
||||
this._energy_units = (
|
||||
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
|
||||
).units;
|
||||
this._excludeList = this._params.device_consumptions.map(
|
||||
(entry) => entry.stat_consumption
|
||||
);
|
||||
this._device = this._params.device;
|
||||
this._excludeList = this._params.device_consumptions
|
||||
.map((entry) => entry.stat_consumption)
|
||||
.filter((id) => id !== this._device?.stat_consumption);
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
@ -88,6 +89,7 @@ export class DialogEnergyDeviceSettings
|
||||
.hass=${this.hass}
|
||||
.helpMissingEntityUrl=${energyStatisticHelpUrl}
|
||||
.includeUnitClass=${energyUnitClasses}
|
||||
.value=${this._device?.stat_consumption}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption.dialog.device_consumption_energy"
|
||||
)}
|
||||
@ -96,6 +98,17 @@ export class DialogEnergyDeviceSettings
|
||||
dialogInitialFocus
|
||||
></ha-statistic-picker>
|
||||
|
||||
<ha-textfield
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.device_consumption.dialog.display_name"
|
||||
)}
|
||||
type="text"
|
||||
.disabled=${!this._device}
|
||||
.value=${this._device?.name || ""}
|
||||
@change=${this._nameChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
|
||||
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
@ -118,6 +131,17 @@ export class DialogEnergyDeviceSettings
|
||||
this._device = { stat_consumption: ev.detail.value };
|
||||
}
|
||||
|
||||
private _nameChanged(ev) {
|
||||
const newDevice = {
|
||||
...this._device!,
|
||||
name: ev.target!.value,
|
||||
} as DeviceConsumptionEnergyPreference;
|
||||
if (!newDevice.name) {
|
||||
delete newDevice.name;
|
||||
}
|
||||
this._device = newDevice;
|
||||
}
|
||||
|
||||
private async _save() {
|
||||
try {
|
||||
await this._params!.saveCallback(this._device!);
|
||||
@ -134,6 +158,10 @@ export class DialogEnergyDeviceSettings
|
||||
ha-statistic-picker {
|
||||
width: 100%;
|
||||
}
|
||||
ha-textfield {
|
||||
margin-top: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -70,6 +70,7 @@ export interface EnergySettingsWaterDialogParams {
|
||||
}
|
||||
|
||||
export interface EnergySettingsDeviceDialogParams {
|
||||
device?: DeviceConsumptionEnergyPreference;
|
||||
device_consumptions: DeviceConsumptionEnergyPreference[];
|
||||
saveCallback: (device: DeviceConsumptionEnergyPreference) => Promise<void>;
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoize from "memoize-one";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
@ -37,10 +38,15 @@ import {
|
||||
protocolIntegrationPicked,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/ha-alert";
|
||||
@ -66,6 +72,11 @@ import {
|
||||
removeEntityRegistryEntry,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../data/entity_registry";
|
||||
import {
|
||||
EntitySources,
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../../data/entity_sources";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import {
|
||||
LabelRegistryEntry,
|
||||
createLabelRegistryEntry,
|
||||
@ -86,14 +97,6 @@ import { configSections } from "../ha-panel-config";
|
||||
import "../integrations/ha-integration-overflow-menu";
|
||||
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import {
|
||||
EntitySources,
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../../data/entity_sources";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
|
||||
export interface StateEntity
|
||||
extends Omit<EntityRegistryEntry, "id" | "unique_id"> {
|
||||
@ -110,6 +113,7 @@ export interface EntityRow extends StateEntity {
|
||||
status: string | undefined;
|
||||
area?: string;
|
||||
localized_platform: string;
|
||||
domain: string;
|
||||
label_entries: LabelRegistryEntry[];
|
||||
}
|
||||
|
||||
@ -149,6 +153,19 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@storage({ key: "entities-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "entities-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({
|
||||
key: "entities-table-collapsed",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@query("hass-tabs-subpage-data-table", true)
|
||||
private _dataTable!: HaTabsSubpageDataTable;
|
||||
|
||||
@ -261,6 +278,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
filterable: true,
|
||||
width: "20%",
|
||||
},
|
||||
domain: {
|
||||
title: localize("ui.panel.config.entities.picker.headers.domain"),
|
||||
sortable: false,
|
||||
hidden: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
},
|
||||
area: {
|
||||
title: localize("ui.panel.config.entities.picker.headers.area"),
|
||||
sortable: true,
|
||||
@ -467,9 +491,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
),
|
||||
unavailable,
|
||||
restored,
|
||||
localized_platform:
|
||||
localize(`component.${entry.platform}.title`) || entry.platform,
|
||||
localized_platform: domainToName(localize, entry.platform),
|
||||
area: area ? area.name : "—",
|
||||
domain: domainToName(localize, computeDomain(entry.entity_id)),
|
||||
status: restored
|
||||
? localize("ui.panel.config.entities.picker.status.restored")
|
||||
: unavailable
|
||||
@ -594,6 +618,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
.filter=${this._filter}
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
clickable
|
||||
@clear-filter=${this._clearFilter}
|
||||
@ -1196,6 +1226,18 @@ ${rejected
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
this._activeGrouping = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCollapseChanged(ev: CustomEvent) {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@ -32,7 +32,7 @@ import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-c
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { Helper, HelperDomain } from "./const";
|
||||
import { Helper, HelperDomain, isHelperDomain } from "./const";
|
||||
import type { ShowDialogHelperDetailParams } from "./show-dialog-helper-detail";
|
||||
|
||||
type HelperCreators = {
|
||||
@ -96,7 +96,7 @@ export class DialogHelperDetail extends LitElement {
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _domain?: HelperDomain;
|
||||
@state() private _domain?: string;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@ -114,8 +114,12 @@ export class DialogHelperDetail extends LitElement {
|
||||
this._params = params;
|
||||
this._domain = params.domain;
|
||||
this._item = undefined;
|
||||
if (this._domain && this._domain in HELPERS) {
|
||||
await HELPERS[this._domain].import();
|
||||
}
|
||||
this._opened = true;
|
||||
await this.updateComplete;
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
Promise.all([
|
||||
getConfigFlowHandlers(this.hass, ["helper"]),
|
||||
// Ensure the titles are loaded before we render the flows.
|
||||
@ -141,7 +145,7 @@ export class DialogHelperDetail extends LitElement {
|
||||
if (this._domain) {
|
||||
content = html`
|
||||
<div class="form" @value-changed=${this._valueChanged}>
|
||||
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : ""}
|
||||
${dynamicElement(`ha-${this._domain}-form`, {
|
||||
hass: this.hass,
|
||||
item: this._item,
|
||||
@ -155,13 +159,15 @@ export class DialogHelperDetail extends LitElement {
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.helpers.dialog.create")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
${this._params?.domain
|
||||
? nothing
|
||||
: html`<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._goBack}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.common.back")}
|
||||
</mwc-button>
|
||||
</mwc-button>`}
|
||||
`;
|
||||
} else if (this._loading || this._helperFlows === undefined) {
|
||||
content = html`<ha-circular-progress
|
||||
@ -253,9 +259,13 @@ export class DialogHelperDetail extends LitElement {
|
||||
"ui.panel.config.helpers.dialog.create_platform",
|
||||
{
|
||||
platform:
|
||||
(isHelperDomain(this._domain) &&
|
||||
this.hass.localize(
|
||||
`ui.panel.config.helpers.types.${this._domain}`
|
||||
) || this._domain,
|
||||
`ui.panel.config.helpers.types.${
|
||||
this._domain as HelperDomain
|
||||
}`
|
||||
)) ||
|
||||
this._domain,
|
||||
}
|
||||
)
|
||||
: this.hass.localize("ui.panel.config.helpers.dialog.create_helper")
|
||||
@ -277,7 +287,16 @@ export class DialogHelperDetail extends LitElement {
|
||||
this._submitting = true;
|
||||
this._error = "";
|
||||
try {
|
||||
await HELPERS[this._domain].create(this.hass, this._item);
|
||||
const createdEntity = await HELPERS[this._domain].create(
|
||||
this.hass,
|
||||
this._item
|
||||
);
|
||||
if (this._params?.dialogClosedCallback && createdEntity.id) {
|
||||
this._params.dialogClosedCallback({
|
||||
flowFinished: true,
|
||||
entityId: `${this._domain}.${createdEntity.id}`,
|
||||
});
|
||||
}
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = err.message || "Unknown error";
|
||||
|
@ -2,7 +2,7 @@ import { Calendar, CalendarOptions } from "@fullcalendar/core";
|
||||
import allLocales from "@fullcalendar/core/locales-all";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import { addDays, isSameDay, isSameWeek, nextDay } from "date-fns";
|
||||
import { Day, addDays, isSameDay, isSameWeek, nextDay } from "date-fns";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
|
@ -24,6 +24,7 @@ import {
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
@ -40,6 +41,7 @@ import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/ha-fab";
|
||||
@ -139,6 +141,19 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@storage({ key: "helpers-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "helpers-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({
|
||||
key: "helpers-table-collapsed",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@state() private _stateItems: HassEntity[] = [];
|
||||
|
||||
@state() private _entityEntries?: Record<string, EntityRegistryEntry>;
|
||||
@ -525,7 +540,12 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
).length}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.data=${helpers}
|
||||
initialGroupColumn="category"
|
||||
.initialGroupColumn=${this._activeGrouping || "category"}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
.activeFilters=${this._activeFilters}
|
||||
@clear-filter=${this._clearFilter}
|
||||
@row-click=${this._openEditDialog}
|
||||
@ -1020,6 +1040,18 @@ ${rejected
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
this._activeGrouping = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCollapseChanged(ev: CustomEvent) {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { DataEntryFlowDialogParams } from "../../../dialogs/config-flow/show-dialog-data-entry-flow";
|
||||
import { HelperDomain } from "./const";
|
||||
|
||||
export const loadHelperDetailDialog = () => import("./dialog-helper-detail");
|
||||
|
||||
export interface ShowDialogHelperDetailParams {
|
||||
domain?: HelperDomain;
|
||||
// Only used for config entries
|
||||
dialogClosedCallback?: DataEntryFlowDialogParams["dialogClosedCallback"];
|
||||
domain?: string;
|
||||
dialogClosedCallback?: (params: {
|
||||
flowFinished: boolean;
|
||||
entryId?: string;
|
||||
entityId?: string;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export const showHelperDetailDialog = (
|
||||
|
@ -37,6 +37,7 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { until } from "lit/directives/until";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { isDevVersion } from "../../../common/config/version";
|
||||
@ -550,10 +551,24 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
`ui.panel.config.integrations.config_entry.state.${item.state}`,
|
||||
];
|
||||
if (item.reason) {
|
||||
this.hass.loadBackendTranslation("config", item.domain);
|
||||
stateTextExtra = html`${this.hass.localize(
|
||||
`component.${item.domain}.config.error.${item.reason}`
|
||||
) || item.reason}`;
|
||||
if (item.error_reason_translation_key) {
|
||||
const lokalisePromExc = this.hass
|
||||
.loadBackendTranslation("exceptions", item.domain)
|
||||
.then((localize) =>
|
||||
localize(
|
||||
`component.${item.domain}.exceptions.${item.error_reason_translation_key}.message`,
|
||||
item.error_reason_translation_placeholders ?? undefined
|
||||
)
|
||||
);
|
||||
stateTextExtra = html`${until(lokalisePromExc)}`;
|
||||
} else {
|
||||
const lokalisePromError = this.hass
|
||||
.loadBackendTranslation("config", item.domain)
|
||||
.then((localize) =>
|
||||
localize(`component.${item.domain}.config.error.${item.reason}`)
|
||||
);
|
||||
stateTextExtra = html`${until(lokalisePromError, item.reason)}`;
|
||||
}
|
||||
} else {
|
||||
stateTextExtra = html`
|
||||
<br />
|
||||
|
@ -238,6 +238,9 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
label: this._buildLabel(device),
|
||||
shape: this._getShape(device),
|
||||
mass: this._getMass(device),
|
||||
color: {
|
||||
background: device.available ? "#66FF99" : "#FF9999",
|
||||
},
|
||||
});
|
||||
if (device.neighbors && device.neighbors.length > 0) {
|
||||
device.neighbors.forEach((neighbor) => {
|
||||
@ -249,13 +252,29 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
from: device.ieee,
|
||||
to: neighbor.ieee,
|
||||
label: neighbor.lqi + "",
|
||||
color: this._getLQI(parseInt(neighbor.lqi)),
|
||||
color: this._getLQI(parseInt(neighbor.lqi)).color,
|
||||
width: this._getLQI(parseInt(neighbor.lqi)).width,
|
||||
length: 2000 - 4 * parseInt(neighbor.lqi),
|
||||
arrows: {
|
||||
from: {
|
||||
enabled: neighbor.relationship !== "Child",
|
||||
},
|
||||
},
|
||||
dashes: neighbor.relationship !== "Child",
|
||||
});
|
||||
} else {
|
||||
edges[idx].color = this._getLQI(
|
||||
(parseInt(edges[idx].label!) + parseInt(neighbor.lqi)) / 2
|
||||
);
|
||||
).color;
|
||||
edges[idx].width = this._getLQI(
|
||||
(parseInt(edges[idx].label!) + parseInt(neighbor.lqi)) / 2
|
||||
).width;
|
||||
edges[idx].length =
|
||||
2000 -
|
||||
6 * ((parseInt(edges[idx].label!) + parseInt(neighbor.lqi)) / 2);
|
||||
edges[idx].label += "/" + neighbor.lqi;
|
||||
delete edges[idx].arrows;
|
||||
delete edges[idx].dashes;
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -264,20 +283,23 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
this._network?.setData({ nodes: this._nodes, edges: edges });
|
||||
}
|
||||
|
||||
private _getLQI(lqi: number): EdgeOptions["color"] {
|
||||
private _getLQI(lqi: number): EdgeOptions {
|
||||
if (lqi > 192) {
|
||||
return { color: "#17ab00", highlight: "#17ab00" };
|
||||
return { color: { color: "#17ab00", highlight: "#17ab00" }, width: 4 };
|
||||
}
|
||||
if (lqi > 128) {
|
||||
return { color: "#e6b402", highlight: "#e6b402" };
|
||||
return { color: { color: "#e6b402", highlight: "#e6b402" }, width: 3 };
|
||||
}
|
||||
if (lqi > 80) {
|
||||
return { color: "#fc4c4c", highlight: "#fc4c4c" };
|
||||
return { color: { color: "#fc4c4c", highlight: "#fc4c4c" }, width: 2 };
|
||||
}
|
||||
return { color: "#bfbfbf", highlight: "#bfbfbf" };
|
||||
return { color: { color: "#bfbfbf", highlight: "#bfbfbf" }, width: 1 };
|
||||
}
|
||||
|
||||
private _getMass(device: ZHADevice): number {
|
||||
if (!device.available) {
|
||||
return 6;
|
||||
}
|
||||
if (device.device_type === "Coordinator") {
|
||||
return 2;
|
||||
}
|
||||
@ -312,8 +334,8 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
} else {
|
||||
label += "\n<b>Device is not in <i>'zigbee.db'</i></b>";
|
||||
}
|
||||
if (!device.available) {
|
||||
label += "\n<b>Device is <i>Offline</i></b>";
|
||||
if (device.area_id) {
|
||||
label += `\n<b>Area ID: </b>${device.area_id}`;
|
||||
}
|
||||
return label;
|
||||
}
|
||||
@ -402,7 +424,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
? {
|
||||
physics: {
|
||||
barnesHut: {
|
||||
springConstant: 0,
|
||||
springConstant: 0.05,
|
||||
avoidOverlap: 10,
|
||||
damping: 0.09,
|
||||
},
|
||||
|
@ -235,7 +235,7 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
|
||||
${item.metadata.label}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${item.metadata.description || item.metadata.label}
|
||||
${item.metadata.description}
|
||||
${item.metadata.description !== null && !item.metadata.writeable
|
||||
? html`<br />`
|
||||
: nothing}
|
||||
|
@ -7,6 +7,7 @@ import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/ha-textarea";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-color-picker";
|
||||
import { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
@ -31,6 +32,8 @@ class DialogLabelDetail
|
||||
|
||||
@state() private _color!: string;
|
||||
|
||||
@state() private _description!: string;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _params?: LabelDetailDialogParams;
|
||||
@ -44,10 +47,12 @@ class DialogLabelDetail
|
||||
this._name = this._params.entry.name || "";
|
||||
this._icon = this._params.entry.icon || "";
|
||||
this._color = this._params.entry.color || "";
|
||||
this._description = this._params.entry.description || "";
|
||||
} else {
|
||||
this._name = this._params.suggestedName || "";
|
||||
this._icon = "";
|
||||
this._color = "";
|
||||
this._description = "";
|
||||
}
|
||||
document.body.addEventListener("keydown", this._handleKeyPress);
|
||||
}
|
||||
@ -118,6 +123,14 @@ class DialogLabelDetail
|
||||
"ui.panel.config.labels.detail.color"
|
||||
)}
|
||||
></ha-color-picker>
|
||||
<ha-textarea
|
||||
.value=${this._description}
|
||||
.configValue=${"description"}
|
||||
@input=${this._input}
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.config.labels.detail.description"
|
||||
)}
|
||||
></ha-textarea>
|
||||
</div>
|
||||
</div>
|
||||
${this._params.entry && this._params.removeEntry
|
||||
@ -169,6 +182,7 @@ class DialogLabelDetail
|
||||
name: this._name.trim(),
|
||||
icon: this._icon.trim() || null,
|
||||
color: this._color.trim() || null,
|
||||
description: this._description.trim() || null,
|
||||
};
|
||||
if (this._params!.entry) {
|
||||
newValue = await this._params!.updateEntry!(values);
|
||||
@ -202,12 +216,14 @@ class DialogLabelDetail
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-textarea,
|
||||
ha-textfield,
|
||||
ha-icon-picker,
|
||||
ha-color-picker {
|
||||
display: block;
|
||||
}
|
||||
ha-color-picker {
|
||||
ha-color-picker,
|
||||
ha-textarea {
|
||||
margin-top: 16px;
|
||||
}
|
||||
`,
|
||||
|
@ -10,15 +10,17 @@ import { LitElement, PropertyValues, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-relative-time";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-relative-time";
|
||||
import {
|
||||
LabelRegistryEntry,
|
||||
LabelRegistryEntryMutableParams,
|
||||
@ -35,7 +37,7 @@ import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showLabelDetailDialog } from "./show-dialog-label-detail";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
|
||||
@customElement("ha-config-labels")
|
||||
export class HaConfigLabels extends LitElement {
|
||||
@ -49,6 +51,13 @@ export class HaConfigLabels extends LitElement {
|
||||
|
||||
@state() private _labels: LabelRegistryEntry[] = [];
|
||||
|
||||
@storage({
|
||||
key: "labels-table-sort",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
private _columns = memoizeOne((localize: LocalizeFunc) => {
|
||||
const columns: DataTableColumnContainer<LabelRegistryEntry> = {
|
||||
icon: {
|
||||
@ -79,6 +88,12 @@ export class HaConfigLabels extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
grows: true,
|
||||
template: (label) => html`
|
||||
<div>${label.name}</div>
|
||||
${label.description
|
||||
? html`<div class="secondary">${label.description}</div>`
|
||||
: nothing}
|
||||
`,
|
||||
},
|
||||
actions: {
|
||||
title: "",
|
||||
@ -143,6 +158,8 @@ export class HaConfigLabels extends LitElement {
|
||||
.data=${this._data(this._labels)}
|
||||
.noDataText=${this.hass.localize("ui.panel.config.labels.no_labels")}
|
||||
hasFab
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@row-click=${this._editLabel}
|
||||
clickable
|
||||
id="label_id"
|
||||
@ -262,6 +279,10 @@ export class HaConfigLabels extends LitElement {
|
||||
`/config/automation/dashboard?historyBack=1&label=${label.label_id}`
|
||||
);
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -16,6 +16,7 @@ import { stringCompare } from "../../../../common/string/compare";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../../components/data-table/ha-data-table";
|
||||
import "../../../../components/ha-clickable-list-item";
|
||||
import "../../../../components/ha-fab";
|
||||
@ -46,6 +47,7 @@ import { showNewDashboardDialog } from "../../dashboard/show-dialog-new-dashboar
|
||||
import { lovelaceTabs } from "../ha-config-lovelace";
|
||||
import { showDashboardConfigureStrategyDialog } from "./show-dialog-lovelace-dashboard-configure-strategy";
|
||||
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
|
||||
type DataTableItem = Pick<
|
||||
LovelaceDashboard,
|
||||
@ -68,6 +70,13 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
|
||||
@state() private _dashboards: LovelaceDashboard[] = [];
|
||||
|
||||
@storage({
|
||||
key: "lovelace-dashboards-table-sort",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
public willUpdate() {
|
||||
if (!this.hasUpdated) {
|
||||
this.hass.loadFragmentTranslation("lovelace");
|
||||
@ -293,6 +302,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
this.hass.localize
|
||||
)}
|
||||
.data=${this._getItems(this._dashboards)}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@row-click=${this._editDashboard}
|
||||
id="url_path"
|
||||
hasFab
|
||||
@ -440,6 +451,10 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -10,9 +10,11 @@ import {
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoize from "memoize-one";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../../components/data-table/ha-data-table";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-fab";
|
||||
@ -33,10 +35,10 @@ import "../../../../layouts/hass-subpage";
|
||||
import "../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../../types";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import { loadLovelaceResources } from "../../../lovelace/common/load-resources";
|
||||
import { lovelaceResourcesTabs } from "../ha-config-lovelace";
|
||||
import { showResourceDetailDialog } from "./show-dialog-lovelace-resource-detail";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
|
||||
@customElement("ha-config-lovelace-resources")
|
||||
export class HaConfigLovelaceRescources extends LitElement {
|
||||
@ -50,6 +52,13 @@ export class HaConfigLovelaceRescources extends LitElement {
|
||||
|
||||
@state() private _resources: LovelaceResource[] = [];
|
||||
|
||||
@storage({
|
||||
key: "lovelace-resources-table-sort",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
private _columns = memoize(
|
||||
(
|
||||
_language,
|
||||
@ -127,6 +136,8 @@ export class HaConfigLovelaceRescources extends LitElement {
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.resources.picker.no_resources"
|
||||
)}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@row-click=${this._editResource}
|
||||
hasFab
|
||||
clickable
|
||||
@ -237,6 +248,10 @@ export class HaConfigLovelaceRescources extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@ -16,7 +16,7 @@ import {
|
||||
mdiPlus,
|
||||
mdiTag,
|
||||
} from "@mdi/js";
|
||||
import { differenceInDays } from "date-fns/esm";
|
||||
import { differenceInDays } from "date-fns";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
@ -32,14 +32,20 @@ import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/ha-button";
|
||||
@ -95,10 +101,6 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor
|
||||
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
|
||||
type SceneItem = SceneEntity & {
|
||||
name: string;
|
||||
@ -144,6 +146,19 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg!: EntityRegistryEntry[];
|
||||
|
||||
@storage({ key: "scene-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "scene-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({
|
||||
key: "scene-table-collapsed",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
});
|
||||
@ -463,7 +478,12 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
).length}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
id="entity_id"
|
||||
initialGroupColumn="category"
|
||||
.initialGroupColumn=${this._activeGrouping || "category"}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
.data=${scenes}
|
||||
.empty=${!this.scenes.length}
|
||||
.activeFilters=${this._activeFilters}
|
||||
@ -975,6 +995,18 @@ ${rejected
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
this._activeGrouping = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCollapseChanged(ev: CustomEvent) {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
mdiTag,
|
||||
mdiTransitConnection,
|
||||
} from "@mdi/js";
|
||||
import { differenceInDays } from "date-fns/esm";
|
||||
import { differenceInDays } from "date-fns";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
@ -33,14 +33,20 @@ import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/ha-fab";
|
||||
@ -97,10 +103,6 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor
|
||||
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
|
||||
type ScriptItem = ScriptEntity & {
|
||||
name: string;
|
||||
@ -148,6 +150,19 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg!: EntityRegistryEntry[];
|
||||
|
||||
@storage({ key: "script-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "script-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({
|
||||
key: "script-table-collapsed",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
});
|
||||
@ -462,7 +477,12 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
{ number: scripts.length }
|
||||
)}
|
||||
hasFilters
|
||||
initialGroupColumn="category"
|
||||
.initialGroupColumn=${this._activeGrouping || "category"}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
@ -1091,6 +1111,18 @@ ${rejected
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
this._activeGrouping = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCollapseChanged(ev: CustomEvent) {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@ -7,6 +7,7 @@ import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-icon";
|
||||
import "../../../components/ha-fab";
|
||||
@ -25,6 +26,7 @@ import { HomeAssistant, Route } from "../../../types";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showAddUserDialog } from "./show-dialog-add-user";
|
||||
import { showUserDetailDialog } from "./show-dialog-user-detail";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
|
||||
@customElement("ha-config-users")
|
||||
export class HaConfigUsers extends LitElement {
|
||||
@ -38,6 +40,19 @@ export class HaConfigUsers extends LitElement {
|
||||
|
||||
@state() private _users: User[] = [];
|
||||
|
||||
@storage({ key: "users-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "users-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({
|
||||
key: "users-table-collapsed",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer<User> = {
|
||||
@ -70,16 +85,14 @@ export class HaConfigUsers extends LitElement {
|
||||
hidden: narrow,
|
||||
template: (user) => html`${user.username || "—"}`,
|
||||
},
|
||||
group_ids: {
|
||||
group: {
|
||||
title: localize("ui.panel.config.users.picker.headers.group"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
width: "20%",
|
||||
direction: "asc",
|
||||
hidden: narrow,
|
||||
template: (user) => html`
|
||||
${localize(`groups.${user.group_ids[0]}`)}
|
||||
`,
|
||||
},
|
||||
is_active: {
|
||||
title: this.hass.localize(
|
||||
@ -164,7 +177,13 @@ export class HaConfigUsers extends LitElement {
|
||||
backPath="/config"
|
||||
.tabs=${configSections.persons}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.data=${this._users}
|
||||
.data=${this._userData(this._users, this.hass.localize)}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
@row-click=${this._editUser}
|
||||
hasFab
|
||||
clickable
|
||||
@ -181,6 +200,13 @@ export class HaConfigUsers extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _userData = memoizeOne((users: User[], localize: LocalizeFunc) =>
|
||||
users.map((user) => ({
|
||||
...user,
|
||||
group: localize(`groups.${user.group_ids[0]}`),
|
||||
}))
|
||||
);
|
||||
|
||||
private async _fetchUsers() {
|
||||
this._users = await fetchUsers(this.hass);
|
||||
|
||||
@ -245,6 +271,18 @@ export class HaConfigUsers extends LitElement {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
this._activeGrouping = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCollapseChanged(ev: CustomEvent) {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
DataTableRowData,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-fab";
|
||||
import { AlexaEntity, fetchCloudAlexaEntities } from "../../../data/alexa";
|
||||
@ -52,6 +53,7 @@ import "./expose/expose-assistant-icon";
|
||||
import { voiceAssistantTabs } from "./ha-config-voice-assistants";
|
||||
import { showExposeEntityDialog } from "./show-dialog-expose-entity";
|
||||
import { showVoiceSettingsDialog } from "./show-dialog-voice-settings";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
|
||||
@customElement("ha-config-voice-assistants-expose")
|
||||
export class VoiceAssistantsExpose extends LitElement {
|
||||
@ -87,6 +89,13 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
string[] | undefined
|
||||
>;
|
||||
|
||||
@storage({
|
||||
key: "voice-expose-table-sort",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@query("hass-tabs-subpage-data-table", true)
|
||||
private _dataTable!: HaTabsSubpageDataTable;
|
||||
|
||||
@ -505,6 +514,8 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
selectable
|
||||
.selected=${this._selectedEntities.length}
|
||||
clickable
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
@clear-filter=${this._clearFilter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@ -696,6 +707,10 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
navigate(window.location.pathname, { replace: true });
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
150
src/panels/config/zone/dialog-home-zone-detail.ts
Normal file
150
src/panels/config/zone/dialog-home-zone-detail.ts
Normal file
@ -0,0 +1,150 @@
|
||||
import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-form/ha-form";
|
||||
import { HomeZoneMutableParams } from "../../../data/zone";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { HomeZoneDetailDialogParams } from "./show-dialog-home-zone-detail";
|
||||
|
||||
const SCHEMA = [
|
||||
{
|
||||
name: "location",
|
||||
required: true,
|
||||
selector: { location: { radius: true, radius_readonly: true } },
|
||||
},
|
||||
];
|
||||
|
||||
class DialogHomeZoneDetail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _error?: Record<string, string>;
|
||||
|
||||
@state() private _data?: HomeZoneMutableParams;
|
||||
|
||||
@state() private _params?: HomeZoneDetailDialogParams;
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
public showDialog(params: HomeZoneDetailDialogParams): void {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
this._data = {
|
||||
latitude: this.hass.config.latitude,
|
||||
longitude: this.hass.config.longitude,
|
||||
};
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._params = undefined;
|
||||
this._data = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params || !this._data) {
|
||||
return nothing;
|
||||
}
|
||||
const latInvalid = String(this._data.latitude) === "";
|
||||
const lngInvalid = String(this._data.longitude) === "";
|
||||
|
||||
const valid = !latInvalid && !lngInvalid;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass!.localize("ui.panel.config.zone.edit_home")
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.schema=${SCHEMA}
|
||||
.data=${this._formData(this._data)}
|
||||
.error=${this._error}
|
||||
.computeLabel=${this._computeLabel}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
<p>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.zone.detail.no_edit_home_zone_radius"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${!valid || this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.zone.detail.update")}
|
||||
</mwc-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _formData = memoizeOne((data: HomeZoneMutableParams) => ({
|
||||
...data,
|
||||
location: {
|
||||
latitude: data.latitude,
|
||||
longitude: data.longitude,
|
||||
radius: this.hass.states["zone.home"]?.attributes?.radius || 100,
|
||||
},
|
||||
}));
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
this._error = undefined;
|
||||
const value = { ...ev.detail.value };
|
||||
value.latitude = value.location.latitude;
|
||||
value.longitude = value.location.longitude;
|
||||
delete value.location;
|
||||
this._data = value;
|
||||
}
|
||||
|
||||
private _computeLabel = (): string => "";
|
||||
|
||||
private async _updateEntry() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
await this._params!.updateEntry!(this._data!);
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = { base: err ? err.message : "Unknown error" };
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: min(600px, 95vw);
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: calc(
|
||||
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
|
||||
);
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-home-zone-detail": DialogHomeZoneDetail;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("dialog-home-zone-detail", DialogHomeZoneDetail);
|
@ -145,30 +145,8 @@ class DialogZoneDetail extends LitElement {
|
||||
required: true,
|
||||
selector: { location: { radius: true, icon } },
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{
|
||||
name: "latitude",
|
||||
required: true,
|
||||
selector: { number: {} },
|
||||
},
|
||||
{
|
||||
name: "longitude",
|
||||
required: true,
|
||||
|
||||
selector: { number: {} },
|
||||
},
|
||||
],
|
||||
},
|
||||
{ name: "passive_note", type: "constant" },
|
||||
{ name: "passive", selector: { boolean: {} } },
|
||||
{
|
||||
name: "radius",
|
||||
required: false,
|
||||
selector: { number: { min: 0, max: 999999, mode: "box" } },
|
||||
},
|
||||
] as const
|
||||
);
|
||||
|
||||
@ -184,15 +162,9 @@ class DialogZoneDetail extends LitElement {
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
this._error = undefined;
|
||||
const value = { ...ev.detail.value };
|
||||
if (
|
||||
value.location.latitude !== this._data!.latitude ||
|
||||
value.location.longitude !== this._data!.longitude ||
|
||||
value.location.radius !== this._data!.radius
|
||||
) {
|
||||
value.latitude = value.location.latitude;
|
||||
value.longitude = value.location.longitude;
|
||||
value.radius = Math.round(value.location.radius);
|
||||
}
|
||||
value.radius = value.location.radius;
|
||||
delete value.location;
|
||||
if (!value.icon) {
|
||||
delete value.icon;
|
||||
|
@ -1,25 +1,26 @@
|
||||
import { mdiCog, mdiPencil, mdiPencilOff, mdiPlus } from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import { mdiPencil, mdiPencilOff, mdiPlus } from "@mdi/js";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/map/ha-locations-editor";
|
||||
import type {
|
||||
@ -29,12 +30,13 @@ import type {
|
||||
import { saveCoreConfig } from "../../../data/core";
|
||||
import { subscribeEntityRegistry } from "../../../data/entity_registry";
|
||||
import {
|
||||
HomeZoneMutableParams,
|
||||
Zone,
|
||||
ZoneMutableParams,
|
||||
createZone,
|
||||
deleteZone,
|
||||
fetchZones,
|
||||
updateZone,
|
||||
Zone,
|
||||
ZoneMutableParams,
|
||||
} from "../../../data/zone";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@ -46,6 +48,7 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showHomeZoneDetailDialog } from "./show-dialog-home-zone-detail";
|
||||
import { showZoneDetailDialog } from "./show-dialog-zone-detail";
|
||||
|
||||
@customElement("ha-config-zone")
|
||||
@ -143,22 +146,25 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<paper-listbox
|
||||
attr-for-selected="data-id"
|
||||
.selected=${this._activeEntry || ""}
|
||||
>
|
||||
<mwc-list>
|
||||
${this._storageItems.map(
|
||||
(entry) => html`
|
||||
<paper-icon-item
|
||||
data-id=${entry.id}
|
||||
@click=${this._itemClicked}
|
||||
<ha-list-item
|
||||
.entry=${entry}
|
||||
.id=${this.narrow ? entry.id : ""}
|
||||
graphic="icon"
|
||||
.hasMeta=${!this.narrow}
|
||||
@request-selected=${this._itemClicked}
|
||||
.value=${entry.id}
|
||||
?selected=${this._activeEntry === entry.id}
|
||||
>
|
||||
<ha-icon .icon=${entry.icon} slot="item-icon"></ha-icon>
|
||||
<paper-item-body>${entry.name}</paper-item-body>
|
||||
<ha-icon .icon=${entry.icon} slot="graphic"></ha-icon>
|
||||
${entry.name}
|
||||
${!this.narrow
|
||||
? html`
|
||||
<div slot="meta">
|
||||
<ha-icon-button
|
||||
.id=${entry.id}
|
||||
.entry=${entry}
|
||||
@click=${this._openEditEntry}
|
||||
.path=${mdiPencil}
|
||||
@ -166,54 +172,69 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.zone.edit_zone"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</paper-icon-item>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
${this._stateItems.map(
|
||||
(stateObject) => html`
|
||||
<paper-icon-item
|
||||
data-id=${stateObject.entity_id}
|
||||
@click=${this._stateItemClicked}
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
.id=${this.narrow ? stateObject.entity_id : ""}
|
||||
.hasMeta=${!this.narrow ||
|
||||
stateObject.entity_id !== "zone.home"}
|
||||
.value=${stateObject.entity_id}
|
||||
@request-selected=${this._stateItemClicked}
|
||||
?selected=${this._activeEntry === stateObject.entity_id}
|
||||
.noEdit=${stateObject.entity_id !== "zone.home" ||
|
||||
!this._canEditCore}
|
||||
>
|
||||
<ha-icon
|
||||
.icon=${stateObject.attributes.icon}
|
||||
slot="item-icon"
|
||||
slot="graphic"
|
||||
>
|
||||
</ha-icon>
|
||||
<paper-item-body>
|
||||
|
||||
${stateObject.attributes.friendly_name ||
|
||||
stateObject.entity_id}
|
||||
</paper-item-body>
|
||||
<div style="display:inline-block">
|
||||
${this.narrow &&
|
||||
stateObject.entity_id === "zone.home" &&
|
||||
!this._canEditCore
|
||||
? nothing
|
||||
: html`<div slot="meta">
|
||||
<ha-icon-button
|
||||
.id=${!this.narrow ? stateObject.entity_id : ""}
|
||||
.entityId=${stateObject.entity_id}
|
||||
.noEdit=${stateObject.entity_id !== "zone.home" ||
|
||||
!this._canEditCore}
|
||||
.path=${stateObject.entity_id === "zone.home" &&
|
||||
this._canEditCore
|
||||
? mdiCog
|
||||
? mdiPencil
|
||||
: mdiPencilOff}
|
||||
.label=${stateObject.entity_id === "zone.home"
|
||||
? hass.localize("ui.panel.config.zone.edit_home")
|
||||
: hass.localize("ui.panel.config.zone.edit_zone")}
|
||||
@click=${this._openCoreConfig}
|
||||
@click=${this._editHomeZone}
|
||||
></ha-icon-button>
|
||||
${stateObject.entity_id !== "zone.home"
|
||||
? html`
|
||||
<simple-tooltip animation-delay="0" position="left">
|
||||
<simple-tooltip
|
||||
animation-delay="0"
|
||||
position="left"
|
||||
>
|
||||
${hass.localize(
|
||||
"ui.panel.config.zone.configured_in_yaml"
|
||||
)}
|
||||
</simple-tooltip>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</paper-icon-item>
|
||||
</div>`}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
</mwc-list>
|
||||
`;
|
||||
|
||||
return html`
|
||||
@ -286,7 +307,11 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
return;
|
||||
}
|
||||
const id = this.route.path.slice(6);
|
||||
this._editZone(id);
|
||||
navigate("/config/zone", { replace: true });
|
||||
if (this.narrow) {
|
||||
return;
|
||||
}
|
||||
this._zoomZone(id);
|
||||
}
|
||||
|
||||
@ -375,32 +400,52 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
this._openDialog();
|
||||
}
|
||||
|
||||
private _itemClicked(ev: Event) {
|
||||
private _itemClicked(ev: CustomEvent) {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.narrow) {
|
||||
this._openEditEntry(ev);
|
||||
return;
|
||||
}
|
||||
const entry: Zone = (ev.currentTarget! as any).entry;
|
||||
this._zoomZone(entry.id);
|
||||
const entryId: string = (ev.currentTarget! as any).value;
|
||||
this._zoomZone(entryId);
|
||||
this._activeEntry = entryId;
|
||||
}
|
||||
|
||||
private _stateItemClicked(ev: Event) {
|
||||
const entityId = (ev.currentTarget! as HTMLElement).getAttribute(
|
||||
"data-id"
|
||||
)!;
|
||||
this._zoomZone(entityId);
|
||||
private _stateItemClicked(ev: CustomEvent) {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entryId: string = (ev.currentTarget! as any).value;
|
||||
|
||||
if (this.narrow && entryId === "zone.home") {
|
||||
this._editHomeZone(ev);
|
||||
return;
|
||||
}
|
||||
|
||||
this._zoomZone(entryId);
|
||||
this._activeEntry = entryId;
|
||||
}
|
||||
|
||||
private async _zoomZone(id: string) {
|
||||
this._map?.fitMarker(id);
|
||||
}
|
||||
|
||||
private async _editZone(id: string) {
|
||||
await this.updateComplete;
|
||||
(this.shadowRoot?.querySelector(`[id="${id}"]`) as HTMLElement)?.click();
|
||||
}
|
||||
|
||||
private _openEditEntry(ev: Event) {
|
||||
const entry: Zone = (ev.currentTarget! as any).entry;
|
||||
this._openDialog(entry);
|
||||
ev.stopPropagation();
|
||||
}
|
||||
|
||||
private async _openCoreConfig(ev) {
|
||||
private async _editHomeZone(ev) {
|
||||
if (ev.currentTarget.noEdit) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.zone.can_not_edit"),
|
||||
@ -409,7 +454,9 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
navigate("/config/general");
|
||||
showHomeZoneDetailDialog(this, {
|
||||
updateEntry: (values) => this._updateHomeZoneEntry(values),
|
||||
});
|
||||
}
|
||||
|
||||
private async _createEntry(values: ZoneMutableParams) {
|
||||
@ -427,6 +474,14 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
this._map?.fitMarker(created.id);
|
||||
}
|
||||
|
||||
private async _updateHomeZoneEntry(values: HomeZoneMutableParams) {
|
||||
await saveCoreConfig(this.hass, {
|
||||
latitude: values.latitude,
|
||||
longitude: values.longitude,
|
||||
});
|
||||
this._zoomZone("zone.home");
|
||||
}
|
||||
|
||||
private async _updateEntry(
|
||||
entry: Zone,
|
||||
values: Partial<ZoneMutableParams>,
|
||||
@ -485,6 +540,9 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
--app-header-background-color: var(--sidebar-background-color);
|
||||
--app-header-text-color: var(--sidebar-text-color);
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-item-meta-size: 48px;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
@ -515,40 +573,16 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
.flex paper-listbox,
|
||||
.flex mwc-list,
|
||||
.flex .empty {
|
||||
border-left: 1px solid var(--divider-color);
|
||||
width: 250px;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
paper-icon-item {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.overflow paper-icon-item:last-child {
|
||||
margin-bottom: 80px;
|
||||
}
|
||||
paper-icon-item.iron-selected:before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
background-color: var(--sidebar-selected-icon-color);
|
||||
opacity: 0.12;
|
||||
transition: opacity 15ms linear;
|
||||
will-change: opacity;
|
||||
}
|
||||
ha-card {
|
||||
margin-bottom: 100px;
|
||||
}
|
||||
ha-card paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
20
src/panels/config/zone/show-dialog-home-zone-detail.ts
Normal file
20
src/panels/config/zone/show-dialog-home-zone-detail.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { HomeZoneMutableParams } from "../../../data/zone";
|
||||
|
||||
export interface HomeZoneDetailDialogParams {
|
||||
updateEntry?: (updates: HomeZoneMutableParams) => Promise<unknown>;
|
||||
}
|
||||
|
||||
export const loadHomeZoneDetailDialog = () =>
|
||||
import("./dialog-home-zone-detail");
|
||||
|
||||
export const showHomeZoneDetailDialog = (
|
||||
element: HTMLElement,
|
||||
params: HomeZoneDetailDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-home-zone-detail",
|
||||
dialogImport: loadHomeZoneDetailDialog,
|
||||
dialogParams: params,
|
||||
});
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user