mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-30 04:02:17 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1dff6ad30 |
+1
-6
@@ -25,18 +25,13 @@ Design:
|
||||
|
||||
Dependencies:
|
||||
- changed-files:
|
||||
# Match when only these files are changed (i.e. don't match PRs that happen to add or remove packages)
|
||||
- any-glob-to-all-files:
|
||||
- any-glob-to-any-file:
|
||||
- package.json
|
||||
- renovate.json
|
||||
- yarn.lock
|
||||
- .yarn/**
|
||||
- .yarnrc.yml
|
||||
- .nvmrc
|
||||
# Dependabot and Renovate branches always match (i.e. compatibility tweaks by members considered minor)
|
||||
- head-branch:
|
||||
- "^renovate/"
|
||||
- "^dependabot/"
|
||||
|
||||
GitHub Actions:
|
||||
- changed-files:
|
||||
|
||||
@@ -48,8 +48,6 @@ jobs:
|
||||
run: yarn run lint:eslint --quiet
|
||||
- name: Run tsc
|
||||
run: yarn run lint:types
|
||||
- name: Run lit-analyzer
|
||||
run: yarn run lint:lit --quiet
|
||||
- name: Run prettier
|
||||
run: yarn run lint:prettier
|
||||
test:
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
echo "home-assistant-frontend==$version" > ./requirements.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2024.01.0
|
||||
uses: home-assistant/wheels@2023.10.5
|
||||
with:
|
||||
abi: cp311
|
||||
tag: musllinux_1_2
|
||||
|
||||
@@ -426,7 +426,6 @@ gulp.task(
|
||||
"fetch-nightly-translations",
|
||||
gulp.series("clean-translations", "ensure-translations-build-dir")
|
||||
),
|
||||
gulp.parallel("create-test-metadata", "create-test-translation"),
|
||||
"build-master-translation",
|
||||
"build-merged-translations",
|
||||
"build-translation-fragment-supervisor",
|
||||
|
||||
@@ -7,9 +7,6 @@ const TerserPlugin = require("terser-webpack-plugin");
|
||||
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
|
||||
const log = require("fancy-log");
|
||||
const WebpackBar = require("webpackbar");
|
||||
const {
|
||||
TransformAsyncModulesPlugin,
|
||||
} = require("transform-async-modules-webpack-plugin");
|
||||
const paths = require("./paths.cjs");
|
||||
const bundle = require("./bundle.cjs");
|
||||
|
||||
@@ -145,6 +142,17 @@ const createWebpackConfig = ({
|
||||
),
|
||||
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 StatsWriterPlugin({
|
||||
@@ -155,8 +163,6 @@ const createWebpackConfig = ({
|
||||
stats: { assets: true, chunks: true, modules: true },
|
||||
transform: (stats) => JSON.stringify(filterStats(stats)),
|
||||
}),
|
||||
!latestBuild &&
|
||||
new TransformAsyncModulesPlugin({ browserslistEnv: "legacy" }),
|
||||
].filter(Boolean),
|
||||
resolve: {
|
||||
extensions: [".ts", ".js", ".json"],
|
||||
|
||||
@@ -11,7 +11,7 @@ class DemoBlackWhiteRow extends LitElement {
|
||||
|
||||
@property() value!: any;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@property() disabled = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
|
||||
@@ -15,7 +15,7 @@ class DemoCard extends LitElement {
|
||||
|
||||
@property() public config!: DemoCardConfig;
|
||||
|
||||
@property({ type: Boolean }) public showConfig = false;
|
||||
@property() public showConfig = false;
|
||||
|
||||
@state() private _size?: number;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class DemoMoreInfo extends LitElement {
|
||||
|
||||
@property() public entityId!: string;
|
||||
|
||||
@property({ type: Boolean }) public showConfig = false;
|
||||
@property() public showConfig!: boolean;
|
||||
|
||||
render() {
|
||||
const state = this._getState(this.entityId, this.hass.states);
|
||||
|
||||
@@ -80,7 +80,7 @@ const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [
|
||||
];
|
||||
|
||||
@customElement("demo-automation-editor-condition")
|
||||
export class DemoAutomationEditorCondition extends LitElement {
|
||||
class DemoHaAutomationEditorCondition extends LitElement {
|
||||
@state() private hass!: HomeAssistant;
|
||||
|
||||
@state() private _disabled = false;
|
||||
@@ -155,6 +155,6 @@ export class DemoAutomationEditorCondition extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-automation-editor-condition": DemoAutomationEditorCondition;
|
||||
"demo-ha-automation-editor-condition": DemoHaAutomationEditorCondition;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
||||
];
|
||||
|
||||
@customElement("demo-automation-editor-trigger")
|
||||
export class DemoAutomationEditorTrigger extends LitElement {
|
||||
class DemoHaAutomationEditorTrigger extends LitElement {
|
||||
@state() private hass!: HomeAssistant;
|
||||
|
||||
@state() private _disabled = false;
|
||||
@@ -201,6 +201,6 @@ export class DemoAutomationEditorTrigger extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-automation-editor-trigger": DemoAutomationEditorTrigger;
|
||||
"demo-ha-automation-editor-trigger": DemoHaAutomationEditorTrigger;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ const CONFIGS = [
|
||||
];
|
||||
|
||||
@customElement("demo-lovelace-media-player-row")
|
||||
export class DemoLovelaceMediaPlayerRow extends LitElement {
|
||||
class DemoHuiMediaPlayerRow extends LitElement {
|
||||
@query("#demos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -73,6 +73,6 @@ export class DemoLovelaceMediaPlayerRow extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-lovelace-media-player-row": DemoLovelaceMediaPlayerRow;
|
||||
"demo-lovelace-media-player-rows": DemoHuiMediaPlayerRow;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,9 +59,3 @@ export class DemoUtilLongPress extends LitElement {
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-misc-util-long-press": DemoUtilLongPress;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,9 +140,3 @@ export class HassioAddonRepositoryEl extends LitElement {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-repository": HassioAddonRepositoryEl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,9 +248,3 @@ export class HassioAddonStore extends LitElement {
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-addon-store": HassioAddonStore;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { mdiFolder, mdiPuzzle } from "@mdi/js";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
@@ -14,7 +16,6 @@ import { formatDateTime } from "../../../src/common/datetime/format_date_time";
|
||||
import { LocalizeFunc } from "../../../src/common/translations/localize";
|
||||
import "../../../src/components/ha-checkbox";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "../../../src/components/ha-textfield";
|
||||
import "../../../src/components/ha-radio";
|
||||
import type { HaRadio } from "../../../src/components/ha-radio";
|
||||
import {
|
||||
@@ -24,9 +25,12 @@ import {
|
||||
} from "../../../src/data/hassio/backup";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
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 type { HaTextField } from "../../../src/components/ha-textfield";
|
||||
|
||||
type BackupOrRestoreKey = keyof TranslationDict["supervisor"]["backup"] &
|
||||
keyof TranslationDict["ui"]["panel"]["page-onboarding"]["restore"];
|
||||
@@ -96,7 +100,7 @@ export class SupervisorBackupContent extends LitElement {
|
||||
|
||||
@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) {
|
||||
super.willUpdate(changedProps);
|
||||
@@ -147,13 +151,13 @@ export class SupervisorBackupContent extends LitElement {
|
||||
)
|
||||
: this.backup.date}
|
||||
</div>`
|
||||
: html`<ha-textfield
|
||||
: html`<paper-input
|
||||
name="backupName"
|
||||
.label=${this._localize("name")}
|
||||
.value=${this.backupName}
|
||||
@change=${this._handleTextValueChanged}
|
||||
@value-changed=${this._handleTextValueChanged}
|
||||
>
|
||||
</ha-textfield>`}
|
||||
</paper-input>`}
|
||||
${!this.backup || this.backup.type === "full"
|
||||
? html`<div class="sub-header">
|
||||
${!this.backup
|
||||
@@ -261,23 +265,23 @@ export class SupervisorBackupContent extends LitElement {
|
||||
: ""}
|
||||
${this.backupHasPassword
|
||||
? html`
|
||||
<ha-textfield
|
||||
<paper-input
|
||||
.label=${this._localize("password")}
|
||||
type="password"
|
||||
name="backupPassword"
|
||||
.value=${this.backupPassword}
|
||||
@change=${this._handleTextValueChanged}
|
||||
@value-changed=${this._handleTextValueChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
</paper-input>
|
||||
${!this.backup
|
||||
? html`<ha-textfield
|
||||
? html` <paper-input
|
||||
.label=${this._localize("confirm_password")}
|
||||
type="password"
|
||||
name="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;
|
||||
}
|
||||
|
||||
private _handleTextValueChanged(ev: InputEvent) {
|
||||
const input = ev.currentTarget as HaTextField;
|
||||
this[input.name!] = input.value;
|
||||
private _handleTextValueChanged(ev: ValueChangedEvent<string>) {
|
||||
const input = ev.currentTarget as PaperInputElement;
|
||||
this[input.name!] = ev.detail.value;
|
||||
}
|
||||
|
||||
private _toggleHasPassword(): void {
|
||||
|
||||
@@ -128,7 +128,6 @@ class HassioAddons extends LitElement {
|
||||
ha-card {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
direction: ltr;
|
||||
}
|
||||
.search {
|
||||
position: sticky;
|
||||
|
||||
@@ -133,8 +133,6 @@ class HassioDashboard extends LitElement {
|
||||
position: fixed;
|
||||
right: calc(16px + env(safe-area-inset-right));
|
||||
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;
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -151,9 +151,3 @@ export class HassioUpdate extends LitElement {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hassio-update": HassioUpdate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import "@material/mwc-list/mwc-list-item";
|
||||
import "@material/mwc-tab";
|
||||
import "@material/mwc-tab-bar";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
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-expansion-panel";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-textfield";
|
||||
import "../../../../src/components/ha-header-bar";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-radio";
|
||||
@@ -34,7 +34,6 @@ import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { HassioNetworkDialogParams } from "./show-dialog-network";
|
||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
||||
|
||||
const IP_VERSIONS = ["ipv4", "ipv6"];
|
||||
|
||||
@@ -246,7 +245,7 @@ export class DialogHassioNetwork
|
||||
${this._wifiConfiguration.auth === "wpa-psk" ||
|
||||
this._wifiConfiguration.auth === "wep"
|
||||
? html`
|
||||
<ha-textfield
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
type="password"
|
||||
id="psk"
|
||||
@@ -254,9 +253,10 @@ export class DialogHassioNetwork
|
||||
"dialog.network.wifi_password"
|
||||
)}
|
||||
version="wifi"
|
||||
@change=${this._handleInputValueChangedWifi}
|
||||
@value-changed=${this
|
||||
._handleInputValueChangedWifi}
|
||||
>
|
||||
</ha-textfield>
|
||||
</paper-input>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
@@ -358,33 +358,33 @@ export class DialogHassioNetwork
|
||||
</div>
|
||||
${this._interface![version].method === "static"
|
||||
? html`
|
||||
<ha-textfield
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
id="address"
|
||||
.label=${this.supervisor.localize("dialog.network.ip_netmask")}
|
||||
.version=${version}
|
||||
.value=${this._toString(this._interface![version].address)}
|
||||
@change=${this._handleInputValueChanged}
|
||||
@value-changed=${this._handleInputValueChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
<ha-textfield
|
||||
</paper-input>
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
id="gateway"
|
||||
.label=${this.supervisor.localize("dialog.network.gateway")}
|
||||
.version=${version}
|
||||
.value=${this._interface![version].gateway}
|
||||
@change=${this._handleInputValueChanged}
|
||||
@value-changed=${this._handleInputValueChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
<ha-textfield
|
||||
</paper-input>
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
id="nameservers"
|
||||
.label=${this.supervisor.localize("dialog.network.dns_servers")}
|
||||
.version=${version}
|
||||
.value=${this._toString(this._interface![version].nameservers)}
|
||||
@change=${this._handleInputValueChanged}
|
||||
@value-changed=${this._handleInputValueChanged}
|
||||
>
|
||||
</ha-textfield>
|
||||
</paper-input>
|
||||
`
|
||||
: ""}
|
||||
</ha-expansion-panel>
|
||||
@@ -517,11 +517,11 @@ export class DialogHassioNetwork
|
||||
this.requestUpdate("_wifiConfiguration");
|
||||
}
|
||||
|
||||
private _handleInputValueChanged(ev: Event): void {
|
||||
const source = ev.target as HaTextField;
|
||||
const value = source.value;
|
||||
private _handleInputValueChanged(ev: CustomEvent): void {
|
||||
const value: string | null | undefined = (ev.target as PaperInputElement)
|
||||
.value;
|
||||
const version = (ev.target as any).version as "ipv4" | "ipv6";
|
||||
const id = source.id;
|
||||
const id = (ev.target as PaperInputElement).id;
|
||||
|
||||
if (
|
||||
!value ||
|
||||
@@ -535,10 +535,10 @@ export class DialogHassioNetwork
|
||||
this._interface[version]![id] = value;
|
||||
}
|
||||
|
||||
private _handleInputValueChangedWifi(ev: Event): void {
|
||||
const source = ev.target as HaTextField;
|
||||
const value = source.value;
|
||||
const id = source.id;
|
||||
private _handleInputValueChangedWifi(ev: CustomEvent): void {
|
||||
const value: string | null | undefined = (ev.target as PaperInputElement)
|
||||
.value;
|
||||
const id = (ev.target as PaperInputElement).id;
|
||||
|
||||
if (
|
||||
!value ||
|
||||
@@ -630,7 +630,7 @@ export class DialogHassioNetwork
|
||||
--expansion-panel-summary-padding: 0 16px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
ha-textfield {
|
||||
paper-input {
|
||||
padding: 0 14px;
|
||||
}
|
||||
mwc-list-item {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
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-body";
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
@@ -25,14 +27,12 @@ import {
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
|
||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
|
||||
import "../../../../src/components/ha-textfield";
|
||||
|
||||
@customElement("dialog-hassio-repositories")
|
||||
class HassioRepositoriesDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@query("#repository_input", true) private _optionInput?: HaTextField;
|
||||
@query("#repository_input", true) private _optionInput?: PaperInputElement;
|
||||
|
||||
@state() private _repositories?: HassioAddonRepository[];
|
||||
|
||||
@@ -145,7 +145,7 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
)
|
||||
: html`<paper-item> No repositories </paper-item>`}
|
||||
<div class="layout horizontal bottom">
|
||||
<ha-textfield
|
||||
<paper-input
|
||||
class="flex-auto"
|
||||
id="repository_input"
|
||||
.value=${this._dialogParams!.url || ""}
|
||||
@@ -154,7 +154,7 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
)}
|
||||
@keydown=${this._handleKeyAdd}
|
||||
dialogInitialFocus
|
||||
></ha-textfield>
|
||||
></paper-input>
|
||||
<mwc-button @click=${this._addRepository}>
|
||||
${this._processing
|
||||
? html`<ha-circular-progress
|
||||
|
||||
@@ -29,10 +29,6 @@ import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
|
||||
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
|
||||
import { HomeAssistant, Route } from "../../src/types";
|
||||
import { getTranslation } from "../../src/util/common-translation";
|
||||
import {
|
||||
computeRTLDirection,
|
||||
setDirectionStyles,
|
||||
} from "../../src/common/util/compute_rtl";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -99,7 +95,6 @@ export class SupervisorBaseElement extends urlSyncMixin(
|
||||
|
||||
if (changedProperties.has("_language") || !this.hasUpdated) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ export default {
|
||||
"*.?(c|m){js,ts}": [
|
||||
"eslint --cache --cache-strategy=content --cache-location=node_modules/.cache/eslint/.eslintcache --fix",
|
||||
"prettier --cache --write",
|
||||
"lit-analyzer",
|
||||
],
|
||||
"*.{json,css,md,markdown,html,y?aml}": "prettier --cache --write",
|
||||
"translations/*/*.json": (files) =>
|
||||
|
||||
+28
-28
@@ -13,8 +13,8 @@
|
||||
"lint:prettier": "prettier . --cache --check",
|
||||
"format:prettier": "prettier . --cache --write",
|
||||
"lint:types": "tsc",
|
||||
"lint:lit": "lit-analyzer \"{.,*}/src/**/*.ts\"",
|
||||
"lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:types && yarn run lint:lit",
|
||||
"lint:lit": "lit-analyzer \"**/src/**/*.ts\" --format markdown --outFile result.md",
|
||||
"lint": "yarn run lint:eslint && yarn run lint:prettier && yarn run lint:types",
|
||||
"format": "yarn run format:eslint && yarn run format:prettier",
|
||||
"postinstall": "husky install",
|
||||
"prepack": "pinst --disable",
|
||||
@@ -25,15 +25,15 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.23.8",
|
||||
"@babel/runtime": "7.23.6",
|
||||
"@braintree/sanitize-url": "7.0.0",
|
||||
"@codemirror/autocomplete": "6.11.1",
|
||||
"@codemirror/commands": "6.3.3",
|
||||
"@codemirror/language": "6.10.0",
|
||||
"@codemirror/commands": "6.3.2",
|
||||
"@codemirror/language": "6.9.3",
|
||||
"@codemirror/legacy-modes": "6.3.3",
|
||||
"@codemirror/search": "6.5.5",
|
||||
"@codemirror/state": "6.4.0",
|
||||
"@codemirror/view": "6.23.0",
|
||||
"@codemirror/state": "6.3.3",
|
||||
"@codemirror/view": "6.22.3",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.12.0",
|
||||
"@formatjs/intl-displaynames": "6.6.4",
|
||||
@@ -81,8 +81,9 @@
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/web": "=1.1.1",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@mdi/js": "7.3.67",
|
||||
"@mdi/svg": "7.3.67",
|
||||
"@polymer/paper-input": "3.2.1",
|
||||
"@polymer/paper-item": "3.0.1",
|
||||
"@polymer/paper-listbox": "3.0.1",
|
||||
"@polymer/paper-tabs": "3.1.0",
|
||||
@@ -100,7 +101,7 @@
|
||||
"app-datepicker": "5.1.1",
|
||||
"chart.js": "4.4.1",
|
||||
"comlink": "4.4.1",
|
||||
"core-js": "3.35.0",
|
||||
"core-js": "3.34.0",
|
||||
"cropperjs": "1.6.1",
|
||||
"date-fns": "2.30.0",
|
||||
"date-fns-tz": "2.0.0",
|
||||
@@ -118,7 +119,7 @@
|
||||
"leaflet-draw": "1.0.4",
|
||||
"lit": "2.8.0",
|
||||
"luxon": "3.4.4",
|
||||
"marked": "11.1.1",
|
||||
"marked": "11.1.0",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "0.3.2",
|
||||
@@ -149,13 +150,13 @@
|
||||
"xss": "1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.23.7",
|
||||
"@babel/core": "7.23.6",
|
||||
"@babel/helper-define-polyfill-provider": "0.4.4",
|
||||
"@babel/plugin-proposal-decorators": "7.23.7",
|
||||
"@babel/plugin-transform-runtime": "7.23.7",
|
||||
"@babel/preset-env": "7.23.8",
|
||||
"@babel/plugin-proposal-decorators": "7.23.6",
|
||||
"@babel/plugin-transform-runtime": "7.23.6",
|
||||
"@babel/preset-env": "7.23.6",
|
||||
"@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",
|
||||
"@lokalise/node-api": "12.1.0",
|
||||
"@octokit/auth-oauth-device": "6.0.1",
|
||||
@@ -168,14 +169,14 @@
|
||||
"@rollup/plugin-node-resolve": "15.2.3",
|
||||
"@rollup/plugin-replace": "5.0.5",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.13",
|
||||
"@types/chromecast-caf-receiver": "6.0.12",
|
||||
"@types/chromecast-caf-sender": "1.0.8",
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/html-minifier-terser": "7.0.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/leaflet": "1.9.8",
|
||||
"@types/leaflet-draw": "1.0.11",
|
||||
"@types/luxon": "3.4.0",
|
||||
"@types/luxon": "3.3.7",
|
||||
"@types/mocha": "10.0.6",
|
||||
"@types/qrcode": "1.5.5",
|
||||
"@types/serve-handler": "6.1.4",
|
||||
@@ -183,13 +184,13 @@
|
||||
"@types/tar": "6.1.10",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||
"@typescript-eslint/parser": "6.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "6.16.0",
|
||||
"@typescript-eslint/parser": "6.16.0",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@web/dev-server-rollup": "0.4.1",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"chai": "5.0.0",
|
||||
"chai": "4.3.10",
|
||||
"del": "7.1.0",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
@@ -216,27 +217,26 @@
|
||||
"instant-mocha": "1.5.2",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "15.2.0",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lit-analyzer": "2.0.2",
|
||||
"lodash.template": "4.5.0",
|
||||
"magic-string": "0.30.5",
|
||||
"map-stream": "0.0.7",
|
||||
"mocha": "10.2.0",
|
||||
"object-hash": "3.0.0",
|
||||
"open": "10.0.3",
|
||||
"open": "10.0.1",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.1.1",
|
||||
"rollup": "2.79.1",
|
||||
"rollup-plugin-string": "3.0.0",
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"rollup-plugin-visualizer": "5.11.0",
|
||||
"serve-handler": "6.1.5",
|
||||
"sinon": "17.0.1",
|
||||
"source-map-url": "0.4.1",
|
||||
"systemjs": "6.14.3",
|
||||
"systemjs": "6.14.2",
|
||||
"tar": "6.2.0",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"transform-async-modules-webpack-plugin": "1.0.2",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"ts-lit-plugin": "2.0.1",
|
||||
"typescript": "5.3.3",
|
||||
"vinyl-buffer": "1.0.1",
|
||||
"vinyl-source-stream": "2.0.0",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20240112.0"
|
||||
version = "20231228.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
import fs from "fs";
|
||||
import util from "util";
|
||||
import child_process from "child_process";
|
||||
|
||||
const exec = util.promisify(child_process.exec);
|
||||
const fs = require("fs");
|
||||
const util = require("util");
|
||||
const exec = util.promisify(require("child_process").exec);
|
||||
|
||||
function patch(version) {
|
||||
const parts = version.split(".");
|
||||
@@ -21,7 +18,7 @@ function today() {
|
||||
|
||||
function auto(version) {
|
||||
const todayVersion = today();
|
||||
if (todayVersion.split(".")[0] !== version.split(".")[0]) {
|
||||
if (todayVersion !== version) {
|
||||
return todayVersion;
|
||||
}
|
||||
return patch(version);
|
||||
@@ -47,7 +44,7 @@ async function main(args) {
|
||||
commit = true;
|
||||
} else {
|
||||
method = args.length > 0 && methods[args[0]];
|
||||
commit = args.length > 1 && args[1] === "--commit";
|
||||
commit = args.length > 1 && args[1] == "--commit";
|
||||
}
|
||||
|
||||
if (!method) {
|
||||
+22
-13
@@ -21,6 +21,7 @@ import {
|
||||
DataEntryFlowStepForm,
|
||||
} from "../data/data_entry_flow";
|
||||
import "./ha-auth-form";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
type State = "loading" | "error" | "step";
|
||||
|
||||
@@ -38,9 +39,7 @@ export class HaAuthFlow extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public step?: DataEntryFlowStep;
|
||||
|
||||
@property({ type: Boolean }) private initStoreToken = false;
|
||||
|
||||
@state() private _storeToken = false;
|
||||
@property({ type: Boolean }) private storeToken = false;
|
||||
|
||||
@state() private _state: State = "loading";
|
||||
|
||||
@@ -57,10 +56,6 @@ export class HaAuthFlow extends LitElement {
|
||||
willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
this._storeToken = this.initStoreToken;
|
||||
}
|
||||
|
||||
if (!changedProps.has("step")) {
|
||||
return;
|
||||
}
|
||||
@@ -160,6 +155,11 @@ export class HaAuthFlow extends LitElement {
|
||||
}
|
||||
|
||||
private _renderForm() {
|
||||
const showBack =
|
||||
this.step?.type === "form" &&
|
||||
this.authProvider?.users &&
|
||||
!["select_mfa_module", "mfa"].includes(this.step.step_id);
|
||||
|
||||
switch (this._state) {
|
||||
case "step":
|
||||
if (this.step == null) {
|
||||
@@ -168,7 +168,12 @@ export class HaAuthFlow extends LitElement {
|
||||
|
||||
return html`
|
||||
${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
|
||||
raised
|
||||
@click=${this._handleSubmit}
|
||||
@@ -222,7 +227,7 @@ export class HaAuthFlow extends LitElement {
|
||||
</h1>
|
||||
${this._computeStepDescription(step)}
|
||||
<ha-auth-form
|
||||
.data=${this._stepData!}
|
||||
.data=${this._stepData}
|
||||
.schema=${autocompleteLoginFields(step.data_schema)}
|
||||
.error=${step.errors}
|
||||
.disabled=${this._submitting}
|
||||
@@ -241,7 +246,7 @@ export class HaAuthFlow extends LitElement {
|
||||
)}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this._storeToken}
|
||||
.checked=${this.storeToken}
|
||||
@change=${this._storeTokenChanged}
|
||||
></ha-checkbox>
|
||||
</ha-formfield>
|
||||
@@ -264,7 +269,7 @@ export class HaAuthFlow extends LitElement {
|
||||
}
|
||||
|
||||
private _storeTokenChanged(e: CustomEvent<HTMLInputElement>) {
|
||||
this._storeToken = (e.currentTarget as HTMLInputElement).checked;
|
||||
this.storeToken = (e.currentTarget as HTMLInputElement).checked;
|
||||
}
|
||||
|
||||
private async _providerChanged(newProvider?: AuthProvider) {
|
||||
@@ -298,7 +303,7 @@ export class HaAuthFlow extends LitElement {
|
||||
this.redirectUri!,
|
||||
data.result,
|
||||
this.oauth2State,
|
||||
this._storeToken
|
||||
this.storeToken
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -380,7 +385,7 @@ export class HaAuthFlow extends LitElement {
|
||||
this.redirectUri!,
|
||||
newStep.result,
|
||||
this.oauth2State,
|
||||
this._storeToken
|
||||
this.storeToken
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -395,6 +400,10 @@ export class HaAuthFlow extends LitElement {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _localFlow() {
|
||||
fireEvent(this, "default-login-flow", { value: false });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -47,7 +47,7 @@ export class HaAuthTextField extends HaTextField {
|
||||
// TODO: live() directive needs casting for lit-analyzer
|
||||
// https://github.com/runem/lit-analyzer/pull/91/files
|
||||
// TODO: lit-analyzer labels min/max as (number|string) instead of string
|
||||
return html`<input
|
||||
return html` <input
|
||||
aria-labelledby=${ifDefined(ariaLabelledbyOrUndef)}
|
||||
aria-controls=${ifDefined(ariaControlsOrUndef)}
|
||||
aria-describedby=${ifDefined(ariaDescribedbyOrUndef)}
|
||||
|
||||
+41
-27
@@ -13,6 +13,7 @@ import {
|
||||
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||
import { registerServiceWorker } from "../util/register-service-worker";
|
||||
import "./ha-auth-flow";
|
||||
import "./ha-local-auth-flow";
|
||||
|
||||
import("./ha-pick-auth-provider");
|
||||
|
||||
@@ -35,12 +36,12 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
|
||||
@state() private _authProviders?: AuthProvider[];
|
||||
|
||||
@state() private _preselectStoreToken = false;
|
||||
|
||||
@state() private _ownInstance = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _forceDefaultLogin = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const query = extractSearchParamsObject() as AuthUrlSearchParams;
|
||||
@@ -83,7 +84,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
display: block;
|
||||
margin-top: 24px;
|
||||
}
|
||||
ha-auth-flow {
|
||||
ha-auth-flow,
|
||||
ha-local-auth-flow {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
@@ -174,29 +176,44 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
|
||||
<div class="card-content">
|
||||
<div
|
||||
class="card-content"
|
||||
@default-login-flow=${this._handleDefaultLoginFlow}
|
||||
>
|
||||
${!this._authProvider
|
||||
? html`<p>
|
||||
${this.localize("ui.panel.page-authorize.initializing")}
|
||||
</p> `
|
||||
: html`<ha-auth-flow
|
||||
: !this._forceDefaultLogin &&
|
||||
this._authProvider!.users &&
|
||||
this.clientId != null &&
|
||||
this.redirectUri != null
|
||||
? html`<ha-local-auth-flow
|
||||
.clientId=${this.clientId}
|
||||
.redirectUri=${this.redirectUri}
|
||||
.oauth2State=${this.oauth2State}
|
||||
.authProvider=${this._authProvider}
|
||||
.authProviders=${this._authProviders}
|
||||
.localize=${this.localize}
|
||||
.initStoreToken=${this._preselectStoreToken}
|
||||
></ha-auth-flow>
|
||||
${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>
|
||||
`
|
||||
: ""}`}
|
||||
.ownInstance=${this._ownInstance}
|
||||
></ha-local-auth-flow>`
|
||||
: html`<ha-auth-flow
|
||||
.clientId=${this.clientId}
|
||||
.redirectUri=${this.redirectUri}
|
||||
.oauth2State=${this.oauth2State}
|
||||
.authProvider=${this._authProvider}
|
||||
.localize=${this.localize}
|
||||
></ha-auth-flow>
|
||||
${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 class="footer">
|
||||
<ha-language-picker
|
||||
@@ -302,14 +319,13 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (authProviders.providers.length === 0) {
|
||||
if (authProviders.length === 0) {
|
||||
this._error = "No auth providers returned. Unable to finish login.";
|
||||
return;
|
||||
}
|
||||
|
||||
this._authProviders = authProviders.providers;
|
||||
this._authProvider = authProviders.providers[0];
|
||||
this._preselectStoreToken = authProviders.preselect_remember_me;
|
||||
this._authProviders = authProviders;
|
||||
this._authProvider = authProviders[0];
|
||||
} catch (err: any) {
|
||||
this._error = "Unable to fetch auth providers.";
|
||||
// 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) {
|
||||
this._authProvider = ev.detail;
|
||||
}
|
||||
@@ -332,9 +352,3 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-authorize": HaAuthorize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,6 @@ import "../components/ha-list-item";
|
||||
import { AuthProvider } from "../data/auth";
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-pick-auth-provider": HaPickAuthProvider;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"pick-auth-provider": AuthProvider;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
mdiFlash,
|
||||
mdiFlower,
|
||||
mdiFormatListBulleted,
|
||||
mdiFormatListCheckbox,
|
||||
mdiFormTextbox,
|
||||
mdiGauge,
|
||||
mdiGoogleAssistant,
|
||||
@@ -65,7 +64,6 @@ import {
|
||||
mdiTransmissionTower,
|
||||
mdiWater,
|
||||
mdiWaterPercent,
|
||||
mdiWeatherPartlyCloudy,
|
||||
mdiWeatherPouring,
|
||||
mdiWeatherRainy,
|
||||
mdiWeatherWindy,
|
||||
@@ -130,7 +128,6 @@ export const FIXED_DOMAIN_ICONS = {
|
||||
updater: mdiCloudUpload,
|
||||
vacuum: mdiRobotVacuum,
|
||||
wake_word: mdiChatSleep,
|
||||
weather: mdiWeatherPartlyCloudy,
|
||||
zone: mdiMapMarkerRadius,
|
||||
};
|
||||
|
||||
@@ -169,7 +166,6 @@ export const FIXED_DEVICE_CLASS_ICONS = {
|
||||
precipitation_intensity: mdiWeatherPouring,
|
||||
pressure: mdiGauge,
|
||||
reactive_power: mdiFlash,
|
||||
shopping_List: mdiFormatListCheckbox,
|
||||
signal_strength: mdiWifi,
|
||||
sound_pressure: mdiEarHearing,
|
||||
speed: mdiSpeedometer,
|
||||
@@ -254,7 +250,6 @@ export const DOMAINS_INPUT_ROW = [
|
||||
"text",
|
||||
"time",
|
||||
"vacuum",
|
||||
"valve",
|
||||
];
|
||||
|
||||
/** States that we consider "off". */
|
||||
@@ -273,7 +268,6 @@ export const DOMAINS_TOGGLE = new Set([
|
||||
"group",
|
||||
"automation",
|
||||
"humidifier",
|
||||
"valve",
|
||||
]);
|
||||
|
||||
/** Domains that have a dynamic entity image / picture. */
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { HassConfig } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { DateFormat, FrontendLocaleData } from "../../data/translation";
|
||||
import { FrontendLocaleData, DateFormat } from "../../data/translation";
|
||||
import "../../resources/intl-polyfill";
|
||||
import { resolveTimeZone } from "./resolve-time-zone";
|
||||
|
||||
// Tuesday, August 10
|
||||
export const formatDateWeekdayDay = (
|
||||
@@ -17,7 +16,7 @@ const formatDateWeekdayDayMem = memoizeOne(
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -34,7 +33,7 @@ const formatDateMem = memoizeOne(
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -51,7 +50,7 @@ const formatDateShortMem = memoizeOne(
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -106,7 +105,7 @@ const formatDateNumericMem = memoizeOne(
|
||||
year: "numeric",
|
||||
month: "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",
|
||||
month: "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, {
|
||||
day: "numeric",
|
||||
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, {
|
||||
month: "long",
|
||||
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) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
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) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
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) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
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) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "short",
|
||||
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { FrontendLocaleData } from "../../data/translation";
|
||||
import "../../resources/intl-polyfill";
|
||||
import { formatDateNumeric } from "./format_date";
|
||||
import { formatTime } from "./format_time";
|
||||
import { resolveTimeZone } from "./resolve-time-zone";
|
||||
import { useAmPm } from "./use_am_pm";
|
||||
|
||||
// August 9, 2021, 8:23 AM
|
||||
@@ -23,7 +22,7 @@ const formatDateTimeMem = memoizeOne(
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
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",
|
||||
minute: "2-digit",
|
||||
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",
|
||||
minute: "2-digit",
|
||||
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",
|
||||
second: "2-digit",
|
||||
hourCycle: useAmPm(locale) ? "h12" : "h23",
|
||||
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { HassConfig } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import "../../resources/intl-polyfill";
|
||||
import { resolveTimeZone } from "./resolve-time-zone";
|
||||
import { useAmPm } from "./use_am_pm";
|
||||
|
||||
// 9:15 PM || 21:15
|
||||
@@ -18,7 +17,7 @@ const formatTimeMem = memoizeOne(
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
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",
|
||||
second: "2-digit",
|
||||
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",
|
||||
minute: "2-digit",
|
||||
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",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
timeZone: resolveTimeZone(locale.time_zone, serverTimeZone),
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../resources/intl-polyfill";
|
||||
|
||||
export const localizeWeekdays = memoizeOne(
|
||||
(language: string, short: boolean): string[] => {
|
||||
|
||||
@@ -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;
|
||||
@@ -50,7 +50,6 @@ export const FIXED_DOMAIN_STATES = {
|
||||
timer: ["active", "idle", "paused"],
|
||||
update: ["on", "off"],
|
||||
vacuum: ["cleaning", "docked", "error", "idle", "paused", "returning"],
|
||||
valve: ["closed", "closing", "open", "opening"],
|
||||
weather: [
|
||||
"clear-night",
|
||||
"cloudy",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { mainWindow } from "../dom/get_main_window";
|
||||
|
||||
export const extractSearchParamsObject = (): Record<string, string> => {
|
||||
const query = {};
|
||||
const searchParams = new URLSearchParams(mainWindow.location.search);
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
for (const [key, value] of searchParams.entries()) {
|
||||
query[key] = value;
|
||||
}
|
||||
@@ -10,7 +8,7 @@ export const extractSearchParamsObject = (): Record<string, string> => {
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -23,7 +21,7 @@ export const createSearchParam = (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]) => {
|
||||
urlParams.set(key, value);
|
||||
});
|
||||
@@ -31,7 +29,7 @@ export const addSearchParam = (params: Record<string, 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);
|
||||
return urlParams.toString();
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public data: TimelineEntity[] = [];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@property() public names?: Record<string, string>;
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public historyData!: HistoryResult;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@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: Number }) public hoursToShow?: number;
|
||||
@property() public hoursToShow?: number;
|
||||
|
||||
@property({ type: Boolean }) public showNames = true;
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { mdiFlash, mdiFlashOff } from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { STATES_OFF } from "../../common/const";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
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 { HomeAssistant } from "../../types";
|
||||
import "../ha-formfield";
|
||||
@@ -24,7 +24,6 @@ const isOn = (stateObj?: HassEntity) =>
|
||||
!STATES_OFF.includes(stateObj.state) &&
|
||||
!isUnavailableState(stateObj.state);
|
||||
|
||||
@customElement("ha-entity-toggle")
|
||||
export class HaEntityToggle extends LitElement {
|
||||
// hass is not a property so that we only re-render on stateObj changes
|
||||
public hass?: HomeAssistant;
|
||||
@@ -129,9 +128,6 @@ export class HaEntityToggle extends LitElement {
|
||||
} else if (stateDomain === "cover") {
|
||||
serviceDomain = "cover";
|
||||
service = turnOn ? "open_cover" : "close_cover";
|
||||
} else if (stateDomain === "valve") {
|
||||
serviceDomain = "valve";
|
||||
service = turnOn ? "open_valve" : "close_valve";
|
||||
} else if (stateDomain === "group") {
|
||||
serviceDomain = "homeassistant";
|
||||
service = turnOn ? "turn_on" : "turn_off";
|
||||
@@ -179,8 +175,4 @@ export class HaEntityToggle extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-entity-toggle": HaEntityToggle;
|
||||
}
|
||||
}
|
||||
customElements.define("ha-entity-toggle", HaEntityToggle);
|
||||
|
||||
@@ -61,7 +61,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
|
||||
@property() public image?: string;
|
||||
|
||||
@property({ type: Boolean }) public showName = false;
|
||||
@property() public showName?: boolean;
|
||||
|
||||
@state() private _timerTimeRemaining?: number;
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export class HaAssistPipelinePicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ type: Boolean }) public includeLastUsed = false;
|
||||
@property() public includeLastUsed = false;
|
||||
|
||||
@state() _pipelines?: AssistPipeline[];
|
||||
|
||||
|
||||
@@ -7,14 +7,15 @@ import { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-big-number")
|
||||
export class HaBigNumber extends LitElement {
|
||||
@property({ type: Number }) public value!: number;
|
||||
@property() public value!: number;
|
||||
|
||||
@property() public unit?: string;
|
||||
|
||||
@property({ attribute: "unit-position" })
|
||||
public unitPosition: "top" | "bottom" = "top";
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public formatOptions: Intl.NumberFormatOptions = {};
|
||||
|
||||
@@ -18,8 +18,7 @@ export interface datePickerDialogParams {
|
||||
max?: string;
|
||||
locale?: string;
|
||||
firstWeekday?: number;
|
||||
canClear?: boolean;
|
||||
onChange: (value: string | undefined) => void;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
const showDatePickerDialog = (
|
||||
@@ -50,8 +49,6 @@ export class HaDateInput extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public canClear?: boolean;
|
||||
|
||||
render() {
|
||||
return html`<ha-textfield
|
||||
.label=${this.label}
|
||||
@@ -61,7 +58,6 @@ export class HaDateInput extends LitElement {
|
||||
helperPersistent
|
||||
readonly
|
||||
@click=${this._openDialog}
|
||||
@keydown=${this._keyDown}
|
||||
.value=${this.value
|
||||
? formatDateNumeric(
|
||||
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",
|
||||
max: this.max,
|
||||
value: this.value,
|
||||
canClear: this.canClear,
|
||||
onChange: (value) => this._valueChanged(value),
|
||||
locale: this.locale.language,
|
||||
firstWeekday: firstWeekdayIndex(this.locale),
|
||||
});
|
||||
}
|
||||
|
||||
private _keyDown(ev: KeyboardEvent) {
|
||||
if (!this.canClear) {
|
||||
return;
|
||||
}
|
||||
if (["Backspace", "Delete"].includes(ev.key)) {
|
||||
this._valueChanged(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(value: string | undefined) {
|
||||
private _valueChanged(value: string) {
|
||||
if (this.value !== value) {
|
||||
this.value = value;
|
||||
fireEvent(this, "change");
|
||||
|
||||
@@ -54,9 +54,9 @@ export class HaDateRangePicker extends LitElement {
|
||||
|
||||
@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;
|
||||
|
||||
|
||||
@@ -50,15 +50,6 @@ export class HaDialogDatePicker extends LitElement {
|
||||
@datepicker-value-updated=${this._valueChanged}
|
||||
.firstDayOfWeek=${this._params.firstWeekday}
|
||||
></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}>
|
||||
${this.hass.localize("ui.dialogs.date-picker.today")}
|
||||
</mwc-button>
|
||||
@@ -75,11 +66,6 @@ export class HaDialogDatePicker extends LitElement {
|
||||
this._value = ev.detail.value;
|
||||
}
|
||||
|
||||
private _clear() {
|
||||
this._params?.onChange(undefined);
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _setToday() {
|
||||
const today = new Date();
|
||||
this._value = format(today, "yyyy-MM-dd");
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface LevelDefinition {
|
||||
}
|
||||
|
||||
@customElement("ha-gauge")
|
||||
export class HaGauge extends LitElement {
|
||||
export class Gauge extends LitElement {
|
||||
@property({ type: Number }) public min = 0;
|
||||
|
||||
@property({ type: Number }) public max = 100;
|
||||
@@ -216,9 +216,3 @@ export class HaGauge extends LitElement {
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-gauge": HaGauge;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import { customElement } from "lit/decorators";
|
||||
class HaLabel extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<span class="label">
|
||||
<slot name="icon"></slot>
|
||||
<slot></slot>
|
||||
</span>
|
||||
`;
|
||||
<span class="label">
|
||||
<slot name="icon"></slot>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -6,27 +6,27 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-labeled-slider")
|
||||
class HaLabeledSlider extends LitElement {
|
||||
@property({ type: Boolean }) public labeled = false;
|
||||
@property() public labeled? = false;
|
||||
|
||||
@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({ type: Boolean }) public extra = false;
|
||||
@property() public extra = false;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@property({ type: Number }) public value?: number;
|
||||
@property() public value?: number;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
@@ -38,7 +38,7 @@ class HaLabeledSlider extends LitElement {
|
||||
.min=${this.min}
|
||||
.max=${this.max}
|
||||
.step=${this.step}
|
||||
.labeled=${this.labeled}
|
||||
labeled=${this.labeled}
|
||||
.disabled=${this.disabled}
|
||||
.value=${this.value}
|
||||
@change=${this._inputChanged}
|
||||
|
||||
@@ -11,7 +11,7 @@ import "./ha-icon-button";
|
||||
class HaMenuButton extends LitElement {
|
||||
@property({ type: Boolean }) public hassio = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
|
||||
@@ -112,9 +112,3 @@ export class HaQrCode extends LitElement {
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-qr-code": HaQrCode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,6 @@ export class HaBackupLocationSelector extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-backup_location": HaBackupLocationSelector;
|
||||
"ha-selector-backup-location": HaBackupLocationSelector;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import "../ha-input-helper-text";
|
||||
export class HaBooleanSelector extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public value = false;
|
||||
@property() public value?: number;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ export class HaNumberSelector extends LitElement {
|
||||
|
||||
@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;
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
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 { repeat } from "lit/directives/repeat";
|
||||
import { SortableEvent } from "sortablejs";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
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 "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
@@ -18,7 +21,6 @@ import "../ha-formfield";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-radio";
|
||||
import "../ha-select";
|
||||
import "../ha-sortable";
|
||||
|
||||
@customElement("ha-selector-select")
|
||||
export class HaSelectSelector extends LitElement {
|
||||
@@ -40,10 +42,50 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
@query("ha-combo-box", true) private comboBox!: HaComboBox;
|
||||
|
||||
private _itemMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
this._move(oldIndex!, newIndex);
|
||||
private _sortable?: SortableInstance;
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
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) {
|
||||
@@ -57,6 +99,11 @@ export class HaSelectSelector extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _destroySortable() {
|
||||
this._sortable?.destroy();
|
||||
this._sortable = undefined;
|
||||
}
|
||||
|
||||
private _filter = "";
|
||||
|
||||
protected render() {
|
||||
@@ -148,43 +195,37 @@ export class HaSelectSelector extends LitElement {
|
||||
return html`
|
||||
${value?.length
|
||||
? html`
|
||||
<ha-sortable
|
||||
no-style
|
||||
.disabled=${!this.selector.select.reorder}
|
||||
@item-moved=${this._itemMoved}
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
value,
|
||||
(item) => item,
|
||||
(item, idx) => {
|
||||
const label =
|
||||
options.find((option) => option.value === item)
|
||||
?.label || item;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
.idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
.label=${label}
|
||||
selected
|
||||
>
|
||||
${this.selector.select?.reorder
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDrag}
|
||||
data-handle
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
${options.find((option) => option.value === item)
|
||||
?.label || item}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
value,
|
||||
(item) => item,
|
||||
(item, idx) => {
|
||||
const label =
|
||||
options.find((option) => option.value === item)?.label ||
|
||||
item;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
.idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
.label=${label}
|
||||
selected
|
||||
>
|
||||
${this.selector.select?.reorder
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDrag}
|
||||
data-handle
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
${options.find((option) => option.value === item)
|
||||
?.label || item}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</ha-chip-set>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
@@ -378,35 +419,25 @@ export class HaSelectSelector extends LitElement {
|
||||
this.comboBox.filteredItems = filteredItems;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
ha-select,
|
||||
mwc-formfield,
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
mwc-list-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
ha-chip-set {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.sortable-fallback {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.sortable-drag {
|
||||
cursor: grabbing;
|
||||
}
|
||||
`;
|
||||
static styles = [
|
||||
sortableStyles,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
ha-select,
|
||||
mwc-formfield,
|
||||
ha-formfield {
|
||||
display: block;
|
||||
}
|
||||
mwc-list-item[disabled] {
|
||||
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
|
||||
}
|
||||
ha-chip-set {
|
||||
padding: 8px 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -47,6 +47,6 @@ export class HaTTSVoiceSelector extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-tts_voice": HaTTSVoiceSelector;
|
||||
"ha-selector-tts-voice": HaTTSVoiceSelector;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,6 @@ export class HaSelectorUiAction extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-ui_action": HaSelectorUiAction;
|
||||
"ha-selector-ui-action": HaSelectorUiAction;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,6 @@ export class HaSelectorUiColor extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-ui_color": HaSelectorUiColor;
|
||||
"ha-selector-ui-color": HaSelectorUiColor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,7 @@ import {
|
||||
HassServices,
|
||||
HassServiceTarget,
|
||||
} from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
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, reflect: true }) public hidePicker?: boolean;
|
||||
|
||||
@state() private _value!: this["value"];
|
||||
|
||||
@state() private _checkedKeys = new Set();
|
||||
@@ -372,14 +363,12 @@ export class HaServiceControl extends LitElement {
|
||||
)) ||
|
||||
serviceData?.description;
|
||||
|
||||
return html`${this.hidePicker
|
||||
? nothing
|
||||
: html`<ha-service-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._value?.service}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._serviceChanged}
|
||||
></ha-service-picker>`}
|
||||
return html`<ha-service-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._value?.service}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._serviceChanged}
|
||||
></ha-service-picker>
|
||||
<div class="description">
|
||||
${description ? html`<p>${description}</p>` : ""}
|
||||
${this._manifest
|
||||
@@ -746,9 +735,6 @@ export class HaServiceControl extends LitElement {
|
||||
margin: var(--service-control-padding, 0 16px);
|
||||
padding: 16px 0;
|
||||
}
|
||||
:host([hidePicker]) p {
|
||||
padding-top: 0;
|
||||
}
|
||||
.checkbox-spacer {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, eventOptions, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { guard } from "lit/directives/guard";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../common/decorators/storage";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@@ -49,12 +50,12 @@ import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
||||
import { UpdateEntity, updateCanInstall } from "../data/update";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||
import type { SortableInstance } from "../resources/sortable";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-menu-button";
|
||||
import "./ha-sortable";
|
||||
import "./ha-svg-icon";
|
||||
import "./user/ha-user-badge";
|
||||
|
||||
@@ -203,13 +204,15 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _issuesCount = 0;
|
||||
|
||||
@state() private _renderEmptySortable = false;
|
||||
|
||||
private _mouseLeaveTimeout?: number;
|
||||
|
||||
private _tooltipHideTimeout?: number;
|
||||
|
||||
private _recentKeydownActiveUntil = 0;
|
||||
|
||||
private _editStyleLoaded = false;
|
||||
private sortableStyleLoaded = false;
|
||||
|
||||
@storage({
|
||||
key: "sidebarPanelOrder",
|
||||
@@ -225,6 +228,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _hiddenPanels: string[] = [];
|
||||
|
||||
private _sortable?: SortableInstance;
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return this.hass.user?.is_admin
|
||||
? [
|
||||
@@ -259,13 +264,14 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
changedProps.has("expanded") ||
|
||||
changedProps.has("narrow") ||
|
||||
changedProps.has("alwaysExpand") ||
|
||||
changedProps.has("editMode") ||
|
||||
changedProps.has("_externalConfig") ||
|
||||
changedProps.has("_updatesCount") ||
|
||||
changedProps.has("_issuesCount") ||
|
||||
changedProps.has("_notifications") ||
|
||||
changedProps.has("editMode") ||
|
||||
changedProps.has("_renderEmptySortable") ||
|
||||
changedProps.has("_hiddenPanels") ||
|
||||
changedProps.has("_panelOrder")
|
||||
(changedProps.has("_panelOrder") && !this.editMode)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -300,8 +306,12 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
if (changedProps.has("alwaysExpand")) {
|
||||
toggleAttribute(this, "expanded", this.alwaysExpand);
|
||||
}
|
||||
if (changedProps.has("editMode") && this.editMode) {
|
||||
this._editModeActivated();
|
||||
if (changedProps.has("editMode")) {
|
||||
if (this.editMode) {
|
||||
this._activateEditMode();
|
||||
} else {
|
||||
this._deactivateEditMode();
|
||||
}
|
||||
}
|
||||
if (!changedProps.has("hass")) {
|
||||
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[]) {
|
||||
return html`
|
||||
<ha-sortable
|
||||
handle-selector="paper-icon-item"
|
||||
.disabled=${!this.editMode}
|
||||
@item-moved=${this._panelMoved}
|
||||
>
|
||||
<div class="reorder-list">${this._renderPanels(beforeSpacer)}</div>
|
||||
</ha-sortable>
|
||||
${this._renderSpacer()}${this._renderHiddenPanels()}
|
||||
`;
|
||||
// prettier-ignore
|
||||
return html`<div id="sortable">
|
||||
${guard([this._hiddenPanels, this._renderEmptySortable], () =>
|
||||
this._renderEmptySortable ? "" : this._renderPanels(beforeSpacer)
|
||||
)}
|
||||
</div>
|
||||
${this._renderSpacer()}
|
||||
${this._renderHiddenPanels()} `;
|
||||
}
|
||||
|
||||
private _renderHiddenPanels() {
|
||||
@@ -685,22 +674,44 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
fireEvent(this, "hass-edit-sidebar", { editMode: true });
|
||||
}
|
||||
|
||||
private async _editModeActivated() {
|
||||
await this._loadEditStyle();
|
||||
private async _activateEditMode() {
|
||||
await Promise.all([this._loadSortableStyle(), this._createSortable()]);
|
||||
}
|
||||
|
||||
private async _loadEditStyle() {
|
||||
if (this._editStyleLoaded) return;
|
||||
private async _loadSortableStyle() {
|
||||
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");
|
||||
style.innerHTML = (editStylesImport.sidebarEditStyle as CSSResult).cssText;
|
||||
style.innerHTML = (sortStylesImport.sortableStyles as CSSResult).cssText;
|
||||
this.shadowRoot!.appendChild(style);
|
||||
|
||||
this.sortableStyleLoaded = true;
|
||||
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() {
|
||||
fireEvent(this, "hass-edit-sidebar", { editMode: false });
|
||||
}
|
||||
@@ -713,8 +724,13 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
// Make a copy for Memoize
|
||||
this._hiddenPanels = [...this._hiddenPanels, panel];
|
||||
// Remove it from the panel order
|
||||
this._panelOrder = this._panelOrder.filter((order) => order !== panel);
|
||||
this._renderEmptySortable = true;
|
||||
await this.updateComplete;
|
||||
const container = this.shadowRoot!.getElementById("sortable")!;
|
||||
while (container.lastElementChild) {
|
||||
container.removeChild(container.lastElementChild);
|
||||
}
|
||||
this._renderEmptySortable = false;
|
||||
}
|
||||
|
||||
private async _unhidePanel(ev: Event) {
|
||||
@@ -723,6 +739,13 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
this._hiddenPanels = this._hiddenPanels.filter(
|
||||
(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) {
|
||||
@@ -887,7 +910,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
.menu mwc-button {
|
||||
width: 100%;
|
||||
}
|
||||
.reorder-list,
|
||||
#sortable,
|
||||
.hidden-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export class HaThemePicker extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean }) includeDefault = false;
|
||||
@property() includeDefault?: boolean = false;
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export const passiveEventOptionsIfSupported = supportsPassiveEventListener
|
||||
: undefined;
|
||||
|
||||
@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 mdcFoundationClass = MDCFixedTopAppBarFoundation;
|
||||
@@ -323,9 +323,3 @@ export class TopAppBarBaseBase extends BaseElement {
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-two-pane-top-app-bar-fixed": TopAppBarBaseBase;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ class MediaUploadButton extends LitElement {
|
||||
${this._uploading > 0
|
||||
? html`
|
||||
<ha-circular-progress
|
||||
size="small"
|
||||
size="tiny"
|
||||
indeterminate
|
||||
area-label="Uploading"
|
||||
slot="icon"
|
||||
|
||||
@@ -133,7 +133,7 @@ export class HaTracePathDetails extends LitElement {
|
||||
|
||||
if (result?.enabled === false) {
|
||||
return html`${this.hass!.localize(
|
||||
"ui.panel.config.automation.trace.path.disabled_step"
|
||||
"ui.panel.config.automation.trace.path.disabled_node"
|
||||
)}`;
|
||||
}
|
||||
|
||||
@@ -208,19 +208,11 @@ export class HaTracePathDetails extends LitElement {
|
||||
const paths = this.trace.trace;
|
||||
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`
|
||||
<div class="padded-box">
|
||||
${data.map(
|
||||
(trace, idx) => html`
|
||||
${data.length > 1
|
||||
${idx > 0
|
||||
? html`<p>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.automation.trace.path.iteration",
|
||||
@@ -248,7 +240,7 @@ export class HaTracePathDetails extends LitElement {
|
||||
if (index === -1) {
|
||||
return html`<div class="padded-box">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.automation.trace.path.step_not_executed"
|
||||
"ui.panel.config.automation.trace.path.node_not_tracked"
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -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 { debounce } from "../common/util/debounce";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { DeviceRegistryEntry } from "./device_registry";
|
||||
import { EntityRegistryEntry } from "./entity_registry";
|
||||
|
||||
export { subscribeAreaRegistry } from "./ws-area_registry";
|
||||
|
||||
export interface AreaRegistryEntry {
|
||||
area_id: string;
|
||||
name: string;
|
||||
@@ -52,6 +53,45 @@ export const deleteAreaRegistryEntry = (hass: HomeAssistant, areaId: string) =>
|
||||
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 = (
|
||||
entities: EntityRegistryEntry[]
|
||||
): AreaEntityLookup => {
|
||||
|
||||
@@ -18,11 +18,6 @@ export interface AssistPipeline {
|
||||
wake_word_id: string | null;
|
||||
}
|
||||
|
||||
export interface AssistDevice {
|
||||
device_id: string;
|
||||
pipeline_entity: string;
|
||||
}
|
||||
|
||||
export interface AssistPipelineMutableParams {
|
||||
name: string;
|
||||
language: string;
|
||||
@@ -371,8 +366,3 @@ export const fetchAssistPipelineLanguages = (hass: HomeAssistant) =>
|
||||
hass.callWS<{ languages: string[] }>({
|
||||
type: "assist_pipeline/language/list",
|
||||
});
|
||||
|
||||
export const listAssistDevices = (hass: HomeAssistant) =>
|
||||
hass.callWS<AssistDevice[]>({
|
||||
type: "assist_pipeline/device/list",
|
||||
});
|
||||
|
||||
+43
-93
@@ -64,6 +64,23 @@ const localizeTimeString = (
|
||||
}
|
||||
};
|
||||
|
||||
const ordinalSuffix = (n: number) => {
|
||||
n %= 100;
|
||||
if ([11, 12, 13].includes(n)) {
|
||||
return "th";
|
||||
}
|
||||
if (n % 10 === 1) {
|
||||
return "st";
|
||||
}
|
||||
if (n % 10 === 2) {
|
||||
return "nd";
|
||||
}
|
||||
if (n % 10 === 3) {
|
||||
return "rd";
|
||||
}
|
||||
return "th";
|
||||
};
|
||||
|
||||
export const describeTrigger = (
|
||||
trigger: Trigger,
|
||||
hass: HomeAssistant,
|
||||
@@ -384,37 +401,14 @@ const tryDescribeTrigger = (
|
||||
// Time Pattern Trigger
|
||||
if (trigger.platform === "time_pattern") {
|
||||
if (!trigger.seconds && !trigger.minutes && !trigger.hours) {
|
||||
return hass.localize(
|
||||
`${triggerTranslationBaseKey}.time_pattern.description.initial`
|
||||
);
|
||||
return "When a time pattern matches";
|
||||
}
|
||||
|
||||
const invalidParts: Array<"seconds" | "minutes" | "hours"> = [];
|
||||
|
||||
let secondsChoice: "every" | "every_interval" | "on_the_xth" | "other" =
|
||||
"other";
|
||||
let minutesChoice:
|
||||
| "every"
|
||||
| "every_interval"
|
||||
| "on_the_xth"
|
||||
| "other"
|
||||
| "has_seconds" = "other";
|
||||
let hoursChoice:
|
||||
| "every"
|
||||
| "every_interval"
|
||||
| "on_the_xth"
|
||||
| "other"
|
||||
| "has_seconds_or_minutes" = "other";
|
||||
|
||||
let seconds = 0;
|
||||
let minutes = 0;
|
||||
let hours = 0;
|
||||
|
||||
let result = "Trigger ";
|
||||
if (trigger.seconds !== undefined) {
|
||||
const seconds_all = trigger.seconds === "*";
|
||||
const seconds_interval =
|
||||
typeof trigger.seconds === "string" && trigger.seconds.startsWith("/");
|
||||
seconds = seconds_all
|
||||
const seconds = seconds_all
|
||||
? 0
|
||||
: typeof trigger.seconds === "number"
|
||||
? trigger.seconds
|
||||
@@ -428,22 +422,22 @@ const tryDescribeTrigger = (
|
||||
seconds < 0 ||
|
||||
(seconds_interval && seconds === 0)
|
||||
) {
|
||||
invalidParts.push("seconds");
|
||||
return "Invalid Time Pattern Seconds";
|
||||
}
|
||||
|
||||
if (seconds_all || (seconds_interval && seconds === 1)) {
|
||||
secondsChoice = "every";
|
||||
result += "every second of ";
|
||||
} else if (seconds_interval) {
|
||||
secondsChoice = "every_interval";
|
||||
result += `every ${seconds} seconds of `;
|
||||
} else {
|
||||
secondsChoice = "on_the_xth";
|
||||
result += `on the ${seconds}${ordinalSuffix(seconds)} second of `;
|
||||
}
|
||||
}
|
||||
if (trigger.minutes !== undefined) {
|
||||
const minutes_all = trigger.minutes === "*";
|
||||
const minutes_interval =
|
||||
typeof trigger.minutes === "string" && trigger.minutes.startsWith("/");
|
||||
minutes = minutes_all
|
||||
const minutes = minutes_all
|
||||
? 0
|
||||
: typeof trigger.minutes === "number"
|
||||
? trigger.minutes
|
||||
@@ -457,30 +451,30 @@ const tryDescribeTrigger = (
|
||||
minutes < 0 ||
|
||||
(minutes_interval && minutes === 0)
|
||||
) {
|
||||
invalidParts.push("minutes");
|
||||
return "Invalid Time Pattern Minutes";
|
||||
}
|
||||
|
||||
if (minutes_all || (minutes_interval && minutes === 1)) {
|
||||
minutesChoice = "every";
|
||||
result += "every minute of ";
|
||||
} else if (minutes_interval) {
|
||||
minutesChoice = "every_interval";
|
||||
result += `every ${minutes} minutes of `;
|
||||
} else {
|
||||
minutesChoice =
|
||||
trigger.seconds !== undefined ? "has_seconds" : "on_the_xth";
|
||||
result += `${
|
||||
trigger.seconds !== undefined ? "" : "on"
|
||||
} the ${minutes}${ordinalSuffix(minutes)} minute of `;
|
||||
}
|
||||
} else if (trigger.seconds !== undefined) {
|
||||
if (trigger.hours !== undefined) {
|
||||
minutes = 0;
|
||||
minutesChoice = "has_seconds";
|
||||
result += `the 0${ordinalSuffix(0)} minute of `;
|
||||
} else {
|
||||
minutesChoice = "every";
|
||||
result += "every minute of ";
|
||||
}
|
||||
}
|
||||
if (trigger.hours !== undefined) {
|
||||
const hours_all = trigger.hours === "*";
|
||||
const hours_interval =
|
||||
typeof trigger.hours === "string" && trigger.hours.startsWith("/");
|
||||
hours = hours_all
|
||||
const hours = hours_all
|
||||
? 0
|
||||
: typeof trigger.hours === "number"
|
||||
? trigger.hours
|
||||
@@ -494,68 +488,24 @@ const tryDescribeTrigger = (
|
||||
hours < 0 ||
|
||||
(hours_interval && hours === 0)
|
||||
) {
|
||||
invalidParts.push("hours");
|
||||
return "Invalid Time Pattern Hours";
|
||||
}
|
||||
|
||||
if (hours_all || (hours_interval && hours === 1)) {
|
||||
hoursChoice = "every";
|
||||
result += "every hour";
|
||||
} else if (hours_interval) {
|
||||
hoursChoice = "every_interval";
|
||||
result += `every ${hours} hours`;
|
||||
} else {
|
||||
hoursChoice =
|
||||
result += `${
|
||||
trigger.seconds !== undefined || trigger.minutes !== undefined
|
||||
? "has_seconds_or_minutes"
|
||||
: "on_the_xth";
|
||||
? ""
|
||||
: "on"
|
||||
} the ${hours}${ordinalSuffix(hours)} hour`;
|
||||
}
|
||||
} else {
|
||||
hoursChoice = "every";
|
||||
result += "every hour";
|
||||
}
|
||||
|
||||
if (invalidParts.length !== 0) {
|
||||
return hass.localize(
|
||||
`${triggerTranslationBaseKey}.time_pattern.description.invalid`,
|
||||
{
|
||||
parts: formatListWithAnds(
|
||||
hass.locale,
|
||||
invalidParts.map((invalidPart) =>
|
||||
hass.localize(
|
||||
`${triggerTranslationBaseKey}.time_pattern.${invalidPart}`
|
||||
)
|
||||
)
|
||||
),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return hass.localize(
|
||||
`${triggerTranslationBaseKey}.time_pattern.description.full`,
|
||||
{
|
||||
secondsChoice: secondsChoice,
|
||||
minutesChoice: minutesChoice,
|
||||
hoursChoice: hoursChoice,
|
||||
seconds: seconds,
|
||||
minutes: minutes,
|
||||
hours: hours,
|
||||
secondsWithOrdinal: hass.localize(
|
||||
`${triggerTranslationBaseKey}.time_pattern.description.ordinal`,
|
||||
{
|
||||
part: seconds,
|
||||
}
|
||||
),
|
||||
minutesWithOrdinal: hass.localize(
|
||||
`${triggerTranslationBaseKey}.time_pattern.description.ordinal`,
|
||||
{
|
||||
part: minutes,
|
||||
}
|
||||
),
|
||||
hoursWithOrdinal: hass.localize(
|
||||
`${triggerTranslationBaseKey}.time_pattern.description.ordinal`,
|
||||
{
|
||||
part: hours,
|
||||
}
|
||||
),
|
||||
}
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Zone Trigger
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||
import type { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
@@ -7,11 +10,6 @@ import type {
|
||||
} from "./entity_registry";
|
||||
import type { EntitySources } from "./entity_sources";
|
||||
|
||||
export {
|
||||
fetchDeviceRegistry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "./ws-device_registry";
|
||||
|
||||
export interface DeviceRegistryEntry {
|
||||
id: string;
|
||||
config_entries: string[];
|
||||
@@ -98,6 +96,39 @@ export const removeConfigEntryFromDevice = (
|
||||
config_entry_id: configEntryId,
|
||||
});
|
||||
|
||||
export const fetchDeviceRegistry = (conn: Connection) =>
|
||||
conn.sendMessagePromise<DeviceRegistryEntry[]>({
|
||||
type: "config/device_registry/list",
|
||||
});
|
||||
|
||||
const subscribeDeviceRegistryUpdates = (
|
||||
conn: Connection,
|
||||
store: Store<DeviceRegistryEntry[]>
|
||||
) =>
|
||||
conn.subscribeEvents(
|
||||
debounce(
|
||||
() =>
|
||||
fetchDeviceRegistry(conn).then((devices) =>
|
||||
store.setState(devices, true)
|
||||
),
|
||||
500,
|
||||
true
|
||||
),
|
||||
"device_registry_updated"
|
||||
);
|
||||
|
||||
export const subscribeDeviceRegistry = (
|
||||
conn: Connection,
|
||||
onChange: (devices: DeviceRegistryEntry[]) => void
|
||||
) =>
|
||||
createCollection<DeviceRegistryEntry[]>(
|
||||
"_dr",
|
||||
fetchDeviceRegistry,
|
||||
subscribeDeviceRegistryUpdates,
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
||||
export const sortDeviceRegistryByName = (
|
||||
entries: DeviceRegistryEntry[],
|
||||
language: string
|
||||
|
||||
@@ -8,8 +8,6 @@ import { HomeAssistant } from "../types";
|
||||
import { LightColor } from "./light";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
|
||||
export { subscribeEntityRegistryDisplay } from "./ws-entity_registry_display";
|
||||
|
||||
type entityCategory = "config" | "diagnostic";
|
||||
|
||||
export interface EntityRegistryDisplayEntry {
|
||||
@@ -24,7 +22,7 @@ export interface EntityRegistryDisplayEntry {
|
||||
display_precision?: number;
|
||||
}
|
||||
|
||||
export interface EntityRegistryDisplayEntryResponse {
|
||||
interface EntityRegistryDisplayEntryResponse {
|
||||
entities: {
|
||||
ei: string;
|
||||
di?: string;
|
||||
@@ -257,6 +255,34 @@ export const subscribeEntityRegistry = (
|
||||
onChange
|
||||
);
|
||||
|
||||
const subscribeEntityRegistryDisplayUpdates = (
|
||||
conn: Connection,
|
||||
store: Store<EntityRegistryDisplayEntryResponse>
|
||||
) =>
|
||||
conn.subscribeEvents(
|
||||
debounce(
|
||||
() =>
|
||||
fetchEntityRegistryDisplay(conn).then((entities) =>
|
||||
store.setState(entities, true)
|
||||
),
|
||||
500,
|
||||
true
|
||||
),
|
||||
"entity_registry_updated"
|
||||
);
|
||||
|
||||
export const subscribeEntityRegistryDisplay = (
|
||||
conn: Connection,
|
||||
onChange: (entities: EntityRegistryDisplayEntryResponse) => void
|
||||
) =>
|
||||
createCollection<EntityRegistryDisplayEntryResponse>(
|
||||
"_entityRegistryDisplay",
|
||||
fetchEntityRegistryDisplay,
|
||||
subscribeEntityRegistryDisplayUpdates,
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
||||
export const sortEntityRegistryByName = (
|
||||
entries: EntityRegistryEntry[],
|
||||
language: string
|
||||
|
||||
@@ -24,6 +24,16 @@ export const fetchPersons = (hass: HomeAssistant) =>
|
||||
config: Person[];
|
||||
}>({ type: "person/list" });
|
||||
|
||||
export const listUserPersons = (): Promise<Record<string, BasePerson>> =>
|
||||
fetch("/api/person/list", {
|
||||
credentials: "same-origin",
|
||||
}).then((resp) => {
|
||||
if (resp.ok) {
|
||||
return resp.json();
|
||||
}
|
||||
throw new Error(resp.statusText);
|
||||
});
|
||||
|
||||
export const createPerson = (
|
||||
hass: HomeAssistant,
|
||||
values: PersonMutableParams
|
||||
|
||||
@@ -52,7 +52,6 @@ export const serviceActionStruct: Describe<ServiceAction> = assign(
|
||||
target: optional(targetStruct),
|
||||
data: optional(object()),
|
||||
response_variable: optional(string()),
|
||||
metadata: optional(object()),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -134,7 +133,6 @@ export interface ServiceAction extends BaseAction {
|
||||
target?: HassServiceTarget;
|
||||
data?: Record<string, unknown>;
|
||||
response_variable?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface DeviceAction extends BaseAction {
|
||||
|
||||
+1
-15
@@ -168,18 +168,6 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
const service =
|
||||
hass.localize(`component.${domain}.services.${serviceName}.name`) ||
|
||||
hass.services[domain][serviceName]?.name;
|
||||
|
||||
if (config.metadata) {
|
||||
return hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.service_name`,
|
||||
{
|
||||
domain: domainToName(hass.localize, domain),
|
||||
name: service || config.service,
|
||||
targets: formatListWithAnds(hass.locale, targets),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return hass.localize(
|
||||
`${actionTranslationBaseKey}.service.description.service_based_on_name`,
|
||||
{
|
||||
@@ -416,9 +404,7 @@ const tryDescribeAction = <T extends ActionType>(
|
||||
if (actionType === "device_action") {
|
||||
const config = action as DeviceAction;
|
||||
if (!config.device_id) {
|
||||
return hass.localize(
|
||||
`${actionTranslationBaseKey}.device_id.description.no_device`
|
||||
);
|
||||
return "Device action";
|
||||
}
|
||||
const localized = localizeDeviceAutomationAction(
|
||||
hass,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface TimeDatePreview {
|
||||
items: [
|
||||
{
|
||||
state: string;
|
||||
attributes: Record<string, any>;
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export const subscribePreviewTimeDate = (
|
||||
hass: HomeAssistant,
|
||||
flow_id: string,
|
||||
flow_type: "config_flow" | "options_flow",
|
||||
user_input: Record<string, any>,
|
||||
callback: (preview: TimeDatePreview) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage(callback, {
|
||||
type: "time_date/start_preview",
|
||||
flow_id,
|
||||
flow_type,
|
||||
user_input,
|
||||
});
|
||||
+5
-11
@@ -18,8 +18,8 @@ export interface TodoItem {
|
||||
uid: string;
|
||||
summary: string;
|
||||
status: TodoItemStatus;
|
||||
description?: string | null;
|
||||
due?: string | null;
|
||||
description?: string;
|
||||
due?: string;
|
||||
}
|
||||
|
||||
export const enum TodoListEntityFeature {
|
||||
@@ -83,12 +83,9 @@ export const updateItem = (
|
||||
item: item.uid,
|
||||
rename: item.summary,
|
||||
status: item.status,
|
||||
description: item.description,
|
||||
description: item.description || undefined,
|
||||
due_datetime: item.due?.includes("T") ? item.due : undefined,
|
||||
due_date:
|
||||
item.due === undefined || item.due?.includes("T")
|
||||
? undefined
|
||||
: item.due,
|
||||
due_date: item.due?.includes("T") ? undefined : item.due || undefined,
|
||||
},
|
||||
{ entity_id }
|
||||
);
|
||||
@@ -105,10 +102,7 @@ export const createItem = (
|
||||
item: item.summary,
|
||||
description: item.description || undefined,
|
||||
due_datetime: item.due?.includes("T") ? item.due : undefined,
|
||||
due_date:
|
||||
item.due === undefined || item.due?.includes("T")
|
||||
? undefined
|
||||
: item.due,
|
||||
due_date: item.due?.includes("T") ? undefined : item.due,
|
||||
},
|
||||
{ entity_id }
|
||||
);
|
||||
|
||||
+2
-8
@@ -14,20 +14,14 @@ export const enum ValveEntityFeature {
|
||||
}
|
||||
|
||||
export function isFullyOpen(stateObj: ValveEntity) {
|
||||
if (
|
||||
stateObj.attributes.current_position !== undefined &&
|
||||
stateObj.attributes.current_position !== null
|
||||
) {
|
||||
if (stateObj.attributes.current_position !== undefined) {
|
||||
return stateObj.attributes.current_position === 100;
|
||||
}
|
||||
return stateObj.state === "open";
|
||||
}
|
||||
|
||||
export function isFullyClosed(stateObj: ValveEntity) {
|
||||
if (
|
||||
stateObj.attributes.current_position !== undefined &&
|
||||
stateObj.attributes.current_position !== null
|
||||
) {
|
||||
if (stateObj.attributes.current_position !== undefined) {
|
||||
return stateObj.attributes.current_position === 0;
|
||||
}
|
||||
return stateObj.state === "closed";
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { AreaRegistryEntry } from "./area_registry";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
|
||||
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
|
||||
);
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { DeviceRegistryEntry } from "./device_registry";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
|
||||
export const fetchDeviceRegistry = (conn: Connection) =>
|
||||
conn.sendMessagePromise<DeviceRegistryEntry[]>({
|
||||
type: "config/device_registry/list",
|
||||
});
|
||||
|
||||
const subscribeDeviceRegistryUpdates = (
|
||||
conn: Connection,
|
||||
store: Store<DeviceRegistryEntry[]>
|
||||
) =>
|
||||
conn.subscribeEvents(
|
||||
debounce(
|
||||
() =>
|
||||
fetchDeviceRegistry(conn).then((devices) =>
|
||||
store.setState(devices, true)
|
||||
),
|
||||
500,
|
||||
true
|
||||
),
|
||||
"device_registry_updated"
|
||||
);
|
||||
|
||||
export const subscribeDeviceRegistry = (
|
||||
conn: Connection,
|
||||
onChange: (devices: DeviceRegistryEntry[]) => void
|
||||
) =>
|
||||
createCollection<DeviceRegistryEntry[]>(
|
||||
"_dr",
|
||||
fetchDeviceRegistry,
|
||||
subscribeDeviceRegistryUpdates,
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import {
|
||||
EntityRegistryDisplayEntryResponse,
|
||||
fetchEntityRegistryDisplay,
|
||||
} from "./entity_registry";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
|
||||
const subscribeEntityRegistryDisplayUpdates = (
|
||||
conn: Connection,
|
||||
store: Store<EntityRegistryDisplayEntryResponse>
|
||||
) =>
|
||||
conn.subscribeEvents(
|
||||
debounce(
|
||||
() =>
|
||||
fetchEntityRegistryDisplay(conn).then((entities) =>
|
||||
store.setState(entities, true)
|
||||
),
|
||||
500,
|
||||
true
|
||||
),
|
||||
"entity_registry_updated"
|
||||
);
|
||||
|
||||
export const subscribeEntityRegistryDisplay = (
|
||||
conn: Connection,
|
||||
onChange: (entities: EntityRegistryDisplayEntryResponse) => void
|
||||
) =>
|
||||
createCollection<EntityRegistryDisplayEntryResponse>(
|
||||
"_entityRegistryDisplay",
|
||||
fetchEntityRegistryDisplay,
|
||||
subscribeEntityRegistryDisplayUpdates,
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
@@ -4,13 +4,15 @@ import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import type { SortableEvent } from "sortablejs";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { AreaFilterValue } from "../../components/ha-area-filter";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-sortable";
|
||||
import { areaCompare } from "../../data/area_registry";
|
||||
import { sortableStyles } from "../../resources/ha-sortable-style";
|
||||
import type { SortableInstance } from "../../resources/sortable";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { HassDialog } from "../make-dialog-manager";
|
||||
@@ -29,18 +31,23 @@ export class DialogAreaFilter
|
||||
|
||||
@state() private _areas: string[] = [];
|
||||
|
||||
public showDialog(dialogParams: AreaFilterDialogParams): void {
|
||||
private _sortable?: SortableInstance;
|
||||
|
||||
public async showDialog(dialogParams: AreaFilterDialogParams): Promise<void> {
|
||||
this._dialogParams = dialogParams;
|
||||
this._hidden = dialogParams.initialValue?.hidden ?? [];
|
||||
const order = dialogParams.initialValue?.order ?? [];
|
||||
const allAreas = Object.keys(this.hass!.areas);
|
||||
this._areas = allAreas.concat().sort(areaCompare(this.hass!.areas, order));
|
||||
await this.updateComplete;
|
||||
this._createSortable();
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._dialogParams = undefined;
|
||||
this._hidden = [];
|
||||
this._areas = [];
|
||||
this._destroySortable();
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -59,14 +66,42 @@ export class DialogAreaFilter
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _areaMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
private async _createSortable() {
|
||||
const Sortable = (await import("../../resources/sortable")).default;
|
||||
if (this._sortable) return;
|
||||
this._sortable = new Sortable(this.shadowRoot!.querySelector(".areas")!, {
|
||||
animation: 150,
|
||||
fallbackClass: "sortable-fallback",
|
||||
handle: ".handle",
|
||||
draggable: ".draggable",
|
||||
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 _destroySortable() {
|
||||
this._sortable?.destroy();
|
||||
this._sortable = undefined;
|
||||
}
|
||||
|
||||
private _dragged(ev: SortableEvent): void {
|
||||
if (ev.oldIndex === ev.newIndex) return;
|
||||
|
||||
const areas = this._areas.concat();
|
||||
|
||||
const option = areas.splice(oldIndex, 1)[0];
|
||||
areas.splice(newIndex, 0, option);
|
||||
const option = areas.splice(ev.oldIndex!, 1)[0];
|
||||
areas.splice(ev.newIndex!, 0, option);
|
||||
|
||||
this._areas = areas;
|
||||
}
|
||||
@@ -85,56 +120,50 @@ export class DialogAreaFilter
|
||||
.heading=${this._dialogParams.title ??
|
||||
this.hass.localize("ui.components.area-filter.title")}
|
||||
>
|
||||
<ha-sortable
|
||||
draggable-selector=".draggable"
|
||||
handle-selector=".handle"
|
||||
@item-moved=${this._areaMoved}
|
||||
>
|
||||
<mwc-list class="areas">
|
||||
${repeat(
|
||||
allAreas,
|
||||
(area) => area,
|
||||
(area, _idx) => {
|
||||
const isVisible = !this._hidden.includes(area);
|
||||
const name = this.hass!.areas[area]?.name || area;
|
||||
return html`
|
||||
<ha-list-item
|
||||
class=${classMap({
|
||||
hidden: !isVisible,
|
||||
draggable: isVisible,
|
||||
})}
|
||||
hasMeta
|
||||
graphic="icon"
|
||||
noninteractive
|
||||
>
|
||||
${isVisible
|
||||
? html`<ha-svg-icon
|
||||
class="handle"
|
||||
.path=${mdiDrag}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
${name}
|
||||
<ha-icon-button
|
||||
tabindex="0"
|
||||
class="action"
|
||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||
slot="meta"
|
||||
.label=${this.hass!.localize(
|
||||
`ui.components.area-filter.${
|
||||
isVisible ? "hide" : "show"
|
||||
}`,
|
||||
{ area: name }
|
||||
)}
|
||||
.area=${area}
|
||||
@click=${this._toggle}
|
||||
></ha-icon-button>
|
||||
</ha-list-item>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</mwc-list>
|
||||
</ha-sortable>
|
||||
<mwc-list class="areas">
|
||||
${repeat(
|
||||
allAreas,
|
||||
(area) => area,
|
||||
(area, _idx) => {
|
||||
const isVisible = !this._hidden.includes(area);
|
||||
const name = this.hass!.areas[area]?.name || area;
|
||||
return html`
|
||||
<ha-list-item
|
||||
class=${classMap({
|
||||
hidden: !isVisible,
|
||||
draggable: isVisible,
|
||||
})}
|
||||
hasMeta
|
||||
graphic="icon"
|
||||
noninteractive
|
||||
>
|
||||
${isVisible
|
||||
? html`<ha-svg-icon
|
||||
class="handle"
|
||||
.path=${mdiDrag}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
${name}
|
||||
<ha-icon-button
|
||||
tabindex="0"
|
||||
class="action"
|
||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||
slot="meta"
|
||||
.label=${this.hass!.localize(
|
||||
`ui.components.area-filter.${
|
||||
isVisible ? "hide" : "show"
|
||||
}`,
|
||||
{ area: name }
|
||||
)}
|
||||
.area=${area}
|
||||
@click=${this._toggle}
|
||||
></ha-icon-button>
|
||||
</ha-list-item>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</mwc-list>
|
||||
<ha-button slot="secondaryAction" dialogAction="cancel">
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
@@ -163,6 +192,7 @@ export class DialogAreaFilter
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
sortableStyles,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { FlowType } from "../../../data/data_entry_flow";
|
||||
import {
|
||||
TimeDatePreview,
|
||||
subscribePreviewTimeDate,
|
||||
} from "../../../data/time_date";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "./entity-preview-row";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
|
||||
@customElement("flow-preview-time_date")
|
||||
class FlowPreviewTimeDate extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public flowType!: FlowType;
|
||||
|
||||
public handler!: string;
|
||||
|
||||
@property() public stepId!: string;
|
||||
|
||||
@property() public flowId!: string;
|
||||
|
||||
@property() public stepData!: Record<string, any>;
|
||||
|
||||
@state() private _preview_items?: HassEntity[];
|
||||
|
||||
private _unsub?: Promise<UnsubscribeFunc>;
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this._unsub) {
|
||||
this._unsub.then((unsub) => unsub());
|
||||
this._unsub = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
willUpdate(changedProps) {
|
||||
if (changedProps.has("stepData")) {
|
||||
this._debouncedSubscribePreview();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`${this._preview_items?.map(
|
||||
(item) =>
|
||||
html`<entity-preview-row
|
||||
.hass=${this.hass}
|
||||
.stateObj=${item}
|
||||
></entity-preview-row>`
|
||||
)}`;
|
||||
}
|
||||
|
||||
private _setPreview = (preview: TimeDatePreview) => {
|
||||
const now = new Date().toISOString();
|
||||
this._preview_items = preview.items.map((item) => ({
|
||||
entity_id: `${this.stepId}.___flow_preview___`,
|
||||
last_changed: now,
|
||||
last_updated: now,
|
||||
context: { id: "", parent_id: null, user_id: null },
|
||||
...item,
|
||||
}));
|
||||
};
|
||||
|
||||
private _debouncedSubscribePreview = debounce(() => {
|
||||
this._subscribePreview();
|
||||
}, 250);
|
||||
|
||||
private async _subscribePreview() {
|
||||
this._preview_items = undefined;
|
||||
if (this._unsub) {
|
||||
(await this._unsub)();
|
||||
this._unsub = undefined;
|
||||
}
|
||||
if (this.flowType === "repair_flow") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._unsub = subscribePreviewTimeDate(
|
||||
this.hass,
|
||||
this.flowId,
|
||||
this.flowType,
|
||||
this.stepData,
|
||||
this._setPreview
|
||||
);
|
||||
} catch (err) {
|
||||
this._preview_items = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"flow-preview-time_date": FlowPreviewTimeDate;
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ class DialogBox extends LitElement {
|
||||
<ha-textfield
|
||||
dialogInitialFocus
|
||||
value=${ifDefined(this._params.defaultValue)}
|
||||
.placeholder=${this._params.placeholder}
|
||||
.placeholder=${ifDefined(this._params.placeholder)}
|
||||
.label=${this._params.inputLabel
|
||||
? this._params.inputLabel
|
||||
: ""}
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import type { SortableEvent } from "sortablejs";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-control-slider";
|
||||
import "../../../../components/ha-sortable";
|
||||
import { UNAVAILABLE } from "../../../../data/entity";
|
||||
import {
|
||||
ExtEntityRegistryEntry,
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
computeDefaultFavoriteColors,
|
||||
} from "../../../../data/light";
|
||||
import { actionHandler } from "../../../../panels/lovelace/common/directives/action-handler-directive";
|
||||
import type { SortableInstance } from "../../../../resources/sortable";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { showConfirmationDialog } from "../../../generic/show-dialog-box";
|
||||
import "./ha-favorite-color-button";
|
||||
@@ -47,7 +48,16 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
|
||||
@state() private _favoriteColors: LightColor[] = [];
|
||||
|
||||
private _sortable?: SortableInstance;
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
if (changedProps.has("editMode")) {
|
||||
if (this.editMode) {
|
||||
this._createSortable();
|
||||
} else {
|
||||
this._destroySortable();
|
||||
}
|
||||
}
|
||||
if (changedProps.has("entry")) {
|
||||
if (this.entry) {
|
||||
if (this.entry.options?.light?.favorite_colors) {
|
||||
@@ -59,10 +69,34 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _colorMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
this._move(oldIndex, newIndex);
|
||||
private async _createSortable() {
|
||||
const Sortable = (await import("../../../../resources/sortable")).default;
|
||||
this._sortable = new Sortable(
|
||||
this.shadowRoot!.querySelector(".container")!,
|
||||
{
|
||||
animation: 150,
|
||||
fallbackClass: "sortable-fallback",
|
||||
draggable: ".color",
|
||||
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) {
|
||||
@@ -73,6 +107,11 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
this._save(favoriteColors);
|
||||
}
|
||||
|
||||
private _destroySortable() {
|
||||
this._sortable?.destroy();
|
||||
this._sortable = undefined;
|
||||
}
|
||||
|
||||
private _apply = (index: number) => {
|
||||
const favorite = this._favoriteColors[index];
|
||||
this.hass.callService("light", "turn_on", {
|
||||
@@ -184,79 +223,72 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-sortable
|
||||
@item-moved=${this._colorMoved}
|
||||
item=".color"
|
||||
no-style
|
||||
.disabled=${!this.editMode}
|
||||
>
|
||||
<div class="container">
|
||||
${this._favoriteColors.map(
|
||||
(color, index) => html`
|
||||
<div class="color">
|
||||
<div
|
||||
class="color-bubble ${classMap({
|
||||
shake: !!this.editMode,
|
||||
})}"
|
||||
<div class="container">
|
||||
${this._favoriteColors.map(
|
||||
(color, index) => html`
|
||||
<div class="color">
|
||||
<div
|
||||
class="color-bubble ${classMap({
|
||||
shake: !!this.editMode,
|
||||
})}"
|
||||
>
|
||||
<ha-favorite-color-button
|
||||
.label=${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.favorite_color.${
|
||||
this.editMode ? "edit" : "set"
|
||||
}`,
|
||||
{ number: index }
|
||||
)}
|
||||
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||
.color=${color}
|
||||
.index=${index}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: !this.editMode && this.hass.user?.is_admin,
|
||||
disabled: this.stateObj!.state === UNAVAILABLE,
|
||||
})}
|
||||
@action=${this._handleColorAction}
|
||||
>
|
||||
<ha-favorite-color-button
|
||||
.label=${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.favorite_color.${
|
||||
this.editMode ? "edit" : "set"
|
||||
}`,
|
||||
{ number: index }
|
||||
)}
|
||||
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||
.color=${color}
|
||||
.index=${index}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: !this.editMode && this.hass.user?.is_admin,
|
||||
disabled: this.stateObj!.state === UNAVAILABLE,
|
||||
})}
|
||||
@action=${this._handleColorAction}
|
||||
>
|
||||
</ha-favorite-color-button>
|
||||
${this.editMode
|
||||
? html`
|
||||
<button
|
||||
@click=${this._handleDeleteButton}
|
||||
class="delete"
|
||||
.index=${index}
|
||||
aria-label=${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.favorite_color.delete`,
|
||||
{ number: index }
|
||||
)}
|
||||
.title=${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.favorite_color.delete`,
|
||||
{ number: index }
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiMinus}></ha-svg-icon>
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-favorite-color-button>
|
||||
${this.editMode
|
||||
? html`
|
||||
<button
|
||||
@click=${this._handleDeleteButton}
|
||||
class="delete"
|
||||
.index=${index}
|
||||
aria-label=${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.favorite_color.delete`,
|
||||
{ number: index }
|
||||
)}
|
||||
.title=${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.favorite_color.delete`,
|
||||
{ number: index }
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiMinus}></ha-svg-icon>
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${this.editMode
|
||||
? html`
|
||||
<ha-outlined-icon-button
|
||||
class="button"
|
||||
@click=${this._handleAddButton}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-outlined-icon-button>
|
||||
<ha-outlined-icon-button
|
||||
@click=${this._exitEditMode}
|
||||
class="button"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
|
||||
</ha-outlined-icon-button>
|
||||
`
|
||||
)}
|
||||
${this.editMode
|
||||
? html`
|
||||
<ha-outlined-icon-button
|
||||
class="button"
|
||||
@click=${this._handleAddButton}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-outlined-icon-button>
|
||||
<ha-outlined-icon-button
|
||||
@click=${this._exitEditMode}
|
||||
class="button"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
|
||||
</ha-outlined-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
+4
-10
@@ -11,22 +11,19 @@ import {
|
||||
import { loadTokens, saveTokens } from "../common/auth/token_storage";
|
||||
import { hassUrl } from "../data/auth";
|
||||
import { isExternal } from "../data/external";
|
||||
import { getRecorderInfo } from "../data/recorder";
|
||||
import { subscribeFrontendUserData } from "../data/frontend";
|
||||
import { fetchConfig } from "../data/lovelace/config/types";
|
||||
import { fetchResources } from "../data/lovelace/resource";
|
||||
import { MAIN_WINDOW_NAME } from "../data/main_window";
|
||||
import { WindowWithPreloads } from "../data/preloads";
|
||||
import { getRecorderInfo } from "../data/recorder";
|
||||
import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
||||
import { subscribeAreaRegistry } from "../data/ws-area_registry";
|
||||
import { subscribeDeviceRegistry } from "../data/ws-device_registry";
|
||||
import { subscribeEntityRegistryDisplay } from "../data/ws-entity_registry_display";
|
||||
import { subscribePanels } from "../data/ws-panels";
|
||||
import { subscribeThemes } from "../data/ws-themes";
|
||||
import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
||||
import { subscribeUser } from "../data/ws-user";
|
||||
import type { ExternalAuth } from "../external_app/external_auth";
|
||||
import "../resources/array.flat.polyfill";
|
||||
import "../resources/safari-14-attachshadow-patch";
|
||||
import { MAIN_WINDOW_NAME } from "../data/main_window";
|
||||
import { WindowWithPreloads } from "../data/preloads";
|
||||
|
||||
window.name = MAIN_WINDOW_NAME;
|
||||
(window as any).frontendVersion = __VERSION__;
|
||||
@@ -116,9 +113,6 @@ window.hassConnection.then(({ conn }) => {
|
||||
// do nothing
|
||||
};
|
||||
subscribeEntities(conn, noop);
|
||||
subscribeEntityRegistryDisplay(conn, noop);
|
||||
subscribeDeviceRegistry(conn, noop);
|
||||
subscribeAreaRegistry(conn, noop);
|
||||
subscribeConfig(conn, noop);
|
||||
subscribeServices(conn, noop);
|
||||
subscribePanels(conn, noop);
|
||||
|
||||
@@ -119,6 +119,7 @@ class HassSubpage extends LitElement {
|
||||
font-size: 20px;
|
||||
height: var(--header-height);
|
||||
padding: 8px 12px;
|
||||
pointer-events: none;
|
||||
background-color: var(--app-header-background-color);
|
||||
font-weight: 400;
|
||||
color: var(--app-header-text-color, white);
|
||||
|
||||
@@ -9,11 +9,9 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { LOCAL_TIME_ZONE } from "../common/datetime/resolve-time-zone";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-alert";
|
||||
import "../components/ha-circular-progress";
|
||||
import "../components/ha-country-picker";
|
||||
import { ConfigUpdateValues, saveCoreConfig } from "../data/core";
|
||||
import { countryCurrency } from "../data/currency";
|
||||
@@ -34,7 +32,8 @@ class OnboardingCoreConfig extends LitElement {
|
||||
|
||||
private _elevation = "0";
|
||||
|
||||
private _timeZone: ConfigUpdateValues["time_zone"] = LOCAL_TIME_ZONE;
|
||||
private _timeZone: ConfigUpdateValues["time_zone"] =
|
||||
Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
|
||||
|
||||
private _language: ConfigUpdateValues["language"] = getLocalLanguage();
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import { formatTime } from "../../common/datetime/format_time";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { isDate } from "../../common/string/is_date";
|
||||
import "../../components/entity/state-info";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-date-input";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-time-input";
|
||||
@@ -25,7 +24,6 @@ import { renderRRuleAsText } from "./recurrence";
|
||||
import { showConfirmEventDialog } from "./show-confirm-event-dialog-box";
|
||||
import { CalendarEventDetailDialogParams } from "./show-dialog-calendar-event-detail";
|
||||
import { showCalendarEventEditDialog } from "./show-dialog-calendar-event-editor";
|
||||
import { resolveTimeZone } from "../../common/datetime/resolve-time-zone";
|
||||
|
||||
class DialogCalendarEventDetail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -139,10 +137,8 @@ class DialogCalendarEventDetail extends LitElement {
|
||||
}
|
||||
|
||||
private _formatDateRange() {
|
||||
const timeZone = resolveTimeZone(
|
||||
this.hass.locale.time_zone,
|
||||
this.hass.config.time_zone
|
||||
);
|
||||
// Parse a dates in the browser timezone
|
||||
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const start = toDate(this._data!.dtstart, { timeZone: timeZone });
|
||||
const endValue = toDate(this._data!.dtend, { timeZone: timeZone });
|
||||
// All day events should be displayed as a day earlier
|
||||
|
||||
@@ -11,19 +11,14 @@ import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { resolveTimeZone } from "../../common/datetime/resolve-time-zone";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
import { isDate } from "../../common/string/is_date";
|
||||
import "../../components/entity/ha-entity-picker";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-date-input";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-formfield";
|
||||
import "../../components/ha-switch";
|
||||
import "../../components/ha-textarea";
|
||||
import "../../components/ha-textfield";
|
||||
import "../../components/ha-time-input";
|
||||
import {
|
||||
CalendarEntityFeature,
|
||||
@@ -33,6 +28,7 @@ import {
|
||||
deleteCalendarEvent,
|
||||
updateCalendarEvent,
|
||||
} from "../../data/calendar";
|
||||
import { TimeZone } from "../../data/translation";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../lovelace/components/hui-generic-entity-row";
|
||||
@@ -68,7 +64,7 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
// Dates are displayed in the timezone according to the user's profile
|
||||
// Dates are manipulated and displayed in the browser timezone
|
||||
// which may be different from the Home Assistant timezone. When
|
||||
// events are persisted, they are relative to the Home Assistant
|
||||
// timezone, but floating without a timezone.
|
||||
@@ -85,10 +81,10 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
computeStateDomain(stateObj) === "calendar" &&
|
||||
supportsFeature(stateObj, CalendarEntityFeature.CREATE_EVENT)
|
||||
)?.entity_id;
|
||||
this._timeZone = resolveTimeZone(
|
||||
this.hass.locale.time_zone,
|
||||
this.hass.config.time_zone
|
||||
);
|
||||
this._timeZone =
|
||||
this.hass.locale.time_zone === TimeZone.local
|
||||
? Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
: this.hass.config.time_zone;
|
||||
if (params.entry) {
|
||||
const entry = params.entry!;
|
||||
this._allDay = isDate(entry.dtstart);
|
||||
@@ -148,9 +144,9 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize(
|
||||
`ui.components.calendar.event.${isCreate ? "add" : "edit"}`
|
||||
)
|
||||
isCreate
|
||||
? this.hass.localize("ui.components.calendar.event.add")
|
||||
: this._summary
|
||||
)}
|
||||
>
|
||||
<div class="content">
|
||||
@@ -581,7 +577,7 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
@media all and (min-width: 450px) and (min-height: 500px) {
|
||||
@media all and (min-width: 450px and min-height: 500px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: min(600px, 95vw);
|
||||
--mdc-dialog-max-width: min(600px, 95vw);
|
||||
|
||||
@@ -35,13 +35,13 @@ import {
|
||||
export class RecurrenceRuleEditor extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@property() public disabled = false;
|
||||
|
||||
@property() public value = "";
|
||||
|
||||
@property() public dtstart?: Date;
|
||||
|
||||
@property({ type: Boolean }) public allDay = false;
|
||||
@property() public allDay?: boolean;
|
||||
|
||||
@property({ attribute: false }) public locale!: HomeAssistant["locale"];
|
||||
|
||||
|
||||
@@ -41,9 +41,9 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
|
||||
@state() public _applicationCredentials: ApplicationCredential[] = [];
|
||||
|
||||
@property({ type: Boolean }) public isWide = false;
|
||||
@property() public isWide!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@property() public route!: Route;
|
||||
|
||||
|
||||
@@ -36,9 +36,9 @@ import {
|
||||
export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public isWide = false;
|
||||
@property() public isWide?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@property() public route!: Route;
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@ import "./ha-config-areas-dashboard";
|
||||
class HaConfigAreas extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public isWide = false;
|
||||
@property() public isWide!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public showAdvanced = false;
|
||||
@property() public showAdvanced!: boolean;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "dashboard",
|
||||
|
||||
@@ -29,8 +29,6 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { domainIconWithoutDefault } from "../../../../common/entity/domain_icon";
|
||||
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import "../../../../components/ha-alert";
|
||||
@@ -192,13 +190,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
<h3 slot="header">
|
||||
<ha-svg-icon
|
||||
class="action-icon"
|
||||
.path=${type === "service" &&
|
||||
"service" in this.action &&
|
||||
this.action.service
|
||||
? domainIconWithoutDefault(
|
||||
computeDomain(this.action.service as string)
|
||||
) || ACTION_ICONS[type!]
|
||||
: ACTION_ICONS[type!]}
|
||||
.path=${ACTION_ICONS[type!]}
|
||||
></ha-svg-icon>
|
||||
${capitalizeFirstLetter(
|
||||
describeAction(this.hass, this._entityReg, this.action)
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import type { SortableEvent } from "sortablejs";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-sortable";
|
||||
import { getService, isService } from "../../../../data/action";
|
||||
import type { AutomationClipboard } from "../../../../data/automation";
|
||||
import { Action } from "../../../../data/script";
|
||||
import { sortableStyles } from "../../../../resources/ha-sortable-style";
|
||||
import type { SortableInstance } from "../../../../resources/sortable";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
PASTE_VALUE,
|
||||
@@ -45,6 +48,8 @@ export default class HaAutomationAction extends LitElement {
|
||||
|
||||
private _actionKeys = new WeakMap<Action, string>();
|
||||
|
||||
private _sortable?: SortableInstance;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this.reOrderMode && !this.nested
|
||||
@@ -58,68 +63,62 @@ export default class HaAutomationAction extends LitElement {
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.re_order_mode.description_actions"
|
||||
)}
|
||||
<ha-button slot="action" @click=${this._exitReOrderMode}>
|
||||
<mwc-button slot="action" @click=${this._exitReOrderMode}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.re_order_mode.exit"
|
||||
)}
|
||||
</ha-button>
|
||||
</mwc-button>
|
||||
</ha-alert>
|
||||
`
|
||||
: null}
|
||||
<ha-sortable
|
||||
handle-selector=".handle"
|
||||
.disabled=${!this.reOrderMode}
|
||||
@item-moved=${this._actionMoved}
|
||||
>
|
||||
<div class="actions">
|
||||
${repeat(
|
||||
this.actions,
|
||||
(action) => this._getKey(action),
|
||||
(action, idx) => html`
|
||||
<ha-automation-action-row
|
||||
.index=${idx}
|
||||
.action=${action}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
.hideMenu=${this.reOrderMode}
|
||||
.reOrderMode=${this.reOrderMode}
|
||||
@duplicate=${this._duplicateAction}
|
||||
@value-changed=${this._actionChanged}
|
||||
@re-order=${this._enterReOrderMode}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
${this.reOrderMode
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.index=${idx}
|
||||
slot="icons"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_up"
|
||||
)}
|
||||
.path=${mdiArrowUp}
|
||||
@click=${this._moveUp}
|
||||
.disabled=${idx === 0}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.index=${idx}
|
||||
slot="icons"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
.path=${mdiArrowDown}
|
||||
@click=${this._moveDown}
|
||||
.disabled=${idx === this.actions.length - 1}
|
||||
></ha-icon-button>
|
||||
<div class="handle" slot="icons">
|
||||
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-automation-action-row>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
<div class="actions">
|
||||
${repeat(
|
||||
this.actions,
|
||||
(action) => this._getKey(action),
|
||||
(action, idx) => html`
|
||||
<ha-automation-action-row
|
||||
.index=${idx}
|
||||
.action=${action}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
.hideMenu=${this.reOrderMode}
|
||||
.reOrderMode=${this.reOrderMode}
|
||||
@duplicate=${this._duplicateAction}
|
||||
@value-changed=${this._actionChanged}
|
||||
@re-order=${this._enterReOrderMode}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
${this.reOrderMode
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.index=${idx}
|
||||
slot="icons"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_up"
|
||||
)}
|
||||
.path=${mdiArrowUp}
|
||||
@click=${this._moveUp}
|
||||
.disabled=${idx === 0}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.index=${idx}
|
||||
slot="icons"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
.path=${mdiArrowDown}
|
||||
@click=${this._moveDown}
|
||||
.disabled=${idx === this.actions.length - 1}
|
||||
></ha-icon-button>
|
||||
<div class="handle" slot="icons">
|
||||
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-automation-action-row>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<ha-button
|
||||
outlined
|
||||
@@ -147,6 +146,13 @@ export default class HaAutomationAction extends LitElement {
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has("reOrderMode")) {
|
||||
if (this.reOrderMode) {
|
||||
this._createSortable();
|
||||
} else {
|
||||
this._destroySortable();
|
||||
}
|
||||
}
|
||||
if (changedProps.has("actions") && this._focusLastActionOnChange) {
|
||||
this._focusLastActionOnChange = false;
|
||||
|
||||
@@ -185,7 +191,6 @@ export default class HaAutomationAction extends LitElement {
|
||||
} else if (isService(action)) {
|
||||
actions = this.actions.concat({
|
||||
service: getService(action),
|
||||
metadata: {},
|
||||
});
|
||||
} else {
|
||||
const elClass = customElements.get(
|
||||
@@ -209,6 +214,33 @@ export default class HaAutomationAction extends LitElement {
|
||||
this.reOrderMode = false;
|
||||
}
|
||||
|
||||
private async _createSortable() {
|
||||
const Sortable = (await import("../../../../resources/sortable")).default;
|
||||
this._sortable = new Sortable(this.shadowRoot!.querySelector(".actions")!, {
|
||||
animation: 150,
|
||||
fallbackClass: "sortable-fallback",
|
||||
handle: ".handle",
|
||||
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 _destroySortable() {
|
||||
this._sortable?.destroy();
|
||||
this._sortable = undefined;
|
||||
}
|
||||
|
||||
private _getKey(action: Action) {
|
||||
if (!this._actionKeys.has(action)) {
|
||||
this._actionKeys.set(action, Math.random().toString());
|
||||
@@ -229,6 +261,11 @@ export default class HaAutomationAction extends LitElement {
|
||||
this._move(index, newIndex);
|
||||
}
|
||||
|
||||
private _dragged(ev: SortableEvent): void {
|
||||
if (ev.oldIndex === ev.newIndex) return;
|
||||
this._move(ev.oldIndex!, ev.newIndex!);
|
||||
}
|
||||
|
||||
private _move(index: number, newIndex: number) {
|
||||
const actions = this.actions.concat();
|
||||
const action = actions.splice(index, 1)[0];
|
||||
@@ -236,12 +273,6 @@ export default class HaAutomationAction extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: actions });
|
||||
}
|
||||
|
||||
private _actionMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
this._move(oldIndex, newIndex);
|
||||
}
|
||||
|
||||
private _actionChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const actions = [...this.actions];
|
||||
@@ -270,36 +301,39 @@ export default class HaAutomationAction extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-automation-action-row {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
scroll-margin-top: 48px;
|
||||
}
|
||||
ha-svg-icon {
|
||||
height: 20px;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
overflow: hidden;
|
||||
}
|
||||
.handle {
|
||||
padding: 12px;
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
}
|
||||
.handle ha-svg-icon {
|
||||
pointer-events: none;
|
||||
height: 24px;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
`;
|
||||
return [
|
||||
sortableStyles,
|
||||
css`
|
||||
ha-automation-action-row {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
scroll-margin-top: 48px;
|
||||
}
|
||||
ha-svg-icon {
|
||||
height: 20px;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
overflow: hidden;
|
||||
}
|
||||
.handle {
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
padding: 12px;
|
||||
}
|
||||
.handle ha-svg-icon {
|
||||
pointer-events: none;
|
||||
height: 24px;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
import { consume } from "@lit-labs/context";
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import type { SortableEvent } from "sortablejs";
|
||||
import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiContentDuplicate,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiDrag,
|
||||
mdiPlus,
|
||||
mdiRenameBox,
|
||||
mdiSort,
|
||||
mdiContentDuplicate,
|
||||
mdiDelete,
|
||||
mdiPlus,
|
||||
mdiArrowUp,
|
||||
mdiArrowDown,
|
||||
mdiDrag,
|
||||
} from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import type { SortableInstance } from "../../../../../resources/sortable";
|
||||
import { ensureArray } from "../../../../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { capitalizeFirstLetter } from "../../../../../common/string/capitalize-first-letter";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-button-menu";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-sortable";
|
||||
import "../../../../../components/ha-button-menu";
|
||||
import { Condition } from "../../../../../data/automation";
|
||||
import { describeCondition } from "../../../../../data/automation_i18n";
|
||||
import { fullEntitiesContext } from "../../../../../data/context";
|
||||
import { EntityRegistryEntry } from "../../../../../data/entity_registry";
|
||||
import {
|
||||
Action,
|
||||
ChooseAction,
|
||||
@@ -38,6 +36,10 @@ import {
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import { ActionElement } from "../ha-automation-action-row";
|
||||
import { describeCondition } from "../../../../../data/automation_i18n";
|
||||
import { fullEntitiesContext } from "../../../../../data/context";
|
||||
import { EntityRegistryEntry } from "../../../../../data/entity_registry";
|
||||
import { sortableStyles } from "../../../../../resources/ha-sortable-style";
|
||||
|
||||
const preventDefault = (ev) => ev.preventDefault();
|
||||
|
||||
@@ -61,6 +63,8 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
|
||||
private _expandLast = false;
|
||||
|
||||
private _sortable?: SortableInstance;
|
||||
|
||||
public static get defaultConfig() {
|
||||
return { choose: [{ conditions: [], sequence: [] }] };
|
||||
}
|
||||
@@ -96,166 +100,157 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
const action = this.action;
|
||||
|
||||
return html`
|
||||
<ha-sortable
|
||||
handle-selector=".handle"
|
||||
.disabled=${!this.reOrderMode}
|
||||
@item-moved=${this._optionMoved}
|
||||
>
|
||||
<div class="options">
|
||||
${repeat(
|
||||
action.choose ? ensureArray(action.choose) : [],
|
||||
(option) => option,
|
||||
(option, idx) => html`
|
||||
<div class="option">
|
||||
<ha-card>
|
||||
<ha-expansion-panel
|
||||
.index=${idx}
|
||||
leftChevron
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<h3 slot="header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.option",
|
||||
{ number: idx + 1 }
|
||||
)}:
|
||||
${option.alias ||
|
||||
(this._expandedStates[idx]
|
||||
? ""
|
||||
: this._getDescription(option))}
|
||||
</h3>
|
||||
${this.reOrderMode
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.index=${idx}
|
||||
slot="icons"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_up"
|
||||
)}
|
||||
.path=${mdiArrowUp}
|
||||
@click=${this._moveUp}
|
||||
.disabled=${idx === 0}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.index=${idx}
|
||||
slot="icons"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
.path=${mdiArrowDown}
|
||||
@click=${this._moveDown}
|
||||
.disabled=${idx ===
|
||||
ensureArray(this.action.choose).length - 1}
|
||||
></ha-icon-button>
|
||||
<div class="handle" slot="icons">
|
||||
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-button-menu
|
||||
slot="icons"
|
||||
.idx=${idx}
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
fixed
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<mwc-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.rename"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiRenameBox}
|
||||
></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
<mwc-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.re_order"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiSort}
|
||||
></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
|
||||
<mwc-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
|
||||
<mwc-list-item
|
||||
class="warning"
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="graphic"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
</ha-button-menu>
|
||||
`}
|
||||
<div class="card-content">
|
||||
<h4>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.conditions"
|
||||
)}:
|
||||
</h4>
|
||||
<ha-automation-condition
|
||||
nested
|
||||
.conditions=${ensureArray<string | Condition>(
|
||||
option.conditions
|
||||
<div class="options">
|
||||
${repeat(
|
||||
action.choose ? ensureArray(action.choose) : [],
|
||||
(option) => option,
|
||||
(option, idx) =>
|
||||
html`<ha-card>
|
||||
<ha-expansion-panel
|
||||
.index=${idx}
|
||||
leftChevron
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<h3 slot="header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.option",
|
||||
{ number: idx + 1 }
|
||||
)}:
|
||||
${option.alias ||
|
||||
(this._expandedStates[idx]
|
||||
? ""
|
||||
: this._getDescription(option))}
|
||||
</h3>
|
||||
${this.reOrderMode
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.index=${idx}
|
||||
slot="icons"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_up"
|
||||
)}
|
||||
.reOrderMode=${this.reOrderMode}
|
||||
.disabled=${this.disabled}
|
||||
.hass=${this.hass}
|
||||
.path=${mdiArrowUp}
|
||||
@click=${this._moveUp}
|
||||
.disabled=${idx === 0}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.index=${idx}
|
||||
slot="icons"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.move_down"
|
||||
)}
|
||||
.path=${mdiArrowDown}
|
||||
@click=${this._moveDown}
|
||||
.disabled=${idx ===
|
||||
ensureArray(this.action.choose).length - 1}
|
||||
></ha-icon-button>
|
||||
<div class="handle" slot="icons">
|
||||
<ha-svg-icon .path=${mdiDrag}></ha-svg-icon>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-button-menu
|
||||
slot="icons"
|
||||
.idx=${idx}
|
||||
@value-changed=${this._conditionChanged}
|
||||
></ha-automation-condition>
|
||||
<h4>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.sequence"
|
||||
)}:
|
||||
</h4>
|
||||
<ha-automation-action
|
||||
nested
|
||||
.actions=${ensureArray(option.sequence) || []}
|
||||
.reOrderMode=${this.reOrderMode}
|
||||
.disabled=${this.disabled}
|
||||
.hass=${this.hass}
|
||||
.idx=${idx}
|
||||
@value-changed=${this._actionChanged}
|
||||
></ha-automation-action>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
</ha-card>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
@action=${this._handleAction}
|
||||
@click=${preventDefault}
|
||||
fixed
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<mwc-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.rename"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiRenameBox}
|
||||
></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
<mwc-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.re_order"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiSort}
|
||||
></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
|
||||
<mwc-list-item
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.duplicate"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
|
||||
<mwc-list-item
|
||||
class="warning"
|
||||
graphic="icon"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="graphic"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</mwc-list-item>
|
||||
</ha-button-menu>
|
||||
`}
|
||||
<div class="card-content">
|
||||
<h4>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.conditions"
|
||||
)}:
|
||||
</h4>
|
||||
<ha-automation-condition
|
||||
nested
|
||||
.conditions=${ensureArray<string | Condition>(
|
||||
option.conditions
|
||||
)}
|
||||
.reOrderMode=${this.reOrderMode}
|
||||
.disabled=${this.disabled}
|
||||
.hass=${this.hass}
|
||||
.idx=${idx}
|
||||
@value-changed=${this._conditionChanged}
|
||||
></ha-automation-condition>
|
||||
<h4>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.sequence"
|
||||
)}:
|
||||
</h4>
|
||||
<ha-automation-action
|
||||
nested
|
||||
.actions=${ensureArray(option.sequence) || []}
|
||||
.reOrderMode=${this.reOrderMode}
|
||||
.disabled=${this.disabled}
|
||||
.hass=${this.hass}
|
||||
.idx=${idx}
|
||||
@value-changed=${this._actionChanged}
|
||||
></ha-automation-action>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
</ha-card>`
|
||||
)}
|
||||
</div>
|
||||
<ha-button
|
||||
outlined
|
||||
.label=${this.hass.localize(
|
||||
@@ -357,6 +352,14 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has("reOrderMode")) {
|
||||
if (this.reOrderMode) {
|
||||
this._createSortable();
|
||||
} else {
|
||||
this._destroySortable();
|
||||
}
|
||||
}
|
||||
|
||||
if (this._expandLast) {
|
||||
const nodes = this.shadowRoot!.querySelectorAll("ha-expansion-panel");
|
||||
nodes[nodes.length - 1].expanded = true;
|
||||
@@ -422,6 +425,11 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
this._move(index, newIndex);
|
||||
}
|
||||
|
||||
private _dragged(ev: SortableEvent): void {
|
||||
if (ev.oldIndex === ev.newIndex) return;
|
||||
this._move(ev.oldIndex!, ev.newIndex!);
|
||||
}
|
||||
|
||||
private _move(index: number, newIndex: number) {
|
||||
const options = ensureArray(this.action.choose)!.concat();
|
||||
const item = options.splice(index, 1)[0];
|
||||
@@ -435,12 +443,6 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _optionMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
this._move(oldIndex, newIndex);
|
||||
}
|
||||
|
||||
private _removeOption(ev: CustomEvent) {
|
||||
const index = (ev.target as any).idx;
|
||||
showConfirmationDialog(this, {
|
||||
@@ -468,23 +470,49 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
|
||||
private _defaultChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._showDefault = true;
|
||||
const defaultAction = ev.detail.value as Action[];
|
||||
const newValue: ChooseAction = {
|
||||
...this.action,
|
||||
default: defaultAction,
|
||||
};
|
||||
if (defaultAction.length === 0) {
|
||||
delete newValue.default;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
const value = ev.detail.value as Action[];
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.action,
|
||||
default: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async _createSortable() {
|
||||
const Sortable = (await import("../../../../../resources/sortable"))
|
||||
.default;
|
||||
this._sortable = new Sortable(this.shadowRoot!.querySelector(".options")!, {
|
||||
animation: 150,
|
||||
fallbackClass: "sortable-fallback",
|
||||
handle: ".handle",
|
||||
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 _destroySortable() {
|
||||
this._sortable?.destroy();
|
||||
this._sortable = undefined;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
sortableStyles,
|
||||
css`
|
||||
.option {
|
||||
ha-card {
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
.add-card mwc-button {
|
||||
@@ -515,9 +543,9 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
padding: 0 16px 16px 16px;
|
||||
}
|
||||
.handle {
|
||||
padding: 12px;
|
||||
cursor: move; /* fallback if grab cursor is unsupported */
|
||||
cursor: grab;
|
||||
padding: 12px;
|
||||
}
|
||||
.handle ha-svg-icon {
|
||||
pointer-events: none;
|
||||
|
||||
@@ -117,16 +117,14 @@ export class HaIfAction extends LitElement implements ActionElement {
|
||||
|
||||
private _elseChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._showElse = true;
|
||||
const elseAction = ev.detail.value as Action[];
|
||||
const newValue: IfAction = {
|
||||
...this.action,
|
||||
else: elseAction,
|
||||
};
|
||||
if (elseAction.length === 0) {
|
||||
delete newValue.else;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
const value = ev.detail.value as Action[];
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.action,
|
||||
else: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
@@ -9,10 +9,10 @@ import type { HomeAssistant } from "../../../../../types";
|
||||
import "../ha-automation-action";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
|
||||
import { isTemplate } from "../../../../../common/string/has-template";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
import { hasTemplate } from "../../../../../common/string/has-template";
|
||||
|
||||
const OPTIONS = ["count", "while", "until", "for_each"] as const;
|
||||
|
||||
@@ -32,13 +32,22 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
return { repeat: { count: 2, sequence: [] } };
|
||||
}
|
||||
|
||||
public willUpdate(changedProperties: PropertyValues) {
|
||||
if (!changedProperties.has("action")) {
|
||||
return;
|
||||
}
|
||||
// Check for templates in action. If found, revert to YAML mode.
|
||||
if (this.action && hasTemplate(this.action)) {
|
||||
fireEvent(
|
||||
this,
|
||||
"ui-mode-not-available",
|
||||
Error(this.hass.localize("ui.errors.config.no_template_editor_support"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
type: string,
|
||||
reOrderMode: boolean,
|
||||
template: boolean
|
||||
) =>
|
||||
(localize: LocalizeFunc, type: string, reOrderMode: boolean) =>
|
||||
[
|
||||
{
|
||||
name: "type",
|
||||
@@ -59,9 +68,7 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
{
|
||||
name: "count",
|
||||
required: true,
|
||||
selector: template
|
||||
? ({ template: {} } as const)
|
||||
: ({ number: { mode: "box", min: 1 } } as const),
|
||||
selector: { number: { mode: "box", min: 1 } },
|
||||
},
|
||||
] as const)
|
||||
: []),
|
||||
@@ -97,10 +104,7 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
const schema = this._schema(
|
||||
this.hass.localize,
|
||||
type ?? "count",
|
||||
this.reOrderMode,
|
||||
"count" in action && typeof action.count === "string"
|
||||
? isTemplate(action.count)
|
||||
: false
|
||||
this.reOrderMode
|
||||
);
|
||||
const data = { ...action, type };
|
||||
return html`<ha-form
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import { mdiClose, mdiContentPaste, mdiPlus } from "@mdi/js";
|
||||
import Fuse, { IFuseOptions } from "fuse.js";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { domainIconWithoutDefault } from "../../../common/entity/domain_icon";
|
||||
import { domainIcon } from "../../../common/entity/domain_icon";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
@@ -45,13 +38,10 @@ import { TRIGGER_GROUPS, TRIGGER_ICONS } from "../../../data/trigger";
|
||||
import { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import {
|
||||
AddAutomationElementDialogParams,
|
||||
PASTE_VALUE,
|
||||
} from "./show-add-automation-element-dialog";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
|
||||
const TYPES = {
|
||||
trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS },
|
||||
@@ -69,8 +59,7 @@ interface ListItem {
|
||||
key: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon?: string;
|
||||
image?: string;
|
||||
icon: string;
|
||||
group: boolean;
|
||||
}
|
||||
|
||||
@@ -90,8 +79,6 @@ const ENTITY_DOMAINS_OTHER = new Set([
|
||||
"image_processing",
|
||||
]);
|
||||
|
||||
const ENTITY_DOMAINS_MAIN = new Set(["notify"]);
|
||||
|
||||
@customElement("add-automation-element-dialog")
|
||||
class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -106,15 +93,13 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
|
||||
@state() private _manifests?: DomainManifestLookup;
|
||||
|
||||
@state() private _domains?: Set<string>;
|
||||
|
||||
@query("ha-dialog") private _dialog?: HaDialog;
|
||||
|
||||
private _fullScreen = false;
|
||||
|
||||
@state() private _width?: number;
|
||||
private _width?: number;
|
||||
|
||||
@state() private _height?: number;
|
||||
private _height?: number;
|
||||
|
||||
public showDialog(params): void {
|
||||
this._params = params;
|
||||
@@ -122,7 +107,6 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
if (this._params?.type === "action") {
|
||||
this.hass.loadBackendTranslation("services");
|
||||
this._fetchManifests();
|
||||
this._calculateUsedDomains();
|
||||
}
|
||||
this._fullScreen = matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
@@ -140,19 +124,8 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
this._prev = undefined;
|
||||
this._filter = "";
|
||||
this._manifests = undefined;
|
||||
this._domains = undefined;
|
||||
}
|
||||
|
||||
private _getGroups = (
|
||||
type: AddAutomationElementDialogParams["type"],
|
||||
group: string | undefined
|
||||
): AutomationElementGroup =>
|
||||
group
|
||||
? isService(group)
|
||||
? {}
|
||||
: TYPES[type].groups[group].members!
|
||||
: TYPES[type].groups;
|
||||
|
||||
private _convertToItem = (
|
||||
key: string,
|
||||
options,
|
||||
@@ -185,7 +158,11 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
services: HomeAssistant["services"],
|
||||
manifests?: DomainManifestLookup
|
||||
): ListItem[] => {
|
||||
const groups = this._getGroups(type, group);
|
||||
const groups: AutomationElementGroup = group
|
||||
? isService(group)
|
||||
? {}
|
||||
: TYPES[type].groups[group].members!
|
||||
: TYPES[type].groups;
|
||||
|
||||
const flattenGroups = (grp: AutomationElementGroup) =>
|
||||
Object.entries(grp).map(([key, options]) =>
|
||||
@@ -215,23 +192,21 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
(
|
||||
type: AddAutomationElementDialogParams["type"],
|
||||
group: string | undefined,
|
||||
domains: Set<string> | undefined,
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"],
|
||||
manifests?: DomainManifestLookup
|
||||
): ListItem[] => {
|
||||
if (type === "action" && isService(group)) {
|
||||
let result = this._services(localize, services, manifests, group);
|
||||
const result = this._services(localize, services, manifests, group);
|
||||
if (group === `${SERVICE_PREFIX}media_player`) {
|
||||
result = [
|
||||
this._convertToItem("play_media", {}, type, localize),
|
||||
...result,
|
||||
];
|
||||
result.unshift(this._convertToItem("play_media", {}, type, localize));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const groups = this._getGroups(type, group);
|
||||
const groups: AutomationElementGroup = group
|
||||
? TYPES[type].groups[group].members!
|
||||
: TYPES[type].groups;
|
||||
|
||||
const result = Object.entries(groups).map(([key, options]) =>
|
||||
this._convertToItem(key, options, type, localize)
|
||||
@@ -240,33 +215,15 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
if (type === "action") {
|
||||
if (!this._group) {
|
||||
result.unshift(
|
||||
...this._serviceGroups(
|
||||
localize,
|
||||
services,
|
||||
manifests,
|
||||
domains,
|
||||
undefined
|
||||
)
|
||||
...this._serviceGroups(localize, services, manifests, undefined)
|
||||
);
|
||||
} else if (this._group === "helpers") {
|
||||
result.unshift(
|
||||
...this._serviceGroups(
|
||||
localize,
|
||||
services,
|
||||
manifests,
|
||||
domains,
|
||||
"helper"
|
||||
)
|
||||
...this._serviceGroups(localize, services, manifests, "helper")
|
||||
);
|
||||
} else if (this._group === "other") {
|
||||
result.unshift(
|
||||
...this._serviceGroups(
|
||||
localize,
|
||||
services,
|
||||
manifests,
|
||||
domains,
|
||||
"other"
|
||||
)
|
||||
...this._serviceGroups(localize, services, manifests, "other")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -286,54 +243,44 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
}
|
||||
);
|
||||
|
||||
private _serviceGroups = (
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"],
|
||||
manifests: DomainManifestLookup | undefined,
|
||||
domains: Set<string> | undefined,
|
||||
type: "helper" | "other" | undefined
|
||||
): ListItem[] => {
|
||||
if (!services || !manifests) {
|
||||
return [];
|
||||
}
|
||||
const result: ListItem[] = [];
|
||||
Object.keys(services).forEach((domain) => {
|
||||
const manifest = manifests[domain];
|
||||
const domainUsed = !domains ? true : domains.has(domain);
|
||||
if (
|
||||
(type === undefined &&
|
||||
(ENTITY_DOMAINS_MAIN.has(domain) ||
|
||||
(manifest?.integration_type === "entity" &&
|
||||
domainUsed &&
|
||||
!ENTITY_DOMAINS_OTHER.has(domain)))) ||
|
||||
(type === "helper" && manifest?.integration_type === "helper") ||
|
||||
(type === "other" &&
|
||||
!ENTITY_DOMAINS_MAIN.has(domain) &&
|
||||
(ENTITY_DOMAINS_OTHER.has(domain) ||
|
||||
(!domainUsed && manifest?.integration_type === "entity") ||
|
||||
!["helper", "entity"].includes(manifest?.integration_type || "")))
|
||||
) {
|
||||
const icon = domainIconWithoutDefault(domain);
|
||||
result.push({
|
||||
group: true,
|
||||
icon,
|
||||
image: !icon
|
||||
? brandsUrl({
|
||||
domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})
|
||||
: undefined,
|
||||
key: `${SERVICE_PREFIX}${domain}`,
|
||||
name: domainToName(localize, domain, manifest),
|
||||
description: "",
|
||||
});
|
||||
private _serviceGroups = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"],
|
||||
manifests: DomainManifestLookup | undefined,
|
||||
type: "helper" | "other" | undefined
|
||||
): ListItem[] => {
|
||||
if (!services || !manifests) {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
return result.sort((a, b) =>
|
||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||
);
|
||||
};
|
||||
const result: ListItem[] = [];
|
||||
Object.keys(services)
|
||||
.sort()
|
||||
.forEach((domain) => {
|
||||
const manifest = manifests[domain];
|
||||
if (
|
||||
(type === undefined &&
|
||||
manifest?.integration_type === "entity" &&
|
||||
!ENTITY_DOMAINS_OTHER.has(domain)) ||
|
||||
(type === "helper" && manifest?.integration_type === "helper") ||
|
||||
(type === "other" &&
|
||||
(ENTITY_DOMAINS_OTHER.has(domain) ||
|
||||
!["helper", "entity"].includes(
|
||||
manifest?.integration_type || ""
|
||||
)))
|
||||
) {
|
||||
result.push({
|
||||
group: true,
|
||||
icon: domainIcon(domain),
|
||||
key: `${SERVICE_PREFIX}${domain}`,
|
||||
name: domainToName(localize, domain, manifest),
|
||||
description: "",
|
||||
});
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
);
|
||||
|
||||
private _services = memoizeOne(
|
||||
(
|
||||
@@ -357,17 +304,9 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
const services_keys = Object.keys(services[dmn]);
|
||||
|
||||
for (const service of services_keys) {
|
||||
const icon = domainIconWithoutDefault(dmn);
|
||||
result.push({
|
||||
group: false,
|
||||
icon,
|
||||
image: !icon
|
||||
? brandsUrl({
|
||||
domain: dmn,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
})
|
||||
: undefined,
|
||||
icon: domainIcon(dmn),
|
||||
key: `${SERVICE_PREFIX}${dmn}.${service}`,
|
||||
name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${
|
||||
this.hass.localize(`component.${dmn}.services.${service}.name`) ||
|
||||
@@ -423,13 +362,6 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
this._manifests = manifests;
|
||||
}
|
||||
|
||||
private _calculateUsedDomains() {
|
||||
const domains = new Set(Object.keys(this.hass.states).map(computeDomain));
|
||||
if (!deepEqual(domains, this._domains)) {
|
||||
this._domains = domains;
|
||||
}
|
||||
}
|
||||
|
||||
protected _opened(): void {
|
||||
// Store the width and height so that when we search, box doesn't jump
|
||||
const boundingRect =
|
||||
@@ -438,16 +370,6 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
this._height = boundingRect?.height;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (
|
||||
this._params?.type === "action" &&
|
||||
changedProperties.has("hass") &&
|
||||
changedProperties.get("hass")?.states !== this.hass.states
|
||||
) {
|
||||
this._calculateUsedDomains();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
@@ -465,7 +387,6 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
: this._getGroupItems(
|
||||
this._params.type,
|
||||
this._group,
|
||||
this._domains,
|
||||
this.hass.localize,
|
||||
this.hass.services,
|
||||
this._manifests
|
||||
@@ -578,18 +499,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
>
|
||||
${item.name}
|
||||
<span slot="secondary">${item.description}</span>
|
||||
${item.icon
|
||||
? html`<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${item.icon}
|
||||
></ha-svg-icon>`
|
||||
: html`<img
|
||||
alt=""
|
||||
slot="graphic"
|
||||
src=${item.image}
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>`}
|
||||
<ha-svg-icon slot="graphic" .path=${item.icon}></ha-svg-icon>
|
||||
${item.group
|
||||
? html`<ha-icon-next slot="meta"></ha-icon-next>`
|
||||
: html`<ha-svg-icon
|
||||
@@ -605,7 +515,6 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
}
|
||||
|
||||
private _back() {
|
||||
this._dialog!.scrollToPos(0, 0);
|
||||
if (this._filter) {
|
||||
this._filter = "";
|
||||
return;
|
||||
@@ -654,10 +563,6 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
ha-icon-next {
|
||||
width: 24px;
|
||||
}
|
||||
mwc-list {
|
||||
max-height: 468px;
|
||||
max-width: 100vw;
|
||||
}
|
||||
search-input {
|
||||
display: block;
|
||||
margin: 0 16px;
|
||||
|
||||
@@ -24,7 +24,7 @@ import "../ha-config-section";
|
||||
export class HaBlueprintAutomationEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public isWide = false;
|
||||
@property() public isWide!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user