Compare commits

..

1 Commits

Author SHA1 Message Date
Bram Kragten
e98eb8de7f Add inital form data to hui-form-editor 2023-12-12 09:58:50 +01:00
318 changed files with 5924 additions and 9268 deletions

View File

@@ -24,7 +24,6 @@ body:
required: true required: true
- label: I have tried a different browser to see if it is related to my browser. - label: I have tried a different browser to see if it is related to my browser.
required: true required: true
- label: I have tried reproducing the issue in [safe mode](https://www.home-assistant.io/blog/2023/11/01/release-202311/#restarting-into-safe-mode) to rule out problems with unsupported custom resources.
- type: markdown - type: markdown
attributes: attributes:
value: | value: |

55
.github/labeler.yml vendored
View File

@@ -1,50 +1,31 @@
Build: Build:
- changed-files: - build-scripts/**
- any-glob-to-any-file: - .browserslistrc
- build-scripts/** - gulpfile.js
- .browserslistrc
- gulpfile.js
Cast: Cast:
- changed-files: - cast/src/**
- any-glob-to-any-file: - src/cast/**
- cast/src/**
- src/cast/**
Demo: Demo:
- changed-files: - demo/src/**
- any-glob-to-any-file: - src/fake_data/**
- demo/src/**
- src/fake_data/**
Design: Design:
- changed-files: - gallery/src/**
- any-glob-to-any-file: - src/fake_data/**
- gallery/src/**
- src/fake_data/**
Dependencies: Dependencies:
- changed-files: - package.json
# Match when only these files are changed (i.e. don't match PRs that happen to add or remove packages) - renovate.json
- any-glob-to-all-files: - yarn.lock
- package.json - .yarn/**
- renovate.json - .yarnrc.yml
- yarn.lock - .nvmrc
- .yarn/**
- .yarnrc.yml
- .nvmrc
# Dependabot and Renovate branches always match (i.e. compatibility tweaks by members considered minor)
- head-branch:
- "^renovate/"
- "^dependabot/"
GitHub Actions: GitHub Actions:
- changed-files: - .github/workflows/**
- any-glob-to-any-file: - .github/*.yml
- .github/workflows/**
- .github/*.yml
Supervisor: Supervisor:
- changed-files: - hassio/src/**
- any-glob-to-any-file:
- hassio/src/**

View File

@@ -26,7 +26,7 @@ jobs:
ref: dev ref: dev
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -62,7 +62,7 @@ jobs:
ref: master ref: master
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -26,7 +26,7 @@ jobs:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -48,8 +48,6 @@ jobs:
run: yarn run lint:eslint --quiet run: yarn run lint:eslint --quiet
- name: Run tsc - name: Run tsc
run: yarn run lint:types run: yarn run lint:types
- name: Run lit-analyzer
run: yarn run lint:lit --quiet
- name: Run prettier - name: Run prettier
run: yarn run lint:prettier run: yarn run lint:prettier
test: test:
@@ -59,14 +57,14 @@ jobs:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
- name: Install dependencies - name: Install dependencies
run: yarn install --immutable run: yarn install --immutable
- name: Build resources - name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data run: ./node_modules/.bin/gulp build-translations build-locale-data
- name: Run Tests - name: Run Tests
run: yarn run test run: yarn run test
build: build:
@@ -77,7 +75,7 @@ jobs:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -101,7 +99,7 @@ jobs:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v3 uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v2

View File

@@ -27,7 +27,7 @@ jobs:
ref: dev ref: dev
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -63,7 +63,7 @@ jobs:
ref: master ref: master
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -19,7 +19,7 @@ jobs:
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn

View File

@@ -34,7 +34,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }} python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.1 uses: actions/setup-node@v4.0.0
with: with:
node-version-file: ".nvmrc" node-version-file: ".nvmrc"
cache: yarn cache: yarn
@@ -74,7 +74,7 @@ jobs:
echo "home-assistant-frontend==$version" > ./requirements.txt echo "home-assistant-frontend==$version" > ./requirements.txt
- name: Build wheels - name: Build wheels
uses: home-assistant/wheels@2024.01.0 uses: home-assistant/wheels@2023.10.5
with: with:
abi: cp311 abi: cp311
tag: musllinux_1_2 tag: musllinux_1_2

View File

@@ -1,7 +1,6 @@
const path = require("path"); const path = require("path");
const env = require("./env.cjs"); const env = require("./env.cjs");
const paths = require("./paths.cjs"); const paths = require("./paths.cjs");
const { dependencies } = require("../package.json");
// GitHub base URL to use for production source maps // GitHub base URL to use for production source maps
// Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version // Nightly builds use the commit SHA, otherwise assumes there is a tag that matches the version
@@ -91,7 +90,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
"@babel/preset-env", "@babel/preset-env",
{ {
useBuiltIns: latestBuild ? false : "usage", useBuiltIns: latestBuild ? false : "usage",
corejs: latestBuild ? false : dependencies["core-js"], corejs: latestBuild ? false : "3.33",
bugfixes: true, bugfixes: true,
shippedProposals: true, shippedProposals: true,
}, },
@@ -141,7 +140,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
// Import helpers and regenerator from runtime package // Import helpers and regenerator from runtime package
[ [
"@babel/plugin-transform-runtime", "@babel/plugin-transform-runtime",
{ version: dependencies["@babel/runtime"] }, { version: require("../package.json").dependencies["@babel/runtime"] },
], ],
// Support some proposals still in TC39 process // Support some proposals still in TC39 process
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }], ["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],

View File

@@ -426,7 +426,6 @@ gulp.task(
"fetch-nightly-translations", "fetch-nightly-translations",
gulp.series("clean-translations", "ensure-translations-build-dir") gulp.series("clean-translations", "ensure-translations-build-dir")
), ),
gulp.parallel("create-test-metadata", "create-test-translation"),
"build-master-translation", "build-master-translation",
"build-merged-translations", "build-merged-translations",
"build-translation-fragment-supervisor", "build-translation-fragment-supervisor",

View File

@@ -7,9 +7,6 @@ const TerserPlugin = require("terser-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin"); const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const log = require("fancy-log"); const log = require("fancy-log");
const WebpackBar = require("webpackbar"); const WebpackBar = require("webpackbar");
const {
TransformAsyncModulesPlugin,
} = require("transform-async-modules-webpack-plugin");
const paths = require("./paths.cjs"); const paths = require("./paths.cjs");
const bundle = require("./bundle.cjs"); const bundle = require("./bundle.cjs");
@@ -145,6 +142,17 @@ const createWebpackConfig = ({
), ),
path.resolve(paths.polymer_dir, "src/util/empty.js") path.resolve(paths.polymer_dir, "src/util/empty.js")
), ),
// See `src/resources/intl-polyfill-legacy.ts` for explanation
!latestBuild &&
new webpack.NormalModuleReplacementPlugin(
new RegExp(
path.resolve(paths.polymer_dir, "src/resources/intl-polyfill.ts")
),
path.resolve(
paths.polymer_dir,
"src/resources/intl-polyfill-legacy.ts"
)
),
!isProdBuild && new LogStartCompilePlugin(), !isProdBuild && new LogStartCompilePlugin(),
isProdBuild && isProdBuild &&
new StatsWriterPlugin({ new StatsWriterPlugin({
@@ -155,8 +163,6 @@ const createWebpackConfig = ({
stats: { assets: true, chunks: true, modules: true }, stats: { assets: true, chunks: true, modules: true },
transform: (stats) => JSON.stringify(filterStats(stats)), transform: (stats) => JSON.stringify(filterStats(stats)),
}), }),
!latestBuild &&
new TransformAsyncModulesPlugin({ browserslistEnv: "legacy" }),
].filter(Boolean), ].filter(Boolean),
resolve: { resolve: {
extensions: [".ts", ".js", ".json"], extensions: [".ts", ".js", ".json"],

View File

@@ -11,7 +11,7 @@ class DemoBlackWhiteRow extends LitElement {
@property() value!: any; @property() value!: any;
@property({ type: Boolean }) public disabled = false; @property() disabled = false;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`

View File

@@ -15,7 +15,7 @@ class DemoCard extends LitElement {
@property() public config!: DemoCardConfig; @property() public config!: DemoCardConfig;
@property({ type: Boolean }) public showConfig = false; @property() public showConfig = false;
@state() private _size?: number; @state() private _size?: number;

View File

@@ -12,7 +12,7 @@ class DemoMoreInfo extends LitElement {
@property() public entityId!: string; @property() public entityId!: string;
@property({ type: Boolean }) public showConfig = false; @property() public showConfig!: boolean;
render() { render() {
const state = this._getState(this.entityId, this.hass.states); const state = this._getState(this.entityId, this.hass.states);

View File

@@ -509,7 +509,7 @@ export default {
away_mode: "on", away_mode: "on",
aux_heat: "off", aux_heat: "off",
unit_of_measurement: "°C", unit_of_measurement: "°C",
friendly_name: "HVAC", friendly_name: "Hvac",
supported_features: 3833, supported_features: 3833,
}, },
last_changed: "2018-07-19T10:44:46.200650+00:00", last_changed: "2018-07-19T10:44:46.200650+00:00",

View File

@@ -80,7 +80,7 @@ const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [
]; ];
@customElement("demo-automation-editor-condition") @customElement("demo-automation-editor-condition")
export class DemoAutomationEditorCondition extends LitElement { class DemoHaAutomationEditorCondition extends LitElement {
@state() private hass!: HomeAssistant; @state() private hass!: HomeAssistant;
@state() private _disabled = false; @state() private _disabled = false;
@@ -155,6 +155,6 @@ export class DemoAutomationEditorCondition extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"demo-automation-editor-condition": DemoAutomationEditorCondition; "demo-ha-automation-editor-condition": DemoHaAutomationEditorCondition;
} }
} }

View File

@@ -126,7 +126,7 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
]; ];
@customElement("demo-automation-editor-trigger") @customElement("demo-automation-editor-trigger")
export class DemoAutomationEditorTrigger extends LitElement { class DemoHaAutomationEditorTrigger extends LitElement {
@state() private hass!: HomeAssistant; @state() private hass!: HomeAssistant;
@state() private _disabled = false; @state() private _disabled = false;
@@ -201,6 +201,6 @@ export class DemoAutomationEditorTrigger extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"demo-automation-editor-trigger": DemoAutomationEditorTrigger; "demo-ha-automation-editor-trigger": DemoHaAutomationEditorTrigger;
} }
} }

View File

@@ -55,7 +55,7 @@ const CONFIGS = [
]; ];
@customElement("demo-lovelace-media-player-row") @customElement("demo-lovelace-media-player-row")
export class DemoLovelaceMediaPlayerRow extends LitElement { class DemoHuiMediaPlayerRow extends LitElement {
@query("#demos") private _demoRoot!: HTMLElement; @query("#demos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult { protected render(): TemplateResult {
@@ -73,6 +73,6 @@ export class DemoLovelaceMediaPlayerRow extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"demo-lovelace-media-player-row": DemoLovelaceMediaPlayerRow; "demo-lovelace-media-player-rows": DemoHuiMediaPlayerRow;
} }
} }

View File

@@ -35,18 +35,6 @@ const ENTITIES = [
friendly_name: "Nest", friendly_name: "Nest",
supported_features: 43, supported_features: 43,
}), }),
getEntity("climate", "sensibo", "fan_only", {
current_temperature: null,
temperature: null,
min_temp: 0,
max_temp: 1,
target_temp_step: 1,
hvac_modes: ["fan_only", "off"],
friendly_name: "Sensibo purifier",
fan_modes: ["low", "high"],
fan_mode: "low",
supported_features: 9,
}),
getEntity("climate", "unavailable", "unavailable", { getEntity("climate", "unavailable", "unavailable", {
supported_features: 43, supported_features: 43,
}), }),
@@ -69,23 +57,6 @@ const CONFIGS = [
entity: climate.nest entity: climate.nest
`, `,
}, },
{
heading: "Fan only example",
config: `
- type: thermostat
entity: climate.sensibo
features:
- type: climate-hvac-modes
hvac_modes:
- fan_only
- 'off'
- type: climate-fan-modes
style: icons
fan_modes:
- low
- high
`,
},
{ {
heading: "Unavailable", heading: "Unavailable",
config: ` config: `

View File

@@ -59,9 +59,3 @@ export class DemoUtilLongPress extends LitElement {
} }
`; `;
} }
declare global {
interface HTMLElementTagNameMap {
"demo-misc-util-long-press": DemoUtilLongPress;
}
}

View File

@@ -31,21 +31,6 @@ const ENTITIES = [
max_temp: 30, max_temp: 30,
supported_features: ClimateEntityFeature.TARGET_TEMPERATURE, supported_features: ClimateEntityFeature.TARGET_TEMPERATURE,
}), }),
getEntity("climate", "fan", "fan_only", {
friendly_name: "Basic fan",
hvac_modes: ["fan_only", "off"],
hvac_mode: "fan_only",
fan_modes: ["low", "high"],
fan_mode: "low",
current_temperature: null,
temperature: null,
min_temp: 0,
max_temp: 1,
target_temp_step: 1,
supported_features:
// eslint-disable-next-line no-bitwise
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE,
}),
getEntity("climate", "hvac", "auto", { getEntity("climate", "hvac", "auto", {
friendly_name: "Basic hvac", friendly_name: "Basic hvac",
hvac_modes: ["auto", "off"], hvac_modes: ["auto", "off"],

View File

@@ -1,6 +1,12 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit"; import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import "../../../../src/components/ha-card"; import "../../../../src/components/ha-card";
import {
UPDATE_SUPPORT_BACKUP,
UPDATE_SUPPORT_PROGRESS,
UPDATE_SUPPORT_INSTALL,
UPDATE_SUPPORT_RELEASE_NOTES,
} from "../../../../src/data/update";
import "../../../../src/dialogs/more-info/more-info-content"; import "../../../../src/dialogs/more-info/more-info-content";
import { getEntity } from "../../../../src/fake_data/entity"; import { getEntity } from "../../../../src/fake_data/entity";
import { import {
@@ -9,14 +15,13 @@ import {
} from "../../../../src/fake_data/provide_hass"; } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-more-infos"; import "../../components/demo-more-infos";
import { LONG_TEXT } from "../../data/text"; import { LONG_TEXT } from "../../data/text";
import { UpdateEntityFeature } from "../../../../src/data/update";
const base_attributes = { const base_attributes = {
title: "Awesome", title: "Awesome",
installed_version: "1.2.2", installed_version: "1.2.2",
latest_version: "1.2.3", latest_version: "1.2.3",
release_url: "https://home-assistant.io", release_url: "https://home-assistant.io",
supported_features: UpdateEntityFeature.INSTALL, supported_features: UPDATE_SUPPORT_INSTALL,
skipped_version: null, skipped_version: null,
in_progress: false, in_progress: false,
release_summary: release_summary:
@@ -56,7 +61,7 @@ const ENTITIES = [
getEntity("update", "update7", "on", { getEntity("update", "update7", "on", {
...base_attributes, ...base_attributes,
supported_features: supported_features:
base_attributes.supported_features + UpdateEntityFeature.BACKUP, base_attributes.supported_features + UPDATE_SUPPORT_BACKUP,
friendly_name: "With backup support", friendly_name: "With backup support",
}), }),
getEntity("update", "update8", "on", { getEntity("update", "update8", "on", {
@@ -68,21 +73,21 @@ const ENTITIES = [
...base_attributes, ...base_attributes,
in_progress: 25, in_progress: 25,
supported_features: supported_features:
base_attributes.supported_features + UpdateEntityFeature.PROGRESS, base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
friendly_name: "With 25 in_progress", friendly_name: "With 25 in_progress",
}), }),
getEntity("update", "update10", "on", { getEntity("update", "update10", "on", {
...base_attributes, ...base_attributes,
in_progress: 50, in_progress: 50,
supported_features: supported_features:
base_attributes.supported_features + UpdateEntityFeature.PROGRESS, base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
friendly_name: "With 50 in_progress", friendly_name: "With 50 in_progress",
}), }),
getEntity("update", "update11", "on", { getEntity("update", "update11", "on", {
...base_attributes, ...base_attributes,
in_progress: 75, in_progress: 75,
supported_features: supported_features:
base_attributes.supported_features + UpdateEntityFeature.PROGRESS, base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
friendly_name: "With 75 in_progress", friendly_name: "With 75 in_progress",
}), }),
getEntity("update", "update12", "unavailable", { getEntity("update", "update12", "unavailable", {
@@ -109,19 +114,19 @@ const ENTITIES = [
...base_attributes, ...base_attributes,
friendly_name: "Update with release notes", friendly_name: "Update with release notes",
supported_features: supported_features:
base_attributes.supported_features + UpdateEntityFeature.RELEASE_NOTES, base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
}), }),
getEntity("update", "update17", "off", { getEntity("update", "update17", "off", {
...base_attributes, ...base_attributes,
friendly_name: "Update with release notes error", friendly_name: "Update with release notes error",
supported_features: supported_features:
base_attributes.supported_features + UpdateEntityFeature.RELEASE_NOTES, base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
}), }),
getEntity("update", "update18", "off", { getEntity("update", "update18", "off", {
...base_attributes, ...base_attributes,
friendly_name: "Update with release notes loading", friendly_name: "Update with release notes loading",
supported_features: supported_features:
base_attributes.supported_features + UpdateEntityFeature.RELEASE_NOTES, base_attributes.supported_features + UPDATE_SUPPORT_RELEASE_NOTES,
}), }),
getEntity("update", "update19", "on", { getEntity("update", "update19", "on", {
...base_attributes, ...base_attributes,
@@ -137,10 +142,9 @@ const ENTITIES = [
getEntity("update", "update21", "on", { getEntity("update", "update21", "on", {
...base_attributes, ...base_attributes,
in_progress: true, in_progress: true,
friendly_name: friendly_name: "Update with in_progress true and UPDATE_SUPPORT_PROGRESS",
"Update with in_progress true and UpdateEntityFeature.PROGRESS",
supported_features: supported_features:
base_attributes.supported_features + UpdateEntityFeature.PROGRESS, base_attributes.supported_features + UPDATE_SUPPORT_PROGRESS,
}), }),
]; ];

View File

@@ -140,9 +140,3 @@ export class HassioAddonRepositoryEl extends LitElement {
]; ];
} }
} }
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-repository": HassioAddonRepositoryEl;
}
}

View File

@@ -248,9 +248,3 @@ export class HassioAddonStore extends LitElement {
`; `;
} }
} }
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-store": HassioAddonStore;
}
}

View File

@@ -1,4 +1,6 @@
import { mdiFolder, mdiPuzzle } from "@mdi/js"; import { mdiFolder, mdiPuzzle } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import { import {
CSSResultGroup, CSSResultGroup,
LitElement, LitElement,
@@ -14,7 +16,6 @@ import { formatDateTime } from "../../../src/common/datetime/format_date_time";
import { LocalizeFunc } from "../../../src/common/translations/localize"; import { LocalizeFunc } from "../../../src/common/translations/localize";
import "../../../src/components/ha-checkbox"; import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-formfield"; import "../../../src/components/ha-formfield";
import "../../../src/components/ha-textfield";
import "../../../src/components/ha-radio"; import "../../../src/components/ha-radio";
import type { HaRadio } from "../../../src/components/ha-radio"; import type { HaRadio } from "../../../src/components/ha-radio";
import { import {
@@ -24,9 +25,12 @@ import {
} from "../../../src/data/hassio/backup"; } from "../../../src/data/hassio/backup";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg"; import { mdiHomeAssistant } from "../../../src/resources/home-assistant-logo-svg";
import { HomeAssistant, TranslationDict } from "../../../src/types"; import {
HomeAssistant,
TranslationDict,
ValueChangedEvent,
} from "../../../src/types";
import "./supervisor-formfield-label"; import "./supervisor-formfield-label";
import type { HaTextField } from "../../../src/components/ha-textfield";
type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] & type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"]; keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
@@ -96,7 +100,7 @@ export class SupervisorBackupContent extends LitElement {
@property() public confirmBackupPassword = ""; @property() public confirmBackupPassword = "";
@query("ha-textfield, ha-radio, ha-checkbox", true) private _focusTarget; @query("paper-input, ha-radio, ha-checkbox", true) private _focusTarget;
public willUpdate(changedProps) { public willUpdate(changedProps) {
super.willUpdate(changedProps); super.willUpdate(changedProps);
@@ -147,13 +151,13 @@ export class SupervisorBackupContent extends LitElement {
) )
: this.backup.date} : this.backup.date}
</div>` </div>`
: html`<ha-textfield : html`<paper-input
name="backupName" name="backupName"
.label=${this._localize("name")} .label=${this._localize("name")}
.value=${this.backupName} .value=${this.backupName}
@change=${this._handleTextValueChanged} @value-changed=${this._handleTextValueChanged}
> >
</ha-textfield>`} </paper-input>`}
${!this.backup || this.backup.type === "full" ${!this.backup || this.backup.type === "full"
? html`<div class="sub-header"> ? html`<div class="sub-header">
${!this.backup ${!this.backup
@@ -261,23 +265,23 @@ export class SupervisorBackupContent extends LitElement {
: ""} : ""}
${this.backupHasPassword ${this.backupHasPassword
? html` ? html`
<ha-textfield <paper-input
.label=${this._localize("password")} .label=${this._localize("password")}
type="password" type="password"
name="backupPassword" name="backupPassword"
.value=${this.backupPassword} .value=${this.backupPassword}
@change=${this._handleTextValueChanged} @value-changed=${this._handleTextValueChanged}
> >
</ha-textfield> </paper-input>
${!this.backup ${!this.backup
? html`<ha-textfield ? html` <paper-input
.label=${this._localize("confirm_password")} .label=${this._localize("confirm_password")}
type="password" type="password"
name="confirmBackupPassword" name="confirmBackupPassword"
.value=${this.confirmBackupPassword} .value=${this.confirmBackupPassword}
@change=${this._handleTextValueChanged} @value-changed=${this._handleTextValueChanged}
> >
</ha-textfield>` </paper-input>`
: ""} : ""}
` `
: ""} : ""}
@@ -425,9 +429,9 @@ export class SupervisorBackupContent extends LitElement {
this[input.name] = input.value; this[input.name] = input.value;
} }
private _handleTextValueChanged(ev: InputEvent) { private _handleTextValueChanged(ev: ValueChangedEvent<string>) {
const input = ev.currentTarget as HaTextField; const input = ev.currentTarget as PaperInputElement;
this[input.name!] = input.value; this[input.name!] = ev.detail.value;
} }
private _toggleHasPassword(): void { private _toggleHasPassword(): void {

View File

@@ -128,7 +128,6 @@ class HassioAddons extends LitElement {
ha-card { ha-card {
cursor: pointer; cursor: pointer;
overflow: hidden; overflow: hidden;
direction: ltr;
} }
.search { .search {
position: sticky; position: sticky;

View File

@@ -133,8 +133,6 @@ class HassioDashboard extends LitElement {
position: fixed; position: fixed;
right: calc(16px + env(safe-area-inset-right)); right: calc(16px + env(safe-area-inset-right));
bottom: calc(16px + env(safe-area-inset-bottom)); bottom: calc(16px + env(safe-area-inset-bottom));
inset-inline-end: calc(16px + env(safe-area-inset-right));
inset-inline-start: initial;
z-index: 1; z-index: 1;
} }
`, `,

View File

@@ -151,9 +151,3 @@ export class HassioUpdate extends LitElement {
]; ];
} }
} }
declare global {
interface HTMLElementTagNameMap {
"hassio-update": HassioUpdate;
}
}

View File

@@ -4,6 +4,7 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-select"; import "../../../../src/components/ha-select";
import { import {
extractApiErrorMessage, extractApiErrorMessage,

View File

@@ -4,6 +4,7 @@ import "@material/mwc-list/mwc-list-item";
import "@material/mwc-tab"; import "@material/mwc-tab";
import "@material/mwc-tab-bar"; import "@material/mwc-tab-bar";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache"; import { cache } from "lit/directives/cache";
@@ -13,7 +14,6 @@ import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel"; import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-formfield"; import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-header-bar"; import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button"; import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-radio"; import "../../../../src/components/ha-radio";
@@ -34,7 +34,6 @@ import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../src/resources/styles"; import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
import { HassioNetworkDialogParams } from "./show-dialog-network"; import { HassioNetworkDialogParams } from "./show-dialog-network";
import type { HaTextField } from "../../../../src/components/ha-textfield";
const IP_VERSIONS = ["ipv4", "ipv6"]; const IP_VERSIONS = ["ipv4", "ipv6"];
@@ -246,7 +245,7 @@ export class DialogHassioNetwork
${this._wifiConfiguration.auth === "wpa-psk" || ${this._wifiConfiguration.auth === "wpa-psk" ||
this._wifiConfiguration.auth === "wep" this._wifiConfiguration.auth === "wep"
? html` ? html`
<ha-textfield <paper-input
class="flex-auto" class="flex-auto"
type="password" type="password"
id="psk" id="psk"
@@ -254,9 +253,10 @@ export class DialogHassioNetwork
"dialog.network.wifi_password" "dialog.network.wifi_password"
)} )}
version="wifi" version="wifi"
@change=${this._handleInputValueChangedWifi} @value-changed=${this
._handleInputValueChangedWifi}
> >
</ha-textfield> </paper-input>
` `
: ""} : ""}
` `
@@ -358,33 +358,33 @@ export class DialogHassioNetwork
</div> </div>
${this._interface![version].method === "static" ${this._interface![version].method === "static"
? html` ? html`
<ha-textfield <paper-input
class="flex-auto" class="flex-auto"
id="address" id="address"
.label=${this.supervisor.localize("dialog.network.ip_netmask")} .label=${this.supervisor.localize("dialog.network.ip_netmask")}
.version=${version} .version=${version}
.value=${this._toString(this._interface![version].address)} .value=${this._toString(this._interface![version].address)}
@change=${this._handleInputValueChanged} @value-changed=${this._handleInputValueChanged}
> >
</ha-textfield> </paper-input>
<ha-textfield <paper-input
class="flex-auto" class="flex-auto"
id="gateway" id="gateway"
.label=${this.supervisor.localize("dialog.network.gateway")} .label=${this.supervisor.localize("dialog.network.gateway")}
.version=${version} .version=${version}
.value=${this._interface![version].gateway} .value=${this._interface![version].gateway}
@change=${this._handleInputValueChanged} @value-changed=${this._handleInputValueChanged}
> >
</ha-textfield> </paper-input>
<ha-textfield <paper-input
class="flex-auto" class="flex-auto"
id="nameservers" id="nameservers"
.label=${this.supervisor.localize("dialog.network.dns_servers")} .label=${this.supervisor.localize("dialog.network.dns_servers")}
.version=${version} .version=${version}
.value=${this._toString(this._interface![version].nameservers)} .value=${this._toString(this._interface![version].nameservers)}
@change=${this._handleInputValueChanged} @value-changed=${this._handleInputValueChanged}
> >
</ha-textfield> </paper-input>
` `
: ""} : ""}
</ha-expansion-panel> </ha-expansion-panel>
@@ -517,11 +517,11 @@ export class DialogHassioNetwork
this.requestUpdate("_wifiConfiguration"); this.requestUpdate("_wifiConfiguration");
} }
private _handleInputValueChanged(ev: Event): void { private _handleInputValueChanged(ev: CustomEvent): void {
const source = ev.target as HaTextField; const value: string | null | undefined = (ev.target as PaperInputElement)
const value = source.value; .value;
const version = (ev.target as any).version as "ipv4" | "ipv6"; const version = (ev.target as any).version as "ipv4" | "ipv6";
const id = source.id; const id = (ev.target as PaperInputElement).id;
if ( if (
!value || !value ||
@@ -535,10 +535,10 @@ export class DialogHassioNetwork
this._interface[version]![id] = value; this._interface[version]![id] = value;
} }
private _handleInputValueChangedWifi(ev: Event): void { private _handleInputValueChangedWifi(ev: CustomEvent): void {
const source = ev.target as HaTextField; const value: string | null | undefined = (ev.target as PaperInputElement)
const value = source.value; .value;
const id = source.id; const id = (ev.target as PaperInputElement).id;
if ( if (
!value || !value ||
@@ -630,7 +630,7 @@ export class DialogHassioNetwork
--expansion-panel-summary-padding: 0 16px; --expansion-panel-summary-padding: 0 16px;
margin: 4px 0; margin: 4px 0;
} }
ha-textfield { paper-input {
padding: 0 14px; padding: 0 14px;
} }
mwc-list-item { mwc-list-item {

View File

@@ -1,5 +1,7 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { mdiDelete, mdiDeleteOff } from "@mdi/js"; import { mdiDelete, mdiDeleteOff } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
@@ -25,14 +27,12 @@ import {
import { haStyle, haStyleDialog } from "../../../../src/resources/styles"; import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
import { HassioRepositoryDialogParams } from "./show-dialog-repositories"; import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
import type { HaTextField } from "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-textfield";
@customElement("dialog-hassio-repositories") @customElement("dialog-hassio-repositories")
class HassioRepositoriesDialog extends LitElement { class HassioRepositoriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@query("#repository_input", true) private _optionInput?: HaTextField; @query("#repository_input", true) private _optionInput?: PaperInputElement;
@state() private _repositories?: HassioAddonRepository[]; @state() private _repositories?: HassioAddonRepository[];
@@ -145,7 +145,7 @@ class HassioRepositoriesDialog extends LitElement {
) )
: html`<paper-item> No repositories </paper-item>`} : html`<paper-item> No repositories </paper-item>`}
<div class="layout horizontal bottom"> <div class="layout horizontal bottom">
<ha-textfield <paper-input
class="flex-auto" class="flex-auto"
id="repository_input" id="repository_input"
.value=${this._dialogParams!.url || ""} .value=${this._dialogParams!.url || ""}
@@ -154,7 +154,7 @@ class HassioRepositoriesDialog extends LitElement {
)} )}
@keydown=${this._handleKeyAdd} @keydown=${this._handleKeyAdd}
dialogInitialFocus dialogInitialFocus
></ha-textfield> ></paper-input>
<mwc-button @click=${this._addRepository}> <mwc-button @click=${this._addRepository}>
${this._processing ${this._processing
? html`<ha-circular-progress ? html`<ha-circular-progress

View File

@@ -29,10 +29,6 @@ import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { urlSyncMixin } from "../../src/state/url-sync-mixin"; import { urlSyncMixin } from "../../src/state/url-sync-mixin";
import { HomeAssistant, Route } from "../../src/types"; import { HomeAssistant, Route } from "../../src/types";
import { getTranslation } from "../../src/util/common-translation"; import { getTranslation } from "../../src/util/common-translation";
import {
computeRTLDirection,
setDirectionStyles,
} from "../../src/common/util/compute_rtl";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
@@ -99,7 +95,6 @@ export class SupervisorBaseElement extends urlSyncMixin(
if (changedProperties.has("_language") || !this.hasUpdated) { if (changedProperties.has("_language") || !this.hasUpdated) {
this._initializeLocalize(); this._initializeLocalize();
this._applyDirection(this.hass);
} }
} }
@@ -220,9 +215,4 @@ export class SupervisorBaseElement extends urlSyncMixin(
); );
} }
} }
private _applyDirection(hass: HomeAssistant) {
const direction = computeRTLDirection(hass);
setDirectionStyles(direction, this);
}
} }

View File

@@ -2,7 +2,6 @@ export default {
"*.?(c|m){js,ts}": [ "*.?(c|m){js,ts}": [
"eslint --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --fix", "eslint --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --fix",
"prettier --cache --write", "prettier --cache --write",
"lit-analyzer",
], ],
"*.{json,css,md,markdown,html,y?aml}": "prettier --cache --write", "*.{json,css,md,markdown,html,y?aml}": "prettier --cache --write",
"translations/*/*.json": (files) => "translations/*/*.json": (files) =>

View File

@@ -13,8 +13,8 @@
"lint:prettier": "prettier . --cache --check", "lint:prettier": "prettier . --cache --check",
"format:prettier": "prettier . --cache --write", "format:prettier": "prettier . --cache --write",
"lint:types": "tsc", "lint:types": "tsc",
"lint:lit": "lit-analyzer \"{.,*}/src/**/*.ts\"", "lint:lit": "lit-analyzer \"**/src/**/*.ts\" --format markdown --outFile result.md",
"lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:types && yarn run lint:lit", "lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:types",
"format": "yarn run format:eslint && yarn run format:prettier", "format": "yarn run format:eslint && yarn run format:prettier",
"postinstall": "husky install", "postinstall": "husky install",
"prepack": "pinst --disable", "prepack": "pinst --disable",
@@ -25,15 +25,15 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@babel/runtime": "7.23.8", "@babel/runtime": "7.23.5",
"@braintree/sanitize-url": "7.0.0", "@braintree/sanitize-url": "7.0.0",
"@codemirror/autocomplete": "6.11.1", "@codemirror/autocomplete": "6.11.1",
"@codemirror/commands": "6.3.3", "@codemirror/commands": "6.3.2",
"@codemirror/language": "6.10.0", "@codemirror/language": "6.9.3",
"@codemirror/legacy-modes": "6.3.3", "@codemirror/legacy-modes": "6.3.3",
"@codemirror/search": "6.5.5", "@codemirror/search": "6.5.5",
"@codemirror/state": "6.4.0", "@codemirror/state": "6.3.3",
"@codemirror/view": "6.23.0", "@codemirror/view": "6.22.1",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.0", "@formatjs/intl-datetimeformat": "6.12.0",
"@formatjs/intl-displaynames": "6.6.4", "@formatjs/intl-displaynames": "6.6.4",
@@ -80,17 +80,18 @@
"@material/mwc-top-app-bar": "0.27.0", "@material/mwc-top-app-bar": "0.27.0",
"@material/mwc-top-app-bar-fixed": "0.27.0", "@material/mwc-top-app-bar-fixed": "0.27.0",
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0", "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
"@material/web": "=1.1.1", "@material/web": "=1.0.1",
"@mdi/js": "7.4.47", "@mdi/js": "7.3.67",
"@mdi/svg": "7.4.47", "@mdi/svg": "7.3.67",
"@polymer/paper-input": "3.2.1",
"@polymer/paper-item": "3.0.1", "@polymer/paper-item": "3.0.1",
"@polymer/paper-listbox": "3.0.1", "@polymer/paper-listbox": "3.0.1",
"@polymer/paper-tabs": "3.1.0", "@polymer/paper-tabs": "3.1.0",
"@polymer/paper-toast": "3.0.1", "@polymer/paper-toast": "3.0.1",
"@polymer/polymer": "3.5.1", "@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0", "@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.3.2", "@vaadin/combo-box": "24.2.5",
"@vaadin/vaadin-themable-mixin": "24.3.2", "@vaadin/vaadin-themable-mixin": "24.2.5",
"@vibrant/color": "3.2.1-alpha.1", "@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1", "@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -100,16 +101,16 @@
"app-datepicker": "5.1.1", "app-datepicker": "5.1.1",
"chart.js": "4.4.1", "chart.js": "4.4.1",
"comlink": "4.4.1", "comlink": "4.4.1",
"core-js": "3.35.0", "core-js": "3.33.3",
"cropperjs": "1.6.1", "cropperjs": "1.6.1",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"date-fns-tz": "2.0.0", "date-fns-tz": "2.0.0",
"deep-clone-simple": "1.1.1", "deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
"element-internals-polyfill": "1.3.10", "element-internals-polyfill": "1.3.9",
"fuse.js": "7.0.0", "fuse.js": "7.0.0",
"google-timezones-json": "1.2.0", "google-timezones-json": "1.2.0",
"hls.js": "1.4.14", "hls.js": "1.4.13",
"home-assistant-js-websocket": "9.1.0", "home-assistant-js-websocket": "9.1.0",
"idb-keyval": "6.2.1", "idb-keyval": "6.2.1",
"intl-messageformat": "10.5.8", "intl-messageformat": "10.5.8",
@@ -118,7 +119,7 @@
"leaflet-draw": "1.0.4", "leaflet-draw": "1.0.4",
"lit": "2.8.0", "lit": "2.8.0",
"luxon": "3.4.4", "luxon": "3.4.4",
"marked": "11.1.1", "marked": "11.0.1",
"memoize-one": "6.0.0", "memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1", "node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2", "proxy-polyfill": "0.3.2",
@@ -137,7 +138,7 @@
"unfetch": "5.0.0", "unfetch": "5.0.0",
"vis-data": "7.1.9", "vis-data": "7.1.9",
"vis-network": "9.1.9", "vis-network": "9.1.9",
"vue": "2.7.16", "vue": "2.7.15",
"vue2-daterange-picker": "0.6.8", "vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0", "weekstart": "2.0.0",
"workbox-cacheable-response": "7.0.0", "workbox-cacheable-response": "7.0.0",
@@ -149,14 +150,14 @@
"xss": "1.0.14" "xss": "1.0.14"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.23.7", "@babel/core": "7.23.5",
"@babel/helper-define-polyfill-provider": "0.4.4", "@babel/helper-define-polyfill-provider": "0.4.3",
"@babel/plugin-proposal-decorators": "7.23.7", "@babel/plugin-proposal-decorators": "7.23.5",
"@babel/plugin-transform-runtime": "7.23.7", "@babel/plugin-transform-runtime": "7.23.4",
"@babel/preset-env": "7.23.8", "@babel/preset-env": "7.23.5",
"@babel/preset-typescript": "7.23.3", "@babel/preset-typescript": "7.23.3",
"@bundle-stats/plugin-webpack-filter": "4.8.4", "@bundle-stats/plugin-webpack-filter": "4.8.3",
"@koa/cors": "5.0.0", "@koa/cors": "4.0.0",
"@lokalise/node-api": "12.1.0", "@lokalise/node-api": "12.1.0",
"@octokit/auth-oauth-device": "6.0.1", "@octokit/auth-oauth-device": "6.0.1",
"@octokit/plugin-retry": "6.0.1", "@octokit/plugin-retry": "6.0.1",
@@ -164,18 +165,18 @@
"@open-wc/dev-server-hmr": "0.1.4", "@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.4", "@rollup/plugin-babel": "6.0.4",
"@rollup/plugin-commonjs": "25.0.7", "@rollup/plugin-commonjs": "25.0.7",
"@rollup/plugin-json": "6.1.0", "@rollup/plugin-json": "6.0.1",
"@rollup/plugin-node-resolve": "15.2.3", "@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-replace": "5.0.5", "@rollup/plugin-replace": "5.0.5",
"@types/babel__plugin-transform-runtime": "7.9.5", "@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.13", "@types/chromecast-caf-receiver": "6.0.12",
"@types/chromecast-caf-sender": "1.0.8", "@types/chromecast-caf-sender": "1.0.8",
"@types/glob": "8.1.0", "@types/glob": "8.1.0",
"@types/html-minifier-terser": "7.0.2", "@types/html-minifier-terser": "7.0.2",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/leaflet": "1.9.8", "@types/leaflet": "1.9.8",
"@types/leaflet-draw": "1.0.11", "@types/leaflet-draw": "1.0.11",
"@types/luxon": "3.4.0", "@types/luxon": "3.3.7",
"@types/mocha": "10.0.6", "@types/mocha": "10.0.6",
"@types/qrcode": "1.5.5", "@types/qrcode": "1.5.5",
"@types/serve-handler": "6.1.4", "@types/serve-handler": "6.1.4",
@@ -183,22 +184,22 @@
"@types/tar": "6.1.10", "@types/tar": "6.1.10",
"@types/ua-parser-js": "0.7.39", "@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29", "@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "6.18.1", "@typescript-eslint/eslint-plugin": "6.13.2",
"@typescript-eslint/parser": "6.18.1", "@typescript-eslint/parser": "6.13.2",
"@web/dev-server": "0.1.38", "@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1", "@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3", "babel-loader": "9.1.3",
"babel-plugin-template-html-minifier": "4.1.0", "babel-plugin-template-html-minifier": "4.1.0",
"chai": "5.0.0", "chai": "4.3.10",
"del": "7.1.0", "del": "7.1.0",
"eslint": "8.56.0", "eslint": "8.55.0",
"eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.1.0", "eslint-config-airbnb-typescript": "17.1.0",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "9.1.0",
"eslint-import-resolver-webpack": "0.13.8", "eslint-import-resolver-webpack": "0.13.8",
"eslint-plugin-disable": "2.0.3", "eslint-plugin-disable": "2.0.3",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.29.0",
"eslint-plugin-lit": "1.11.0", "eslint-plugin-lit": "1.10.1",
"eslint-plugin-lit-a11y": "4.1.1", "eslint-plugin-lit-a11y": "4.1.1",
"eslint-plugin-unused-imports": "3.0.0", "eslint-plugin-unused-imports": "3.0.0",
"eslint-plugin-wc": "2.0.4", "eslint-plugin-wc": "2.0.4",
@@ -216,27 +217,26 @@
"instant-mocha": "1.5.2", "instant-mocha": "1.5.2",
"jszip": "3.10.1", "jszip": "3.10.1",
"lint-staged": "15.2.0", "lint-staged": "15.2.0",
"lit-analyzer": "2.0.3", "lit-analyzer": "2.0.1",
"lodash.template": "4.5.0", "lodash.template": "4.5.0",
"magic-string": "0.30.5", "magic-string": "0.30.5",
"map-stream": "0.0.7", "map-stream": "0.0.7",
"mocha": "10.2.0", "mocha": "10.2.0",
"object-hash": "3.0.0", "object-hash": "3.0.0",
"open": "10.0.3", "open": "9.1.0",
"pinst": "3.0.0", "pinst": "3.0.0",
"prettier": "3.1.1", "prettier": "3.1.0",
"rollup": "2.79.1", "rollup": "2.79.1",
"rollup-plugin-string": "3.0.0", "rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2", "rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.12.0", "rollup-plugin-visualizer": "5.10.0",
"serve-handler": "6.1.5", "serve-handler": "6.1.5",
"sinon": "17.0.1", "sinon": "17.0.1",
"source-map-url": "0.4.1", "source-map-url": "0.4.1",
"systemjs": "6.14.3", "systemjs": "6.14.2",
"tar": "6.2.0", "tar": "6.2.0",
"terser-webpack-plugin": "5.3.10", "terser-webpack-plugin": "5.3.9",
"transform-async-modules-webpack-plugin": "1.0.2", "ts-lit-plugin": "2.0.1",
"ts-lit-plugin": "2.0.2",
"typescript": "5.3.3", "typescript": "5.3.3",
"vinyl-buffer": "1.0.1", "vinyl-buffer": "1.0.1",
"vinyl-source-stream": "2.0.0", "vinyl-source-stream": "2.0.0",
@@ -245,7 +245,7 @@
"webpack-dev-server": "4.15.1", "webpack-dev-server": "4.15.1",
"webpack-manifest-plugin": "5.0.0", "webpack-manifest-plugin": "5.0.0",
"webpack-stats-plugin": "1.1.3", "webpack-stats-plugin": "1.1.3",
"webpackbar": "6.0.0", "webpackbar": "5.0.2",
"workbox-build": "7.0.0" "workbox-build": "7.0.0"
}, },
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch", "_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",

View File

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

View File

@@ -1,10 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
/* eslint-disable no-console */ const fs = require("fs");
import fs from "fs"; const util = require("util");
import util from "util"; const exec = util.promisify(require("child_process").exec);
import child_process from "child_process";
const exec = util.promisify(child_process.exec);
function patch(version) { function patch(version) {
const parts = version.split("."); const parts = version.split(".");
@@ -21,7 +18,7 @@ function today() {
function auto(version) { function auto(version) {
const todayVersion = today(); const todayVersion = today();
if (todayVersion.split(".")[0] !== version.split(".")[0]) { if (todayVersion !== version) {
return todayVersion; return todayVersion;
} }
return patch(version); return patch(version);
@@ -47,7 +44,7 @@ async function main(args) {
commit = true; commit = true;
} else { } else {
method = args.length > 0 && methods[args[0]]; method = args.length > 0 && methods[args[0]];
commit = args.length > 1 && args[1] === "--commit"; commit = args.length > 1 && args[1] == "--commit";
} }
if (!method) { if (!method) {

View File

@@ -21,6 +21,7 @@ import {
DataEntryFlowStepForm, DataEntryFlowStepForm,
} from "../data/data_entry_flow"; } from "../data/data_entry_flow";
import "./ha-auth-form"; import "./ha-auth-form";
import { fireEvent } from "../common/dom/fire_event";
type State = "loading" | "error" | "step"; type State = "loading" | "error" | "step";
@@ -38,9 +39,7 @@ export class HaAuthFlow extends LitElement {
@property({ attribute: false }) public step?: DataEntryFlowStep; @property({ attribute: false }) public step?: DataEntryFlowStep;
@property({ type: Boolean }) private initStoreToken = false; @property({ type: Boolean }) private storeToken = false;
@state() private _storeToken = false;
@state() private _state: State = "loading"; @state() private _state: State = "loading";
@@ -57,10 +56,6 @@ export class HaAuthFlow extends LitElement {
willUpdate(changedProps: PropertyValues) { willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps); super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._storeToken = this.initStoreToken;
}
if (!changedProps.has("step")) { if (!changedProps.has("step")) {
return; return;
} }
@@ -160,6 +155,11 @@ export class HaAuthFlow extends LitElement {
} }
private _renderForm() { private _renderForm() {
const showBack =
this.step?.type === "form" &&
this.authProvider?.users &&
!["select_mfa_module", "mfa"].includes(this.step.step_id);
switch (this._state) { switch (this._state) {
case "step": case "step":
if (this.step == null) { if (this.step == null) {
@@ -168,7 +168,12 @@ export class HaAuthFlow extends LitElement {
return html` return html`
${this._renderStep(this.step)} ${this._renderStep(this.step)}
<div class="action"> <div class="action ${showBack ? "space-between" : ""}">
${showBack
? html`<mwc-button @click=${this._localFlow}>
${this.localize("ui.panel.page-authorize.form.previous")}
</mwc-button>`
: nothing}
<mwc-button <mwc-button
raised raised
@click=${this._handleSubmit} @click=${this._handleSubmit}
@@ -222,7 +227,7 @@ export class HaAuthFlow extends LitElement {
</h1> </h1>
${this._computeStepDescription(step)} ${this._computeStepDescription(step)}
<ha-auth-form <ha-auth-form
.data=${this._stepData!} .data=${this._stepData}
.schema=${autocompleteLoginFields(step.data_schema)} .schema=${autocompleteLoginFields(step.data_schema)}
.error=${step.errors} .error=${step.errors}
.disabled=${this._submitting} .disabled=${this._submitting}
@@ -241,7 +246,7 @@ export class HaAuthFlow extends LitElement {
)} )}
> >
<ha-checkbox <ha-checkbox
.checked=${this._storeToken} .checked=${this.storeToken}
@change=${this._storeTokenChanged} @change=${this._storeTokenChanged}
></ha-checkbox> ></ha-checkbox>
</ha-formfield> </ha-formfield>
@@ -264,7 +269,7 @@ export class HaAuthFlow extends LitElement {
} }
private _storeTokenChanged(e: CustomEvent<HTMLInputElement>) { private _storeTokenChanged(e: CustomEvent<HTMLInputElement>) {
this._storeToken = (e.currentTarget as HTMLInputElement).checked; this.storeToken = (e.currentTarget as HTMLInputElement).checked;
} }
private async _providerChanged(newProvider?: AuthProvider) { private async _providerChanged(newProvider?: AuthProvider) {
@@ -298,7 +303,7 @@ export class HaAuthFlow extends LitElement {
this.redirectUri!, this.redirectUri!,
data.result, data.result,
this.oauth2State, this.oauth2State,
this._storeToken this.storeToken
); );
return; return;
} }
@@ -380,7 +385,7 @@ export class HaAuthFlow extends LitElement {
this.redirectUri!, this.redirectUri!,
newStep.result, newStep.result,
this.oauth2State, this.oauth2State,
this._storeToken this.storeToken
); );
return; return;
} }
@@ -395,6 +400,10 @@ export class HaAuthFlow extends LitElement {
this._submitting = false; this._submitting = false;
} }
} }
private _localFlow() {
fireEvent(this, "default-login-flow", { value: false });
}
} }
declare global { declare global {

View File

@@ -47,7 +47,7 @@ export class HaAuthTextField extends HaTextField {
// TODO: live() directive needs casting for lit-analyzer // TODO: live() directive needs casting for lit-analyzer
// https://github.com/runem/lit-analyzer/pull/91/files // https://github.com/runem/lit-analyzer/pull/91/files
// TODO: lit-analyzer labels min/max as (number|string) instead of string // TODO: lit-analyzer labels min/max as (number|string) instead of string
return html`<input return html` <input
aria-labelledby=${ifDefined(ariaLabelledbyOrUndef)} aria-labelledby=${ifDefined(ariaLabelledbyOrUndef)}
aria-controls=${ifDefined(ariaControlsOrUndef)} aria-controls=${ifDefined(ariaControlsOrUndef)}
aria-describedby=${ifDefined(ariaDescribedbyOrUndef)} aria-describedby=${ifDefined(ariaDescribedbyOrUndef)}

View File

@@ -13,6 +13,7 @@ import {
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin"; import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
import { registerServiceWorker } from "../util/register-service-worker"; import { registerServiceWorker } from "../util/register-service-worker";
import "./ha-auth-flow"; import "./ha-auth-flow";
import "./ha-local-auth-flow";
import("./ha-pick-auth-provider"); import("./ha-pick-auth-provider");
@@ -35,12 +36,12 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
@state() private _authProviders?: AuthProvider[]; @state() private _authProviders?: AuthProvider[];
@state() private _preselectStoreToken = false;
@state() private _ownInstance = false; @state() private _ownInstance = false;
@state() private _error?: string; @state() private _error?: string;
@state() private _forceDefaultLogin = false;
constructor() { constructor() {
super(); super();
const query = extractSearchParamsObject() as AuthUrlSearchParams; const query = extractSearchParamsObject() as AuthUrlSearchParams;
@@ -83,7 +84,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
display: block; display: block;
margin-top: 24px; margin-top: 24px;
} }
ha-auth-flow { ha-auth-flow,
ha-local-auth-flow {
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
@@ -174,29 +176,44 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
</ha-alert>` </ha-alert>`
: nothing} : nothing}
<div class="card-content"> <div
class="card-content"
@default-login-flow=${this._handleDefaultLoginFlow}
>
${!this._authProvider ${!this._authProvider
? html`<p> ? html`<p>
${this.localize("ui.panel.page-authorize.initializing")} ${this.localize("ui.panel.page-authorize.initializing")}
</p> ` </p> `
: html`<ha-auth-flow : !this._forceDefaultLogin &&
this._authProvider!.users &&
this.clientId != null &&
this.redirectUri != null
? html`<ha-local-auth-flow
.clientId=${this.clientId} .clientId=${this.clientId}
.redirectUri=${this.redirectUri} .redirectUri=${this.redirectUri}
.oauth2State=${this.oauth2State} .oauth2State=${this.oauth2State}
.authProvider=${this._authProvider} .authProvider=${this._authProvider}
.authProviders=${this._authProviders}
.localize=${this.localize} .localize=${this.localize}
.initStoreToken=${this._preselectStoreToken} .ownInstance=${this._ownInstance}
></ha-auth-flow> ></ha-local-auth-flow>`
${inactiveProviders!.length > 0 : html`<ha-auth-flow
? html` .clientId=${this.clientId}
<ha-pick-auth-provider .redirectUri=${this.redirectUri}
.localize=${this.localize} .oauth2State=${this.oauth2State}
.clientId=${this.clientId} .authProvider=${this._authProvider}
.authProviders=${inactiveProviders!} .localize=${this.localize}
@pick-auth-provider=${this._handleAuthProviderPick} ></ha-auth-flow>
></ha-pick-auth-provider> ${inactiveProviders!.length > 0
` ? html`
: ""}`} <ha-pick-auth-provider
.localize=${this.localize}
.clientId=${this.clientId}
.authProviders=${inactiveProviders}
@pick-auth-provider=${this._handleAuthProviderPick}
></ha-pick-auth-provider>
`
: ""}`}
</div> </div>
<div class="footer"> <div class="footer">
<ha-language-picker <ha-language-picker
@@ -302,14 +319,13 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
return; return;
} }
if (authProviders.providers.length === 0) { if (authProviders.length === 0) {
this._error = "No auth providers returned. Unable to finish login."; this._error = "No auth providers returned. Unable to finish login.";
return; return;
} }
this._authProviders = authProviders.providers; this._authProviders = authProviders;
this._authProvider = authProviders.providers[0]; this._authProvider = authProviders[0];
this._preselectStoreToken = authProviders.preselect_remember_me;
} catch (err: any) { } catch (err: any) {
this._error = "Unable to fetch auth providers."; this._error = "Unable to fetch auth providers.";
// eslint-disable-next-line // eslint-disable-next-line
@@ -317,6 +333,10 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
} }
} }
private _handleDefaultLoginFlow(ev) {
this._forceDefaultLogin = ev.detail.value;
}
private async _handleAuthProviderPick(ev) { private async _handleAuthProviderPick(ev) {
this._authProvider = ev.detail; this._authProvider = ev.detail;
} }
@@ -332,9 +352,3 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
} }
} }
} }
declare global {
interface HTMLElementTagNameMap {
"ha-authorize": HaAuthorize;
}
}

View File

@@ -0,0 +1,485 @@
/* eslint-disable lit/prefer-static-styles */
import "@material/mwc-button";
import { mdiEye, mdiEyeOff } from "@mdi/js";
import { html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-alert";
import "../components/ha-button";
import "../components/ha-icon-button";
import "../components/user/ha-person-badge";
import {
AuthProvider,
createLoginFlow,
deleteLoginFlow,
redirectWithAuthCode,
submitLoginFlow,
} from "../data/auth";
import { DataEntryFlowStep } from "../data/data_entry_flow";
import { BasePerson, listUserPersons } from "../data/person";
import "./ha-auth-textfield";
import type { HaAuthTextField } from "./ha-auth-textfield";
@customElement("ha-local-auth-flow")
export class HaLocalAuthFlow extends LitElement {
@property({ attribute: false }) public authProvider?: AuthProvider;
@property({ attribute: false }) public authProviders?: AuthProvider[];
@property() public clientId?: string;
@property() public redirectUri?: string;
@property() public oauth2State?: string;
@property({ type: Boolean }) public ownInstance = false;
@property() public localize!: LocalizeFunc;
@state() private _error?: string;
@state() private _step?: DataEntryFlowStep;
@state() private _submitting = false;
@state() private _persons?: Record<string, BasePerson>;
@state() private _selectedUser?: string;
@state() private _unmaskedPassword = false;
createRenderRoot() {
return this;
}
willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._load();
}
}
protected render() {
if (!this.authProvider?.users || !this._persons) {
return nothing;
}
const userIds = Object.keys(this.authProvider.users).filter(
(userId) => userId in this._persons!
);
return html`
<style>
.content {
max-width: 560px;
}
.persons {
margin-top: 24px;
display: flex;
flex-wrap: wrap;
gap: 16px;
justify-content: center;
}
.persons.force-small {
max-width: 350px;
}
.person {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
text-align: center;
cursor: pointer;
width: 80px;
}
.person[role="button"] {
outline: none;
padding: 8px;
border-radius: 4px;
}
.person[role="button"]:focus-visible {
background: rgba(var(--rgb-primary-color), 0.1);
}
.person p {
margin-bottom: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
}
ha-person-badge {
width: 80px;
height: 80px;
--person-badge-font-size: 2em;
}
form {
width: 100%;
}
ha-auth-textfield {
display: block !important;
position: relative;
}
ha-auth-textfield ha-icon-button {
position: absolute;
top: 4px;
right: 4px;
z-index: 9;
}
.login-form {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 336px;
margin-top: 24px;
}
.login-form .person {
cursor: default;
width: auto;
}
.login-form .person p {
font-size: 28px;
margin-top: 24px;
margin-bottom: 32px;
line-height: normal;
}
.login-form ha-person-badge {
width: 120px;
height: 120px;
--person-badge-font-size: 3em;
}
ha-list-item {
margin-top: 16px;
}
ha-button {
--mdc-typography-button-text-transform: none;
}
.forgot-password-container {
text-align: right;
padding: 8px 0 16px 0;
}
a.forgot-password {
color: var(--primary-color);
text-decoration: none;
font-size: 0.875rem;
}
button {
color: var(--primary-color);
background: none;
border: none;
padding: 8px;
font: inherit;
font-size: 0.875rem;
text-align: left;
cursor: pointer;
outline: none;
border-radius: 4px;
}
button:focus-visible {
background: rgba(var(--rgb-primary-color), 0.1);
}
</style>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
${this._step
? html`<ha-auth-flow
.clientId=${this.clientId}
.redirectUri=${this.redirectUri}
.oauth2State=${this.oauth2State}
.step=${this._step}
storeToken
.localize=${this.localize}
></ha-auth-flow>`
: this._selectedUser
? html`<div class="login-form">
<div class="person">
<ha-person-badge
.person=${this._persons[this._selectedUser]}
></ha-person-badge>
<p>${this._persons[this._selectedUser].name}</p>
</div>
<form>
<input
type="hidden"
name="username"
autocomplete="username"
readonly
.value=${this.authProvider.users[this._selectedUser]}
/>
<ha-auth-textfield
.type=${this._unmaskedPassword ? "text" : "password"}
autocomplete="current-password"
id="password"
name="password"
.label=${this.localize(
"ui.panel.page-authorize.form.providers.homeassistant.step.init.data.password"
)}
required
autoValidate
iconTrailing
validationMessage="Required"
>
<ha-icon-button
toggles
.label=${this.localize(
this._unmaskedPassword
? "ui.panel.page-authorize.form.hide_password"
: "ui.panel.page-authorize.form.show_password"
) ||
(this._unmaskedPassword
? "Hide password"
: "Show password")}
@click=${this._toggleUnmaskedPassword}
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
></ha-icon-button>
</ha-auth-textfield>
<div class="forgot-password-container">
<a
class="forgot-password"
href="https://www.home-assistant.io/docs/locked_out/#forgot-password"
target="_blank"
rel="noreferrer noopener"
>${this.localize(
"ui.panel.page-authorize.forgot_password"
)}</a
>
</div>
<div class="action space-between">
<mwc-button
@click=${this._restart}
.disabled=${this._submitting}
>
${this.localize("ui.panel.page-authorize.form.previous")}
</mwc-button>
<mwc-button
raised
@click=${this._handleSubmit}
.disabled=${this._submitting}
>
${this.localize("ui.panel.page-authorize.form.next")}
</mwc-button>
</div>
</form>
</div>`
: html`<h1>
${this.localize("ui.panel.page-authorize.welcome_home")}
</h1>
${this.localize("ui.panel.page-authorize.who_is_logging_in")}
<div
class="persons ${userIds.length < 10 && userIds.length % 4 === 1
? "force-small"
: ""}"
>
${userIds.map((userId) => {
const person = this._persons![userId];
return html`<div
class="person"
.userId=${userId}
@click=${this._personSelected}
@keyup=${this._handleKeyUp}
role="button"
tabindex="0"
>
<ha-person-badge .person=${person}></ha-person-badge>
<p>${person.name}</p>
</div>`;
})}
</div>
<div class="action">
<button @click=${this._otherLogin} tabindex="0">
${this.localize("ui.panel.page-authorize.other_options")}
</button>
</div>`}
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.addEventListener("keypress", (ev) => {
if (ev.key === "Enter") {
this._handleSubmit(ev);
}
});
}
protected updated(changedProps: PropertyValues) {
if (changedProps.has("_selectedUser") && this._selectedUser) {
const passwordElement = this.renderRoot.querySelector(
"#password"
) as HaAuthTextField;
passwordElement.updateComplete.then(() => {
passwordElement.focus();
});
}
}
private async _load() {
try {
this._persons = await listUserPersons();
} catch {
this._persons = {};
this._error = "Failed to fetch persons";
}
}
private _restart() {
this._selectedUser = undefined;
this._error = undefined;
}
private _toggleUnmaskedPassword() {
this._unmaskedPassword = !this._unmaskedPassword;
}
private _handleKeyUp(ev: KeyboardEvent) {
if (ev.key === "Enter" || ev.key === " ") {
this._personSelected(ev);
}
}
private async _personSelected(ev) {
const userId = ev.currentTarget.userId;
if (
this.ownInstance &&
this.authProviders?.find((prv) => prv.type === "trusted_networks")
) {
try {
const flowResponse = await createLoginFlow(
this.clientId,
this.redirectUri,
["trusted_networks", null]
);
const data = await flowResponse.json();
if (data.type === "create_entry") {
redirectWithAuthCode(
this.redirectUri!,
data.result,
this.oauth2State,
true
);
return;
}
try {
if (!data.data_schema[0].options.find((opt) => opt[0] === userId)) {
throw new Error("User not available");
}
const postData = { user: userId, client_id: this.clientId };
const response = await submitLoginFlow(data.flow_id, postData);
if (response.ok) {
const result = await response.json();
if (result.type === "create_entry") {
redirectWithAuthCode(
this.redirectUri!,
result.result,
this.oauth2State,
true
);
return;
}
} else {
throw new Error("Invalid response");
}
} catch {
deleteLoginFlow(data.flow_id).catch((err) => {
// eslint-disable-next-line no-console
console.error("Error delete obsoleted auth flow", err);
});
}
} catch {
// Ignore
}
}
this._selectedUser = userId;
}
private async _handleSubmit(ev: Event) {
ev.preventDefault();
if (!this.authProvider?.users || !this._selectedUser) {
return;
}
this._error = undefined;
this._submitting = true;
const flowResponse = await createLoginFlow(
this.clientId,
this.redirectUri,
["homeassistant", null]
);
const data = await flowResponse.json();
const postData = {
username: this.authProvider.users[this._selectedUser],
password: (this.renderRoot.querySelector("#password") as HaAuthTextField)
.value,
client_id: this.clientId,
};
try {
const response = await submitLoginFlow(data.flow_id, postData);
const newStep = await response.json();
if (response.status === 403) {
this._error = newStep.message;
return;
}
if (newStep.type === "create_entry") {
redirectWithAuthCode(
this.redirectUri!,
newStep.result,
this.oauth2State,
true
);
return;
}
if (newStep.errors.base) {
this._error = this.localize(
`ui.panel.page-authorize.form.providers.homeassistant.error.${newStep.errors.base}`
);
throw new Error(this._error);
}
this._step = newStep;
} catch {
deleteLoginFlow(data.flow_id).catch((err) => {
// eslint-disable-next-line no-console
console.error("Error delete obsoleted auth flow", err);
});
if (!this._error) {
this._error = this.localize(
"ui.panel.page-authorize.form.unknown_error"
);
}
} finally {
this._submitting = false;
}
}
private _otherLogin() {
fireEvent(this, "default-login-flow", { value: true });
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-local-auth-flow": HaLocalAuthFlow;
}
interface HASSDomEvents {
"default-login-flow": { value: boolean };
}
}

View File

@@ -8,9 +8,6 @@ import "../components/ha-list-item";
import { AuthProvider } from "../data/auth"; import { AuthProvider } from "../data/auth";
declare global { declare global {
interface HTMLElementTagNameMap {
"ha-pick-auth-provider": HaPickAuthProvider;
}
interface HASSDomEvents { interface HASSDomEvents {
"pick-auth-provider": AuthProvider; "pick-auth-provider": AuthProvider;
} }

View File

@@ -29,7 +29,6 @@ import {
mdiFlash, mdiFlash,
mdiFlower, mdiFlower,
mdiFormatListBulleted, mdiFormatListBulleted,
mdiFormatListCheckbox,
mdiFormTextbox, mdiFormTextbox,
mdiGauge, mdiGauge,
mdiGoogleAssistant, mdiGoogleAssistant,
@@ -65,7 +64,6 @@ import {
mdiTransmissionTower, mdiTransmissionTower,
mdiWater, mdiWater,
mdiWaterPercent, mdiWaterPercent,
mdiWeatherPartlyCloudy,
mdiWeatherPouring, mdiWeatherPouring,
mdiWeatherRainy, mdiWeatherRainy,
mdiWeatherWindy, mdiWeatherWindy,
@@ -130,7 +128,6 @@ export const FIXED_DOMAIN_ICONS = {
updater: mdiCloudUpload, updater: mdiCloudUpload,
vacuum: mdiRobotVacuum, vacuum: mdiRobotVacuum,
wake_word: mdiChatSleep, wake_word: mdiChatSleep,
weather: mdiWeatherPartlyCloudy,
zone: mdiMapMarkerRadius, zone: mdiMapMarkerRadius,
}; };
@@ -169,7 +166,6 @@ export const FIXED_DEVICE_CLASS_ICONS = {
precipitation_intensity: mdiWeatherPouring, precipitation_intensity: mdiWeatherPouring,
pressure: mdiGauge, pressure: mdiGauge,
reactive_power: mdiFlash, reactive_power: mdiFlash,
shopping_List: mdiFormatListCheckbox,
signal_strength: mdiWifi, signal_strength: mdiWifi,
sound_pressure: mdiEarHearing, sound_pressure: mdiEarHearing,
speed: mdiSpeedometer, speed: mdiSpeedometer,
@@ -254,7 +250,6 @@ export const DOMAINS_INPUT_ROW = [
"text", "text",
"time", "time",
"vacuum", "vacuum",
"valve",
]; ];
/** States that we consider "off". */ /** States that we consider "off". */
@@ -273,7 +268,6 @@ export const DOMAINS_TOGGLE = new Set([
"group", "group",
"automation", "automation",
"humidifier", "humidifier",
"valve",
]); ]);
/** Domains that have a dynamic entity image / picture. */ /** Domains that have a dynamic entity image / picture. */

View File

@@ -1,8 +1,7 @@
import { HassConfig } from "home-assistant-js-websocket"; import { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { DateFormat, FrontendLocaleData } from "../../data/translation"; import { FrontendLocaleData, DateFormat } from "../../data/translation";
import "../../resources/intl-polyfill"; import "../../resources/intl-polyfill";
import { resolveTimeZone } from "./resolve-time-zone";
// Tuesday, August 10 // Tuesday, August 10
export const formatDateWeekdayDay = ( export const formatDateWeekdayDay = (
@@ -17,7 +16,7 @@ const formatDateWeekdayDayMem = memoizeOne(
weekday: "long", weekday: "long",
month: "long", month: "long",
day: "numeric", day: "numeric",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
}) })
); );
@@ -34,7 +33,7 @@ const formatDateMem = memoizeOne(
year: "numeric", year: "numeric",
month: "long", month: "long",
day: "numeric", day: "numeric",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
}) })
); );
@@ -51,7 +50,7 @@ const formatDateShortMem = memoizeOne(
year: "numeric", year: "numeric",
month: "short", month: "short",
day: "numeric", day: "numeric",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
}) })
); );
@@ -106,7 +105,7 @@ const formatDateNumericMem = memoizeOne(
year: "numeric", year: "numeric",
month: "numeric", month: "numeric",
day: "numeric", day: "numeric",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
}); });
} }
@@ -114,7 +113,7 @@ const formatDateNumericMem = memoizeOne(
year: "numeric", year: "numeric",
month: "numeric", month: "numeric",
day: "numeric", day: "numeric",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
}); });
} }
); );
@@ -131,7 +130,7 @@ const formatDateVeryShortMem = memoizeOne(
new Intl.DateTimeFormat(locale.language, { new Intl.DateTimeFormat(locale.language, {
day: "numeric", day: "numeric",
month: "short", month: "short",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
}) })
); );
@@ -147,7 +146,7 @@ const formatDateMonthYearMem = memoizeOne(
new Intl.DateTimeFormat(locale.language, { new Intl.DateTimeFormat(locale.language, {
month: "long", month: "long",
year: "numeric", year: "numeric",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
}) })
); );
@@ -162,7 +161,7 @@ const formatDateMonthMem = memoizeOne(
(locale: FrontendLocaleData, serverTimeZone: string) => (locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, { new Intl.DateTimeFormat(locale.language, {
month: "long", month: "long",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
}) })
); );
@@ -177,7 +176,7 @@ const formatDateYearMem = memoizeOne(
(locale: FrontendLocaleData, serverTimeZone: string) => (locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, { new Intl.DateTimeFormat(locale.language, {
year: "numeric", year: "numeric",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
}) })
); );
@@ -192,7 +191,7 @@ const formatDateWeekdayMem = memoizeOne(
(locale: FrontendLocaleData, serverTimeZone: string) => (locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, { new Intl.DateTimeFormat(locale.language, {
weekday: "long", weekday: "long",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
}) })
); );
@@ -207,6 +206,6 @@ const formatDateWeekdayShortMem = memoizeOne(
(locale: FrontendLocaleData, serverTimeZone: string) => (locale: FrontendLocaleData, serverTimeZone: string) =>
new Intl.DateTimeFormat(locale.language, { new Intl.DateTimeFormat(locale.language, {
weekday: "short", weekday: "short",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
}) })
); );

View File

@@ -4,7 +4,6 @@ import { FrontendLocaleData } from "../../data/translation";
import "../../resources/intl-polyfill"; import "../../resources/intl-polyfill";
import { formatDateNumeric } from "./format_date"; import { formatDateNumeric } from "./format_date";
import { formatTime } from "./format_time"; import { formatTime } from "./format_time";
import { resolveTimeZone } from "./resolve-time-zone";
import { useAmPm } from "./use_am_pm"; import { useAmPm } from "./use_am_pm";
// August 9, 2021, 8:23 AM // August 9, 2021, 8:23 AM
@@ -23,7 +22,7 @@ const formatDateTimeMem = memoizeOne(
hour: useAmPm(locale) ? "numeric" : "2-digit", hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "2-digit", minute: "2-digit",
hourCycle: useAmPm(locale) ? "h12" : "h23", hourCycle: useAmPm(locale) ? "h12" : "h23",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
}) })
); );
@@ -43,7 +42,7 @@ const formatShortDateTimeWithYearMem = memoizeOne(
hour: useAmPm(locale) ? "numeric" : "2-digit", hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "2-digit", minute: "2-digit",
hourCycle: useAmPm(locale) ? "h12" : "h23", hourCycle: useAmPm(locale) ? "h12" : "h23",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
}) })
); );
@@ -62,7 +61,7 @@ const formatShortDateTimeMem = memoizeOne(
hour: useAmPm(locale) ? "numeric" : "2-digit", hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "2-digit", minute: "2-digit",
hourCycle: useAmPm(locale) ? "h12" : "h23", hourCycle: useAmPm(locale) ? "h12" : "h23",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
}) })
); );
@@ -83,7 +82,7 @@ const formatDateTimeWithSecondsMem = memoizeOne(
minute: "2-digit", minute: "2-digit",
second: "2-digit", second: "2-digit",
hourCycle: useAmPm(locale) ? "h12" : "h23", hourCycle: useAmPm(locale) ? "h12" : "h23",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
}) })
); );

View File

@@ -2,7 +2,6 @@ import { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { FrontendLocaleData } from "../../data/translation"; import { FrontendLocaleData } from "../../data/translation";
import "../../resources/intl-polyfill"; import "../../resources/intl-polyfill";
import { resolveTimeZone } from "./resolve-time-zone";
import { useAmPm } from "./use_am_pm"; import { useAmPm } from "./use_am_pm";
// 9:15 PM || 21:15 // 9:15 PM || 21:15
@@ -18,7 +17,7 @@ const formatTimeMem = memoizeOne(
hour: "numeric", hour: "numeric",
minute: "2-digit", minute: "2-digit",
hourCycle: useAmPm(locale) ? "h12" : "h23", hourCycle: useAmPm(locale) ? "h12" : "h23",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
}) })
); );
@@ -36,7 +35,7 @@ const formatTimeWithSecondsMem = memoizeOne(
minute: "2-digit", minute: "2-digit",
second: "2-digit", second: "2-digit",
hourCycle: useAmPm(locale) ? "h12" : "h23", hourCycle: useAmPm(locale) ? "h12" : "h23",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
}) })
); );
@@ -54,7 +53,7 @@ const formatTimeWeekdayMem = memoizeOne(
hour: useAmPm(locale) ? "numeric" : "2-digit", hour: useAmPm(locale) ? "numeric" : "2-digit",
minute: "2-digit", minute: "2-digit",
hourCycle: useAmPm(locale) ? "h12" : "h23", hourCycle: useAmPm(locale) ? "h12" : "h23",
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
}) })
); );
@@ -72,6 +71,6 @@ const formatTime24hMem = memoizeOne(
hour: "numeric", hour: "numeric",
minute: "2-digit", minute: "2-digit",
hour12: false, hour12: false,
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
}) })
); );

View File

@@ -1,32 +0,0 @@
import memoizeOne from "memoize-one";
import "../../resources/intl-polyfill";
export const localizeWeekdays = memoizeOne(
(language: string, short: boolean): string[] => {
const days: string[] = [];
const format = new Intl.DateTimeFormat(language, {
weekday: short ? "short" : "long",
timeZone: "UTC",
});
for (let i = 0; i < 7; i++) {
const date = new Date(Date.UTC(1970, 0, 1 + 3 + i));
days.push(format.format(date));
}
return days;
}
);
export const localizeMonths = memoizeOne(
(language: string, short: boolean): string[] => {
const months: string[] = [];
const format = new Intl.DateTimeFormat(language, {
month: short ? "short" : "long",
timeZone: "UTC",
});
for (let i = 0; i < 12; i++) {
const date = new Date(Date.UTC(1970, 0 + i, 1));
months.push(format.format(date));
}
return months;
}
);

View File

@@ -1,15 +0,0 @@
import { TimeZone } from "../../data/translation";
// Browser time zone can be determined from Intl, with fallback to UTC for polyfill or no support.
// Alternatively, we could fallback to a fixed offset IANA zone (e.g. "Etc/GMT+5") using
// Date.prototype.getTimeOffset(), but IANA only has whole hour Etc zones, and problems
// might occur with relative time due to DST.
// Use optional chain instead of polyfill import since polyfill will always return UTC
export const LOCAL_TIME_ZONE =
Intl.DateTimeFormat?.().resolvedOptions?.().timeZone ?? "UTC";
// Pick time zone based on user profile option. Core zone is used when local cannot be determined.
export const resolveTimeZone = (option: TimeZone, serverTimeZone: string) =>
option === TimeZone.local && LOCAL_TIME_ZONE !== "UTC"
? LOCAL_TIME_ZONE
: serverTimeZone;

View File

@@ -28,12 +28,10 @@ import {
mdiLockAlert, mdiLockAlert,
mdiLockClock, mdiLockClock,
mdiLockOpen, mdiLockOpen,
mdiMeterGas,
mdiMotionSensor, mdiMotionSensor,
mdiPackage, mdiPackage,
mdiPackageDown, mdiPackageDown,
mdiPackageUp, mdiPackageUp,
mdiPipeValve,
mdiPowerPlug, mdiPowerPlug,
mdiPowerPlugOff, mdiPowerPlugOff,
mdiRestart, mdiRestart,
@@ -276,16 +274,6 @@ export const domainIconWithoutDefault = (
: mdiPackageUp : mdiPackageUp
: mdiPackage; : mdiPackage;
case "valve":
switch (stateObj?.attributes.device_class) {
case "water":
return mdiPipeValve;
case "gas":
return mdiMeterGas;
default:
return mdiPipeValve;
}
case "water_heater": case "water_heater":
return compareState === "off" ? mdiWaterBoilerOff : mdiWaterBoiler; return compareState === "off" ? mdiWaterBoilerOff : mdiWaterBoiler;

View File

@@ -50,7 +50,6 @@ export const FIXED_DOMAIN_STATES = {
timer: ["active", "idle", "paused"], timer: ["active", "idle", "paused"],
update: ["on", "off"], update: ["on", "off"],
vacuum: ["cleaning", "docked", "error", "idle", "paused", "returning"], vacuum: ["cleaning", "docked", "error", "idle", "paused", "returning"],
valve: ["closed", "closing", "open", "opening"],
weather: [ weather: [
"clear-night", "clear-night",
"cloudy", "cloudy",

View File

@@ -42,8 +42,6 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
return compareState !== "standby"; return compareState !== "standby";
case "vacuum": case "vacuum":
return !["idle", "docked", "paused"].includes(compareState); return !["idle", "docked", "paused"].includes(compareState);
case "valve":
return compareState !== "closed";
case "plant": case "plant":
return compareState === "problem"; return compareState === "problem";
case "group": case "group":

View File

@@ -37,7 +37,6 @@ const STATE_COLORED_DOMAIN = new Set([
"timer", "timer",
"update", "update",
"vacuum", "vacuum",
"valve",
"water_heater", "water_heater",
]); ]);

View File

@@ -1,8 +1,6 @@
import { mainWindow } from "../dom/get_main_window";
export const extractSearchParamsObject = (): Record<string, string> => { export const extractSearchParamsObject = (): Record<string, string> => {
const query = {}; const query = {};
const searchParams = new URLSearchParams(mainWindow.location.search); const searchParams = new URLSearchParams(location.search);
for (const [key, value] of searchParams.entries()) { for (const [key, value] of searchParams.entries()) {
query[key] = value; query[key] = value;
} }
@@ -10,7 +8,7 @@ export const extractSearchParamsObject = (): Record<string, string> => {
}; };
export const extractSearchParam = (param: string): string | null => { export const extractSearchParam = (param: string): string | null => {
const urlParams = new URLSearchParams(mainWindow.location.search); const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(param); return urlParams.get(param);
}; };
@@ -23,7 +21,7 @@ export const createSearchParam = (params: Record<string, string>): string => {
}; };
export const addSearchParam = (params: Record<string, string>): string => { export const addSearchParam = (params: Record<string, string>): string => {
const urlParams = new URLSearchParams(mainWindow.location.search); const urlParams = new URLSearchParams(window.location.search);
Object.entries(params).forEach(([key, value]) => { Object.entries(params).forEach(([key, value]) => {
urlParams.set(key, value); urlParams.set(key, value);
}); });
@@ -31,7 +29,7 @@ export const addSearchParam = (params: Record<string, string>): string => {
}; };
export const removeSearchParam = (param: string): string => { export const removeSearchParam = (param: string): string => {
const urlParams = new URLSearchParams(mainWindow.location.search); const urlParams = new URLSearchParams(window.location.search);
urlParams.delete(param); urlParams.delete(param);
return urlParams.toString(); return urlParams.toString();
}; };

View File

@@ -23,7 +23,7 @@ export class StateHistoryChartTimeline extends LitElement {
@property({ attribute: false }) public data: TimelineEntity[] = []; @property({ attribute: false }) public data: TimelineEntity[] = [];
@property({ type: Boolean }) public narrow = false; @property() public narrow!: boolean;
@property() public names?: Record<string, string>; @property() public names?: Record<string, string>;

View File

@@ -52,7 +52,7 @@ export class StateHistoryCharts extends LitElement {
@property({ attribute: false }) public historyData!: HistoryResult; @property({ attribute: false }) public historyData!: HistoryResult;
@property({ type: Boolean }) public narrow = false; @property() public narrow!: boolean;
@property() public names?: Record<string, string>; @property() public names?: Record<string, string>;
@@ -65,7 +65,7 @@ export class StateHistoryCharts extends LitElement {
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false; @property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
@property({ type: Number }) public hoursToShow?: number; @property() public hoursToShow?: number;
@property({ type: Boolean }) public showNames = true; @property({ type: Boolean }) public showNames = true;

View File

@@ -101,8 +101,7 @@ export class StatisticsChart extends LitElement {
changedProps.has("unit") || changedProps.has("unit") ||
changedProps.has("period") || changedProps.has("period") ||
changedProps.has("chartType") || changedProps.has("chartType") ||
changedProps.has("logarithmicScale") || changedProps.has("logarithmicScale")
changedProps.has("hideLegend")
) { ) {
this._createOptions(); this._createOptions();
} }

View File

@@ -5,10 +5,6 @@ import DateRangePicker from "vue2-daterange-picker";
// @ts-ignore // @ts-ignore
import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css"; import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import {
localizeWeekdays,
localizeMonths,
} from "../common/datetime/localize_date";
// Set the current date to the left picker instead of the right picker because the right is hidden // Set the current date to the left picker instead of the right picker because the right is hidden
const CustomDateRangePicker = Vue.extend({ const CustomDateRangePicker = Vue.extend({
@@ -67,10 +63,6 @@ const Component = Vue.extend({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
language: {
type: String,
default: "en",
},
}, },
render(createElement) { render(createElement) {
// @ts-expect-error // @ts-expect-error
@@ -85,8 +77,6 @@ const Component = Vue.extend({
ranges: this.ranges ? {} : false, ranges: this.ranges ? {} : false,
"locale-data": { "locale-data": {
firstDay: this.firstDay, firstDay: this.firstDay,
daysOfWeek: localizeWeekdays(this.language, true),
monthNames: localizeMonths(this.language, false),
}, },
}, },
model: { model: {
@@ -155,8 +145,6 @@ class DateRangePickerElement extends WrappedElement {
); );
color: var(--primary-text-color); color: var(--primary-text-color);
min-width: initial !important; min-width: initial !important;
max-height: var(--date-range-picker-max-height);
overflow-y: auto;
} }
.daterangepicker:before { .daterangepicker:before {
display: none; display: none;
@@ -174,7 +162,7 @@ class DateRangePickerElement extends WrappedElement {
color: var(--secondary-text-color); color: var(--secondary-text-color);
border-radius: 0; border-radius: 0;
outline: none; outline: none;
min-width: 32px; width: 32px;
height: 32px; height: 32px;
} }
.daterangepicker td.off, .daterangepicker td.off,
@@ -250,9 +238,6 @@ class DateRangePickerElement extends WrappedElement {
} }
.daterangepicker .drp-calendar.left { .daterangepicker .drp-calendar.left {
padding: 8px; padding: 8px;
width: unset;
max-width: unset;
min-width: 270px;
} }
.daterangepicker.show-calendar .ranges { .daterangepicker.show-calendar .ranges {
margin-top: 0; margin-top: 0;

View File

@@ -1,18 +1,18 @@
import { mdiFlash, mdiFlashOff } from "@mdi/js"; import { mdiFlash, mdiFlashOff } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { import {
css,
CSSResultGroup, CSSResultGroup,
html,
LitElement, LitElement,
PropertyValues, PropertyValues,
TemplateResult, TemplateResult,
css,
html,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import { STATES_OFF } from "../../common/const"; import { STATES_OFF } from "../../common/const";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { UNAVAILABLE, UNKNOWN, isUnavailableState } from "../../data/entity"; import { isUnavailableState, UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { forwardHaptic } from "../../data/haptics"; import { forwardHaptic } from "../../data/haptics";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-formfield"; import "../ha-formfield";
@@ -24,7 +24,6 @@ const isOn = (stateObj?: HassEntity) =>
!STATES_OFF.includes(stateObj.state) && !STATES_OFF.includes(stateObj.state) &&
!isUnavailableState(stateObj.state); !isUnavailableState(stateObj.state);
@customElement("ha-entity-toggle")
export class HaEntityToggle extends LitElement { export class HaEntityToggle extends LitElement {
// hass is not a property so that we only re-render on stateObj changes // hass is not a property so that we only re-render on stateObj changes
public hass?: HomeAssistant; public hass?: HomeAssistant;
@@ -129,9 +128,6 @@ export class HaEntityToggle extends LitElement {
} else if (stateDomain === "cover") { } else if (stateDomain === "cover") {
serviceDomain = "cover"; serviceDomain = "cover";
service = turnOn ? "open_cover" : "close_cover"; service = turnOn ? "open_cover" : "close_cover";
} else if (stateDomain === "valve") {
serviceDomain = "valve";
service = turnOn ? "open_valve" : "close_valve";
} else if (stateDomain === "group") { } else if (stateDomain === "group") {
serviceDomain = "homeassistant"; serviceDomain = "homeassistant";
service = turnOn ? "turn_on" : "turn_off"; service = turnOn ? "turn_on" : "turn_off";
@@ -179,8 +175,4 @@ export class HaEntityToggle extends LitElement {
} }
} }
declare global { customElements.define("ha-entity-toggle", HaEntityToggle);
interface HTMLElementTagNameMap {
"ha-entity-toggle": HaEntityToggle;
}
}

View File

@@ -61,7 +61,7 @@ export class HaStateLabelBadge extends LitElement {
@property() public image?: string; @property() public image?: string;
@property({ type: Boolean }) public showName = false; @property() public showName?: boolean;
@state() private _timerTimeRemaining?: number; @state() private _timerTimeRemaining?: number;

View File

@@ -446,7 +446,6 @@ export class HaAreaPicker extends LitElement {
cancel: () => { cancel: () => {
this._setValue(undefined); this._setValue(undefined);
this._suggestion = undefined; this._suggestion = undefined;
this.comboBox.setInputValue("");
}, },
}); });
} }

View File

@@ -31,7 +31,7 @@ export class HaAssistPipelinePicker extends LitElement {
@property({ type: Boolean }) public required = false; @property({ type: Boolean }) public required = false;
@property({ type: Boolean }) public includeLastUsed = false; @property() public includeLastUsed = false;
@state() _pipelines?: AssistPipeline[]; @state() _pipelines?: AssistPipeline[];

View File

@@ -7,14 +7,15 @@ import { HomeAssistant } from "../types";
@customElement("ha-big-number") @customElement("ha-big-number")
export class HaBigNumber extends LitElement { export class HaBigNumber extends LitElement {
@property({ type: Number }) public value!: number; @property() public value!: number;
@property() public unit?: string; @property() public unit?: string;
@property({ attribute: "unit-position" }) @property({ attribute: "unit-position" })
public unitPosition: "top" | "bottom" = "top"; public unitPosition: "top" | "bottom" = "top";
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false })
public hass?: HomeAssistant;
@property({ attribute: false }) @property({ attribute: false })
public formatOptions: Intl.NumberFormatOptions = {}; public formatOptions: Intl.NumberFormatOptions = {};
@@ -85,7 +86,6 @@ export class HaBigNumber extends LitElement {
.value .decimal { .value .decimal {
font-size: 0.42em; font-size: 0.42em;
line-height: 1.33; line-height: 1.33;
min-height: 1.33em;
} }
.value .unit { .value .unit {
font-size: 0.33em; font-size: 0.33em;

View File

@@ -3,15 +3,9 @@ import { CheckListItemBase } from "@material/mwc-list/mwc-check-list-item-base";
import { styles as controlStyles } from "@material/mwc-list/mwc-control-list-item.css"; import { styles as controlStyles } from "@material/mwc-list/mwc-control-list-item.css";
import { styles } from "@material/mwc-list/mwc-list-item.css"; import { styles } from "@material/mwc-list/mwc-list-item.css";
import { customElement } from "lit/decorators"; import { customElement } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-check-list-item") @customElement("ha-check-list-item")
export class HaCheckListItem extends CheckListItemBase { export class HaCheckListItem extends CheckListItemBase {
async onChange(event) {
super.onChange(event);
fireEvent(this, event.type);
}
static override styles = [ static override styles = [
styles, styles,
controlStyles, controlStyles,
@@ -28,15 +22,6 @@ export class HaCheckListItem extends CheckListItemBase {
margin-inline-start: 0px; margin-inline-start: 0px;
direction: var(--direction); direction: var(--direction);
} }
.mdc-deprecated-list-item__meta {
flex-shrink: 0;
direction: var(--direction);
margin-inline-start: auto;
margin-inline-end: 0;
}
.mdc-deprecated-list-item__graphic {
margin-top: var(--check-list-item-graphic-margin-top);
}
`, `,
]; ];
} }

View File

@@ -180,7 +180,7 @@ export class HaComboBox extends LitElement {
></div>`} ></div>`}
.icon=${this.icon} .icon=${this.icon}
.invalid=${this.invalid} .invalid=${this.invalid}
.helper=${this.helper} helper=${ifDefined(this.helper)}
helperPersistent helperPersistent
> >
<slot name="icon" slot="leadingIcon"></slot> <slot name="icon" slot="leadingIcon"></slot>

View File

@@ -18,8 +18,7 @@ export interface datePickerDialogParams {
max?: string; max?: string;
locale?: string; locale?: string;
firstWeekday?: number; firstWeekday?: number;
canClear?: boolean; onChange: (value: string) => void;
onChange: (value: string | undefined) => void;
} }
const showDatePickerDialog = ( const showDatePickerDialog = (
@@ -50,8 +49,6 @@ export class HaDateInput extends LitElement {
@property() public helper?: string; @property() public helper?: string;
@property({ type: Boolean }) public canClear?: boolean;
render() { render() {
return html`<ha-textfield return html`<ha-textfield
.label=${this.label} .label=${this.label}
@@ -61,7 +58,6 @@ export class HaDateInput extends LitElement {
helperPersistent helperPersistent
readonly readonly
@click=${this._openDialog} @click=${this._openDialog}
@keydown=${this._keyDown}
.value=${this.value .value=${this.value
? formatDateNumeric( ? formatDateNumeric(
new Date(`${this.value.split("T")[0]}T00:00:00`), new Date(`${this.value.split("T")[0]}T00:00:00`),
@@ -86,23 +82,13 @@ export class HaDateInput extends LitElement {
min: this.min || "1970-01-01", min: this.min || "1970-01-01",
max: this.max, max: this.max,
value: this.value, value: this.value,
canClear: this.canClear,
onChange: (value) => this._valueChanged(value), onChange: (value) => this._valueChanged(value),
locale: this.locale.language, locale: this.locale.language,
firstWeekday: firstWeekdayIndex(this.locale), firstWeekday: firstWeekdayIndex(this.locale),
}); });
} }
private _keyDown(ev: KeyboardEvent) { private _valueChanged(value: string) {
if (!this.canClear) {
return;
}
if (["Backspace", "Delete"].includes(ev.key)) {
this._valueChanged(undefined);
}
}
private _valueChanged(value: string | undefined) {
if (this.value !== value) { if (this.value !== value) {
this.value = value; this.value = value;
fireEvent(this, "change"); fireEvent(this, "change");

View File

@@ -54,9 +54,9 @@ export class HaDateRangePicker extends LitElement {
@state() private _ranges?: DateRangePickerRanges; @state() private _ranges?: DateRangePickerRanges;
@property({ type: Boolean }) public autoApply = false; @property() public autoApply = false;
@property({ type: Boolean }) public timePicker = true; @property() public timePicker = true;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@@ -253,7 +253,6 @@ export class HaDateRangePicker extends LitElement {
opening-direction=${this.openingDirection || opening-direction=${this.openingDirection ||
this._calcedOpeningDirection} this._calcedOpeningDirection}
first-day=${firstWeekdayIndex(this.hass.locale)} first-day=${firstWeekdayIndex(this.hass.locale)}
language=${this.hass.locale.language}
> >
<div slot="input" class="date-range-inputs" @click=${this._handleClick}> <div slot="input" class="date-range-inputs" @click=${this._handleClick}>
${!this.minimal ${!this.minimal

View File

@@ -50,15 +50,6 @@ export class HaDialogDatePicker extends LitElement {
@datepicker-value-updated=${this._valueChanged} @datepicker-value-updated=${this._valueChanged}
.firstDayOfWeek=${this._params.firstWeekday} .firstDayOfWeek=${this._params.firstWeekday}
></app-datepicker> ></app-datepicker>
${this._params.canClear
? html`<mwc-button
slot="secondaryAction"
@click=${this._clear}
class="warning"
>
${this.hass.localize("ui.dialogs.date-picker.clear")}
</mwc-button>`
: nothing}
<mwc-button slot="secondaryAction" @click=${this._setToday}> <mwc-button slot="secondaryAction" @click=${this._setToday}>
${this.hass.localize("ui.dialogs.date-picker.today")} ${this.hass.localize("ui.dialogs.date-picker.today")}
</mwc-button> </mwc-button>
@@ -75,11 +66,6 @@ export class HaDialogDatePicker extends LitElement {
this._value = ev.detail.value; this._value = ev.detail.value;
} }
private _clear() {
this._params?.onChange(undefined);
this.closeDialog();
}
private _setToday() { private _setToday() {
const today = new Date(); const today = new Date();
this._value = format(today, "yyyy-MM-dd"); this._value = format(today, "yyyy-MM-dd");

View File

@@ -13,15 +13,13 @@ export const createCloseHeading = (
hass: HomeAssistant | undefined, hass: HomeAssistant | undefined,
title: string | TemplateResult title: string | TemplateResult
) => html` ) => html`
<div class="header_title"> <div class="header_title">${title}</div>
<span>${title}</span> <ha-icon-button
<ha-icon-button .label=${hass?.localize("ui.dialogs.generic.close") ?? "Close"}
.label=${hass?.localize("ui.dialogs.generic.close") ?? "Close"} .path=${mdiClose}
.path=${mdiClose} dialogAction="close"
dialogAction="close" class="header_button"
class="header_button" ></ha-icon-button>
></ha-icon-button>
</div>
`; `;
@customElement("ha-dialog") @customElement("ha-dialog")
@@ -96,12 +94,15 @@ export class HaDialog extends DialogBase {
} }
.mdc-dialog__title { .mdc-dialog__title {
padding: 24px 24px 0 24px; padding: 24px 24px 0 24px;
text-overflow: ellipsis;
overflow: hidden;
} }
.mdc-dialog__actions { .mdc-dialog__actions {
padding: 12px 24px 12px 24px; padding: 12px 24px 12px 24px;
} }
.mdc-dialog__title::before { .mdc-dialog__title::before {
content: unset; display: block;
height: 0px;
} }
.mdc-dialog .mdc-dialog__content { .mdc-dialog .mdc-dialog__content {
position: var(--dialog-content-position, relative); position: var(--dialog-content-position, relative);
@@ -125,26 +126,19 @@ export class HaDialog extends DialogBase {
flex-direction: column; flex-direction: column;
} }
.header_title { .header_title {
position: relative; margin-right: 32px;
padding-right: 40px; margin-inline-end: 32px;
padding-inline-end: 40px; margin-inline-start: initial;
padding-inline-start: initial;
direction: var(--direction); direction: var(--direction);
} }
.header_title span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: block;
}
.header_button { .header_button {
position: absolute; position: absolute;
right: -8px; right: 16px;
top: -8px; top: 14px;
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
inset-inline-start: initial; inset-inline-start: initial;
inset-inline-end: -8px; inset-inline-end: 16px;
direction: var(--direction); direction: var(--direction);
} }
.dialog-actions { .dialog-actions {

View File

@@ -19,7 +19,7 @@ export interface LevelDefinition {
} }
@customElement("ha-gauge") @customElement("ha-gauge")
export class HaGauge extends LitElement { export class Gauge extends LitElement {
@property({ type: Number }) public min = 0; @property({ type: Number }) public min = 0;
@property({ type: Number }) public max = 100; @property({ type: Number }) public max = 100;
@@ -216,9 +216,3 @@ export class HaGauge extends LitElement {
`; `;
} }
} }
declare global {
interface HTMLElementTagNameMap {
"ha-gauge": HaGauge;
}
}

View File

@@ -5,11 +5,11 @@ import { customElement } from "lit/decorators";
class HaLabel extends LitElement { class HaLabel extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<span class="label"> <span class="label">
<slot name="icon"></slot> <slot name="icon"></slot>
<slot></slot> <slot></slot>
</span> </div>
`; `;
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@@ -6,27 +6,27 @@ import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-labeled-slider") @customElement("ha-labeled-slider")
class HaLabeledSlider extends LitElement { class HaLabeledSlider extends LitElement {
@property({ type: Boolean }) public labeled = false; @property() public labeled? = false;
@property() public caption?: string; @property() public caption?: string;
@property({ type: Boolean }) public disabled = false; @property() public disabled?: boolean;
@property({ type: Boolean }) public required = true; @property() public required?: boolean;
@property({ type: Number }) public min = 0; @property() public min: number = 0;
@property({ type: Number }) public max = 100; @property() public max: number = 100;
@property({ type: Number }) public step = 1; @property() public step: number = 1;
@property() public helper?: string; @property() public helper?: string;
@property({ type: Boolean }) public extra = false; @property() public extra = false;
@property() public icon?: string; @property() public icon?: string;
@property({ type: Number }) public value?: number; @property() public value?: number;
protected render() { protected render() {
return html` return html`
@@ -38,7 +38,7 @@ class HaLabeledSlider extends LitElement {
.min=${this.min} .min=${this.min}
.max=${this.max} .max=${this.max}
.step=${this.step} .step=${this.step}
.labeled=${this.labeled} labeled=${this.labeled}
.disabled=${this.disabled} .disabled=${this.disabled}
.value=${this.value} .value=${this.value}
@change=${this._inputChanged} @change=${this._inputChanged}

View File

@@ -36,24 +36,16 @@ export class HaListItem extends ListItemBase {
--mdc-list-item-graphic-margin, --mdc-list-item-graphic-margin,
16px 16px
) !important; ) !important;
direction: var(--direction) !important; direction: var(--direction);
} }
span.material-icons:last-of-type { span.material-icons:last-of-type {
margin-inline-start: auto !important; margin-inline-start: auto !important;
margin-inline-end: 0px !important; margin-inline-end: 0px !important;
direction: var(--direction) !important; direction: var(--direction);
} }
.mdc-deprecated-list-item__meta { .mdc-deprecated-list-item__meta {
display: var(--mdc-list-item-meta-display); display: var(--mdc-list-item-meta-display);
align-items: center; align-items: center;
flex-shrink: 0;
}
:host([graphic="icon"]:not([twoline]))
.mdc-deprecated-list-item__graphic {
margin-inline-end: var(
--mdc-list-item-graphic-margin,
20px
) !important;
} }
:host([multiline-secondary]) { :host([multiline-secondary]) {
height: auto; height: auto;
@@ -86,15 +78,6 @@ export class HaListItem extends ListItemBase {
pointer-events: unset; pointer-events: unset;
} }
`, `,
// safari workaround - must be explicit
document.dir === "rtl"
? css`
span.material-icons:first-of-type,
span.material-icons:last-of-type {
direction: rtl !important;
}
`
: css``,
]; ];
} }
} }

View File

@@ -95,15 +95,6 @@ class HaMarkdownElement extends ReactiveElement {
} }
node.firstElementChild!.replaceWith(alertNote); node.firstElementChild!.replaceWith(alertNote);
} }
} else if (
node instanceof HTMLElement &&
["ha-alert", "ha-qr-code", "ha-icon", "ha-svg-icon"].includes(
node.localName
)
) {
import(
/* webpackInclude: /(ha-alert)|(ha-qr-code)|(ha-icon)|(ha-svg-icon)/ */ `./${node.localName}`
);
} }
} }
} }

View File

@@ -2,6 +2,11 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "./ha-markdown-element"; import "./ha-markdown-element";
// Import components that are allwoed to be defined.
import "./ha-alert";
import "./ha-icon";
import "./ha-svg-icon";
@customElement("ha-markdown") @customElement("ha-markdown")
export class HaMarkdown extends LitElement { export class HaMarkdown extends LitElement {
@property() public content?; @property() public content?;

View File

@@ -11,7 +11,7 @@ import "./ha-icon-button";
class HaMenuButton extends LitElement { class HaMenuButton extends LitElement {
@property({ type: Boolean }) public hassio = false; @property({ type: Boolean }) public hassio = false;
@property({ type: Boolean }) public narrow = false; @property() public narrow!: boolean;
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;

View File

@@ -1,120 +0,0 @@
import { LitElement, PropertyValues, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import QRCode from "qrcode";
@customElement("ha-qr-code")
export class HaQrCode extends LitElement {
@property() public data?: string;
@property({ attribute: "error-correction-level" })
public errorCorrectionLevel: "low" | "medium" | "quartile" | "high" =
"medium";
@property({ type: Number })
public width = 4;
@property({ type: Number })
public scale = 4;
@property({ type: Number })
public margin = 4;
@property({ type: Number }) public maskPattern?:
| 0
| 1
| 2
| 3
| 4
| 5
| 6
| 7;
@property({ attribute: "center-image" }) public centerImage?: string;
@state() private _error?: string;
@query("canvas") private _canvas?: HTMLCanvasElement;
protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (
(changedProperties.has("data") ||
changedProperties.has("scale") ||
changedProperties.has("width") ||
changedProperties.has("margin") ||
changedProperties.has("maskPattern") ||
changedProperties.has("errorCorrectionLevel")) &&
this._error
) {
this._error = undefined;
}
}
updated(changedProperties: PropertyValues) {
const canvas = this._canvas;
if (
canvas &&
this.data &&
(changedProperties.has("data") ||
changedProperties.has("scale") ||
changedProperties.has("width") ||
changedProperties.has("margin") ||
changedProperties.has("maskPattern") ||
changedProperties.has("errorCorrectionLevel") ||
changedProperties.has("centerImage"))
) {
const computedStyles = getComputedStyle(this);
QRCode.toCanvas(canvas, this.data, {
errorCorrectionLevel: this.errorCorrectionLevel,
width: this.width,
scale: this.scale,
margin: this.margin,
maskPattern: this.maskPattern,
color: {
light: computedStyles.getPropertyValue("--card-background-color"),
dark: computedStyles.getPropertyValue("--primary-text-color"),
},
}).catch((err) => {
this._error = err.message;
});
if (this.centerImage) {
const context = this._canvas!.getContext("2d");
const imageObj = new Image();
imageObj.src = this.centerImage;
imageObj.onload = () => {
context?.drawImage(
imageObj,
canvas.width * 0.375,
canvas.height * 0.375,
canvas.width / 4,
canvas.height / 4
);
};
}
}
}
render() {
if (!this.data) {
return nothing;
}
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}
return html`<canvas></canvas>`;
}
static styles = css`
:host {
display: block;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-qr-code": HaQrCode;
}
}

View File

@@ -41,6 +41,6 @@ export class HaBackupLocationSelector extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-selector-backup_location": HaBackupLocationSelector; "ha-selector-backup-location": HaBackupLocationSelector;
} }
} }

View File

@@ -10,7 +10,7 @@ import "../ha-input-helper-text";
export class HaBooleanSelector extends LitElement { export class HaBooleanSelector extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@property({ type: Boolean }) public value = false; @property() public value?: number;
@property() public label?: string; @property() public label?: string;

View File

@@ -14,9 +14,9 @@ export class HaNumberSelector extends LitElement {
@property() public selector!: NumberSelector; @property() public selector!: NumberSelector;
@property({ type: Number }) public value?: number; @property() public value?: number;
@property({ type: Number }) public placeholder?: number; @property() public placeholder?: number;
@property() public label?: string; @property() public label?: string;
@@ -43,22 +43,6 @@ export class HaNumberSelector extends LitElement {
this.selector.number?.min === undefined || this.selector.number?.min === undefined ||
this.selector.number?.max === undefined; this.selector.number?.max === undefined;
let sliderStep;
if (!isBox) {
sliderStep = this.selector.number!.step ?? 1;
if (sliderStep === "any") {
sliderStep = 1;
// divide the range of the slider by 100 steps
const step =
(this.selector.number!.max! - this.selector.number!.min!) / 100;
// biggest step size is 1, round the step size to a division of 1
while (sliderStep > step) {
sliderStep /= 10;
}
}
}
return html` return html`
<div class="input"> <div class="input">
${!isBox ${!isBox
@@ -68,10 +52,12 @@ export class HaNumberSelector extends LitElement {
: ""} : ""}
<ha-slider <ha-slider
labeled labeled
.min=${this.selector.number!.min} .min=${this.selector.number?.min}
.max=${this.selector.number!.max} .max=${this.selector.number?.max}
.value=${this.value ?? ""} .value=${this.value ?? ""}
.step=${sliderStep} .step=${this.selector.number?.step === "any"
? undefined
: this.selector.number?.step ?? 1}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}
@change=${this._handleSliderChange} @change=${this._handleSliderChange}

View File

@@ -1,13 +1,16 @@
import "@material/mwc-list/mwc-list-item"; import "@material/mwc-list/mwc-list-item";
import { mdiDrag } from "@mdi/js"; import { mdiDrag } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, PropertyValues, css, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import { SortableEvent } from "sortablejs";
import { ensureArray } from "../../common/array/ensure-array"; import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation"; import { stopPropagation } from "../../common/dom/stop_propagation";
import { caseInsensitiveStringCompare } from "../../common/string/compare"; import { caseInsensitiveStringCompare } from "../../common/string/compare";
import type { SelectOption, SelectSelector } from "../../data/selector"; import type { SelectOption, SelectSelector } from "../../data/selector";
import { sortableStyles } from "../../resources/ha-sortable-style";
import { SortableInstance } from "../../resources/sortable";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import "../chips/ha-chip-set"; import "../chips/ha-chip-set";
import "../chips/ha-input-chip"; import "../chips/ha-input-chip";
@@ -18,7 +21,6 @@ import "../ha-formfield";
import "../ha-input-helper-text"; import "../ha-input-helper-text";
import "../ha-radio"; import "../ha-radio";
import "../ha-select"; import "../ha-select";
import "../ha-sortable";
@customElement("ha-selector-select") @customElement("ha-selector-select")
export class HaSelectSelector extends LitElement { export class HaSelectSelector extends LitElement {
@@ -40,10 +42,50 @@ export class HaSelectSelector extends LitElement {
@query("ha-combo-box", true) private comboBox!: HaComboBox; @query("ha-combo-box", true) private comboBox!: HaComboBox;
private _itemMoved(ev: CustomEvent): void { private _sortable?: SortableInstance;
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail; protected updated(changedProps: PropertyValues): void {
this._move(oldIndex!, newIndex); if (changedProps.has("value") || changedProps.has("selector")) {
const sortableNeeded =
this.selector.select?.multiple &&
this.selector.select.reorder &&
this.value?.length;
if (!this._sortable && sortableNeeded) {
this._createSortable();
} else if (this._sortable && !sortableNeeded) {
this._destroySortable();
}
}
}
private async _createSortable() {
const Sortable = (await import("../../resources/sortable")).default;
this._sortable = new Sortable(
this.shadowRoot!.querySelector("ha-chip-set")!,
{
animation: 150,
fallbackClass: "sortable-fallback",
draggable: "ha-input-chip",
onChoose: (evt: SortableEvent) => {
(evt.item as any).placeholder =
document.createComment("sort-placeholder");
evt.item.after((evt.item as any).placeholder);
},
onEnd: (evt: SortableEvent) => {
// put back in original location
if ((evt.item as any).placeholder) {
(evt.item as any).placeholder.replaceWith(evt.item);
delete (evt.item as any).placeholder;
}
this._dragged(evt);
},
}
);
}
private _dragged(ev: SortableEvent): void {
if (ev.oldIndex === ev.newIndex) return;
this._move(ev.oldIndex!, ev.newIndex!);
} }
private _move(index: number, newIndex: number) { private _move(index: number, newIndex: number) {
@@ -57,6 +99,11 @@ export class HaSelectSelector extends LitElement {
}); });
} }
private _destroySortable() {
this._sortable?.destroy();
this._sortable = undefined;
}
private _filter = ""; private _filter = "";
protected render() { protected render() {
@@ -148,43 +195,37 @@ export class HaSelectSelector extends LitElement {
return html` return html`
${value?.length ${value?.length
? html` ? html`
<ha-sortable <ha-chip-set>
no-style ${repeat(
.disabled=${!this.selector.select.reorder} value,
@item-moved=${this._itemMoved} (item) => item,
> (item, idx) => {
<ha-chip-set> const label =
${repeat( options.find((option) => option.value === item)?.label ||
value, item;
(item) => item, return html`
(item, idx) => { <ha-input-chip
const label = .idx=${idx}
options.find((option) => option.value === item) @remove=${this._removeItem}
?.label || item; .label=${label}
return html` selected
<ha-input-chip >
.idx=${idx} ${this.selector.select?.reorder
@remove=${this._removeItem} ? html`
.label=${label} <ha-svg-icon
selected slot="icon"
> .path=${mdiDrag}
${this.selector.select?.reorder data-handle
? html` ></ha-svg-icon>
<ha-svg-icon `
slot="icon" : nothing}
.path=${mdiDrag} ${options.find((option) => option.value === item)
data-handle ?.label || item}
></ha-svg-icon> </ha-input-chip>
` `;
: nothing} }
${options.find((option) => option.value === item) )}
?.label || item} </ha-chip-set>
</ha-input-chip>
`;
}
)}
</ha-chip-set>
</ha-sortable>
` `
: nothing} : nothing}
@@ -378,35 +419,25 @@ export class HaSelectSelector extends LitElement {
this.comboBox.filteredItems = filteredItems; this.comboBox.filteredItems = filteredItems;
} }
static styles = css` static styles = [
:host { sortableStyles,
position: relative; css`
} :host {
ha-select, position: relative;
mwc-formfield, }
ha-formfield { ha-select,
display: block; mwc-formfield,
} ha-formfield {
mwc-list-item[disabled] { display: block;
--mdc-theme-text-primary-on-background: var(--disabled-text-color); }
} mwc-list-item[disabled] {
ha-chip-set { --mdc-theme-text-primary-on-background: var(--disabled-text-color);
padding: 8px 0; }
} ha-chip-set {
padding: 8px 0;
.sortable-fallback { }
display: none; `,
opacity: 0; ];
}
.sortable-ghost {
opacity: 0.4;
}
.sortable-drag {
cursor: grabbing;
}
`;
} }
declare global { declare global {

View File

@@ -70,15 +70,15 @@ const SELECTOR_SCHEMAS = {
number: [ number: [
{ {
name: "min", name: "min",
selector: { number: { mode: "box", step: "any" } }, selector: { number: { mode: "box" } },
}, },
{ {
name: "max", name: "max",
selector: { number: { mode: "box", step: "any" } }, selector: { number: { mode: "box" } },
}, },
{ {
name: "step", name: "step",
selector: { number: { mode: "box", step: "any" } }, selector: { number: { mode: "box" } },
}, },
] as const, ] as const,
object: [] as const, object: [] as const,

View File

@@ -47,6 +47,6 @@ export class HaTTSVoiceSelector extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-selector-tts_voice": HaTTSVoiceSelector; "ha-selector-tts-voice": HaTTSVoiceSelector;
} }
} }

View File

@@ -39,6 +39,6 @@ export class HaSelectorUiAction extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-selector-ui_action": HaSelectorUiAction; "ha-selector-ui-action": HaSelectorUiAction;
} }
} }

View File

@@ -36,6 +36,6 @@ export class HaSelectorUiColor extends LitElement {
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-selector-ui_color": HaSelectorUiColor; "ha-selector-ui-color": HaSelectorUiColor;
} }
} }

View File

@@ -4,14 +4,7 @@ import {
HassServices, HassServices,
HassServiceTarget, HassServiceTarget,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
nothing,
} from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { ensureArray } from "../common/array/ensure-array"; import { ensureArray } from "../common/array/ensure-array";
@@ -90,8 +83,6 @@ export class HaServiceControl extends LitElement {
@property({ type: Boolean }) public showAdvanced?: boolean; @property({ type: Boolean }) public showAdvanced?: boolean;
@property({ type: Boolean, reflect: true }) public hidePicker?: boolean;
@state() private _value!: this["value"]; @state() private _value!: this["value"];
@state() private _checkedKeys = new Set(); @state() private _checkedKeys = new Set();
@@ -372,14 +363,12 @@ export class HaServiceControl extends LitElement {
)) || )) ||
serviceData?.description; serviceData?.description;
return html`${this.hidePicker return html`<ha-service-picker
? nothing .hass=${this.hass}
: html`<ha-service-picker .value=${this._value?.service}
.hass=${this.hass} .disabled=${this.disabled}
.value=${this._value?.service} @value-changed=${this._serviceChanged}
.disabled=${this.disabled} ></ha-service-picker>
@value-changed=${this._serviceChanged}
></ha-service-picker>`}
<div class="description"> <div class="description">
${description ? html`<p>${description}</p>` : ""} ${description ? html`<p>${description}</p>` : ""}
${this._manifest ${this._manifest
@@ -533,14 +522,6 @@ export class HaServiceControl extends LitElement {
defaultValue = field.selector.constant?.value; defaultValue = field.selector.constant?.value;
} }
if (
defaultValue == null &&
field?.selector &&
"boolean" in field.selector
) {
defaultValue = false;
}
if (defaultValue != null) { if (defaultValue != null) {
data = { data = {
...this._value?.data, ...this._value?.data,
@@ -746,9 +727,6 @@ export class HaServiceControl extends LitElement {
margin: var(--service-control-padding, 0 16px); margin: var(--service-control-padding, 0 16px);
padding: 16px 0; padding: 16px 0;
} }
:host([hidePicker]) p {
padding-top: 0;
}
.checkbox-spacer { .checkbox-spacer {
width: 32px; width: 32px;
} }

View File

@@ -35,12 +35,7 @@ export class HaSettingsRow extends LitElement {
align-items: center; align-items: center;
} }
.body { .body {
padding-top: 8px; padding: 8px 16px 8px 0;
padding-bottom: 8px;
padding-left: 0;
padding-inline-start: 0;
padding-right: 16x;
padding-inline-end: 16px;
overflow: hidden; overflow: hidden;
display: var(--layout-vertical_-_display); display: var(--layout-vertical_-_display);
flex-direction: var(--layout-vertical_-_flex-direction); flex-direction: var(--layout-vertical_-_flex-direction);

View File

@@ -33,6 +33,7 @@ import {
} from "lit"; } from "lit";
import { customElement, eventOptions, property, state } from "lit/decorators"; import { customElement, eventOptions, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { guard } from "lit/directives/guard";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { storage } from "../common/decorators/storage"; import { storage } from "../common/decorators/storage";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
@@ -49,12 +50,12 @@ import { subscribeRepairsIssueRegistry } from "../data/repairs";
import { UpdateEntity, updateCanInstall } from "../data/update"; import { UpdateEntity, updateCanInstall } from "../data/update";
import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive"; import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import type { SortableInstance } from "../resources/sortable";
import { haStyleScrollbar } from "../resources/styles"; import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types"; import type { HomeAssistant, PanelInfo, Route } from "../types";
import "./ha-icon"; import "./ha-icon";
import "./ha-icon-button"; import "./ha-icon-button";
import "./ha-menu-button"; import "./ha-menu-button";
import "./ha-sortable";
import "./ha-svg-icon"; import "./ha-svg-icon";
import "./user/ha-user-badge"; import "./user/ha-user-badge";
@@ -203,13 +204,15 @@ class HaSidebar extends SubscribeMixin(LitElement) {
@state() private _issuesCount = 0; @state() private _issuesCount = 0;
@state() private _renderEmptySortable = false;
private _mouseLeaveTimeout?: number; private _mouseLeaveTimeout?: number;
private _tooltipHideTimeout?: number; private _tooltipHideTimeout?: number;
private _recentKeydownActiveUntil = 0; private _recentKeydownActiveUntil = 0;
private _editStyleLoaded = false; private sortableStyleLoaded = false;
@storage({ @storage({
key: "sidebarPanelOrder", key: "sidebarPanelOrder",
@@ -225,6 +228,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}) })
private _hiddenPanels: string[] = []; private _hiddenPanels: string[] = [];
private _sortable?: SortableInstance;
public hassSubscribe(): UnsubscribeFunc[] { public hassSubscribe(): UnsubscribeFunc[] {
return this.hass.user?.is_admin return this.hass.user?.is_admin
? [ ? [
@@ -259,13 +264,14 @@ class HaSidebar extends SubscribeMixin(LitElement) {
changedProps.has("expanded") || changedProps.has("expanded") ||
changedProps.has("narrow") || changedProps.has("narrow") ||
changedProps.has("alwaysExpand") || changedProps.has("alwaysExpand") ||
changedProps.has("editMode") ||
changedProps.has("_externalConfig") || changedProps.has("_externalConfig") ||
changedProps.has("_updatesCount") || changedProps.has("_updatesCount") ||
changedProps.has("_issuesCount") || changedProps.has("_issuesCount") ||
changedProps.has("_notifications") || changedProps.has("_notifications") ||
changedProps.has("editMode") ||
changedProps.has("_renderEmptySortable") ||
changedProps.has("_hiddenPanels") || changedProps.has("_hiddenPanels") ||
changedProps.has("_panelOrder") (changedProps.has("_panelOrder") && !this.editMode)
) { ) {
return true; return true;
} }
@@ -300,8 +306,12 @@ class HaSidebar extends SubscribeMixin(LitElement) {
if (changedProps.has("alwaysExpand")) { if (changedProps.has("alwaysExpand")) {
toggleAttribute(this, "expanded", this.alwaysExpand); toggleAttribute(this, "expanded", this.alwaysExpand);
} }
if (changedProps.has("editMode") && this.editMode) { if (changedProps.has("editMode")) {
this._editModeActivated(); if (this.editMode) {
this._activateEditMode();
} else {
this._deactivateEditMode();
}
} }
if (!changedProps.has("hass")) { if (!changedProps.has("hass")) {
return; return;
@@ -460,36 +470,15 @@ class HaSidebar extends SubscribeMixin(LitElement) {
`; `;
} }
private _panelMoved(ev: CustomEvent) {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
const [beforeSpacer] = computePanels(
this.hass.panels,
this.hass.defaultPanel,
this._panelOrder,
this._hiddenPanels,
this.hass.locale
);
const panelOrder = beforeSpacer.map((panel) => panel.url_path);
const panel = panelOrder.splice(oldIndex, 1)[0];
panelOrder.splice(newIndex, 0, panel);
this._panelOrder = panelOrder;
}
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) { private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
return html` // prettier-ignore
<ha-sortable return html`<div id="sortable">
handle-selector="paper-icon-item" ${guard([this._hiddenPanels, this._renderEmptySortable], () =>
.disabled=${!this.editMode} this._renderEmptySortable ? "" : this._renderPanels(beforeSpacer)
@item-moved=${this._panelMoved} )}
> </div>
<div class="reorder-list">${this._renderPanels(beforeSpacer)}</div> ${this._renderSpacer()}
</ha-sortable> ${this._renderHiddenPanels()} `;
${this._renderSpacer()}${this._renderHiddenPanels()}
`;
} }
private _renderHiddenPanels() { private _renderHiddenPanels() {
@@ -685,22 +674,44 @@ class HaSidebar extends SubscribeMixin(LitElement) {
fireEvent(this, "hass-edit-sidebar", { editMode: true }); fireEvent(this, "hass-edit-sidebar", { editMode: true });
} }
private async _editModeActivated() { private async _activateEditMode() {
await this._loadEditStyle(); await Promise.all([this._loadSortableStyle(), this._createSortable()]);
} }
private async _loadEditStyle() { private async _loadSortableStyle() {
if (this._editStyleLoaded) return; if (this.sortableStyleLoaded) return;
const editStylesImport = await import("../resources/ha-sidebar-edit-style"); const sortStylesImport = await import("../resources/ha-sortable-style");
const style = document.createElement("style"); const style = document.createElement("style");
style.innerHTML = (editStylesImport.sidebarEditStyle as CSSResult).cssText; style.innerHTML = (sortStylesImport.sortableStyles as CSSResult).cssText;
this.shadowRoot!.appendChild(style); this.shadowRoot!.appendChild(style);
this.sortableStyleLoaded = true;
await this.updateComplete; await this.updateComplete;
} }
private async _createSortable() {
const Sortable = (await import("../resources/sortable")).default;
this._sortable = new Sortable(
this.shadowRoot!.getElementById("sortable")!,
{
animation: 150,
fallbackClass: "sortable-fallback",
dataIdAttr: "data-panel",
handle: "paper-icon-item",
onSort: async () => {
this._panelOrder = this._sortable!.toArray();
},
}
);
}
private _deactivateEditMode() {
this._sortable?.destroy();
this._sortable = undefined;
}
private _closeEditMode() { private _closeEditMode() {
fireEvent(this, "hass-edit-sidebar", { editMode: false }); fireEvent(this, "hass-edit-sidebar", { editMode: false });
} }
@@ -713,8 +724,13 @@ class HaSidebar extends SubscribeMixin(LitElement) {
} }
// Make a copy for Memoize // Make a copy for Memoize
this._hiddenPanels = [...this._hiddenPanels, panel]; this._hiddenPanels = [...this._hiddenPanels, panel];
// Remove it from the panel order this._renderEmptySortable = true;
this._panelOrder = this._panelOrder.filter((order) => order !== panel); await this.updateComplete;
const container = this.shadowRoot!.getElementById("sortable")!;
while (container.lastElementChild) {
container.removeChild(container.lastElementChild);
}
this._renderEmptySortable = false;
} }
private async _unhidePanel(ev: Event) { private async _unhidePanel(ev: Event) {
@@ -723,6 +739,13 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this._hiddenPanels = this._hiddenPanels.filter( this._hiddenPanels = this._hiddenPanels.filter(
(hidden) => hidden !== panel (hidden) => hidden !== panel
); );
this._renderEmptySortable = true;
await this.updateComplete;
const container = this.shadowRoot!.getElementById("sortable")!;
while (container.lastElementChild) {
container.removeChild(container.lastElementChild);
}
this._renderEmptySortable = false;
} }
private _itemMouseEnter(ev: MouseEvent) { private _itemMouseEnter(ev: MouseEvent) {
@@ -887,7 +910,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
.menu mwc-button { .menu mwc-button {
width: 100%; width: 100%;
} }
.reorder-list, #sortable,
.hidden-panel { .hidden-panel {
display: none; display: none;
} }

View File

@@ -11,7 +11,6 @@ export class HaSlider extends MdSlider {
:host { :host {
--md-sys-color-primary: var(--primary-color); --md-sys-color-primary: var(--primary-color);
--md-sys-color-outline: var(--outline-color); --md-sys-color-outline: var(--outline-color);
--md-sys-color-on-surface: var(--primary-text-color);
--md-slider-handle-width: 14px; --md-slider-handle-width: 14px;
--md-slider-handle-height: 14px; --md-slider-handle-height: 14px;
min-width: 100px; min-width: 100px;

View File

@@ -1,153 +0,0 @@
/* eslint-disable lit/prefer-static-styles */
import { html, LitElement, nothing, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import type { SortableEvent } from "sortablejs";
import { fireEvent } from "../common/dom/fire_event";
import type { SortableInstance } from "../resources/sortable";
declare global {
interface HASSDomEvents {
"item-moved": {
oldIndex: number;
newIndex: number;
};
}
}
@customElement("ha-sortable")
export class HaSortable extends LitElement {
private _sortable?: SortableInstance;
@property({ type: Boolean })
public disabled = false;
@property({ type: Boolean, attribute: "no-style" })
public noStyle: boolean = false;
@property({ type: String, attribute: "draggable-selector" })
public draggableSelector?: string;
@property({ type: String, attribute: "handle-selector" })
public handleSelector?: string;
protected updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has("disabled")) {
if (this.disabled) {
this._destroySortable();
} else {
this._createSortable();
}
}
}
// Workaround for connectedCallback just after disconnectedCallback (when dragging sortable with sortable children)
private _shouldBeDestroy = false;
public disconnectedCallback() {
super.disconnectedCallback();
this._shouldBeDestroy = true;
setTimeout(() => {
if (this._shouldBeDestroy) {
this._destroySortable();
this._shouldBeDestroy = false;
}
}, 1);
}
public connectedCallback() {
super.connectedCallback();
this._shouldBeDestroy = false;
}
protected createRenderRoot() {
return this;
}
protected render() {
if (this.noStyle) return nothing;
return html`
<style>
.sortable-fallback {
display: none;
opacity: 0;
}
.sortable-ghost {
border: 2px solid var(--primary-color);
background: rgba(var(--rgb-primary-color), 0.25);
border-radius: 4px;
opacity: 0.4;
}
.sortable-drag {
border-radius: 4px;
opacity: 1;
background: var(--card-background-color);
box-shadow: 0px 4px 8px 3px #00000026;
cursor: grabbing;
}
</style>
`;
}
private async _createSortable() {
if (this._sortable) return;
const container = this.children[0] as HTMLElement | undefined;
if (!container) return;
const Sortable = (await import("../resources/sortable")).default;
const options: SortableInstance.Options = {
animation: 150,
onChoose: this._handleChoose,
onEnd: this._handleEnd,
};
if (this.draggableSelector) {
options.draggable = this.draggableSelector;
}
if (this.handleSelector) {
options.handle = this.handleSelector;
}
this._sortable = new Sortable(container, options);
}
private _handleEnd = (evt: SortableEvent) => {
// put back in original location
if ((evt.item as any).placeholder) {
(evt.item as any).placeholder.replaceWith(evt.item);
delete (evt.item as any).placeholder;
}
// if item was not moved, ignore
if (
evt.oldIndex === undefined ||
evt.newIndex === undefined ||
evt.oldIndex === evt.newIndex
) {
return;
}
fireEvent(this, "item-moved", {
oldIndex: evt.oldIndex!,
newIndex: evt.newIndex!,
});
};
private _handleChoose = (evt: SortableEvent) => {
(evt.item as any).placeholder = document.createComment("sort-placeholder");
evt.item.after((evt.item as any).placeholder);
};
private _destroySortable() {
if (!this._sortable) return;
this._sortable.destroy();
this._sortable = undefined;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-sortable": HaSortable;
}
}

View File

@@ -21,7 +21,7 @@ export class HaThemePicker extends LitElement {
@property() public label?: string; @property() public label?: string;
@property({ type: Boolean }) includeDefault = false; @property() includeDefault?: boolean = false;
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;

View File

@@ -17,7 +17,7 @@ export const passiveEventOptionsIfSupported = supportsPassiveEventListener
: undefined; : undefined;
@customElement("ha-two-pane-top-app-bar-fixed") @customElement("ha-two-pane-top-app-bar-fixed")
export class TopAppBarBaseBase extends BaseElement { export abstract class TopAppBarBaseBase extends BaseElement {
protected override mdcFoundation!: MDCFixedTopAppBarFoundation; protected override mdcFoundation!: MDCFixedTopAppBarFoundation;
protected override mdcFoundationClass = MDCFixedTopAppBarFoundation; protected override mdcFoundationClass = MDCFixedTopAppBarFoundation;
@@ -280,8 +280,6 @@ export class TopAppBarBaseBase extends BaseElement {
} }
#title { #title {
border-right: 1px solid rgba(255, 255, 255, 0.12); border-right: 1px solid rgba(255, 255, 255, 0.12);
border-inline-end: 1px solid rgba(255, 255, 255, 0.12);
border-inline-start: initial;
box-sizing: border-box; box-sizing: border-box;
flex: 0 0 var(--sidepane-width, 250px); flex: 0 0 var(--sidepane-width, 250px);
width: var(--sidepane-width, 250px); width: var(--sidepane-width, 250px);
@@ -292,8 +290,6 @@ export class TopAppBarBaseBase extends BaseElement {
} }
.pane { .pane {
border-right: 1px solid var(--divider-color); border-right: 1px solid var(--divider-color);
border-inline-end: 1px solid var(--divider-color);
border-inline-start: initial;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex: 0 0 var(--sidepane-width, 250px); flex: 0 0 var(--sidepane-width, 250px);
@@ -323,9 +319,3 @@ export class TopAppBarBaseBase extends BaseElement {
`, `,
]; ];
} }
declare global {
interface HTMLElementTagNameMap {
"ha-two-pane-top-app-bar-fixed": TopAppBarBaseBase;
}
}

View File

@@ -1,102 +0,0 @@
import { mdiStop, mdiValveClosed, mdiValveOpen } from "@mdi/js";
import { CSSResultGroup, LitElement, html, css, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { supportsFeature } from "../common/entity/supports-feature";
import {
ValveEntity,
ValveEntityFeature,
canClose,
canOpen,
canStop,
} from "../data/valve";
import type { HomeAssistant } from "../types";
import "./ha-icon-button";
@customElement("ha-valve-controls")
class HaValveControls extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj!: ValveEntity;
protected render() {
if (!this.stateObj) {
return nothing;
}
return html`
<div class="state">
<ha-icon-button
class=${classMap({
hidden: !supportsFeature(this.stateObj, ValveEntityFeature.OPEN),
})}
.label=${this.hass.localize("ui.card.valve.open_valve")}
@click=${this._onOpenTap}
.disabled=${!canOpen(this.stateObj)}
.path=${mdiValveOpen}
>
</ha-icon-button>
<ha-icon-button
class=${classMap({
hidden: !supportsFeature(this.stateObj, ValveEntityFeature.STOP),
})}
.label=${this.hass.localize("ui.card.valve.stop_valve")}
@click=${this._onStopTap}
.disabled=${!canStop(this.stateObj)}
.path=${mdiStop}
></ha-icon-button>
<ha-icon-button
class=${classMap({
hidden: !supportsFeature(this.stateObj, ValveEntityFeature.CLOSE),
})}
.label=${this.hass.localize("ui.card.valve.close_valve")}
@click=${this._onCloseTap}
.disabled=${!canClose(this.stateObj)}
.path=${mdiValveClosed}
>
</ha-icon-button>
</div>
`;
}
private _onOpenTap(ev): void {
ev.stopPropagation();
this.hass.callService("valve", "open_valve", {
entity_id: this.stateObj.entity_id,
});
}
private _onCloseTap(ev): void {
ev.stopPropagation();
this.hass.callService("valve", "close_valve", {
entity_id: this.stateObj.entity_id,
});
}
private _onStopTap(ev): void {
ev.stopPropagation();
this.hass.callService("valve", "stop_valve", {
entity_id: this.stateObj.entity_id,
});
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
}
.state {
white-space: nowrap;
}
.hidden {
visibility: hidden !important;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-valve-controls": HaValveControls;
}
}

View File

@@ -62,7 +62,7 @@ class BrowseMediaTTS extends LitElement {
this.hass.localize( this.hass.localize(
"ui.components.media-browser.tts.example_message", "ui.components.media-browser.tts.example_message",
{ {
name: this.hass.user?.name || "Alice", name: this.hass.user?.name || "",
} }
)} )}
> >

View File

@@ -53,7 +53,7 @@ class MediaUploadButton extends LitElement {
${this._uploading > 0 ${this._uploading > 0
? html` ? html`
<ha-circular-progress <ha-circular-progress
size="small" size="tiny"
indeterminate indeterminate
area-label="Uploading" area-label="Uploading"
slot="icon" slot="icon"

View File

@@ -1,12 +1,5 @@
import { dump } from "js-yaml"; import { dump } from "js-yaml";
import { import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
css,
CSSResultGroup,
html,
LitElement,
nothing,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
@@ -25,12 +18,6 @@ import { traceTabStyles } from "./trace-tab-styles";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import type { NodeInfo } from "./hat-script-graph"; import type { NodeInfo } from "./hat-script-graph";
const TRACE_PATH_TABS = [
"step_config",
"changed_variables",
"logbook",
] as const;
@customElement("ha-trace-path-details") @customElement("ha-trace-path-details")
export class HaTracePathDetails extends LitElement { export class HaTracePathDetails extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@@ -47,7 +34,7 @@ export class HaTracePathDetails extends LitElement {
@property() public trackedNodes!: Record<string, any>; @property() public trackedNodes!: Record<string, any>;
@state() private _view: (typeof TRACE_PATH_TABS)[number] = "step_config"; @state() private _view: "config" | "changed_variables" | "logbook" = "config";
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
@@ -56,21 +43,23 @@ export class HaTracePathDetails extends LitElement {
</div> </div>
<div class="tabs top"> <div class="tabs top">
${TRACE_PATH_TABS.map( ${[
(view) => html` ["config", "Step Config"],
["changed_variables", "Changed Variables"],
["logbook", "Related logbook entries"],
].map(
([view, label]) => html`
<button <button
.view=${view} .view=${view}
class=${classMap({ active: this._view === view })} class=${classMap({ active: this._view === view })}
@click=${this._showTab} @click=${this._showTab}
> >
${this.hass!.localize( ${label}
`ui.panel.config.automation.trace.tabs.${view}`
)}
</button> </button>
` `
)} )}
</div> </div>
${this._view === "step_config" ${this._view === "config"
? this._renderSelectedConfig() ? this._renderSelectedConfig()
: this._view === "changed_variables" : this._view === "changed_variables"
? this._renderChangedVars() ? this._renderChangedVars()
@@ -82,9 +71,7 @@ export class HaTracePathDetails extends LitElement {
const paths = this.trace.trace; const paths = this.trace.trace;
if (!this.selected?.path) { if (!this.selected?.path) {
return this.hass!.localize( return "Select a node on the left for more information.";
"ui.panel.config.automation.trace.path.choose"
);
} }
// HACK: default choice node is not part of paths. We filter them out here by checking parent. // HACK: default choice node is not part of paths. We filter them out here by checking parent.
@@ -95,16 +82,12 @@ export class HaTracePathDetails extends LitElement {
] as ChooseActionTraceStep[]; ] as ChooseActionTraceStep[];
if (parentTraceInfo && parentTraceInfo[0]?.result?.choice === "default") { if (parentTraceInfo && parentTraceInfo[0]?.result?.choice === "default") {
return this.hass!.localize( return "The default action was executed because no options matched.";
"ui.panel.config.automation.trace.path.default_action_executed"
);
} }
} }
if (!(this.selected.path in paths)) { if (!(this.selected.path in paths)) {
return this.hass!.localize( return "This node was not executed and so no further trace information is available.";
"ui.panel.config.automation.trace.path.no_further_execution"
);
} }
const parts: TemplateResult[][] = []; const parts: TemplateResult[][] = [];
@@ -132,53 +115,29 @@ export class HaTracePathDetails extends LitElement {
trace as any; trace as any;
if (result?.enabled === false) { if (result?.enabled === false) {
return html`${this.hass!.localize( return html`This node was disabled and skipped during execution so
"ui.panel.config.automation.trace.path.disabled_step" no further trace information is available.`;
)}`;
} }
return html` return html`
${curPath === this.selected.path ${curPath === this.selected.path
? "" ? ""
: html`<h2> : html`<h2>${curPath.substr(this.selected.path.length + 1)}</h2>`}
${curPath.substring(this.selected.path.length + 1)} ${data.length === 1 ? "" : html`<h3>Iteration ${idx + 1}</h3>`}
</h2>`} Executed:
${data.length === 1 ${formatDateTimeWithSeconds(
? nothing new Date(timestamp),
: html`<h3> this.hass.locale,
${this.hass!.localize( this.hass.config
"ui.panel.config.automation.trace.path.iteration", )}<br />
{ number: idx + 1 }
)}
</h3>`}
${this.hass!.localize(
"ui.panel.config.automation.trace.path.executed",
{
time: formatDateTimeWithSeconds(
new Date(timestamp),
this.hass.locale,
this.hass.config
),
}
)}
<br />
${result ${result
? html`${this.hass!.localize( ? html`Result:
"ui.panel.config.automation.trace.path.result"
)}
<pre>${dump(result)}</pre>` <pre>${dump(result)}</pre>`
: error : error
? html`<div class="error"> ? html`<div class="error">Error: ${error}</div>`
${this.hass!.localize( : ""}
"ui.panel.config.automation.trace.path.error",
{
error: error,
}
)}
</div>`
: nothing}
${Object.keys(rest).length === 0 ${Object.keys(rest).length === 0
? nothing ? ""
: html`<pre>${dump(rest)}</pre>`} : html`<pre>${dump(rest)}</pre>`}
`; `;
}) })
@@ -190,49 +149,30 @@ export class HaTracePathDetails extends LitElement {
private _renderSelectedConfig() { private _renderSelectedConfig() {
if (!this.selected?.path) { if (!this.selected?.path) {
return nothing; return "";
} }
const config = getDataFromPath(this.trace!.config, this.selected.path); const config = getDataFromPath(this.trace!.config, this.selected.path);
return config return config
? html`<ha-code-editor ? html`<ha-code-editor
.value=${dump(config).trimEnd()} .value=${dump(config).trimRight()}
readOnly readOnly
dir="ltr" dir="ltr"
></ha-code-editor>` ></ha-code-editor>`
: this.hass!.localize( : "Unable to find config";
"ui.panel.config.automation.trace.path.unable_to_find_config"
);
} }
private _renderChangedVars() { private _renderChangedVars() {
const paths = this.trace.trace; const paths = this.trace.trace;
const data: ActionTraceStep[] = paths[this.selected.path]; const data: ActionTraceStep[] = paths[this.selected.path];
if (data === undefined) {
return html`<div class="padded-box">
${this.hass!.localize(
"ui.panel.config.automation.trace.path.step_not_executed"
)}
</div>`;
}
return html` return html`
<div class="padded-box"> <div class="padded-box">
${data.map( ${data.map(
(trace, idx) => html` (trace, idx) => html`
${data.length > 1 ${idx > 0 ? html`<p>Iteration ${idx + 1}</p>` : ""}
? html`<p>
${this.hass!.localize(
"ui.panel.config.automation.trace.path.iteration",
{ number: idx + 1 }
)}
</p>`
: ""}
${Object.keys(trace.changed_variables || {}).length === 0 ${Object.keys(trace.changed_variables || {}).length === 0
? this.hass!.localize( ? "No variables changed"
"ui.panel.config.automation.trace.path.no_variables_changed" : html`<pre>${dump(trace.changed_variables).trimRight()}</pre>`}
)
: html`<pre>${dump(trace.changed_variables).trimEnd()}</pre>`}
` `
)} )}
</div> </div>
@@ -246,11 +186,7 @@ export class HaTracePathDetails extends LitElement {
const index = trackedPaths.indexOf(this.selected.path); const index = trackedPaths.indexOf(this.selected.path);
if (index === -1) { if (index === -1) {
return html`<div class="padded-box"> return html`<div class="padded-box">Node not tracked.</div>`;
${this.hass!.localize(
"ui.panel.config.automation.trace.path.step_not_executed"
)}
</div>`;
} }
let entries: LogbookEntry[]; let entries: LogbookEntry[];
@@ -298,9 +234,7 @@ export class HaTracePathDetails extends LitElement {
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note> <hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
` `
: html`<div class="padded-box"> : html`<div class="padded-box">
${this.hass!.localize( No Logbook entries found for this step.
"ui.panel.config.automation.trace.path.no_logbook_entries"
)}
</div>`; </div>`;
} }

View File

@@ -125,6 +125,10 @@ export class HatGraphNode extends LitElement {
:host([notEnabled]:hover) circle { :host([notEnabled]:hover) circle {
--stroke-clr: var(--disabled-hover-clr); --stroke-clr: var(--disabled-hover-clr);
} }
svg {
width: 100%;
height: 100%;
}
circle, circle,
path.connector { path.connector {
stroke: var(--stroke-clr); stroke: var(--stroke-clr);

View File

@@ -5,8 +5,6 @@ import {
mdiCallSplit, mdiCallSplit,
mdiCodeBraces, mdiCodeBraces,
mdiDevices, mdiDevices,
mdiDotsHorizontal,
mdiExcavator,
mdiGestureDoubleTap, mdiGestureDoubleTap,
mdiHandBackRight, mdiHandBackRight,
mdiPalette, mdiPalette,
@@ -15,12 +13,10 @@ import {
mdiRoomService, mdiRoomService,
mdiShuffleDisabled, mdiShuffleDisabled,
mdiTimerOutline, mdiTimerOutline,
mdiTools,
mdiTrafficLight, mdiTrafficLight,
} from "@mdi/js"; } from "@mdi/js";
import { AutomationElementGroup } from "./automation";
export const ACTION_ICONS = { export const ACTION_TYPES = {
condition: mdiAbTesting, condition: mdiAbTesting,
delay: mdiTimerOutline, delay: mdiTimerOutline,
event: mdiGestureDoubleTap, event: mdiGestureDoubleTap,
@@ -38,44 +34,6 @@ export const ACTION_ICONS = {
variables: mdiApplicationVariableOutline, variables: mdiApplicationVariableOutline,
} as const; } as const;
export const YAML_ONLY_ACTION_TYPES = new Set<keyof typeof ACTION_ICONS>([ export const YAML_ONLY_ACTION_TYPES = new Set<keyof typeof ACTION_TYPES>([
"variables", "variables",
]); ]);
export const ACTION_GROUPS: AutomationElementGroup = {
device_id: {},
helpers: {
icon: mdiTools,
members: {},
},
building_blocks: {
icon: mdiExcavator,
members: {
condition: {},
delay: {},
wait_template: {},
wait_for_trigger: {},
repeat: {},
choose: {},
if: {},
stop: {},
parallel: {},
variables: {},
},
},
other: {
icon: mdiDotsHorizontal,
members: {
event: {},
service: {},
},
},
} as const;
export const SERVICE_PREFIX = "__SERVICE__";
export const isService = (key: string | undefined): boolean | undefined =>
key?.startsWith(SERVICE_PREFIX);
export const getService = (key: string): string =>
key.substring(SERVICE_PREFIX.length);

View File

@@ -1,10 +1,11 @@
import { Connection, createCollection } from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store";
import { stringCompare } from "../common/string/compare"; import { stringCompare } from "../common/string/compare";
import { debounce } from "../common/util/debounce";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { DeviceRegistryEntry } from "./device_registry"; import { DeviceRegistryEntry } from "./device_registry";
import { EntityRegistryEntry } from "./entity_registry"; import { EntityRegistryEntry } from "./entity_registry";
export { subscribeAreaRegistry } from "./ws-area_registry";
export interface AreaRegistryEntry { export interface AreaRegistryEntry {
area_id: string; area_id: string;
name: string; name: string;
@@ -52,6 +53,45 @@ export const deleteAreaRegistryEntry = (hass: HomeAssistant, areaId: string) =>
area_id: areaId, area_id: areaId,
}); });
const fetchAreaRegistry = (conn: Connection) =>
conn
.sendMessagePromise({
type: "config/area_registry/list",
})
.then((areas) =>
(areas as AreaRegistryEntry[]).sort((ent1, ent2) =>
stringCompare(ent1.name, ent2.name)
)
);
const subscribeAreaRegistryUpdates = (
conn: Connection,
store: Store<AreaRegistryEntry[]>
) =>
conn.subscribeEvents(
debounce(
() =>
fetchAreaRegistry(conn).then((areas: AreaRegistryEntry[]) =>
store.setState(areas, true)
),
500,
true
),
"area_registry_updated"
);
export const subscribeAreaRegistry = (
conn: Connection,
onChange: (areas: AreaRegistryEntry[]) => void
) =>
createCollection<AreaRegistryEntry[]>(
"_areaRegistry",
fetchAreaRegistry,
subscribeAreaRegistryUpdates,
conn,
onChange
);
export const getAreaEntityLookup = ( export const getAreaEntityLookup = (
entities: EntityRegistryEntry[] entities: EntityRegistryEntry[]
): AreaEntityLookup => { ): AreaEntityLookup => {
@@ -88,7 +128,7 @@ export const areaCompare =
(entries?: HomeAssistant["areas"], order?: string[]) => (entries?: HomeAssistant["areas"], order?: string[]) =>
(a: string, b: string) => { (a: string, b: string) => {
const indexA = order ? order.indexOf(a) : -1; const indexA = order ? order.indexOf(a) : -1;
const indexB = order ? order.indexOf(b) : -1; const indexB = order ? order.indexOf(b) : 1;
if (indexA === -1 && indexB === -1) { if (indexA === -1 && indexB === -1) {
const nameA = entries?.[a]?.name ?? a; const nameA = entries?.[a]?.name ?? a;
const nameB = entries?.[b]?.name ?? b; const nameB = entries?.[b]?.name ?? b;

View File

@@ -18,11 +18,6 @@ export interface AssistPipeline {
wake_word_id: string | null; wake_word_id: string | null;
} }
export interface AssistDevice {
device_id: string;
pipeline_entity: string;
}
export interface AssistPipelineMutableParams { export interface AssistPipelineMutableParams {
name: string; name: string;
language: string; language: string;
@@ -371,8 +366,3 @@ export const fetchAssistPipelineLanguages = (hass: HomeAssistant) =>
hass.callWS<{ languages: string[] }>({ hass.callWS<{ languages: string[] }>({
type: "assist_pipeline/language/list", type: "assist_pipeline/language/list",
}); });
export const listAssistDevices = (hass: HomeAssistant) =>
hass.callWS<AssistDevice[]>({
type: "assist_pipeline/device/list",
});

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