mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-04 23:41:46 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 09982a9238 |
@@ -36,7 +36,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@5752577ea7cc5aefb064b0b21432f18fe4d6ba90 # master
|
||||
uses: home-assistant/actions/helpers/verify-version@d56d093b9ab8d2105bc0cb6ee9bcc0ef4ec8b96d # master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
|
||||
@@ -6,9 +6,9 @@ import rootConfig from "../eslint.config.mjs";
|
||||
export default tseslint.config(...rootConfig, {
|
||||
rules: {
|
||||
"no-console": "off",
|
||||
"import-x/no-extraneous-dependencies": "off",
|
||||
"import-x/extensions": "off",
|
||||
"import-x/no-dynamic-require": "off",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"import/extensions": "off",
|
||||
"import/no-dynamic-require": "off",
|
||||
"global-require": "off",
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
"prefer-arrow-callback": "off",
|
||||
|
||||
@@ -6,6 +6,7 @@ import presetEnv from "@babel/preset-env";
|
||||
import compilationTargets from "@babel/helper-compilation-targets";
|
||||
import coreJSCompat from "core-js-compat";
|
||||
import { logPlugin } from "@babel/preset-env/lib/debug.js";
|
||||
// eslint-disable-next-line import/no-relative-packages
|
||||
import shippedPolyfills from "../node_modules/babel-plugin-polyfill-corejs3/lib/shipped-proposals.js";
|
||||
import { babelOptions } from "./bundle.cjs";
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ export const demoConfigs: (() => Promise<DemoConfig>)[] = [
|
||||
() => import("./jimpower").then((mod) => mod.demoJimpower),
|
||||
];
|
||||
|
||||
// eslint-disable-next-line import-x/no-mutable-exports
|
||||
// eslint-disable-next-line import/no-mutable-exports
|
||||
export let selectedDemoConfigIndex = 0;
|
||||
// eslint-disable-next-line import-x/no-mutable-exports
|
||||
// eslint-disable-next-line import/no-mutable-exports
|
||||
export let selectedDemoConfig: Promise<DemoConfig> =
|
||||
demoConfigs[selectedDemoConfigIndex]();
|
||||
|
||||
|
||||
+14
-59
@@ -1,5 +1,6 @@
|
||||
// @ts-check
|
||||
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import unusedImports from "eslint-plugin-unused-imports";
|
||||
import globals from "globals";
|
||||
import path from "node:path";
|
||||
@@ -12,7 +13,6 @@ import { configs as litConfigs } from "eslint-plugin-lit";
|
||||
import { configs as wcConfigs } from "eslint-plugin-wc";
|
||||
import { configs as a11yConfigs } from "eslint-plugin-lit-a11y";
|
||||
import html from "@html-eslint/eslint-plugin";
|
||||
import importX from "eslint-plugin-import-x";
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = path.dirname(_filename);
|
||||
@@ -22,27 +22,8 @@ const compat = new FlatCompat({
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
// Load airbnb-base via FlatCompat for non-import rules only.
|
||||
// eslint-plugin-import is incompatible with ESLint 10 (uses removed APIs),
|
||||
// so we strip its plugin/rules/settings and use eslint-plugin-import-x instead.
|
||||
const airbnbConfigs = compat.extends("airbnb-base").map((config) => {
|
||||
const { plugins = {}, rules = {}, settings = {}, ...rest } = config;
|
||||
return {
|
||||
...rest,
|
||||
plugins: Object.fromEntries(
|
||||
Object.entries(plugins).filter(([key]) => key !== "import")
|
||||
),
|
||||
rules: Object.fromEntries(
|
||||
Object.entries(rules).filter(([key]) => !key.startsWith("import/"))
|
||||
),
|
||||
settings: Object.fromEntries(
|
||||
Object.entries(settings).filter(([key]) => !key.startsWith("import/"))
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
export default tseslint.config(
|
||||
...airbnbConfigs,
|
||||
...compat.extends("airbnb-base"),
|
||||
eslintConfigPrettier,
|
||||
litConfigs["flat/all"],
|
||||
tseslint.configs.recommended,
|
||||
@@ -50,7 +31,6 @@ export default tseslint.config(
|
||||
tseslint.configs.stylistic,
|
||||
wcConfigs["flat/recommended"],
|
||||
a11yConfigs.recommended,
|
||||
importX.flatConfigs.recommended,
|
||||
{
|
||||
plugins: {
|
||||
"unused-imports": unusedImports,
|
||||
@@ -78,7 +58,7 @@ export default tseslint.config(
|
||||
},
|
||||
|
||||
settings: {
|
||||
"import-x/resolver": {
|
||||
"import/resolver": {
|
||||
webpack: {
|
||||
config: "./rspack.config.cjs",
|
||||
},
|
||||
@@ -107,20 +87,12 @@ export default tseslint.config(
|
||||
"prefer-destructuring": "off",
|
||||
"no-restricted-globals": [2, "event"],
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
||||
"object-curly-newline": "off",
|
||||
"default-case": "off",
|
||||
"wc/no-self-class": "off",
|
||||
"no-shadow": "off",
|
||||
"no-use-before-define": "off",
|
||||
"import/prefer-default-export": "off",
|
||||
"import/no-default-export": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"import/no-cycle": "off",
|
||||
|
||||
// import-x rules (migrated from eslint-plugin-import / airbnb-base)
|
||||
"import-x/named": "off",
|
||||
"import-x/prefer-default-export": "off",
|
||||
"import-x/no-default-export": "off",
|
||||
"import-x/no-unresolved": "off",
|
||||
"import-x/no-cycle": "off",
|
||||
"import-x/extensions": [
|
||||
"import/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
{
|
||||
@@ -128,24 +100,12 @@ export default tseslint.config(
|
||||
js: "never",
|
||||
},
|
||||
],
|
||||
"import-x/no-mutable-exports": "error",
|
||||
"import-x/no-amd": "error",
|
||||
"import-x/first": "error",
|
||||
"import-x/order": [
|
||||
"error",
|
||||
{ groups: [["builtin", "external", "internal"]] },
|
||||
],
|
||||
"import-x/newline-after-import": "error",
|
||||
"import-x/no-absolute-path": "error",
|
||||
"import-x/no-dynamic-require": "error",
|
||||
"import-x/no-webpack-loader-syntax": "error",
|
||||
"import-x/no-named-default": "error",
|
||||
"import-x/no-self-import": "error",
|
||||
"import-x/no-useless-path-segments": ["error", { commonjs: true }],
|
||||
"import-x/no-import-module-exports": ["error", { exceptions: [] }],
|
||||
"import-x/no-relative-packages": "error",
|
||||
|
||||
// TypeScript rules
|
||||
"no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
|
||||
"object-curly-newline": "off",
|
||||
"default-case": "off",
|
||||
"wc/no-self-class": "off",
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/camelcase": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
@@ -225,6 +185,7 @@ export default tseslint.config(
|
||||
allowObjectTypes: "always",
|
||||
},
|
||||
],
|
||||
"no-use-before-define": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -233,12 +194,6 @@ export default tseslint.config(
|
||||
globals: globals.audioWorklet,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/entrypoints/service-worker.ts"],
|
||||
languageOptions: {
|
||||
globals: globals.serviceworker,
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
html,
|
||||
|
||||
@@ -488,79 +488,6 @@ const SCHEMAS: {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Tabs",
|
||||
translations: {
|
||||
settings: "Settings",
|
||||
tab_general: "General",
|
||||
tab_appearance: "Appearance",
|
||||
name: "Name",
|
||||
entity: "Entity",
|
||||
theme: "Theme",
|
||||
state_color: "Color on state",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "tabs",
|
||||
name: "settings",
|
||||
tabs: [
|
||||
{
|
||||
name: "general",
|
||||
icon: "mdi:cog",
|
||||
schema: [
|
||||
{ name: "name", selector: { text: {} } },
|
||||
{ name: "entity", required: true, selector: { entity: {} } },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "appearance",
|
||||
icon: "mdi:palette",
|
||||
schema: [
|
||||
{ name: "theme", selector: { theme: {} } },
|
||||
{ name: "state_color", selector: { boolean: {} } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Tabs (compact)",
|
||||
translations: {
|
||||
settings: "Settings",
|
||||
tab_general: "General",
|
||||
tab_appearance: "Appearance",
|
||||
name: "Name",
|
||||
entity: "Entity",
|
||||
theme: "Theme",
|
||||
state_color: "Color on state",
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
type: "tabs",
|
||||
name: "settings",
|
||||
fill_tabs: false,
|
||||
tabs: [
|
||||
{
|
||||
name: "general",
|
||||
icon: "mdi:cog",
|
||||
schema: [
|
||||
{ name: "name", selector: { text: {} } },
|
||||
{ name: "entity", required: true, selector: { entity: {} } },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "appearance",
|
||||
icon: "mdi:palette",
|
||||
schema: [
|
||||
{ name: "theme", selector: { theme: {} } },
|
||||
{ name: "state_color", selector: { boolean: {} } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-components-ha-form")
|
||||
@@ -608,12 +535,8 @@ class DemoHaForm extends LitElement {
|
||||
.error=${info.error}
|
||||
.disabled=${this.disabled[idx]}
|
||||
.computeError=${(error) => translations[error] || error}
|
||||
.computeLabel=${(schema, _data, options) => {
|
||||
if (options?.tab) {
|
||||
return translations[`tab_${options.tab}`] || options.tab;
|
||||
}
|
||||
return translations[schema.name] || schema.name;
|
||||
}}
|
||||
.computeLabel=${(schema) =>
|
||||
translations[schema.name] || schema.name}
|
||||
.computeHelper=${() => "Helper text"}
|
||||
@value-changed=${this._handleValueChanged}
|
||||
.sampleIdx=${idx}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
title: Textarea
|
||||
---
|
||||
|
||||
# Textarea `<ha-textarea>`
|
||||
|
||||
A multiline text input component supporting Home Assistant theming and validation, based on webawesome textarea.
|
||||
Supports autogrow, hints, validation, and both material and outlined appearances.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Example usage
|
||||
|
||||
```html
|
||||
<ha-textarea label="Description" value="Hello world"></ha-textarea>
|
||||
|
||||
<ha-textarea
|
||||
label="Notes"
|
||||
placeholder="Type here..."
|
||||
resize="auto"
|
||||
></ha-textarea>
|
||||
|
||||
<ha-textarea label="Required field" required></ha-textarea>
|
||||
|
||||
<ha-textarea label="Disabled" disabled value="Can't edit this"></ha-textarea>
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
This component is based on the webawesome textarea component.
|
||||
|
||||
**Slots**
|
||||
|
||||
- `label`: Custom label content. Overrides the `label` property.
|
||||
- `hint`: Custom hint content. Overrides the `hint` property.
|
||||
|
||||
**Properties/Attributes**
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ------------------ | -------------------------------------------------------------- | ------- | ------------------------------------------------------------------------ |
|
||||
| value | String | - | The current value of the textarea. |
|
||||
| label | String | "" | The textarea's label text. |
|
||||
| hint | String | "" | The textarea's hint/helper text. |
|
||||
| placeholder | String | "" | Placeholder text shown when the textarea is empty. |
|
||||
| rows | Number | 4 | The number of visible text rows. |
|
||||
| resize | "none"/"vertical"/"horizontal"/"both"/"auto" | "none" | Controls the textarea's resize behavior. |
|
||||
| readonly | Boolean | false | Makes the textarea readonly. |
|
||||
| disabled | Boolean | false | Disables the textarea and prevents user interaction. |
|
||||
| required | Boolean | false | Makes the textarea a required field. |
|
||||
| auto-validate | Boolean | false | Validates the textarea on blur instead of on form submit. |
|
||||
| invalid | Boolean | false | Marks the textarea as invalid. |
|
||||
| validation-message | String | "" | Custom validation message shown when the textarea is invalid. |
|
||||
| minlength | Number | - | The minimum length of input that will be considered valid. |
|
||||
| maxlength | Number | - | The maximum length of input that will be considered valid. |
|
||||
| name | String | - | The name of the textarea, submitted as a name/value pair with form data. |
|
||||
| autocapitalize | "off"/"none"/"on"/"sentences"/"words"/"characters" | "" | Controls whether and how text input is automatically capitalized. |
|
||||
| autocomplete | String | - | Indicates whether the browser's autocomplete feature should be used. |
|
||||
| autofocus | Boolean | false | Automatically focuses the textarea when the page loads. |
|
||||
| spellcheck | Boolean | true | Enables or disables the browser's spellcheck feature. |
|
||||
| inputmode | "none"/"text"/"decimal"/"numeric"/"tel"/"search"/"email"/"url" | "" | Hints at the type of data for showing an appropriate virtual keyboard. |
|
||||
| enterkeyhint | "enter"/"done"/"go"/"next"/"previous"/"search"/"send" | "" | Customizes the label or icon of the Enter key on virtual keyboards. |
|
||||
|
||||
#### CSS Parts
|
||||
|
||||
- `wa-base` - The underlying wa-textarea base wrapper.
|
||||
- `wa-hint` - The underlying wa-textarea hint container.
|
||||
- `wa-textarea` - The underlying wa-textarea textarea element.
|
||||
|
||||
**CSS Custom Properties**
|
||||
|
||||
- `--ha-textarea-padding-bottom` - Padding below the textarea host.
|
||||
- `--ha-textarea-max-height` - Maximum height of the textarea when using `resize="auto"`. Defaults to `200px`.
|
||||
- `--ha-textarea-required-marker` - The marker shown after the label for required fields. Defaults to `"*"`.
|
||||
@@ -1,151 +0,0 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-textarea";
|
||||
|
||||
@customElement("demo-components-ha-textarea")
|
||||
export class DemoHaTextarea extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-textarea in ${mode}">
|
||||
<div class="card-content">
|
||||
<h3>Basic</h3>
|
||||
<div class="row">
|
||||
<ha-textarea label="Default"></ha-textarea>
|
||||
<ha-textarea
|
||||
label="With value"
|
||||
value="Hello world"
|
||||
></ha-textarea>
|
||||
<ha-textarea
|
||||
label="With placeholder"
|
||||
placeholder="Type here..."
|
||||
></ha-textarea>
|
||||
</div>
|
||||
|
||||
<h3>Autogrow</h3>
|
||||
<div class="row">
|
||||
<ha-textarea
|
||||
label="Autogrow empty"
|
||||
resize="auto"
|
||||
></ha-textarea>
|
||||
<ha-textarea
|
||||
label="Autogrow with value"
|
||||
resize="auto"
|
||||
value="This textarea will grow as you type more content into it. Try adding more lines to see the effect."
|
||||
></ha-textarea>
|
||||
</div>
|
||||
|
||||
<h3>States</h3>
|
||||
<div class="row">
|
||||
<ha-textarea
|
||||
label="Disabled"
|
||||
disabled
|
||||
value="Disabled"
|
||||
></ha-textarea>
|
||||
<ha-textarea
|
||||
label="Readonly"
|
||||
readonly
|
||||
value="Readonly"
|
||||
></ha-textarea>
|
||||
<ha-textarea label="Required" required></ha-textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ha-textarea
|
||||
label="Invalid"
|
||||
invalid
|
||||
validation-message="This field is required"
|
||||
value=""
|
||||
></ha-textarea>
|
||||
<ha-textarea
|
||||
label="With hint"
|
||||
hint="Supports Markdown"
|
||||
></ha-textarea>
|
||||
<ha-textarea
|
||||
label="With rows"
|
||||
.rows=${6}
|
||||
placeholder="6 rows"
|
||||
></ha-textarea>
|
||||
</div>
|
||||
|
||||
<h3>No label</h3>
|
||||
<div class="row">
|
||||
<ha-textarea
|
||||
placeholder="No label, just placeholder"
|
||||
></ha-textarea>
|
||||
<ha-textarea
|
||||
resize="auto"
|
||||
placeholder="No label, autogrow"
|
||||
></ha-textarea>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
applyThemesOnElement(
|
||||
this.shadowRoot!.querySelector(".dark"),
|
||||
{
|
||||
default_theme: "default",
|
||||
default_dark_theme: "default",
|
||||
themes: {},
|
||||
darkMode: true,
|
||||
theme: "default",
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.dark,
|
||||
.light {
|
||||
display: block;
|
||||
background-color: var(--primary-background-color);
|
||||
padding: 0 50px;
|
||||
}
|
||||
ha-card {
|
||||
margin: 24px auto;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
h3 {
|
||||
margin: var(--ha-space-4) 0 var(--ha-space-1) 0;
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
.row > * {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-textarea": DemoHaTextarea;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import { mdiArrowCollapseDown, mdiDownload } from "@mdi/js";
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { IntersectionController } from "@lit-labs/observers/intersection-controller.js";
|
||||
import { LitElement, type PropertyValues, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default {
|
||||
"*.?(c|m){js,ts}": [
|
||||
"eslint --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --fix",
|
||||
"eslint --flag v10_config_lookup_from_file --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --fix",
|
||||
"prettier --cache --write",
|
||||
"lit-analyzer --quiet",
|
||||
],
|
||||
|
||||
+14
-16
@@ -8,8 +8,8 @@
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "script/build_frontend",
|
||||
"lint:eslint": "eslint \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --max-warnings=0",
|
||||
"format:eslint": "eslint \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --fix",
|
||||
"lint:eslint": "eslint --flag v10_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --max-warnings=0",
|
||||
"format:eslint": "eslint --flag v10_config_lookup_from_file \"**/src/**/*.{js,ts,html}\" --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --ignore-pattern=.gitignore --fix",
|
||||
"lint:prettier": "prettier . --cache --check",
|
||||
"format:prettier": "prettier . --cache --write",
|
||||
"lint:types": "tsc",
|
||||
@@ -34,7 +34,7 @@
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/state": "6.6.0",
|
||||
"@codemirror/view": "6.41.0",
|
||||
"@codemirror/view": "6.40.0",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.3.1",
|
||||
@@ -73,6 +73,8 @@
|
||||
"@material/mwc-radio": "0.27.0",
|
||||
"@material/mwc-select": "0.27.0",
|
||||
"@material/mwc-switch": "0.27.0",
|
||||
"@material/mwc-textarea": "0.27.0",
|
||||
"@material/mwc-textfield": "0.27.0",
|
||||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
@@ -80,14 +82,14 @@
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.21",
|
||||
"@swc/helpers": "0.5.20",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"barcode-detector": "3.1.2",
|
||||
"barcode-detector": "3.1.1",
|
||||
"cally": "0.9.2",
|
||||
"color-name": "2.1.0",
|
||||
"comlink": "4.4.2",
|
||||
@@ -100,7 +102,7 @@
|
||||
"dialog-polyfill": "0.5.6",
|
||||
"echarts": "6.0.0",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.2.0",
|
||||
"fuse.js": "7.1.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "7.0.0",
|
||||
"hls.js": "1.6.15",
|
||||
@@ -142,18 +144,16 @@
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.2",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.0",
|
||||
"@eslint/eslintrc": "3.3.5",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.58.1",
|
||||
"@lokalise/node-api": "15.6.1",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.7",
|
||||
"@rspack/core": "1.7.11",
|
||||
"@rsdoctor/rspack-plugin": "1.5.6",
|
||||
"@rspack/core": "1.7.10",
|
||||
"@rspack/dev-server": "1.2.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
"@types/chromecast-caf-receiver": "6.0.25",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/culori": "4.0.1",
|
||||
@@ -174,12 +174,11 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.1",
|
||||
"eslint": "10.2.0",
|
||||
"eslint": "9.39.4",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.11",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-import-x": "4.16.2",
|
||||
"eslint-plugin-lit": "2.2.1",
|
||||
"eslint-plugin-lit-a11y": "5.1.1",
|
||||
"eslint-plugin-unused-imports": "4.4.1",
|
||||
@@ -187,7 +186,6 @@
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.4",
|
||||
"glob": "13.0.6",
|
||||
"globals": "17.4.0",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
@@ -210,7 +208,7 @@
|
||||
"terser-webpack-plugin": "5.4.0",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.2",
|
||||
"typescript-eslint": "8.58.0",
|
||||
"typescript-eslint": "8.57.2",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.2",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
|
||||
@@ -27,7 +27,6 @@ export type DateRange =
|
||||
| "this_year"
|
||||
| "now-7d"
|
||||
| "now-30d"
|
||||
| "now-365d"
|
||||
| "now-12m"
|
||||
| "now-1h"
|
||||
| "now-12h"
|
||||
@@ -103,11 +102,6 @@ export const calcDateRange = (
|
||||
),
|
||||
calcDate(today, endOfMonth, locale, hassConfig),
|
||||
];
|
||||
case "now-365d":
|
||||
return [
|
||||
calcDate(today, subDays, locale, hassConfig, 365),
|
||||
calcDate(today, subDays, locale, hassConfig, 0),
|
||||
];
|
||||
case "now-1h":
|
||||
return [
|
||||
calcDate(today, subHours, locale, hassConfig, 1),
|
||||
|
||||
@@ -38,14 +38,6 @@ export interface HASSDomEvent<T> extends Event {
|
||||
detail: T;
|
||||
}
|
||||
|
||||
export type HASSDomTargetEvent<T extends EventTarget> = Event & {
|
||||
target: T;
|
||||
};
|
||||
|
||||
export type HASSDomCurrentTargetEvent<T extends EventTarget> = Event & {
|
||||
currentTarget: T;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dispatches a custom event with an optional detail value.
|
||||
*
|
||||
|
||||
@@ -242,18 +242,14 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
|
||||
},
|
||||
};
|
||||
|
||||
export const getStatesDomain = (
|
||||
export const getStates = (
|
||||
hass: HomeAssistant,
|
||||
domain: string,
|
||||
attribute?: string | undefined
|
||||
state: HassEntity,
|
||||
attribute: string | undefined = undefined
|
||||
): string[] => {
|
||||
const domain = computeStateDomain(state);
|
||||
const result: string[] = [];
|
||||
|
||||
if (!attribute) {
|
||||
// All entities can have unavailable states
|
||||
result.push(...UNAVAILABLE_STATES);
|
||||
}
|
||||
|
||||
if (!attribute && domain in FIXED_DOMAIN_STATES) {
|
||||
result.push(...FIXED_DOMAIN_STATES[domain]);
|
||||
} else if (
|
||||
@@ -264,7 +260,21 @@ export const getStatesDomain = (
|
||||
result.push(...FIXED_DOMAIN_ATTRIBUTE_STATES[domain][attribute]);
|
||||
}
|
||||
|
||||
// Dynamic values based on the entities
|
||||
switch (domain) {
|
||||
case "climate":
|
||||
if (!attribute) {
|
||||
result.push(...state.attributes.hvac_modes);
|
||||
} else if (attribute === "fan_mode") {
|
||||
result.push(...state.attributes.fan_modes);
|
||||
} else if (attribute === "preset_mode") {
|
||||
result.push(...state.attributes.preset_modes);
|
||||
} else if (attribute === "swing_mode") {
|
||||
result.push(...state.attributes.swing_modes);
|
||||
} else if (attribute === "swing_horizontal_mode") {
|
||||
result.push(...state.attributes.swing_horizontal_modes);
|
||||
}
|
||||
break;
|
||||
case "device_tracker":
|
||||
case "person":
|
||||
if (!attribute) {
|
||||
@@ -283,37 +293,6 @@ export const getStatesDomain = (
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getStates = (
|
||||
hass: HomeAssistant,
|
||||
state: HassEntity,
|
||||
attribute: string | undefined = undefined
|
||||
): string[] => {
|
||||
const domain = computeStateDomain(state);
|
||||
const result: string[] = [];
|
||||
|
||||
// Fixed values based on a domain
|
||||
result.push(...getStatesDomain(hass, domain, attribute));
|
||||
|
||||
// Dynamic values based on the entities
|
||||
switch (domain) {
|
||||
case "climate":
|
||||
if (!attribute) {
|
||||
result.push(...state.attributes.hvac_modes);
|
||||
} else if (attribute === "fan_mode") {
|
||||
result.push(...state.attributes.fan_modes);
|
||||
} else if (attribute === "preset_mode") {
|
||||
result.push(...state.attributes.preset_modes);
|
||||
} else if (attribute === "swing_mode") {
|
||||
result.push(...state.attributes.swing_modes);
|
||||
} else if (attribute === "swing_horizontal_mode") {
|
||||
result.push(...state.attributes.swing_horizontal_modes);
|
||||
}
|
||||
break;
|
||||
case "event":
|
||||
if (attribute === "event_type") {
|
||||
result.push(...state.attributes.event_types);
|
||||
@@ -374,5 +353,9 @@ export const getStates = (
|
||||
break;
|
||||
}
|
||||
|
||||
if (!attribute) {
|
||||
// All entities can have unavailable states
|
||||
result.push(...UNAVAILABLE_STATES);
|
||||
}
|
||||
return [...new Set(result)];
|
||||
};
|
||||
|
||||
@@ -10,10 +10,13 @@
|
||||
*
|
||||
* @see https://github.com/home-assistant/frontend/issues/28732
|
||||
*/
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { directive, Directive } from "lit-html/directive.js";
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { setCommittedValue } from "lit-html/directive-helpers.js";
|
||||
// eslint-disable-next-line lit/no-legacy-imports
|
||||
import { nothing } from "lit-html";
|
||||
// eslint-disable-next-line import/extensions
|
||||
import type { Part } from "lit-html/directive.js";
|
||||
|
||||
class KeyedES5 extends Directive {
|
||||
|
||||
@@ -17,10 +17,6 @@ import memoizeOne from "memoize-one";
|
||||
import { STRINGS_SEPARATOR_DOT } from "../../common/const";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
@@ -107,7 +103,6 @@ export interface DataTableRowData {
|
||||
export type SortableColumnContainer = Record<string, ClonedDataTableColumnData>;
|
||||
|
||||
const UNDEFINED_GROUP_KEY = "zzzzz_undefined";
|
||||
const AUTO_FOCUS_ALLOWED_ACTIVE_TAGS = ["BODY", "HTML", "HOME-ASSISTANT"];
|
||||
|
||||
@customElement("ha-data-table")
|
||||
export class HaDataTable extends LitElement {
|
||||
@@ -171,10 +166,6 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@query("slot[name='header']") private _header!: HTMLSlotElement;
|
||||
|
||||
@query(".mdc-data-table__header-row") private _headerRow?: HTMLDivElement;
|
||||
|
||||
@query("lit-virtualizer") private _scroller?: HTMLElement;
|
||||
|
||||
@state() private _collapsedGroups: string[] = [];
|
||||
|
||||
@state() private _lastSelectedRowId: string | null = null;
|
||||
@@ -251,28 +242,16 @@ export class HaDataTable extends LitElement {
|
||||
this.updateComplete.then(() => this._calcTableHeight());
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (!this._headerRow) {
|
||||
protected updated() {
|
||||
const header = this.renderRoot.querySelector(".mdc-data-table__header-row");
|
||||
if (!header) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._headerRow.scrollWidth > this._headerRow.clientWidth) {
|
||||
this.style.setProperty(
|
||||
"--table-row-width",
|
||||
`${this._headerRow.scrollWidth}px`
|
||||
);
|
||||
if (header.scrollWidth > header.clientWidth) {
|
||||
this.style.setProperty("--table-row-width", `${header.scrollWidth}px`);
|
||||
} else {
|
||||
this.style.removeProperty("--table-row-width");
|
||||
}
|
||||
|
||||
if (
|
||||
changedProps.has("selectable") ||
|
||||
(!this.autoHeight &&
|
||||
document.activeElement &&
|
||||
AUTO_FOCUS_ALLOWED_ACTIVE_TAGS.includes(document.activeElement.tagName))
|
||||
) {
|
||||
this._focusScroller();
|
||||
}
|
||||
}
|
||||
|
||||
public willUpdate(properties: PropertyValues) {
|
||||
@@ -538,7 +517,6 @@ export class HaDataTable extends LitElement {
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
class="mdc-data-table__content scroller ha-scrollbar"
|
||||
tabindex=${ifDefined(!this.autoHeight ? "0" : undefined)}
|
||||
@scroll=${this._saveScrollPos}
|
||||
.items=${this._groupData(
|
||||
this._filteredData,
|
||||
@@ -851,10 +829,8 @@ export class HaDataTable extends LitElement {
|
||||
): Promise<DataTableRowData[]> => filterData(data, columns, filter)
|
||||
);
|
||||
|
||||
private _handleHeaderClick(
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { columnId: string }>
|
||||
) {
|
||||
const columnId = ev.currentTarget.columnId;
|
||||
private _handleHeaderClick(ev: Event) {
|
||||
const columnId = (ev.currentTarget as any).columnId;
|
||||
if (!this.columns[columnId].sortable) {
|
||||
return;
|
||||
}
|
||||
@@ -872,12 +848,11 @@ export class HaDataTable extends LitElement {
|
||||
column: columnId,
|
||||
direction: this.sortDirection,
|
||||
});
|
||||
|
||||
this._focusScroller();
|
||||
}
|
||||
|
||||
private _handleHeaderRowCheckboxClick(ev: HASSDomTargetEvent<HaCheckbox>) {
|
||||
if (ev.target.checked) {
|
||||
private _handleHeaderRowCheckboxClick(ev: Event) {
|
||||
const checkbox = ev.target as HaCheckbox;
|
||||
if (checkbox.checked) {
|
||||
this.selectAll();
|
||||
} else {
|
||||
this._checkedRows = [];
|
||||
@@ -886,10 +861,9 @@ export class HaDataTable extends LitElement {
|
||||
this._lastSelectedRowId = null;
|
||||
}
|
||||
|
||||
private _handleRowCheckboxClicked = (
|
||||
ev: HASSDomCurrentTargetEvent<HaCheckbox & { rowId: string }>
|
||||
) => {
|
||||
const rowId = ev.currentTarget.rowId;
|
||||
private _handleRowCheckboxClicked = (ev: Event) => {
|
||||
const checkbox = ev.currentTarget as HaCheckbox;
|
||||
const rowId = (checkbox as any).rowId;
|
||||
|
||||
const groupedData = this._groupData(
|
||||
this._filteredData,
|
||||
@@ -926,7 +900,7 @@ export class HaDataTable extends LitElement {
|
||||
...this._selectRange(groupedData, lastSelectedRowIndex, rowIndex),
|
||||
];
|
||||
}
|
||||
} else if (!ev.currentTarget.checked) {
|
||||
} else if (!checkbox.checked) {
|
||||
if (!this._checkedRows.includes(rowId)) {
|
||||
this._checkedRows = [...this._checkedRows, rowId];
|
||||
}
|
||||
@@ -964,9 +938,7 @@ export class HaDataTable extends LitElement {
|
||||
return checkedRows;
|
||||
}
|
||||
|
||||
private _handleRowClick = (
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { rowId: string }>
|
||||
) => {
|
||||
private _handleRowClick = (ev: Event) => {
|
||||
if (
|
||||
ev
|
||||
.composedPath()
|
||||
@@ -982,13 +954,14 @@ export class HaDataTable extends LitElement {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const rowId = ev.currentTarget.rowId;
|
||||
const rowId = (ev.currentTarget as any).rowId;
|
||||
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
|
||||
};
|
||||
|
||||
private _setTitle(ev: HASSDomCurrentTargetEvent<HTMLElement>) {
|
||||
if (ev.currentTarget.scrollWidth > ev.currentTarget.offsetWidth) {
|
||||
ev.currentTarget.setAttribute("title", ev.currentTarget.innerText);
|
||||
private _setTitle(ev: Event) {
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
if (target.scrollWidth > target.offsetWidth) {
|
||||
target.setAttribute("title", target.innerText);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1010,12 +983,6 @@ export class HaDataTable extends LitElement {
|
||||
this._debounceSearch((ev.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
private _focusScroller(): void {
|
||||
this._scroller?.focus({
|
||||
preventScroll: true,
|
||||
});
|
||||
}
|
||||
|
||||
private async _calcTableHeight() {
|
||||
if (this.autoHeight) {
|
||||
return;
|
||||
@@ -1025,27 +992,23 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _saveScrollPos(e: HASSDomTargetEvent<HTMLDivElement>) {
|
||||
this._savedScrollPos = e.target.scrollTop;
|
||||
private _saveScrollPos(e: Event) {
|
||||
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
|
||||
|
||||
if (this._headerRow) {
|
||||
this._headerRow.scrollLeft = e.target.scrollLeft;
|
||||
}
|
||||
this.renderRoot.querySelector(".mdc-data-table__header-row")!.scrollLeft = (
|
||||
e.target as HTMLDivElement
|
||||
).scrollLeft;
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _scrollContent(e: HASSDomTargetEvent<HTMLDivElement>) {
|
||||
if (!this._scroller) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._scroller.scrollLeft = e.target.scrollLeft;
|
||||
private _scrollContent(e: Event) {
|
||||
this.renderRoot.querySelector("lit-virtualizer")!.scrollLeft = (
|
||||
e.target as HTMLDivElement
|
||||
).scrollLeft;
|
||||
}
|
||||
|
||||
private _collapseGroup = (
|
||||
ev: HASSDomCurrentTargetEvent<HTMLElement & { group: string }>
|
||||
) => {
|
||||
const groupName = ev.currentTarget.group;
|
||||
private _collapseGroup = (ev: Event) => {
|
||||
const groupName = (ev.currentTarget as any).group;
|
||||
if (this._collapsedGroups.includes(groupName)) {
|
||||
this._collapsedGroups = this._collapsedGroups.filter(
|
||||
(grp) => grp !== groupName
|
||||
@@ -1468,11 +1431,6 @@ export class HaDataTable extends LitElement {
|
||||
contain: size layout !important;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
lit-virtualizer:focus,
|
||||
lit-virtualizer:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -133,8 +133,7 @@ export class HaDateRangePicker extends LitElement {
|
||||
${!this.minimal
|
||||
? html`<ha-textarea
|
||||
id="field"
|
||||
rows="1"
|
||||
resize="auto"
|
||||
mobile-multiline
|
||||
@click=${this._openPicker}
|
||||
@keydown=${this._handleKeydown}
|
||||
.value=${(isThisYear(this.startDate)
|
||||
@@ -337,7 +336,14 @@ export class HaDateRangePicker extends LitElement {
|
||||
private _setTextareaFocusStyle(focused: boolean) {
|
||||
const textarea = this.renderRoot.querySelector("ha-textarea");
|
||||
if (textarea) {
|
||||
textarea.setFocused(focused);
|
||||
const foundation = (textarea as any).mdcFoundation;
|
||||
if (foundation) {
|
||||
if (focused) {
|
||||
foundation.activateFocus();
|
||||
} else {
|
||||
foundation.deactivateFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,8 +38,6 @@ export class HaEntityStatePicker extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ attribute: "no-entity", type: Boolean }) public noEntity = false;
|
||||
|
||||
private _getItems = memoizeOne(
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
@@ -126,8 +124,7 @@ export class HaEntityStatePicker extends LitElement {
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.disabled=${this.disabled ||
|
||||
(!this.entityId && this.noEntity === false)}
|
||||
.disabled=${this.disabled || !this.entityId}
|
||||
.autofocus=${this.autofocus}
|
||||
.required=${this.required}
|
||||
.label=${this.label ??
|
||||
|
||||
@@ -38,17 +38,6 @@ export const computeInitialHaFormData = (
|
||||
// Only add expandable data if it's required or any of its children have initial values.
|
||||
data[field.name] = expandableData;
|
||||
}
|
||||
} else if (field.type === "tabs") {
|
||||
const tabsData: Record<string, unknown> = {};
|
||||
for (const tab of field.tabs) {
|
||||
Object.assign(tabsData, computeInitialHaFormData(tab.schema));
|
||||
}
|
||||
const flattenTabs = field.flatten ?? !field.name;
|
||||
if (flattenTabs) {
|
||||
Object.assign(data, tabsData);
|
||||
} else if (field.required || Object.keys(tabsData).length) {
|
||||
data[field.name] = tabsData;
|
||||
}
|
||||
} else if (!field.required) {
|
||||
// Do nothing.
|
||||
} else if (field.type === "boolean") {
|
||||
|
||||
@@ -100,7 +100,9 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
inputMode="numeric"
|
||||
.label=${this.label}
|
||||
.hint=${this.helper}
|
||||
.value=${this.data?.toString() ?? ""}
|
||||
.value=${this.data !== undefined && this.data !== null
|
||||
? this.data.toString()
|
||||
: ""}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.schema.required}
|
||||
.autoValidate=${this.schema.required}
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-icon";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-tab-group";
|
||||
import "../ha-tab-group-tab";
|
||||
import "./ha-form";
|
||||
import type { HaForm } from "./ha-form";
|
||||
import type {
|
||||
HaFormDataContainer,
|
||||
HaFormElement,
|
||||
HaFormSchema,
|
||||
HaFormTabsSchema,
|
||||
} from "./types";
|
||||
|
||||
@customElement("ha-form-tabs")
|
||||
export class HaFormTabs extends LitElement implements HaFormElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public data!: HaFormDataContainer;
|
||||
|
||||
@property({ attribute: false }) public schema!: HaFormTabsSchema;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ attribute: false }) public computeLabel?: (
|
||||
schema: HaFormSchema,
|
||||
data?: HaFormDataContainer,
|
||||
options?: { path?: string[]; tab?: string }
|
||||
) => string;
|
||||
|
||||
@property({ attribute: false }) public computeHelper?: (
|
||||
schema: HaFormSchema,
|
||||
options?: { path?: string[] }
|
||||
) => string;
|
||||
|
||||
@property({ attribute: false }) public localizeValue?: (
|
||||
key: string
|
||||
) => string;
|
||||
|
||||
@state() private _activeTab?: string;
|
||||
|
||||
private _handleTabShow = (ev: CustomEvent<{ name: string }>) => {
|
||||
const name = ev.detail?.name;
|
||||
if (name !== undefined) {
|
||||
this._activeTab = name;
|
||||
}
|
||||
};
|
||||
|
||||
protected willUpdate(changedProps: Map<PropertyKey, unknown>): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("schema") && this.schema.tabs.length) {
|
||||
const first = this.schema.tabs[0]!.name;
|
||||
if (
|
||||
this._activeTab === undefined ||
|
||||
!this.schema.tabs.some((t) => t.name === this._activeTab)
|
||||
) {
|
||||
this._activeTab = first;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
const forms = this.renderRoot.querySelectorAll<HaForm>("ha-form");
|
||||
let valid = true;
|
||||
forms.forEach((form) => {
|
||||
if (!form.reportValidity()) {
|
||||
valid = false;
|
||||
}
|
||||
});
|
||||
return valid;
|
||||
}
|
||||
|
||||
private _computeLabel = (
|
||||
schema: HaFormSchema,
|
||||
data?: HaFormDataContainer,
|
||||
options?: { path?: string[] }
|
||||
) => {
|
||||
if (!this.computeLabel) {
|
||||
return undefined;
|
||||
}
|
||||
return this.computeLabel(schema, data, {
|
||||
...options,
|
||||
path: [...(options?.path || []), this.schema.name],
|
||||
});
|
||||
};
|
||||
|
||||
private _computeHelper = (
|
||||
schema: HaFormSchema,
|
||||
options?: { path?: string[] }
|
||||
) => {
|
||||
if (!this.computeHelper) {
|
||||
return undefined;
|
||||
}
|
||||
return this.computeHelper(schema, {
|
||||
...options,
|
||||
path: [...(options?.path || []), this.schema.name],
|
||||
});
|
||||
};
|
||||
|
||||
private _tabTitle(tabName: string): string {
|
||||
if (!this.computeLabel) {
|
||||
return tabName;
|
||||
}
|
||||
return (
|
||||
this.computeLabel(this.schema, this.data, {
|
||||
path: [...(this.schema.name ? [this.schema.name] : [])],
|
||||
tab: tabName,
|
||||
}) ?? tabName
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const tabs = this.schema.tabs;
|
||||
if (!tabs.length) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const active = this._activeTab ?? tabs[0]!.name;
|
||||
const fillTabs = this.schema.fill_tabs !== false;
|
||||
|
||||
return html`
|
||||
<ha-tab-group ?fill-tabs=${fillTabs} @wa-tab-show=${this._handleTabShow}>
|
||||
${tabs.map(
|
||||
(tab) => html`
|
||||
<ha-tab-group-tab
|
||||
slot="nav"
|
||||
.panel=${tab.name}
|
||||
.active=${active === tab.name}
|
||||
>
|
||||
${tab.icon
|
||||
? html`<ha-icon .icon=${tab.icon}></ha-icon>`
|
||||
: tab.iconPath
|
||||
? html`<ha-svg-icon .path=${tab.iconPath}></ha-svg-icon>`
|
||||
: nothing}
|
||||
${this._tabTitle(tab.name)}
|
||||
</ha-tab-group-tab>
|
||||
`
|
||||
)}
|
||||
</ha-tab-group>
|
||||
<div class="panels">
|
||||
${tabs.map((tab) => {
|
||||
const hidden = active !== tab.name;
|
||||
return html`
|
||||
<div class="panel" ?hidden=${hidden}>
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this.data}
|
||||
.schema=${tab.schema}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
.localizeValue=${this.localizeValue}
|
||||
></ha-form>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
}
|
||||
.panels {
|
||||
padding-top: var(--ha-space-4);
|
||||
}
|
||||
.panel[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
:host ha-form {
|
||||
display: block;
|
||||
}
|
||||
ha-tab-group {
|
||||
display: block;
|
||||
}
|
||||
ha-tab-group-tab ha-icon,
|
||||
ha-tab-group-tab ha-svg-icon {
|
||||
flex-shrink: 0;
|
||||
margin-inline-end: var(--ha-space-2);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-form-tabs": HaFormTabs;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ const LOAD_ELEMENTS = {
|
||||
float: () => import("./ha-form-float"),
|
||||
grid: () => import("./ha-form-grid"),
|
||||
expandable: () => import("./ha-form-expandable"),
|
||||
tabs: () => import("./ha-form-tabs"),
|
||||
integer: () => import("./ha-form-integer"),
|
||||
multi_select: () => import("./ha-form-multi_select"),
|
||||
positive_time_period_dict: () =>
|
||||
|
||||
@@ -14,8 +14,7 @@ export type HaFormSchema =
|
||||
| HaFormSelector
|
||||
| HaFormGridSchema
|
||||
| HaFormExpandableSchema
|
||||
| HaFormOptionalActionsSchema
|
||||
| HaFormTabsSchema;
|
||||
| HaFormOptionalActionsSchema;
|
||||
|
||||
export interface HaFormBaseSchema {
|
||||
name: string;
|
||||
@@ -55,26 +54,6 @@ export interface HaFormOptionalActionsSchema extends HaFormBaseSchema {
|
||||
schema: readonly HaFormSchema[];
|
||||
}
|
||||
|
||||
/** One tab pane inside a {@link HaFormTabsSchema} (not a standalone form field). */
|
||||
export interface HaFormTabDefinition {
|
||||
name: string;
|
||||
icon?: string;
|
||||
iconPath?: string;
|
||||
schema: readonly HaFormSchema[];
|
||||
}
|
||||
|
||||
export interface HaFormTabsSchema extends HaFormBaseSchema {
|
||||
type: "tabs";
|
||||
/** When true (default), tab field values merge into the parent data object. */
|
||||
flatten?: boolean;
|
||||
/**
|
||||
* When true (default), tab labels share width equally across the tab bar.
|
||||
* Set to false for compact tabs that only use their natural width.
|
||||
*/
|
||||
fill_tabs?: boolean;
|
||||
tabs: readonly HaFormTabDefinition[];
|
||||
}
|
||||
|
||||
export interface HaFormSelector extends HaFormBaseSchema {
|
||||
type?: never;
|
||||
selector: Selector;
|
||||
@@ -125,13 +104,6 @@ export interface HaFormTimeSchema extends HaFormBaseSchema {
|
||||
}
|
||||
|
||||
// Type utility to unionize a schema array by flattening any grid schemas
|
||||
type SchemaUnionTabs<T extends readonly HaFormTabDefinition[]> =
|
||||
T[number] extends infer Tab
|
||||
? Tab extends HaFormTabDefinition
|
||||
? SchemaUnion<Tab["schema"]>
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type SchemaUnion<
|
||||
SchemaArray extends readonly HaFormSchema[],
|
||||
Schema = SchemaArray[number],
|
||||
@@ -140,9 +112,7 @@ export type SchemaUnion<
|
||||
| HaFormExpandableSchema
|
||||
| HaFormOptionalActionsSchema
|
||||
? SchemaUnion<Schema["schema"]> | Schema
|
||||
: Schema extends HaFormTabsSchema
|
||||
? SchemaUnionTabs<Schema["tabs"]> | Schema
|
||||
: Schema;
|
||||
: Schema;
|
||||
|
||||
export type HaFormDataContainer = Record<string, HaFormData>;
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ export class HaGauge extends LitElement {
|
||||
? svg`
|
||||
<path
|
||||
class="needle"
|
||||
d="M -34,-3 L -48,-1 A 1,1,0,0,0,-48,1 L -34,3 A 2,2,0,0,0,-34,-3 Z"
|
||||
d="M -36,-2 L -44,-1 A 1,1,0,0,0,-44,1 L -36,2 A 2,2,0,0,0,-36,-2 Z"
|
||||
|
||||
style=${styleMap({ transform: `rotate(${this._angle}deg)` })}
|
||||
/>
|
||||
@@ -243,19 +243,19 @@ export class HaGauge extends LitElement {
|
||||
.levels-base {
|
||||
fill: none;
|
||||
stroke: var(--primary-background-color);
|
||||
stroke-width: 12;
|
||||
stroke-width: 6;
|
||||
stroke-linecap: butt;
|
||||
}
|
||||
|
||||
.level {
|
||||
fill: none;
|
||||
stroke-width: 12;
|
||||
stroke-width: 6;
|
||||
stroke-linecap: butt;
|
||||
}
|
||||
|
||||
.value {
|
||||
fill: none;
|
||||
stroke-width: 12;
|
||||
stroke-width: 6;
|
||||
stroke: var(--gauge-color);
|
||||
stroke-linecap: butt;
|
||||
transition: stroke-dashoffset 1s ease 0s;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { ReactiveElement, render, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import hash from "object-hash";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@@ -98,7 +98,6 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.noEntity=${this.selector.state?.no_entity ?? false}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
allow-custom-value
|
||||
|
||||
@@ -65,14 +65,15 @@ export class HaTextSelector extends LitElement {
|
||||
.label=${this.label}
|
||||
.placeholder=${this.placeholder}
|
||||
.value=${this.value || ""}
|
||||
.hint=${this.helper}
|
||||
.helper=${this.helper}
|
||||
helperPersistent
|
||||
.disabled=${this.disabled}
|
||||
@input=${this._handleChange}
|
||||
autocapitalize="none"
|
||||
.autocomplete=${this.selector.text?.autocomplete}
|
||||
spellcheck="false"
|
||||
.required=${this.required}
|
||||
resize="auto"
|
||||
autogrow
|
||||
></ha-textarea>`;
|
||||
}
|
||||
return html`<ha-input
|
||||
|
||||
@@ -26,11 +26,6 @@ export class HaTabGroupTab extends Tab {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tab {
|
||||
width: var(--ha-tab-base-width, auto);
|
||||
justify-content: var(--ha-tab-base-justify-content, flex-start);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
:host(:hover:not([disabled]):not([active])) .tab {
|
||||
color: var(--wa-color-brand-on-quiet);
|
||||
|
||||
@@ -13,28 +13,6 @@ export class HaTabGroup extends TabGroup {
|
||||
|
||||
@property({ attribute: "tab-only", type: Boolean }) tabOnly = true;
|
||||
|
||||
/** When true (default), each tab trigger grows to fill the tab row evenly. */
|
||||
@property({ type: Boolean, reflect: true, attribute: "fill-tabs" })
|
||||
fillTabs = true;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
// Prevent the tab group from consuming Alt+Arrow and Cmd+Arrow keys,
|
||||
// which browsers use for back/forward navigation.
|
||||
this.addEventListener("keydown", this._handleKeyDown, true);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("keydown", this._handleKeyDown, true);
|
||||
}
|
||||
|
||||
private _handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.altKey || event.metaKey) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
protected override handleClick(event: MouseEvent) {
|
||||
if (this._dragScrollController.scrolled) {
|
||||
return;
|
||||
@@ -74,13 +52,6 @@ export class HaTabGroup extends TabGroup {
|
||||
.scroll-button::part(base):hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
:host([fill-tabs]) .tab-group-top .tabs ::slotted(ha-tab-group-tab),
|
||||
:host([fill-tabs]) .tab-group-bottom .tabs ::slotted(ha-tab-group-tab) {
|
||||
flex: 1;
|
||||
--ha-tab-base-width: 100%;
|
||||
--ha-tab-base-justify-content: center;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
+48
-231
@@ -1,249 +1,66 @@
|
||||
import "@home-assistant/webawesome/dist/components/textarea/textarea";
|
||||
import type WaTextarea from "@home-assistant/webawesome/dist/components/textarea/textarea";
|
||||
import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { WaInputMixin, waInputStyles } from "./input/wa-input-mixin";
|
||||
import { TextAreaBase } from "@material/mwc-textarea/mwc-textarea-base";
|
||||
import { styles as textfieldStyles } from "@material/mwc-textfield/mwc-textfield.css";
|
||||
import { styles as textareaStyles } from "@material/mwc-textarea/mwc-textarea.css";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
/**
|
||||
* Home Assistant textarea component
|
||||
*
|
||||
* @element ha-textarea
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* A multi-line text input component supporting Home Assistant theming and validation, based on webawesome textarea.
|
||||
*
|
||||
* @slot label - Custom label content. Overrides the `label` property.
|
||||
* @slot hint - Custom hint content. Overrides the `hint` property.
|
||||
*
|
||||
* @csspart wa-base - The underlying wa-textarea base wrapper.
|
||||
* @csspart wa-hint - The underlying wa-textarea hint container.
|
||||
* @csspart wa-textarea - The underlying wa-textarea textarea element.
|
||||
*
|
||||
* @cssprop --ha-textarea-padding-bottom - Padding below the textarea host.
|
||||
* @cssprop --ha-textarea-max-height - Maximum height of the textarea when using `resize="auto"`. Defaults to `200px`.
|
||||
* @cssprop --ha-textarea-required-marker - The marker shown after the label for required fields. Defaults to `"*"`.
|
||||
*
|
||||
* @attr {string} label - The textarea's label text.
|
||||
* @attr {string} hint - The textarea's hint/helper text.
|
||||
* @attr {string} placeholder - Placeholder text shown when the textarea is empty.
|
||||
* @attr {boolean} readonly - Makes the textarea readonly.
|
||||
* @attr {boolean} disabled - Disables the textarea and prevents user interaction.
|
||||
* @attr {boolean} required - Makes the textarea a required field.
|
||||
* @attr {number} rows - Number of visible text rows.
|
||||
* @attr {number} minlength - Minimum number of characters required.
|
||||
* @attr {number} maxlength - Maximum number of characters allowed.
|
||||
* @attr {("none"|"vertical"|"horizontal"|"both"|"auto")} resize - Controls the textarea's resize behavior. Defaults to `"none"`.
|
||||
* @attr {boolean} auto-validate - Validates the textarea on blur instead of on form submit.
|
||||
* @attr {boolean} invalid - Marks the textarea as invalid.
|
||||
* @attr {string} validation-message - Custom validation message shown when the textarea is invalid.
|
||||
*/
|
||||
@customElement("ha-textarea")
|
||||
export class HaTextArea extends WaInputMixin(LitElement) {
|
||||
@property({ type: Number })
|
||||
public rows?: number;
|
||||
export class HaTextArea extends TextAreaBase {
|
||||
@property({ type: Boolean, reflect: true }) autogrow = false;
|
||||
|
||||
@property()
|
||||
public resize: "none" | "vertical" | "horizontal" | "both" | "auto" = "none";
|
||||
|
||||
@query("wa-textarea")
|
||||
private _textarea?: WaTextarea;
|
||||
|
||||
private readonly _hasSlotController = new HasSlotController(
|
||||
this,
|
||||
"label",
|
||||
"hint"
|
||||
);
|
||||
|
||||
protected get _formControl(): WaTextarea | undefined {
|
||||
return this._textarea;
|
||||
}
|
||||
|
||||
protected readonly _requiredMarkerCSSVar = "--ha-textarea-required-marker";
|
||||
|
||||
/** Programmatically toggle focus styling (used by ha-date-range-picker). */
|
||||
public setFocused(focused: boolean): void {
|
||||
if (focused) {
|
||||
this.toggleAttribute("focused", true);
|
||||
} else {
|
||||
this.removeAttribute("focused");
|
||||
updated(changedProperties: PropertyValues) {
|
||||
super.updated(changedProperties);
|
||||
if (this.autogrow && changedProperties.has("value")) {
|
||||
this.mdcRoot.dataset.value = this.value + '=\u200B"'; // add a zero-width space to correctly wrap
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const hasLabelSlot = this.label
|
||||
? false
|
||||
: this._hasSlotController.test("label");
|
||||
|
||||
const hasHintSlot = this.hint
|
||||
? false
|
||||
: this._hasSlotController.test("hint");
|
||||
|
||||
return html`
|
||||
<wa-textarea
|
||||
.value=${this.value ?? null}
|
||||
.placeholder=${this.placeholder}
|
||||
.readonly=${this.readonly}
|
||||
.required=${this.required}
|
||||
.rows=${this.rows ?? 4}
|
||||
.resize=${this.resize}
|
||||
.disabled=${this.disabled}
|
||||
name=${ifDefined(this.name)}
|
||||
autocapitalize=${ifDefined(this.autocapitalize || undefined)}
|
||||
autocomplete=${ifDefined(this.autocomplete)}
|
||||
.autofocus=${this.autofocus}
|
||||
.spellcheck=${this.spellcheck}
|
||||
inputmode=${ifDefined(this.inputmode || undefined)}
|
||||
enterkeyhint=${ifDefined(this.enterkeyhint || undefined)}
|
||||
minlength=${ifDefined(this.minlength)}
|
||||
maxlength=${ifDefined(this.maxlength)}
|
||||
class=${classMap({
|
||||
input: true,
|
||||
invalid: this.invalid || this._invalid,
|
||||
"label-raised":
|
||||
(this.value !== undefined && this.value !== "") ||
|
||||
(this.label && this.placeholder),
|
||||
"no-label": !this.label,
|
||||
"hint-hidden":
|
||||
!this.hint &&
|
||||
!hasHintSlot &&
|
||||
!this.required &&
|
||||
!this._invalid &&
|
||||
!this.invalid,
|
||||
})}
|
||||
@input=${this._handleInput}
|
||||
@change=${this._handleChange}
|
||||
@blur=${this._handleBlur}
|
||||
@wa-invalid=${this._handleInvalid}
|
||||
exportparts="base:wa-base, hint:wa-hint, textarea:wa-textarea"
|
||||
>
|
||||
${this.label || hasLabelSlot
|
||||
? html`<slot name="label" slot="label"
|
||||
>${this.label
|
||||
? this._renderLabel(this.label, this.required)
|
||||
: nothing}</slot
|
||||
>`
|
||||
: nothing}
|
||||
<div
|
||||
slot="hint"
|
||||
class=${classMap({
|
||||
error: this.invalid || this._invalid,
|
||||
})}
|
||||
role=${ifDefined(this.invalid || this._invalid ? "alert" : undefined)}
|
||||
aria-live="polite"
|
||||
>
|
||||
${this._invalid || this.invalid
|
||||
? this.validationMessage || this._textarea?.validationMessage
|
||||
: this.hint ||
|
||||
(hasHintSlot ? html`<slot name="hint"></slot>` : nothing)}
|
||||
</div>
|
||||
</wa-textarea>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
waInputStyles,
|
||||
static override styles = [
|
||||
textfieldStyles,
|
||||
textareaStyles,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding-bottom: var(--ha-textarea-padding-bottom);
|
||||
--mdc-text-field-fill-color: var(--ha-color-form-background);
|
||||
}
|
||||
|
||||
/* Label styling */
|
||||
wa-textarea::part(label) {
|
||||
width: calc(100% - var(--ha-space-2));
|
||||
background-color: var(--ha-color-form-background);
|
||||
transition:
|
||||
all var(--wa-transition-normal) ease-in-out,
|
||||
background-color var(--wa-transition-normal) ease-in-out;
|
||||
padding-inline-start: var(--ha-space-3);
|
||||
padding-inline-end: var(--ha-space-3);
|
||||
margin: var(--ha-space-1) var(--ha-space-1) 0;
|
||||
padding-top: var(--ha-space-4);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
:host([autogrow]) .mdc-text-field {
|
||||
position: relative;
|
||||
min-height: 74px;
|
||||
min-width: 178px;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
:host(:focus-within) wa-textarea::part(label),
|
||||
:host([focused]) wa-textarea::part(label) {
|
||||
color: var(--primary-color);
|
||||
:host([autogrow]) .mdc-text-field:after {
|
||||
content: attr(data-value);
|
||||
margin-top: 23px;
|
||||
margin-bottom: 9px;
|
||||
line-height: var(--ha-line-height-normal);
|
||||
min-height: 42px;
|
||||
padding: 0px 32px 0 16px;
|
||||
letter-spacing: var(
|
||||
--mdc-typography-subtitle1-letter-spacing,
|
||||
0.009375em
|
||||
);
|
||||
visibility: hidden;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
wa-textarea.label-raised::part(label),
|
||||
:host(:focus-within) wa-textarea::part(label),
|
||||
:host([focused]) wa-textarea::part(label) {
|
||||
padding-top: var(--ha-space-2);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
|
||||
wa-textarea.no-label::part(label) {
|
||||
height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Base styling */
|
||||
wa-textarea::part(base) {
|
||||
min-height: 56px;
|
||||
padding-top: var(--ha-space-6);
|
||||
padding-bottom: var(--ha-space-2);
|
||||
}
|
||||
|
||||
wa-textarea.no-label::part(base) {
|
||||
padding-top: var(--ha-space-3);
|
||||
}
|
||||
|
||||
wa-textarea::part(base)::after {
|
||||
content: "";
|
||||
:host([autogrow]) .mdc-text-field__input {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background-color: var(--ha-color-border-neutral-loud);
|
||||
transition:
|
||||
height var(--wa-transition-normal) ease-in-out,
|
||||
background-color var(--wa-transition-normal) ease-in-out;
|
||||
height: calc(100% - 32px);
|
||||
}
|
||||
|
||||
:host(:focus-within) wa-textarea::part(base)::after,
|
||||
:host([focused]) wa-textarea::part(base)::after {
|
||||
height: 2px;
|
||||
background-color: var(--primary-color);
|
||||
:host([autogrow]) .mdc-text-field.mdc-text-field--no-label:after {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:host(:focus-within) wa-textarea.invalid::part(base)::after,
|
||||
wa-textarea.invalid:not([disabled])::part(base)::after {
|
||||
background-color: var(--ha-color-border-danger-normal);
|
||||
.mdc-floating-label {
|
||||
inset-inline-start: 16px !important;
|
||||
inset-inline-end: initial !important;
|
||||
transform-origin: var(--float-start) top;
|
||||
}
|
||||
|
||||
/* Textarea element styling */
|
||||
wa-textarea::part(textarea) {
|
||||
padding: 0 var(--ha-space-4);
|
||||
font-family: var(--ha-font-family-body);
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
|
||||
:host([resize="auto"]) wa-textarea::part(textarea) {
|
||||
max-height: var(--ha-textarea-max-height, 200px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
wa-textarea:hover::part(base),
|
||||
wa-textarea:hover::part(label) {
|
||||
background-color: var(--ha-color-form-background-hover);
|
||||
}
|
||||
|
||||
wa-textarea[disabled]::part(textarea) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
wa-textarea[disabled]::part(base),
|
||||
wa-textarea[disabled]::part(label) {
|
||||
background-color: var(--ha-color-form-background-disabled);
|
||||
@media only screen and (min-width: 459px) {
|
||||
:host([mobile-multiline]) .mdc-text-field__input {
|
||||
white-space: nowrap;
|
||||
max-height: 16px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
import { supportsPassiveEventListener } from "@material/mwc-base/utils";
|
||||
import type { MDCTopAppBarAdapter } from "@material/top-app-bar/adapter";
|
||||
import { strings } from "@material/top-app-bar/constants";
|
||||
// eslint-disable-next-line import-x/no-named-as-default
|
||||
import MDCFixedTopAppBarFoundation from "@material/top-app-bar/fixed/foundation";
|
||||
import { html, css, nothing } from "lit";
|
||||
import { property, query, customElement } from "lit/decorators";
|
||||
|
||||
+359
-119
@@ -11,14 +11,15 @@ import {
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-tooltip";
|
||||
import { WaInputMixin, waInputStyles } from "./wa-input-mixin";
|
||||
import { nativeElementInternalsSupported } from "../../common/feature-detect/support-native-element-internals";
|
||||
|
||||
export type InputType =
|
||||
| "date"
|
||||
@@ -77,16 +78,35 @@ export type InputType =
|
||||
* @attr {string} validation-message - Custom validation message shown when the input is invalid.
|
||||
*/
|
||||
@customElement("ha-input")
|
||||
export class HaInput extends WaInputMixin(LitElement) {
|
||||
export class HaInput extends LitElement {
|
||||
@property({ reflect: true }) appearance: "material" | "outlined" = "material";
|
||||
|
||||
@property({ reflect: true })
|
||||
public type: InputType = "text";
|
||||
|
||||
@property()
|
||||
public value?: string;
|
||||
|
||||
/** The input's label. */
|
||||
@property()
|
||||
public label = "";
|
||||
|
||||
/** The input's hint. */
|
||||
@property()
|
||||
public hint? = "";
|
||||
|
||||
/** Adds a clear button when the input is not empty. */
|
||||
@property({ type: Boolean, attribute: "with-clear" })
|
||||
public withClear = false;
|
||||
|
||||
/** Placeholder text to show as a hint when the input is empty. */
|
||||
@property()
|
||||
public placeholder = "";
|
||||
|
||||
/** Makes the input readonly. */
|
||||
@property({ type: Boolean })
|
||||
public readonly = false;
|
||||
|
||||
/** Adds a button to toggle the password's visibility. */
|
||||
@property({ type: Boolean, attribute: "password-toggle" })
|
||||
public passwordToggle = false;
|
||||
@@ -99,10 +119,22 @@ export class HaInput extends WaInputMixin(LitElement) {
|
||||
@property({ type: Boolean, attribute: "without-spin-buttons" })
|
||||
public withoutSpinButtons = false;
|
||||
|
||||
/** Makes the input a required field. */
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public required = false;
|
||||
|
||||
/** A regular expression pattern to validate input against. */
|
||||
@property()
|
||||
public pattern?: string;
|
||||
|
||||
/** The minimum length of input that will be considered valid. */
|
||||
@property({ type: Number })
|
||||
public minlength?: number;
|
||||
|
||||
/** The maximum length of input that will be considered valid. */
|
||||
@property({ type: Number })
|
||||
public maxlength?: number;
|
||||
|
||||
/** The input's minimum value. Only applies to date and number input types. */
|
||||
@property()
|
||||
public min?: number | string;
|
||||
@@ -115,13 +147,88 @@ export class HaInput extends WaInputMixin(LitElement) {
|
||||
@property()
|
||||
public step?: number | "any";
|
||||
|
||||
/** Controls whether and how text input is automatically capitalized. */
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public autocapitalize:
|
||||
| "off"
|
||||
| "none"
|
||||
| "on"
|
||||
| "sentences"
|
||||
| "words"
|
||||
| "characters"
|
||||
| "" = "";
|
||||
|
||||
/** Indicates whether the browser's autocorrect feature is on or off. */
|
||||
@property({ type: Boolean })
|
||||
public autocorrect = false;
|
||||
|
||||
/** Specifies what permission the browser has to provide assistance in filling out form field values. */
|
||||
@property()
|
||||
public autocomplete?: string;
|
||||
|
||||
/** Indicates that the input should receive focus on page load. */
|
||||
@property({ type: Boolean })
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public autofocus = false;
|
||||
|
||||
/** Used to customize the label or icon of the Enter key on virtual keyboards. */
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public enterkeyhint:
|
||||
| "enter"
|
||||
| "done"
|
||||
| "go"
|
||||
| "next"
|
||||
| "previous"
|
||||
| "search"
|
||||
| "send"
|
||||
| "" = "";
|
||||
|
||||
/** Enables spell checking on the input. */
|
||||
@property({ type: Boolean })
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public spellcheck = true;
|
||||
|
||||
/** Tells the browser what type of data will be entered by the user. */
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public inputmode:
|
||||
| "none"
|
||||
| "text"
|
||||
| "decimal"
|
||||
| "numeric"
|
||||
| "tel"
|
||||
| "search"
|
||||
| "email"
|
||||
| "url"
|
||||
| "" = "";
|
||||
|
||||
/** The name of the input, submitted as a name/value pair with form data. */
|
||||
@property()
|
||||
public name?: string;
|
||||
|
||||
/** Disables the form control. */
|
||||
@property({ type: Boolean })
|
||||
public disabled = false;
|
||||
|
||||
/** Custom validation message to show when the input is invalid. */
|
||||
@property({ attribute: "validation-message" })
|
||||
public validationMessage? = "";
|
||||
|
||||
/** When true, validates the input on blur instead of on form submit. */
|
||||
@property({ type: Boolean, attribute: "auto-validate" })
|
||||
public autoValidate = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public invalid = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "inset-label" })
|
||||
public insetLabel = false;
|
||||
|
||||
@state()
|
||||
private _invalid = false;
|
||||
|
||||
@query("wa-input")
|
||||
private _input?: WaInput;
|
||||
|
||||
@@ -133,8 +240,37 @@ export class HaInput extends WaInputMixin(LitElement) {
|
||||
"start"
|
||||
);
|
||||
|
||||
protected get _formControl(): WaInput | undefined {
|
||||
return this._input;
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
/** Selects all the text in the input. */
|
||||
public select(): void {
|
||||
this._input?.select();
|
||||
}
|
||||
|
||||
/** Sets the start and end positions of the text selection (0-based). */
|
||||
public setSelectionRange(
|
||||
selectionStart: number,
|
||||
selectionEnd: number,
|
||||
selectionDirection?: "forward" | "backward" | "none"
|
||||
): void {
|
||||
this._input?.setSelectionRange(
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
selectionDirection
|
||||
);
|
||||
}
|
||||
|
||||
/** Replaces a range of text with a new string. */
|
||||
public setRangeText(
|
||||
replacement: string,
|
||||
start?: number,
|
||||
end?: number,
|
||||
selectMode?: "select" | "start" | "end" | "preserve"
|
||||
): void {
|
||||
this._input?.setRangeText(replacement, start, end, selectMode);
|
||||
}
|
||||
|
||||
/** Displays the browser picker for an input element. */
|
||||
@@ -152,6 +288,19 @@ export class HaInput extends WaInputMixin(LitElement) {
|
||||
this._input?.stepDown();
|
||||
}
|
||||
|
||||
public checkValidity(): boolean {
|
||||
return nativeElementInternalsSupported
|
||||
? (this._input?.checkValidity() ?? true)
|
||||
: true;
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
const valid = this.checkValidity();
|
||||
|
||||
this._invalid = !valid;
|
||||
return valid;
|
||||
}
|
||||
|
||||
protected override async firstUpdated(
|
||||
changedProperties: PropertyValues
|
||||
): Promise<void> {
|
||||
@@ -202,7 +351,6 @@ export class HaInput extends WaInputMixin(LitElement) {
|
||||
.name=${this.name}
|
||||
.disabled=${this.disabled}
|
||||
class=${classMap({
|
||||
input: true,
|
||||
invalid: this.invalid || this._invalid,
|
||||
"label-raised":
|
||||
(this.value !== undefined && this.value !== "") ||
|
||||
@@ -224,9 +372,7 @@ export class HaInput extends WaInputMixin(LitElement) {
|
||||
>
|
||||
${this.label || hasLabelSlot
|
||||
? html`<slot name="label" slot="label"
|
||||
>${this.label
|
||||
? this._renderLabel(this.label, this.required)
|
||||
: nothing}</slot
|
||||
>${this._renderLabel(this.label, this.required)}</slot
|
||||
>`
|
||||
: nothing}
|
||||
<slot name="start" slot="start" @slotchange=${this._syncStartSlotWidth}>
|
||||
@@ -273,6 +419,27 @@ export class HaInput extends WaInputMixin(LitElement) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
private _handleInput() {
|
||||
this.value = this._input?.value ?? undefined;
|
||||
if (this._invalid && this._input?.checkValidity()) {
|
||||
this._invalid = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleChange() {
|
||||
this.value = this._input?.value ?? undefined;
|
||||
}
|
||||
|
||||
private _handleBlur() {
|
||||
if (this.autoValidate) {
|
||||
this._invalid = !this._input?.checkValidity();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleInvalid() {
|
||||
this._invalid = true;
|
||||
}
|
||||
|
||||
private _syncStartSlotWidth = () => {
|
||||
const startEl = this._input?.shadowRoot?.querySelector(
|
||||
'[part~="start"]'
|
||||
@@ -293,133 +460,206 @@ export class HaInput extends WaInputMixin(LitElement) {
|
||||
}
|
||||
};
|
||||
|
||||
static styles = [
|
||||
waInputStyles,
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding-top: var(--ha-input-padding-top);
|
||||
padding-bottom: var(--ha-input-padding-bottom, var(--ha-space-2));
|
||||
text-align: var(--ha-input-text-align, start);
|
||||
}
|
||||
:host([appearance="outlined"]) {
|
||||
padding-bottom: var(--ha-input-padding-bottom);
|
||||
}
|
||||
private _renderLabel = memoizeOne((label: string, required: boolean) => {
|
||||
if (!required) {
|
||||
return label;
|
||||
}
|
||||
|
||||
wa-input::part(label) {
|
||||
padding-inline-start: calc(
|
||||
var(--start-slot-width, 0px) + var(--ha-space-4)
|
||||
);
|
||||
padding-inline-end: var(--ha-space-4);
|
||||
padding-top: var(--ha-space-5);
|
||||
}
|
||||
let marker = getComputedStyle(this).getPropertyValue(
|
||||
"--ha-input-required-marker"
|
||||
);
|
||||
|
||||
:host([appearance="material"]:focus-within) wa-input::part(label) {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
if (!marker) {
|
||||
marker = "*";
|
||||
}
|
||||
|
||||
wa-input.label-raised::part(label),
|
||||
:host(:focus-within) wa-input::part(label),
|
||||
:host([type="date"]) wa-input::part(label) {
|
||||
padding-top: var(--ha-space-3);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
if (marker.startsWith('"') && marker.endsWith('"')) {
|
||||
marker = marker.slice(1, -1);
|
||||
}
|
||||
|
||||
wa-input::part(base) {
|
||||
height: 56px;
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
if (!marker) {
|
||||
return label;
|
||||
}
|
||||
|
||||
:host([appearance="outlined"]) wa-input.no-label::part(base) {
|
||||
height: 32px;
|
||||
padding: 0 var(--ha-space-2);
|
||||
}
|
||||
return `${label}${marker}`;
|
||||
});
|
||||
|
||||
:host([appearance="outlined"]) wa-input::part(base) {
|
||||
border: 1px solid var(--ha-color-border-neutral-quiet);
|
||||
background-color: var(--card-background-color);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
transition: border-color var(--wa-transition-normal) ease-in-out;
|
||||
}
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding-top: var(--ha-input-padding-top);
|
||||
padding-bottom: var(--ha-input-padding-bottom, var(--ha-space-2));
|
||||
text-align: var(--ha-input-text-align, start);
|
||||
}
|
||||
:host([appearance="outlined"]) {
|
||||
padding-bottom: var(--ha-input-padding-bottom);
|
||||
}
|
||||
wa-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
--wa-transition-fast: var(--wa-transition-normal);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host([appearance="material"]) ::part(base)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background-color: var(--ha-color-border-neutral-loud);
|
||||
transition:
|
||||
height var(--wa-transition-normal) ease-in-out,
|
||||
background-color var(--wa-transition-normal) ease-in-out;
|
||||
}
|
||||
wa-input::part(label) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-family: var(--ha-font-family-body);
|
||||
transition: all var(--wa-transition-normal) ease-in-out;
|
||||
color: var(--secondary-text-color);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
padding-inline-start: calc(
|
||||
var(--start-slot-width, 0px) + var(--ha-space-4)
|
||||
);
|
||||
padding-inline-end: var(--ha-space-4);
|
||||
padding-top: var(--ha-space-5);
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
|
||||
:host([appearance="material"]:focus-within) wa-input::part(base)::after {
|
||||
height: 2px;
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
:host([appearance="material"]:focus-within) wa-input::part(label) {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
:host([appearance="material"]:focus-within)
|
||||
wa-input.invalid::part(base)::after,
|
||||
:host([appearance="material"])
|
||||
wa-input.invalid:not([disabled])::part(base)::after {
|
||||
background-color: var(--ha-color-border-danger-normal);
|
||||
}
|
||||
:host(:focus-within) wa-input.invalid::part(label),
|
||||
wa-input.invalid:not([disabled])::part(label) {
|
||||
color: var(--ha-color-fill-danger-loud-resting);
|
||||
}
|
||||
|
||||
wa-input::part(input) {
|
||||
padding-top: var(--ha-space-3);
|
||||
padding-inline-start: var(--input-padding-inline-start, 0);
|
||||
}
|
||||
wa-input.label-raised::part(label),
|
||||
:host(:focus-within) wa-input::part(label),
|
||||
:host([type="date"]) wa-input::part(label) {
|
||||
padding-top: var(--ha-space-3);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
}
|
||||
|
||||
wa-input.no-label::part(input) {
|
||||
padding-top: 0;
|
||||
}
|
||||
:host([type="color"]) wa-input::part(input) {
|
||||
padding-top: var(--ha-space-6);
|
||||
padding-bottom: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
:host([type="color"]) wa-input.no-label::part(input) {
|
||||
padding: var(--ha-space-2);
|
||||
}
|
||||
:host([type="color"]) wa-input.no-label::part(base) {
|
||||
padding: 0;
|
||||
}
|
||||
wa-input::part(input)::placeholder {
|
||||
color: var(--ha-color-neutral-60);
|
||||
}
|
||||
wa-input::part(base) {
|
||||
height: 56px;
|
||||
background-color: var(--ha-color-form-background);
|
||||
border-top-left-radius: var(--ha-border-radius-sm);
|
||||
border-top-right-radius: var(--ha-border-radius-sm);
|
||||
border-bottom-left-radius: var(--ha-border-radius-square);
|
||||
border-bottom-right-radius: var(--ha-border-radius-square);
|
||||
border: none;
|
||||
padding: 0 var(--ha-space-4);
|
||||
position: relative;
|
||||
transition: background-color var(--wa-transition-normal) ease-in-out;
|
||||
}
|
||||
|
||||
wa-input::part(base):hover {
|
||||
background-color: var(--ha-color-form-background-hover);
|
||||
}
|
||||
:host([appearance="outlined"]) wa-input.no-label::part(base) {
|
||||
height: 32px;
|
||||
padding: 0 var(--ha-space-2);
|
||||
}
|
||||
|
||||
:host([appearance="outlined"]) wa-input::part(base):hover {
|
||||
border-color: var(--ha-color-border-neutral-normal);
|
||||
}
|
||||
:host([appearance="outlined"]:focus-within) wa-input::part(base) {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
:host([appearance="outlined"]) wa-input::part(base) {
|
||||
border: 1px solid var(--ha-color-border-neutral-quiet);
|
||||
background-color: var(--card-background-color);
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
transition: border-color var(--wa-transition-normal) ease-in-out;
|
||||
}
|
||||
|
||||
wa-input:disabled::part(base) {
|
||||
background-color: var(--ha-color-form-background-disabled);
|
||||
}
|
||||
:host([appearance="material"]) ::part(base)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background-color: var(--ha-color-border-neutral-loud);
|
||||
transition:
|
||||
height var(--wa-transition-normal) ease-in-out,
|
||||
background-color var(--wa-transition-normal) ease-in-out;
|
||||
}
|
||||
|
||||
wa-input:disabled::part(label) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
:host([appearance="material"]:focus-within) wa-input::part(base)::after {
|
||||
height: 2px;
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
wa-input::part(end) {
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
:host([appearance="material"]:focus-within)
|
||||
wa-input.invalid::part(base)::after,
|
||||
:host([appearance="material"])
|
||||
wa-input.invalid:not([disabled])::part(base)::after {
|
||||
background-color: var(--ha-color-border-danger-normal);
|
||||
}
|
||||
|
||||
:host([appearance="outlined"]) wa-input.no-label {
|
||||
--ha-icon-button-size: 24px;
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
wa-input::part(input) {
|
||||
padding-top: var(--ha-space-3);
|
||||
padding-inline-start: var(--input-padding-inline-start, 0);
|
||||
}
|
||||
|
||||
wa-input.no-label::part(input) {
|
||||
padding-top: 0;
|
||||
}
|
||||
:host([type="color"]) wa-input::part(input) {
|
||||
padding-top: var(--ha-space-6);
|
||||
padding-bottom: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
:host([type="color"]) wa-input.no-label::part(input) {
|
||||
padding: var(--ha-space-2);
|
||||
}
|
||||
:host([type="color"]) wa-input.no-label::part(base) {
|
||||
padding: 0;
|
||||
}
|
||||
wa-input::part(input)::placeholder {
|
||||
color: var(--ha-color-neutral-60);
|
||||
}
|
||||
|
||||
:host(:focus-within) wa-input::part(base) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
wa-input::part(base):hover {
|
||||
background-color: var(--ha-color-form-background-hover);
|
||||
}
|
||||
|
||||
:host([appearance="outlined"]) wa-input::part(base):hover {
|
||||
border-color: var(--ha-color-border-neutral-normal);
|
||||
}
|
||||
:host([appearance="outlined"]:focus-within) wa-input::part(base) {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
wa-input:disabled::part(base) {
|
||||
background-color: var(--ha-color-form-background-disabled);
|
||||
}
|
||||
|
||||
wa-input:disabled::part(label) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
wa-input::part(hint) {
|
||||
min-height: var(--ha-space-5);
|
||||
margin-block-start: 0;
|
||||
margin-inline-start: var(--ha-space-3);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
wa-input.hint-hidden::part(hint) {
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--ha-color-on-danger-quiet);
|
||||
}
|
||||
|
||||
wa-input::part(end) {
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
:host([appearance="outlined"]) wa-input.no-label {
|
||||
--ha-icon-button-size: 24px;
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,357 +0,0 @@
|
||||
import { type LitElement, css } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { Constructor } from "../../types";
|
||||
import { nativeElementInternalsSupported } from "../../common/feature-detect/support-native-element-internals";
|
||||
|
||||
/**
|
||||
* Minimal interface for the inner wa-input / wa-textarea element.
|
||||
*/
|
||||
export interface WaInput {
|
||||
value: string | null;
|
||||
select(): void;
|
||||
setSelectionRange(
|
||||
start: number,
|
||||
end: number,
|
||||
direction?: "forward" | "backward" | "none"
|
||||
): void;
|
||||
setRangeText(
|
||||
replacement: string,
|
||||
start?: number,
|
||||
end?: number,
|
||||
selectMode?: "select" | "start" | "end" | "preserve"
|
||||
): void;
|
||||
checkValidity(): boolean;
|
||||
validationMessage: string;
|
||||
}
|
||||
|
||||
export interface WaInputMixinInterface {
|
||||
value?: string;
|
||||
label: string;
|
||||
hint?: string;
|
||||
placeholder: string;
|
||||
readonly: boolean;
|
||||
required: boolean;
|
||||
minlength?: number;
|
||||
maxlength?: number;
|
||||
autocapitalize:
|
||||
| "off"
|
||||
| "none"
|
||||
| "on"
|
||||
| "sentences"
|
||||
| "words"
|
||||
| "characters"
|
||||
| "";
|
||||
autocomplete?: string;
|
||||
autofocus: boolean;
|
||||
spellcheck: boolean;
|
||||
inputmode:
|
||||
| "none"
|
||||
| "text"
|
||||
| "decimal"
|
||||
| "numeric"
|
||||
| "tel"
|
||||
| "search"
|
||||
| "email"
|
||||
| "url"
|
||||
| "";
|
||||
enterkeyhint:
|
||||
| "enter"
|
||||
| "done"
|
||||
| "go"
|
||||
| "next"
|
||||
| "previous"
|
||||
| "search"
|
||||
| "send"
|
||||
| "";
|
||||
name?: string;
|
||||
disabled: boolean;
|
||||
validationMessage?: string;
|
||||
autoValidate: boolean;
|
||||
invalid: boolean;
|
||||
select(): void;
|
||||
setSelectionRange(
|
||||
start: number,
|
||||
end: number,
|
||||
direction?: "forward" | "backward" | "none"
|
||||
): void;
|
||||
setRangeText(
|
||||
replacement: string,
|
||||
start?: number,
|
||||
end?: number,
|
||||
selectMode?: "select" | "start" | "end" | "preserve"
|
||||
): void;
|
||||
checkValidity(): boolean;
|
||||
reportValidity(): boolean;
|
||||
}
|
||||
|
||||
export const WaInputMixin = <T extends Constructor<LitElement>>(
|
||||
superClass: T
|
||||
) => {
|
||||
class FormControlMixinClass extends superClass {
|
||||
@property()
|
||||
public value?: string;
|
||||
|
||||
@property()
|
||||
public label? = "";
|
||||
|
||||
@property()
|
||||
public hint? = "";
|
||||
|
||||
@property()
|
||||
public placeholder? = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
public readonly = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public required = false;
|
||||
|
||||
@property({ type: Number })
|
||||
public minlength?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
public maxlength?: number;
|
||||
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public autocapitalize:
|
||||
| "off"
|
||||
| "none"
|
||||
| "on"
|
||||
| "sentences"
|
||||
| "words"
|
||||
| "characters"
|
||||
| "" = "";
|
||||
|
||||
@property()
|
||||
public autocomplete?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public autofocus = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public spellcheck = true;
|
||||
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public inputmode:
|
||||
| "none"
|
||||
| "text"
|
||||
| "decimal"
|
||||
| "numeric"
|
||||
| "tel"
|
||||
| "search"
|
||||
| "email"
|
||||
| "url"
|
||||
| "" = "";
|
||||
|
||||
@property()
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
public enterkeyhint:
|
||||
| "enter"
|
||||
| "done"
|
||||
| "go"
|
||||
| "next"
|
||||
| "previous"
|
||||
| "search"
|
||||
| "send"
|
||||
| "" = "";
|
||||
|
||||
@property()
|
||||
public name?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public disabled = false;
|
||||
|
||||
@property({ attribute: "validation-message" })
|
||||
public validationMessage? = "";
|
||||
|
||||
@property({ type: Boolean, attribute: "auto-validate" })
|
||||
public autoValidate = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public invalid = false;
|
||||
|
||||
@state()
|
||||
protected _invalid = false;
|
||||
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Override in subclass to return the inner wa-input / wa-textarea element.
|
||||
*/
|
||||
protected get _formControl(): WaInput | undefined {
|
||||
throw new Error("_formControl getter must be implemented by subclass");
|
||||
}
|
||||
|
||||
/**
|
||||
* Override in subclass to set the CSS custom property name
|
||||
* used for the required-marker character (e.g. "--ha-input-required-marker").
|
||||
*/
|
||||
protected readonly _requiredMarkerCSSVar: string =
|
||||
"--ha-input-required-marker";
|
||||
|
||||
public select(): void {
|
||||
this._formControl?.select();
|
||||
}
|
||||
|
||||
public setSelectionRange(
|
||||
selectionStart: number,
|
||||
selectionEnd: number,
|
||||
selectionDirection?: "forward" | "backward" | "none"
|
||||
): void {
|
||||
this._formControl?.setSelectionRange(
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
selectionDirection
|
||||
);
|
||||
}
|
||||
|
||||
public setRangeText(
|
||||
replacement: string,
|
||||
start?: number,
|
||||
end?: number,
|
||||
selectMode?: "select" | "start" | "end" | "preserve"
|
||||
): void {
|
||||
this._formControl?.setRangeText(replacement, start, end, selectMode);
|
||||
}
|
||||
|
||||
public checkValidity(): boolean {
|
||||
return nativeElementInternalsSupported
|
||||
? (this._formControl?.checkValidity() ?? true)
|
||||
: true;
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
const valid = this.checkValidity();
|
||||
this._invalid = !valid;
|
||||
return valid;
|
||||
}
|
||||
|
||||
protected _handleInput(): void {
|
||||
this.value = this._formControl?.value ?? undefined;
|
||||
if (this._invalid && this._formControl?.checkValidity()) {
|
||||
this._invalid = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected _handleChange(): void {
|
||||
this.value = this._formControl?.value ?? undefined;
|
||||
}
|
||||
|
||||
protected _handleBlur(): void {
|
||||
if (this.autoValidate) {
|
||||
this._invalid = !this._formControl?.checkValidity();
|
||||
}
|
||||
}
|
||||
|
||||
protected _handleInvalid(): void {
|
||||
this._invalid = true;
|
||||
}
|
||||
|
||||
protected _renderLabel = memoizeOne((label: string, required: boolean) => {
|
||||
if (!required) {
|
||||
return label;
|
||||
}
|
||||
|
||||
let marker = getComputedStyle(this).getPropertyValue(
|
||||
this._requiredMarkerCSSVar
|
||||
);
|
||||
|
||||
if (!marker) {
|
||||
marker = "*";
|
||||
}
|
||||
|
||||
if (marker.startsWith('"') && marker.endsWith('"')) {
|
||||
marker = marker.slice(1, -1);
|
||||
}
|
||||
|
||||
if (!marker) {
|
||||
return label;
|
||||
}
|
||||
|
||||
return `${label}${marker}`;
|
||||
});
|
||||
}
|
||||
|
||||
return FormControlMixinClass;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shared styles for form controls (ha-input / ha-textarea).
|
||||
* Both components add the `control` CSS class to the inner wa-input / wa-textarea
|
||||
* element so these rules can target them with a single selector.
|
||||
*/
|
||||
export const waInputStyles = css`
|
||||
/* Inner element reset */
|
||||
.input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
--wa-transition-fast: var(--wa-transition-normal);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Label base */
|
||||
.input::part(label) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-family: var(--ha-font-family-body);
|
||||
transition: all var(--wa-transition-normal) ease-in-out;
|
||||
color: var(--secondary-text-color);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
|
||||
/* Invalid label */
|
||||
:host(:focus-within) .input.invalid::part(label),
|
||||
.input.invalid:not([disabled])::part(label) {
|
||||
color: var(--ha-color-fill-danger-loud-resting);
|
||||
}
|
||||
|
||||
/* Base common */
|
||||
.input::part(base) {
|
||||
background-color: var(--ha-color-form-background);
|
||||
border-top-left-radius: var(--ha-border-radius-sm);
|
||||
border-top-right-radius: var(--ha-border-radius-sm);
|
||||
border-bottom-left-radius: var(--ha-border-radius-square);
|
||||
border-bottom-right-radius: var(--ha-border-radius-square);
|
||||
border: none;
|
||||
position: relative;
|
||||
transition: background-color var(--wa-transition-normal) ease-in-out;
|
||||
}
|
||||
|
||||
/* Focus outline removal */
|
||||
:host(:focus-within) .input::part(base) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Hint */
|
||||
.input::part(hint) {
|
||||
min-height: var(--ha-space-5);
|
||||
margin-block-start: 0;
|
||||
margin-inline-start: var(--ha-space-3);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
.input.hint-hidden::part(hint) {
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Error hint text */
|
||||
.error {
|
||||
color: var(--ha-color-on-danger-quiet);
|
||||
}
|
||||
`;
|
||||
@@ -58,7 +58,7 @@ class BrowseMediaTTS extends LitElement {
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<ha-textarea
|
||||
resize="auto"
|
||||
autogrow
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.media-browser.tts.message"
|
||||
)}
|
||||
@@ -200,7 +200,7 @@ class BrowseMediaTTS extends LitElement {
|
||||
}
|
||||
|
||||
private async _ttsClicked(): Promise<void> {
|
||||
const message = this.shadowRoot!.querySelector("ha-textarea")!.value ?? "";
|
||||
const message = this.shadowRoot!.querySelector("ha-textarea")!.value;
|
||||
this._message = message;
|
||||
const item = { ...this.item };
|
||||
const query = new URLSearchParams();
|
||||
|
||||
+44
-95
@@ -60,9 +60,6 @@ export const enum CalendarEntityFeature {
|
||||
UPDATE_EVENT = 4,
|
||||
}
|
||||
|
||||
/** Type for date values that can come from REST API or subscription */
|
||||
type CalendarDateValue = string | { dateTime: string } | { date: string };
|
||||
|
||||
export const fetchCalendarEvents = async (
|
||||
hass: HomeAssistant,
|
||||
start: Date,
|
||||
@@ -75,11 +72,11 @@ export const fetchCalendarEvents = async (
|
||||
|
||||
const calEvents: CalendarEvent[] = [];
|
||||
const errors: string[] = [];
|
||||
const promises: Promise<CalendarEventApiData[]>[] = [];
|
||||
const promises: Promise<CalendarEvent[]>[] = [];
|
||||
|
||||
calendars.forEach((cal) => {
|
||||
promises.push(
|
||||
hass.callApi<CalendarEventApiData[]>(
|
||||
hass.callApi<CalendarEvent[]>(
|
||||
"GET",
|
||||
`calendars/${cal.entity_id}${params}`
|
||||
)
|
||||
@@ -87,7 +84,7 @@ export const fetchCalendarEvents = async (
|
||||
});
|
||||
|
||||
for (const [idx, promise] of promises.entries()) {
|
||||
let result: CalendarEventApiData[];
|
||||
let result: CalendarEvent[];
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
result = await promise;
|
||||
@@ -97,16 +94,54 @@ export const fetchCalendarEvents = async (
|
||||
}
|
||||
const cal = calendars[idx];
|
||||
result.forEach((ev) => {
|
||||
const normalized = normalizeSubscriptionEventData(ev, cal);
|
||||
if (normalized) {
|
||||
calEvents.push(normalized);
|
||||
const eventStart = getCalendarDate(ev.start);
|
||||
const eventEnd = getCalendarDate(ev.end);
|
||||
if (!eventStart || !eventEnd) {
|
||||
return;
|
||||
}
|
||||
const eventData: CalendarEventData = {
|
||||
uid: ev.uid,
|
||||
summary: ev.summary,
|
||||
description: ev.description,
|
||||
location: ev.location,
|
||||
dtstart: eventStart,
|
||||
dtend: eventEnd,
|
||||
recurrence_id: ev.recurrence_id,
|
||||
rrule: ev.rrule,
|
||||
};
|
||||
const event: CalendarEvent = {
|
||||
start: eventStart,
|
||||
end: eventEnd,
|
||||
title: ev.summary,
|
||||
backgroundColor: cal.backgroundColor,
|
||||
borderColor: cal.backgroundColor,
|
||||
calendar: cal.entity_id,
|
||||
eventData: eventData,
|
||||
};
|
||||
|
||||
calEvents.push(event);
|
||||
});
|
||||
}
|
||||
|
||||
return { events: calEvents, errors };
|
||||
};
|
||||
|
||||
const getCalendarDate = (dateObj: any): string | undefined => {
|
||||
if (typeof dateObj === "string") {
|
||||
return dateObj;
|
||||
}
|
||||
|
||||
if (dateObj.dateTime) {
|
||||
return dateObj.dateTime;
|
||||
}
|
||||
|
||||
if (dateObj.date) {
|
||||
return dateObj.date;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getCalendars = (
|
||||
hass: HomeAssistant,
|
||||
element: Element,
|
||||
@@ -185,89 +220,3 @@ export const deleteCalendarEvent = (
|
||||
recurrence_id,
|
||||
recurrence_range,
|
||||
});
|
||||
|
||||
/**
|
||||
* Calendar event data from both REST API and WebSocket subscription.
|
||||
* Both APIs use the same data format.
|
||||
*/
|
||||
export interface CalendarEventApiData {
|
||||
summary: string;
|
||||
start: CalendarDateValue;
|
||||
end: CalendarDateValue;
|
||||
description?: string | null;
|
||||
location?: string | null;
|
||||
uid?: string | null;
|
||||
recurrence_id?: string | null;
|
||||
rrule?: string | null;
|
||||
}
|
||||
|
||||
export interface CalendarEventSubscription {
|
||||
events: CalendarEventApiData[] | null;
|
||||
}
|
||||
|
||||
export const subscribeCalendarEvents = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
start: Date,
|
||||
end: Date,
|
||||
callback: (update: CalendarEventSubscription) => void
|
||||
) =>
|
||||
hass.connection.subscribeMessage<CalendarEventSubscription>(callback, {
|
||||
type: "calendar/event/subscribe",
|
||||
entity_id,
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
});
|
||||
|
||||
const getCalendarDate = (dateObj: CalendarDateValue): string | undefined => {
|
||||
if (typeof dateObj === "string") {
|
||||
return dateObj;
|
||||
}
|
||||
|
||||
if ("dateTime" in dateObj) {
|
||||
return dateObj.dateTime;
|
||||
}
|
||||
|
||||
if ("date" in dateObj) {
|
||||
return dateObj.date;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalize calendar event data from API format to internal format.
|
||||
* Handles both REST API format (with dateTime/date objects) and subscription format (strings).
|
||||
* Converts to internal format with { dtstart, dtend, ... }
|
||||
*/
|
||||
export const normalizeSubscriptionEventData = (
|
||||
eventData: CalendarEventApiData,
|
||||
calendar: Calendar
|
||||
): CalendarEvent | null => {
|
||||
const eventStart = getCalendarDate(eventData.start);
|
||||
const eventEnd = getCalendarDate(eventData.end);
|
||||
|
||||
if (!eventStart || !eventEnd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedEventData: CalendarEventData = {
|
||||
summary: eventData.summary,
|
||||
dtstart: eventStart,
|
||||
dtend: eventEnd,
|
||||
description: eventData.description ?? undefined,
|
||||
uid: eventData.uid ?? undefined,
|
||||
recurrence_id: eventData.recurrence_id ?? undefined,
|
||||
rrule: eventData.rrule ?? undefined,
|
||||
};
|
||||
|
||||
return {
|
||||
start: eventStart,
|
||||
end: eventEnd,
|
||||
title: eventData.summary,
|
||||
backgroundColor: calendar.backgroundColor,
|
||||
borderColor: calendar.backgroundColor,
|
||||
calendar: calendar.entity_id,
|
||||
eventData: normalizedEventData,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -458,7 +458,6 @@ export interface StateSelector {
|
||||
attribute?: string;
|
||||
hide_states?: string[];
|
||||
multiple?: boolean;
|
||||
no_entity?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -516,7 +516,7 @@ class DataEntryFlowDialog extends LitElement {
|
||||
}
|
||||
} else if (_step.next_flow[0] === "config_subentries_flow") {
|
||||
if (_step.type === "create_entry") {
|
||||
showSubConfigFlowDialog(this, _step.result!, "", {
|
||||
showSubConfigFlowDialog(this, _step.result!, _step.next_flow[0], {
|
||||
continueFlowId: _step.next_flow[1],
|
||||
navigateToResult: this._params!.navigateToResult,
|
||||
dialogClosedCallback: this._params!.dialogClosedCallback,
|
||||
|
||||
@@ -99,16 +99,6 @@ export const showConfigFlowDialog = (
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === "tabs" && options?.tab) {
|
||||
const sectionPrefix = field.name ? `sections.${field.name}.` : "";
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${step.handler}.config.step.${step.step_id}.${sectionPrefix}tabs.${options.tab}.name`,
|
||||
step.description_placeholders
|
||||
) || options.tab
|
||||
);
|
||||
}
|
||||
|
||||
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||
|
||||
return (
|
||||
@@ -127,17 +117,6 @@ export const showConfigFlowDialog = (
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === "tabs" && options?.tab) {
|
||||
const sectionPrefix = field.name ? `sections.${field.name}.` : "";
|
||||
const tabDescription = hass.localize(
|
||||
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.${sectionPrefix}tabs.${options.tab}.description`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return tabDescription
|
||||
? html`<ha-markdown breaks .content=${tabDescription}></ha-markdown>`
|
||||
: "";
|
||||
}
|
||||
|
||||
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||
|
||||
const description = hass.localize(
|
||||
|
||||
@@ -62,14 +62,14 @@ export interface FlowConfig {
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepForm,
|
||||
field: HaFormSchema,
|
||||
options: { path?: string[]; tab?: string; [key: string]: any }
|
||||
options: { path?: string[]; [key: string]: any }
|
||||
): string;
|
||||
|
||||
renderShowFormStepFieldHelper(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepForm,
|
||||
field: HaFormSchema,
|
||||
options: { path?: string[]; tab?: string; [key: string]: any }
|
||||
options: { path?: string[]; [key: string]: any }
|
||||
): TemplateResult | string;
|
||||
|
||||
renderShowFormStepFieldError(
|
||||
|
||||
@@ -103,16 +103,6 @@ export const showOptionsFlowDialog = (
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === "tabs" && options?.tab) {
|
||||
const sectionPrefix = field.name ? `sections.${field.name}.` : "";
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.${sectionPrefix}tabs.${options.tab}.name`,
|
||||
step.description_placeholders
|
||||
) || options.tab
|
||||
);
|
||||
}
|
||||
|
||||
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||
|
||||
return (
|
||||
@@ -131,20 +121,6 @@ export const showOptionsFlowDialog = (
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === "tabs" && options?.tab) {
|
||||
const sectionPrefix = field.name ? `sections.${field.name}.` : "";
|
||||
const tabDescription = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.${sectionPrefix}tabs.${options.tab}.description`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return tabDescription
|
||||
? html`<ha-markdown
|
||||
breaks
|
||||
.content=${tabDescription}
|
||||
></ha-markdown>`
|
||||
: "";
|
||||
}
|
||||
|
||||
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||
|
||||
const description = hass.localize(
|
||||
|
||||
@@ -39,7 +39,6 @@ export const showSubConfigFlowDialog = (
|
||||
},
|
||||
fetchFlow: async (hass, flowId) => {
|
||||
const step = await fetchSubConfigFlow(hass, flowId);
|
||||
flowType = (step.handler as unknown as [string, string])[1];
|
||||
await hass.loadFragmentTranslation("config");
|
||||
await hass.loadBackendTranslation(
|
||||
"config_subentries",
|
||||
@@ -95,16 +94,6 @@ export const showSubConfigFlowDialog = (
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === "tabs" && options?.tab) {
|
||||
const sectionPrefix = field.name ? `sections.${field.name}.` : "";
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.${sectionPrefix}tabs.${options.tab}.name`,
|
||||
step.description_placeholders
|
||||
) || options.tab
|
||||
);
|
||||
}
|
||||
|
||||
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||
|
||||
return (
|
||||
@@ -123,17 +112,6 @@ export const showSubConfigFlowDialog = (
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === "tabs" && options?.tab) {
|
||||
const sectionPrefix = field.name ? `sections.${field.name}.` : "";
|
||||
const tabDescription = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.config_subentries.${flowType}.step.${step.step_id}.${sectionPrefix}tabs.${options.tab}.description`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return tabDescription
|
||||
? html`<ha-markdown breaks .content=${tabDescription}></ha-markdown>`
|
||||
: "";
|
||||
}
|
||||
|
||||
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||
|
||||
const description = hass.localize(
|
||||
|
||||
@@ -75,17 +75,7 @@ class StepFlowForm extends LitElement {
|
||||
handleReadOnlyField(sectionField)
|
||||
),
|
||||
}
|
||||
: field.type === "tabs" && field.tabs
|
||||
? {
|
||||
...field,
|
||||
tabs: field.tabs.map((tab) => ({
|
||||
...tab,
|
||||
schema: tab.schema.map((tabField) =>
|
||||
handleReadOnlyField(tabField)
|
||||
),
|
||||
})),
|
||||
}
|
||||
: handleReadOnlyField(field)
|
||||
: handleReadOnlyField(field)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ export class DialogEnterCode
|
||||
|
||||
private _numberClick(e: MouseEvent): void {
|
||||
const val = (e.currentTarget! as any).value;
|
||||
this._input!.value = (this._input!.value ?? "") + val;
|
||||
this._input!.value = this._input!.value + val;
|
||||
this._showClearButton = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -177,10 +177,7 @@ export const showDialog = async (
|
||||
throw new Error("Unknown dialog type loaded");
|
||||
}
|
||||
|
||||
const targetParent = (parentElement || element).shadowRoot!;
|
||||
if (dialogElement!.parentNode !== targetParent) {
|
||||
targetParent.appendChild(dialogElement!);
|
||||
}
|
||||
(parentElement || element).shadowRoot!.appendChild(dialogElement!);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -76,7 +76,7 @@ export class TTSTryDialog extends LitElement {
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-textarea
|
||||
resize="auto"
|
||||
autogrow
|
||||
id="message"
|
||||
.label=${this.hass.localize("ui.dialogs.tts-try.message")}
|
||||
.placeholder=${this.hass.localize(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/triple-slash-reference */
|
||||
|
||||
/// <reference path="../types/service-worker.d.ts" />
|
||||
/* eslint-env serviceworker */
|
||||
import type { RouteHandler } from "workbox-core";
|
||||
import { cacheNames } from "workbox-core";
|
||||
import { CacheableResponsePlugin } from "workbox-cacheable-response";
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
query,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { customElement, eventOptions, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { canShowPage } from "../common/config/can_show_page";
|
||||
@@ -81,8 +75,6 @@ export class HassTabsSubpage extends LitElement {
|
||||
|
||||
@state() private _activeTab?: PageNavigation;
|
||||
|
||||
@query(".content") private _content?: HTMLDivElement;
|
||||
|
||||
// @ts-ignore
|
||||
@restoreScroll(".content") private _savedScrollPos?: number;
|
||||
|
||||
@@ -219,15 +211,6 @@ export class HassTabsSubpage extends LitElement {
|
||||
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
|
||||
}
|
||||
|
||||
public focusContentScroller() {
|
||||
if (!this._content) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._content.style.outline = "none";
|
||||
this._content.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
private _backTapped(): void {
|
||||
if (this.backCallback) {
|
||||
this.backCallback();
|
||||
|
||||
@@ -34,7 +34,6 @@ const COMPONENTS = {
|
||||
light: () => import("../panels/light/ha-panel-light"),
|
||||
security: () => import("../panels/security/ha-panel-security"),
|
||||
climate: () => import("../panels/climate/ha-panel-climate"),
|
||||
maintenance: () => import("../panels/maintenance/ha-panel-maintenance"),
|
||||
home: () => import("../panels/home/ha-panel-home"),
|
||||
notfound: () => import("../panels/notfound/ha-panel-notfound"),
|
||||
};
|
||||
|
||||
@@ -36,19 +36,11 @@ class NotificationManager extends LitElement {
|
||||
@query("ha-toast")
|
||||
private _toast!: HTMLElementTagNameMap["ha-toast"] | undefined;
|
||||
|
||||
private _showDialogId = 0;
|
||||
|
||||
public async showDialog(parameters: ShowToastParams) {
|
||||
const showId = ++this._showDialogId;
|
||||
|
||||
if (!parameters.id || this._parameters?.id !== parameters.id) {
|
||||
await this._toast?.hide();
|
||||
}
|
||||
|
||||
if (showId !== this._showDialogId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (parameters.duration === 0) {
|
||||
this._parameters = undefined;
|
||||
return;
|
||||
@@ -64,11 +56,6 @@ class NotificationManager extends LitElement {
|
||||
}
|
||||
|
||||
await this.updateComplete;
|
||||
|
||||
if (showId !== this._showDialogId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._toast?.show();
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
)}
|
||||
.value=${this._description}
|
||||
@change=${this._handleDescriptionChanged}
|
||||
resize="auto"
|
||||
autogrow
|
||||
></ha-textarea>
|
||||
<ha-entity-picker
|
||||
name="calendar"
|
||||
|
||||
@@ -21,17 +21,8 @@ import "../../components/ha-spinner";
|
||||
import "../../components/ha-state-icon";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-two-pane-top-app-bar-fixed";
|
||||
import type {
|
||||
Calendar,
|
||||
CalendarEvent,
|
||||
CalendarEventSubscription,
|
||||
CalendarEventApiData,
|
||||
} from "../../data/calendar";
|
||||
import {
|
||||
getCalendars,
|
||||
normalizeSubscriptionEventData,
|
||||
subscribeCalendarEvents,
|
||||
} from "../../data/calendar";
|
||||
import type { Calendar, CalendarEvent } from "../../data/calendar";
|
||||
import { fetchCalendarEvents, getCalendars } from "../../data/calendar";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import { subscribeEntityRegistry } from "../../data/entity/entity_registry";
|
||||
import { fetchIntegrationManifest } from "../../data/integration";
|
||||
@@ -55,8 +46,6 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _error?: string = undefined;
|
||||
|
||||
@state() private _errorCalendars: string[] = [];
|
||||
|
||||
@state() private _entityRegistry?: EntityRegistryEntry[];
|
||||
|
||||
@state()
|
||||
@@ -70,8 +59,6 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _end?: Date;
|
||||
|
||||
private _unsubs: Record<string, Promise<UnsubscribeFunc>> = {};
|
||||
|
||||
private _showPaneController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width > 750,
|
||||
});
|
||||
@@ -91,7 +78,6 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
super.disconnectedCallback();
|
||||
this._mql?.removeListener(this._setIsMobile!);
|
||||
this._mql = undefined;
|
||||
this._unsubscribeAll();
|
||||
}
|
||||
|
||||
private _setIsMobile = (ev: MediaQueryListEvent) => {
|
||||
@@ -104,11 +90,15 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
this._entityRegistry = entities;
|
||||
// Refresh calendars when entity registry updates (includes color changes)
|
||||
this._calendars = getCalendars(this.hass, this, this._entityRegistry);
|
||||
// Resubscribe events if view dates are available (handles both initial load and color updates)
|
||||
// Refetch events if view dates are available (handles both initial load and color updates)
|
||||
if (this._start && this._end) {
|
||||
this._unsubscribeAll().then(() => {
|
||||
this._events = [];
|
||||
this._subscribeCalendarEvents(this._selectedCalendars);
|
||||
this._fetchEvents(
|
||||
this._start,
|
||||
this._end,
|
||||
this._selectedCalendars
|
||||
).then((result) => {
|
||||
this._events = result.events;
|
||||
this._handleErrors(result.errors);
|
||||
});
|
||||
}
|
||||
}),
|
||||
@@ -222,95 +212,19 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
.map((cal) => cal);
|
||||
}
|
||||
|
||||
private _subscribeCalendarEvents(calendars: Calendar[]): void {
|
||||
if (!this._start || !this._end || calendars.length === 0) {
|
||||
return;
|
||||
private async _fetchEvents(
|
||||
start: Date | undefined,
|
||||
end: Date | undefined,
|
||||
calendars: Calendar[]
|
||||
): Promise<{ events: CalendarEvent[]; errors: string[] }> {
|
||||
if (!calendars.length || !start || !end) {
|
||||
return { events: [], errors: [] };
|
||||
}
|
||||
|
||||
this._error = undefined;
|
||||
|
||||
calendars.forEach((calendar) => {
|
||||
// Unsubscribe existing subscription if any
|
||||
if (calendar.entity_id in this._unsubs) {
|
||||
this._unsubs[calendar.entity_id]
|
||||
.then((unsubFunc) => unsubFunc())
|
||||
.catch(() => {
|
||||
// Subscription may have already been closed
|
||||
});
|
||||
}
|
||||
|
||||
const unsub = subscribeCalendarEvents(
|
||||
this.hass,
|
||||
calendar.entity_id,
|
||||
this._start!,
|
||||
this._end!,
|
||||
(update: CalendarEventSubscription) => {
|
||||
this._handleCalendarUpdate(calendar, update);
|
||||
}
|
||||
);
|
||||
this._unsubs[calendar.entity_id] = unsub;
|
||||
});
|
||||
return fetchCalendarEvents(this.hass, start, end, calendars);
|
||||
}
|
||||
|
||||
private _handleCalendarUpdate(
|
||||
calendar: Calendar,
|
||||
update: CalendarEventSubscription
|
||||
): void {
|
||||
// Remove events from this calendar
|
||||
this._events = this._events.filter(
|
||||
(event) => event.calendar !== calendar.entity_id
|
||||
);
|
||||
|
||||
if (update.events === null) {
|
||||
// Error fetching events
|
||||
if (!this._errorCalendars.includes(calendar.entity_id)) {
|
||||
this._errorCalendars = [...this._errorCalendars, calendar.entity_id];
|
||||
}
|
||||
this._handleErrors(this._errorCalendars);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from error list if successfully loaded
|
||||
this._errorCalendars = this._errorCalendars.filter(
|
||||
(id) => id !== calendar.entity_id
|
||||
);
|
||||
this._handleErrors(this._errorCalendars);
|
||||
|
||||
// Add new events from this calendar
|
||||
const newEvents: CalendarEvent[] = update.events
|
||||
.map((eventData: CalendarEventApiData) =>
|
||||
normalizeSubscriptionEventData(eventData, calendar)
|
||||
)
|
||||
.filter((event): event is CalendarEvent => event !== null);
|
||||
|
||||
this._events = [...this._events, ...newEvents];
|
||||
}
|
||||
|
||||
private async _unsubscribeAll(): Promise<void> {
|
||||
await Promise.all(
|
||||
Object.values(this._unsubs).map((unsub) =>
|
||||
unsub
|
||||
.then((unsubFunc) => unsubFunc())
|
||||
.catch(() => {
|
||||
// Subscription may have already been closed
|
||||
})
|
||||
)
|
||||
);
|
||||
this._unsubs = {};
|
||||
}
|
||||
|
||||
private _unsubscribeCalendar(entityId: string): void {
|
||||
if (entityId in this._unsubs) {
|
||||
this._unsubs[entityId]
|
||||
.then((unsubFunc) => unsubFunc())
|
||||
.catch(() => {
|
||||
// Subscription may have already been closed
|
||||
});
|
||||
delete this._unsubs[entityId];
|
||||
}
|
||||
}
|
||||
|
||||
private _requestSelected(ev: Event) {
|
||||
private async _requestSelected(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const item = ev.currentTarget as HaDropdownItem;
|
||||
const entityId = item.value as string;
|
||||
@@ -326,10 +240,13 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
if (!calendar) {
|
||||
return;
|
||||
}
|
||||
this._subscribeCalendarEvents([calendar]);
|
||||
const result = await this._fetchEvents(this._start, this._end, [
|
||||
calendar,
|
||||
]);
|
||||
this._events = [...this._events, ...result.events];
|
||||
this._handleErrors(result.errors);
|
||||
} else {
|
||||
this._deSelectedCalendars = [...this._deSelectedCalendars, entityId];
|
||||
this._unsubscribeCalendar(entityId);
|
||||
this._events = this._events.filter(
|
||||
(event) => event.calendar !== entityId
|
||||
);
|
||||
@@ -354,15 +271,23 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
|
||||
): Promise<void> {
|
||||
this._start = ev.detail.start;
|
||||
this._end = ev.detail.end;
|
||||
await this._unsubscribeAll();
|
||||
this._events = [];
|
||||
this._subscribeCalendarEvents(this._selectedCalendars);
|
||||
const result = await this._fetchEvents(
|
||||
this._start,
|
||||
this._end,
|
||||
this._selectedCalendars
|
||||
);
|
||||
this._events = result.events;
|
||||
this._handleErrors(result.errors);
|
||||
}
|
||||
|
||||
private async _handleRefresh(): Promise<void> {
|
||||
await this._unsubscribeAll();
|
||||
this._events = [];
|
||||
this._subscribeCalendarEvents(this._selectedCalendars);
|
||||
const result = await this._fetchEvents(
|
||||
this._start,
|
||||
this._end,
|
||||
this._selectedCalendars
|
||||
);
|
||||
this._events = result.events;
|
||||
this._handleErrors(result.errors);
|
||||
}
|
||||
|
||||
private _handleErrors(error_entity_ids: string[]) {
|
||||
|
||||
@@ -32,7 +32,7 @@ class PanelClimate extends LitElement {
|
||||
|
||||
@state() private _lovelace?: Lovelace;
|
||||
|
||||
@state() private _searchParams = new URLSearchParams(window.location.search);
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
@@ -99,36 +99,41 @@ class PanelClimate extends LitElement {
|
||||
return html`
|
||||
<div class="header ${classMap({ narrow: this.narrow })}">
|
||||
<div class="toolbar">
|
||||
${this._searchParams.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`}
|
||||
${
|
||||
this._searchParms.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`
|
||||
}
|
||||
<div class="main-title">${this.hass.localize("panel.climate")}</div>
|
||||
</div>
|
||||
</div>
|
||||
${this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}> </hui-view-background>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}> </hui-view-background>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</hui-view-container>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -165,9 +165,10 @@ class DialogAutomationSave extends LitElement implements HassDialog {
|
||||
"ui.panel.config.automation.editor.description.placeholder"
|
||||
)}
|
||||
name="description"
|
||||
resize="auto"
|
||||
autogrow
|
||||
.value=${this._newDescription}
|
||||
.hint=${supportsMarkdownHelper(this.hass.localize)}
|
||||
.helper=${supportsMarkdownHelper(this.hass.localize)}
|
||||
helperPersistent
|
||||
@input=${this._valueChanged}
|
||||
></ha-textarea>`
|
||||
: nothing}
|
||||
|
||||
@@ -73,16 +73,13 @@ export class HaPlatformCondition extends LitElement {
|
||||
}
|
||||
|
||||
if (
|
||||
oldValue?.condition !== this.condition?.condition &&
|
||||
this.condition &&
|
||||
oldValue?.condition !== this.condition.condition &&
|
||||
this.description?.fields
|
||||
) {
|
||||
const hadOptions = "options" in this.condition;
|
||||
const updatedOptions = this.condition.options
|
||||
? { ...this.condition.options }
|
||||
: {};
|
||||
const loadDefaults = !hadOptions;
|
||||
let updatedDefaultValue = false;
|
||||
const updatedOptions = {};
|
||||
const loadDefaults = !("options" in this.condition);
|
||||
// Set mandatory bools without a default value to false
|
||||
Object.entries(this.description.fields).forEach(([key, field]) => {
|
||||
if (
|
||||
@@ -109,7 +106,7 @@ export class HaPlatformCondition extends LitElement {
|
||||
updatedOptions[key] = field.default;
|
||||
}
|
||||
});
|
||||
if (!hadOptions || updatedDefaultValue) {
|
||||
if (updatedDefaultValue) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.condition,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../../components/ha-textarea";
|
||||
import type { TemplateCondition } from "../../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
|
||||
@@ -108,16 +108,13 @@ export class HaPlatformTrigger extends LitElement {
|
||||
}
|
||||
|
||||
if (
|
||||
oldValue?.trigger !== this.trigger?.trigger &&
|
||||
this.trigger &&
|
||||
oldValue?.trigger !== this.trigger.trigger &&
|
||||
this.description?.fields
|
||||
) {
|
||||
const hadOptions = "options" in this.trigger;
|
||||
const updatedOptions = this.trigger.options
|
||||
? { ...this.trigger.options }
|
||||
: {};
|
||||
const loadDefaults = !hadOptions;
|
||||
let updatedDefaultValue = false;
|
||||
const updatedOptions = {};
|
||||
const loadDefaults = !("options" in this.trigger);
|
||||
// Set mandatory bools without a default value to false
|
||||
Object.entries(this.description.fields).forEach(([key, field]) => {
|
||||
if (
|
||||
@@ -145,7 +142,7 @@ export class HaPlatformTrigger extends LitElement {
|
||||
}
|
||||
});
|
||||
|
||||
if (!hadOptions || updatedDefaultValue) {
|
||||
if (updatedDefaultValue) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.trigger,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "../../../../../components/ha-textarea";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@@ -8,6 +8,7 @@ import "../../../../components/ha-markdown-element";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-select";
|
||||
import "../../../../components/ha-spinner";
|
||||
import "../../../../components/ha-textarea";
|
||||
import { fetchSupportPackage } from "../../../../data/cloud";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { fileDownload } from "../../../../util/file_download";
|
||||
|
||||
@@ -101,7 +101,7 @@ export class DialogTryTts extends LitElement {
|
||||
>
|
||||
<div>
|
||||
<ha-textarea
|
||||
resize="auto"
|
||||
autogrow
|
||||
id="message"
|
||||
autofocus
|
||||
.label=${this.hass.localize(
|
||||
|
||||
@@ -69,7 +69,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _parse() {
|
||||
const sentences = (this._sentencesInput.value || "")
|
||||
const sentences = this._sentencesInput.value
|
||||
.split("\n")
|
||||
.filter((a) => a !== "");
|
||||
const { results } = await debugAgent(this.hass, sentences, this._language!);
|
||||
@@ -139,7 +139,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) {
|
||||
`
|
||||
: nothing}
|
||||
<ha-textarea
|
||||
resize="auto"
|
||||
autogrow
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.developer-tools.tabs.assist.sentences"
|
||||
)}
|
||||
|
||||
+18
-8
@@ -11,6 +11,7 @@ import {
|
||||
NetworkType,
|
||||
getMatterNodeDiagnostics,
|
||||
} from "../../../../../../data/matter";
|
||||
import { getMatterLockInfo } from "../../../../../../data/matter-lock";
|
||||
import type { HomeAssistant } from "../../../../../../types";
|
||||
import { showMatterManageFabricsDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-manage-fabrics";
|
||||
import { showMatterOpenCommissioningWindowDialog } from "../../../../integrations/integration-panels/matter/show-dialog-matter-open-commissioning-window";
|
||||
@@ -108,14 +109,23 @@ export const getMatterDeviceActions = async (
|
||||
);
|
||||
|
||||
if (lockEntity) {
|
||||
actions.push({
|
||||
label: hass.localize("ui.panel.config.matter.device_actions.manage_lock"),
|
||||
icon: mdiAccountLock,
|
||||
action: () =>
|
||||
showMatterLockManageDialog(el, {
|
||||
entity_id: lockEntity.entity_id,
|
||||
}),
|
||||
});
|
||||
try {
|
||||
const lockInfo = await getMatterLockInfo(hass, lockEntity.entity_id);
|
||||
if (lockInfo.supports_user_management) {
|
||||
actions.push({
|
||||
label: hass.localize(
|
||||
"ui.panel.config.matter.device_actions.manage_lock"
|
||||
),
|
||||
icon: mdiAccountLock,
|
||||
action: () =>
|
||||
showMatterLockManageDialog(el, {
|
||||
entity_id: lockEntity.entity_id,
|
||||
}),
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Lock info not available, skip lock management action
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
|
||||
@@ -445,9 +445,9 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
);
|
||||
|
||||
const labels = labelReg && device?.labels;
|
||||
const labelsEntries = (labels || [])
|
||||
.map((lbl) => labelReg!.find((label) => label.label_id === lbl))
|
||||
.filter((entry): entry is LabelRegistryEntry => entry !== undefined);
|
||||
const labelsEntries = (labels || []).map(
|
||||
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
|
||||
);
|
||||
|
||||
let floorName;
|
||||
if (
|
||||
|
||||
@@ -55,7 +55,6 @@ import {
|
||||
import type { ImprovDiscoveredDevice } from "../../../external_app/external_messaging";
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import type { HassTabsSubpage } from "../../../layouts/hass-tabs-subpage";
|
||||
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
@@ -171,8 +170,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
|
||||
@query("ha-input-search") private _searchInput!: HaInputSearch;
|
||||
|
||||
@query("hass-tabs-subpage") private _tabsSubpage?: HassTabsSubpage;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener(
|
||||
@@ -427,10 +424,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
this.configEntries.map((entry) => entry.domain)
|
||||
);
|
||||
}
|
||||
|
||||
if (this.configEntries && this.configEntriesInProgress) {
|
||||
this._tabsSubpage?.focusContentScroller();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
|
||||
+7
-61
@@ -3,7 +3,6 @@ import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-dialog-footer";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
@@ -96,60 +95,18 @@ class DialogMatterLockManage extends LitElement {
|
||||
? html`<div class="center">
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>`
|
||||
: this._lockInfo && !this._lockInfo.supports_user_management
|
||||
? html`<div class="content">
|
||||
<ha-alert alert-type="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.lock.errors.no_user_management"
|
||||
)}
|
||||
</ha-alert>
|
||||
${this._renderDocsLink()}
|
||||
</div>`
|
||||
: html`<div class="content">${this._renderUsers()}</div>`}
|
||||
: html`<div class="content">${this._renderUsers()}</div>`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderDocsLink() {
|
||||
return html`<a
|
||||
class="docs-link"
|
||||
href="https://www.home-assistant.io/integrations/matter/#lock-user-and-credential-management"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.matter.lock.errors.learn_more")}
|
||||
</a>`;
|
||||
}
|
||||
|
||||
private get _supportsPinCredential(): boolean {
|
||||
return this._lockInfo?.supported_credential_types?.includes("pin") ?? false;
|
||||
}
|
||||
|
||||
private _renderUsers() {
|
||||
const occupiedUsers = this._users.filter(
|
||||
(u) => u.user_status !== "available"
|
||||
);
|
||||
|
||||
const hasNoManageableCredentials =
|
||||
!this._lockInfo?.supported_credential_types?.length;
|
||||
|
||||
return html`
|
||||
<div class="users-content">
|
||||
${hasNoManageableCredentials
|
||||
? html`<ha-alert alert-type="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.lock.errors.no_credential_types_supported"
|
||||
)}
|
||||
</ha-alert>
|
||||
${this._renderDocsLink()}`
|
||||
: !this._supportsPinCredential
|
||||
? html`<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.lock.errors.pin_not_supported"
|
||||
)}
|
||||
</ha-alert>
|
||||
${this._renderDocsLink()}`
|
||||
: nothing}
|
||||
${occupiedUsers.length === 0
|
||||
? html`<p class="empty">
|
||||
${this.hass.localize(
|
||||
@@ -190,14 +147,12 @@ class DialogMatterLockManage extends LitElement {
|
||||
)}
|
||||
</ha-md-list>
|
||||
`}
|
||||
${this._supportsPinCredential
|
||||
? html`<div class="actions">
|
||||
<ha-button @click=${this._addUser}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.matter.lock.users.add")}
|
||||
</ha-button>
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="actions">
|
||||
<ha-button @click=${this._addUser}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.matter.lock.users.add")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -295,15 +250,6 @@ class DialogMatterLockManage extends LitElement {
|
||||
.content {
|
||||
min-height: 300px;
|
||||
}
|
||||
.content > ha-alert {
|
||||
margin: var(--ha-space-4);
|
||||
}
|
||||
.docs-link {
|
||||
display: block;
|
||||
padding: 0 var(--ha-space-4);
|
||||
color: var(--primary-color);
|
||||
font-size: 14px;
|
||||
}
|
||||
.users-content {
|
||||
padding: var(--ha-space-4) 0;
|
||||
}
|
||||
|
||||
+2
-12
@@ -65,9 +65,6 @@ class DialogMatterLockUserEdit extends LitElement {
|
||||
}
|
||||
|
||||
const isNew = !this._params.user;
|
||||
const supportsPinCredential =
|
||||
this._params.lockInfo?.supported_credential_types?.includes("pin") ??
|
||||
false;
|
||||
const title = isNew
|
||||
? this.hass.localize("ui.panel.config.matter.lock.users.add")
|
||||
: this.hass.localize("ui.panel.config.matter.lock.users.edit");
|
||||
@@ -95,7 +92,7 @@ class DialogMatterLockUserEdit extends LitElement {
|
||||
maxlength="10"
|
||||
></ha-input>
|
||||
|
||||
${isNew && supportsPinCredential
|
||||
${isNew
|
||||
? html`
|
||||
<ha-input
|
||||
.label=${this.hass.localize(
|
||||
@@ -116,13 +113,6 @@ class DialogMatterLockUserEdit extends LitElement {
|
||||
></ha-input>
|
||||
`
|
||||
: nothing}
|
||||
${isNew && !supportsPinCredential
|
||||
? html`<ha-alert alert-type="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.lock.errors.no_credential_types_supported"
|
||||
)}
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
|
||||
<div class="user-type-section">
|
||||
<label
|
||||
@@ -150,7 +140,7 @@ class DialogMatterLockUserEdit extends LitElement {
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
.disabled=${this._saving || (isNew && !supportsPinCredential)}
|
||||
.disabled=${this._saving}
|
||||
>
|
||||
${this._saving
|
||||
? html`<ha-spinner size="small"></ha-spinner>`
|
||||
|
||||
@@ -161,7 +161,7 @@ class ZHAAddDevicesPage extends LitElement {
|
||||
? html`<ha-textarea
|
||||
readonly
|
||||
class="log"
|
||||
resize="auto"
|
||||
autogrow
|
||||
.value=${this._formattedEvents}
|
||||
>
|
||||
</ha-textarea>`
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from "lit";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { IntersectionController } from "@lit-labs/observers/intersection-controller.js";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import "../../../components/chips/ha-assist-chip";
|
||||
|
||||
@@ -72,7 +72,6 @@ export const PANEL_DASHBOARDS = [
|
||||
"security",
|
||||
"climate",
|
||||
"energy",
|
||||
"maintenance",
|
||||
] as string[];
|
||||
|
||||
type DataTableItem = Pick<
|
||||
|
||||
@@ -132,7 +132,7 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
|
||||
<h3 slot="header">${this.field.name ?? this.key}</h3>
|
||||
<h3 slot="header">${this.key}</h3>
|
||||
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
</ha-automation-row>
|
||||
|
||||
@@ -322,7 +322,7 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
(entry?.device_id ? devices[entry.device_id!]?.area_id : undefined);
|
||||
const area = areaId ? areas[areaId] : undefined;
|
||||
const _assistants = Object.keys(
|
||||
exposedEntities?.[entityState.entity_id] ?? {}
|
||||
exposedEntities?.[entityState.entity_id]
|
||||
).filter(
|
||||
(key) =>
|
||||
showAssistants.includes(key) &&
|
||||
@@ -384,7 +384,7 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
assistants: [
|
||||
...(exposedEntities
|
||||
? Object.keys(
|
||||
exposedEntities?.[entityState.entity_id] ?? {}
|
||||
exposedEntities?.[entityState.entity_id]
|
||||
).filter(
|
||||
(key) =>
|
||||
showAssistants.includes(key) &&
|
||||
|
||||
@@ -74,7 +74,11 @@ export class EnergyDashboardStrategy extends ReactiveElement {
|
||||
_config: EnergyDashboardStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceConfig> {
|
||||
const prefs = await fetchEnergyPrefs(hass);
|
||||
const [prefs, localize] = await Promise.all([
|
||||
fetchEnergyPrefs(hass),
|
||||
hass.loadFragmentTranslation("energy"),
|
||||
]);
|
||||
const localizeFunc = localize ?? hass.localize;
|
||||
|
||||
if (
|
||||
!prefs ||
|
||||
@@ -138,7 +142,7 @@ export class EnergyDashboardStrategy extends ReactiveElement {
|
||||
...view,
|
||||
title:
|
||||
view.title ||
|
||||
hass.localize(`ui.panel.energy.title.${view.path}` as LocalizeKeys),
|
||||
localizeFunc(`ui.panel.energy.title.${view.path}` as LocalizeKeys),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,36 +99,41 @@ class PanelLight extends LitElement {
|
||||
return html`
|
||||
<div class="header ${classMap({ narrow: this.narrow })}">
|
||||
<div class="toolbar">
|
||||
${this._searchParms.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`}
|
||||
${
|
||||
this._searchParms.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`
|
||||
}
|
||||
<div class="main-title">${this.hass.localize("panel.light")}</div>
|
||||
</div>
|
||||
</div>
|
||||
${this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}> </hui-view-background>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}> </hui-view-background>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</hui-view-container>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,16 +13,8 @@ import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-spinner";
|
||||
import type {
|
||||
Calendar,
|
||||
CalendarEvent,
|
||||
CalendarEventSubscription,
|
||||
CalendarEventApiData,
|
||||
} from "../../../data/calendar";
|
||||
import {
|
||||
normalizeSubscriptionEventData,
|
||||
subscribeCalendarEvents,
|
||||
} from "../../../data/calendar";
|
||||
import type { Calendar, CalendarEvent } from "../../../data/calendar";
|
||||
import { fetchCalendarEvents } from "../../../data/calendar";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import { subscribeEntityRegistry } from "../../../data/entity/entity_registry";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
@@ -85,8 +77,6 @@ export class HuiCalendarCard
|
||||
|
||||
@state() private _error?: string = undefined;
|
||||
|
||||
@state() private _errorCalendars: string[] = [];
|
||||
|
||||
@state() private _entityRegistry?: EntityRegistryEntry[];
|
||||
|
||||
@state() private _eventsLoaded = false;
|
||||
@@ -97,8 +87,6 @@ export class HuiCalendarCard
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
private _unsubs: Record<string, Promise<UnsubscribeFunc>> = {};
|
||||
|
||||
public setConfig(config: CalendarCardConfig): void {
|
||||
if (!config.entities?.length) {
|
||||
throw new Error("Entities must be specified");
|
||||
@@ -109,8 +97,7 @@ export class HuiCalendarCard
|
||||
}
|
||||
|
||||
if (this._config?.entities !== config.entities) {
|
||||
this._unsubscribeAll();
|
||||
// Subscription will happen when view-changed event fires
|
||||
this._fetchCalendarEvents();
|
||||
}
|
||||
|
||||
this._config = { initial_view: "dayGridMonth", ...config };
|
||||
@@ -192,7 +179,6 @@ export class HuiCalendarCard
|
||||
if (this._resizeObserver) {
|
||||
this._resizeObserver.disconnect();
|
||||
}
|
||||
this._unsubscribeAll();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -245,11 +231,9 @@ export class HuiCalendarCard
|
||||
return;
|
||||
}
|
||||
|
||||
// Resubscribe when entity registry changes (to update colors)
|
||||
// Refetch events when entity registry changes (to update colors)
|
||||
if (changedProps.has("_entityRegistry") && this._entityRegistry) {
|
||||
this._unsubscribeAll().then(() => {
|
||||
this._subscribeCalendarEvents();
|
||||
});
|
||||
this._fetchCalendarEvents();
|
||||
}
|
||||
|
||||
// If no calendars configured, mark events as loaded to hide spinner
|
||||
@@ -276,94 +260,38 @@ export class HuiCalendarCard
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleViewChanged(
|
||||
ev: HASSDomEvent<CalendarViewChanged>
|
||||
): Promise<void> {
|
||||
private _handleViewChanged(ev: HASSDomEvent<CalendarViewChanged>): void {
|
||||
this._startDate = ev.detail.start;
|
||||
this._endDate = ev.detail.end;
|
||||
this._eventsLoaded = false;
|
||||
await this._unsubscribeAll();
|
||||
this._subscribeCalendarEvents();
|
||||
this._fetchCalendarEvents();
|
||||
}
|
||||
|
||||
private _subscribeCalendarEvents(): void {
|
||||
if (!this.hass || !this._startDate || !this._endDate) {
|
||||
private async _fetchCalendarEvents(): Promise<void> {
|
||||
if (!this._startDate || !this._endDate) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._error = undefined;
|
||||
|
||||
this._calendars.forEach((calendar) => {
|
||||
const unsub = subscribeCalendarEvents(
|
||||
this.hass!,
|
||||
calendar.entity_id,
|
||||
this._startDate!,
|
||||
this._endDate!,
|
||||
(update: CalendarEventSubscription) => {
|
||||
this._handleCalendarUpdate(calendar, update);
|
||||
}
|
||||
);
|
||||
this._unsubs[calendar.entity_id] = unsub;
|
||||
});
|
||||
}
|
||||
|
||||
private _handleCalendarUpdate(
|
||||
calendar: Calendar,
|
||||
update: CalendarEventSubscription
|
||||
): void {
|
||||
// Remove events from this calendar
|
||||
this._events = this._events.filter(
|
||||
(event) => event.calendar !== calendar.entity_id
|
||||
const result = await fetchCalendarEvents(
|
||||
this.hass!,
|
||||
this._startDate,
|
||||
this._endDate,
|
||||
this._calendars
|
||||
);
|
||||
this._events = result.events;
|
||||
// Wait for component update and one animation frame for FullCalendar to render
|
||||
this.updateComplete.then(() => {
|
||||
requestAnimationFrame(() => {
|
||||
this._eventsLoaded = true;
|
||||
});
|
||||
});
|
||||
|
||||
if (update.events === null) {
|
||||
// Error fetching events
|
||||
if (!this._errorCalendars.includes(calendar.entity_id)) {
|
||||
this._errorCalendars = [...this._errorCalendars, calendar.entity_id];
|
||||
}
|
||||
if (result.errors.length > 0) {
|
||||
this._error = `${this.hass!.localize(
|
||||
"ui.components.calendar.event_retrieval_error"
|
||||
)}`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from error list if successfully loaded
|
||||
this._errorCalendars = this._errorCalendars.filter(
|
||||
(id) => id !== calendar.entity_id
|
||||
);
|
||||
if (this._errorCalendars.length === 0) {
|
||||
this._error = undefined;
|
||||
}
|
||||
|
||||
// Add new events from this calendar
|
||||
const newEvents: CalendarEvent[] = update.events
|
||||
.map((eventData: CalendarEventApiData) =>
|
||||
normalizeSubscriptionEventData(eventData, calendar)
|
||||
)
|
||||
.filter((event): event is CalendarEvent => event !== null);
|
||||
|
||||
this._events = [...this._events, ...newEvents];
|
||||
|
||||
if (!this._eventsLoaded) {
|
||||
this.updateComplete.then(() => {
|
||||
requestAnimationFrame(() => {
|
||||
this._eventsLoaded = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _unsubscribeAll(): Promise<void> {
|
||||
await Promise.all(
|
||||
Object.values(this._unsubs).map((unsub) =>
|
||||
unsub
|
||||
.then((unsubFunc) => unsubFunc())
|
||||
.catch(() => {
|
||||
// Subscription may have already been closed
|
||||
})
|
||||
)
|
||||
);
|
||||
this._unsubs = {};
|
||||
}
|
||||
|
||||
private _measureCard() {
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
HOME_SUMMARIES_ICONS,
|
||||
type HomeSummary,
|
||||
} from "../strategies/home/helpers/home-summaries";
|
||||
import { filterNeedsAttentionEntities } from "../../maintenance/strategies/maintenance-view-strategy";
|
||||
import type { LovelaceCard, LovelaceGridOptions } from "../types";
|
||||
import { tileCardStyle } from "./tile/tile-card-style";
|
||||
import type { HomeSummaryCard } from "./types";
|
||||
@@ -44,7 +43,6 @@ const COLORS: Record<HomeSummary, string> = {
|
||||
climate: "deep-orange",
|
||||
security: "blue-grey",
|
||||
media_players: "blue",
|
||||
maintenance: "grey",
|
||||
energy: "amber",
|
||||
persons: "green",
|
||||
};
|
||||
@@ -257,31 +255,6 @@ export class HuiHomeSummaryCard
|
||||
})
|
||||
: this.hass.localize("ui.card.home-summary.no_media_playing");
|
||||
}
|
||||
case "maintenance": {
|
||||
const maintenanceFilters = HOME_SUMMARIES_FILTERS.maintenance.map(
|
||||
(filter) => generateEntityFilter(this.hass!, filter)
|
||||
);
|
||||
|
||||
const maintenanceEntities = findEntities(
|
||||
allEntities,
|
||||
maintenanceFilters
|
||||
);
|
||||
|
||||
const needsAttentionEntities = filterNeedsAttentionEntities(
|
||||
this.hass!,
|
||||
maintenanceEntities
|
||||
);
|
||||
|
||||
if (needsAttentionEntities.length > 0) {
|
||||
return this.hass.localize(
|
||||
"ui.card.home-summary.count_maintenance_issues",
|
||||
{
|
||||
count: needsAttentionEntities.length,
|
||||
}
|
||||
);
|
||||
}
|
||||
return this.hass.localize("ui.card.home-summary.all_maintenance_good");
|
||||
}
|
||||
case "energy": {
|
||||
if (!this._energyData) {
|
||||
return "";
|
||||
|
||||
@@ -415,7 +415,6 @@ export interface MapCardConfig extends LovelaceCardConfig {
|
||||
dark_mode?: boolean;
|
||||
theme_mode?: ThemeMode;
|
||||
cluster?: boolean;
|
||||
conditions?: Condition[];
|
||||
}
|
||||
|
||||
export interface MarkdownCardConfig extends LovelaceCardConfig {
|
||||
|
||||
@@ -460,8 +460,8 @@ export function addEntityToCondition(
|
||||
condition.condition === "numeric_state"
|
||||
) {
|
||||
return {
|
||||
...condition,
|
||||
entity: entityId,
|
||||
...condition,
|
||||
};
|
||||
}
|
||||
return condition;
|
||||
|
||||
@@ -57,7 +57,6 @@ import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-ripple";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { EnergyData } from "../../../data/energy";
|
||||
import {
|
||||
@@ -77,7 +76,6 @@ const RANGE_KEYS: DateRange[] = [
|
||||
"this_year",
|
||||
"now-7d",
|
||||
"now-30d",
|
||||
"now-365d",
|
||||
"now-12m",
|
||||
];
|
||||
|
||||
@@ -125,10 +123,6 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _collapseButtons = false;
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
private _loadingTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
@@ -351,13 +345,6 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
: ``}
|
||||
</div>`
|
||||
: nothing}
|
||||
<ha-spinner
|
||||
class=${classMap({
|
||||
"loading-indicator": true,
|
||||
"is-loading": this._loading,
|
||||
})}
|
||||
size="small"
|
||||
></ha-spinner>
|
||||
</section>
|
||||
<section class="date-actions">
|
||||
<div class="overflow">
|
||||
@@ -508,7 +495,6 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
energyCollection.setPeriod(this._startDate!, this._endDate!);
|
||||
energyCollection.refresh();
|
||||
this._scheduleLoadingIndicator();
|
||||
}
|
||||
|
||||
private _dateRangeChanged(ev) {
|
||||
@@ -685,8 +671,6 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _updateDates(energyData: EnergyData): void {
|
||||
clearTimeout(this._loadingTimer);
|
||||
this._loading = false;
|
||||
this._compare = energyData.startCompare !== undefined;
|
||||
this._startDate = energyData.start;
|
||||
this._endDate = energyData.end || endOfToday();
|
||||
@@ -705,16 +689,6 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
this._compare ? CompareMode.PREVIOUS : CompareMode.NONE
|
||||
);
|
||||
energyCollection.refresh();
|
||||
this._scheduleLoadingIndicator();
|
||||
}
|
||||
|
||||
private _scheduleLoadingIndicator() {
|
||||
// Add a delay before showing the loading indicator
|
||||
// Basically to ensure there's no "flash loading" when data is loaded quickly
|
||||
clearTimeout(this._loadingTimer);
|
||||
this._loadingTimer = setTimeout(() => {
|
||||
this._loading = true;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
private _getDatePickerPlacement = memoizeOne(
|
||||
@@ -764,7 +738,6 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.header-title {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
@@ -789,18 +762,6 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.loading-indicator {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
right: var(--ha-space-2);
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transition: opacity var(--ha-animation-duration-normal) ease-in-out;
|
||||
}
|
||||
.loading-indicator.is-loading {
|
||||
opacity: 1;
|
||||
}
|
||||
.date-actions .overflow {
|
||||
display: flex;
|
||||
|
||||
@@ -21,7 +21,6 @@ import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
@@ -34,45 +33,13 @@ import { ICON_CONDITION } from "../../common/icon-condition";
|
||||
import type {
|
||||
Condition,
|
||||
LegacyCondition,
|
||||
OrCondition,
|
||||
AndCondition,
|
||||
NotCondition,
|
||||
} from "../../common/validate-condition";
|
||||
import {
|
||||
checkConditionsMet,
|
||||
validateConditionalConfig,
|
||||
} from "../../common/validate-condition";
|
||||
import type { LovelaceConditionEditorConstructor } from "./types";
|
||||
import type { PresetState } from "./types/ha-card-condition-state";
|
||||
|
||||
const NO_ENTITY_CONDITIONS = ["state", "numeric_state"];
|
||||
|
||||
const CONTAINER_CONDITIONS = ["and", "or", "not"];
|
||||
|
||||
const NO_ENTITY_CONDITIONS_EXT = [
|
||||
...CONTAINER_CONDITIONS,
|
||||
...NO_ENTITY_CONDITIONS,
|
||||
];
|
||||
|
||||
const isNoEntityCondition = (condition: string, noEntity: boolean): boolean =>
|
||||
NO_ENTITY_CONDITIONS.includes(condition) && noEntity;
|
||||
|
||||
export const getConditionClassName = (condition: string, noEntity: boolean) => {
|
||||
if (isNoEntityCondition(condition, noEntity)) {
|
||||
return `ha-card-condition-${condition}-no_entity`;
|
||||
}
|
||||
return `ha-card-condition-${condition}`;
|
||||
};
|
||||
|
||||
const containsNoEntityCondition = (
|
||||
condition: Condition,
|
||||
noEntity: boolean
|
||||
): boolean =>
|
||||
noEntity &&
|
||||
CONTAINER_CONDITIONS.includes(condition.condition) &&
|
||||
(condition as OrCondition | AndCondition | NotCondition).conditions?.some(
|
||||
(c) => NO_ENTITY_CONDITIONS.includes(c.condition)
|
||||
) === true;
|
||||
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
|
||||
@customElement("ha-card-condition-editor")
|
||||
export class HaCardConditionEditor extends LitElement {
|
||||
@@ -80,10 +47,6 @@ export class HaCardConditionEditor extends LitElement {
|
||||
|
||||
@property({ attribute: false }) condition!: Condition | LegacyCondition;
|
||||
|
||||
@property({ attribute: "no-entity", type: Boolean }) public noEntity = false;
|
||||
|
||||
@property({ attribute: false }) public presetStates: PresetState[] = [];
|
||||
|
||||
@storage({
|
||||
key: "dashboardConditionClipboard",
|
||||
state: false,
|
||||
@@ -105,7 +68,7 @@ export class HaCardConditionEditor extends LitElement {
|
||||
private get _editor() {
|
||||
if (!this._condition) return undefined;
|
||||
return customElements.get(
|
||||
getConditionClassName(this._condition.condition, this.noEntity)
|
||||
`ha-card-condition-${this._condition.condition}`
|
||||
) as LovelaceConditionEditorConstructor | undefined;
|
||||
}
|
||||
|
||||
@@ -176,15 +139,12 @@ export class HaCardConditionEditor extends LitElement {
|
||||
>
|
||||
</ha-icon-button>
|
||||
|
||||
${isNoEntityCondition(condition.condition, this.noEntity) ||
|
||||
containsNoEntityCondition(condition, this.noEntity)
|
||||
? nothing
|
||||
: html`<ha-dropdown-item value="test">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.test"
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiFlask}></ha-svg-icon>
|
||||
</ha-dropdown-item>`}
|
||||
<ha-dropdown-item value="test">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.condition-editor.test"
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiFlask}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-dropdown-item value="duplicate">
|
||||
${this.hass.localize(
|
||||
@@ -257,22 +217,10 @@ export class HaCardConditionEditor extends LitElement {
|
||||
></ha-yaml-editor>
|
||||
`
|
||||
: html`
|
||||
${dynamicElement(
|
||||
getConditionClassName(condition.condition, this.noEntity),
|
||||
{
|
||||
hass: this.hass,
|
||||
condition: condition,
|
||||
...(this.noEntity &&
|
||||
NO_ENTITY_CONDITIONS_EXT.includes(condition.condition)
|
||||
? {
|
||||
noEntity: this.noEntity,
|
||||
...(condition.condition === "numeric_state"
|
||||
? {}
|
||||
: { presetStates: this.presetStates }),
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
)}
|
||||
${dynamicElement(`ha-card-condition-${condition.condition}`, {
|
||||
hass: this.hass,
|
||||
condition: condition,
|
||||
})}
|
||||
`}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
|
||||
@@ -7,7 +7,6 @@ import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -17,23 +16,18 @@ import type {
|
||||
LegacyCondition,
|
||||
} from "../../common/validate-condition";
|
||||
import "./ha-card-condition-editor";
|
||||
import {
|
||||
type HaCardConditionEditor,
|
||||
getConditionClassName,
|
||||
} from "./ha-card-condition-editor";
|
||||
import type { HaCardConditionEditor } from "./ha-card-condition-editor";
|
||||
import type { LovelaceConditionEditorConstructor } from "./types";
|
||||
import "./types/ha-card-condition-and";
|
||||
import "./types/ha-card-condition-location";
|
||||
import "./types/ha-card-condition-not";
|
||||
import "./types/ha-card-condition-numeric_state";
|
||||
import "./types/ha-card-condition-numeric_state-no_entity";
|
||||
import "./types/ha-card-condition-or";
|
||||
import "./types/ha-card-condition-screen";
|
||||
import "./types/ha-card-condition-state";
|
||||
import type { PresetState } from "./types/ha-card-condition-state";
|
||||
import "./types/ha-card-condition-state-no_entity";
|
||||
import "./types/ha-card-condition-time";
|
||||
import "./types/ha-card-condition-user";
|
||||
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
|
||||
const UI_CONDITION = [
|
||||
"location",
|
||||
@@ -64,10 +58,6 @@ export class HaCardConditionsEditor extends LitElement {
|
||||
| LegacyCondition
|
||||
)[];
|
||||
|
||||
@property({ attribute: "no-entity", type: Boolean }) public noEntity = false;
|
||||
|
||||
@property({ attribute: false }) public presetStates: PresetState[] = [];
|
||||
|
||||
private _focusLastConditionOnChange = false;
|
||||
|
||||
protected firstUpdated() {
|
||||
@@ -111,8 +101,6 @@ export class HaCardConditionsEditor extends LitElement {
|
||||
@value-changed=${this._conditionChanged}
|
||||
.hass=${this.hass}
|
||||
.condition=${cond}
|
||||
.noEntity=${this.noEntity}
|
||||
.presetStates=${this.presetStates}
|
||||
></ha-card-condition-editor>
|
||||
`
|
||||
)}
|
||||
@@ -168,12 +156,13 @@ export class HaCardConditionsEditor extends LitElement {
|
||||
const newCondition = deepClone(this._clipboard!);
|
||||
conditions.push(newCondition);
|
||||
} else {
|
||||
const elClass = customElements.get(
|
||||
getConditionClassName(condition, this.noEntity)
|
||||
) as LovelaceConditionEditorConstructor | undefined;
|
||||
const elClass = customElements.get(`ha-card-condition-${condition}`) as
|
||||
| LovelaceConditionEditorConstructor
|
||||
| undefined;
|
||||
|
||||
const defaultConfig = elClass?.defaultConfig;
|
||||
conditions.push(defaultConfig ? { ...defaultConfig } : { condition });
|
||||
conditions.push(
|
||||
elClass?.defaultConfig ? { ...elClass.defaultConfig } : { condition }
|
||||
);
|
||||
}
|
||||
|
||||
this._focusLastConditionOnChange = true;
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
StateCondition,
|
||||
} from "../../../common/validate-condition";
|
||||
import "../ha-card-conditions-editor";
|
||||
import type { PresetState } from "./ha-card-condition-state";
|
||||
|
||||
const andConditionStruct = object({
|
||||
condition: literal("and"),
|
||||
@@ -25,10 +24,6 @@ export class HaCardConditionNumericAnd extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ attribute: "no-entity", type: Boolean }) public noEntity = false;
|
||||
|
||||
@property({ attribute: false }) public presetStates: PresetState[] = [];
|
||||
|
||||
public static get defaultConfig(): AndCondition {
|
||||
return { condition: "and", conditions: [] };
|
||||
}
|
||||
@@ -43,8 +38,6 @@ export class HaCardConditionNumericAnd extends LitElement {
|
||||
nested
|
||||
.hass=${this.hass}
|
||||
.conditions=${this.condition.conditions}
|
||||
.noEntity=${this.noEntity}
|
||||
.presetStates=${this.presetStates}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-card-conditions-editor>
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
StateCondition,
|
||||
} from "../../../common/validate-condition";
|
||||
import "../ha-card-conditions-editor";
|
||||
import type { PresetState } from "./ha-card-condition-state";
|
||||
|
||||
const notConditionStruct = object({
|
||||
condition: literal("not"),
|
||||
@@ -25,10 +24,6 @@ export class HaCardConditionNot extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ attribute: "no-entity", type: Boolean }) public noEntity = false;
|
||||
|
||||
@property({ attribute: false }) public presetStates: PresetState[] = [];
|
||||
|
||||
public static get defaultConfig(): NotCondition {
|
||||
return { condition: "not", conditions: [] };
|
||||
}
|
||||
@@ -43,8 +38,6 @@ export class HaCardConditionNot extends LitElement {
|
||||
nested
|
||||
.hass=${this.hass}
|
||||
.conditions=${this.condition.conditions}
|
||||
.noEntity=${this.noEntity}
|
||||
.presetStates=${this.presetStates}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-card-conditions-editor>
|
||||
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { NumericStateCondition } from "../../../common/validate-condition";
|
||||
import { HaCardConditionNumericState } from "./ha-card-condition-numeric_state";
|
||||
|
||||
@customElement("ha-card-condition-numeric_state-no_entity")
|
||||
export class HaCardConditionNumericStateNoEntity extends HaCardConditionNumericState {
|
||||
public static get defaultConfig(): NumericStateCondition {
|
||||
return { condition: "numeric_state" };
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-card-condition-numeric_state-no_entity": HaCardConditionNumericStateNoEntity;
|
||||
}
|
||||
}
|
||||
@@ -30,8 +30,6 @@ export class HaCardConditionNumericState extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ attribute: "no-entity", type: Boolean }) public noEntity = false;
|
||||
|
||||
public static get defaultConfig(): NumericStateCondition {
|
||||
return { condition: "numeric_state", entity: "" };
|
||||
}
|
||||
@@ -41,9 +39,9 @@ export class HaCardConditionNumericState extends LitElement {
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(noEntity: boolean, stateObj?: HassEntity) =>
|
||||
(stateObj?: HassEntity) =>
|
||||
[
|
||||
...(noEntity ? [] : [{ name: "entity", selector: { entity: {} } }]),
|
||||
{ name: "entity", selector: { entity: {} } },
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
@@ -82,7 +80,7 @@ export class HaCardConditionNumericState extends LitElement {
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this.condition}
|
||||
.schema=${this._schema(this.noEntity, stateObj)}
|
||||
.schema=${this._schema(stateObj)}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._valueChanged}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
StateCondition,
|
||||
} from "../../../common/validate-condition";
|
||||
import "../ha-card-conditions-editor";
|
||||
import type { PresetState } from "./ha-card-condition-state";
|
||||
|
||||
const orConditionStruct = object({
|
||||
condition: literal("or"),
|
||||
@@ -25,10 +24,6 @@ export class HaCardConditionOr extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ attribute: "no-entity", type: Boolean }) public noEntity = false;
|
||||
|
||||
@property({ attribute: false }) public presetStates: PresetState[] = [];
|
||||
|
||||
public static get defaultConfig(): OrCondition {
|
||||
return { condition: "or", conditions: [] };
|
||||
}
|
||||
@@ -43,8 +38,6 @@ export class HaCardConditionOr extends LitElement {
|
||||
nested
|
||||
.hass=${this.hass}
|
||||
.conditions=${this.condition.conditions}
|
||||
.noEntity=${this.noEntity}
|
||||
.presetStates=${this.presetStates}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-card-conditions-editor>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { StateCondition } from "../../../common/validate-condition";
|
||||
import { HaCardConditionState } from "./ha-card-condition-state";
|
||||
|
||||
@customElement("ha-card-condition-state-no_entity")
|
||||
export class HaCardConditionStateNoEntity extends HaCardConditionState {
|
||||
public static get defaultConfig(): StateCondition {
|
||||
return {
|
||||
condition: "state",
|
||||
state: "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-card-condition-state-no_entity": HaCardConditionStateNoEntity;
|
||||
}
|
||||
}
|
||||
@@ -27,11 +27,6 @@ interface StateConditionData {
|
||||
state?: string | string[];
|
||||
}
|
||||
|
||||
export interface PresetState {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
@customElement("ha-card-condition-state")
|
||||
export class HaCardConditionState extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -40,10 +35,6 @@ export class HaCardConditionState extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ attribute: "no-entity", type: Boolean }) public noEntity = false;
|
||||
|
||||
@property({ attribute: false }) public presetStates: PresetState[] = [];
|
||||
|
||||
public static get defaultConfig(): StateCondition {
|
||||
return { condition: "state", entity: "", state: "" };
|
||||
}
|
||||
@@ -64,9 +55,9 @@ export class HaCardConditionState extends LitElement {
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(noEntity: boolean, localize: LocalizeFunc, presetStates: PresetState[]) =>
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
...(noEntity ? [] : [{ name: "entity", selector: { entity: {} } }]),
|
||||
{ name: "entity", selector: { entity: {} } },
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
@@ -95,25 +86,13 @@ export class HaCardConditionState extends LitElement {
|
||||
},
|
||||
},
|
||||
{
|
||||
...(noEntity
|
||||
? {
|
||||
name: "state",
|
||||
selector: {
|
||||
state: {
|
||||
extra_options: presetStates,
|
||||
no_entity: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: "state",
|
||||
selector: {
|
||||
state: {},
|
||||
},
|
||||
context: {
|
||||
filter_entity: "entity",
|
||||
},
|
||||
}),
|
||||
name: "state",
|
||||
selector: {
|
||||
state: {},
|
||||
},
|
||||
context: {
|
||||
filter_entity: "entity",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -134,11 +113,7 @@ export class HaCardConditionState extends LitElement {
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${this._schema(
|
||||
this.noEntity,
|
||||
this.hass.localize,
|
||||
this.presetStates
|
||||
)}
|
||||
.schema=${this._schema(this.hass.localize)}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._valueChanged}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
|
||||
@@ -15,48 +15,36 @@ import {
|
||||
string,
|
||||
union,
|
||||
} from "superstruct";
|
||||
import { arrayLiteralIncludes } from "../../../../common/array/literal-includes";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import {
|
||||
FIXED_DOMAIN_STATES,
|
||||
getStatesDomain,
|
||||
} from "../../../../common/entity/get_states";
|
||||
import { hasLocation } from "../../../../common/entity/has_location";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../../../common/translations/localize";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import { orderProperties } from "../../../../common/util/order-properties";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import { MAP_CARD_MARKER_LABEL_MODES } from "../../../../components/map/ha-map";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-selector/ha-selector-select";
|
||||
import "../../../../components/ha-switch";
|
||||
import { MAP_CARD_MARKER_LABEL_MODES } from "../../../../components/map/ha-map";
|
||||
import { UNAVAILABLE_STATES } from "../../../../data/entity/entity";
|
||||
import type { SelectSelector } from "../../../../data/selector";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
|
||||
import { THEME_MODES } from "../../../../types";
|
||||
import { DEFAULT_HOURS_TO_SHOW, DEFAULT_ZOOM } from "../../cards/hui-map-card";
|
||||
import type { MapCardConfig, MapEntityConfig } from "../../cards/types";
|
||||
import type { Condition } from "../../common/validate-condition";
|
||||
import "../../components/hui-entity-editor";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import "../conditions/ha-card-condition-editor";
|
||||
import type { PresetState } from "../conditions/types/ha-card-condition-state";
|
||||
import "../hui-sub-element-editor";
|
||||
import { processEditorEntities } from "../process-editor-entities";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import type {
|
||||
EditDetailElementEvent,
|
||||
EntitiesEditorEvent,
|
||||
SubElementEditorConfig,
|
||||
EntitiesEditorEvent,
|
||||
} from "../types";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { processEditorEntities } from "../process-editor-entities";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
|
||||
export const mapEntitiesConfigStruct = union([
|
||||
@@ -117,8 +105,6 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
|
||||
|
||||
private _locationEntities: string[] = [];
|
||||
|
||||
@state() private _presetStates: PresetState[] = [];
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
@@ -240,31 +226,6 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
|
||||
})
|
||||
);
|
||||
|
||||
private _preparePresetStates() {
|
||||
if (!this.hass || this._presetStates.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const states = getStatesDomain(this.hass!, "device_tracker");
|
||||
this._presetStates = states.map((stateRaw) => {
|
||||
let stateTranslated;
|
||||
if (arrayLiteralIncludes(UNAVAILABLE_STATES)(stateRaw)) {
|
||||
stateTranslated = this.hass!.localize(
|
||||
`state.default.${stateRaw}` as LocalizeKeys
|
||||
);
|
||||
} else if (
|
||||
arrayLiteralIncludes(FIXED_DOMAIN_STATES.device_tracker)(stateRaw)
|
||||
) {
|
||||
stateTranslated = this.hass!.localize(
|
||||
`component.device_tracker.entity_component._.state.${stateRaw}`
|
||||
);
|
||||
} else {
|
||||
stateTranslated = stateRaw;
|
||||
}
|
||||
return { value: stateRaw, label: stateTranslated };
|
||||
});
|
||||
}
|
||||
|
||||
public setConfig(config: MapCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
|
||||
@@ -298,7 +259,6 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
|
||||
: Object.keys(this.hass!.states).filter((entity_id) =>
|
||||
hasLocation(this.hass!.states[entity_id])
|
||||
);
|
||||
this._preparePresetStates();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -366,45 +326,9 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
|
||||
this.hass.localize
|
||||
)}
|
||||
></ha-selector-select>
|
||||
${this.renderConditions()}
|
||||
`;
|
||||
}
|
||||
|
||||
renderConditions() {
|
||||
const conditions = this._config?.conditions ?? [];
|
||||
return html`
|
||||
<h3>
|
||||
${this.hass!.localize("ui.panel.lovelace.editor.card.map.conditions")}
|
||||
</h3>
|
||||
<p class="intro">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.map.conditions_helper"
|
||||
)}
|
||||
</p>
|
||||
<ha-card-conditions-editor
|
||||
no-entity
|
||||
.hass=${this.hass!}
|
||||
.conditions=${conditions}
|
||||
.presetStates=${this._presetStates}
|
||||
@value-changed=${this._conditionsChanged}
|
||||
>
|
||||
</ha-card-conditions-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
private _conditionsChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const conditions = ev.detail.value as Condition[];
|
||||
const newConfig: MapCardConfig = {
|
||||
...this._config!,
|
||||
conditions: conditions,
|
||||
};
|
||||
if (newConfig.conditions?.length === 0) {
|
||||
delete newConfig.conditions;
|
||||
}
|
||||
fireEvent(this, "config-changed", { config: newConfig });
|
||||
}
|
||||
|
||||
private _goBack(): void {
|
||||
this._subElementEditorConfig = undefined;
|
||||
}
|
||||
@@ -622,16 +546,7 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
configElementStyle,
|
||||
css`
|
||||
.intro {
|
||||
margin: 0;
|
||||
margin-bottom: var(--ha-space-1);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
return [configElementStyle, css``];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,6 @@ export class HuiPictureHeaderFooter
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -59,8 +59,6 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
|
||||
light: () => import("../../light/strategies/light-view-strategy"),
|
||||
security: () => import("../../security/strategies/security-view-strategy"),
|
||||
climate: () => import("../../climate/strategies/climate-view-strategy"),
|
||||
maintenance: () =>
|
||||
import("../../maintenance/strategies/maintenance-view-strategy"),
|
||||
},
|
||||
section: {
|
||||
"common-controls": () =>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { EntityFilter } from "../../../../../common/entity/entity_filter";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import { maintenanceEntityFilters } from "../../../../maintenance/strategies/maintenance-view-strategy";
|
||||
import { climateEntityFilters } from "../../../../climate/strategies/climate-view-strategy";
|
||||
import { lightEntityFilters } from "../../../../light/strategies/light-view-strategy";
|
||||
import { securityEntityFilters } from "../../../../security/strategies/security-view-strategy";
|
||||
@@ -10,7 +9,6 @@ export const HOME_SUMMARIES = [
|
||||
"climate",
|
||||
"security",
|
||||
"media_players",
|
||||
"maintenance",
|
||||
"energy",
|
||||
"persons",
|
||||
] as const;
|
||||
@@ -22,7 +20,6 @@ export const HOME_SUMMARIES_ICONS: Record<HomeSummary, string> = {
|
||||
climate: "mdi:home-thermometer",
|
||||
security: "mdi:security",
|
||||
media_players: "mdi:multimedia",
|
||||
maintenance: "mdi:wrench",
|
||||
energy: "mdi:lightning-bolt",
|
||||
persons: "mdi:account-multiple",
|
||||
};
|
||||
@@ -32,7 +29,6 @@ export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
|
||||
climate: climateEntityFilters,
|
||||
security: securityEntityFilters,
|
||||
media_players: [{ domain: "media_player", entity_category: "none" }],
|
||||
maintenance: maintenanceEntityFilters,
|
||||
energy: [], // Uses energy collection data
|
||||
persons: [{ domain: "person" }],
|
||||
};
|
||||
@@ -41,12 +37,7 @@ export const getSummaryLabel = (
|
||||
localize: LocalizeFunc,
|
||||
summary: HomeSummary
|
||||
) => {
|
||||
if (
|
||||
summary === "light" ||
|
||||
summary === "climate" ||
|
||||
summary === "security" ||
|
||||
summary === "maintenance"
|
||||
) {
|
||||
if (summary === "light" || summary === "climate" || summary === "security") {
|
||||
return localize(`panel.${summary}`);
|
||||
}
|
||||
return localize(`ui.panel.lovelace.strategy.home.summary_list.${summary}`);
|
||||
|
||||
@@ -223,10 +223,6 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
generateEntityFilter(hass, filter)
|
||||
);
|
||||
|
||||
const maintenanceFilters = HOME_SUMMARIES_FILTERS.maintenance.map(
|
||||
(filter) => generateEntityFilter(hass, filter)
|
||||
);
|
||||
|
||||
const hasLights =
|
||||
hass.panels.light && findEntities(allEntities, lightsFilters).length > 0;
|
||||
const hasMediaPlayers =
|
||||
@@ -237,9 +233,6 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
const hasSecurity =
|
||||
hass.panels.security &&
|
||||
findEntities(allEntities, securityFilters).length > 0;
|
||||
const hasMaintenance =
|
||||
hass.panels.maintenance &&
|
||||
findEntities(allEntities, maintenanceFilters).length > 0;
|
||||
|
||||
const weatherFilter = generateEntityFilter(hass, {
|
||||
domain: "weather",
|
||||
@@ -329,17 +322,6 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
navigation_path: "media-players",
|
||||
},
|
||||
} satisfies HomeSummaryCard),
|
||||
hasMaintenance &&
|
||||
({
|
||||
type: "home-summary",
|
||||
summary: "maintenance",
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: config.home_panel
|
||||
? "/maintenance?historyBack=1&backPath=/home"
|
||||
: "/maintenance?historyBack=1",
|
||||
},
|
||||
} satisfies HomeSummaryCard),
|
||||
weatherEntity &&
|
||||
!hiddenSummaries.has("weather") &&
|
||||
({
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { goBack } from "../../common/navigate";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-menu-button";
|
||||
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import "../lovelace/views/hui-view";
|
||||
import "../lovelace/views/hui-view-background";
|
||||
import "../lovelace/views/hui-view-container";
|
||||
|
||||
const MAINTENANCE_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
|
||||
strategy: {
|
||||
type: "maintenance",
|
||||
},
|
||||
};
|
||||
|
||||
@customElement("ha-panel-maintenance")
|
||||
class PanelMaintenance extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@state() private _viewIndex = 0;
|
||||
|
||||
@state() private _lovelace?: Lovelace;
|
||||
|
||||
@state() private _searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
// Initial setup
|
||||
if (!this.hasUpdated) {
|
||||
this._setup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!changedProps.has("hass")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
if (oldHass && oldHass.localize !== this.hass.localize) {
|
||||
this._setLovelace();
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldHass && this.hass) {
|
||||
// If the entity registry changed, ask the user if they want to refresh the config
|
||||
if (
|
||||
oldHass.entities !== this.hass.entities ||
|
||||
oldHass.devices !== this.hass.devices ||
|
||||
oldHass.areas !== this.hass.areas ||
|
||||
oldHass.floors !== this.hass.floors ||
|
||||
oldHass.panels !== this.hass.panels
|
||||
) {
|
||||
if (this.hass.config.state === "RUNNING") {
|
||||
this._debounceRegistriesChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If ha started, refresh the config
|
||||
if (
|
||||
this.hass.config.state === "RUNNING" &&
|
||||
oldHass.config.state !== "RUNNING"
|
||||
) {
|
||||
this._setLovelace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _setup() {
|
||||
await this.hass.loadFragmentTranslation("lovelace");
|
||||
this._setLovelace();
|
||||
}
|
||||
|
||||
private _debounceRegistriesChanged = debounce(
|
||||
() => this._registriesChanged(),
|
||||
200
|
||||
);
|
||||
|
||||
private _registriesChanged = async () => {
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
private _back(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
goBack();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="header ${classMap({ narrow: this.narrow })}">
|
||||
<div class="toolbar">
|
||||
${this._searchParams.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`}
|
||||
<div class="main-title">
|
||||
${this.hass.localize("panel.maintenance")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}> </hui-view-background>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view>
|
||||
</hui-view-container>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private async _setLovelace() {
|
||||
const viewConfig = await generateLovelaceViewStrategy(
|
||||
MAINTENANCE_LOVELACE_VIEW_CONFIG,
|
||||
this.hass
|
||||
);
|
||||
|
||||
const config = { views: [viewConfig] };
|
||||
const rawConfig = { views: [MAINTENANCE_LOVELACE_VIEW_CONFIG] };
|
||||
|
||||
if (deepEqual(config, this._lovelace?.config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._lovelace = {
|
||||
config: config,
|
||||
rawConfig: rawConfig,
|
||||
editMode: false,
|
||||
urlPath: "maintenance",
|
||||
mode: "generated",
|
||||
locale: this.hass.locale,
|
||||
enableFullEditMode: () => undefined,
|
||||
saveConfig: async () => undefined,
|
||||
deleteConfig: async () => undefined,
|
||||
setEditMode: () => undefined,
|
||||
showToast: () => undefined,
|
||||
};
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
.header {
|
||||
background-color: var(--app-header-background-color);
|
||||
color: var(--app-header-text-color, white);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: calc(
|
||||
var(--mdc-top-app-bar-width, 100%) - var(
|
||||
--safe-area-inset-right,
|
||||
0px
|
||||
)
|
||||
);
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
|
||||
backdrop-filter: var(--app-header-backdrop-filter, none);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
:host([narrow]) .header {
|
||||
width: calc(
|
||||
var(--mdc-top-app-bar-width, 100%) - var(
|
||||
--safe-area-inset-left,
|
||||
0px
|
||||
) - var(--safe-area-inset-right, 0px)
|
||||
);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
:host([scrolled]) .header {
|
||||
box-shadow: var(
|
||||
--mdc-top-app-bar-fixed-box-shadow,
|
||||
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
|
||||
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
|
||||
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
|
||||
);
|
||||
}
|
||||
.toolbar {
|
||||
height: var(--header-height);
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
padding: 0px 12px;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
box-sizing: border-box;
|
||||
border-bottom: var(--app-header-border-bottom, none);
|
||||
}
|
||||
:host([narrow]) .toolbar {
|
||||
padding: 0 4px;
|
||||
}
|
||||
.main-title {
|
||||
margin-inline-start: var(--ha-space-6);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
flex-grow: 1;
|
||||
}
|
||||
.narrow .main-title {
|
||||
margin-inline-start: var(--ha-space-2);
|
||||
}
|
||||
hui-view-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
padding-inline-end: var(--safe-area-inset-right);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
}
|
||||
:host([narrow]) hui-view-container {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-inline-start: var(--safe-area-inset-left);
|
||||
}
|
||||
hui-view {
|
||||
flex: 1 1 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-panel-maintenance": PanelMaintenance;
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { getAreasFloorHierarchy } from "../../../common/areas/areas-floor-hierarchy";
|
||||
import {
|
||||
findEntities,
|
||||
generateEntityFilter,
|
||||
type EntityFilter,
|
||||
} from "../../../common/entity/entity_filter";
|
||||
import { floorDefaultIcon } from "../../../components/ha-floor-icon";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { TileCardConfig } from "../../lovelace/cards/types";
|
||||
|
||||
export interface MaintenanceViewStrategyConfig {
|
||||
type: "maintenance";
|
||||
}
|
||||
|
||||
export const maintenanceEntityFilters: EntityFilter[] = [
|
||||
{
|
||||
domain: "sensor",
|
||||
device_class: ["battery"],
|
||||
},
|
||||
{
|
||||
domain: "binary_sensor",
|
||||
device_class: ["battery", "battery_charging"],
|
||||
entity_category: "none",
|
||||
},
|
||||
];
|
||||
|
||||
const LOW_BATTERY_THRESHOLD = 20;
|
||||
|
||||
export const filterNeedsAttentionEntities = (
|
||||
hass: HomeAssistant,
|
||||
entityIds: string[]
|
||||
): string[] =>
|
||||
entityIds.filter((entityId) => {
|
||||
const stateValue = parseFloat(hass.states[entityId]?.state ?? "");
|
||||
return !isNaN(stateValue) && stateValue <= LOW_BATTERY_THRESHOLD;
|
||||
});
|
||||
|
||||
const computeBatteryTileCard = (entityId: string): TileCardConfig => ({
|
||||
type: "tile",
|
||||
entity: entityId,
|
||||
name: { type: "device" },
|
||||
});
|
||||
|
||||
const processAreasForBattery = (
|
||||
areaIds: string[],
|
||||
hass: HomeAssistant,
|
||||
entities: string[]
|
||||
): LovelaceCardConfig[] => {
|
||||
const cards: LovelaceCardConfig[] = [];
|
||||
|
||||
for (const areaId of areaIds) {
|
||||
const area = hass.areas[areaId];
|
||||
if (!area) continue;
|
||||
|
||||
const areaFilter = generateEntityFilter(hass, {
|
||||
area: area.area_id,
|
||||
});
|
||||
const areaBatteryEntities = entities.filter(areaFilter);
|
||||
const areaCards: LovelaceCardConfig[] = [];
|
||||
|
||||
for (const entityId of areaBatteryEntities) {
|
||||
areaCards.push(computeBatteryTileCard(entityId));
|
||||
}
|
||||
|
||||
if (areaCards.length > 0) {
|
||||
cards.push({
|
||||
heading_style: "subtitle",
|
||||
type: "heading",
|
||||
heading: area.name,
|
||||
tap_action: hass.panels.home
|
||||
? {
|
||||
action: "navigate",
|
||||
navigation_path: `/home/areas-${area.area_id}`,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
cards.push(...areaCards);
|
||||
}
|
||||
}
|
||||
|
||||
return cards;
|
||||
};
|
||||
|
||||
const processUnassignedEntities = (
|
||||
hass: HomeAssistant,
|
||||
entities: string[]
|
||||
): LovelaceCardConfig[] => {
|
||||
const unassignedFilter = generateEntityFilter(hass, {
|
||||
area: null,
|
||||
});
|
||||
const unassignedEntities = entities.filter(unassignedFilter);
|
||||
const cards: LovelaceCardConfig[] = [];
|
||||
|
||||
for (const entityId of unassignedEntities) {
|
||||
cards.push(computeBatteryTileCard(entityId));
|
||||
}
|
||||
|
||||
return cards;
|
||||
};
|
||||
|
||||
@customElement("maintenance-view-strategy")
|
||||
export class MaintenanceViewStrategy extends ReactiveElement {
|
||||
static async generate(
|
||||
_config: MaintenanceViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const areas = Object.values(hass.areas);
|
||||
const floors = Object.values(hass.floors);
|
||||
const hierarchy = getAreasFloorHierarchy(floors, areas);
|
||||
|
||||
const sections: LovelaceSectionRawConfig[] = [];
|
||||
|
||||
const allEntities = Object.keys(hass.states);
|
||||
|
||||
const batteryFilters = maintenanceEntityFilters.map((filter) =>
|
||||
generateEntityFilter(hass, filter)
|
||||
);
|
||||
|
||||
const entities = findEntities(allEntities, batteryFilters);
|
||||
|
||||
const floorCount =
|
||||
hierarchy.floors.length + (hierarchy.areas.length ? 1 : 0);
|
||||
|
||||
// Process floors
|
||||
for (const floorStructure of hierarchy.floors) {
|
||||
const floorId = floorStructure.id;
|
||||
const areaIds = floorStructure.areas;
|
||||
const floor = hass.floors[floorId];
|
||||
|
||||
const section: LovelaceSectionRawConfig = {
|
||||
type: "grid",
|
||||
column_span: 2,
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading:
|
||||
floorCount > 1
|
||||
? floor.name
|
||||
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
|
||||
icon: floor.icon || floorDefaultIcon(floor),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const areaCards = processAreasForBattery(areaIds, hass, entities);
|
||||
|
||||
if (areaCards.length > 0) {
|
||||
section.cards!.push(...areaCards);
|
||||
sections.push(section);
|
||||
}
|
||||
}
|
||||
|
||||
// Process unassigned areas
|
||||
if (hierarchy.areas.length > 0) {
|
||||
const section: LovelaceSectionRawConfig = {
|
||||
type: "grid",
|
||||
column_span: 2,
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading:
|
||||
floorCount > 1
|
||||
? hass.localize(
|
||||
"ui.panel.lovelace.strategy.maintenance.other_devices"
|
||||
)
|
||||
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const areaCards = processAreasForBattery(hierarchy.areas, hass, entities);
|
||||
|
||||
if (areaCards.length > 0) {
|
||||
section.cards!.push(...areaCards);
|
||||
sections.push(section);
|
||||
}
|
||||
}
|
||||
|
||||
// Process unassigned entities
|
||||
const unassignedCards = processUnassignedEntities(hass, entities);
|
||||
|
||||
if (unassignedCards.length > 0) {
|
||||
const section: LovelaceSectionRawConfig = {
|
||||
type: "grid",
|
||||
column_span: 2,
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading:
|
||||
sections.length > 0
|
||||
? hass.localize(
|
||||
"ui.panel.lovelace.strategy.maintenance.other_devices"
|
||||
)
|
||||
: hass.localize(
|
||||
"ui.panel.lovelace.strategy.maintenance.devices"
|
||||
),
|
||||
},
|
||||
...unassignedCards,
|
||||
],
|
||||
};
|
||||
sections.push(section);
|
||||
}
|
||||
|
||||
return {
|
||||
type: "sections",
|
||||
max_columns: 2,
|
||||
sections: sections,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"maintenance-view-strategy": MaintenanceViewStrategy;
|
||||
}
|
||||
}
|
||||
@@ -99,36 +99,41 @@ class PanelSecurity extends LitElement {
|
||||
return html`
|
||||
<div class="header ${classMap({ narrow: this.narrow })}">
|
||||
<div class="toolbar">
|
||||
${this._searchParms.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`}
|
||||
${
|
||||
this._searchParms.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`
|
||||
}
|
||||
<div class="main-title">${this.hass.localize("panel.security")}</div>
|
||||
</div>
|
||||
</div>
|
||||
${this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}> </hui-view-background>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}> </hui-view-background>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</hui-view-container>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -180,10 +180,10 @@ class DialogTodoItemEditor extends LitElement {
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.todo.item.description"
|
||||
)}
|
||||
.hint=${supportsMarkdownHelper(this.hass.localize)}
|
||||
.helper=${supportsMarkdownHelper(this.hass.localize)}
|
||||
.value=${this._description}
|
||||
@input=${this._handleDescriptionChanged}
|
||||
resize="auto"
|
||||
autogrow
|
||||
.disabled=${!canUpdate}
|
||||
></ha-textarea>`
|
||||
: nothing}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import-x/namespace -- echarts/core uses complex re-exports that static analysis can't resolve */
|
||||
// Import the echarts core module, which provides the necessary interfaces for using echarts.
|
||||
import * as echarts from "echarts/core";
|
||||
|
||||
@@ -27,6 +26,7 @@ import { CanvasRenderer } from "echarts/renderers";
|
||||
// Import graphic utilities from zrender for use in charts
|
||||
// This avoids importing from the full "echarts" package which has a separate registry
|
||||
// zrender is a direct dependency of echarts and always available
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import LinearGradient from "zrender/lib/graphic/LinearGradient";
|
||||
|
||||
import "./axis-proxy-patch";
|
||||
|
||||
@@ -73,7 +73,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
"Safe mode",
|
||||
text:
|
||||
this.hass!.localize("ui.dialogs.safe_mode.text") ||
|
||||
"Home Assistant is running in safe mode, custom integrations and community frontend modules are not available. Restart Home Assistant to exit safe mode.",
|
||||
"Home Assistant is running in safe mode, custom integrations and modules are not available. Restart Home Assistant to exit safe mode.",
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -68,11 +68,6 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
|
||||
private __loadedFragmentTranslations = new Set<string>();
|
||||
|
||||
private __inflightFragmentTranslations = new Map<
|
||||
string,
|
||||
Promise<LocalizeFunc>
|
||||
>();
|
||||
|
||||
private __loadedTranslations: Record<string, LoadedTranslationCategory> =
|
||||
{};
|
||||
|
||||
@@ -263,7 +258,6 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
this._applyDirection(hass);
|
||||
this._loadCoreTranslations(hass.language);
|
||||
this.__loadedFragmentTranslations = new Set();
|
||||
this.__inflightFragmentTranslations = new Map();
|
||||
this._loadFragmentTranslations(hass.language, hass.panelUrl);
|
||||
}
|
||||
|
||||
@@ -386,20 +380,12 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this.__inflightFragmentTranslations.has(fragment)) {
|
||||
return this.__inflightFragmentTranslations.get(fragment)!;
|
||||
}
|
||||
if (this.__loadedFragmentTranslations.has(fragment)) {
|
||||
return this.hass!.localize;
|
||||
}
|
||||
const promise = getTranslation(fragment, language).then((result) =>
|
||||
this._updateResources(language, result.data).finally(() => {
|
||||
this.__inflightFragmentTranslations.delete(fragment);
|
||||
this.__loadedFragmentTranslations.add(fragment);
|
||||
})
|
||||
);
|
||||
this.__inflightFragmentTranslations.set(fragment, promise);
|
||||
return promise;
|
||||
this.__loadedFragmentTranslations.add(fragment);
|
||||
const result = await getTranslation(fragment, language);
|
||||
return this._updateResources(language, result.data);
|
||||
}
|
||||
|
||||
private async _loadCoreTranslations(language: string) {
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"security": "Security",
|
||||
"climate": "Climate",
|
||||
"home": "Overview",
|
||||
"maintenance": "Maintenance",
|
||||
"notfound": "Page not found"
|
||||
},
|
||||
"state": {
|
||||
@@ -218,8 +217,6 @@
|
||||
"all_secure": "All secure",
|
||||
"no_media_playing": "No media playing",
|
||||
"count_media_playing": "{count} {count, plural,\n one {playing}\n other {playing}\n}",
|
||||
"count_maintenance_issues": "{count} {count, plural,\n one {issue}\n other {issues}\n}",
|
||||
"all_maintenance_good": "All good",
|
||||
"count_persons_home": "{count} {count, plural,\n one {person}\n other {people}\n} home",
|
||||
"nobody_home": "No one home"
|
||||
},
|
||||
@@ -1063,7 +1060,6 @@
|
||||
"this_month": "This month",
|
||||
"now-7d": "Last 7 days",
|
||||
"now-30d": "Last 30 days",
|
||||
"now-365d": "Last 365 days",
|
||||
"this_year": "This year",
|
||||
"now-12m": "Last 12 months"
|
||||
}
|
||||
@@ -1444,7 +1440,7 @@
|
||||
},
|
||||
"safe_mode": {
|
||||
"title": "Safe mode",
|
||||
"text": "Home Assistant is running in safe mode, custom integrations and community frontend modules are not available. Restart Home Assistant to exit safe mode."
|
||||
"text": "Home Assistant is running in safe mode, custom integrations and frontend modules are not available. Restart Home Assistant to exit safe mode."
|
||||
},
|
||||
"quick-bar": {
|
||||
"commands_title": "Commands",
|
||||
@@ -2011,9 +2007,9 @@
|
||||
},
|
||||
"restart-safe-mode": {
|
||||
"title": "Restart Home Assistant in safe mode",
|
||||
"description": "Restarts Home Assistant without loading any custom integrations and community frontend modules.",
|
||||
"description": "Restarts Home Assistant without loading any custom integrations and frontend modules.",
|
||||
"confirm_title": "Restart Home Assistant in safe mode?",
|
||||
"confirm_description": "This will restart Home Assistant without loading any custom integrations and community frontend modules.",
|
||||
"confirm_description": "This will restart Home Assistant without loading any custom integrations and frontend modules.",
|
||||
"confirm_action": "Restart",
|
||||
"confirm_action_backup": "Wait and Restart",
|
||||
"failed": "Failed to restart Home Assistant"
|
||||
@@ -7813,7 +7809,7 @@
|
||||
"ping_device": "Ping device",
|
||||
"open_commissioning_window": "Share device",
|
||||
"manage_fabrics": "Manage fabrics",
|
||||
"manage_lock": "Manage access",
|
||||
"manage_lock": "Manage lock",
|
||||
"view_thread_network": "View Thread network"
|
||||
},
|
||||
"manage_fabrics": {
|
||||
@@ -7856,8 +7852,8 @@
|
||||
"copy_code": "Copy code"
|
||||
},
|
||||
"lock": {
|
||||
"manage": "Manage access",
|
||||
"dialog_title": "Manage access",
|
||||
"manage": "Manage lock",
|
||||
"dialog_title": "Manage lock",
|
||||
"users": {
|
||||
"title": "Users",
|
||||
"add": "Add user",
|
||||
@@ -7941,10 +7937,6 @@
|
||||
"load_failed": "Failed to load lock information",
|
||||
"save_failed": "Failed to save changes",
|
||||
"not_supported": "This feature is not supported by your lock",
|
||||
"no_user_management": "This lock does not support user management. Users and credentials cannot be managed here.",
|
||||
"pin_not_supported": "This lock does not support PIN codes. You can still view and manage existing users.",
|
||||
"no_credential_types_supported": "This lock does not support any credential types that can be managed here. You can still view existing users.",
|
||||
"learn_more": "Learn more about lock management",
|
||||
"user_not_found": "User not found",
|
||||
"name_required": "Please enter a name for this user.",
|
||||
"pin_required": "Please enter a PIN code.",
|
||||
@@ -8262,7 +8254,6 @@
|
||||
"media_players": "Media players",
|
||||
"other_devices": "Other devices",
|
||||
"weather": "Weather",
|
||||
"maintenance": "Maintenance",
|
||||
"energy": "Today's energy",
|
||||
"persons": "Presence"
|
||||
},
|
||||
@@ -8301,10 +8292,6 @@
|
||||
"devices": "Devices",
|
||||
"other_devices": "Other devices"
|
||||
},
|
||||
"maintenance": {
|
||||
"devices": "Devices",
|
||||
"other_devices": "Other devices"
|
||||
},
|
||||
"home_media_players": {
|
||||
"media_players": "Media players",
|
||||
"other_media_players": "Other media players"
|
||||
@@ -9423,8 +9410,6 @@
|
||||
"state": "State",
|
||||
"attribute": "Attribute"
|
||||
},
|
||||
"conditions": "Entity visibility conditions",
|
||||
"conditions_helper": "Each selected entity will be shown when all conditions below are met. If no conditions are set, entities are always shown.",
|
||||
"description": "This card allows you to display entities on a map."
|
||||
},
|
||||
"markdown": {
|
||||
@@ -10017,7 +10002,7 @@
|
||||
},
|
||||
"badge_picker": {
|
||||
"no_description": "No description available.",
|
||||
"custom_badge": "Community",
|
||||
"custom_badge": "Custom",
|
||||
"domain": "Domain",
|
||||
"entity": "Entity",
|
||||
"by_entity": "By entity",
|
||||
@@ -10716,7 +10701,7 @@
|
||||
"external_panel": {
|
||||
"question_trust": "Do you trust the external panel {name} at {link}?",
|
||||
"complete_access": "It will have access to all data in Home Assistant.",
|
||||
"hide_message": "Check docs for the 'Custom panel' integration to hide this message"
|
||||
"hide_message": "Check docs for the panel_custom component to hide this message"
|
||||
}
|
||||
},
|
||||
"energy": {
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { MatterLockInfo } from "../../../../../src/data/matter-lock";
|
||||
|
||||
/**
|
||||
* These tests verify the display logic for the lock management dialog,
|
||||
* ensuring the correct alert is shown based on lock capabilities.
|
||||
*/
|
||||
|
||||
type AlertState =
|
||||
| "no_user_management"
|
||||
| "no_credential_types_supported"
|
||||
| "pin_not_supported"
|
||||
| "full_support";
|
||||
|
||||
/**
|
||||
* Mirrors the branching logic in dialog-matter-lock-manage.ts render()
|
||||
* and _renderUsers() to determine which alert (if any) to display.
|
||||
*/
|
||||
function getAlertState(lockInfo: MatterLockInfo | undefined): AlertState {
|
||||
if (lockInfo && !lockInfo.supports_user_management) {
|
||||
return "no_user_management";
|
||||
}
|
||||
|
||||
const hasNoManageableCredentials =
|
||||
!lockInfo?.supported_credential_types?.length;
|
||||
|
||||
if (hasNoManageableCredentials) {
|
||||
return "no_credential_types_supported";
|
||||
}
|
||||
|
||||
const supportsPinCredential =
|
||||
lockInfo?.supported_credential_types?.includes("pin") ?? false;
|
||||
|
||||
if (!supportsPinCredential) {
|
||||
return "pin_not_supported";
|
||||
}
|
||||
|
||||
return "full_support";
|
||||
}
|
||||
|
||||
describe("dialog-matter-lock-manage alert logic", () => {
|
||||
it("shows no_user_management when lock does not support user management", () => {
|
||||
const lockInfo: MatterLockInfo = {
|
||||
supports_user_management: false,
|
||||
supported_credential_types: [],
|
||||
max_users: null,
|
||||
max_pin_users: null,
|
||||
max_rfid_users: null,
|
||||
max_credentials_per_user: null,
|
||||
min_pin_length: null,
|
||||
max_pin_length: null,
|
||||
min_rfid_length: null,
|
||||
max_rfid_length: null,
|
||||
};
|
||||
|
||||
expect(getAlertState(lockInfo)).toBe("no_user_management");
|
||||
});
|
||||
|
||||
it("shows no_user_management even if credential types are listed", () => {
|
||||
const lockInfo: MatterLockInfo = {
|
||||
supports_user_management: false,
|
||||
supported_credential_types: ["pin"],
|
||||
max_users: null,
|
||||
max_pin_users: null,
|
||||
max_rfid_users: null,
|
||||
max_credentials_per_user: null,
|
||||
min_pin_length: 4,
|
||||
max_pin_length: 8,
|
||||
min_rfid_length: null,
|
||||
max_rfid_length: null,
|
||||
};
|
||||
|
||||
expect(getAlertState(lockInfo)).toBe("no_user_management");
|
||||
});
|
||||
|
||||
it("shows no_credential_types_supported when user management is supported but no credential types", () => {
|
||||
const lockInfo: MatterLockInfo = {
|
||||
supports_user_management: true,
|
||||
supported_credential_types: [],
|
||||
max_users: 10,
|
||||
max_pin_users: null,
|
||||
max_rfid_users: null,
|
||||
max_credentials_per_user: null,
|
||||
min_pin_length: null,
|
||||
max_pin_length: null,
|
||||
min_rfid_length: null,
|
||||
max_rfid_length: null,
|
||||
};
|
||||
|
||||
expect(getAlertState(lockInfo)).toBe("no_credential_types_supported");
|
||||
});
|
||||
|
||||
it("shows pin_not_supported when user management is supported with non-pin credentials only", () => {
|
||||
const lockInfo: MatterLockInfo = {
|
||||
supports_user_management: true,
|
||||
supported_credential_types: ["rfid"],
|
||||
max_users: 10,
|
||||
max_pin_users: null,
|
||||
max_rfid_users: 5,
|
||||
max_credentials_per_user: 3,
|
||||
min_pin_length: null,
|
||||
max_pin_length: null,
|
||||
min_rfid_length: 4,
|
||||
max_rfid_length: 8,
|
||||
};
|
||||
|
||||
expect(getAlertState(lockInfo)).toBe("pin_not_supported");
|
||||
});
|
||||
|
||||
it("shows full_support when user management and pin are both supported", () => {
|
||||
const lockInfo: MatterLockInfo = {
|
||||
supports_user_management: true,
|
||||
supported_credential_types: ["pin"],
|
||||
max_users: 10,
|
||||
max_pin_users: 10,
|
||||
max_rfid_users: null,
|
||||
max_credentials_per_user: 5,
|
||||
min_pin_length: 4,
|
||||
max_pin_length: 8,
|
||||
min_rfid_length: null,
|
||||
max_rfid_length: null,
|
||||
};
|
||||
|
||||
expect(getAlertState(lockInfo)).toBe("full_support");
|
||||
});
|
||||
|
||||
it("shows full_support when both pin and rfid are supported", () => {
|
||||
const lockInfo: MatterLockInfo = {
|
||||
supports_user_management: true,
|
||||
supported_credential_types: ["pin", "rfid"],
|
||||
max_users: 10,
|
||||
max_pin_users: 10,
|
||||
max_rfid_users: 5,
|
||||
max_credentials_per_user: 5,
|
||||
min_pin_length: 4,
|
||||
max_pin_length: 8,
|
||||
min_rfid_length: 4,
|
||||
max_rfid_length: 8,
|
||||
};
|
||||
|
||||
expect(getAlertState(lockInfo)).toBe("full_support");
|
||||
});
|
||||
|
||||
it("handles undefined lockInfo as no_credential_types_supported", () => {
|
||||
expect(getAlertState(undefined)).toBe("no_credential_types_supported");
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user