mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-06 11:46:30 +00:00
Compare commits
1 Commits
20240610.1
...
fix-menu-o
Author | SHA1 | Date | |
---|---|---|---|
![]() |
29a103e884 |
@@ -115,7 +115,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"unused-imports/no-unused-imports": "error",
|
"unused-imports/no-unused-imports": "error",
|
||||||
"lit/attribute-names": "warn",
|
|
||||||
"lit/attribute-value-entities": "off",
|
"lit/attribute-value-entities": "off",
|
||||||
"lit/no-template-map": "off",
|
"lit/no-template-map": "off",
|
||||||
"lit/no-native-attributes": "warn",
|
"lit/no-native-attributes": "warn",
|
||||||
@@ -126,5 +125,6 @@
|
|||||||
"lit-a11y/anchor-is-valid": "warn",
|
"lit-a11y/anchor-is-valid": "warn",
|
||||||
"lit-a11y/role-has-required-aria-attrs": "warn"
|
"lit-a11y/role-has-required-aria-attrs": "warn"
|
||||||
},
|
},
|
||||||
"plugins": ["unused-imports"]
|
"plugins": ["disable", "unused-imports"],
|
||||||
|
"processor": "disable/disable"
|
||||||
}
|
}
|
||||||
|
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 }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.2
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.2
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
|
12
.github/workflows/ci.yaml
vendored
12
.github/workflows/ci.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.2
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.2
|
||||||
with:
|
with:
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.2
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.2
|
||||||
with:
|
with:
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.2
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.2
|
||||||
with:
|
with:
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
IS_TEST: "true"
|
IS_TEST: "true"
|
||||||
- name: Upload bundle stats
|
- name: Upload bundle stats
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.1
|
||||||
with:
|
with:
|
||||||
name: frontend-bundle-stats
|
name: frontend-bundle-stats
|
||||||
path: build/stats/*.json
|
path: build/stats/*.json
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.2
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.2
|
||||||
with:
|
with:
|
||||||
@@ -113,7 +113,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
IS_TEST: "true"
|
IS_TEST: "true"
|
||||||
- name: Upload bundle stats
|
- name: Upload bundle stats
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.1
|
||||||
with:
|
with:
|
||||||
name: supervisor-bundle-stats
|
name: supervisor-bundle-stats
|
||||||
path: build/stats/*.json
|
path: build/stats/*.json
|
||||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.2
|
||||||
with:
|
with:
|
||||||
# We must fetch at least the immediate parents so that if this is
|
# We must fetch at least the immediate parents so that if this is
|
||||||
# a pull request then we can checkout the head.
|
# a pull request then we can checkout the head.
|
||||||
|
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 }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.2
|
||||||
with:
|
with:
|
||||||
ref: dev
|
ref: dev
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.2
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|
||||||
|
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 }}
|
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.2
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.2
|
||||||
|
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')
|
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||||
steps:
|
steps:
|
||||||
- name: Check out files from GitHub
|
- name: Check out files from GitHub
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.2
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4.0.2
|
uses: actions/setup-node@v4.0.2
|
||||||
|
6
.github/workflows/nightly.yaml
vendored
6
.github/workflows/nightly.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.2
|
||||||
|
|
||||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
@@ -57,14 +57,14 @@ jobs:
|
|||||||
run: tar -czvf translations.tar.gz translations
|
run: tar -czvf translations.tar.gz translations
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.1
|
||||||
with:
|
with:
|
||||||
name: wheels
|
name: wheels
|
||||||
path: dist/home_assistant_frontend*.whl
|
path: dist/home_assistant_frontend*.whl
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload translations
|
- name: Upload translations
|
||||||
uses: actions/upload-artifact@v4.3.3
|
uses: actions/upload-artifact@v4.3.1
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
path: translations.tar.gz
|
path: translations.tar.gz
|
||||||
|
2
.github/workflows/relative-ci.yaml
vendored
2
.github/workflows/relative-ci.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Send bundle stats and build information to RelativeCI
|
- name: Send bundle stats and build information to RelativeCI
|
||||||
uses: relative-ci/agent-action@v2.1.11
|
uses: relative-ci/agent-action@v2.1.10
|
||||||
with:
|
with:
|
||||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
|
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
contents: write # Required to upload release assets
|
contents: write # Required to upload release assets
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.2
|
||||||
|
|
||||||
- name: Verify version
|
- name: Verify version
|
||||||
uses: home-assistant/actions/helpers/verify-version@master
|
uses: home-assistant/actions/helpers/verify-version@master
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
script/release
|
script/release
|
||||||
|
|
||||||
- name: Upload release assets
|
- name: Upload release assets
|
||||||
uses: softprops/action-gh-release@v2.0.5
|
uses: softprops/action-gh-release@v2.0.4
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
dist/*.whl
|
dist/*.whl
|
||||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.1.6
|
uses: actions/checkout@v4.1.2
|
||||||
|
|
||||||
- name: Upload Translations
|
- name: Upload Translations
|
||||||
run: |
|
run: |
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -6,4 +6,4 @@ enableGlobalCache: false
|
|||||||
|
|
||||||
nodeLinker: node-modules
|
nodeLinker: node-modules
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-4.2.2.cjs
|
yarnPath: .yarn/releases/yarn-4.1.1.cjs
|
||||||
|
@@ -1,56 +1,7 @@
|
|||||||
import defineProvider from "@babel/helper-define-polyfill-provider";
|
import defineProvider from "@babel/helper-define-polyfill-provider";
|
||||||
import { join } from "node:path";
|
|
||||||
import paths from "../paths.cjs";
|
|
||||||
|
|
||||||
const POLYFILL_DIR = join(paths.polymer_dir, "src/resources/polyfills");
|
|
||||||
|
|
||||||
// List of polyfill keys with supported browser targets for the functionality
|
// List of polyfill keys with supported browser targets for the functionality
|
||||||
const PolyfillSupport = {
|
const PolyfillSupport = {
|
||||||
// Note states and shadowRoot properties should be supported.
|
|
||||||
"element-internals": {
|
|
||||||
android: 90,
|
|
||||||
chrome: 90,
|
|
||||||
edge: 90,
|
|
||||||
firefox: 126,
|
|
||||||
ios: 17.4,
|
|
||||||
opera: 76,
|
|
||||||
opera_mobile: 64,
|
|
||||||
safari: 17.4,
|
|
||||||
samsung: 15.0,
|
|
||||||
},
|
|
||||||
"element-append": {
|
|
||||||
android: 54,
|
|
||||||
chrome: 54,
|
|
||||||
edge: 17,
|
|
||||||
firefox: 49,
|
|
||||||
ios: 10.0,
|
|
||||||
opera: 41,
|
|
||||||
opera_mobile: 41,
|
|
||||||
safari: 10.0,
|
|
||||||
samsung: 6.0,
|
|
||||||
},
|
|
||||||
"element-getattributenames": {
|
|
||||||
android: 61,
|
|
||||||
chrome: 61,
|
|
||||||
edge: 18,
|
|
||||||
firefox: 45,
|
|
||||||
ios: 10.3,
|
|
||||||
opera: 48,
|
|
||||||
opera_mobile: 45,
|
|
||||||
safari: 10.1,
|
|
||||||
samsung: 8.0,
|
|
||||||
},
|
|
||||||
"element-toggleattribute": {
|
|
||||||
android: 69,
|
|
||||||
chrome: 69,
|
|
||||||
edge: 18,
|
|
||||||
firefox: 63,
|
|
||||||
ios: 12.0,
|
|
||||||
opera: 56,
|
|
||||||
opera_mobile: 48,
|
|
||||||
safari: 12.0,
|
|
||||||
samsung: 10.0,
|
|
||||||
},
|
|
||||||
fetch: {
|
fetch: {
|
||||||
android: 42,
|
android: 42,
|
||||||
chrome: 42,
|
chrome: 42,
|
||||||
@@ -62,31 +13,6 @@ const PolyfillSupport = {
|
|||||||
safari: 10.1,
|
safari: 10.1,
|
||||||
samsung: 4.0,
|
samsung: 4.0,
|
||||||
},
|
},
|
||||||
"intl-getcanonicallocales": {
|
|
||||||
android: 54,
|
|
||||||
chrome: 54,
|
|
||||||
edge: 16,
|
|
||||||
firefox: 48,
|
|
||||||
ios: 10.3,
|
|
||||||
opera: 41,
|
|
||||||
opera_mobile: 41,
|
|
||||||
safari: 10.1,
|
|
||||||
samsung: 6.0,
|
|
||||||
},
|
|
||||||
"intl-locale": {
|
|
||||||
android: 74,
|
|
||||||
chrome: 74,
|
|
||||||
edge: 79,
|
|
||||||
firefox: 75,
|
|
||||||
ios: 14.0,
|
|
||||||
opera: 62,
|
|
||||||
opera_mobile: 53,
|
|
||||||
safari: 14.0,
|
|
||||||
samsung: 11.0,
|
|
||||||
},
|
|
||||||
"intl-other": {
|
|
||||||
// Not specified (i.e. always try polyfill) since compatibility depends on supported locales
|
|
||||||
},
|
|
||||||
proxy: {
|
proxy: {
|
||||||
android: 49,
|
android: 49,
|
||||||
chrome: 49,
|
chrome: 49,
|
||||||
@@ -98,67 +24,17 @@ const PolyfillSupport = {
|
|||||||
safari: 10.0,
|
safari: 10.0,
|
||||||
samsung: 5.0,
|
samsung: 5.0,
|
||||||
},
|
},
|
||||||
"resize-observer": {
|
|
||||||
android: 64,
|
|
||||||
chrome: 64,
|
|
||||||
edge: 79,
|
|
||||||
firefox: 69,
|
|
||||||
ios: 13.4,
|
|
||||||
opera: 51,
|
|
||||||
opera_mobile: 47,
|
|
||||||
safari: 13.1,
|
|
||||||
samsung: 9.0,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Map of global variables and/or instance and static properties to the
|
// Map of global variables and/or instance and static properties to the
|
||||||
// corresponding polyfill key and actual module to import
|
// corresponding polyfill key and actual module to import
|
||||||
const polyfillMap = {
|
const polyfillMap = {
|
||||||
global: {
|
global: {
|
||||||
fetch: { key: "fetch", module: "unfetch/polyfill" },
|
|
||||||
Proxy: { key: "proxy", module: "proxy-polyfill" },
|
Proxy: { key: "proxy", module: "proxy-polyfill" },
|
||||||
ResizeObserver: {
|
fetch: { key: "fetch", module: "unfetch/polyfill" },
|
||||||
key: "resize-observer",
|
|
||||||
module: join(POLYFILL_DIR, "resize-observer.ts"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
instance: {
|
|
||||||
attachInternals: {
|
|
||||||
key: "element-internals",
|
|
||||||
module: "element-internals-polyfill",
|
|
||||||
},
|
|
||||||
...Object.fromEntries(
|
|
||||||
["append", "getAttributeNames", "toggleAttribute"].map((prop) => {
|
|
||||||
const key = `element-${prop.toLowerCase()}`;
|
|
||||||
return [prop, { key, module: join(POLYFILL_DIR, `${key}.ts`) }];
|
|
||||||
})
|
|
||||||
),
|
|
||||||
},
|
|
||||||
static: {
|
|
||||||
Intl: {
|
|
||||||
getCanonicalLocales: {
|
|
||||||
key: "intl-getcanonicallocales",
|
|
||||||
module: join(POLYFILL_DIR, "intl-polyfill.ts"),
|
|
||||||
},
|
|
||||||
Locale: {
|
|
||||||
key: "intl-locale",
|
|
||||||
module: join(POLYFILL_DIR, "intl-polyfill.ts"),
|
|
||||||
},
|
|
||||||
...Object.fromEntries(
|
|
||||||
[
|
|
||||||
"DateTimeFormat",
|
|
||||||
"DisplayNames",
|
|
||||||
"ListFormat",
|
|
||||||
"NumberFormat",
|
|
||||||
"PluralRules",
|
|
||||||
"RelativeTimeFormat",
|
|
||||||
].map((obj) => [
|
|
||||||
obj,
|
|
||||||
{ key: "intl-other", module: join(POLYFILL_DIR, "intl-polyfill.ts") },
|
|
||||||
])
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
instance: {},
|
||||||
|
static: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create plugin using the same factory as for CoreJS
|
// Create plugin using the same factory as for CoreJS
|
||||||
@@ -166,16 +42,14 @@ export default defineProvider(
|
|||||||
({ createMetaResolver, debug, shouldInjectPolyfill }) => {
|
({ createMetaResolver, debug, shouldInjectPolyfill }) => {
|
||||||
const resolvePolyfill = createMetaResolver(polyfillMap);
|
const resolvePolyfill = createMetaResolver(polyfillMap);
|
||||||
return {
|
return {
|
||||||
name: "custom-polyfill",
|
name: "HA Custom",
|
||||||
polyfills: PolyfillSupport,
|
polyfills: PolyfillSupport,
|
||||||
usageGlobal(meta, utils) {
|
usageGlobal(meta, utils) {
|
||||||
const polyfill = resolvePolyfill(meta);
|
const polyfill = resolvePolyfill(meta);
|
||||||
if (polyfill && shouldInjectPolyfill(polyfill.desc.key)) {
|
if (polyfill && shouldInjectPolyfill(polyfill.desc.key)) {
|
||||||
debug(polyfill.desc.key);
|
debug(polyfill.desc.key);
|
||||||
utils.injectGlobalImport(polyfill.desc.module);
|
utils.injectGlobalImport(polyfill.desc.module);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -3,8 +3,6 @@ const env = require("./env.cjs");
|
|||||||
const paths = require("./paths.cjs");
|
const paths = require("./paths.cjs");
|
||||||
const { dependencies } = require("../package.json");
|
const { dependencies } = require("../package.json");
|
||||||
|
|
||||||
const BABEL_PLUGINS = path.join(__dirname, "babel-plugins");
|
|
||||||
|
|
||||||
// GitHub base URL to use for production source maps
|
// GitHub base URL to use for production source maps
|
||||||
// Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version
|
// Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version
|
||||||
module.exports.sourceMapURL = () => {
|
module.exports.sourceMapURL = () => {
|
||||||
@@ -102,12 +100,22 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
|||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
[
|
[
|
||||||
path.join(BABEL_PLUGINS, "inline-constants-plugin.cjs"),
|
path.resolve(
|
||||||
|
paths.polymer_dir,
|
||||||
|
"build-scripts/babel-plugins/inline-constants-plugin.cjs"
|
||||||
|
),
|
||||||
{
|
{
|
||||||
modules: ["@mdi/js"],
|
modules: ["@mdi/js"],
|
||||||
ignoreModuleNotFound: true,
|
ignoreModuleNotFound: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
path.resolve(
|
||||||
|
paths.polymer_dir,
|
||||||
|
"build-scripts/babel-plugins/custom-polyfill-plugin.js"
|
||||||
|
),
|
||||||
|
{ method: "usage-global" },
|
||||||
|
],
|
||||||
// Minify template literals for production
|
// Minify template literals for production
|
||||||
isProdBuild && [
|
isProdBuild && [
|
||||||
"template-html-minifier",
|
"template-html-minifier",
|
||||||
@@ -145,27 +153,6 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
|||||||
],
|
],
|
||||||
sourceMaps: !isTestBuild,
|
sourceMaps: !isTestBuild,
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
|
||||||
// Add plugin to inject various polyfills, excluding the polyfills
|
|
||||||
// themselves to prevent self-injection.
|
|
||||||
plugins: [
|
|
||||||
[
|
|
||||||
path.join(BABEL_PLUGINS, "custom-polyfill-plugin.js"),
|
|
||||||
{ method: "usage-global" },
|
|
||||||
],
|
|
||||||
],
|
|
||||||
exclude: [
|
|
||||||
path.join(paths.polymer_dir, "src/resources/polyfills"),
|
|
||||||
...[
|
|
||||||
"@formatjs/(?:ecma402-abstract|intl-\\w+)",
|
|
||||||
"@lit-labs/virtualizer/polyfills",
|
|
||||||
"@webcomponents/scoped-custom-element-registry",
|
|
||||||
"element-internals-polyfill",
|
|
||||||
"proxy-polyfill",
|
|
||||||
"unfetch",
|
|
||||||
].map((p) => new RegExp(`/node_modules/${p}/`)),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
// Use unambiguous for dependencies so that require() is correctly injected into CommonJS files
|
// Use unambiguous for dependencies so that require() is correctly injected into CommonJS files
|
||||||
// Exclusions are needed in some cases where ES modules have no static imports or exports, such as polyfills
|
// Exclusions are needed in some cases where ES modules have no static imports or exports, such as polyfills
|
||||||
|
@@ -9,7 +9,7 @@ import gulp from "gulp";
|
|||||||
import jszip from "jszip";
|
import jszip from "jszip";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import process from "process";
|
import process from "process";
|
||||||
import { extract } from "tar";
|
import tar from "tar";
|
||||||
|
|
||||||
const MAX_AGE = 24; // hours
|
const MAX_AGE = 24; // hours
|
||||||
const OWNER = "home-assistant";
|
const OWNER = "home-assistant";
|
||||||
@@ -156,7 +156,7 @@ gulp.task("fetch-nightly-translations", async function () {
|
|||||||
console.log("Unpacking downloaded translations...");
|
console.log("Unpacking downloaded translations...");
|
||||||
const zip = await jszip.loadAsync(downloadResponse.data);
|
const zip = await jszip.loadAsync(downloadResponse.data);
|
||||||
await deleteCurrent;
|
await deleteCurrent;
|
||||||
const extractStream = zip.file(/.*/)[0].nodeStream().pipe(extract());
|
const extractStream = zip.file(/.*/)[0].nodeStream().pipe(tar.extract());
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
extractStream.on("close", resolve).on("error", reject);
|
extractStream.on("close", resolve).on("error", reject);
|
||||||
});
|
});
|
||||||
|
@@ -1,112 +1,92 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
import { createHash } from "crypto";
|
||||||
|
import { deleteSync } from "del";
|
||||||
import { deleteAsync } from "del";
|
import { mkdirSync, readdirSync, readFileSync, renameSync } from "fs";
|
||||||
import { glob } from "glob";
|
import { writeFile } from "node:fs/promises";
|
||||||
import gulp from "gulp";
|
import gulp from "gulp";
|
||||||
|
import flatmap from "gulp-flatmap";
|
||||||
|
import transform from "gulp-json-transform";
|
||||||
|
import merge from "gulp-merge-json";
|
||||||
import rename from "gulp-rename";
|
import rename from "gulp-rename";
|
||||||
import merge from "lodash.merge";
|
import path from "path";
|
||||||
import { createHash } from "node:crypto";
|
import vinylBuffer from "vinyl-buffer";
|
||||||
import { mkdir, readFile } from "node:fs/promises";
|
import source from "vinyl-source-stream";
|
||||||
import { basename, join } from "node:path";
|
|
||||||
import { PassThrough, Transform } from "node:stream";
|
|
||||||
import { finished } from "node:stream/promises";
|
|
||||||
import env from "../env.cjs";
|
import env from "../env.cjs";
|
||||||
import paths from "../paths.cjs";
|
import paths from "../paths.cjs";
|
||||||
|
import { mapFiles } from "../util.cjs";
|
||||||
import "./fetch-nightly-translations.js";
|
import "./fetch-nightly-translations.js";
|
||||||
|
|
||||||
const inFrontendDir = "translations/frontend";
|
const inFrontendDir = "translations/frontend";
|
||||||
const inBackendDir = "translations/backend";
|
const inBackendDir = "translations/backend";
|
||||||
const workDir = "build/translations";
|
const workDir = "build/translations";
|
||||||
const outDir = join(workDir, "output");
|
const fullDir = workDir + "/full";
|
||||||
const EN_SRC = join(paths.translations_src, "en.json");
|
const coreDir = workDir + "/core";
|
||||||
const TEST_LOCALE = "en-x-test";
|
const outDir = workDir + "/output";
|
||||||
|
|
||||||
let mergeBackend = false;
|
let mergeBackend = false;
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"translations-enable-merge-backend",
|
"translations-enable-merge-backend",
|
||||||
gulp.parallel(async () => {
|
gulp.parallel((done) => {
|
||||||
mergeBackend = true;
|
mergeBackend = true;
|
||||||
|
done();
|
||||||
}, "allow-setup-fetch-nightly-translations")
|
}, "allow-setup-fetch-nightly-translations")
|
||||||
);
|
);
|
||||||
|
|
||||||
// Transform stream to apply a function on Vinyl JSON files (buffer mode only).
|
// Panel translations which should be split from the core translations.
|
||||||
// The provided function can either return a new object, or an array of
|
const TRANSLATION_FRAGMENTS = Object.keys(
|
||||||
// [object, subdirectory] pairs for fragmentizing the JSON.
|
JSON.parse(
|
||||||
class CustomJSON extends Transform {
|
readFileSync(
|
||||||
constructor(func, reviver = null) {
|
path.resolve(paths.polymer_dir, "src/translations/en.json"),
|
||||||
super({ objectMode: true });
|
"utf-8"
|
||||||
this._func = func;
|
)
|
||||||
this._reviver = reviver;
|
).ui.panel
|
||||||
}
|
);
|
||||||
|
|
||||||
async _transform(file, _, callback) {
|
function recursiveFlatten(prefix, data) {
|
||||||
try {
|
let output = {};
|
||||||
let obj = JSON.parse(file.contents.toString(), this._reviver);
|
Object.keys(data).forEach((key) => {
|
||||||
if (this._func) obj = this._func(obj, file.path);
|
if (typeof data[key] === "object") {
|
||||||
for (const [outObj, dir] of Array.isArray(obj) ? obj : [[obj, ""]]) {
|
output = {
|
||||||
const outFile = file.clone({ contents: false });
|
...output,
|
||||||
outFile.contents = Buffer.from(JSON.stringify(outObj));
|
...recursiveFlatten(prefix + key + ".", data[key]),
|
||||||
outFile.dirname += `/${dir}`;
|
};
|
||||||
this.push(outFile);
|
|
||||||
}
|
|
||||||
callback(null);
|
|
||||||
} catch (err) {
|
|
||||||
callback(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform stream to merge Vinyl JSON files (buffer mode only).
|
|
||||||
class MergeJSON extends Transform {
|
|
||||||
_objects = [];
|
|
||||||
|
|
||||||
constructor(stem, startObj = {}, reviver = null) {
|
|
||||||
super({ objectMode: true, allowHalfOpen: false });
|
|
||||||
this._stem = stem;
|
|
||||||
this._startObj = structuredClone(startObj);
|
|
||||||
this._reviver = reviver;
|
|
||||||
}
|
|
||||||
|
|
||||||
async _transform(file, _, callback) {
|
|
||||||
try {
|
|
||||||
this._objects.push(JSON.parse(file.contents.toString(), this._reviver));
|
|
||||||
if (!this._outFile) this._outFile = file.clone({ contents: false });
|
|
||||||
callback(null);
|
|
||||||
} catch (err) {
|
|
||||||
callback(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async _flush(callback) {
|
|
||||||
try {
|
|
||||||
const mergedObj = merge(this._startObj, ...this._objects);
|
|
||||||
this._outFile.contents = Buffer.from(JSON.stringify(mergedObj));
|
|
||||||
this._outFile.stem = this._stem;
|
|
||||||
callback(null, this._outFile);
|
|
||||||
} catch (err) {
|
|
||||||
callback(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Utility to flatten object keys to single level using separator
|
|
||||||
const flatten = (data, prefix = "", sep = ".") => {
|
|
||||||
const output = {};
|
|
||||||
for (const [key, value] of Object.entries(data)) {
|
|
||||||
if (typeof value === "object") {
|
|
||||||
Object.assign(output, flatten(value, prefix + key + sep, sep));
|
|
||||||
} else {
|
} else {
|
||||||
output[prefix + key] = value;
|
output[prefix + key] = data[key];
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
return output;
|
return output;
|
||||||
};
|
}
|
||||||
|
|
||||||
// Filter functions that can be passed directly to JSON.parse()
|
function flatten(data) {
|
||||||
const emptyReviver = (_key, value) => value || undefined;
|
return recursiveFlatten("", data);
|
||||||
const testReviver = (_key, value) =>
|
}
|
||||||
value && typeof value === "string" ? "TRANSLATED" : value;
|
|
||||||
|
function emptyFilter(data) {
|
||||||
|
const newData = {};
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
if (data[key]) {
|
||||||
|
if (typeof data[key] === "object") {
|
||||||
|
newData[key] = emptyFilter(data[key]);
|
||||||
|
} else {
|
||||||
|
newData[key] = data[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function recursiveEmpty(data) {
|
||||||
|
const newData = {};
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
if (data[key]) {
|
||||||
|
if (typeof data[key] === "object") {
|
||||||
|
newData[key] = recursiveEmpty(data[key]);
|
||||||
|
} else {
|
||||||
|
newData[key] = "TRANSLATED";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return newData;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace Lokalise key placeholders with their actual values.
|
* Replace Lokalise key placeholders with their actual values.
|
||||||
@@ -115,44 +95,60 @@ const testReviver = (_key, value) =>
|
|||||||
* be included in src/translations/en.json, but still be usable while
|
* be included in src/translations/en.json, but still be usable while
|
||||||
* developing locally.
|
* developing locally.
|
||||||
*
|
*
|
||||||
* @link https://docs.lokalise.com/en/articles/1400528-key-referencing
|
* @link https://docs.lokalise.co/article/KO5SZWLLsy-key-referencing
|
||||||
*/
|
*/
|
||||||
const KEY_REFERENCE = /\[%key:([^%]+)%\]/;
|
const re_key_reference = /\[%key:([^%]+)%\]/;
|
||||||
const lokaliseTransform = (data, path, original = data) => {
|
function lokaliseTransform(data, original, file) {
|
||||||
const output = {};
|
const output = {};
|
||||||
for (const [key, value] of Object.entries(data)) {
|
Object.entries(data).forEach(([key, value]) => {
|
||||||
if (typeof value === "object") {
|
if (value instanceof Object) {
|
||||||
output[key] = lokaliseTransform(value, path, original);
|
output[key] = lokaliseTransform(value, original, file);
|
||||||
} else {
|
} else {
|
||||||
output[key] = value.replace(KEY_REFERENCE, (_match, lokalise_key) => {
|
output[key] = value.replace(re_key_reference, (_match, lokalise_key) => {
|
||||||
const replace = lokalise_key.split("::").reduce((tr, k) => {
|
const replace = lokalise_key.split("::").reduce((tr, k) => {
|
||||||
if (!tr) {
|
if (!tr) {
|
||||||
throw Error(`Invalid key placeholder ${lokalise_key} in ${path}`);
|
throw Error(
|
||||||
|
`Invalid key placeholder ${lokalise_key} in ${file.path}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return tr[k];
|
return tr[k];
|
||||||
}, original);
|
}, original);
|
||||||
if (typeof replace !== "string") {
|
if (typeof replace !== "string") {
|
||||||
throw Error(`Invalid key placeholder ${lokalise_key} in ${path}`);
|
throw Error(
|
||||||
|
`Invalid key placeholder ${lokalise_key} in ${file.path}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return replace;
|
return replace;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
return output;
|
return output;
|
||||||
};
|
}
|
||||||
|
|
||||||
gulp.task("clean-translations", () => deleteAsync([workDir]));
|
gulp.task("clean-translations", async () => deleteSync([workDir]));
|
||||||
|
|
||||||
const makeWorkDir = () => mkdir(workDir, { recursive: true });
|
gulp.task("ensure-translations-build-dir", async () => {
|
||||||
|
mkdirSync(workDir, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
const createTestTranslation = () =>
|
gulp.task("create-test-metadata", () =>
|
||||||
|
env.isProdBuild()
|
||||||
|
? Promise.resolve()
|
||||||
|
: writeFile(
|
||||||
|
workDir + "/testMetadata.json",
|
||||||
|
JSON.stringify({ test: { nativeName: "Test" } })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task("create-test-translation", () =>
|
||||||
env.isProdBuild()
|
env.isProdBuild()
|
||||||
? Promise.resolve()
|
? Promise.resolve()
|
||||||
: gulp
|
: gulp
|
||||||
.src(EN_SRC)
|
.src(path.join(paths.translations_src, "en.json"))
|
||||||
.pipe(new CustomJSON(null, testReviver))
|
.pipe(transform((data, _file) => recursiveEmpty(data)))
|
||||||
.pipe(rename(`${TEST_LOCALE}.json`))
|
.pipe(rename("test.json"))
|
||||||
.pipe(gulp.dest(workDir));
|
.pipe(gulp.dest(workDir))
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This task will build a master translation file, to be used as the base for
|
* This task will build a master translation file, to be used as the base for
|
||||||
@@ -163,164 +159,279 @@ const createTestTranslation = () =>
|
|||||||
* project is buildable immediately after merging new translation keys, since
|
* project is buildable immediately after merging new translation keys, since
|
||||||
* the Lokalise update to translations/en.json will not happen immediately.
|
* the Lokalise update to translations/en.json will not happen immediately.
|
||||||
*/
|
*/
|
||||||
const createMasterTranslation = () =>
|
gulp.task("build-master-translation", () => {
|
||||||
gulp
|
const src = [path.join(paths.translations_src, "en.json")];
|
||||||
.src([EN_SRC, ...(mergeBackend ? [`${inBackendDir}/en.json`] : [])])
|
|
||||||
.pipe(new CustomJSON(lokaliseTransform))
|
|
||||||
.pipe(new MergeJSON("en"))
|
|
||||||
.pipe(gulp.dest(workDir));
|
|
||||||
|
|
||||||
const FRAGMENTS = ["base"];
|
if (mergeBackend) {
|
||||||
|
src.push(path.join(inBackendDir, "en.json"));
|
||||||
const toggleSupervisorFragment = async () => {
|
|
||||||
FRAGMENTS[0] = "supervisor";
|
|
||||||
};
|
|
||||||
|
|
||||||
const panelFragment = (fragment) =>
|
|
||||||
fragment !== "base" && fragment !== "supervisor";
|
|
||||||
|
|
||||||
const HASHES = new Map();
|
|
||||||
|
|
||||||
const createTranslations = async () => {
|
|
||||||
// Parse and store the master to avoid repeating this for each locale, then
|
|
||||||
// add the panel fragments when processing the app.
|
|
||||||
const enMaster = JSON.parse(await readFile(`${workDir}/en.json`, "utf-8"));
|
|
||||||
if (FRAGMENTS[0] === "base") {
|
|
||||||
FRAGMENTS.push(...Object.keys(enMaster.ui.panel));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The downstream pipeline is setup first. It hashes the merged data for
|
return gulp
|
||||||
// each locale, then fragmentizes and flattens the data for final output.
|
.src(src)
|
||||||
const translationFiles = await glob([
|
.pipe(transform((data, file) => lokaliseTransform(data, data, file)))
|
||||||
`${inFrontendDir}/!(en).json`,
|
|
||||||
...(env.isProdBuild() ? [] : [`${workDir}/${TEST_LOCALE}.json`]),
|
|
||||||
]);
|
|
||||||
const hashStream = new Transform({
|
|
||||||
objectMode: true,
|
|
||||||
transform: async (file, _, callback) => {
|
|
||||||
const hash = env.isProdBuild()
|
|
||||||
? createHash("md5").update(file.contents).digest("hex")
|
|
||||||
: "dev";
|
|
||||||
HASHES.set(file.stem, hash);
|
|
||||||
file.stem += `-${hash}`;
|
|
||||||
callback(null, file);
|
|
||||||
},
|
|
||||||
}).setMaxListeners(translationFiles.length + 1);
|
|
||||||
const fragmentsStream = hashStream
|
|
||||||
.pipe(
|
.pipe(
|
||||||
new CustomJSON((data) =>
|
merge({
|
||||||
FRAGMENTS.map((fragment) => {
|
fileName: "en.json",
|
||||||
switch (fragment) {
|
|
||||||
case "base":
|
|
||||||
// Remove the panels and supervisor to create the base translations
|
|
||||||
return [
|
|
||||||
flatten({
|
|
||||||
...data,
|
|
||||||
ui: { ...data.ui, panel: undefined },
|
|
||||||
supervisor: undefined,
|
|
||||||
}),
|
|
||||||
"",
|
|
||||||
];
|
|
||||||
case "supervisor":
|
|
||||||
// Supervisor key is at the top level
|
|
||||||
return [flatten(data.supervisor), ""];
|
|
||||||
default:
|
|
||||||
// Create a fragment with only the given panel
|
|
||||||
return [
|
|
||||||
flatten(data.ui.panel[fragment], `ui.panel.${fragment}.`),
|
|
||||||
fragment,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.pipe(gulp.dest(outDir));
|
|
||||||
|
|
||||||
// Send the English master downstream first, then for each other locale
|
|
||||||
// generate merged JSON data to continue piping. It begins with the master
|
|
||||||
// translation as a failsafe for untranslated strings, and merges all parent
|
|
||||||
// tags into one file for each specific subtag
|
|
||||||
//
|
|
||||||
// TODO: This is a naive interpretation of BCP47 that should be improved.
|
|
||||||
// Will be OK for now as long as we don't have anything more complicated
|
|
||||||
// than a base translation + region.
|
|
||||||
gulp
|
|
||||||
.src(`${workDir}/en.json`)
|
|
||||||
.pipe(new PassThrough({ objectMode: true }))
|
|
||||||
.pipe(hashStream, { end: false });
|
|
||||||
const mergesFinished = [];
|
|
||||||
for (const translationFile of translationFiles) {
|
|
||||||
const locale = basename(translationFile, ".json");
|
|
||||||
const subtags = locale.split("-");
|
|
||||||
const mergeFiles = [];
|
|
||||||
for (let i = 1; i <= subtags.length; i++) {
|
|
||||||
const lang = subtags.slice(0, i).join("-");
|
|
||||||
if (lang === TEST_LOCALE) {
|
|
||||||
mergeFiles.push(`${workDir}/${TEST_LOCALE}.json`);
|
|
||||||
} else if (lang !== "en") {
|
|
||||||
mergeFiles.push(`${inFrontendDir}/${lang}.json`);
|
|
||||||
if (mergeBackend) {
|
|
||||||
mergeFiles.push(`${inBackendDir}/${lang}.json`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const mergeStream = gulp
|
|
||||||
.src(mergeFiles, { allowEmpty: true })
|
|
||||||
.pipe(new MergeJSON(locale, enMaster, emptyReviver));
|
|
||||||
mergesFinished.push(finished(mergeStream));
|
|
||||||
mergeStream.pipe(hashStream, { end: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all merges to finish, then it's safe to end writing to the
|
|
||||||
// downstream pipeline and wait for all fragments to finish writing.
|
|
||||||
await Promise.all(mergesFinished);
|
|
||||||
hashStream.end();
|
|
||||||
await finished(fragmentsStream);
|
|
||||||
};
|
|
||||||
|
|
||||||
const writeTranslationMetaData = () =>
|
|
||||||
gulp
|
|
||||||
.src([`${paths.translations_src}/translationMetadata.json`])
|
|
||||||
.pipe(
|
|
||||||
new CustomJSON((meta) => {
|
|
||||||
// Add the test translation in development.
|
|
||||||
if (!env.isProdBuild()) {
|
|
||||||
meta[TEST_LOCALE] = { nativeName: "Translation Test" };
|
|
||||||
}
|
|
||||||
// Filter out locales without a native name, and add the hashes.
|
|
||||||
for (const locale of Object.keys(meta)) {
|
|
||||||
if (!meta[locale].nativeName) {
|
|
||||||
meta[locale] = undefined;
|
|
||||||
console.warn(
|
|
||||||
`Skipping locale ${locale} because native name is not translated.`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
meta[locale].hash = HASHES.get(locale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
fragments: FRAGMENTS.filter(panelFragment),
|
|
||||||
translations: meta,
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.pipe(gulp.dest(workDir));
|
.pipe(gulp.dest(fullDir));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("build-merged-translations", () =>
|
||||||
|
gulp
|
||||||
|
.src([
|
||||||
|
inFrontendDir + "/*.json",
|
||||||
|
"!" + inFrontendDir + "/en.json",
|
||||||
|
...(env.isProdBuild() ? [] : [workDir + "/test.json"]),
|
||||||
|
])
|
||||||
|
.pipe(transform((data, file) => lokaliseTransform(data, data, file)))
|
||||||
|
.pipe(
|
||||||
|
flatmap((stream, file) => {
|
||||||
|
// For each language generate a merged json file. It begins with the master
|
||||||
|
// translation as a failsafe for untranslated strings, and merges all parent
|
||||||
|
// tags into one file for each specific subtag
|
||||||
|
//
|
||||||
|
// TODO: This is a naive interpretation of BCP47 that should be improved.
|
||||||
|
// Will be OK for now as long as we don't have anything more complicated
|
||||||
|
// than a base translation + region.
|
||||||
|
const tr = path.basename(file.history[0], ".json");
|
||||||
|
const subtags = tr.split("-");
|
||||||
|
const src = [fullDir + "/en.json"];
|
||||||
|
for (let i = 1; i <= subtags.length; i++) {
|
||||||
|
const lang = subtags.slice(0, i).join("-");
|
||||||
|
if (lang === "test") {
|
||||||
|
src.push(workDir + "/test.json");
|
||||||
|
} else if (lang !== "en") {
|
||||||
|
src.push(inFrontendDir + "/" + lang + ".json");
|
||||||
|
if (mergeBackend) {
|
||||||
|
src.push(inBackendDir + "/" + lang + ".json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return gulp
|
||||||
|
.src(src, { allowEmpty: true })
|
||||||
|
.pipe(transform((data) => emptyFilter(data)))
|
||||||
|
.pipe(
|
||||||
|
merge({
|
||||||
|
fileName: tr + ".json",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.pipe(gulp.dest(fullDir));
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let taskName;
|
||||||
|
|
||||||
|
const splitTasks = [];
|
||||||
|
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||||
|
taskName = "build-translation-fragment-" + fragment;
|
||||||
|
gulp.task(taskName, () =>
|
||||||
|
// Return only the translations for this fragment.
|
||||||
|
gulp
|
||||||
|
.src(fullDir + "/*.json")
|
||||||
|
.pipe(
|
||||||
|
transform((data) => ({
|
||||||
|
ui: {
|
||||||
|
panel: {
|
||||||
|
[fragment]: data.ui.panel[fragment],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.pipe(gulp.dest(workDir + "/" + fragment))
|
||||||
|
);
|
||||||
|
splitTasks.push(taskName);
|
||||||
|
});
|
||||||
|
|
||||||
|
taskName = "build-translation-core";
|
||||||
|
gulp.task(taskName, () =>
|
||||||
|
// Remove the fragment translations from the core translation.
|
||||||
|
gulp
|
||||||
|
.src(fullDir + "/*.json")
|
||||||
|
.pipe(
|
||||||
|
transform((data, _file) => {
|
||||||
|
TRANSLATION_FRAGMENTS.forEach((fragment) => {
|
||||||
|
delete data.ui.panel[fragment];
|
||||||
|
});
|
||||||
|
delete data.supervisor;
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.pipe(gulp.dest(coreDir))
|
||||||
|
);
|
||||||
|
|
||||||
|
splitTasks.push(taskName);
|
||||||
|
|
||||||
|
gulp.task("build-flattened-translations", () =>
|
||||||
|
// Flatten the split versions of our translations, and move them into outDir
|
||||||
|
gulp
|
||||||
|
.src(
|
||||||
|
TRANSLATION_FRAGMENTS.map(
|
||||||
|
(fragment) => workDir + "/" + fragment + "/*.json"
|
||||||
|
).concat(coreDir + "/*.json"),
|
||||||
|
{ base: workDir }
|
||||||
|
)
|
||||||
|
.pipe(
|
||||||
|
transform((data) =>
|
||||||
|
// Polymer.AppLocalizeBehavior requires flattened json
|
||||||
|
flatten(data)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.pipe(
|
||||||
|
rename((filePath) => {
|
||||||
|
if (filePath.dirname === "core") {
|
||||||
|
filePath.dirname = "";
|
||||||
|
}
|
||||||
|
// In dev we create the file with the fake hash in the filename
|
||||||
|
if (!env.isProdBuild()) {
|
||||||
|
filePath.basename += "-dev";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.pipe(gulp.dest(outDir))
|
||||||
|
);
|
||||||
|
|
||||||
|
const fingerprints = {};
|
||||||
|
|
||||||
|
gulp.task("build-translation-fingerprints", () => {
|
||||||
|
// Fingerprint full file of each language
|
||||||
|
const files = readdirSync(fullDir);
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
fingerprints[files[i].split(".")[0]] = {
|
||||||
|
// In dev we create fake hashes
|
||||||
|
hash: env.isProdBuild()
|
||||||
|
? createHash("md5")
|
||||||
|
.update(readFileSync(path.join(fullDir, files[i]), "utf-8"))
|
||||||
|
.digest("hex")
|
||||||
|
: "dev",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// In dev we create the file with the fake hash in the filename
|
||||||
|
if (env.isProdBuild()) {
|
||||||
|
mapFiles(outDir, ".json", (filename) => {
|
||||||
|
const parsed = path.parse(filename);
|
||||||
|
|
||||||
|
// nl.json -> nl-<hash>.json
|
||||||
|
if (!(parsed.name in fingerprints)) {
|
||||||
|
throw new Error(`Unable to find hash for ${filename}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
renameSync(
|
||||||
|
filename,
|
||||||
|
`${parsed.dir}/${parsed.name}-${fingerprints[parsed.name].hash}${
|
||||||
|
parsed.ext
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = source("translationFingerprints.json");
|
||||||
|
stream.write(JSON.stringify(fingerprints));
|
||||||
|
process.nextTick(() => stream.end());
|
||||||
|
return stream.pipe(vinylBuffer()).pipe(gulp.dest(workDir));
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task("build-translation-fragment-supervisor", () =>
|
||||||
|
gulp
|
||||||
|
.src(fullDir + "/*.json")
|
||||||
|
.pipe(transform((data) => data.supervisor))
|
||||||
|
.pipe(
|
||||||
|
rename((filePath) => {
|
||||||
|
// In dev we create the file with the fake hash in the filename
|
||||||
|
if (!env.isProdBuild()) {
|
||||||
|
filePath.basename += "-dev";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.pipe(gulp.dest(workDir + "/supervisor"))
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task("build-translation-flatten-supervisor", () =>
|
||||||
|
gulp
|
||||||
|
.src(workDir + "/supervisor/*.json")
|
||||||
|
.pipe(
|
||||||
|
transform((data) =>
|
||||||
|
// Polymer.AppLocalizeBehavior requires flattened json
|
||||||
|
flatten(data)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.pipe(gulp.dest(outDir))
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task("build-translation-write-metadata", () =>
|
||||||
|
gulp
|
||||||
|
.src([
|
||||||
|
path.join(paths.translations_src, "translationMetadata.json"),
|
||||||
|
...(env.isProdBuild() ? [] : [workDir + "/testMetadata.json"]),
|
||||||
|
workDir + "/translationFingerprints.json",
|
||||||
|
])
|
||||||
|
.pipe(merge({}))
|
||||||
|
.pipe(
|
||||||
|
transform((data) => {
|
||||||
|
const newData = {};
|
||||||
|
Object.entries(data).forEach(([key, value]) => {
|
||||||
|
// Filter out translations without native name.
|
||||||
|
if (value.nativeName) {
|
||||||
|
newData[key] = value;
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
`Skipping language ${key}. Native name was not translated.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return newData;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.pipe(
|
||||||
|
transform((data) => ({
|
||||||
|
fragments: TRANSLATION_FRAGMENTS,
|
||||||
|
translations: data,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.pipe(rename("translationMetadata.json"))
|
||||||
|
.pipe(gulp.dest(workDir))
|
||||||
|
);
|
||||||
|
|
||||||
|
gulp.task(
|
||||||
|
"create-translations",
|
||||||
|
gulp.series(
|
||||||
|
gulp.parallel("create-test-metadata", "create-test-translation"),
|
||||||
|
"build-master-translation",
|
||||||
|
"build-merged-translations",
|
||||||
|
gulp.parallel(...splitTasks),
|
||||||
|
"build-flattened-translations"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"build-translations",
|
"build-translations",
|
||||||
gulp.series(
|
gulp.series(
|
||||||
gulp.parallel(
|
gulp.parallel(
|
||||||
"fetch-nightly-translations",
|
"fetch-nightly-translations",
|
||||||
gulp.series("clean-translations", makeWorkDir)
|
gulp.series("clean-translations", "ensure-translations-build-dir")
|
||||||
),
|
),
|
||||||
createTestTranslation,
|
"create-translations",
|
||||||
createMasterTranslation,
|
"build-translation-fingerprints",
|
||||||
createTranslations,
|
"build-translation-write-metadata"
|
||||||
writeTranslationMetaData
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
gulp.task(
|
gulp.task(
|
||||||
"build-supervisor-translations",
|
"build-supervisor-translations",
|
||||||
gulp.series(toggleSupervisorFragment, "build-translations")
|
gulp.series(
|
||||||
|
gulp.parallel(
|
||||||
|
"fetch-nightly-translations",
|
||||||
|
gulp.series("clean-translations", "ensure-translations-build-dir")
|
||||||
|
),
|
||||||
|
gulp.parallel("create-test-metadata", "create-test-translation"),
|
||||||
|
"build-master-translation",
|
||||||
|
"build-merged-translations",
|
||||||
|
"build-translation-fragment-supervisor",
|
||||||
|
"build-translation-flatten-supervisor",
|
||||||
|
"build-translation-fingerprints",
|
||||||
|
"build-translation-write-metadata"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
@@ -99,7 +99,7 @@ gulp.task("webpack-watch-app", () => {
|
|||||||
).watch({ poll: isWsl }, doneHandler());
|
).watch({ poll: isWsl }, doneHandler());
|
||||||
gulp.watch(
|
gulp.watch(
|
||||||
path.join(paths.translations_src, "en.json"),
|
path.join(paths.translations_src, "en.json"),
|
||||||
gulp.series("build-translations", "copy-translations-app")
|
gulp.series("create-translations", "copy-translations-app")
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
16
build-scripts/util.cjs
Normal file
16
build-scripts/util.cjs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
// Helper function to map recursively over files in a folder and it's subfolders
|
||||||
|
module.exports.mapFiles = function mapFiles(startPath, filter, mapFunc) {
|
||||||
|
const files = fs.readdirSync(startPath);
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const filename = path.join(startPath, files[i]);
|
||||||
|
const stat = fs.lstatSync(filename);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
mapFiles(filename, filter, mapFunc);
|
||||||
|
} else if (filename.indexOf(filter) >= 0) {
|
||||||
|
mapFunc(filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@@ -10,7 +10,6 @@ const WebpackBar = require("webpackbar");
|
|||||||
const {
|
const {
|
||||||
TransformAsyncModulesPlugin,
|
TransformAsyncModulesPlugin,
|
||||||
} = require("transform-async-modules-webpack-plugin");
|
} = require("transform-async-modules-webpack-plugin");
|
||||||
const { dependencies } = require("../package.json");
|
|
||||||
const paths = require("./paths.cjs");
|
const paths = require("./paths.cjs");
|
||||||
const bundle = require("./bundle.cjs");
|
const bundle = require("./bundle.cjs");
|
||||||
|
|
||||||
@@ -157,15 +156,11 @@ const createWebpackConfig = ({
|
|||||||
transform: (stats) => JSON.stringify(filterStats(stats)),
|
transform: (stats) => JSON.stringify(filterStats(stats)),
|
||||||
}),
|
}),
|
||||||
!latestBuild &&
|
!latestBuild &&
|
||||||
new TransformAsyncModulesPlugin({
|
new TransformAsyncModulesPlugin({ browserslistEnv: "legacy" }),
|
||||||
browserslistEnv: "legacy",
|
|
||||||
runtime: { version: dependencies["@babel/runtime"] },
|
|
||||||
}),
|
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: [".ts", ".js", ".json"],
|
extensions: [".ts", ".js", ".json"],
|
||||||
alias: {
|
alias: {
|
||||||
"lit/static-html$": "lit/static-html.js",
|
|
||||||
"lit/decorators$": "lit/decorators.js",
|
"lit/decorators$": "lit/decorators.js",
|
||||||
"lit/directive$": "lit/directive.js",
|
"lit/directive$": "lit/directive.js",
|
||||||
"lit/directives/until$": "lit/directives/until.js",
|
"lit/directives/until$": "lit/directives/until.js",
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import "@material/mwc-button/mwc-button";
|
import "@material/mwc-button/mwc-button";
|
||||||
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
|
import { mdiCast, mdiCastConnected } from "@mdi/js";
|
||||||
import "@polymer/paper-item/paper-icon-item";
|
import "@polymer/paper-item/paper-icon-item";
|
||||||
import "@polymer/paper-listbox/paper-listbox";
|
import "@polymer/paper-listbox/paper-listbox";
|
||||||
import { Auth, Connection } from "home-assistant-js-websocket";
|
import { Auth, Connection } from "home-assistant-js-websocket";
|
||||||
@@ -104,11 +104,8 @@ class HcCast extends LitElement {
|
|||||||
slot="item-icon"
|
slot="item-icon"
|
||||||
></ha-icon>
|
></ha-icon>
|
||||||
`
|
`
|
||||||
: html`<ha-svg-icon
|
: ""}
|
||||||
slot="item-icon"
|
${view.title || view.path}
|
||||||
.path=${mdiViewDashboard}
|
|
||||||
></ha-svg-icon>`}
|
|
||||||
${view.title || view.path || "Unnamed view"}
|
|
||||||
</paper-icon-item>
|
</paper-icon-item>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
@@ -253,8 +250,7 @@ class HcCast extends LitElement {
|
|||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
paper-listbox ha-icon,
|
paper-listbox ha-icon {
|
||||||
paper-listbox ha-svg-icon {
|
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,6 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|||||||
import { customElement, property, query } from "lit/decorators";
|
import { customElement, property, query } from "lit/decorators";
|
||||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||||
import { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
|
import { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
|
||||||
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
|
|
||||||
import { Lovelace } from "../../../../src/panels/lovelace/types";
|
import { Lovelace } from "../../../../src/panels/lovelace/types";
|
||||||
import "../../../../src/panels/lovelace/views/hui-view";
|
import "../../../../src/panels/lovelace/views/hui-view";
|
||||||
import { HomeAssistant } from "../../../../src/types";
|
import { HomeAssistant } from "../../../../src/types";
|
||||||
@@ -62,12 +61,7 @@ class HcLovelace extends LitElement {
|
|||||||
const index = this._viewIndex;
|
const index = this._viewIndex;
|
||||||
|
|
||||||
if (index !== undefined) {
|
if (index !== undefined) {
|
||||||
const title = getPanelTitleFromUrlPath(
|
const dashboardTitle = this.lovelaceConfig.title || this.urlPath;
|
||||||
this.hass,
|
|
||||||
this.urlPath || "lovelace"
|
|
||||||
);
|
|
||||||
|
|
||||||
const dashboardTitle = title || this.urlPath;
|
|
||||||
|
|
||||||
const viewTitle =
|
const viewTitle =
|
||||||
this.lovelaceConfig.views[index].title ||
|
this.lovelaceConfig.views[index].title ||
|
||||||
@@ -86,17 +80,10 @@ class HcLovelace extends LitElement {
|
|||||||
this.lovelaceConfig.views[index].background ||
|
this.lovelaceConfig.views[index].background ||
|
||||||
this.lovelaceConfig.background;
|
this.lovelaceConfig.background;
|
||||||
|
|
||||||
const backgroundStyle =
|
if (configBackground) {
|
||||||
typeof configBackground === "string"
|
|
||||||
? configBackground
|
|
||||||
: configBackground?.image
|
|
||||||
? `center / cover no-repeat url('${configBackground.image}')`
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (backgroundStyle) {
|
|
||||||
this._huiView!.style.setProperty(
|
this._huiView!.style.setProperty(
|
||||||
"--lovelace-background",
|
"--lovelace-background",
|
||||||
backgroundStyle
|
configBackground
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this._huiView!.style.removeProperty("--lovelace-background");
|
this._huiView!.style.removeProperty("--lovelace-background");
|
||||||
|
@@ -35,7 +35,6 @@ import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/lo
|
|||||||
import { HassElement } from "../../../../src/state/hass-element";
|
import { HassElement } from "../../../../src/state/hass-element";
|
||||||
import { castContext } from "../cast_context";
|
import { castContext } from "../cast_context";
|
||||||
import "./hc-launch-screen";
|
import "./hc-launch-screen";
|
||||||
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
|
|
||||||
|
|
||||||
const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = {
|
const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = {
|
||||||
strategy: {
|
strategy: {
|
||||||
@@ -360,11 +359,7 @@ export class HcMain extends HassElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) {
|
private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) {
|
||||||
const title = getPanelTitleFromUrlPath(
|
castContext.setApplicationState(lovelaceConfig.title || "");
|
||||||
this.hass!,
|
|
||||||
this._urlPath || "lovelace"
|
|
||||||
);
|
|
||||||
castContext.setApplicationState(title || "");
|
|
||||||
this._lovelaceConfig = lovelaceConfig;
|
this._lovelaceConfig = lovelaceConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,7 +10,6 @@ import {
|
|||||||
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
|
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
|
||||||
import { HomeAssistant } from "../../src/types";
|
import { HomeAssistant } from "../../src/types";
|
||||||
import { selectedDemoConfig } from "./configs/demo-configs";
|
import { selectedDemoConfig } from "./configs/demo-configs";
|
||||||
import { mockAreaRegistry } from "./stubs/area_registry";
|
|
||||||
import { mockAuth } from "./stubs/auth";
|
import { mockAuth } from "./stubs/auth";
|
||||||
import { mockConfigEntries } from "./stubs/config_entries";
|
import { mockConfigEntries } from "./stubs/config_entries";
|
||||||
import { mockEnergy } from "./stubs/energy";
|
import { mockEnergy } from "./stubs/energy";
|
||||||
@@ -24,10 +23,10 @@ import { mockLovelace } from "./stubs/lovelace";
|
|||||||
import { mockMediaPlayer } from "./stubs/media_player";
|
import { mockMediaPlayer } from "./stubs/media_player";
|
||||||
import { mockPersistentNotification } from "./stubs/persistent_notification";
|
import { mockPersistentNotification } from "./stubs/persistent_notification";
|
||||||
import { mockRecorder } from "./stubs/recorder";
|
import { mockRecorder } from "./stubs/recorder";
|
||||||
|
import { mockTodo } from "./stubs/todo";
|
||||||
import { mockSensor } from "./stubs/sensor";
|
import { mockSensor } from "./stubs/sensor";
|
||||||
import { mockSystemLog } from "./stubs/system_log";
|
import { mockSystemLog } from "./stubs/system_log";
|
||||||
import { mockTemplate } from "./stubs/template";
|
import { mockTemplate } from "./stubs/template";
|
||||||
import { mockTodo } from "./stubs/todo";
|
|
||||||
import { mockTranslations } from "./stubs/translations";
|
import { mockTranslations } from "./stubs/translations";
|
||||||
|
|
||||||
@customElement("ha-demo")
|
@customElement("ha-demo")
|
||||||
@@ -63,7 +62,6 @@ export class HaDemo extends HomeAssistantAppEl {
|
|||||||
mockEnergy(hass);
|
mockEnergy(hass);
|
||||||
mockPersistentNotification(hass);
|
mockPersistentNotification(hass);
|
||||||
mockConfigEntries(hass);
|
mockConfigEntries(hass);
|
||||||
mockAreaRegistry(hass);
|
|
||||||
mockEntityRegistry(hass, [
|
mockEntityRegistry(hass, [
|
||||||
{
|
{
|
||||||
config_entry_id: "co2signal",
|
config_entry_id: "co2signal",
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { format, startOfToday, startOfTomorrow } from "date-fns";
|
import { format, startOfToday, startOfTomorrow } from "date-fns/esm";
|
||||||
import {
|
import {
|
||||||
EnergyInfo,
|
EnergyInfo,
|
||||||
EnergyPreferences,
|
EnergyPreferences,
|
||||||
|
@@ -19,15 +19,15 @@ export const mockLovelace = (
|
|||||||
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
|
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
|
||||||
};
|
};
|
||||||
|
|
||||||
customElements.whenDefined("hui-card").then(() => {
|
customElements.whenDefined("hui-view").then(() => {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const HUIView = customElements.get("hui-card");
|
const HUIView = customElements.get("hui-view");
|
||||||
// Patch HUI-VIEW to make the lovelace object available to the demo card
|
// Patch HUI-VIEW to make the lovelace object available to the demo card
|
||||||
const oldCreateCard = HUIView!.prototype.createElement;
|
const oldCreateCard = HUIView!.prototype.createCardElement;
|
||||||
|
|
||||||
HUIView!.prototype.createElement = function (config) {
|
HUIView!.prototype.createCardElement = function (config) {
|
||||||
const el = oldCreateCard.call(this, config);
|
const el = oldCreateCard.call(this, config);
|
||||||
if (config.type === "custom:ha-demo-card") {
|
if (el.tagName === "HA-DEMO-CARD") {
|
||||||
(el as HADemoCard).lovelace = this.lovelace;
|
(el as HADemoCard).lovelace = this.lovelace;
|
||||||
}
|
}
|
||||||
return el;
|
return el;
|
||||||
|
@@ -64,12 +64,6 @@ const ACTIONS = [
|
|||||||
entity_id: "input_boolean.toggle_4",
|
entity_id: "input_boolean.toggle_4",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
sequence: [
|
|
||||||
{ scene: "scene.kitchen_morning" },
|
|
||||||
{ service: "light.turn_off", target: { entity_id: "light.kitchen" } },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
parallel: [
|
parallel: [
|
||||||
{ scene: "scene.kitchen_morning" },
|
{ scene: "scene.kitchen_morning" },
|
||||||
@@ -142,7 +136,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
|||||||
<div class="action">
|
<div class="action">
|
||||||
<span>
|
<span>
|
||||||
${this._action
|
${this._action
|
||||||
? describeAction(this.hass, [], [], [], this._action)
|
? describeAction(this.hass, [], this._action)
|
||||||
: "<invalid YAML>"}
|
: "<invalid YAML>"}
|
||||||
</span>
|
</span>
|
||||||
<ha-yaml-editor
|
<ha-yaml-editor
|
||||||
@@ -155,7 +149,7 @@ export class DemoAutomationDescribeAction extends LitElement {
|
|||||||
${ACTIONS.map(
|
${ACTIONS.map(
|
||||||
(conf) => html`
|
(conf) => html`
|
||||||
<div class="action">
|
<div class="action">
|
||||||
<span>${describeAction(this.hass, [], [], [], conf as any)}</span>
|
<span>${describeAction(this.hass, [], conf as any)}</span>
|
||||||
<pre>${dump(conf)}</pre>
|
<pre>${dump(conf)}</pre>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
@@ -20,7 +20,6 @@ import { HaWaitForTriggerAction } from "../../../../src/panels/config/automation
|
|||||||
import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template";
|
import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template";
|
||||||
import { Action } from "../../../../src/data/script";
|
import { Action } from "../../../../src/data/script";
|
||||||
import { HaConditionAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-condition";
|
import { HaConditionAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-condition";
|
||||||
import { HaSequenceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-sequence";
|
|
||||||
import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel";
|
import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel";
|
||||||
import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if";
|
import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if";
|
||||||
import { HaStopAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-stop";
|
import { HaStopAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-stop";
|
||||||
@@ -40,7 +39,6 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [
|
|||||||
{ name: "If-Then", actions: [HaIfAction.defaultConfig] },
|
{ name: "If-Then", actions: [HaIfAction.defaultConfig] },
|
||||||
{ name: "Choose", actions: [HaChooseAction.defaultConfig] },
|
{ name: "Choose", actions: [HaChooseAction.defaultConfig] },
|
||||||
{ name: "Variables", actions: [{ variables: { hello: "1" } }] },
|
{ name: "Variables", actions: [{ variables: { hello: "1" } }] },
|
||||||
{ name: "Sequence", actions: [HaSequenceAction.defaultConfig] },
|
|
||||||
{ name: "Parallel", actions: [HaParallelAction.defaultConfig] },
|
{ name: "Parallel", actions: [HaParallelAction.defaultConfig] },
|
||||||
{ name: "Stop", actions: [HaStopAction.defaultConfig] },
|
{ name: "Stop", actions: [HaStopAction.defaultConfig] },
|
||||||
];
|
];
|
||||||
|
@@ -187,7 +187,7 @@ export class DemoHaControlSelect extends LitElement {
|
|||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
--control-select-color: var(--state-fan-active-color);
|
--control-select-color: var(--state-fan-active-color);
|
||||||
--control-select-thickness: 130px;
|
--control-select-thickness: 130px;
|
||||||
--control-select-border-radius: 36px;
|
--control-select-border-radius: 48px;
|
||||||
}
|
}
|
||||||
.vertical-selects {
|
.vertical-selects {
|
||||||
height: 300px;
|
height: 300px;
|
||||||
|
@@ -151,7 +151,7 @@ export class DemoHaBarSlider extends LitElement {
|
|||||||
--control-slider-background: #ffcf4c;
|
--control-slider-background: #ffcf4c;
|
||||||
--control-slider-background-opacity: 0.2;
|
--control-slider-background-opacity: 0.2;
|
||||||
--control-slider-thickness: 130px;
|
--control-slider-thickness: 130px;
|
||||||
--control-slider-border-radius: 36px;
|
--control-slider-border-radius: 48px;
|
||||||
}
|
}
|
||||||
.vertical-sliders {
|
.vertical-sliders {
|
||||||
height: 300px;
|
height: 300px;
|
||||||
|
@@ -118,7 +118,7 @@ export class DemoHaControlSwitch extends LitElement {
|
|||||||
--control-switch-on-color: var(--green-color);
|
--control-switch-on-color: var(--green-color);
|
||||||
--control-switch-off-color: var(--red-color);
|
--control-switch-off-color: var(--red-color);
|
||||||
--control-switch-thickness: 130px;
|
--control-switch-thickness: 130px;
|
||||||
--control-switch-border-radius: 36px;
|
--control-switch-border-radius: 48px;
|
||||||
--control-switch-padding: 6px;
|
--control-switch-padding: 6px;
|
||||||
--mdc-icon-size: 24px;
|
--mdc-icon-size: 24px;
|
||||||
}
|
}
|
||||||
|
@@ -161,14 +161,12 @@ const LABELS: LabelRegistryEntry[] = [
|
|||||||
name: "Energy",
|
name: "Energy",
|
||||||
icon: null,
|
icon: null,
|
||||||
color: "yellow",
|
color: "yellow",
|
||||||
description: null,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label_id: "entertainment",
|
label_id: "entertainment",
|
||||||
name: "Entertainment",
|
name: "Entertainment",
|
||||||
icon: "mdi:popcorn",
|
icon: "mdi:popcorn",
|
||||||
color: "blue",
|
color: "blue",
|
||||||
description: null,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -56,46 +56,48 @@ export class DemoDateTimeDateTimeNumeric extends LitElement {
|
|||||||
<div class="center">12 Hours</div>
|
<div class="center">12 Hours</div>
|
||||||
<div class="center">24 Hours</div>
|
<div class="center">24 Hours</div>
|
||||||
</div>
|
</div>
|
||||||
${Object.entries(translationMetadata.translations).map(
|
${Object.entries(translationMetadata.translations)
|
||||||
([key, value]) => html`
|
.filter(([key, _]) => key !== "test")
|
||||||
<div class="container">
|
.map(
|
||||||
<div>${value.nativeName}</div>
|
([key, value]) => html`
|
||||||
<div class="center">
|
<div class="container">
|
||||||
${formatDateTimeNumeric(
|
<div>${value.nativeName}</div>
|
||||||
this.date,
|
<div class="center">
|
||||||
{
|
${formatDateTimeNumeric(
|
||||||
...defaultLocale,
|
this.date,
|
||||||
language: key,
|
{
|
||||||
time_format: TimeFormat.language,
|
...defaultLocale,
|
||||||
},
|
language: key,
|
||||||
demoConfig
|
time_format: TimeFormat.language,
|
||||||
)}
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatDateTimeNumeric(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.am_pm,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatDateTimeNumeric(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.twenty_four,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="center">
|
`
|
||||||
${formatDateTimeNumeric(
|
)}
|
||||||
this.date,
|
|
||||||
{
|
|
||||||
...defaultLocale,
|
|
||||||
language: key,
|
|
||||||
time_format: TimeFormat.am_pm,
|
|
||||||
},
|
|
||||||
demoConfig
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="center">
|
|
||||||
${formatDateTimeNumeric(
|
|
||||||
this.date,
|
|
||||||
{
|
|
||||||
...defaultLocale,
|
|
||||||
language: key,
|
|
||||||
time_format: TimeFormat.twenty_four,
|
|
||||||
},
|
|
||||||
demoConfig
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</mwc-list>
|
</mwc-list>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -56,46 +56,48 @@ export class DemoDateTimeDateTimeSeconds extends LitElement {
|
|||||||
<div class="center">12 Hours</div>
|
<div class="center">12 Hours</div>
|
||||||
<div class="center">24 Hours</div>
|
<div class="center">24 Hours</div>
|
||||||
</div>
|
</div>
|
||||||
${Object.entries(translationMetadata.translations).map(
|
${Object.entries(translationMetadata.translations)
|
||||||
([key, value]) => html`
|
.filter(([key, _]) => key !== "test")
|
||||||
<div class="container">
|
.map(
|
||||||
<div>${value.nativeName}</div>
|
([key, value]) => html`
|
||||||
<div class="center">
|
<div class="container">
|
||||||
${formatDateTimeWithSeconds(
|
<div>${value.nativeName}</div>
|
||||||
this.date,
|
<div class="center">
|
||||||
{
|
${formatDateTimeWithSeconds(
|
||||||
...defaultLocale,
|
this.date,
|
||||||
language: key,
|
{
|
||||||
time_format: TimeFormat.language,
|
...defaultLocale,
|
||||||
},
|
language: key,
|
||||||
demoConfig
|
time_format: TimeFormat.language,
|
||||||
)}
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatDateTimeWithSeconds(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.am_pm,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatDateTimeWithSeconds(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.twenty_four,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="center">
|
`
|
||||||
${formatDateTimeWithSeconds(
|
)}
|
||||||
this.date,
|
|
||||||
{
|
|
||||||
...defaultLocale,
|
|
||||||
language: key,
|
|
||||||
time_format: TimeFormat.am_pm,
|
|
||||||
},
|
|
||||||
demoConfig
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="center">
|
|
||||||
${formatDateTimeWithSeconds(
|
|
||||||
this.date,
|
|
||||||
{
|
|
||||||
...defaultLocale,
|
|
||||||
language: key,
|
|
||||||
time_format: TimeFormat.twenty_four,
|
|
||||||
},
|
|
||||||
demoConfig
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</mwc-list>
|
</mwc-list>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -56,46 +56,48 @@ export class DemoDateTimeDateTimeShortYear extends LitElement {
|
|||||||
<div class="center">12 Hours</div>
|
<div class="center">12 Hours</div>
|
||||||
<div class="center">24 Hours</div>
|
<div class="center">24 Hours</div>
|
||||||
</div>
|
</div>
|
||||||
${Object.entries(translationMetadata.translations).map(
|
${Object.entries(translationMetadata.translations)
|
||||||
([key, value]) => html`
|
.filter(([key, _]) => key !== "test")
|
||||||
<div class="container">
|
.map(
|
||||||
<div>${value.nativeName}</div>
|
([key, value]) => html`
|
||||||
<div class="center">
|
<div class="container">
|
||||||
${formatShortDateTimeWithYear(
|
<div>${value.nativeName}</div>
|
||||||
this.date,
|
<div class="center">
|
||||||
{
|
${formatShortDateTimeWithYear(
|
||||||
...defaultLocale,
|
this.date,
|
||||||
language: key,
|
{
|
||||||
time_format: TimeFormat.language,
|
...defaultLocale,
|
||||||
},
|
language: key,
|
||||||
demoConfig
|
time_format: TimeFormat.language,
|
||||||
)}
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatShortDateTimeWithYear(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.am_pm,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatShortDateTimeWithYear(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.twenty_four,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="center">
|
`
|
||||||
${formatShortDateTimeWithYear(
|
)}
|
||||||
this.date,
|
|
||||||
{
|
|
||||||
...defaultLocale,
|
|
||||||
language: key,
|
|
||||||
time_format: TimeFormat.am_pm,
|
|
||||||
},
|
|
||||||
demoConfig
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="center">
|
|
||||||
${formatShortDateTimeWithYear(
|
|
||||||
this.date,
|
|
||||||
{
|
|
||||||
...defaultLocale,
|
|
||||||
language: key,
|
|
||||||
time_format: TimeFormat.twenty_four,
|
|
||||||
},
|
|
||||||
demoConfig
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</mwc-list>
|
</mwc-list>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -56,46 +56,48 @@ export class DemoDateTimeDateTimeShort extends LitElement {
|
|||||||
<div class="center">12 Hours</div>
|
<div class="center">12 Hours</div>
|
||||||
<div class="center">24 Hours</div>
|
<div class="center">24 Hours</div>
|
||||||
</div>
|
</div>
|
||||||
${Object.entries(translationMetadata.translations).map(
|
${Object.entries(translationMetadata.translations)
|
||||||
([key, value]) => html`
|
.filter(([key, _]) => key !== "test")
|
||||||
<div class="container">
|
.map(
|
||||||
<div>${value.nativeName}</div>
|
([key, value]) => html`
|
||||||
<div class="center">
|
<div class="container">
|
||||||
${formatShortDateTime(
|
<div>${value.nativeName}</div>
|
||||||
this.date,
|
<div class="center">
|
||||||
{
|
${formatShortDateTime(
|
||||||
...defaultLocale,
|
this.date,
|
||||||
language: key,
|
{
|
||||||
time_format: TimeFormat.language,
|
...defaultLocale,
|
||||||
},
|
language: key,
|
||||||
demoConfig
|
time_format: TimeFormat.language,
|
||||||
)}
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatShortDateTime(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.am_pm,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatShortDateTime(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.twenty_four,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="center">
|
`
|
||||||
${formatShortDateTime(
|
)}
|
||||||
this.date,
|
|
||||||
{
|
|
||||||
...defaultLocale,
|
|
||||||
language: key,
|
|
||||||
time_format: TimeFormat.am_pm,
|
|
||||||
},
|
|
||||||
demoConfig
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="center">
|
|
||||||
${formatShortDateTime(
|
|
||||||
this.date,
|
|
||||||
{
|
|
||||||
...defaultLocale,
|
|
||||||
language: key,
|
|
||||||
time_format: TimeFormat.twenty_four,
|
|
||||||
},
|
|
||||||
demoConfig
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</mwc-list>
|
</mwc-list>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -56,46 +56,48 @@ export class DemoDateTimeDateTime extends LitElement {
|
|||||||
<div class="center">12 Hours</div>
|
<div class="center">12 Hours</div>
|
||||||
<div class="center">24 Hours</div>
|
<div class="center">24 Hours</div>
|
||||||
</div>
|
</div>
|
||||||
${Object.entries(translationMetadata.translations).map(
|
${Object.entries(translationMetadata.translations)
|
||||||
([key, value]) => html`
|
.filter(([key, _]) => key !== "test")
|
||||||
<div class="container">
|
.map(
|
||||||
<div>${value.nativeName}</div>
|
([key, value]) => html`
|
||||||
<div class="center">
|
<div class="container">
|
||||||
${formatDateTime(
|
<div>${value.nativeName}</div>
|
||||||
this.date,
|
<div class="center">
|
||||||
{
|
${formatDateTime(
|
||||||
...defaultLocale,
|
this.date,
|
||||||
language: key,
|
{
|
||||||
time_format: TimeFormat.language,
|
...defaultLocale,
|
||||||
},
|
language: key,
|
||||||
demoConfig
|
time_format: TimeFormat.language,
|
||||||
)}
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatDateTime(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.am_pm,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatDateTime(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.twenty_four,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="center">
|
`
|
||||||
${formatDateTime(
|
)}
|
||||||
this.date,
|
|
||||||
{
|
|
||||||
...defaultLocale,
|
|
||||||
language: key,
|
|
||||||
time_format: TimeFormat.am_pm,
|
|
||||||
},
|
|
||||||
demoConfig
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="center">
|
|
||||||
${formatDateTime(
|
|
||||||
this.date,
|
|
||||||
{
|
|
||||||
...defaultLocale,
|
|
||||||
language: key,
|
|
||||||
time_format: TimeFormat.twenty_four,
|
|
||||||
},
|
|
||||||
demoConfig
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</mwc-list>
|
</mwc-list>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -35,57 +35,59 @@ export class DemoDateTimeDate extends LitElement {
|
|||||||
<div class="center">Month-Day-Year</div>
|
<div class="center">Month-Day-Year</div>
|
||||||
<div class="center">Year-Month-Day</div>
|
<div class="center">Year-Month-Day</div>
|
||||||
</div>
|
</div>
|
||||||
${Object.entries(translationMetadata.translations).map(
|
${Object.entries(translationMetadata.translations)
|
||||||
([key, value]) => html`
|
.filter(([key, _]) => key !== "test")
|
||||||
<div class="container">
|
.map(
|
||||||
<div>${value.nativeName}</div>
|
([key, value]) => html`
|
||||||
<div class="center">
|
<div class="container">
|
||||||
${formatDateNumeric(
|
<div>${value.nativeName}</div>
|
||||||
date,
|
<div class="center">
|
||||||
{
|
${formatDateNumeric(
|
||||||
...defaultLocale,
|
date,
|
||||||
language: key,
|
{
|
||||||
date_format: DateFormat.language,
|
...defaultLocale,
|
||||||
},
|
language: key,
|
||||||
demoConfig
|
date_format: DateFormat.language,
|
||||||
)}
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatDateNumeric(
|
||||||
|
date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
date_format: DateFormat.DMY,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatDateNumeric(
|
||||||
|
date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
date_format: DateFormat.MDY,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatDateNumeric(
|
||||||
|
date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
date_format: DateFormat.YMD,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="center">
|
`
|
||||||
${formatDateNumeric(
|
)}
|
||||||
date,
|
|
||||||
{
|
|
||||||
...defaultLocale,
|
|
||||||
language: key,
|
|
||||||
date_format: DateFormat.DMY,
|
|
||||||
},
|
|
||||||
demoConfig
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="center">
|
|
||||||
${formatDateNumeric(
|
|
||||||
date,
|
|
||||||
{
|
|
||||||
...defaultLocale,
|
|
||||||
language: key,
|
|
||||||
date_format: DateFormat.MDY,
|
|
||||||
},
|
|
||||||
demoConfig
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="center">
|
|
||||||
${formatDateNumeric(
|
|
||||||
date,
|
|
||||||
{
|
|
||||||
...defaultLocale,
|
|
||||||
language: key,
|
|
||||||
date_format: DateFormat.YMD,
|
|
||||||
},
|
|
||||||
demoConfig
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</mwc-list>
|
</mwc-list>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -56,46 +56,48 @@ export class DemoDateTimeTimeSeconds extends LitElement {
|
|||||||
<div class="center">12 Hours</div>
|
<div class="center">12 Hours</div>
|
||||||
<div class="center">24 Hours</div>
|
<div class="center">24 Hours</div>
|
||||||
</div>
|
</div>
|
||||||
${Object.entries(translationMetadata.translations).map(
|
${Object.entries(translationMetadata.translations)
|
||||||
([key, value]) => html`
|
.filter(([key, _]) => key !== "test")
|
||||||
<div class="container">
|
.map(
|
||||||
<div>${value.nativeName}</div>
|
([key, value]) => html`
|
||||||
<div class="center">
|
<div class="container">
|
||||||
${formatTimeWithSeconds(
|
<div>${value.nativeName}</div>
|
||||||
this.date,
|
<div class="center">
|
||||||
{
|
${formatTimeWithSeconds(
|
||||||
...defaultLocale,
|
this.date,
|
||||||
language: key,
|
{
|
||||||
time_format: TimeFormat.language,
|
...defaultLocale,
|
||||||
},
|
language: key,
|
||||||
demoConfig
|
time_format: TimeFormat.language,
|
||||||
)}
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatTimeWithSeconds(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.am_pm,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatTimeWithSeconds(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.twenty_four,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="center">
|
`
|
||||||
${formatTimeWithSeconds(
|
)}
|
||||||
this.date,
|
|
||||||
{
|
|
||||||
...defaultLocale,
|
|
||||||
language: key,
|
|
||||||
time_format: TimeFormat.am_pm,
|
|
||||||
},
|
|
||||||
demoConfig
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="center">
|
|
||||||
${formatTimeWithSeconds(
|
|
||||||
this.date,
|
|
||||||
{
|
|
||||||
...defaultLocale,
|
|
||||||
language: key,
|
|
||||||
time_format: TimeFormat.twenty_four,
|
|
||||||
},
|
|
||||||
demoConfig
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</mwc-list>
|
</mwc-list>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -56,46 +56,48 @@ export class DemoDateTimeTimeWeekday extends LitElement {
|
|||||||
<div class="center">12 Hours</div>
|
<div class="center">12 Hours</div>
|
||||||
<div class="center">24 Hours</div>
|
<div class="center">24 Hours</div>
|
||||||
</div>
|
</div>
|
||||||
${Object.entries(translationMetadata.translations).map(
|
${Object.entries(translationMetadata.translations)
|
||||||
([key, value]) => html`
|
.filter(([key, _]) => key !== "test")
|
||||||
<div class="container">
|
.map(
|
||||||
<div>${value.nativeName}</div>
|
([key, value]) => html`
|
||||||
<div class="center">
|
<div class="container">
|
||||||
${formatTimeWeekday(
|
<div>${value.nativeName}</div>
|
||||||
this.date,
|
<div class="center">
|
||||||
{
|
${formatTimeWeekday(
|
||||||
...defaultLocale,
|
this.date,
|
||||||
language: key,
|
{
|
||||||
time_format: TimeFormat.language,
|
...defaultLocale,
|
||||||
},
|
language: key,
|
||||||
demoConfig
|
time_format: TimeFormat.language,
|
||||||
)}
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatTimeWeekday(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.am_pm,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatTimeWeekday(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.twenty_four,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="center">
|
`
|
||||||
${formatTimeWeekday(
|
)}
|
||||||
this.date,
|
|
||||||
{
|
|
||||||
...defaultLocale,
|
|
||||||
language: key,
|
|
||||||
time_format: TimeFormat.am_pm,
|
|
||||||
},
|
|
||||||
demoConfig
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="center">
|
|
||||||
${formatTimeWeekday(
|
|
||||||
this.date,
|
|
||||||
{
|
|
||||||
...defaultLocale,
|
|
||||||
language: key,
|
|
||||||
time_format: TimeFormat.twenty_four,
|
|
||||||
},
|
|
||||||
demoConfig
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</mwc-list>
|
</mwc-list>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -56,46 +56,48 @@ export class DemoDateTimeTime extends LitElement {
|
|||||||
<div class="center">12 Hours</div>
|
<div class="center">12 Hours</div>
|
||||||
<div class="center">24 Hours</div>
|
<div class="center">24 Hours</div>
|
||||||
</div>
|
</div>
|
||||||
${Object.entries(translationMetadata.translations).map(
|
${Object.entries(translationMetadata.translations)
|
||||||
([key, value]) => html`
|
.filter(([key, _]) => key !== "test")
|
||||||
<div class="container">
|
.map(
|
||||||
<div>${value.nativeName}</div>
|
([key, value]) => html`
|
||||||
<div class="center">
|
<div class="container">
|
||||||
${formatTime(
|
<div>${value.nativeName}</div>
|
||||||
this.date,
|
<div class="center">
|
||||||
{
|
${formatTime(
|
||||||
...defaultLocale,
|
this.date,
|
||||||
language: key,
|
{
|
||||||
time_format: TimeFormat.language,
|
...defaultLocale,
|
||||||
},
|
language: key,
|
||||||
demoConfig
|
time_format: TimeFormat.language,
|
||||||
)}
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatTime(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.am_pm,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
${formatTime(
|
||||||
|
this.date,
|
||||||
|
{
|
||||||
|
...defaultLocale,
|
||||||
|
language: key,
|
||||||
|
time_format: TimeFormat.twenty_four,
|
||||||
|
},
|
||||||
|
demoConfig
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="center">
|
`
|
||||||
${formatTime(
|
)}
|
||||||
this.date,
|
|
||||||
{
|
|
||||||
...defaultLocale,
|
|
||||||
language: key,
|
|
||||||
time_format: TimeFormat.am_pm,
|
|
||||||
},
|
|
||||||
demoConfig
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="center">
|
|
||||||
${formatTime(
|
|
||||||
this.date,
|
|
||||||
{
|
|
||||||
...defaultLocale,
|
|
||||||
language: key,
|
|
||||||
time_format: TimeFormat.twenty_four,
|
|
||||||
},
|
|
||||||
demoConfig
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</mwc-list>
|
</mwc-list>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,6 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
|||||||
import { customElement, query } from "lit/decorators";
|
import { customElement, query } from "lit/decorators";
|
||||||
import { CoverEntityFeature } from "../../../../src/data/cover";
|
import { CoverEntityFeature } from "../../../../src/data/cover";
|
||||||
import { LightColorMode } from "../../../../src/data/light";
|
import { LightColorMode } from "../../../../src/data/light";
|
||||||
import { LockEntityFeature } from "../../../../src/data/lock";
|
|
||||||
import { VacuumEntityFeature } from "../../../../src/data/vacuum";
|
import { VacuumEntityFeature } from "../../../../src/data/vacuum";
|
||||||
import { getEntity } from "../../../../src/fake_data/entity";
|
import { getEntity } from "../../../../src/fake_data/entity";
|
||||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||||
@@ -21,11 +20,6 @@ const ENTITIES = [
|
|||||||
getEntity("light", "unavailable", "unavailable", {
|
getEntity("light", "unavailable", "unavailable", {
|
||||||
friendly_name: "Unavailable entity",
|
friendly_name: "Unavailable entity",
|
||||||
}),
|
}),
|
||||||
getEntity("lock", "front_door", "locked", {
|
|
||||||
friendly_name: "Front Door Lock",
|
|
||||||
device_class: "lock",
|
|
||||||
supported_features: LockEntityFeature.OPEN,
|
|
||||||
}),
|
|
||||||
getEntity("climate", "thermostat", "heat", {
|
getEntity("climate", "thermostat", "heat", {
|
||||||
current_temperature: 73,
|
current_temperature: 73,
|
||||||
min_temp: 45,
|
min_temp: 45,
|
||||||
@@ -144,24 +138,6 @@ const CONFIGS = [
|
|||||||
- type: "color-temp"
|
- type: "color-temp"
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
heading: "Lock commands feature",
|
|
||||||
config: `
|
|
||||||
- type: tile
|
|
||||||
entity: lock.front_door
|
|
||||||
features:
|
|
||||||
- type: "lock-commands"
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
heading: "Lock open door feature",
|
|
||||||
config: `
|
|
||||||
- type: tile
|
|
||||||
entity: lock.front_door
|
|
||||||
features:
|
|
||||||
- type: "lock-open-door"
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
heading: "Vacuum commands feature",
|
heading: "Vacuum commands feature",
|
||||||
config: `
|
config: `
|
||||||
|
@@ -368,7 +368,6 @@ export class DemoEntityState extends LitElement {
|
|||||||
hass.localize,
|
hass.localize,
|
||||||
entry.stateObj,
|
entry.stateObj,
|
||||||
hass.locale,
|
hass.locale,
|
||||||
[], // numericDeviceClasses
|
|
||||||
hass.config,
|
hass.config,
|
||||||
hass.entities
|
hass.entities
|
||||||
)}`,
|
)}`,
|
||||||
|
@@ -36,8 +36,6 @@ const createConfigEntry = (
|
|||||||
pref_disable_new_entities: false,
|
pref_disable_new_entities: false,
|
||||||
pref_disable_polling: false,
|
pref_disable_polling: false,
|
||||||
reason: null,
|
reason: null,
|
||||||
error_reason_translation_key: null,
|
|
||||||
error_reason_translation_placeholders: null,
|
|
||||||
...override,
|
...override,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,7 +1,4 @@
|
|||||||
import { globIterate } from "glob";
|
import { globIterate } from "glob";
|
||||||
import { availableParallelism } from "node:os";
|
|
||||||
|
|
||||||
process.env.UV_THREADPOOL_SIZE = availableParallelism();
|
|
||||||
|
|
||||||
const gulpImports = [];
|
const gulpImports = [];
|
||||||
|
|
||||||
|
@@ -1,19 +1,19 @@
|
|||||||
import { mdiRefresh, mdiStorePlus } from "@mdi/js";
|
import { mdiStorePlus, mdiUpdate } from "@mdi/js";
|
||||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { atLeastVersion } from "../../../src/common/config/version";
|
import { atLeastVersion } from "../../../src/common/config/version";
|
||||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
|
||||||
import "../../../src/components/ha-fab";
|
import "../../../src/components/ha-fab";
|
||||||
import { reloadHassioAddons } from "../../../src/data/hassio/addon";
|
|
||||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
|
||||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
|
||||||
import "../../../src/layouts/hass-subpage";
|
|
||||||
import "../../../src/layouts/hass-tabs-subpage";
|
import "../../../src/layouts/hass-tabs-subpage";
|
||||||
import { haStyle } from "../../../src/resources/styles";
|
import { haStyle } from "../../../src/resources/styles";
|
||||||
import { HomeAssistant, Route } from "../../../src/types";
|
import { HomeAssistant, Route } from "../../../src/types";
|
||||||
import { supervisorTabs } from "../hassio-tabs";
|
import { supervisorTabs } from "../hassio-tabs";
|
||||||
import "./hassio-addons";
|
import "./hassio-addons";
|
||||||
|
import "../../../src/layouts/hass-subpage";
|
||||||
|
import { reloadHassioAddons } from "../../../src/data/hassio/addon";
|
||||||
|
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||||
|
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||||
|
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||||
|
|
||||||
@customElement("hassio-dashboard")
|
@customElement("hassio-dashboard")
|
||||||
class HassioDashboard extends LitElement {
|
class HassioDashboard extends LitElement {
|
||||||
@@ -43,7 +43,7 @@ class HassioDashboard extends LitElement {
|
|||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
slot="toolbar-icon"
|
slot="toolbar-icon"
|
||||||
@click=${this._handleCheckUpdates}
|
@click=${this._handleCheckUpdates}
|
||||||
.path=${mdiRefresh}
|
.path=${mdiUpdate}
|
||||||
.label=${this.supervisor.localize("store.check_updates")}
|
.label=${this.supervisor.localize("store.check_updates")}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
<hassio-addons
|
<hassio-addons
|
||||||
|
151
package.json
151
package.json
@@ -25,24 +25,24 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.24.7",
|
"@babel/runtime": "7.24.1",
|
||||||
"@braintree/sanitize-url": "7.0.2",
|
"@braintree/sanitize-url": "7.0.1",
|
||||||
"@codemirror/autocomplete": "6.16.2",
|
"@codemirror/autocomplete": "6.15.0",
|
||||||
"@codemirror/commands": "6.6.0",
|
"@codemirror/commands": "6.3.3",
|
||||||
"@codemirror/language": "6.10.2",
|
"@codemirror/language": "6.10.1",
|
||||||
"@codemirror/legacy-modes": "6.4.0",
|
"@codemirror/legacy-modes": "6.3.3",
|
||||||
"@codemirror/search": "6.5.6",
|
"@codemirror/search": "6.5.6",
|
||||||
"@codemirror/state": "6.4.1",
|
"@codemirror/state": "6.4.1",
|
||||||
"@codemirror/view": "6.27.0",
|
"@codemirror/view": "6.26.1",
|
||||||
"@egjs/hammerjs": "2.0.17",
|
"@egjs/hammerjs": "2.0.17",
|
||||||
"@formatjs/intl-datetimeformat": "6.12.5",
|
"@formatjs/intl-datetimeformat": "6.12.3",
|
||||||
"@formatjs/intl-displaynames": "6.6.8",
|
"@formatjs/intl-displaynames": "6.6.6",
|
||||||
"@formatjs/intl-getcanonicallocales": "2.3.0",
|
"@formatjs/intl-getcanonicallocales": "2.3.0",
|
||||||
"@formatjs/intl-listformat": "7.5.7",
|
"@formatjs/intl-listformat": "7.5.5",
|
||||||
"@formatjs/intl-locale": "4.0.0",
|
"@formatjs/intl-locale": "3.4.5",
|
||||||
"@formatjs/intl-numberformat": "8.10.3",
|
"@formatjs/intl-numberformat": "8.10.1",
|
||||||
"@formatjs/intl-pluralrules": "5.2.14",
|
"@formatjs/intl-pluralrules": "5.2.12",
|
||||||
"@formatjs/intl-relativetimeformat": "11.2.14",
|
"@formatjs/intl-relativetimeformat": "11.2.12",
|
||||||
"@fullcalendar/core": "6.1.11",
|
"@fullcalendar/core": "6.1.11",
|
||||||
"@fullcalendar/daygrid": "6.1.11",
|
"@fullcalendar/daygrid": "6.1.11",
|
||||||
"@fullcalendar/interaction": "6.1.11",
|
"@fullcalendar/interaction": "6.1.11",
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
"@lit-labs/context": "0.4.1",
|
"@lit-labs/context": "0.4.1",
|
||||||
"@lit-labs/motion": "1.0.7",
|
"@lit-labs/motion": "1.0.7",
|
||||||
"@lit-labs/observers": "2.0.2",
|
"@lit-labs/observers": "2.0.2",
|
||||||
"@lit-labs/virtualizer": "2.0.13",
|
"@lit-labs/virtualizer": "2.0.12",
|
||||||
"@lrnwebcomponents/simple-tooltip": "8.0.2",
|
"@lrnwebcomponents/simple-tooltip": "8.0.2",
|
||||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||||
@@ -70,6 +70,7 @@
|
|||||||
"@material/mwc-list": "0.27.0",
|
"@material/mwc-list": "0.27.0",
|
||||||
"@material/mwc-menu": "0.27.0",
|
"@material/mwc-menu": "0.27.0",
|
||||||
"@material/mwc-radio": "0.27.0",
|
"@material/mwc-radio": "0.27.0",
|
||||||
|
"@material/mwc-ripple": "0.27.0",
|
||||||
"@material/mwc-select": "0.27.0",
|
"@material/mwc-select": "0.27.0",
|
||||||
"@material/mwc-snackbar": "0.27.0",
|
"@material/mwc-snackbar": "0.27.0",
|
||||||
"@material/mwc-switch": "0.27.0",
|
"@material/mwc-switch": "0.27.0",
|
||||||
@@ -80,7 +81,7 @@
|
|||||||
"@material/mwc-top-app-bar": "0.27.0",
|
"@material/mwc-top-app-bar": "0.27.0",
|
||||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||||
"@material/web": "1.5.0",
|
"@material/web": "=1.3.0",
|
||||||
"@mdi/js": "7.4.47",
|
"@mdi/js": "7.4.47",
|
||||||
"@mdi/svg": "7.4.47",
|
"@mdi/svg": "7.4.47",
|
||||||
"@polymer/paper-item": "3.0.1",
|
"@polymer/paper-item": "3.0.1",
|
||||||
@@ -88,8 +89,8 @@
|
|||||||
"@polymer/paper-tabs": "3.1.0",
|
"@polymer/paper-tabs": "3.1.0",
|
||||||
"@polymer/polymer": "3.5.1",
|
"@polymer/polymer": "3.5.1",
|
||||||
"@thomasloven/round-slider": "0.6.0",
|
"@thomasloven/round-slider": "0.6.0",
|
||||||
"@vaadin/combo-box": "24.3.13",
|
"@vaadin/combo-box": "24.3.10",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.3.13",
|
"@vaadin/vaadin-themable-mixin": "24.3.10",
|
||||||
"@vibrant/color": "3.2.1-alpha.1",
|
"@vibrant/color": "3.2.1-alpha.1",
|
||||||
"@vibrant/core": "3.2.1-alpha.1",
|
"@vibrant/core": "3.2.1-alpha.1",
|
||||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||||
@@ -97,28 +98,28 @@
|
|||||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||||
"app-datepicker": "5.1.1",
|
"app-datepicker": "5.1.1",
|
||||||
"chart.js": "4.4.3",
|
"chart.js": "4.4.2",
|
||||||
"color-name": "2.0.0",
|
"color-name": "2.0.0",
|
||||||
"comlink": "4.4.1",
|
"comlink": "4.4.1",
|
||||||
"core-js": "3.37.1",
|
"core-js": "3.36.1",
|
||||||
"cropperjs": "1.6.2",
|
"cropperjs": "1.6.1",
|
||||||
"date-fns": "3.6.0",
|
"date-fns": "2.30.0",
|
||||||
"date-fns-tz": "3.1.3",
|
"date-fns-tz": "2.0.1",
|
||||||
"deep-clone-simple": "1.1.1",
|
"deep-clone-simple": "1.1.1",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
"element-internals-polyfill": "1.3.11",
|
"element-internals-polyfill": "1.3.10",
|
||||||
"fuse.js": "7.0.0",
|
"fuse.js": "7.0.0",
|
||||||
"google-timezones-json": "1.2.0",
|
"google-timezones-json": "1.2.0",
|
||||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||||
"home-assistant-js-websocket": "9.3.0",
|
"home-assistant-js-websocket": "9.2.1",
|
||||||
"idb-keyval": "6.2.1",
|
"idb-keyval": "6.2.1",
|
||||||
"intl-messageformat": "10.5.14",
|
"intl-messageformat": "10.5.11",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"leaflet": "1.9.4",
|
"leaflet": "1.9.4",
|
||||||
"leaflet-draw": "1.0.4",
|
"leaflet-draw": "1.0.4",
|
||||||
"lit": "2.8.0",
|
"lit": "2.8.0",
|
||||||
"luxon": "3.4.4",
|
"luxon": "3.4.4",
|
||||||
"marked": "12.0.2",
|
"marked": "12.0.1",
|
||||||
"memoize-one": "6.0.0",
|
"memoize-one": "6.0.0",
|
||||||
"node-vibrant": "3.2.1-alpha.1",
|
"node-vibrant": "3.2.1-alpha.1",
|
||||||
"proxy-polyfill": "0.3.2",
|
"proxy-polyfill": "0.3.2",
|
||||||
@@ -133,118 +134,121 @@
|
|||||||
"tinykeys": "2.1.0",
|
"tinykeys": "2.1.0",
|
||||||
"tsparticles-engine": "2.12.0",
|
"tsparticles-engine": "2.12.0",
|
||||||
"tsparticles-preset-links": "2.12.0",
|
"tsparticles-preset-links": "2.12.0",
|
||||||
"ua-parser-js": "1.0.38",
|
"ua-parser-js": "1.0.37",
|
||||||
"unfetch": "5.0.0",
|
"unfetch": "5.0.0",
|
||||||
"vis-data": "7.1.9",
|
"vis-data": "7.1.9",
|
||||||
"vis-network": "9.1.9",
|
"vis-network": "9.1.9",
|
||||||
"vue": "2.7.16",
|
"vue": "2.7.16",
|
||||||
"vue2-daterange-picker": "0.6.8",
|
"vue2-daterange-picker": "0.6.8",
|
||||||
"weekstart": "2.0.0",
|
"weekstart": "2.0.0",
|
||||||
"workbox-cacheable-response": "7.1.0",
|
"workbox-cacheable-response": "7.0.0",
|
||||||
"workbox-core": "7.1.0",
|
"workbox-core": "7.0.0",
|
||||||
"workbox-expiration": "7.1.0",
|
"workbox-expiration": "7.0.0",
|
||||||
"workbox-precaching": "7.1.0",
|
"workbox-precaching": "7.0.0",
|
||||||
"workbox-routing": "7.1.0",
|
"workbox-routing": "7.0.0",
|
||||||
"workbox-strategies": "7.1.0",
|
"workbox-strategies": "7.0.0",
|
||||||
"xss": "1.0.15"
|
"xss": "1.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.24.7",
|
"@babel/core": "7.24.3",
|
||||||
"@babel/helper-define-polyfill-provider": "0.6.2",
|
"@babel/helper-define-polyfill-provider": "0.6.1",
|
||||||
"@babel/plugin-proposal-decorators": "7.24.7",
|
"@babel/plugin-proposal-decorators": "7.24.1",
|
||||||
"@babel/plugin-transform-runtime": "7.24.7",
|
"@babel/plugin-transform-runtime": "7.24.3",
|
||||||
"@babel/preset-env": "7.24.7",
|
"@babel/preset-env": "7.24.3",
|
||||||
"@babel/preset-typescript": "7.24.7",
|
"@babel/preset-typescript": "7.24.1",
|
||||||
"@bundle-stats/plugin-webpack-filter": "4.13.2",
|
"@bundle-stats/plugin-webpack-filter": "4.12.2",
|
||||||
"@koa/cors": "5.0.0",
|
"@koa/cors": "5.0.0",
|
||||||
"@lokalise/node-api": "12.5.0",
|
"@lokalise/node-api": "12.3.0",
|
||||||
"@octokit/auth-oauth-device": "7.1.1",
|
"@octokit/auth-oauth-device": "7.0.1",
|
||||||
"@octokit/plugin-retry": "7.1.1",
|
"@octokit/plugin-retry": "7.0.3",
|
||||||
"@octokit/rest": "20.1.1",
|
"@octokit/rest": "20.0.2",
|
||||||
"@open-wc/dev-server-hmr": "0.1.4",
|
"@open-wc/dev-server-hmr": "0.1.4",
|
||||||
"@rollup/plugin-babel": "6.0.4",
|
"@rollup/plugin-babel": "6.0.4",
|
||||||
"@rollup/plugin-commonjs": "25.0.8",
|
"@rollup/plugin-commonjs": "25.0.7",
|
||||||
"@rollup/plugin-json": "6.1.0",
|
"@rollup/plugin-json": "6.1.0",
|
||||||
"@rollup/plugin-node-resolve": "15.2.3",
|
"@rollup/plugin-node-resolve": "15.2.3",
|
||||||
"@rollup/plugin-replace": "5.0.7",
|
"@rollup/plugin-replace": "5.0.5",
|
||||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||||
"@types/chromecast-caf-receiver": "6.0.14",
|
"@types/chromecast-caf-receiver": "6.0.13",
|
||||||
"@types/chromecast-caf-sender": "1.0.10",
|
"@types/chromecast-caf-sender": "1.0.9",
|
||||||
"@types/color-name": "1.1.4",
|
"@types/color-name": "1.1.3",
|
||||||
"@types/glob": "8.1.0",
|
"@types/glob": "8.1.0",
|
||||||
"@types/html-minifier-terser": "7.0.2",
|
"@types/html-minifier-terser": "7.0.2",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/leaflet": "1.9.12",
|
"@types/leaflet": "1.9.8",
|
||||||
"@types/leaflet-draw": "1.0.11",
|
"@types/leaflet-draw": "1.0.11",
|
||||||
"@types/lodash.merge": "4.6.9",
|
|
||||||
"@types/luxon": "3.4.2",
|
"@types/luxon": "3.4.2",
|
||||||
"@types/mocha": "10.0.6",
|
"@types/mocha": "10.0.6",
|
||||||
"@types/qrcode": "1.5.5",
|
"@types/qrcode": "1.5.5",
|
||||||
"@types/serve-handler": "6.1.4",
|
"@types/serve-handler": "6.1.4",
|
||||||
"@types/sortablejs": "1.15.8",
|
"@types/sortablejs": "1.15.8",
|
||||||
"@types/tar": "6.1.13",
|
"@types/tar": "6.1.11",
|
||||||
"@types/ua-parser-js": "0.7.39",
|
"@types/ua-parser-js": "0.7.39",
|
||||||
"@types/webspeechapi": "0.0.29",
|
"@types/webspeechapi": "0.0.29",
|
||||||
"@typescript-eslint/eslint-plugin": "7.12.0",
|
"@typescript-eslint/eslint-plugin": "7.4.0",
|
||||||
"@typescript-eslint/parser": "7.12.0",
|
"@typescript-eslint/parser": "7.4.0",
|
||||||
"@web/dev-server": "0.1.38",
|
"@web/dev-server": "0.1.38",
|
||||||
"@web/dev-server-rollup": "0.4.1",
|
"@web/dev-server-rollup": "0.4.1",
|
||||||
"babel-loader": "9.1.3",
|
"babel-loader": "9.1.3",
|
||||||
"babel-plugin-template-html-minifier": "4.1.0",
|
"babel-plugin-template-html-minifier": "4.1.0",
|
||||||
"chai": "5.1.1",
|
"chai": "5.1.0",
|
||||||
"del": "7.1.0",
|
"del": "7.1.0",
|
||||||
"eslint": "8.57.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"eslint-config-airbnb-base": "15.0.0",
|
||||||
"eslint-config-airbnb-typescript": "18.0.0",
|
"eslint-config-airbnb-typescript": "18.0.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
"eslint-import-resolver-webpack": "0.13.8",
|
"eslint-import-resolver-webpack": "0.13.8",
|
||||||
|
"eslint-plugin-disable": "2.0.3",
|
||||||
"eslint-plugin-import": "2.29.1",
|
"eslint-plugin-import": "2.29.1",
|
||||||
"eslint-plugin-lit": "1.14.0",
|
"eslint-plugin-lit": "1.11.0",
|
||||||
"eslint-plugin-lit-a11y": "4.1.2",
|
"eslint-plugin-lit-a11y": "4.1.2",
|
||||||
"eslint-plugin-unused-imports": "4.0.0",
|
"eslint-plugin-unused-imports": "3.1.0",
|
||||||
"eslint-plugin-wc": "2.1.0",
|
"eslint-plugin-wc": "2.0.4",
|
||||||
"fancy-log": "2.0.0",
|
"fancy-log": "2.0.0",
|
||||||
"fs-extra": "11.2.0",
|
"fs-extra": "11.2.0",
|
||||||
"glob": "10.4.1",
|
"glob": "10.3.10",
|
||||||
"gulp": "5.0.0",
|
"gulp": "4.0.2",
|
||||||
|
"gulp-flatmap": "1.0.2",
|
||||||
"gulp-json-transform": "0.5.0",
|
"gulp-json-transform": "0.5.0",
|
||||||
|
"gulp-merge-json": "2.2.1",
|
||||||
"gulp-rename": "2.0.0",
|
"gulp-rename": "2.0.0",
|
||||||
"gulp-zopfli-green": "6.0.1",
|
"gulp-zopfli-green": "6.0.1",
|
||||||
"html-minifier-terser": "7.2.0",
|
"html-minifier-terser": "7.2.0",
|
||||||
"husky": "9.0.11",
|
"husky": "9.0.11",
|
||||||
"instant-mocha": "1.5.2",
|
"instant-mocha": "1.5.2",
|
||||||
"jszip": "3.10.1",
|
"jszip": "3.10.1",
|
||||||
"lint-staged": "15.2.5",
|
"lint-staged": "15.2.2",
|
||||||
"lit-analyzer": "2.0.3",
|
"lit-analyzer": "2.0.3",
|
||||||
"lodash.merge": "4.6.2",
|
|
||||||
"lodash.template": "4.5.0",
|
"lodash.template": "4.5.0",
|
||||||
"magic-string": "0.30.10",
|
"magic-string": "0.30.8",
|
||||||
"map-stream": "0.0.7",
|
"map-stream": "0.0.7",
|
||||||
"mocha": "10.4.0",
|
"mocha": "10.3.0",
|
||||||
"object-hash": "3.0.0",
|
"object-hash": "3.0.0",
|
||||||
"open": "10.1.0",
|
"open": "10.1.0",
|
||||||
"pinst": "3.0.0",
|
"pinst": "3.0.0",
|
||||||
"prettier": "3.3.1",
|
"prettier": "3.2.5",
|
||||||
"rollup": "2.79.1",
|
"rollup": "2.79.1",
|
||||||
"rollup-plugin-string": "3.0.0",
|
"rollup-plugin-string": "3.0.0",
|
||||||
"rollup-plugin-terser": "7.0.2",
|
"rollup-plugin-terser": "7.0.2",
|
||||||
"rollup-plugin-visualizer": "5.12.0",
|
"rollup-plugin-visualizer": "5.12.0",
|
||||||
"serve-handler": "6.1.5",
|
"serve-handler": "6.1.5",
|
||||||
"sinon": "18.0.0",
|
"sinon": "17.0.1",
|
||||||
"source-map-url": "0.4.1",
|
"source-map-url": "0.4.1",
|
||||||
"systemjs": "6.15.1",
|
"systemjs": "6.14.3",
|
||||||
"tar": "7.2.0",
|
"tar": "6.2.1",
|
||||||
"terser-webpack-plugin": "5.3.10",
|
"terser-webpack-plugin": "5.3.10",
|
||||||
"transform-async-modules-webpack-plugin": "1.1.1",
|
"transform-async-modules-webpack-plugin": "1.0.4",
|
||||||
"ts-lit-plugin": "2.0.2",
|
"ts-lit-plugin": "2.0.2",
|
||||||
"typescript": "5.4.5",
|
"typescript": "5.4.3",
|
||||||
|
"vinyl-buffer": "1.0.1",
|
||||||
|
"vinyl-source-stream": "2.0.0",
|
||||||
"webpack": "5.91.0",
|
"webpack": "5.91.0",
|
||||||
"webpack-cli": "5.1.4",
|
"webpack-cli": "5.1.4",
|
||||||
"webpack-dev-server": "5.0.4",
|
"webpack-dev-server": "5.0.4",
|
||||||
"webpack-manifest-plugin": "5.0.0",
|
"webpack-manifest-plugin": "5.0.0",
|
||||||
"webpack-stats-plugin": "1.1.3",
|
"webpack-stats-plugin": "1.1.3",
|
||||||
"webpackbar": "6.0.1",
|
"webpackbar": "6.0.1",
|
||||||
"workbox-build": "7.1.1"
|
"workbox-build": "7.0.0"
|
||||||
},
|
},
|
||||||
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
|
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
@@ -253,9 +257,8 @@
|
|||||||
"lit": "2.8.0",
|
"lit": "2.8.0",
|
||||||
"clean-css": "5.3.3",
|
"clean-css": "5.3.3",
|
||||||
"@lit/reactive-element": "1.6.3",
|
"@lit/reactive-element": "1.6.3",
|
||||||
"@fullcalendar/daygrid": "6.1.11",
|
|
||||||
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
|
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
|
||||||
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.2.2"
|
"packageManager": "yarn@4.1.1"
|
||||||
}
|
}
|
||||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "home-assistant-frontend"
|
name = "home-assistant-frontend"
|
||||||
version = "20240610.1"
|
version = "20240402.0"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "The Home Assistant frontend"
|
description = "The Home Assistant frontend"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@@ -40,11 +40,6 @@
|
|||||||
"matchPackageNames": ["tsparticles-engine"],
|
"matchPackageNames": ["tsparticles-engine"],
|
||||||
"matchPackagePrefixes": ["tsparticles-preset-"]
|
"matchPackagePrefixes": ["tsparticles-preset-"]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"description": "Group date-fns with dependent timezone package",
|
|
||||||
"groupName": "date-fns",
|
|
||||||
"matchPackageNames": ["date-fns", "date-fns-tz"]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"description": "Group and temporarily disable WDS packages",
|
"description": "Group and temporarily disable WDS packages",
|
||||||
"groupName": "Web Dev Server",
|
"groupName": "Web Dev Server",
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { toZonedTime, fromZonedTime } from "date-fns-tz";
|
import { utcToZonedTime, zonedTimeToUtc } from "date-fns-tz";
|
||||||
import { HassConfig } from "home-assistant-js-websocket";
|
import { HassConfig } from "home-assistant-js-websocket";
|
||||||
import { FrontendLocaleData, TimeZone } from "../../data/translation";
|
import { FrontendLocaleData, TimeZone } from "../../data/translation";
|
||||||
|
|
||||||
@@ -8,10 +8,10 @@ const calcZonedDate = (
|
|||||||
fn: (date: Date, options?: any) => Date | number | boolean,
|
fn: (date: Date, options?: any) => Date | number | boolean,
|
||||||
options?
|
options?
|
||||||
) => {
|
) => {
|
||||||
const inputZoned = toZonedTime(date, tz);
|
const inputZoned = utcToZonedTime(date, tz);
|
||||||
const fnZoned = fn(inputZoned, options);
|
const fnZoned = fn(inputZoned, options);
|
||||||
if (fnZoned instanceof Date) {
|
if (fnZoned instanceof Date) {
|
||||||
return fromZonedTime(fnZoned, tz) as Date;
|
return zonedTimeToUtc(fnZoned, tz) as Date;
|
||||||
}
|
}
|
||||||
return fnZoned;
|
return fnZoned;
|
||||||
};
|
};
|
||||||
@@ -51,6 +51,6 @@ export const calcDateDifferenceProperty = (
|
|||||||
locale,
|
locale,
|
||||||
config,
|
config,
|
||||||
locale.time_zone === TimeZone.server
|
locale.time_zone === TimeZone.server
|
||||||
? toZonedTime(startDate, config.time_zone)
|
? utcToZonedTime(startDate, config.time_zone)
|
||||||
: startDate
|
: startDate
|
||||||
);
|
);
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import { getWeekStartByLocale } from "weekstart";
|
import { getWeekStartByLocale } from "weekstart";
|
||||||
import { FrontendLocaleData, FirstWeekday } from "../../data/translation";
|
import { FrontendLocaleData, FirstWeekday } from "../../data/translation";
|
||||||
|
|
||||||
|
import "../../resources/intl-polyfill";
|
||||||
|
|
||||||
export const weekdays = [
|
export const weekdays = [
|
||||||
"sunday",
|
"sunday",
|
||||||
"monday",
|
"monday",
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { HassConfig } from "home-assistant-js-websocket";
|
import { HassConfig } from "home-assistant-js-websocket";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { DateFormat, FrontendLocaleData } from "../../data/translation";
|
import { DateFormat, FrontendLocaleData } from "../../data/translation";
|
||||||
|
import "../../resources/intl-polyfill";
|
||||||
import { resolveTimeZone } from "./resolve-time-zone";
|
import { resolveTimeZone } from "./resolve-time-zone";
|
||||||
|
|
||||||
// Tuesday, August 10
|
// Tuesday, August 10
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { HassConfig } from "home-assistant-js-websocket";
|
import { HassConfig } from "home-assistant-js-websocket";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
import { FrontendLocaleData } from "../../data/translation";
|
||||||
|
import "../../resources/intl-polyfill";
|
||||||
import { formatDateNumeric } from "./format_date";
|
import { formatDateNumeric } from "./format_date";
|
||||||
import { formatTime } from "./format_time";
|
import { formatTime } from "./format_time";
|
||||||
import { resolveTimeZone } from "./resolve-time-zone";
|
import { resolveTimeZone } from "./resolve-time-zone";
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { HaDurationData } from "../../components/ha-duration-input";
|
import { HaDurationData } from "../../components/ha-duration-input";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
import { FrontendLocaleData } from "../../data/translation";
|
||||||
|
import "../../resources/intl-polyfill";
|
||||||
|
|
||||||
const leftPad = (num: number) => (num < 10 ? `0${num}` : num);
|
const leftPad = (num: number) => (num < 10 ? `0${num}` : num);
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { HassConfig } from "home-assistant-js-websocket";
|
import { HassConfig } from "home-assistant-js-websocket";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
import { FrontendLocaleData } from "../../data/translation";
|
||||||
|
import "../../resources/intl-polyfill";
|
||||||
import { resolveTimeZone } from "./resolve-time-zone";
|
import { resolveTimeZone } from "./resolve-time-zone";
|
||||||
import { useAmPm } from "./use_am_pm";
|
import { useAmPm } from "./use_am_pm";
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import "../../resources/intl-polyfill";
|
||||||
|
|
||||||
export const localizeWeekdays = memoizeOne(
|
export const localizeWeekdays = memoizeOne(
|
||||||
(language: string, short: boolean): string[] => {
|
(language: string, short: boolean): string[] => {
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
import { FrontendLocaleData } from "../../data/translation";
|
||||||
|
import "../../resources/intl-polyfill";
|
||||||
import { selectUnit } from "../util/select-unit";
|
import { selectUnit } from "../util/select-unit";
|
||||||
|
|
||||||
const formatRelTimeMem = memoizeOne(
|
const formatRelTimeMem = memoizeOne(
|
||||||
|
@@ -108,8 +108,6 @@ export const storage =
|
|||||||
subscribe?: boolean;
|
subscribe?: boolean;
|
||||||
state?: boolean;
|
state?: boolean;
|
||||||
stateOptions?: InternalPropertyDeclaration;
|
stateOptions?: InternalPropertyDeclaration;
|
||||||
serializer?: (value: any) => any;
|
|
||||||
deserializer?: (value: any) => any;
|
|
||||||
}): any =>
|
}): any =>
|
||||||
(clsElement: ClassElement) => {
|
(clsElement: ClassElement) => {
|
||||||
const storageName = options.storage || "localStorage";
|
const storageName = options.storage || "localStorage";
|
||||||
@@ -143,9 +141,7 @@ export const storage =
|
|||||||
|
|
||||||
const getValue = (): any =>
|
const getValue = (): any =>
|
||||||
storageInstance.hasKey(storageKey!)
|
storageInstance.hasKey(storageKey!)
|
||||||
? options.deserializer
|
? storageInstance.getValue(storageKey!)
|
||||||
? options.deserializer(storageInstance.getValue(storageKey!))
|
|
||||||
: storageInstance.getValue(storageKey!)
|
|
||||||
: initVal;
|
: initVal;
|
||||||
|
|
||||||
const setValue = (el: ReactiveElement, value: any) => {
|
const setValue = (el: ReactiveElement, value: any) => {
|
||||||
@@ -153,10 +149,7 @@ export const storage =
|
|||||||
if (options.state) {
|
if (options.state) {
|
||||||
oldValue = getValue();
|
oldValue = getValue();
|
||||||
}
|
}
|
||||||
storageInstance.setValue(
|
storageInstance.setValue(storageKey!, value);
|
||||||
storageKey!,
|
|
||||||
options.serializer ? options.serializer(value) : value
|
|
||||||
);
|
|
||||||
if (options.state) {
|
if (options.state) {
|
||||||
el.requestUpdate(clsElement.key, oldValue);
|
el.requestUpdate(clsElement.key, oldValue);
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
export type MediaQueriesListener = () => void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach a media query. Listener is called right away and when it matches.
|
* Attach a media query. Listener is called right away and when it matches.
|
||||||
* @param mediaQuery media query to match.
|
* @param mediaQuery media query to match.
|
||||||
@@ -9,7 +7,7 @@ export type MediaQueriesListener = () => void;
|
|||||||
export const listenMediaQuery = (
|
export const listenMediaQuery = (
|
||||||
mediaQuery: string,
|
mediaQuery: string,
|
||||||
matchesChanged: (matches: boolean) => void
|
matchesChanged: (matches: boolean) => void
|
||||||
): MediaQueriesListener => {
|
) => {
|
||||||
const mql = matchMedia(mediaQuery);
|
const mql = matchMedia(mediaQuery);
|
||||||
const listener = (e) => matchesChanged(e.matches);
|
const listener = (e) => matchesChanged(e.matches);
|
||||||
mql.addListener(listener);
|
mql.addListener(listener);
|
||||||
|
@@ -19,11 +19,28 @@ import { blankBeforeUnit } from "../translations/blank_before_unit";
|
|||||||
import { LocalizeFunc } from "../translations/localize";
|
import { LocalizeFunc } from "../translations/localize";
|
||||||
import { computeDomain } from "./compute_domain";
|
import { computeDomain } from "./compute_domain";
|
||||||
|
|
||||||
|
export const computeStateDisplaySingleEntity = (
|
||||||
|
localize: LocalizeFunc,
|
||||||
|
stateObj: HassEntity,
|
||||||
|
locale: FrontendLocaleData,
|
||||||
|
config: HassConfig,
|
||||||
|
entity: EntityRegistryDisplayEntry | undefined,
|
||||||
|
state?: string
|
||||||
|
): string =>
|
||||||
|
computeStateDisplayFromEntityAttributes(
|
||||||
|
localize,
|
||||||
|
locale,
|
||||||
|
config,
|
||||||
|
entity,
|
||||||
|
stateObj.entity_id,
|
||||||
|
stateObj.attributes,
|
||||||
|
state !== undefined ? state : stateObj.state
|
||||||
|
);
|
||||||
|
|
||||||
export const computeStateDisplay = (
|
export const computeStateDisplay = (
|
||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
stateObj: HassEntity,
|
stateObj: HassEntity,
|
||||||
locale: FrontendLocaleData,
|
locale: FrontendLocaleData,
|
||||||
sensorNumericDeviceClasses: string[],
|
|
||||||
config: HassConfig,
|
config: HassConfig,
|
||||||
entities: HomeAssistant["entities"],
|
entities: HomeAssistant["entities"],
|
||||||
state?: string
|
state?: string
|
||||||
@@ -35,7 +52,6 @@ export const computeStateDisplay = (
|
|||||||
return computeStateDisplayFromEntityAttributes(
|
return computeStateDisplayFromEntityAttributes(
|
||||||
localize,
|
localize,
|
||||||
locale,
|
locale,
|
||||||
sensorNumericDeviceClasses,
|
|
||||||
config,
|
config,
|
||||||
entity,
|
entity,
|
||||||
stateObj.entity_id,
|
stateObj.entity_id,
|
||||||
@@ -47,7 +63,6 @@ export const computeStateDisplay = (
|
|||||||
export const computeStateDisplayFromEntityAttributes = (
|
export const computeStateDisplayFromEntityAttributes = (
|
||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
locale: FrontendLocaleData,
|
locale: FrontendLocaleData,
|
||||||
sensorNumericDeviceClasses: string[],
|
|
||||||
config: HassConfig,
|
config: HassConfig,
|
||||||
entity: EntityRegistryDisplayEntry | undefined,
|
entity: EntityRegistryDisplayEntry | undefined,
|
||||||
entityId: string,
|
entityId: string,
|
||||||
@@ -58,15 +73,8 @@ export const computeStateDisplayFromEntityAttributes = (
|
|||||||
return localize(`state.default.${state}`);
|
return localize(`state.default.${state}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const domain = computeDomain(entityId);
|
|
||||||
|
|
||||||
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
|
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
|
||||||
if (
|
if (isNumericFromAttributes(attributes)) {
|
||||||
isNumericFromAttributes(
|
|
||||||
attributes,
|
|
||||||
domain === "sensor" ? sensorNumericDeviceClasses : []
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// state is duration
|
// state is duration
|
||||||
if (
|
if (
|
||||||
attributes.device_class === "duration" &&
|
attributes.device_class === "duration" &&
|
||||||
@@ -112,6 +120,8 @@ export const computeStateDisplayFromEntityAttributes = (
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const domain = computeDomain(entityId);
|
||||||
|
|
||||||
if (domain === "datetime") {
|
if (domain === "datetime") {
|
||||||
const time = new Date(state);
|
const time = new Date(state);
|
||||||
return formatDateTime(time, locale, config);
|
return formatDateTime(time, locale, config);
|
||||||
@@ -177,14 +187,11 @@ export const computeStateDisplayFromEntityAttributes = (
|
|||||||
if (
|
if (
|
||||||
[
|
[
|
||||||
"button",
|
"button",
|
||||||
"conversation",
|
|
||||||
"event",
|
"event",
|
||||||
"image",
|
"image",
|
||||||
"input_button",
|
"input_button",
|
||||||
"notify",
|
|
||||||
"scene",
|
"scene",
|
||||||
"stt",
|
"stt",
|
||||||
"tag",
|
|
||||||
"tts",
|
"tts",
|
||||||
"wake_word",
|
"wake_word",
|
||||||
].includes(domain) ||
|
].includes(domain) ||
|
||||||
|
@@ -28,15 +28,7 @@ export const FIXED_DOMAIN_STATES = {
|
|||||||
input_button: [],
|
input_button: [],
|
||||||
lawn_mower: ["error", "paused", "mowing", "docked"],
|
lawn_mower: ["error", "paused", "mowing", "docked"],
|
||||||
light: ["on", "off"],
|
light: ["on", "off"],
|
||||||
lock: [
|
lock: ["jammed", "locked", "locking", "unlocked", "unlocking"],
|
||||||
"jammed",
|
|
||||||
"locked",
|
|
||||||
"locking",
|
|
||||||
"unlocked",
|
|
||||||
"unlocking",
|
|
||||||
"opening",
|
|
||||||
"open",
|
|
||||||
],
|
|
||||||
media_player: [
|
media_player: [
|
||||||
"off",
|
"off",
|
||||||
"on",
|
"on",
|
||||||
|
@@ -12,10 +12,11 @@ export const formatLanguageCode = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatLanguageCodeMem = memoizeOne(
|
const formatLanguageCodeMem = memoizeOne((locale: FrontendLocaleData) =>
|
||||||
(locale: FrontendLocaleData) =>
|
Intl && "DisplayNames" in Intl
|
||||||
new Intl.DisplayNames(locale.language, {
|
? new Intl.DisplayNames(locale.language, {
|
||||||
type: "language",
|
type: "language",
|
||||||
fallback: "code",
|
fallback: "code",
|
||||||
})
|
})
|
||||||
|
: undefined
|
||||||
);
|
);
|
||||||
|
@@ -11,7 +11,6 @@ declare global {
|
|||||||
|
|
||||||
export interface NavigateOptions {
|
export interface NavigateOptions {
|
||||||
replace?: boolean;
|
replace?: boolean;
|
||||||
data?: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const navigate = (path: string, options?: NavigateOptions) => {
|
export const navigate = (path: string, options?: NavigateOptions) => {
|
||||||
@@ -25,7 +24,7 @@ export const navigate = (path: string, options?: NavigateOptions) => {
|
|||||||
if (__DEMO__) {
|
if (__DEMO__) {
|
||||||
if (replace) {
|
if (replace) {
|
||||||
mainWindow.history.replaceState(
|
mainWindow.history.replaceState(
|
||||||
mainWindow.history.state?.root ? { root: true } : options?.data ?? null,
|
mainWindow.history.state?.root ? { root: true } : null,
|
||||||
"",
|
"",
|
||||||
`${mainWindow.location.pathname}#${path}`
|
`${mainWindow.location.pathname}#${path}`
|
||||||
);
|
);
|
||||||
@@ -34,12 +33,12 @@ export const navigate = (path: string, options?: NavigateOptions) => {
|
|||||||
}
|
}
|
||||||
} else if (replace) {
|
} else if (replace) {
|
||||||
mainWindow.history.replaceState(
|
mainWindow.history.replaceState(
|
||||||
mainWindow.history.state?.root ? { root: true } : options?.data ?? null,
|
mainWindow.history.state?.root ? { root: true } : null,
|
||||||
"",
|
"",
|
||||||
path
|
path
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
mainWindow.history.pushState(options?.data ?? null, "", path);
|
mainWindow.history.pushState(null, "", path);
|
||||||
}
|
}
|
||||||
fireEvent(mainWindow, "location-changed", {
|
fireEvent(mainWindow, "location-changed", {
|
||||||
replace,
|
replace,
|
||||||
|
@@ -14,12 +14,8 @@ export const isNumericState = (stateObj: HassEntity): boolean =>
|
|||||||
isNumericFromAttributes(stateObj.attributes);
|
isNumericFromAttributes(stateObj.attributes);
|
||||||
|
|
||||||
export const isNumericFromAttributes = (
|
export const isNumericFromAttributes = (
|
||||||
attributes: HassEntityAttributeBase,
|
attributes: HassEntityAttributeBase
|
||||||
numericDeviceClasses?: string[]
|
): boolean => !!attributes.unit_of_measurement || !!attributes.state_class;
|
||||||
): boolean =>
|
|
||||||
!!attributes.unit_of_measurement ||
|
|
||||||
!!attributes.state_class ||
|
|
||||||
(numericDeviceClasses || []).includes(attributes.device_class || "");
|
|
||||||
|
|
||||||
export const numberFormatToLocale = (
|
export const numberFormatToLocale = (
|
||||||
localeOptions: FrontendLocaleData
|
localeOptions: FrontendLocaleData
|
||||||
@@ -63,18 +59,30 @@ export const formatNumber = (
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
localeOptions?.number_format !== NumberFormat.none &&
|
localeOptions?.number_format !== NumberFormat.none &&
|
||||||
!Number.isNaN(Number(num))
|
!Number.isNaN(Number(num)) &&
|
||||||
|
Intl
|
||||||
) {
|
) {
|
||||||
return new Intl.NumberFormat(
|
try {
|
||||||
locale,
|
return new Intl.NumberFormat(
|
||||||
getDefaultFormatOptions(num, options)
|
locale,
|
||||||
).format(Number(num));
|
getDefaultFormatOptions(num, options)
|
||||||
|
).format(Number(num));
|
||||||
|
} catch (err: any) {
|
||||||
|
// Don't fail when using "TEST" language
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(err);
|
||||||
|
return new Intl.NumberFormat(
|
||||||
|
undefined,
|
||||||
|
getDefaultFormatOptions(num, options)
|
||||||
|
).format(Number(num));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!Number.isNaN(Number(num)) &&
|
!Number.isNaN(Number(num)) &&
|
||||||
num !== "" &&
|
num !== "" &&
|
||||||
localeOptions?.number_format === NumberFormat.none
|
localeOptions?.number_format === NumberFormat.none &&
|
||||||
|
Intl
|
||||||
) {
|
) {
|
||||||
// If NumberFormat is none, use en-US format without grouping.
|
// If NumberFormat is none, use en-US format without grouping.
|
||||||
return new Intl.NumberFormat(
|
return new Intl.NumberFormat(
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
|
import "../../resources/intl-polyfill";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
import { FrontendLocaleData } from "../../data/translation";
|
||||||
|
|
||||||
export const formatListWithAnds = (
|
export const formatListWithAnds = (
|
||||||
|
@@ -21,8 +21,7 @@ export const computeFormatFunctions = async (
|
|||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
locale: FrontendLocaleData,
|
locale: FrontendLocaleData,
|
||||||
config: HassConfig,
|
config: HassConfig,
|
||||||
entities: HomeAssistant["entities"],
|
entities: HomeAssistant["entities"]
|
||||||
sensorNumericDeviceClasses: string[]
|
|
||||||
): Promise<{
|
): Promise<{
|
||||||
formatEntityState: FormatEntityStateFunc;
|
formatEntityState: FormatEntityStateFunc;
|
||||||
formatEntityAttributeValue: FormatEntityAttributeValueFunc;
|
formatEntityAttributeValue: FormatEntityAttributeValueFunc;
|
||||||
@@ -36,15 +35,7 @@ export const computeFormatFunctions = async (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
formatEntityState: (stateObj, state) =>
|
formatEntityState: (stateObj, state) =>
|
||||||
computeStateDisplay(
|
computeStateDisplay(localize, stateObj, locale, config, entities, state),
|
||||||
localize,
|
|
||||||
stateObj,
|
|
||||||
locale,
|
|
||||||
sensorNumericDeviceClasses,
|
|
||||||
config,
|
|
||||||
entities,
|
|
||||||
state
|
|
||||||
),
|
|
||||||
formatEntityAttributeValue: (stateObj, attribute, value) =>
|
formatEntityAttributeValue: (stateObj, attribute, value) =>
|
||||||
computeAttributeValueDisplay(
|
computeAttributeValueDisplay(
|
||||||
localize,
|
localize,
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
import type { IntlMessageFormat } from "intl-messageformat";
|
import IntlMessageFormat from "intl-messageformat";
|
||||||
import type { HTMLTemplateResult } from "lit";
|
import type { HTMLTemplateResult } from "lit";
|
||||||
import { polyfillLocaleData } from "../../resources/polyfills/locale-data-polyfill";
|
import { polyfillLocaleData } from "../../resources/locale-data-polyfill";
|
||||||
import { Resources, TranslationDict } from "../../types";
|
import { Resources, TranslationDict } from "../../types";
|
||||||
import { fireEvent } from "../dom/fire_event";
|
|
||||||
|
|
||||||
// Exclude some patterns from key type checking for now
|
// Exclude some patterns from key type checking for now
|
||||||
// These are intended to be removed as errors are fixed
|
// These are intended to be removed as errors are fixed
|
||||||
@@ -82,15 +81,14 @@ export interface FormatsType {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const computeLocalize = async <Keys extends string = LocalizeKeys>(
|
export const computeLocalize = async <Keys extends string = LocalizeKeys>(
|
||||||
cache: HTMLElement & {
|
cache: any,
|
||||||
_localizationCache?: Record<string, IntlMessageFormat>;
|
|
||||||
},
|
|
||||||
language: string,
|
language: string,
|
||||||
resources: Resources,
|
resources: Resources,
|
||||||
formats?: FormatsType
|
formats?: FormatsType
|
||||||
): Promise<LocalizeFunc<Keys>> => {
|
): Promise<LocalizeFunc<Keys>> => {
|
||||||
const { IntlMessageFormat } = await import("intl-messageformat");
|
await import("../../resources/intl-polyfill").then(() =>
|
||||||
await polyfillLocaleData(language);
|
polyfillLocaleData(language)
|
||||||
|
);
|
||||||
|
|
||||||
// Every time any of the parameters change, invalidate the strings cache.
|
// Every time any of the parameters change, invalidate the strings cache.
|
||||||
cache._localizationCache = {};
|
cache._localizationCache = {};
|
||||||
@@ -109,7 +107,7 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messageKey = key + translatedValue;
|
const messageKey = key + translatedValue;
|
||||||
let translatedMessage = cache._localizationCache![messageKey] as
|
let translatedMessage = cache._localizationCache[messageKey] as
|
||||||
| IntlMessageFormat
|
| IntlMessageFormat
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
@@ -123,7 +121,7 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>(
|
|||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
return "Translation error: " + err.message;
|
return "Translation error: " + err.message;
|
||||||
}
|
}
|
||||||
cache._localizationCache![messageKey] = translatedMessage;
|
cache._localizationCache[messageKey] = translatedMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
let argObject = {};
|
let argObject = {};
|
||||||
@@ -139,12 +137,6 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>(
|
|||||||
try {
|
try {
|
||||||
return translatedMessage.format<string>(argObject) as string;
|
return translatedMessage.format<string>(argObject) as string;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error("Translation error", key, language, err);
|
|
||||||
fireEvent(cache, "write_log", {
|
|
||||||
level: "error",
|
|
||||||
message: `Failed to format translation for key '${key}' in language '${language}'. ${err}`,
|
|
||||||
});
|
|
||||||
return "Translation " + err;
|
return "Translation " + err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -1,9 +0,0 @@
|
|||||||
export const hasRejectedItems = <T = any>(results: PromiseSettledResult<T>[]) =>
|
|
||||||
results.some((result) => result.status === "rejected");
|
|
||||||
|
|
||||||
export const rejectedItems = <T = any>(
|
|
||||||
results: PromiseSettledResult<T>[]
|
|
||||||
): PromiseRejectedResult[] =>
|
|
||||||
results.filter(
|
|
||||||
(result) => result.status === "rejected"
|
|
||||||
) as PromiseRejectedResult[];
|
|
@@ -1,4 +1,4 @@
|
|||||||
import { differenceInDays, differenceInWeeks, startOfWeek } from "date-fns";
|
import { differenceInDays, differenceInWeeks, startOfWeek } from "date-fns/esm";
|
||||||
import { FrontendLocaleData } from "../../data/translation";
|
import { FrontendLocaleData } from "../../data/translation";
|
||||||
import { firstWeekdayIndex } from "../datetime/first_weekday";
|
import { firstWeekdayIndex } from "../datetime/first_weekday";
|
||||||
|
|
||||||
|
@@ -34,7 +34,7 @@ import {
|
|||||||
endOfMonth,
|
endOfMonth,
|
||||||
endOfQuarter,
|
endOfQuarter,
|
||||||
endOfYear,
|
endOfYear,
|
||||||
} from "date-fns";
|
} from "date-fns/esm";
|
||||||
import {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
formatDateMonth,
|
formatDateMonth,
|
||||||
|
@@ -313,38 +313,31 @@ export class HaChartBase extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _loading = false;
|
|
||||||
|
|
||||||
private async _setupChart() {
|
private async _setupChart() {
|
||||||
if (this._loading) return;
|
|
||||||
const ctx: CanvasRenderingContext2D = this.renderRoot
|
const ctx: CanvasRenderingContext2D = this.renderRoot
|
||||||
.querySelector("canvas")!
|
.querySelector("canvas")!
|
||||||
.getContext("2d")!;
|
.getContext("2d")!;
|
||||||
this._loading = true;
|
|
||||||
try {
|
|
||||||
const ChartConstructor = (await import("../../resources/chartjs")).Chart;
|
|
||||||
|
|
||||||
const computedStyles = getComputedStyle(this);
|
const ChartConstructor = (await import("../../resources/chartjs")).Chart;
|
||||||
|
|
||||||
ChartConstructor.defaults.borderColor =
|
const computedStyles = getComputedStyle(this);
|
||||||
computedStyles.getPropertyValue("--divider-color");
|
|
||||||
ChartConstructor.defaults.color = computedStyles.getPropertyValue(
|
|
||||||
"--secondary-text-color"
|
|
||||||
);
|
|
||||||
ChartConstructor.defaults.font.family =
|
|
||||||
computedStyles.getPropertyValue("--mdc-typography-body1-font-family") ||
|
|
||||||
computedStyles.getPropertyValue("--mdc-typography-font-family") ||
|
|
||||||
"Roboto, Noto, sans-serif";
|
|
||||||
|
|
||||||
this.chart = new ChartConstructor(ctx, {
|
ChartConstructor.defaults.borderColor =
|
||||||
type: this.chartType,
|
computedStyles.getPropertyValue("--divider-color");
|
||||||
data: this.data,
|
ChartConstructor.defaults.color = computedStyles.getPropertyValue(
|
||||||
options: this._createOptions(),
|
"--secondary-text-color"
|
||||||
plugins: this._createPlugins(),
|
);
|
||||||
});
|
ChartConstructor.defaults.font.family =
|
||||||
} finally {
|
computedStyles.getPropertyValue("--mdc-typography-body1-font-family") ||
|
||||||
this._loading = false;
|
computedStyles.getPropertyValue("--mdc-typography-font-family") ||
|
||||||
}
|
"Roboto, Noto, sans-serif";
|
||||||
|
|
||||||
|
this.chart = new ChartConstructor(ctx, {
|
||||||
|
type: this.chartType,
|
||||||
|
data: this.data,
|
||||||
|
options: this._createOptions(),
|
||||||
|
plugins: this._createPlugins(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createOptions() {
|
private _createOptions() {
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import "element-internals-polyfill";
|
||||||
import { MdAssistChip } from "@material/web/chips/assist-chip";
|
import { MdAssistChip } from "@material/web/chips/assist-chip";
|
||||||
import { css, html } from "lit";
|
import { css, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
@@ -44,8 +45,8 @@ export class HaAssistChip extends MdAssistChip {
|
|||||||
margin-inline-start: var(--_icon-label-space);
|
margin-inline-start: var(--_icon-label-space);
|
||||||
}
|
}
|
||||||
::before {
|
::before {
|
||||||
background: var(--ha-assist-chip-container-color, transparent);
|
background: var(--ha-assist-chip-container-color);
|
||||||
opacity: var(--ha-assist-chip-container-opacity, 1);
|
opacity: var(--ha-assist-chip-container-opacity);
|
||||||
}
|
}
|
||||||
:where(.active)::before {
|
:where(.active)::before {
|
||||||
background: var(--ha-assist-chip-active-container-color);
|
background: var(--ha-assist-chip-active-container-color);
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import "element-internals-polyfill";
|
||||||
import { MdChipSet } from "@material/web/chips/chip-set";
|
import { MdChipSet } from "@material/web/chips/chip-set";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
|
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import "element-internals-polyfill";
|
||||||
import { MdFilterChip } from "@material/web/chips/filter-chip";
|
import { MdFilterChip } from "@material/web/chips/filter-chip";
|
||||||
import { css, html } from "lit";
|
import { css, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import "element-internals-polyfill";
|
||||||
import { MdInputChip } from "@material/web/chips/input-chip";
|
import { MdInputChip } from "@material/web/chips/input-chip";
|
||||||
import { css } from "lit";
|
import { css } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
@@ -19,7 +20,6 @@ export class HaInputChip extends MdInputChip {
|
|||||||
0.15
|
0.15
|
||||||
);
|
);
|
||||||
--ha-input-chip-selected-container-opacity: 1;
|
--ha-input-chip-selected-container-opacity: 1;
|
||||||
--md-input-chip-label-text-font: Roboto, sans-serif;
|
|
||||||
}
|
}
|
||||||
/** Set the size of mdc icons **/
|
/** Set the size of mdc icons **/
|
||||||
::slotted([slot="icon"]) {
|
::slotted([slot="icon"]) {
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
import { mdiArrowDown, mdiArrowUp, mdiChevronUp } from "@mdi/js";
|
import { mdiArrowDown, mdiArrowUp } from "@mdi/js";
|
||||||
import deepClone from "deep-clone-simple";
|
import deepClone from "deep-clone-simple";
|
||||||
import {
|
import {
|
||||||
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
|
html,
|
||||||
LitElement,
|
LitElement,
|
||||||
|
nothing,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
css,
|
|
||||||
html,
|
|
||||||
nothing,
|
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import {
|
import {
|
||||||
customElement,
|
customElement,
|
||||||
@@ -22,9 +22,7 @@ import { styleMap } from "lit/directives/style-map";
|
|||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { stringCompare } from "../../common/string/compare";
|
|
||||||
import { debounce } from "../../common/util/debounce";
|
import { debounce } from "../../common/util/debounce";
|
||||||
import { groupBy } from "../../common/util/group-by";
|
|
||||||
import { nextRender } from "../../common/util/render-status";
|
import { nextRender } from "../../common/util/render-status";
|
||||||
import { haStyleScrollbar } from "../../resources/styles";
|
import { haStyleScrollbar } from "../../resources/styles";
|
||||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||||
@@ -34,6 +32,16 @@ import type { HaCheckbox } from "../ha-checkbox";
|
|||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
import "../search-input";
|
import "../search-input";
|
||||||
import { filterData, sortData } from "./sort-filter";
|
import { filterData, sortData } from "./sort-filter";
|
||||||
|
import { groupBy } from "../../common/util/group-by";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// for fire event
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"selection-changed": SelectionChangedEvent;
|
||||||
|
"row-click": RowClickedEvent;
|
||||||
|
"sorting-changed": SortingChangedEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface RowClickedEvent {
|
export interface RowClickedEvent {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -43,10 +51,6 @@ export interface SelectionChangedEvent {
|
|||||||
value: string[];
|
value: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollapsedChangedEvent {
|
|
||||||
value: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SortingChangedEvent {
|
export interface SortingChangedEvent {
|
||||||
column: string;
|
column: string;
|
||||||
direction: SortingDirection;
|
direction: SortingDirection;
|
||||||
@@ -137,14 +141,10 @@ export class HaDataTable extends LitElement {
|
|||||||
|
|
||||||
@property() public groupColumn?: string;
|
@property() public groupColumn?: string;
|
||||||
|
|
||||||
@property({ attribute: false }) public groupOrder?: string[];
|
|
||||||
|
|
||||||
@property() public sortColumn?: string;
|
@property() public sortColumn?: string;
|
||||||
|
|
||||||
@property() public sortDirection: SortingDirection = null;
|
@property() public sortDirection: SortingDirection = null;
|
||||||
|
|
||||||
@property({ attribute: false }) public initialCollapsedGroups?: string[];
|
|
||||||
|
|
||||||
@state() private _filterable = false;
|
@state() private _filterable = false;
|
||||||
|
|
||||||
@state() private _filter = "";
|
@state() private _filter = "";
|
||||||
@@ -157,8 +157,6 @@ export class HaDataTable extends LitElement {
|
|||||||
|
|
||||||
@state() private _items: DataTableRowData[] = [];
|
@state() private _items: DataTableRowData[] = [];
|
||||||
|
|
||||||
@state() private _collapsedGroups: string[] = [];
|
|
||||||
|
|
||||||
private _checkableRowsCount?: number;
|
private _checkableRowsCount?: number;
|
||||||
|
|
||||||
private _checkedRows: string[] = [];
|
private _checkedRows: string[] = [];
|
||||||
@@ -214,19 +212,17 @@ export class HaDataTable extends LitElement {
|
|||||||
(column) => column.filterable
|
(column) => column.filterable
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!this.sortColumn) {
|
for (const columnId in this.columns) {
|
||||||
for (const columnId in this.columns) {
|
if (this.columns[columnId].direction) {
|
||||||
if (this.columns[columnId].direction) {
|
this.sortDirection = this.columns[columnId].direction!;
|
||||||
this.sortDirection = this.columns[columnId].direction!;
|
this.sortColumn = columnId;
|
||||||
this.sortColumn = columnId;
|
|
||||||
|
|
||||||
fireEvent(this, "sorting-changed", {
|
fireEvent(this, "sorting-changed", {
|
||||||
column: columnId,
|
column: columnId,
|
||||||
direction: this.sortDirection,
|
direction: this.sortDirection,
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,23 +247,13 @@ export class HaDataTable extends LitElement {
|
|||||||
).length;
|
).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.hasUpdated && this.initialCollapsedGroups) {
|
|
||||||
this._collapsedGroups = this.initialCollapsedGroups;
|
|
||||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
|
||||||
} else if (properties.has("groupColumn")) {
|
|
||||||
this._collapsedGroups = [];
|
|
||||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
properties.has("data") ||
|
properties.has("data") ||
|
||||||
properties.has("columns") ||
|
properties.has("columns") ||
|
||||||
properties.has("_filter") ||
|
properties.has("_filter") ||
|
||||||
properties.has("sortColumn") ||
|
properties.has("sortColumn") ||
|
||||||
properties.has("sortDirection") ||
|
properties.has("sortDirection") ||
|
||||||
properties.has("groupColumn") ||
|
properties.has("groupColumn")
|
||||||
properties.has("groupOrder") ||
|
|
||||||
properties.has("_collapsedGroups")
|
|
||||||
) {
|
) {
|
||||||
this._sortFilterData();
|
this._sortFilterData();
|
||||||
}
|
}
|
||||||
@@ -460,8 +446,6 @@ export class HaDataTable extends LitElement {
|
|||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
@mouseover=${this._setTitle}
|
|
||||||
@focus=${this._setTitle}
|
|
||||||
role=${column.main ? "rowheader" : "cell"}
|
role=${column.main ? "rowheader" : "cell"}
|
||||||
class="mdc-data-table__cell ${classMap({
|
class="mdc-data-table__cell ${classMap({
|
||||||
"mdc-data-table__cell--flex": column.type === "flex",
|
"mdc-data-table__cell--flex": column.type === "flex",
|
||||||
@@ -529,7 +513,11 @@ export class HaDataTable extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.appendRow || this.hasFab || this.groupColumn) {
|
if (this.appendRow || this.hasFab || this.groupColumn) {
|
||||||
let items = [...data];
|
const items = [...data];
|
||||||
|
|
||||||
|
if (this.appendRow) {
|
||||||
|
items.push({ append: true, content: this.appendRow });
|
||||||
|
}
|
||||||
|
|
||||||
if (this.groupColumn) {
|
if (this.groupColumn) {
|
||||||
const grouped = groupBy(items, (item) => item[this.groupColumn!]);
|
const grouped = groupBy(items, (item) => item[this.groupColumn!]);
|
||||||
@@ -541,66 +529,39 @@ export class HaDataTable extends LitElement {
|
|||||||
const sorted: {
|
const sorted: {
|
||||||
[key: string]: DataTableRowData[];
|
[key: string]: DataTableRowData[];
|
||||||
} = Object.keys(grouped)
|
} = Object.keys(grouped)
|
||||||
.sort((a, b) => {
|
.sort()
|
||||||
const orderA = this.groupOrder?.indexOf(a) ?? -1;
|
|
||||||
const orderB = this.groupOrder?.indexOf(b) ?? -1;
|
|
||||||
if (orderA !== orderB) {
|
|
||||||
if (orderA === -1) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (orderB === -1) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return orderA - orderB;
|
|
||||||
}
|
|
||||||
return stringCompare(
|
|
||||||
["", "-", "—"].includes(a) ? "zzz" : a,
|
|
||||||
["", "-", "—"].includes(b) ? "zzz" : b,
|
|
||||||
this.hass.locale.language
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.reduce((obj, key) => {
|
.reduce((obj, key) => {
|
||||||
obj[key] = grouped[key];
|
obj[key] = grouped[key];
|
||||||
return obj;
|
return obj;
|
||||||
}, {});
|
}, {});
|
||||||
const groupedItems: DataTableRowData[] = [];
|
const groupedItems: DataTableRowData[] = [];
|
||||||
Object.entries(sorted).forEach(([groupName, rows]) => {
|
Object.entries(sorted).forEach(([groupName, rows]) => {
|
||||||
groupedItems.push({
|
if (
|
||||||
append: true,
|
groupName !== UNDEFINED_GROUP_KEY ||
|
||||||
content: html`<div
|
Object.keys(sorted).length > 1
|
||||||
class="mdc-data-table__cell group-header"
|
) {
|
||||||
role="cell"
|
groupedItems.push({
|
||||||
.group=${groupName}
|
append: true,
|
||||||
@click=${this._collapseGroup}
|
content: html`<div
|
||||||
>
|
class="mdc-data-table__cell group-header"
|
||||||
<ha-icon-button
|
role="cell"
|
||||||
.path=${mdiChevronUp}
|
|
||||||
class=${this._collapsedGroups.includes(groupName)
|
|
||||||
? "collapsed"
|
|
||||||
: ""}
|
|
||||||
>
|
>
|
||||||
</ha-icon-button>
|
${groupName === UNDEFINED_GROUP_KEY ? "" : groupName || ""}
|
||||||
${groupName === UNDEFINED_GROUP_KEY
|
</div>`,
|
||||||
? this.hass.localize("ui.components.data-table.ungrouped")
|
});
|
||||||
: groupName || ""}
|
|
||||||
</div>`,
|
|
||||||
});
|
|
||||||
if (!this._collapsedGroups.includes(groupName)) {
|
|
||||||
groupedItems.push(...rows);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
items = groupedItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.appendRow) {
|
groupedItems.push(...rows);
|
||||||
items.push({ append: true, content: this.appendRow });
|
});
|
||||||
|
|
||||||
|
this._items = groupedItems;
|
||||||
|
} else {
|
||||||
|
this._items = items;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.hasFab) {
|
if (this.hasFab) {
|
||||||
items.push({ empty: true });
|
this._items = [...this._items, { empty: true }];
|
||||||
}
|
}
|
||||||
|
|
||||||
this._items = items;
|
|
||||||
} else {
|
} else {
|
||||||
this._items = data;
|
this._items = data;
|
||||||
}
|
}
|
||||||
@@ -681,13 +642,6 @@ export class HaDataTable extends LitElement {
|
|||||||
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
|
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
private _setTitle(ev: Event) {
|
|
||||||
const target = ev.currentTarget as HTMLElement;
|
|
||||||
if (target.scrollWidth > target.offsetWidth) {
|
|
||||||
target.setAttribute("title", target.innerText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _checkedRowsChanged() {
|
private _checkedRowsChanged() {
|
||||||
// force scroller to update, change it's items
|
// force scroller to update, change it's items
|
||||||
if (this._items.length) {
|
if (this._items.length) {
|
||||||
@@ -718,40 +672,6 @@ export class HaDataTable extends LitElement {
|
|||||||
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
|
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _collapseGroup = (ev: Event) => {
|
|
||||||
const groupName = (ev.currentTarget as any).group;
|
|
||||||
if (this._collapsedGroups.includes(groupName)) {
|
|
||||||
this._collapsedGroups = this._collapsedGroups.filter(
|
|
||||||
(grp) => grp !== groupName
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this._collapsedGroups = [...this._collapsedGroups, groupName];
|
|
||||||
}
|
|
||||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
|
||||||
};
|
|
||||||
|
|
||||||
public expandAllGroups() {
|
|
||||||
this._collapsedGroups = [];
|
|
||||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
|
||||||
}
|
|
||||||
|
|
||||||
public collapseAllGroups() {
|
|
||||||
if (
|
|
||||||
!this.groupColumn ||
|
|
||||||
!this.data.some((item) => item[this.groupColumn!])
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const grouped = groupBy(this.data, (item) => item[this.groupColumn!]);
|
|
||||||
if (grouped.undefined) {
|
|
||||||
// undefined is a reserved group name
|
|
||||||
grouped[UNDEFINED_GROUP_KEY] = grouped.undefined;
|
|
||||||
delete grouped.undefined;
|
|
||||||
}
|
|
||||||
this._collapsedGroups = Object.keys(grouped);
|
|
||||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
@@ -1004,22 +924,8 @@ export class HaDataTable extends LitElement {
|
|||||||
|
|
||||||
.group-header {
|
.group-header {
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
padding-left: 12px;
|
|
||||||
padding-inline-start: 12px;
|
|
||||||
padding-inline-end: initial;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-header ha-icon-button {
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-header ha-icon-button.collapsed {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
@@ -1118,12 +1024,4 @@ declare global {
|
|||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"ha-data-table": HaDataTable;
|
"ha-data-table": HaDataTable;
|
||||||
}
|
}
|
||||||
|
|
||||||
// for fire event
|
|
||||||
interface HASSDomEvents {
|
|
||||||
"selection-changed": SelectionChangedEvent;
|
|
||||||
"row-click": RowClickedEvent;
|
|
||||||
"sorting-changed": SortingChangedEvent;
|
|
||||||
"collapsed-changed": CollapsedChangedEvent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -11,10 +11,10 @@ import {
|
|||||||
} from "../common/datetime/localize_date";
|
} from "../common/datetime/localize_date";
|
||||||
import { mainWindow } from "../common/dom/get_main_window";
|
import { mainWindow } from "../common/dom/get_main_window";
|
||||||
|
|
||||||
|
// Set the current date to the left picker instead of the right picker because the right is hidden
|
||||||
const CustomDateRangePicker = Vue.extend({
|
const CustomDateRangePicker = Vue.extend({
|
||||||
mixins: [DateRangePicker],
|
mixins: [DateRangePicker],
|
||||||
methods: {
|
methods: {
|
||||||
// Set the current date to the left picker instead of the right picker because the right is hidden
|
|
||||||
selectMonthDate() {
|
selectMonthDate() {
|
||||||
const dt: Date = this.end || new Date();
|
const dt: Date = this.end || new Date();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -23,33 +23,6 @@ const CustomDateRangePicker = Vue.extend({
|
|||||||
month: dt.getMonth() + 1,
|
month: dt.getMonth() + 1,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// Fix the start/end date calculation when selecting a date range. The
|
|
||||||
// original code keeps track of the first clicked date (in_selection) but it
|
|
||||||
// never sets it to either the start or end date variables, so if the
|
|
||||||
// in_selection date is between the start and end date that were set by the
|
|
||||||
// hover the selection will enter a broken state that's counter-intuitive
|
|
||||||
// when hovering between weeks and leads to a random date when selecting a
|
|
||||||
// range across months. This bug doesn't seem to be present on v0.6.7 of the
|
|
||||||
// lib
|
|
||||||
hoverDate(value: Date) {
|
|
||||||
if (this.readonly) return;
|
|
||||||
|
|
||||||
if (this.in_selection) {
|
|
||||||
const pickA = this.in_selection as Date;
|
|
||||||
const pickB = value;
|
|
||||||
|
|
||||||
this.start = this.normalizeDatetime(
|
|
||||||
Math.min(pickA.valueOf(), pickB.valueOf()),
|
|
||||||
this.start
|
|
||||||
);
|
|
||||||
this.end = this.normalizeDatetime(
|
|
||||||
Math.max(pickA.valueOf(), pickB.valueOf()),
|
|
||||||
this.end
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$emit("hover-date", value);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -76,8 +76,6 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public entityFilter?: HaEntityPickerEntityFilterFunc;
|
public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||||
|
|
||||||
@property({ type: Array }) public createDomains?: string[];
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this.hass) {
|
if (!this.hass) {
|
||||||
return nothing;
|
return nothing;
|
||||||
@@ -105,7 +103,6 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
.value=${entityId}
|
.value=${entityId}
|
||||||
.label=${this.pickedEntityLabel}
|
.label=${this.pickedEntityLabel}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.createDomains=${this.createDomains}
|
|
||||||
@value-changed=${this._entityChanged}
|
@value-changed=${this._entityChanged}
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,7 +122,6 @@ class HaEntitiesPickerLight extends LitElement {
|
|||||||
.label=${this.pickEntityLabel}
|
.label=${this.pickEntityLabel}
|
||||||
.helper=${this.helper}
|
.helper=${this.helper}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.createDomains=${this.createDomains}
|
|
||||||
.required=${this.required && !currentEntities.length}
|
.required=${this.required && !currentEntities.length}
|
||||||
@value-changed=${this._addEntity}
|
@value-changed=${this._addEntity}
|
||||||
></ha-entity-picker>
|
></ha-entity-picker>
|
||||||
|
@@ -18,12 +18,6 @@ import "../ha-icon-button";
|
|||||||
import "../ha-svg-icon";
|
import "../ha-svg-icon";
|
||||||
import "./state-badge";
|
import "./state-badge";
|
||||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||||
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
|
|
||||||
import { domainToName } from "../../data/integration";
|
|
||||||
import {
|
|
||||||
isHelperDomain,
|
|
||||||
HelperDomain,
|
|
||||||
} from "../../panels/config/helpers/const";
|
|
||||||
|
|
||||||
interface HassEntityWithCachedName extends HassEntity, ScorableTextItem {
|
interface HassEntityWithCachedName extends HassEntity, ScorableTextItem {
|
||||||
friendly_name: string;
|
friendly_name: string;
|
||||||
@@ -31,8 +25,6 @@ interface HassEntityWithCachedName extends HassEntity, ScorableTextItem {
|
|||||||
|
|
||||||
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
||||||
|
|
||||||
const CREATE_ID = "___create-new-entity___";
|
|
||||||
|
|
||||||
@customElement("ha-entity-picker")
|
@customElement("ha-entity-picker")
|
||||||
export class HaEntityPicker extends LitElement {
|
export class HaEntityPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -52,8 +44,6 @@ export class HaEntityPicker extends LitElement {
|
|||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
@property({ type: Array }) public createDomains?: string[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show entities from specific domains.
|
* Show entities from specific domains.
|
||||||
* @type {Array}
|
* @type {Array}
|
||||||
@@ -140,11 +130,7 @@ export class HaEntityPicker extends LitElement {
|
|||||||
></state-badge>`
|
></state-badge>`
|
||||||
: ""}
|
: ""}
|
||||||
<span>${item.friendly_name}</span>
|
<span>${item.friendly_name}</span>
|
||||||
<span slot="secondary"
|
<span slot="secondary">${item.entity_id}</span>
|
||||||
>${item.entity_id.startsWith(CREATE_ID)
|
|
||||||
? this.hass.localize("ui.components.entity.entity-picker.new_entity")
|
|
||||||
: item.entity_id}</span
|
|
||||||
>
|
|
||||||
</ha-list-item>`;
|
</ha-list-item>`;
|
||||||
|
|
||||||
private _getStates = memoizeOne(
|
private _getStates = memoizeOne(
|
||||||
@@ -157,8 +143,7 @@ export class HaEntityPicker extends LitElement {
|
|||||||
includeDeviceClasses: this["includeDeviceClasses"],
|
includeDeviceClasses: this["includeDeviceClasses"],
|
||||||
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
|
includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
|
||||||
includeEntities: this["includeEntities"],
|
includeEntities: this["includeEntities"],
|
||||||
excludeEntities: this["excludeEntities"],
|
excludeEntities: this["excludeEntities"]
|
||||||
createDomains: this["createDomains"]
|
|
||||||
): HassEntityWithCachedName[] => {
|
): HassEntityWithCachedName[] => {
|
||||||
let states: HassEntityWithCachedName[] = [];
|
let states: HassEntityWithCachedName[] = [];
|
||||||
|
|
||||||
@@ -167,34 +152,6 @@ export class HaEntityPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
let entityIds = Object.keys(hass.states);
|
let entityIds = Object.keys(hass.states);
|
||||||
|
|
||||||
const createItems = createDomains?.length
|
|
||||||
? createDomains.map((domain) => {
|
|
||||||
const newFriendlyName = hass.localize(
|
|
||||||
"ui.components.entity.entity-picker.create_helper",
|
|
||||||
{
|
|
||||||
domain: isHelperDomain(domain)
|
|
||||||
? hass.localize(
|
|
||||||
`ui.panel.config.helpers.types.${domain as HelperDomain}`
|
|
||||||
)
|
|
||||||
: domainToName(hass.localize, domain),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
entity_id: CREATE_ID + domain,
|
|
||||||
state: "on",
|
|
||||||
last_changed: "",
|
|
||||||
last_updated: "",
|
|
||||||
context: { id: "", user_id: null, parent_id: null },
|
|
||||||
friendly_name: newFriendlyName,
|
|
||||||
attributes: {
|
|
||||||
icon: "mdi:plus",
|
|
||||||
},
|
|
||||||
strings: [domain, newFriendlyName],
|
|
||||||
};
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (!entityIds.length) {
|
if (!entityIds.length) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -214,7 +171,6 @@ export class HaEntityPicker extends LitElement {
|
|||||||
},
|
},
|
||||||
strings: [],
|
strings: [],
|
||||||
},
|
},
|
||||||
...createItems,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,14 +281,9 @@ export class HaEntityPicker extends LitElement {
|
|||||||
},
|
},
|
||||||
strings: [],
|
strings: [],
|
||||||
},
|
},
|
||||||
...createItems,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (createItems?.length) {
|
|
||||||
states.push(...createItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
return states;
|
return states;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -359,18 +310,13 @@ export class HaEntityPicker extends LitElement {
|
|||||||
this.includeDeviceClasses,
|
this.includeDeviceClasses,
|
||||||
this.includeUnitOfMeasurement,
|
this.includeUnitOfMeasurement,
|
||||||
this.includeEntities,
|
this.includeEntities,
|
||||||
this.excludeEntities,
|
this.excludeEntities
|
||||||
this.createDomains
|
|
||||||
);
|
);
|
||||||
if (this._initedStates) {
|
if (this._initedStates) {
|
||||||
this.comboBox.filteredItems = this._states;
|
this.comboBox.filteredItems = this._states;
|
||||||
}
|
}
|
||||||
this._initedStates = true;
|
this._initedStates = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProps.has("createDomains") && this.createDomains?.length) {
|
|
||||||
this.hass.loadFragmentTranslation("config");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
@@ -405,21 +351,9 @@ export class HaEntityPicker extends LitElement {
|
|||||||
this._opened = ev.detail.value;
|
this._opened = ev.detail.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _valueChanged(ev: ValueChangedEvent<string | undefined>) {
|
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const newValue = ev.detail.value?.trim();
|
const newValue = ev.detail.value;
|
||||||
|
|
||||||
if (newValue && newValue.startsWith(CREATE_ID)) {
|
|
||||||
const domain = newValue.substring(CREATE_ID.length);
|
|
||||||
showHelperDetailDialog(this, {
|
|
||||||
domain,
|
|
||||||
dialogClosedCallback: (item) => {
|
|
||||||
if (item.entityId) this._setValue(item.entityId);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newValue !== this._value) {
|
if (newValue !== this._value) {
|
||||||
this._setValue(newValue);
|
this._setValue(newValue);
|
||||||
}
|
}
|
||||||
@@ -427,13 +361,13 @@ export class HaEntityPicker extends LitElement {
|
|||||||
|
|
||||||
private _filterChanged(ev: CustomEvent): void {
|
private _filterChanged(ev: CustomEvent): void {
|
||||||
const target = ev.target as HaComboBox;
|
const target = ev.target as HaComboBox;
|
||||||
const filterString = ev.detail.value.trim().toLowerCase();
|
const filterString = ev.detail.value.toLowerCase();
|
||||||
target.filteredItems = filterString.length
|
target.filteredItems = filterString.length
|
||||||
? fuzzyFilterSort<HassEntityWithCachedName>(filterString, this._states)
|
? fuzzyFilterSort<HassEntityWithCachedName>(filterString, this._states)
|
||||||
: this._states;
|
: this._states;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setValue(value: string | undefined) {
|
private _setValue(value: string) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
fireEvent(this, "value-changed", { value });
|
fireEvent(this, "value-changed", { value });
|
||||||
|
@@ -1,9 +1,8 @@
|
|||||||
import { mdiTextureBox } from "@mdi/js";
|
import { mdiTextureBox } from "@mdi/js";
|
||||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit";
|
import { LitElement, PropertyValues, TemplateResult, html } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
@@ -12,7 +11,6 @@ import {
|
|||||||
ScorableTextItem,
|
ScorableTextItem,
|
||||||
fuzzyFilterSort,
|
fuzzyFilterSort,
|
||||||
} from "../common/string/filter/sequence-matching";
|
} from "../common/string/filter/sequence-matching";
|
||||||
import { computeRTL } from "../common/util/compute_rtl";
|
|
||||||
import { AreaRegistryEntry } from "../data/area_registry";
|
import { AreaRegistryEntry } from "../data/area_registry";
|
||||||
import {
|
import {
|
||||||
DeviceEntityDisplayLookup,
|
DeviceEntityDisplayLookup,
|
||||||
@@ -34,7 +32,6 @@ import "./ha-floor-icon";
|
|||||||
import "./ha-icon-button";
|
import "./ha-icon-button";
|
||||||
import "./ha-list-item";
|
import "./ha-list-item";
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
import "./ha-tree-indicator";
|
|
||||||
|
|
||||||
type ScorableAreaFloorEntry = ScorableTextItem & FloorAreaEntry;
|
type ScorableAreaFloorEntry = ScorableTextItem & FloorAreaEntry;
|
||||||
|
|
||||||
@@ -44,11 +41,28 @@ interface FloorAreaEntry {
|
|||||||
icon: string | null;
|
icon: string | null;
|
||||||
strings: string[];
|
strings: string[];
|
||||||
type: "floor" | "area";
|
type: "floor" | "area";
|
||||||
level: number | null;
|
|
||||||
hasFloor?: boolean;
|
hasFloor?: boolean;
|
||||||
lastArea?: boolean;
|
level: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rowRenderer: ComboBoxLitRenderer<FloorAreaEntry> = (item) =>
|
||||||
|
html`<ha-list-item
|
||||||
|
graphic="icon"
|
||||||
|
style=${item.type === "area" && item.hasFloor
|
||||||
|
? "--mdc-list-side-padding-left: 48px;"
|
||||||
|
: ""}
|
||||||
|
>
|
||||||
|
${item.type === "floor"
|
||||||
|
? html`<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>`
|
||||||
|
: item.icon
|
||||||
|
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
|
||||||
|
: html`<ha-svg-icon
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiTextureBox}
|
||||||
|
></ha-svg-icon>`}
|
||||||
|
${item.name}
|
||||||
|
</ha-list-item>`;
|
||||||
|
|
||||||
@customElement("ha-area-floor-picker")
|
@customElement("ha-area-floor-picker")
|
||||||
export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@@ -137,44 +151,6 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
|||||||
await this.comboBox?.focus();
|
await this.comboBox?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _rowRenderer: ComboBoxLitRenderer<FloorAreaEntry> = (item) => {
|
|
||||||
const rtl = computeRTL(this.hass);
|
|
||||||
return html`
|
|
||||||
<ha-list-item
|
|
||||||
graphic="icon"
|
|
||||||
style=${item.type === "area" && item.hasFloor
|
|
||||||
? rtl
|
|
||||||
? "--mdc-list-side-padding-right: 48px;"
|
|
||||||
: "--mdc-list-side-padding-left: 48px;"
|
|
||||||
: ""}
|
|
||||||
>
|
|
||||||
${item.type === "area" && item.hasFloor
|
|
||||||
? html`<ha-tree-indicator
|
|
||||||
style=${styleMap({
|
|
||||||
width: "48px",
|
|
||||||
position: "absolute",
|
|
||||||
top: "0px",
|
|
||||||
left: rtl ? undefined : "8px",
|
|
||||||
right: rtl ? "8px" : undefined,
|
|
||||||
transform: rtl ? "scaleX(-1)" : "",
|
|
||||||
})}
|
|
||||||
.end=${item.lastArea}
|
|
||||||
slot="graphic"
|
|
||||||
></ha-tree-indicator>`
|
|
||||||
: nothing}
|
|
||||||
${item.type === "floor"
|
|
||||||
? html`<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>`
|
|
||||||
: item.icon
|
|
||||||
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
|
|
||||||
: html`<ha-svg-icon
|
|
||||||
slot="graphic"
|
|
||||||
.path=${mdiTextureBox}
|
|
||||||
></ha-svg-icon>`}
|
|
||||||
${item.name}
|
|
||||||
</ha-list-item>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
private _getAreas = memoizeOne(
|
private _getAreas = memoizeOne(
|
||||||
(
|
(
|
||||||
floors: FloorRegistryEntry[],
|
floors: FloorRegistryEntry[],
|
||||||
@@ -388,7 +364,7 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
output.push(
|
output.push(
|
||||||
...floorAreas.map((area, index, array) => ({
|
...floorAreas.map((area) => ({
|
||||||
id: area.area_id,
|
id: area.area_id,
|
||||||
type: "area" as const,
|
type: "area" as const,
|
||||||
name: area.name,
|
name: area.name,
|
||||||
@@ -396,7 +372,6 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
|||||||
strings: [area.area_id, ...area.aliases, area.name],
|
strings: [area.area_id, ...area.aliases, area.name],
|
||||||
hasFloor: true,
|
hasFloor: true,
|
||||||
level: null,
|
level: null,
|
||||||
lastArea: index === array.length - 1,
|
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -470,7 +445,7 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
|||||||
.placeholder=${this.placeholder
|
.placeholder=${this.placeholder
|
||||||
? this.hass.areas[this.placeholder]?.name
|
? this.hass.areas[this.placeholder]?.name
|
||||||
: undefined}
|
: undefined}
|
||||||
.renderer=${this._rowRenderer}
|
.renderer=${rowRenderer}
|
||||||
@filter-changed=${this._filterChanged}
|
@filter-changed=${this._filterChanged}
|
||||||
@opened-changed=${this._openedChanged}
|
@opened-changed=${this._openedChanged}
|
||||||
@value-changed=${this._areaChanged}
|
@value-changed=${this._areaChanged}
|
||||||
|
@@ -428,8 +428,6 @@ export class HaAreaPicker extends LitElement {
|
|||||||
|
|
||||||
(ev.target as any).value = this._value;
|
(ev.target as any).value = this._value;
|
||||||
|
|
||||||
this.hass.loadFragmentTranslation("config");
|
|
||||||
|
|
||||||
showAreaRegistryDetailDialog(this, {
|
showAreaRegistryDetailDialog(this, {
|
||||||
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
|
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
|
||||||
createEntry: async (values) => {
|
createEntry: async (values) => {
|
||||||
|
@@ -14,8 +14,6 @@ export class HaCard extends LitElement {
|
|||||||
--ha-card-background,
|
--ha-card-background,
|
||||||
var(--card-background-color, white)
|
var(--card-background-color, white)
|
||||||
);
|
);
|
||||||
-webkit-backdrop-filter: var(--ha-card-backdrop-filter, none);
|
|
||||||
backdrop-filter: var(--ha-card-backdrop-filter, none);
|
|
||||||
box-shadow: var(--ha-card-box-shadow, none);
|
box-shadow: var(--ha-card-box-shadow, none);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-radius: var(--ha-card-border-radius, 12px);
|
border-radius: var(--ha-card-border-radius, 12px);
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
|
import "element-internals-polyfill";
|
||||||
import { MdCircularProgress } from "@material/web/progress/circular-progress";
|
import { MdCircularProgress } from "@material/web/progress/circular-progress";
|
||||||
import { PropertyValues, css } from "lit";
|
import { CSSResult, PropertyValues, css } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
|
|
||||||
@customElement("ha-circular-progress")
|
@customElement("ha-circular-progress")
|
||||||
@@ -31,15 +32,17 @@ export class HaCircularProgress extends MdCircularProgress {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static override styles = [
|
static get styles(): CSSResult[] {
|
||||||
...super.styles,
|
return [
|
||||||
css`
|
...super.styles,
|
||||||
:host {
|
css`
|
||||||
--md-sys-color-primary: var(--primary-color);
|
:host {
|
||||||
--md-circular-progress-size: 48px;
|
--md-sys-color-primary: var(--primary-color);
|
||||||
}
|
--md-circular-progress-size: 48px;
|
||||||
`,
|
}
|
||||||
];
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@@ -1,7 +1,14 @@
|
|||||||
|
import { Ripple } from "@material/mwc-ripple";
|
||||||
|
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import {
|
||||||
|
customElement,
|
||||||
|
eventOptions,
|
||||||
|
property,
|
||||||
|
queryAsync,
|
||||||
|
state,
|
||||||
|
} from "lit/decorators";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import "./ha-ripple";
|
|
||||||
|
|
||||||
@customElement("ha-control-button")
|
@customElement("ha-control-button")
|
||||||
export class HaControlButton extends LitElement {
|
export class HaControlButton extends LitElement {
|
||||||
@@ -9,6 +16,10 @@ export class HaControlButton extends LitElement {
|
|||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
|
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
|
||||||
|
|
||||||
|
@state() private _shouldRenderRipple = false;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<button
|
<button
|
||||||
@@ -17,13 +28,54 @@ export class HaControlButton extends LitElement {
|
|||||||
aria-label=${ifDefined(this.label)}
|
aria-label=${ifDefined(this.label)}
|
||||||
title=${ifDefined(this.label)}
|
title=${ifDefined(this.label)}
|
||||||
.disabled=${Boolean(this.disabled)}
|
.disabled=${Boolean(this.disabled)}
|
||||||
|
@focus=${this.handleRippleFocus}
|
||||||
|
@blur=${this.handleRippleBlur}
|
||||||
|
@mousedown=${this.handleRippleActivate}
|
||||||
|
@mouseup=${this.handleRippleDeactivate}
|
||||||
|
@mouseenter=${this.handleRippleMouseEnter}
|
||||||
|
@mouseleave=${this.handleRippleMouseLeave}
|
||||||
|
@touchstart=${this.handleRippleActivate}
|
||||||
|
@touchend=${this.handleRippleDeactivate}
|
||||||
|
@touchcancel=${this.handleRippleDeactivate}
|
||||||
>
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<ha-ripple .disabled=${this.disabled}></ha-ripple>
|
${this._shouldRenderRipple && !this.disabled
|
||||||
|
? html`<mwc-ripple></mwc-ripple>`
|
||||||
|
: ""}
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
|
||||||
|
this._shouldRenderRipple = true;
|
||||||
|
return this._ripple;
|
||||||
|
});
|
||||||
|
|
||||||
|
@eventOptions({ passive: true })
|
||||||
|
private handleRippleActivate(evt?: Event) {
|
||||||
|
this._rippleHandlers.startPress(evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRippleDeactivate() {
|
||||||
|
this._rippleHandlers.endPress();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRippleMouseEnter() {
|
||||||
|
this._rippleHandlers.startHover();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRippleMouseLeave() {
|
||||||
|
this._rippleHandlers.endHover();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRippleFocus() {
|
||||||
|
this._rippleHandlers.startFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRippleBlur() {
|
||||||
|
this._rippleHandlers.endFocus();
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
:host {
|
:host {
|
||||||
@@ -34,7 +86,6 @@ export class HaControlButton extends LitElement {
|
|||||||
--control-button-border-radius: 10px;
|
--control-button-border-radius: 10px;
|
||||||
--control-button-padding: 8px;
|
--control-button-padding: 8px;
|
||||||
--mdc-icon-size: 20px;
|
--mdc-icon-size: 20px;
|
||||||
--ha-ripple-color: var(--secondary-text-color);
|
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@@ -62,14 +113,12 @@ export class HaControlButton extends LitElement {
|
|||||||
outline: none;
|
outline: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: none;
|
background: none;
|
||||||
|
--mdc-ripple-color: var(--control-button-background-color);
|
||||||
/* For safari border-radius overflow */
|
/* For safari border-radius overflow */
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
.button:focus-visible {
|
|
||||||
--control-button-background-opacity: 0.4;
|
|
||||||
}
|
|
||||||
.button::before {
|
.button::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@@ -1,14 +1,22 @@
|
|||||||
|
import { Ripple } from "@material/mwc-ripple";
|
||||||
|
import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
|
||||||
import { SelectBase } from "@material/mwc-select/mwc-select-base";
|
import { SelectBase } from "@material/mwc-select/mwc-select-base";
|
||||||
import { mdiMenuDown } from "@mdi/js";
|
import { mdiMenuDown } from "@mdi/js";
|
||||||
import { css, html, nothing } from "lit";
|
import { css, html, nothing } from "lit";
|
||||||
import { customElement, property, query } from "lit/decorators";
|
import {
|
||||||
|
customElement,
|
||||||
|
eventOptions,
|
||||||
|
property,
|
||||||
|
query,
|
||||||
|
queryAsync,
|
||||||
|
state,
|
||||||
|
} from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import { debounce } from "../common/util/debounce";
|
import { debounce } from "../common/util/debounce";
|
||||||
import { nextRender } from "../common/util/render-status";
|
import { nextRender } from "../common/util/render-status";
|
||||||
import "./ha-icon";
|
import "./ha-icon";
|
||||||
import type { HaIcon } from "./ha-icon";
|
import type { HaIcon } from "./ha-icon";
|
||||||
import "./ha-ripple";
|
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
import type { HaSvgIcon } from "./ha-svg-icon";
|
import type { HaSvgIcon } from "./ha-svg-icon";
|
||||||
|
|
||||||
@@ -24,6 +32,10 @@ export class HaControlSelectMenu extends SelectBase {
|
|||||||
@property({ type: Boolean, attribute: "hide-label" })
|
@property({ type: Boolean, attribute: "hide-label" })
|
||||||
public hideLabel = false;
|
public hideLabel = false;
|
||||||
|
|
||||||
|
@queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>;
|
||||||
|
|
||||||
|
@state() private _shouldRenderRipple = false;
|
||||||
|
|
||||||
public override render() {
|
public override render() {
|
||||||
const classes = {
|
const classes = {
|
||||||
"select-disabled": this.disabled,
|
"select-disabled": this.disabled,
|
||||||
@@ -57,10 +69,17 @@ export class HaControlSelectMenu extends SelectBase {
|
|||||||
aria-labelledby=${ifDefined(labelledby)}
|
aria-labelledby=${ifDefined(labelledby)}
|
||||||
aria-label=${ifDefined(labelAttribute)}
|
aria-label=${ifDefined(labelAttribute)}
|
||||||
aria-required=${this.required}
|
aria-required=${this.required}
|
||||||
|
@click=${this.onClick}
|
||||||
@focus=${this.onFocus}
|
@focus=${this.onFocus}
|
||||||
@blur=${this.onBlur}
|
@blur=${this.onBlur}
|
||||||
@click=${this.onClick}
|
|
||||||
@keydown=${this.onKeydown}
|
@keydown=${this.onKeydown}
|
||||||
|
@mousedown=${this.handleRippleActivate}
|
||||||
|
@mouseup=${this.handleRippleDeactivate}
|
||||||
|
@mouseenter=${this.handleRippleMouseEnter}
|
||||||
|
@mouseleave=${this.handleRippleMouseLeave}
|
||||||
|
@touchstart=${this.handleRippleActivate}
|
||||||
|
@touchend=${this.handleRippleDeactivate}
|
||||||
|
@touchcancel=${this.handleRippleDeactivate}
|
||||||
>
|
>
|
||||||
${this.renderIcon()}
|
${this.renderIcon()}
|
||||||
<div class="content">
|
<div class="content">
|
||||||
@@ -72,7 +91,9 @@ export class HaControlSelectMenu extends SelectBase {
|
|||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this.renderArrow()}
|
${this.renderArrow()}
|
||||||
<ha-ripple .disabled=${this.disabled}></ha-ripple>
|
${this._shouldRenderRipple && !this.disabled
|
||||||
|
? html` <mwc-ripple></mwc-ripple> `
|
||||||
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this.renderMenu()}
|
${this.renderMenu()}
|
||||||
</div>
|
</div>
|
||||||
@@ -114,6 +135,46 @@ export class HaControlSelectMenu extends SelectBase {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected onFocus() {
|
||||||
|
this.handleRippleFocus();
|
||||||
|
super.onFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onBlur() {
|
||||||
|
this.handleRippleBlur();
|
||||||
|
super.onBlur();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _rippleHandlers: RippleHandlers = new RippleHandlers(() => {
|
||||||
|
this._shouldRenderRipple = true;
|
||||||
|
return this._ripple;
|
||||||
|
});
|
||||||
|
|
||||||
|
@eventOptions({ passive: true })
|
||||||
|
private handleRippleActivate(evt?: Event) {
|
||||||
|
this._rippleHandlers.startPress(evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRippleDeactivate() {
|
||||||
|
this._rippleHandlers.endPress();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRippleMouseEnter() {
|
||||||
|
this._rippleHandlers.startHover();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRippleMouseLeave() {
|
||||||
|
this._rippleHandlers.endHover();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRippleFocus() {
|
||||||
|
this._rippleHandlers.startFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRippleBlur() {
|
||||||
|
this._rippleHandlers.endFocus();
|
||||||
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
window.addEventListener("translations-updated", this._translationsUpdated);
|
window.addEventListener("translations-updated", this._translationsUpdated);
|
||||||
@@ -143,7 +204,6 @@ export class HaControlSelectMenu extends SelectBase {
|
|||||||
--control-select-menu-height: 48px;
|
--control-select-menu-height: 48px;
|
||||||
--control-select-menu-padding: 6px 10px;
|
--control-select-menu-padding: 6px 10px;
|
||||||
--mdc-icon-size: 20px;
|
--mdc-icon-size: 20px;
|
||||||
--ha-ripple-color: var(--secondary-text-color);
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
width: auto;
|
width: auto;
|
||||||
@@ -164,6 +224,7 @@ export class HaControlSelectMenu extends SelectBase {
|
|||||||
outline: none;
|
outline: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: none;
|
background: none;
|
||||||
|
--mdc-ripple-color: var(--control-select-menu-background-color);
|
||||||
/* For safari border-radius overflow */
|
/* For safari border-radius overflow */
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
transition: color 180ms ease-in-out;
|
transition: color 180ms ease-in-out;
|
||||||
@@ -203,10 +264,6 @@ export class HaControlSelectMenu extends SelectBase {
|
|||||||
letter-spacing: inherit;
|
letter-spacing: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-anchor:focus-visible {
|
|
||||||
--control-select-menu-background-opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-anchor::before {
|
.select-anchor::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@@ -67,9 +67,6 @@ export class HaControlSlider extends LitElement {
|
|||||||
@property({ attribute: "tooltip-mode" })
|
@property({ attribute: "tooltip-mode" })
|
||||||
public tooltipMode: TooltipMode = "interaction";
|
public tooltipMode: TooltipMode = "interaction";
|
||||||
|
|
||||||
@property({ attribute: "touch-action" })
|
|
||||||
public touchAction?: string;
|
|
||||||
|
|
||||||
@property({ type: Number })
|
@property({ type: Number })
|
||||||
public value?: number;
|
public value?: number;
|
||||||
|
|
||||||
@@ -155,7 +152,7 @@ export class HaControlSlider extends LitElement {
|
|||||||
setupListeners() {
|
setupListeners() {
|
||||||
if (this.slider && !this._mc) {
|
if (this.slider && !this._mc) {
|
||||||
this._mc = new Manager(this.slider, {
|
this._mc = new Manager(this.slider, {
|
||||||
touchAction: this.touchAction ?? (this.vertical ? "pan-x" : "pan-y"),
|
touchAction: this.vertical ? "pan-x" : "pan-y",
|
||||||
});
|
});
|
||||||
this._mc.add(
|
this._mc.add(
|
||||||
new Pan({
|
new Pan({
|
||||||
|
@@ -33,9 +33,6 @@ export class HaControlSwitch extends LitElement {
|
|||||||
// SVG icon path (if you need a non SVG icon instead, use the provided off icon slot to pass an <ha-icon slot="icon-off"> in)
|
// SVG icon path (if you need a non SVG icon instead, use the provided off icon slot to pass an <ha-icon slot="icon-off"> in)
|
||||||
@property({ type: String }) pathOff?: string;
|
@property({ type: String }) pathOff?: string;
|
||||||
|
|
||||||
@property({ attribute: "touch-action" })
|
|
||||||
public touchAction?: string;
|
|
||||||
|
|
||||||
private _mc?: HammerManager;
|
private _mc?: HammerManager;
|
||||||
|
|
||||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||||
@@ -76,7 +73,7 @@ export class HaControlSwitch extends LitElement {
|
|||||||
setupListeners() {
|
setupListeners() {
|
||||||
if (this.switch && !this._mc) {
|
if (this.switch && !this._mc) {
|
||||||
this._mc = new Manager(this.switch, {
|
this._mc = new Manager(this.switch, {
|
||||||
touchAction: this.touchAction ?? (this.vertical ? "pan-x" : "pan-y"),
|
touchAction: this.vertical ? "pan-x" : "pan-y",
|
||||||
});
|
});
|
||||||
this._mc.add(
|
this._mc.add(
|
||||||
new Swipe({
|
new Swipe({
|
||||||
|
@@ -19,7 +19,6 @@ import { HomeAssistant } from "../types";
|
|||||||
import "./ha-list-item";
|
import "./ha-list-item";
|
||||||
import "./ha-select";
|
import "./ha-select";
|
||||||
import type { HaSelect } from "./ha-select";
|
import type { HaSelect } from "./ha-select";
|
||||||
import { getExtendedEntityRegistryEntry } from "../data/entity_registry";
|
|
||||||
|
|
||||||
const NONE = "__NONE_OPTION__";
|
const NONE = "__NONE_OPTION__";
|
||||||
|
|
||||||
@@ -108,23 +107,13 @@ export class HaConversationAgentPicker extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _maybeFetchConfigEntry() {
|
private async _maybeFetchConfigEntry() {
|
||||||
if (!this.value || !(this.value in this.hass.entities)) {
|
if (!this.value || this.value === "homeassistant") {
|
||||||
this._configEntry = undefined;
|
this._configEntry = undefined;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const regEntry = await getExtendedEntityRegistryEntry(
|
|
||||||
this.hass,
|
|
||||||
this.value
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!regEntry.config_entry_id) {
|
|
||||||
this._configEntry = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._configEntry = (
|
this._configEntry = (
|
||||||
await getConfigEntry(this.hass, regEntry.config_entry_id)
|
await getConfigEntry(this.hass, this.value)
|
||||||
).config_entry;
|
).config_entry;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._configEntry = undefined;
|
this._configEntry = undefined;
|
||||||
|
@@ -4,6 +4,7 @@ import memoizeOne from "memoize-one";
|
|||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||||
|
import "../resources/intl-polyfill";
|
||||||
import "./ha-list-item";
|
import "./ha-list-item";
|
||||||
import "./ha-select";
|
import "./ha-select";
|
||||||
import type { HaSelect } from "./ha-select";
|
import type { HaSelect } from "./ha-select";
|
||||||
@@ -281,10 +282,14 @@ export class HaCountryPicker extends LitElement {
|
|||||||
private _getOptions = memoizeOne(
|
private _getOptions = memoizeOne(
|
||||||
(language?: string, countries?: string[]) => {
|
(language?: string, countries?: string[]) => {
|
||||||
let options: { label: string; value: string }[] = [];
|
let options: { label: string; value: string }[] = [];
|
||||||
const countryDisplayNames = new Intl.DisplayNames(language, {
|
const countryDisplayNames =
|
||||||
type: "region",
|
Intl && "DisplayNames" in Intl
|
||||||
fallback: "code",
|
? new Intl.DisplayNames(language, {
|
||||||
});
|
type: "region",
|
||||||
|
fallback: "code",
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (countries) {
|
if (countries) {
|
||||||
options = countries.map((country) => ({
|
options = countries.map((country) => ({
|
||||||
value: country,
|
value: country,
|
||||||
|
@@ -4,6 +4,7 @@ import memoizeOne from "memoize-one";
|
|||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||||
|
import "../resources/intl-polyfill";
|
||||||
import "./ha-list-item";
|
import "./ha-list-item";
|
||||||
import "./ha-select";
|
import "./ha-select";
|
||||||
import type { HaSelect } from "./ha-select";
|
import type { HaSelect } from "./ha-select";
|
||||||
@@ -169,9 +170,12 @@ const CURRENCIES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const curSymbol = (currency: string, locale?: string) =>
|
const curSymbol = (currency: string, locale?: string) =>
|
||||||
new Intl.NumberFormat(locale, { style: "currency", currency })
|
Intl && "NumberFormat" in Intl
|
||||||
.formatToParts(1)
|
? new Intl.NumberFormat(locale, { style: "currency", currency })
|
||||||
.find((x) => x.type === "currency")?.value;
|
.formatToParts(1)
|
||||||
|
.find((x) => x.type === "currency")?.value
|
||||||
|
: currency;
|
||||||
|
|
||||||
@customElement("ha-currency-picker")
|
@customElement("ha-currency-picker")
|
||||||
export class HaCurrencyPicker extends LitElement {
|
export class HaCurrencyPicker extends LitElement {
|
||||||
@property() public language = "en";
|
@property() public language = "en";
|
||||||
@@ -185,10 +189,13 @@ export class HaCurrencyPicker extends LitElement {
|
|||||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||||
|
|
||||||
private _getOptions = memoizeOne((language?: string) => {
|
private _getOptions = memoizeOne((language?: string) => {
|
||||||
const currencyDisplayNames = new Intl.DisplayNames(language, {
|
const currencyDisplayNames =
|
||||||
type: "currency",
|
Intl && "DisplayNames" in Intl
|
||||||
fallback: "code",
|
? new Intl.DisplayNames(language, {
|
||||||
});
|
type: "currency",
|
||||||
|
fallback: "code",
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
const options = CURRENCIES.map((currency) => ({
|
const options = CURRENCIES.map((currency) => ({
|
||||||
value: currency,
|
value: currency,
|
||||||
label: `${
|
label: `${
|
||||||
|
@@ -75,14 +75,8 @@ export class HaDialog extends DialogBase {
|
|||||||
var(--divider-color)
|
var(--divider-color)
|
||||||
);
|
);
|
||||||
z-index: var(--dialog-z-index, 8);
|
z-index: var(--dialog-z-index, 8);
|
||||||
-webkit-backdrop-filter: var(
|
-webkit-backdrop-filter: var(--dialog-backdrop-filter, none);
|
||||||
--ha-dialog-scrim-backdrop-filter,
|
backdrop-filter: var(--dialog-backdrop-filter, none);
|
||||||
var(--dialog-backdrop-filter, none)
|
|
||||||
);
|
|
||||||
backdrop-filter: var(
|
|
||||||
--ha-dialog-scrim-backdrop-filter,
|
|
||||||
var(--dialog-backdrop-filter, none)
|
|
||||||
);
|
|
||||||
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
|
--mdc-dialog-box-shadow: var(--dialog-box-shadow, none);
|
||||||
--mdc-typography-headline6-font-weight: 400;
|
--mdc-typography-headline6-font-weight: 400;
|
||||||
--mdc-typography-headline6-font-size: 1.574rem;
|
--mdc-typography-headline6-font-size: 1.574rem;
|
||||||
@@ -125,12 +119,6 @@ export class HaDialog extends DialogBase {
|
|||||||
margin-top: var(--dialog-surface-margin-top);
|
margin-top: var(--dialog-surface-margin-top);
|
||||||
min-height: var(--mdc-dialog-min-height, auto);
|
min-height: var(--mdc-dialog-min-height, auto);
|
||||||
border-radius: var(--ha-dialog-border-radius, 28px);
|
border-radius: var(--ha-dialog-border-radius, 28px);
|
||||||
-webkit-backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
|
|
||||||
backdrop-filter: var(--ha-dialog-surface-backdrop-filter, none);
|
|
||||||
background: var(
|
|
||||||
--ha-dialog-surface-background,
|
|
||||||
var(--mdc-theme-surface, #fff)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@@ -21,8 +21,6 @@ export class HaExpansionPanel extends LitElement {
|
|||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) leftChevron = false;
|
@property({ type: Boolean, reflect: true }) leftChevron = false;
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) noCollapse = false;
|
|
||||||
|
|
||||||
@property() header?: string;
|
@property() header?: string;
|
||||||
|
|
||||||
@property() secondary?: string;
|
@property() secondary?: string;
|
||||||
@@ -36,17 +34,16 @@ export class HaExpansionPanel extends LitElement {
|
|||||||
<div class="top ${classMap({ expanded: this.expanded })}">
|
<div class="top ${classMap({ expanded: this.expanded })}">
|
||||||
<div
|
<div
|
||||||
id="summary"
|
id="summary"
|
||||||
class=${classMap({ noCollapse: this.noCollapse })}
|
|
||||||
@click=${this._toggleContainer}
|
@click=${this._toggleContainer}
|
||||||
@keydown=${this._toggleContainer}
|
@keydown=${this._toggleContainer}
|
||||||
@focus=${this._focusChanged}
|
@focus=${this._focusChanged}
|
||||||
@blur=${this._focusChanged}
|
@blur=${this._focusChanged}
|
||||||
role="button"
|
role="button"
|
||||||
tabindex=${this.noCollapse ? -1 : 0}
|
tabindex="0"
|
||||||
aria-expanded=${this.expanded}
|
aria-expanded=${this.expanded}
|
||||||
aria-controls="sect1"
|
aria-controls="sect1"
|
||||||
>
|
>
|
||||||
${this.leftChevron && !this.noCollapse
|
${this.leftChevron
|
||||||
? html`
|
? html`
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
.path=${mdiChevronDown}
|
.path=${mdiChevronDown}
|
||||||
@@ -60,7 +57,7 @@ export class HaExpansionPanel extends LitElement {
|
|||||||
<slot class="secondary" name="secondary">${this.secondary}</slot>
|
<slot class="secondary" name="secondary">${this.secondary}</slot>
|
||||||
</div>
|
</div>
|
||||||
</slot>
|
</slot>
|
||||||
${!this.leftChevron && !this.noCollapse
|
${!this.leftChevron
|
||||||
? html`
|
? html`
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
.path=${mdiChevronDown}
|
.path=${mdiChevronDown}
|
||||||
@@ -109,9 +106,6 @@ export class HaExpansionPanel extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
if (this.noCollapse) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newExpanded = !this.expanded;
|
const newExpanded = !this.expanded;
|
||||||
fireEvent(this, "expanded-will-change", { expanded: newExpanded });
|
fireEvent(this, "expanded-will-change", { expanded: newExpanded });
|
||||||
this._container.style.overflow = "hidden";
|
this._container.style.overflow = "hidden";
|
||||||
@@ -136,9 +130,6 @@ export class HaExpansionPanel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _focusChanged(ev) {
|
private _focusChanged(ev) {
|
||||||
if (this.noCollapse) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.shadowRoot!.querySelector(".top")!.classList.toggle(
|
this.shadowRoot!.querySelector(".top")!.classList.toggle(
|
||||||
"focused",
|
"focused",
|
||||||
ev.type === "focus"
|
ev.type === "focus"
|
||||||
@@ -200,9 +191,6 @@ export class HaExpansionPanel extends LitElement {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
#summary.noCollapse {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-icon.expanded {
|
.summary-icon.expanded {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
|
@@ -1,20 +1,12 @@
|
|||||||
import { SelectedDetail } from "@material/mwc-list";
|
import { SelectedDetail } from "@material/mwc-list";
|
||||||
import "@material/mwc-menu/mwc-menu-surface";
|
import "@material/mwc-menu/mwc-menu-surface";
|
||||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
import {
|
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
|
||||||
html,
|
|
||||||
LitElement,
|
|
||||||
nothing,
|
|
||||||
PropertyValues,
|
|
||||||
} from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { Blueprints, fetchBlueprints } from "../data/blueprint";
|
|
||||||
import { findRelated, RelatedResult } from "../data/search";
|
import { findRelated, RelatedResult } from "../data/search";
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
|
import { Blueprints, fetchBlueprints } from "../data/blueprint";
|
||||||
|
|
||||||
@customElement("ha-filter-blueprints")
|
@customElement("ha-filter-blueprints")
|
||||||
export class HaFilterBlueprints extends LitElement {
|
export class HaFilterBlueprints extends LitElement {
|
||||||
@@ -32,16 +24,6 @@ export class HaFilterBlueprints extends LitElement {
|
|||||||
|
|
||||||
@state() private _blueprints?: Blueprints;
|
@state() private _blueprints?: Blueprints;
|
||||||
|
|
||||||
public willUpdate(properties: PropertyValues) {
|
|
||||||
super.willUpdate(properties);
|
|
||||||
|
|
||||||
if (!this.hasUpdated) {
|
|
||||||
if (this.value?.length) {
|
|
||||||
this._findRelated();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<ha-expansion-panel
|
<ha-expansion-panel
|
||||||
@@ -53,11 +35,7 @@ export class HaFilterBlueprints extends LitElement {
|
|||||||
<div slot="header" class="header">
|
<div slot="header" class="header">
|
||||||
${this.hass.localize("ui.panel.config.blueprint.caption")}
|
${this.hass.localize("ui.panel.config.blueprint.caption")}
|
||||||
${this.value?.length
|
${this.value?.length
|
||||||
? html`<div class="badge">${this.value?.length}</div>
|
? html`<div class="badge">${this.value?.length}</div>`
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiFilterVariantRemove}
|
|
||||||
@click=${this._clearFilter}
|
|
||||||
></ha-icon-button>`
|
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._blueprints && this._shouldRender
|
${this._blueprints && this._shouldRender
|
||||||
@@ -113,6 +91,7 @@ export class HaFilterBlueprints extends LitElement {
|
|||||||
ev: CustomEvent<SelectedDetail<Set<number>>>
|
ev: CustomEvent<SelectedDetail<Set<number>>>
|
||||||
) {
|
) {
|
||||||
const blueprints = this._blueprints!;
|
const blueprints = this._blueprints!;
|
||||||
|
const relatedPromises: Promise<RelatedResult>[] = [];
|
||||||
|
|
||||||
if (!ev.detail.index.size) {
|
if (!ev.detail.index.size) {
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
@@ -128,33 +107,13 @@ export class HaFilterBlueprints extends LitElement {
|
|||||||
for (const index of ev.detail.index) {
|
for (const index of ev.detail.index) {
|
||||||
const blueprintId = Object.keys(blueprints)[index];
|
const blueprintId = Object.keys(blueprints)[index];
|
||||||
value.push(blueprintId);
|
value.push(blueprintId);
|
||||||
}
|
|
||||||
|
|
||||||
this.value = value;
|
|
||||||
|
|
||||||
this._findRelated();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _findRelated() {
|
|
||||||
if (!this.value?.length) {
|
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
|
||||||
value: [],
|
|
||||||
items: undefined,
|
|
||||||
});
|
|
||||||
this.value = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const relatedPromises: Promise<RelatedResult>[] = [];
|
|
||||||
|
|
||||||
for (const blueprintId of this.value) {
|
|
||||||
if (this.type) {
|
if (this.type) {
|
||||||
relatedPromises.push(
|
relatedPromises.push(
|
||||||
findRelated(this.hass, `${this.type}_blueprint`, blueprintId)
|
findRelated(this.hass, `${this.type}_blueprint`, blueprintId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.value = value;
|
||||||
const results = await Promise.all(relatedPromises);
|
const results = await Promise.all(relatedPromises);
|
||||||
const items: Set<string> = new Set();
|
const items: Set<string> = new Set();
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
@@ -164,20 +123,11 @@ export class HaFilterBlueprints extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
value: this.value,
|
value,
|
||||||
items: this.type ? items : undefined,
|
items: this.type ? items : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearFilter(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
this.value = undefined;
|
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
|
||||||
value: undefined,
|
|
||||||
items: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
@@ -197,10 +147,6 @@ export class HaFilterBlueprints extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.header ha-icon-button {
|
|
||||||
margin-inline-start: auto;
|
|
||||||
margin-inline-end: 8px;
|
|
||||||
}
|
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
@@ -2,7 +2,6 @@ import { ActionDetail, SelectedDetail } from "@material/mwc-list";
|
|||||||
import {
|
import {
|
||||||
mdiDelete,
|
mdiDelete,
|
||||||
mdiDotsVertical,
|
mdiDotsVertical,
|
||||||
mdiFilterVariantRemove,
|
|
||||||
mdiPencil,
|
mdiPencil,
|
||||||
mdiPlus,
|
mdiPlus,
|
||||||
mdiTag,
|
mdiTag,
|
||||||
@@ -69,11 +68,7 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
|||||||
<div slot="header" class="header">
|
<div slot="header" class="header">
|
||||||
${this.hass.localize("ui.panel.config.category.caption")}
|
${this.hass.localize("ui.panel.config.category.caption")}
|
||||||
${this.value?.length
|
${this.value?.length
|
||||||
? html`<div class="badge">${this.value?.length}</div>
|
? html`<div class="badge">${this.value?.length}</div>`
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiFilterVariantRemove}
|
|
||||||
@click=${this._clearFilter}
|
|
||||||
></ha-icon-button>`
|
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._shouldRender
|
${this._shouldRender
|
||||||
@@ -259,15 +254,6 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearFilter(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
this.value = undefined;
|
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
|
||||||
value: undefined,
|
|
||||||
items: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
@@ -288,10 +274,6 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.header ha-icon-button {
|
|
||||||
margin-inline-start: auto;
|
|
||||||
margin-inline-end: 8px;
|
|
||||||
}
|
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@@ -14,11 +13,10 @@ import { stringCompare } from "../common/string/compare";
|
|||||||
import { computeDeviceName } from "../data/device_registry";
|
import { computeDeviceName } from "../data/device_registry";
|
||||||
import { findRelated, RelatedResult } from "../data/search";
|
import { findRelated, RelatedResult } from "../data/search";
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import { loadVirtualizer } from "../resources/virtualizer";
|
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-check-list-item";
|
|
||||||
import "./ha-expansion-panel";
|
import "./ha-expansion-panel";
|
||||||
import "./search-input-outlined";
|
import "./ha-check-list-item";
|
||||||
|
import { loadVirtualizer } from "../resources/virtualizer";
|
||||||
|
|
||||||
@customElement("ha-filter-devices")
|
@customElement("ha-filter-devices")
|
||||||
export class HaFilterDevices extends LitElement {
|
export class HaFilterDevices extends LitElement {
|
||||||
@@ -34,16 +32,11 @@ export class HaFilterDevices extends LitElement {
|
|||||||
|
|
||||||
@state() private _shouldRender = false;
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
@state() private _filter?: string;
|
|
||||||
|
|
||||||
public willUpdate(properties: PropertyValues) {
|
public willUpdate(properties: PropertyValues) {
|
||||||
super.willUpdate(properties);
|
super.willUpdate(properties);
|
||||||
|
|
||||||
if (!this.hasUpdated) {
|
if (!this.hasUpdated) {
|
||||||
loadVirtualizer();
|
loadVirtualizer();
|
||||||
if (this.value?.length) {
|
|
||||||
this._findRelated();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,33 +51,19 @@ export class HaFilterDevices extends LitElement {
|
|||||||
<div slot="header" class="header">
|
<div slot="header" class="header">
|
||||||
${this.hass.localize("ui.panel.config.devices.caption")}
|
${this.hass.localize("ui.panel.config.devices.caption")}
|
||||||
${this.value?.length
|
${this.value?.length
|
||||||
? html`<div class="badge">${this.value?.length}</div>
|
? html`<div class="badge">${this.value?.length}</div>`
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiFilterVariantRemove}
|
|
||||||
@click=${this._clearFilter}
|
|
||||||
></ha-icon-button>`
|
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._shouldRender
|
${this._shouldRender
|
||||||
? html`<search-input-outlined
|
? html`<mwc-list class="ha-scrollbar">
|
||||||
.hass=${this.hass}
|
<lit-virtualizer
|
||||||
.filter=${this._filter}
|
.items=${this._devices(this.hass.devices, this.value)}
|
||||||
@value-changed=${this._handleSearchChange}
|
.keyFunction=${this._keyFunction}
|
||||||
|
.renderItem=${this._renderItem}
|
||||||
|
@click=${this._handleItemClick}
|
||||||
>
|
>
|
||||||
</search-input-outlined>
|
</lit-virtualizer>
|
||||||
<mwc-list class="ha-scrollbar" multi>
|
</mwc-list>`
|
||||||
<lit-virtualizer
|
|
||||||
.items=${this._devices(
|
|
||||||
this.hass.devices,
|
|
||||||
this._filter || "",
|
|
||||||
this.value
|
|
||||||
)}
|
|
||||||
.keyFunction=${this._keyFunction}
|
|
||||||
.renderItem=${this._renderItem}
|
|
||||||
@click=${this._handleItemClick}
|
|
||||||
>
|
|
||||||
</lit-virtualizer>
|
|
||||||
</mwc-list>`
|
|
||||||
: nothing}
|
: nothing}
|
||||||
</ha-expansion-panel>
|
</ha-expansion-panel>
|
||||||
`;
|
`;
|
||||||
@@ -93,14 +72,12 @@ export class HaFilterDevices extends LitElement {
|
|||||||
private _keyFunction = (device) => device?.id;
|
private _keyFunction = (device) => device?.id;
|
||||||
|
|
||||||
private _renderItem = (device) =>
|
private _renderItem = (device) =>
|
||||||
!device
|
html`<ha-check-list-item
|
||||||
? nothing
|
.value=${device.id}
|
||||||
: html`<ha-check-list-item
|
.selected=${this.value?.includes(device.id)}
|
||||||
.value=${device.id}
|
>
|
||||||
.selected=${this.value?.includes(device.id) ?? false}
|
${computeDeviceName(device, this.hass)}
|
||||||
>
|
</ha-check-list-item>`;
|
||||||
${computeDeviceName(device, this.hass)}
|
|
||||||
</ha-check-list-item>`;
|
|
||||||
|
|
||||||
private _handleItemClick(ev) {
|
private _handleItemClick(ev) {
|
||||||
const listItem = ev.target.closest("ha-check-list-item");
|
const listItem = ev.target.closest("ha-check-list-item");
|
||||||
@@ -122,7 +99,7 @@ export class HaFilterDevices extends LitElement {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.expanded) return;
|
if (!this.expanded) return;
|
||||||
this.renderRoot.querySelector("mwc-list")!.style.height =
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
`${this.clientHeight - 49}px`;
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,28 +112,16 @@ export class HaFilterDevices extends LitElement {
|
|||||||
this.expanded = ev.detail.expanded;
|
this.expanded = ev.detail.expanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleSearchChange(ev: CustomEvent) {
|
private _devices = memoizeOne((devices: HomeAssistant["devices"], _value) => {
|
||||||
this._filter = ev.detail.value.toLowerCase();
|
const values = Object.values(devices);
|
||||||
}
|
return values.sort((a, b) =>
|
||||||
|
stringCompare(
|
||||||
private _devices = memoizeOne(
|
a.name_by_user || a.name || "",
|
||||||
(devices: HomeAssistant["devices"], filter: string, _value) => {
|
b.name_by_user || b.name || "",
|
||||||
const values = Object.values(devices);
|
this.hass.locale.language
|
||||||
return values
|
)
|
||||||
.filter(
|
);
|
||||||
(device) =>
|
});
|
||||||
!filter ||
|
|
||||||
computeDeviceName(device, this.hass).toLowerCase().includes(filter)
|
|
||||||
)
|
|
||||||
.sort((a, b) =>
|
|
||||||
stringCompare(
|
|
||||||
computeDeviceName(a, this.hass),
|
|
||||||
computeDeviceName(b, this.hass),
|
|
||||||
this.hass.locale.language
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private async _findRelated() {
|
private async _findRelated() {
|
||||||
const relatedPromises: Promise<RelatedResult>[] = [];
|
const relatedPromises: Promise<RelatedResult>[] = [];
|
||||||
@@ -193,15 +158,6 @@ export class HaFilterDevices extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearFilter(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
this.value = undefined;
|
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
|
||||||
value: undefined,
|
|
||||||
items: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
@@ -222,10 +178,6 @@ export class HaFilterDevices extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.header ha-icon-button {
|
|
||||||
margin-inline-start: auto;
|
|
||||||
margin-inline-end: 8px;
|
|
||||||
}
|
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
@@ -245,10 +197,6 @@ export class HaFilterDevices extends LitElement {
|
|||||||
ha-check-list-item {
|
ha-check-list-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
search-input-outlined {
|
|
||||||
display: block;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -1,204 +0,0 @@
|
|||||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import { repeat } from "lit/directives/repeat";
|
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
|
||||||
import { stringCompare } from "../common/string/compare";
|
|
||||||
import { domainToName } from "../data/integration";
|
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
|
||||||
import type { HomeAssistant } from "../types";
|
|
||||||
import "./ha-domain-icon";
|
|
||||||
import "./search-input-outlined";
|
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
|
||||||
|
|
||||||
@customElement("ha-filter-domains")
|
|
||||||
export class HaFilterDomains extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public value?: string[];
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public narrow = false;
|
|
||||||
|
|
||||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
|
||||||
|
|
||||||
@state() private _shouldRender = false;
|
|
||||||
|
|
||||||
@state() private _filter?: string;
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
return html`
|
|
||||||
<ha-expansion-panel
|
|
||||||
leftChevron
|
|
||||||
.expanded=${this.expanded}
|
|
||||||
@expanded-will-change=${this._expandedWillChange}
|
|
||||||
@expanded-changed=${this._expandedChanged}
|
|
||||||
>
|
|
||||||
<div slot="header" class="header">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.entities.picker.headers.domain"
|
|
||||||
)}
|
|
||||||
${this.value?.length
|
|
||||||
? html`<div class="badge">${this.value?.length}</div>
|
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiFilterVariantRemove}
|
|
||||||
@click=${this._clearFilter}
|
|
||||||
></ha-icon-button>`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
${this._shouldRender
|
|
||||||
? html`<search-input-outlined
|
|
||||||
.hass=${this.hass}
|
|
||||||
.filter=${this._filter}
|
|
||||||
@value-changed=${this._handleSearchChange}
|
|
||||||
>
|
|
||||||
</search-input-outlined>
|
|
||||||
<mwc-list
|
|
||||||
class="ha-scrollbar"
|
|
||||||
@click=${this._handleItemClick}
|
|
||||||
multi
|
|
||||||
>
|
|
||||||
${repeat(
|
|
||||||
this._domains(this.hass.states, this._filter),
|
|
||||||
(i) => i,
|
|
||||||
(domain) =>
|
|
||||||
html`<ha-check-list-item
|
|
||||||
.value=${domain}
|
|
||||||
.selected=${(this.value || []).includes(domain)}
|
|
||||||
graphic="icon"
|
|
||||||
>
|
|
||||||
<ha-domain-icon
|
|
||||||
slot="graphic"
|
|
||||||
.hass=${this.hass}
|
|
||||||
.domain=${domain}
|
|
||||||
brandFallback
|
|
||||||
></ha-domain-icon>
|
|
||||||
${domainToName(this.hass.localize, domain)}
|
|
||||||
</ha-check-list-item>`
|
|
||||||
)}
|
|
||||||
</mwc-list> `
|
|
||||||
: nothing}
|
|
||||||
</ha-expansion-panel>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _domains = memoizeOne((states, filter) => {
|
|
||||||
const domains = new Set<string>();
|
|
||||||
Object.keys(states).forEach((entityId) => {
|
|
||||||
domains.add(computeDomain(entityId));
|
|
||||||
});
|
|
||||||
|
|
||||||
return Array.from(domains.values())
|
|
||||||
.filter(
|
|
||||||
(entry) =>
|
|
||||||
!filter ||
|
|
||||||
entry.toLowerCase().includes(filter) ||
|
|
||||||
domainToName(this.hass.localize, entry).toLowerCase().includes(filter)
|
|
||||||
)
|
|
||||||
.sort((a, b) => stringCompare(a, b, this.hass.locale.language));
|
|
||||||
});
|
|
||||||
|
|
||||||
protected updated(changed) {
|
|
||||||
if (changed.has("expanded") && this.expanded) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!this.expanded) return;
|
|
||||||
this.renderRoot.querySelector("mwc-list")!.style.height =
|
|
||||||
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _expandedWillChange(ev) {
|
|
||||||
this._shouldRender = ev.detail.expanded;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _expandedChanged(ev) {
|
|
||||||
this.expanded = ev.detail.expanded;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleItemClick(ev) {
|
|
||||||
const listItem = ev.target.closest("ha-check-list-item");
|
|
||||||
const value = listItem?.value;
|
|
||||||
if (!value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.value?.includes(value)) {
|
|
||||||
this.value = this.value?.filter((val) => val !== value);
|
|
||||||
} else {
|
|
||||||
this.value = [...(this.value || []), value];
|
|
||||||
}
|
|
||||||
|
|
||||||
listItem.selected = this.value.includes(value);
|
|
||||||
|
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
|
||||||
value: this.value,
|
|
||||||
items: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _clearFilter(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
this.value = undefined;
|
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
|
||||||
value: undefined,
|
|
||||||
items: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleSearchChange(ev: CustomEvent) {
|
|
||||||
this._filter = ev.detail.value.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
haStyleScrollbar,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
border-bottom: 1px solid var(--divider-color);
|
|
||||||
}
|
|
||||||
:host([expanded]) {
|
|
||||||
flex: 1;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
ha-expansion-panel {
|
|
||||||
--ha-card-border-radius: 0;
|
|
||||||
--expansion-panel-content-padding: 0;
|
|
||||||
}
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.header ha-icon-button {
|
|
||||||
margin-inline-start: initial;
|
|
||||||
margin-inline-end: 8px;
|
|
||||||
}
|
|
||||||
.badge {
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 8px;
|
|
||||||
margin-inline-start: 8px;
|
|
||||||
margin-inline-end: initial;
|
|
||||||
min-width: 16px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: 50%;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 11px;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
line-height: 16px;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0px 2px;
|
|
||||||
color: var(--text-primary-color);
|
|
||||||
}
|
|
||||||
search-input-outlined {
|
|
||||||
display: block;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"ha-filter-domains": HaFilterDomains;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,4 +1,3 @@
|
|||||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
@@ -15,11 +14,10 @@ import { computeStateName } from "../common/entity/compute_state_name";
|
|||||||
import { stringCompare } from "../common/string/compare";
|
import { stringCompare } from "../common/string/compare";
|
||||||
import { findRelated, RelatedResult } from "../data/search";
|
import { findRelated, RelatedResult } from "../data/search";
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import { loadVirtualizer } from "../resources/virtualizer";
|
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-check-list-item";
|
|
||||||
import "./ha-state-icon";
|
import "./ha-state-icon";
|
||||||
import "./search-input-outlined";
|
import "./ha-check-list-item";
|
||||||
|
import { loadVirtualizer } from "../resources/virtualizer";
|
||||||
|
|
||||||
@customElement("ha-filter-entities")
|
@customElement("ha-filter-entities")
|
||||||
export class HaFilterEntities extends LitElement {
|
export class HaFilterEntities extends LitElement {
|
||||||
@@ -35,16 +33,11 @@ export class HaFilterEntities extends LitElement {
|
|||||||
|
|
||||||
@state() private _shouldRender = false;
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
@state() private _filter?: string;
|
|
||||||
|
|
||||||
public willUpdate(properties: PropertyValues) {
|
public willUpdate(properties: PropertyValues) {
|
||||||
super.willUpdate(properties);
|
super.willUpdate(properties);
|
||||||
|
|
||||||
if (!this.hasUpdated) {
|
if (!this.hasUpdated) {
|
||||||
loadVirtualizer();
|
loadVirtualizer();
|
||||||
if (this.value?.length) {
|
|
||||||
this._findRelated();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,27 +52,16 @@ export class HaFilterEntities extends LitElement {
|
|||||||
<div slot="header" class="header">
|
<div slot="header" class="header">
|
||||||
${this.hass.localize("ui.panel.config.entities.caption")}
|
${this.hass.localize("ui.panel.config.entities.caption")}
|
||||||
${this.value?.length
|
${this.value?.length
|
||||||
? html`<div class="badge">${this.value?.length}</div>
|
? html`<div class="badge">${this.value?.length}</div>`
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiFilterVariantRemove}
|
|
||||||
@click=${this._clearFilter}
|
|
||||||
></ha-icon-button>`
|
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._shouldRender
|
${this._shouldRender
|
||||||
? html`
|
? html`
|
||||||
<search-input-outlined
|
<mwc-list class="ha-scrollbar">
|
||||||
.hass=${this.hass}
|
|
||||||
.filter=${this._filter}
|
|
||||||
@value-changed=${this._handleSearchChange}
|
|
||||||
>
|
|
||||||
</search-input-outlined>
|
|
||||||
<mwc-list class="ha-scrollbar" multi>
|
|
||||||
<lit-virtualizer
|
<lit-virtualizer
|
||||||
.items=${this._entities(
|
.items=${this._entities(
|
||||||
this.hass.states,
|
this.hass.states,
|
||||||
this.type,
|
this.type,
|
||||||
this._filter || "",
|
|
||||||
this.value
|
this.value
|
||||||
)}
|
)}
|
||||||
.keyFunction=${this._keyFunction}
|
.keyFunction=${this._keyFunction}
|
||||||
@@ -99,7 +81,7 @@ export class HaFilterEntities extends LitElement {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.expanded) return;
|
if (!this.expanded) return;
|
||||||
this.renderRoot.querySelector("mwc-list")!.style.height =
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
`${this.clientHeight - 49}px`;
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,20 +89,18 @@ export class HaFilterEntities extends LitElement {
|
|||||||
private _keyFunction = (entity) => entity?.entity_id;
|
private _keyFunction = (entity) => entity?.entity_id;
|
||||||
|
|
||||||
private _renderItem = (entity) =>
|
private _renderItem = (entity) =>
|
||||||
!entity
|
html`<ha-check-list-item
|
||||||
? nothing
|
.value=${entity.entity_id}
|
||||||
: html`<ha-check-list-item
|
.selected=${this.value?.includes(entity.entity_id)}
|
||||||
.value=${entity.entity_id}
|
graphic="icon"
|
||||||
.selected=${this.value?.includes(entity.entity_id) ?? false}
|
>
|
||||||
graphic="icon"
|
<ha-state-icon
|
||||||
>
|
slot="graphic"
|
||||||
<ha-state-icon
|
.hass=${this.hass}
|
||||||
slot="graphic"
|
.stateObj=${entity}
|
||||||
.hass=${this.hass}
|
></ha-state-icon>
|
||||||
.stateObj=${entity}
|
${computeStateName(entity)}
|
||||||
></ha-state-icon>
|
</ha-check-list-item>`;
|
||||||
${computeStateName(entity)}
|
|
||||||
</ha-check-list-item>`;
|
|
||||||
|
|
||||||
private _handleItemClick(ev) {
|
private _handleItemClick(ev) {
|
||||||
const listItem = ev.target.closest("ha-check-list-item");
|
const listItem = ev.target.closest("ha-check-list-item");
|
||||||
@@ -145,27 +125,12 @@ export class HaFilterEntities extends LitElement {
|
|||||||
this.expanded = ev.detail.expanded;
|
this.expanded = ev.detail.expanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleSearchChange(ev: CustomEvent) {
|
|
||||||
this._filter = ev.detail.value.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _entities = memoizeOne(
|
private _entities = memoizeOne(
|
||||||
(
|
(states: HomeAssistant["states"], type: this["type"], _value) => {
|
||||||
states: HomeAssistant["states"],
|
|
||||||
type: this["type"],
|
|
||||||
filter: string,
|
|
||||||
_value
|
|
||||||
) => {
|
|
||||||
const values = Object.values(states);
|
const values = Object.values(states);
|
||||||
return values
|
return values
|
||||||
.filter(
|
.filter(
|
||||||
(entityState) =>
|
(entityState) => !type || computeStateDomain(entityState) !== type
|
||||||
(!type || computeStateDomain(entityState) !== type) &&
|
|
||||||
(!filter ||
|
|
||||||
entityState.entity_id.toLowerCase().includes(filter) ||
|
|
||||||
entityState.attributes.friendly_name
|
|
||||||
?.toLowerCase()
|
|
||||||
.includes(filter))
|
|
||||||
)
|
)
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
stringCompare(
|
stringCompare(
|
||||||
@@ -189,12 +154,15 @@ export class HaFilterEntities extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const value: string[] = [];
|
||||||
|
|
||||||
for (const entityId of this.value) {
|
for (const entityId of this.value) {
|
||||||
|
value.push(entityId);
|
||||||
if (this.type) {
|
if (this.type) {
|
||||||
relatedPromises.push(findRelated(this.hass, "entity", entityId));
|
relatedPromises.push(findRelated(this.hass, "entity", entityId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.value = value;
|
||||||
const results = await Promise.all(relatedPromises);
|
const results = await Promise.all(relatedPromises);
|
||||||
const items: Set<string> = new Set();
|
const items: Set<string> = new Set();
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
@@ -204,20 +172,11 @@ export class HaFilterEntities extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
value: this.value,
|
value,
|
||||||
items: this.type ? items : undefined,
|
items: this.type ? items : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearFilter(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
this.value = undefined;
|
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
|
||||||
value: undefined,
|
|
||||||
items: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
@@ -237,10 +196,6 @@ export class HaFilterEntities extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.header ha-icon-button {
|
|
||||||
margin-inline-start: auto;
|
|
||||||
margin-inline-end: 8px;
|
|
||||||
}
|
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
@@ -261,10 +216,6 @@ export class HaFilterEntities extends LitElement {
|
|||||||
--mdc-list-item-graphic-margin: 16px;
|
--mdc-list-item-graphic-margin: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
search-input-outlined {
|
|
||||||
display: block;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -1,26 +1,17 @@
|
|||||||
import "@material/mwc-menu/mwc-menu-surface";
|
import "@material/mwc-menu/mwc-menu-surface";
|
||||||
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
|
import { mdiTextureBox } from "@mdi/js";
|
||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import {
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
CSSResultGroup,
|
|
||||||
LitElement,
|
|
||||||
PropertyValues,
|
|
||||||
css,
|
|
||||||
html,
|
|
||||||
nothing,
|
|
||||||
} from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { computeRTL } from "../common/util/compute_rtl";
|
|
||||||
import {
|
import {
|
||||||
FloorRegistryEntry,
|
FloorRegistryEntry,
|
||||||
getFloorAreaLookup,
|
getFloorAreaLookup,
|
||||||
subscribeFloorRegistry,
|
subscribeFloorRegistry,
|
||||||
} from "../data/floor_registry";
|
} from "../data/floor_registry";
|
||||||
import { RelatedResult, findRelated } from "../data/search";
|
import { findRelated, RelatedResult } from "../data/search";
|
||||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
@@ -28,7 +19,6 @@ import "./ha-check-list-item";
|
|||||||
import "./ha-floor-icon";
|
import "./ha-floor-icon";
|
||||||
import "./ha-icon";
|
import "./ha-icon";
|
||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
import "./ha-tree-indicator";
|
|
||||||
|
|
||||||
@customElement("ha-filter-floor-areas")
|
@customElement("ha-filter-floor-areas")
|
||||||
export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
||||||
@@ -49,16 +39,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
|||||||
|
|
||||||
@state() private _floors?: FloorRegistryEntry[];
|
@state() private _floors?: FloorRegistryEntry[];
|
||||||
|
|
||||||
public willUpdate(properties: PropertyValues) {
|
|
||||||
super.willUpdate(properties);
|
|
||||||
|
|
||||||
if (!this.hasUpdated) {
|
|
||||||
if (this.value?.floors?.length || this.value?.areas?.length) {
|
|
||||||
this._findRelated();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const areas = this._areas(this.hass.areas, this._floors);
|
const areas = this._areas(this.hass.areas, this._floors);
|
||||||
|
|
||||||
@@ -73,13 +53,9 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
|||||||
${this.hass.localize("ui.panel.config.areas.caption")}
|
${this.hass.localize("ui.panel.config.areas.caption")}
|
||||||
${this.value?.areas?.length || this.value?.floors?.length
|
${this.value?.areas?.length || this.value?.floors?.length
|
||||||
? html`<div class="badge">
|
? html`<div class="badge">
|
||||||
${(this.value?.areas?.length || 0) +
|
${(this.value?.areas?.length || 0) +
|
||||||
(this.value?.floors?.length || 0)}
|
(this.value?.floors?.length || 0)}
|
||||||
</div>
|
</div>`
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiFilterVariantRemove}
|
|
||||||
@click=${this._clearFilter}
|
|
||||||
></ha-icon-button>`
|
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._shouldRender
|
${this._shouldRender
|
||||||
@@ -106,10 +82,8 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
|||||||
</ha-check-list-item>
|
</ha-check-list-item>
|
||||||
${repeat(
|
${repeat(
|
||||||
floor.areas,
|
floor.areas,
|
||||||
(area, index) =>
|
(area) => area.area_id,
|
||||||
`${area.area_id}${index === floor.areas.length - 1 ? "___last" : ""}`,
|
(area) => this._renderArea(area)
|
||||||
(area, index) =>
|
|
||||||
this._renderArea(area, index === floor.areas.length - 1)
|
|
||||||
)}
|
)}
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
@@ -125,37 +99,23 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderArea(area, last: boolean = false) {
|
private _renderArea(area) {
|
||||||
const hasFloor = !!area.floor_id;
|
return html`<ha-check-list-item
|
||||||
return html`
|
.value=${area.area_id}
|
||||||
<ha-check-list-item
|
.selected=${this.value?.areas?.includes(area.area_id) || false}
|
||||||
.value=${area.area_id}
|
.type=${"areas"}
|
||||||
.selected=${this.value?.areas?.includes(area.area_id) || false}
|
graphic="icon"
|
||||||
.type=${"areas"}
|
class=${area.floor_id ? "floor" : ""}
|
||||||
graphic="icon"
|
@request-selected=${this._handleItemClick}
|
||||||
@request-selected=${this._handleItemClick}
|
>
|
||||||
class=${classMap({
|
${area.icon
|
||||||
rtl: computeRTL(this.hass),
|
? html`<ha-icon slot="graphic" .icon=${area.icon}></ha-icon>`
|
||||||
floor: hasFloor,
|
: html`<ha-svg-icon
|
||||||
})}
|
slot="graphic"
|
||||||
>
|
.path=${mdiTextureBox}
|
||||||
${hasFloor
|
></ha-svg-icon>`}
|
||||||
? html`
|
${area.name}
|
||||||
<ha-tree-indicator
|
</ha-check-list-item>`;
|
||||||
.end=${last}
|
|
||||||
slot="graphic"
|
|
||||||
></ha-tree-indicator>
|
|
||||||
`
|
|
||||||
: nothing}
|
|
||||||
${area.icon
|
|
||||||
? html`<ha-icon slot="graphic" .icon=${area.icon}></ha-icon>`
|
|
||||||
: html`<ha-svg-icon
|
|
||||||
slot="graphic"
|
|
||||||
.path=${mdiTextureBox}
|
|
||||||
></ha-svg-icon>`}
|
|
||||||
${area.name}
|
|
||||||
</ha-check-list-item>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleItemClick(ev) {
|
private _handleItemClick(ev) {
|
||||||
@@ -207,10 +167,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected firstUpdated() {
|
|
||||||
this._findRelated();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _expandedWillChange(ev) {
|
private _expandedWillChange(ev) {
|
||||||
this._shouldRender = ev.detail.expanded;
|
this._shouldRender = ev.detail.expanded;
|
||||||
}
|
}
|
||||||
@@ -282,15 +238,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearFilter(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
this.value = undefined;
|
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
|
||||||
value: undefined,
|
|
||||||
items: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
@@ -310,10 +257,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.header ha-icon-button {
|
|
||||||
margin-inline-start: auto;
|
|
||||||
margin-inline-end: 8px;
|
|
||||||
}
|
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
@@ -334,26 +277,9 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
|||||||
--mdc-list-item-graphic-margin: 16px;
|
--mdc-list-item-graphic-margin: 16px;
|
||||||
}
|
}
|
||||||
.floor {
|
.floor {
|
||||||
padding-left: 48px;
|
padding-left: 32px;
|
||||||
padding-inline-start: 48px;
|
padding-inline-start: 32px;
|
||||||
padding-inline-end: 16px;
|
|
||||||
}
|
}
|
||||||
ha-tree-indicator {
|
|
||||||
width: 56px;
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
}
|
|
||||||
.rtl ha-tree-indicator {
|
|
||||||
right: 0px;
|
|
||||||
left: initial;
|
|
||||||
transform: scaleX(-1);
|
|
||||||
}
|
|
||||||
.subdir {
|
|
||||||
margin-inline-end: 8px;
|
|
||||||
opacity: .6;
|
|
||||||
}
|
|
||||||
.
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
import { SelectedDetail } from "@material/mwc-list";
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-domain-icon";
|
import "./ha-domain-icon";
|
||||||
import "./search-input-outlined";
|
|
||||||
|
|
||||||
@customElement("ha-filter-integrations")
|
@customElement("ha-filter-integrations")
|
||||||
export class HaFilterIntegrations extends LitElement {
|
export class HaFilterIntegrations extends LitElement {
|
||||||
@@ -28,8 +27,6 @@ export class HaFilterIntegrations extends LitElement {
|
|||||||
|
|
||||||
@state() private _shouldRender = false;
|
@state() private _shouldRender = false;
|
||||||
|
|
||||||
@state() private _filter?: string;
|
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<ha-expansion-panel
|
<ha-expansion-panel
|
||||||
@@ -41,27 +38,18 @@ export class HaFilterIntegrations extends LitElement {
|
|||||||
<div slot="header" class="header">
|
<div slot="header" class="header">
|
||||||
${this.hass.localize("ui.panel.config.integrations.caption")}
|
${this.hass.localize("ui.panel.config.integrations.caption")}
|
||||||
${this.value?.length
|
${this.value?.length
|
||||||
? html`<div class="badge">${this.value?.length}</div>
|
? html`<div class="badge">${this.value?.length}</div>`
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiFilterVariantRemove}
|
|
||||||
@click=${this._clearFilter}
|
|
||||||
></ha-icon-button>`
|
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._manifests && this._shouldRender
|
${this._manifests && this._shouldRender
|
||||||
? html`<search-input-outlined
|
? html`
|
||||||
.hass=${this.hass}
|
|
||||||
.filter=${this._filter}
|
|
||||||
@value-changed=${this._handleSearchChange}
|
|
||||||
>
|
|
||||||
</search-input-outlined>
|
|
||||||
<mwc-list
|
<mwc-list
|
||||||
class="ha-scrollbar"
|
@selected=${this._integrationsSelected}
|
||||||
@click=${this._handleItemClick}
|
|
||||||
multi
|
multi
|
||||||
|
class="ha-scrollbar"
|
||||||
>
|
>
|
||||||
${repeat(
|
${repeat(
|
||||||
this._integrations(this._manifests, this._filter, this.value),
|
this._integrations(this._manifests, this.value),
|
||||||
(i) => i.domain,
|
(i) => i.domain,
|
||||||
(integration) =>
|
(integration) =>
|
||||||
html`<ha-check-list-item
|
html`<ha-check-list-item
|
||||||
@@ -80,7 +68,8 @@ export class HaFilterIntegrations extends LitElement {
|
|||||||
${integration.name || integration.domain}
|
${integration.name || integration.domain}
|
||||||
</ha-check-list-item>`
|
</ha-check-list-item>`
|
||||||
)}
|
)}
|
||||||
</mwc-list> `
|
</mwc-list>
|
||||||
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
</ha-expansion-panel>
|
</ha-expansion-panel>
|
||||||
`;
|
`;
|
||||||
@@ -91,7 +80,7 @@ export class HaFilterIntegrations extends LitElement {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.expanded) return;
|
if (!this.expanded) return;
|
||||||
this.renderRoot.querySelector("mwc-list")!.style.height =
|
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||||
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
`${this.clientHeight - 49}px`;
|
||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,17 +98,12 @@ export class HaFilterIntegrations extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _integrations = memoizeOne(
|
private _integrations = memoizeOne(
|
||||||
(manifest: IntegrationManifest[], filter: string | undefined, _value) =>
|
(manifest: IntegrationManifest[], _value) =>
|
||||||
manifest
|
manifest
|
||||||
.filter(
|
.filter(
|
||||||
(mnfst) =>
|
(mnfst) =>
|
||||||
(!mnfst.integration_type ||
|
!mnfst.integration_type ||
|
||||||
!["entity", "system", "hardware"].includes(
|
!["entity", "system", "hardware"].includes(mnfst.integration_type)
|
||||||
mnfst.integration_type
|
|
||||||
)) &&
|
|
||||||
(!filter ||
|
|
||||||
mnfst.name.toLowerCase().includes(filter) ||
|
|
||||||
mnfst.domain.toLowerCase().includes(filter))
|
|
||||||
)
|
)
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
stringCompare(
|
stringCompare(
|
||||||
@@ -130,38 +114,34 @@ export class HaFilterIntegrations extends LitElement {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
private _handleItemClick(ev) {
|
private async _integrationsSelected(
|
||||||
const listItem = ev.target.closest("ha-check-list-item");
|
ev: CustomEvent<SelectedDetail<Set<number>>>
|
||||||
const value = listItem?.value;
|
) {
|
||||||
if (!value) {
|
const integrations = this._integrations(this._manifests!, this.value);
|
||||||
|
|
||||||
|
if (!ev.detail.index.size) {
|
||||||
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
|
value: [],
|
||||||
|
items: undefined,
|
||||||
|
});
|
||||||
|
this.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.value?.includes(value)) {
|
|
||||||
this.value = this.value?.filter((val) => val !== value);
|
const value: string[] = [];
|
||||||
} else {
|
|
||||||
this.value = [...(this.value || []), value];
|
for (const index of ev.detail.index) {
|
||||||
|
const domain = integrations[index].domain;
|
||||||
|
value.push(domain);
|
||||||
}
|
}
|
||||||
listItem.selected = this.value?.includes(value);
|
this.value = value;
|
||||||
|
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
fireEvent(this, "data-table-filter-changed", {
|
||||||
value: this.value,
|
value,
|
||||||
items: undefined,
|
items: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearFilter(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
this.value = undefined;
|
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
|
||||||
value: undefined,
|
|
||||||
items: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleSearchChange(ev: CustomEvent) {
|
|
||||||
this._filter = ev.detail.value.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
@@ -181,10 +161,6 @@ export class HaFilterIntegrations extends LitElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.header ha-icon-button {
|
|
||||||
margin-inline-start: auto;
|
|
||||||
margin-inline-end: 8px;
|
|
||||||
}
|
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
@@ -201,10 +177,6 @@ export class HaFilterIntegrations extends LitElement {
|
|||||||
padding: 0px 2px;
|
padding: 0px 2px;
|
||||||
color: var(--text-primary-color);
|
color: var(--text-primary-color);
|
||||||
}
|
}
|
||||||
search-input-outlined {
|
|
||||||
display: block;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@@ -1,18 +1,19 @@
|
|||||||
import { SelectedDetail } from "@material/mwc-list";
|
import { SelectedDetail } from "@material/mwc-list";
|
||||||
import "@material/mwc-menu/mwc-menu-surface";
|
import "@material/mwc-menu/mwc-menu-surface";
|
||||||
import { mdiCog, mdiFilterVariantRemove } from "@mdi/js";
|
import { mdiPlus } from "@mdi/js";
|
||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
import { computeCssColor } from "../common/color/compute-color";
|
import { computeCssColor } from "../common/color/compute-color";
|
||||||
import { fireEvent } from "../common/dom/fire_event";
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
import { navigate } from "../common/navigate";
|
|
||||||
import {
|
import {
|
||||||
LabelRegistryEntry,
|
LabelRegistryEntry,
|
||||||
|
createLabelRegistryEntry,
|
||||||
subscribeLabelRegistry,
|
subscribeLabelRegistry,
|
||||||
} from "../data/label_registry";
|
} from "../data/label_registry";
|
||||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
|
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
|
||||||
import { haStyleScrollbar } from "../resources/styles";
|
import { haStyleScrollbar } from "../resources/styles";
|
||||||
import type { HomeAssistant } from "../types";
|
import type { HomeAssistant } from "../types";
|
||||||
import "./ha-check-list-item";
|
import "./ha-check-list-item";
|
||||||
@@ -53,11 +54,7 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
|||||||
<div slot="header" class="header">
|
<div slot="header" class="header">
|
||||||
${this.hass.localize("ui.panel.config.labels.caption")}
|
${this.hass.localize("ui.panel.config.labels.caption")}
|
||||||
${this.value?.length
|
${this.value?.length
|
||||||
? html`<div class="badge">${this.value?.length}</div>
|
? html`<div class="badge">${this.value?.length}</div>`
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiFilterVariantRemove}
|
|
||||||
@click=${this._clearFilter}
|
|
||||||
></ha-icon-button>`
|
|
||||||
: nothing}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
${this._shouldRender
|
${this._shouldRender
|
||||||
@@ -98,11 +95,11 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
|||||||
${this.expanded
|
${this.expanded
|
||||||
? html`<ha-list-item
|
? html`<ha-list-item
|
||||||
graphic="icon"
|
graphic="icon"
|
||||||
@click=${this._manageLabels}
|
@click=${this._addLabel}
|
||||||
class="add"
|
class="add"
|
||||||
>
|
>
|
||||||
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
|
<ha-svg-icon slot="graphic" .path=${mdiPlus}></ha-svg-icon>
|
||||||
${this.hass.localize("ui.panel.config.labels.manage_labels")}
|
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||||
</ha-list-item>`
|
</ha-list-item>`
|
||||||
: nothing}
|
: nothing}
|
||||||
`;
|
`;
|
||||||
@@ -118,8 +115,10 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _manageLabels() {
|
private _addLabel() {
|
||||||
navigate("/config/labels");
|
showLabelDetailDialog(this, {
|
||||||
|
createEntry: (values) => createLabelRegistryEntry(this.hass, values),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _expandedWillChange(ev) {
|
private _expandedWillChange(ev) {
|
||||||
@@ -154,15 +153,6 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearFilter(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
this.value = undefined;
|
|
||||||
fireEvent(this, "data-table-filter-changed", {
|
|
||||||
value: undefined,
|
|
||||||
items: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
haStyleScrollbar,
|
haStyleScrollbar,
|
||||||
@@ -183,10 +173,6 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.header ha-icon-button {
|
|
||||||
margin-inline-start: auto;
|
|
||||||
margin-inline-end: 8px;
|
|
||||||
}
|
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user