Compare commits

..

7 Commits

36 changed files with 1662 additions and 1550 deletions

View File

@@ -1,10 +1,7 @@
/* eslint-disable lit/prefer-static-styles */
import type { TemplateResult } from "lit";
import { html } from "lit";
import { customElement } from "lit/decorators";
import { HaFormString } from "../components/ha-form/ha-form-string";
import "../components/ha-icon-button";
import "./ha-auth-textfield";
import "../components/ha-input";
@customElement("ha-auth-form-string")
export class HaAuthFormString extends HaFormString {
@@ -12,59 +9,9 @@ export class HaAuthFormString extends HaFormString {
return this;
}
protected render(): TemplateResult {
return html`
<style>
ha-auth-form-string {
display: block;
position: relative;
}
ha-auth-form-string[own-margin] {
margin-bottom: 5px;
}
ha-auth-form-string ha-auth-textfield {
display: block !important;
}
ha-auth-form-string ha-icon-button {
position: absolute;
top: 8px;
right: 8px;
inset-inline-start: initial;
inset-inline-end: 8px;
--ha-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
}
</style>
<ha-auth-textfield
.type=${!this.isPassword
? this.stringType
: this.unmaskedPassword
? "text"
: "password"}
.label=${this.label}
.value=${this.data || ""}
.helper=${this.helper}
helperPersistent
.disabled=${this.disabled}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
.name=${this.schema.name}
.autocomplete=${this.schema.autocomplete}
?autofocus=${this.schema.autofocus}
.suffix=${this.isPassword
? // reserve some space for the icon.
html`<div style="width: 24px"></div>`
: this.schema.description?.suffix}
.validationMessage=${this.schema.required
? this.localize?.("ui.panel.page-authorize.form.error_required")
: undefined}
@input=${this._valueChanged}
@change=${this._valueChanged}
></ha-auth-textfield>
${this.renderIcon()}
`;
public connectedCallback(): void {
super.connectedCallback();
this.style.position = "relative";
}
}

View File

@@ -1,264 +0,0 @@
/* eslint-disable lit/value-after-constraints */
/* eslint-disable lit/prefer-static-styles */
import { floatingLabel } from "@material/mwc-floating-label/mwc-floating-label-directive";
import type { TemplateResult } from "lit";
import { html } from "lit";
import { customElement } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { live } from "lit/directives/live";
import { HaTextField } from "../components/ha-textfield";
@customElement("ha-auth-textfield")
export class HaAuthTextField extends HaTextField {
protected renderLabel(): TemplateResult | string {
return !this.label
? ""
: html`
<span
.floatingLabelFoundation=${floatingLabel(
this.label
) as unknown as any}
.id=${this.name}
>${this.label}</span
>
`;
}
protected renderInput(shouldRenderHelperText: boolean): TemplateResult {
const minOrUndef = this.minLength === -1 ? undefined : this.minLength;
const maxOrUndef = this.maxLength === -1 ? undefined : this.maxLength;
const autocapitalizeOrUndef = this.autocapitalize
? (this.autocapitalize as
| "off"
| "none"
| "on"
| "sentences"
| "words"
| "characters")
: undefined;
const showValidationMessage = this.validationMessage && !this.isUiValid;
const ariaLabelledbyOrUndef = this.label ? this.name : undefined;
const ariaControlsOrUndef = shouldRenderHelperText
? "helper-text"
: undefined;
const ariaDescribedbyOrUndef =
this.focused || this.helperPersistent || showValidationMessage
? "helper-text"
: undefined;
// 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
aria-labelledby=${ifDefined(ariaLabelledbyOrUndef)}
aria-controls=${ifDefined(ariaControlsOrUndef)}
aria-describedby=${ifDefined(ariaDescribedbyOrUndef)}
class="mdc-text-field__input"
type=${this.type}
.value=${live(this.value) as unknown as string}
?disabled=${this.disabled}
placeholder=${this.placeholder}
?required=${this.required}
?readonly=${this.readOnly}
minlength=${ifDefined(minOrUndef)}
maxlength=${ifDefined(maxOrUndef)}
pattern=${ifDefined(this.pattern ? this.pattern : undefined)}
min=${ifDefined(this.min === "" ? undefined : (this.min as number))}
max=${ifDefined(this.max === "" ? undefined : (this.max as number))}
step=${ifDefined(this.step === null ? undefined : (this.step as number))}
size=${ifDefined(this.size === null ? undefined : this.size)}
name=${ifDefined(this.name === "" ? undefined : this.name)}
inputmode=${ifDefined(this.inputMode)}
autocapitalize=${ifDefined(autocapitalizeOrUndef)}
?autofocus=${this.autofocus}
@input=${this.handleInputChange}
@focus=${this.onInputFocus}
@blur=${this.onInputBlur}
/>`;
}
public render() {
return html`
<style>
ha-auth-textfield {
display: inline-flex;
flex-direction: column;
outline: none;
}
ha-auth-textfield:not([disabled]):hover
:not(.mdc-text-field--invalid):not(.mdc-text-field--focused)
mwc-notched-outline {
--mdc-notched-outline-border-color: var(
--mdc-text-field-outlined-hover-border-color,
rgba(0, 0, 0, 0.87)
);
}
ha-auth-textfield:not([disabled])
.mdc-text-field:not(.mdc-text-field--outlined) {
background-color: var(--mdc-text-field-fill-color, whitesmoke);
}
ha-auth-textfield:not([disabled])
.mdc-text-field.mdc-text-field--invalid
mwc-notched-outline {
--mdc-notched-outline-border-color: var(
--mdc-text-field-error-color,
var(--mdc-theme-error, #b00020)
);
}
ha-auth-textfield:not([disabled])
.mdc-text-field.mdc-text-field--invalid
+ .mdc-text-field-helper-line
.mdc-text-field-character-counter,
ha-auth-textfield:not([disabled])
.mdc-text-field.mdc-text-field--invalid
.mdc-text-field__icon {
color: var(
--mdc-text-field-error-color,
var(--mdc-theme-error, #b00020)
);
}
ha-auth-textfield:not([disabled])
.mdc-text-field:not(.mdc-text-field--invalid):not(
.mdc-text-field--focused
)
.mdc-floating-label,
ha-auth-textfield:not([disabled])
.mdc-text-field:not(.mdc-text-field--invalid):not(
.mdc-text-field--focused
)
.mdc-floating-label::after {
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
}
ha-auth-textfield:not([disabled])
.mdc-text-field.mdc-text-field--focused
mwc-notched-outline {
--mdc-notched-outline-stroke-width: 2px;
}
ha-auth-textfield:not([disabled])
.mdc-text-field.mdc-text-field--focused:not(.mdc-text-field--invalid)
mwc-notched-outline {
--mdc-notched-outline-border-color: var(
--mdc-text-field-focused-label-color,
var(--mdc-theme-primary, rgba(98, 0, 238, 0.87))
);
}
ha-auth-textfield:not([disabled])
.mdc-text-field.mdc-text-field--focused:not(.mdc-text-field--invalid)
.mdc-floating-label {
color: #6200ee;
color: var(--mdc-theme-primary, #6200ee);
}
ha-auth-textfield:not([disabled])
.mdc-text-field
.mdc-text-field__input {
color: var(--mdc-text-field-ink-color, rgba(0, 0, 0, 0.87));
}
ha-auth-textfield:not([disabled])
.mdc-text-field
.mdc-text-field__input::placeholder {
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
}
ha-auth-textfield:not([disabled])
.mdc-text-field-helper-line
.mdc-text-field-helper-text:not(
.mdc-text-field-helper-text--validation-msg
),
ha-auth-textfield:not([disabled])
.mdc-text-field-helper-line:not(.mdc-text-field--invalid)
.mdc-text-field-character-counter {
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
}
ha-auth-textfield[disabled]
.mdc-text-field:not(.mdc-text-field--outlined) {
background-color: var(--mdc-text-field-disabled-fill-color, #fafafa);
}
ha-auth-textfield[disabled]
.mdc-text-field.mdc-text-field--outlined
mwc-notched-outline {
--mdc-notched-outline-border-color: var(
--mdc-text-field-outlined-disabled-border-color,
rgba(0, 0, 0, 0.06)
);
}
ha-auth-textfield[disabled]
.mdc-text-field:not(.mdc-text-field--invalid):not(
.mdc-text-field--focused
)
.mdc-floating-label,
ha-auth-textfield[disabled]
.mdc-text-field:not(.mdc-text-field--invalid):not(
.mdc-text-field--focused
)
.mdc-floating-label::after {
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38));
}
ha-auth-textfield[disabled] .mdc-text-field .mdc-text-field__input,
ha-auth-textfield[disabled]
.mdc-text-field
.mdc-text-field__input::placeholder {
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38));
}
ha-auth-textfield[disabled]
.mdc-text-field-helper-line
.mdc-text-field-helper-text,
ha-auth-textfield[disabled]
.mdc-text-field-helper-line
.mdc-text-field-character-counter {
color: var(--mdc-text-field-disabled-ink-color, rgba(0, 0, 0, 0.38));
}
ha-auth-textfield:not([disabled])
.mdc-text-field.mdc-text-field--focused:not(.mdc-text-field--invalid)
.mdc-floating-label {
color: var(--mdc-theme-primary, #6200ee);
}
ha-auth-textfield[no-spinner] input::-webkit-outer-spin-button,
ha-auth-textfield[no-spinner] input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
ha-auth-textfield[no-spinner] input[type="number"] {
-moz-appearance: textfield;
}
</style>
${super.render()}
`;
}
protected createRenderRoot() {
// add parent style to light dom
const style = document.createElement("style");
style.textContent = HaTextField.elementStyles as unknown as string;
this.append(style);
return this;
}
public firstUpdated() {
super.firstUpdated();
if (this.autofocus) {
this.focus();
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-auth-textfield": HaAuthTextField;
}
}

View File

@@ -1,15 +1,11 @@
import { mdiEye, mdiEyeOff } from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type {
LocalizeFunc,
LocalizeKeys,
} from "../../common/translations/localize";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../ha-icon-button";
import "../ha-textfield";
import type { HaTextField } from "../ha-textfield";
import "../ha-input";
import type { HaInput } from "../ha-input";
import type {
HaFormElement,
HaFormStringData,
@@ -37,7 +33,7 @@ export class HaFormString extends LitElement implements HaFormElement {
@state() protected unmaskedPassword = false;
@query("ha-textfield") private _input?: HaTextField;
@query("ha-input") private _input?: HaInput;
public focus(): void {
if (this._input) {
@@ -47,48 +43,29 @@ export class HaFormString extends LitElement implements HaFormElement {
protected render(): TemplateResult {
return html`
<ha-textfield
.type=${!this.isPassword
? this.stringType
: this.unmaskedPassword
? "text"
: "password"}
<ha-input
.passwordToggle=${this.isPassword}
.passwordVisible=${this.unmaskedPassword}
.type=${!this.isPassword ? this.stringType : "password"}
.label=${this.label}
.value=${this.data || ""}
.helper=${this.helper}
helperPersistent
.hint=${this.helper}
.disabled=${this.disabled}
.required=${this.schema.required}
.autoValidate=${this.schema.required}
.required=${!!this.schema.required}
.autoValidate=${!!this.schema.required}
.name=${this.schema.name}
.autofocus=${this.schema.autofocus}
.autofocus=${!!this.schema.autofocus}
.autocomplete=${this.schema.autocomplete}
.suffix=${this.isPassword
? // reserve some space for the icon.
html`<div style="width: 24px"></div>`
: this.schema.description?.suffix}
.validationMessage=${this.schema.required
? this.localize?.("ui.common.error_required")
: undefined}
@input=${this._valueChanged}
@change=${this._valueChanged}
></ha-textfield>
${this.renderIcon()}
`;
}
protected renderIcon() {
if (!this.isPassword) return nothing;
return html`
<ha-icon-button
.label=${this.localize?.(
`${this.localizeBaseKey}.${
this.unmaskedPassword ? "hide_password" : "show_password"
}` as LocalizeKeys
)}
@click=${this.toggleUnmaskedPassword}
.path=${this.unmaskedPassword ? mdiEyeOff : mdiEye}
></ha-icon-button>
>
${this.schema.description?.suffix
? html`<span slot="end">${this.schema.description.suffix}</span>`
: nothing}
</ha-input>
`;
}
@@ -98,12 +75,8 @@ export class HaFormString extends LitElement implements HaFormElement {
}
}
protected toggleUnmaskedPassword(): void {
this.unmaskedPassword = !this.unmaskedPassword;
}
protected _valueChanged(ev: Event): void {
let value: string | undefined = (ev.target as HaTextField).value;
let value: string | undefined = (ev.target as HaInput).value;
if (this.data === value) {
return;
}
@@ -115,10 +88,10 @@ export class HaFormString extends LitElement implements HaFormElement {
});
}
protected get stringType(): string {
protected get stringType(): "email" | "url" | "text" {
if (this.schema.format) {
if (["email", "url"].includes(this.schema.format)) {
return this.schema.format;
return this.schema.format as "email" | "url";
}
if (this.schema.format === "fqdnurl") {
return "url";
@@ -139,20 +112,6 @@ export class HaFormString extends LitElement implements HaFormElement {
:host([own-margin]) {
margin-bottom: 5px;
}
ha-textfield {
display: block;
}
ha-icon-button {
position: absolute;
top: 8px;
right: 8px;
inset-inline-start: initial;
inset-inline-end: 8px;
--ha-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
}
`;
}

473
src/components/ha-input.ts Normal file
View File

@@ -0,0 +1,473 @@
import { preventDefault } from "@fullcalendar/core/internal";
import "@home-assistant/webawesome/dist/components/animation/animation";
import "@home-assistant/webawesome/dist/components/input/input";
import type WaInput from "@home-assistant/webawesome/dist/components/input/input";
import { mdiClose, mdiEye, mdiEyeOff, mdiInformationOutline } from "@mdi/js";
import { LitElement, type PropertyValues, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import "./ha-icon-button";
import "./ha-svg-icon";
import "./ha-tooltip";
@customElement("ha-input")
export class HaInput extends LitElement {
/** The type of input. */
@property()
public type:
| "date"
| "datetime-local"
| "email"
| "number"
| "password"
| "search"
| "tel"
| "text"
| "time"
| "url" = "text";
/** The current value of the input. */
@property()
public value?: string;
/** The input's size. */
@property()
public size: "small" | "medium" | "large" = "medium";
/** The input's visual appearance. */
@property()
public appearance: "filled" | "outlined" | "filled-outlined" = "outlined";
/** Draws a pill-style input with rounded edges. */
@property({ type: Boolean })
public pill = false;
/** The input's label. */
@property()
public label = "";
/** The input's hint. */
@property()
public hint? = "";
/** Adds a clear button when the input is not empty. */
@property({ type: Boolean, attribute: "with-clear" })
public withClear = false;
/** Placeholder text to show as a hint when the input is empty. */
@property()
public placeholder = "";
/** Makes the input readonly. */
@property({ type: Boolean })
public readonly = false;
/** Adds a button to toggle the password's visibility. */
@property({ type: Boolean, attribute: "password-toggle" })
public passwordToggle = false;
/** Determines whether or not the password is currently visible. */
@property({ type: Boolean, attribute: "password-visible" })
public passwordVisible = false;
/** Hides the browser's built-in increment/decrement spin buttons for number inputs. */
@property({ type: Boolean, attribute: "without-spin-buttons" })
public withoutSpinButtons = false;
/** Makes the input a required field. */
@property({ type: Boolean })
public required = false;
/** A regular expression pattern to validate input against. */
@property()
public pattern?: string;
/** The minimum length of input that will be considered valid. */
@property({ type: Number })
public minlength?: number;
/** The maximum length of input that will be considered valid. */
@property({ type: Number })
public maxlength?: number;
/** The input's minimum value. Only applies to date and number input types. */
@property()
public min?: number | string;
/** The input's maximum value. Only applies to date and number input types. */
@property()
public max?: number | string;
/** Specifies the granularity that the value must adhere to. */
@property()
public step?: number | "any";
/** Controls whether and how text input is automatically capitalized. */
@property()
// eslint-disable-next-line lit/no-native-attributes
public autocapitalize:
| "off"
| "none"
| "on"
| "sentences"
| "words"
| "characters"
| "" = "";
/** Indicates whether the browser's autocorrect feature is on or off. */
@property({ type: Boolean })
public autocorrect = false;
/** Specifies what permission the browser has to provide assistance in filling out form field values. */
@property()
public autocomplete?: string;
/** Indicates that the input should receive focus on page load. */
@property({ type: Boolean })
// eslint-disable-next-line lit/no-native-attributes
public autofocus = false;
/** Used to customize the label or icon of the Enter key on virtual keyboards. */
@property()
// eslint-disable-next-line lit/no-native-attributes
public enterkeyhint:
| "enter"
| "done"
| "go"
| "next"
| "previous"
| "search"
| "send"
| "" = "";
/** Enables spell checking on the input. */
@property({ type: Boolean })
// eslint-disable-next-line lit/no-native-attributes
public spellcheck = true;
/** Tells the browser what type of data will be entered by the user. */
@property()
// eslint-disable-next-line lit/no-native-attributes
public inputmode:
| "none"
| "text"
| "decimal"
| "numeric"
| "tel"
| "search"
| "email"
| "url"
| "" = "";
/** The name of the input, submitted as a name/value pair with form data. */
@property()
public name?: string;
/** Disables the form control. */
@property({ type: Boolean })
public disabled = false;
/** Custom validation message to show when the input is invalid. */
@property({ attribute: "validation-message" })
public validationMessage? = "";
/** When true, validates the input on blur instead of on form submit. */
@property({ type: Boolean, attribute: "auto-validate" })
public autoValidate = false;
@property({ type: Boolean })
public invalid = false;
@state()
private _invalid = false;
@query("wa-input")
private _input?: WaInput;
static shadowRootOptions: ShadowRootInit = {
mode: "open",
delegatesFocus: true,
};
/** Selects all the text in the input. */
public select(): void {
this._input?.select();
}
/** Sets the start and end positions of the text selection (0-based). */
public setSelectionRange(
selectionStart: number,
selectionEnd: number,
selectionDirection?: "forward" | "backward" | "none"
): void {
this._input?.setSelectionRange(
selectionStart,
selectionEnd,
selectionDirection
);
}
/** Replaces a range of text with a new string. */
public setRangeText(
replacement: string,
start?: number,
end?: number,
selectMode?: "select" | "start" | "end" | "preserve"
): void {
this._input?.setRangeText(replacement, start, end, selectMode);
}
/** Displays the browser picker for an input element. */
public showPicker(): void {
this._input?.showPicker();
}
/** Increments the value of a numeric input type by the value of the step attribute. */
public stepUp(): void {
this._input?.stepUp();
}
/** Decrements the value of a numeric input type by the value of the step attribute. */
public stepDown(): void {
this._input?.stepDown();
}
public checkValidity(): boolean {
return this._input?.checkValidity() ?? true;
}
public reportValidity(): boolean {
const valid = this.checkValidity();
this._invalid = !valid;
return valid;
}
protected override updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
const nativeInput = this._input?.input;
if (!nativeInput) return;
// wa-input hardcodes aria-describedby="hint" pointing to its internal hint slot wrapper.
// We remove it and use aria-description instead to properly convey our hint or error text.
// TODO: fix upstream in wa-input
nativeInput.removeAttribute("aria-describedby");
// wa-input doesn't set aria-invalid on its internal <input>, so we do it manually
// TODO: fix upstream in wa-input
if (changedProperties.has("invalid") || changedProperties.has("_invalid")) {
const isInvalid = this.invalid || this._invalid;
nativeInput.setAttribute("aria-invalid", String(isInvalid));
}
// Expose hint or validation error to screen readers on the input itself
const description =
this.invalid || this._invalid
? this.validationMessage || this._input?.validationMessage
: this.hint;
if (description) {
nativeInput.setAttribute("aria-description", description);
} else {
nativeInput.removeAttribute("aria-description");
}
}
protected render() {
return html`
<wa-input
.type=${this.type}
.value=${this.value ?? null}
.size=${this.size}
.appearance=${this.appearance}
.withClear=${this.withClear}
.placeholder=${this.placeholder}
.readonly=${this.readonly}
.passwordToggle=${this.passwordToggle}
.passwordVisible=${this.passwordVisible}
.withoutSpinButtons=${this.withoutSpinButtons}
.required=${this.required}
.pattern=${this.pattern}
.minlength=${this.minlength}
.maxlength=${this.maxlength}
.min=${this.min}
.max=${this.max}
.step=${this.step}
.autocapitalize=${this.autocapitalize || undefined}
.autocorrect=${this.autocorrect ? "on" : "off"}
.autocomplete=${this.autocomplete}
.autofocus=${this.autofocus}
.enterkeyhint=${this.enterkeyhint || undefined}
.spellcheck=${this.spellcheck}
.inputmode=${this.inputmode || undefined}
.name=${this.name}
.disabled=${this.disabled}
class=${this.invalid || this._invalid ? "invalid" : ""}
@input=${this._handleInput}
@change=${this._handleChange}
@blur=${this._handleBlur}
@wa-invalid=${this._handleInvalid}
>
<div class="label" slot="label">
<span>
<slot name="label">${this.label}</slot>
</span>
${this.hint
? html`<ha-icon-button
@click=${preventDefault}
.path=${mdiInformationOutline}
.label=${"Hint"}
hide-title
id="hint"
></ha-icon-button>
<ha-tooltip for="hint">${this.hint}</ha-tooltip> `
: nothing}
</div>
<slot name="start" slot="start"></slot>
<slot name="end" slot="end"></slot>
<slot name="clear-icon" slot="clear-icon">
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</slot>
<slot name="show-password-icon" slot="show-password-icon">
<ha-svg-icon .path=${mdiEye}></ha-svg-icon>
</slot>
<slot name="hide-password-icon" slot="hide-password-icon">
<ha-svg-icon .path=${mdiEyeOff}></ha-svg-icon>
</slot>
<div
slot="hint"
class="error ${this.invalid || this._invalid ? "visible" : ""}"
role="alert"
aria-live="assertive"
>
<span
>${this.validationMessage || this._input?.validationMessage}</span
>
</div>
</wa-input>
`;
}
private _handleInput() {
this.value = this._input?.value ?? undefined;
if (this._invalid && this._input?.checkValidity()) {
this._invalid = false;
}
}
private _handleChange() {
this.value = this._input?.value ?? undefined;
}
private _handleBlur() {
if (this.autoValidate) {
this._invalid = !this._input?.checkValidity();
}
}
private _handleInvalid() {
this._invalid = true;
}
static styles = css`
:host {
display: flex;
align-items: flex-start;
padding-top: var(--ha-input-padding-top, var(--ha-space-2));
padding-bottom: var(--ha-input-padding-bottom, var(--ha-space-2));
text-align: var(--ha-input-text-align, start);
}
wa-input {
flex: 1;
min-width: 0;
}
wa-input::part(base):focus-within {
outline: none;
--wa-form-control-border-color: var(--ha-color-border-primary-normal);
}
wa-input.invalid,
wa-input.invalid::part(base):focus-within {
--wa-form-control-border-color: var(--ha-color-border-danger-normal);
}
wa-input::part(label) {
margin-block-end: 2px;
}
.label {
height: 24px;
display: flex;
width: 100%;
align-items: center;
color: var(--ha-color-text-secondary);
font-size: var(--ha-font-size-s);
font-weight: var(--ha-font-weight-medium);
gap: var(--ha-space-1);
}
.label span {
line-height: 1;
flex: 1;
min-width: 0;
overflow-x: clip;
overflow-y: visible;
text-overflow: ellipsis;
white-space: nowrap;
}
.label ha-svg-icon {
color: var(--ha-color-on-disabled-normal);
--mdc-icon-size: 16px;
}
#hint {
--ha-icon-button-size: 16px;
--mdc-icon-size: 16px;
color: var(--ha-color-on-disabled-normal);
}
wa-input::part(hint) {
margin-block-start: 0;
color: var(--ha-color-on-danger-quiet);
font-size: var(--ha-font-size-s);
margin-inline-start: var(--ha-space-3);
}
.error {
padding-top: var(--ha-space-1);
transition:
opacity 0.3s ease-out,
height 0.3s ease-out;
height: 0;
overflow: hidden;
}
.error span {
transition: transform 0.3s ease-out;
display: inline-block;
transform: translateY(
calc(-1 * (var(--ha-font-size-s) + var(--ha-space-1)))
);
}
.error.visible {
height: calc(var(--ha-font-size-s) + var(--ha-space-2));
}
.error.visible span {
transform: translateY(0);
}
wa-input::part(end) {
color: var(--ha-color-text-secondary);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-input": HaInput;
}
}

View File

@@ -1,34 +0,0 @@
import { styles } from "@material/web/textfield/internal/filled-styles";
import { FilledTextField } from "@material/web/textfield/internal/filled-text-field";
import { styles as sharedStyles } from "@material/web/textfield/internal/shared-styles";
import { css } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-md-textfield")
export class HaMdTextfield extends FilledTextField {
static override styles = [
sharedStyles,
styles,
css`
:host {
--ha-icon-display: block;
--md-sys-color-primary: var(--primary-text-color);
--md-sys-color-secondary: var(--secondary-text-color);
--md-sys-color-surface: var(--card-background-color);
--md-sys-color-on-surface-variant: var(--secondary-text-color);
--md-sys-color-surface-container-highest: var(--input-fill-color);
--md-sys-color-on-surface: var(--input-ink-color);
--md-sys-color-surface-container: var(--input-fill-color);
--md-sys-color-secondary-container: var(--input-fill-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-md-textfield": HaMdTextfield;
}
}

View File

@@ -1,211 +0,0 @@
import type { TextAreaCharCounter } from "@material/mwc-textfield/mwc-textfield-base";
import { mdiEye, mdiEyeOff } from "@mdi/js";
import { LitElement, css, html } from "lit";
import {
customElement,
eventOptions,
property,
query,
state,
} from "lit/decorators";
import type { HomeAssistant } from "../types";
import "./ha-icon-button";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@customElement("ha-password-field")
export class HaPasswordField extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public invalid?: boolean;
@property({ attribute: "error-message" }) public errorMessage?: string;
@property({ type: Boolean }) public icon = false;
// eslint-disable-next-line lit/attribute-names
@property({ type: Boolean }) public iconTrailing = false;
@property() public autocomplete?: string;
@property({ type: Boolean }) public autocorrect = true;
@property({ attribute: "input-spellcheck" })
public inputSpellcheck?: string;
@property({ type: String }) value = "";
@property({ type: String }) placeholder = "";
@property({ type: String }) label = "";
@property({ type: Boolean, reflect: true }) disabled = false;
@property({ type: Boolean }) required = false;
// eslint-disable-next-line lit/attribute-names
@property({ type: Number }) minLength = -1;
// eslint-disable-next-line lit/attribute-names
@property({ type: Number }) maxLength = -1;
@property({ type: Boolean, reflect: true }) outlined = false;
@property({ type: String }) helper = "";
// eslint-disable-next-line lit/attribute-names
@property({ type: Boolean }) validateOnInitialRender = false;
// eslint-disable-next-line lit/attribute-names
@property({ type: String }) validationMessage = "";
// eslint-disable-next-line lit/attribute-names
@property({ type: Boolean }) autoValidate = false;
@property({ type: String }) pattern = "";
@property({ type: Number }) size: number | null = null;
// eslint-disable-next-line lit/attribute-names
@property({ type: Boolean }) helperPersistent = false;
// eslint-disable-next-line lit/attribute-names
@property({ type: Boolean }) charCounter: boolean | TextAreaCharCounter =
false;
// eslint-disable-next-line lit/attribute-names
@property({ type: Boolean }) endAligned = false;
@property({ type: String }) prefix = "";
@property({ type: String }) suffix = "";
@property({ type: String }) name = "";
@property({ type: String, attribute: "input-mode" })
inputMode!: string;
// eslint-disable-next-line lit/attribute-names
@property({ type: Boolean }) readOnly = false;
// eslint-disable-next-line lit/no-native-attributes
@property({ attribute: false }) autocapitalize = "";
@state() private _unmaskedPassword = false;
@query("ha-textfield") private _textField!: HaTextField;
protected render() {
return html`<ha-textfield
.invalid=${this.invalid}
.errorMessage=${this.errorMessage}
.icon=${this.icon}
.iconTrailing=${this.iconTrailing}
.autocomplete=${this.autocomplete}
.autocorrect=${this.autocorrect}
.inputSpellcheck=${this.inputSpellcheck}
.value=${this.value}
.placeholder=${this.placeholder}
.label=${this.label}
.disabled=${this.disabled}
.required=${this.required}
.minLength=${this.minLength}
.maxLength=${this.maxLength}
.outlined=${this.outlined}
.helper=${this.helper}
.validateOnInitialRender=${this.validateOnInitialRender}
.validationMessage=${this.validationMessage}
.autoValidate=${this.autoValidate}
.pattern=${this.pattern}
.size=${this.size}
.helperPersistent=${this.helperPersistent}
.charCounter=${this.charCounter}
.endAligned=${this.endAligned}
.prefix=${this.prefix}
.name=${this.name}
.inputMode=${this.inputMode}
.readOnly=${this.readOnly}
.autocapitalize=${this.autocapitalize}
.type=${this._unmaskedPassword ? "text" : "password"}
.suffix=${html`<div style="width: 24px"></div>`}
@input=${this._handleInputEvent}
@change=${this._handleChangeEvent}
></ha-textfield>
<ha-icon-button
.label=${this.hass?.localize(
this._unmaskedPassword
? "ui.components.selectors.text.hide_password"
: "ui.components.selectors.text.show_password"
) || (this._unmaskedPassword ? "Hide password" : "Show password")}
@click=${this._toggleUnmaskedPassword}
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
></ha-icon-button>`;
}
public focus(): void {
this._textField.focus();
}
public checkValidity(): boolean {
return this._textField.checkValidity();
}
public reportValidity(): boolean {
return this._textField.reportValidity();
}
public setCustomValidity(message: string): void {
return this._textField.setCustomValidity(message);
}
public layout(): Promise<void> {
return this._textField.layout();
}
private _toggleUnmaskedPassword(): void {
this._unmaskedPassword = !this._unmaskedPassword;
}
@eventOptions({ passive: true })
private _handleInputEvent(ev) {
this.value = ev.target.value;
}
@eventOptions({ passive: true })
private _handleChangeEvent(ev) {
this.value = ev.target.value;
this._reDispatchEvent(ev);
}
private _reDispatchEvent(oldEvent: Event) {
const newEvent = new Event(oldEvent.type, oldEvent);
this.dispatchEvent(newEvent);
}
static styles = css`
:host {
display: block;
position: relative;
}
ha-textfield {
width: 100%;
}
ha-icon-button {
position: absolute;
top: 8px;
right: 8px;
inset-inline-start: initial;
inset-inline-end: 8px;
--ha-icon-button-size: 40px;
--mdc-icon-size: 20px;
color: var(--secondary-text-color);
direction: var(--direction);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-password-field": HaPasswordField;
}
}

View File

@@ -1,242 +1,351 @@
import { TextFieldBase } from "@material/mwc-textfield/mwc-textfield-base";
import { styles } from "@material/mwc-textfield/mwc-textfield.css";
import type { TemplateResult, PropertyValues } from "lit";
import { html, css } from "lit";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { mainWindow } from "../common/dom/get_main_window";
import "./ha-input";
import type { HaInput } from "./ha-input";
/**
* Legacy wrapper around ha-input that preserves the mwc-textfield API.
* New code should use ha-input directly.
* @deprecated Use ha-input instead.
*/
@customElement("ha-textfield")
export class HaTextField extends TextFieldBase {
@property({ type: Boolean }) public invalid?: boolean;
export class HaTextField extends LitElement {
@property({ type: String })
public value = "";
@property({ attribute: "error-message" }) public errorMessage?: string;
@property({ type: String })
public type:
| "text"
| "search"
| "tel"
| "url"
| "email"
| "password"
| "date"
| "month"
| "week"
| "time"
| "datetime-local"
| "number"
| "color" = "text";
@property({ type: String })
public label = "";
@property({ type: String })
public placeholder = "";
@property({ type: String })
public prefix = "";
@property({ type: String })
public suffix = "";
@property({ type: Boolean })
// @ts-ignore
@property({ type: Boolean }) public icon = false;
public icon = false;
@property({ type: Boolean })
// @ts-ignore
// eslint-disable-next-line lit/attribute-names
@property({ type: Boolean }) public iconTrailing = false;
public iconTrailing = false;
@property() public autocomplete?: string;
@property({ type: Boolean })
public disabled = false;
@property({ type: Boolean }) public autocorrect = true;
@property({ type: Boolean })
public required = false;
@property({ type: Number, attribute: "minlength" })
public minLength = -1;
@property({ type: Number, attribute: "maxlength" })
public maxLength = -1;
@property({ type: Boolean, reflect: true })
public outlined = false;
@property({ type: String })
public helper = "";
@property({ type: Boolean, attribute: "validateoninitialrender" })
public validateOnInitialRender = false;
@property({ type: String, attribute: "validationmessage" })
public validationMessage = "";
@property({ type: Boolean, attribute: "autovalidate" })
public autoValidate = false;
@property({ type: String })
public pattern = "";
@property()
public min: number | string = "";
@property()
public max: number | string = "";
@property()
public step: number | "any" | null = null;
@property({ type: Number })
public size: number | null = null;
@property({ type: Boolean, attribute: "helperpersistent" })
public helperPersistent = false;
@property({ attribute: "charcounter" })
public charCounter: boolean | "external" | "internal" = false;
@property({ type: Boolean, attribute: "endaligned" })
public endAligned = false;
@property({ type: String, attribute: "inputmode" })
public inputMode = "";
@property({ type: Boolean, reflect: true, attribute: "readonly" })
public readOnly = false;
@property({ type: String })
public name = "";
@property({ type: String })
// eslint-disable-next-line lit/no-native-attributes
public autocapitalize = "";
// --- ha-textfield-specific properties ---
@property({ type: Boolean })
public invalid = false;
@property({ attribute: "error-message" })
public errorMessage?: string;
@property()
public autocomplete?: string;
@property({ type: Boolean })
public autocorrect = true;
@property({ attribute: "input-spellcheck" })
public inputSpellcheck?: string;
@query("input") public formElement!: HTMLInputElement;
@query("ha-input")
private _haInput?: HaInput;
override updated(changedProperties: PropertyValues) {
static shadowRootOptions: ShadowRootInit = {
mode: "open",
delegatesFocus: true,
};
public get formElement(): HTMLInputElement | undefined {
return (this._haInput as any)?._input?.input;
}
public select(): void {
this._haInput?.select();
}
public setSelectionRange(
selectionStart: number,
selectionEnd: number,
selectionDirection?: "forward" | "backward" | "none"
): void {
this._haInput?.setSelectionRange(
selectionStart,
selectionEnd,
selectionDirection
);
}
public setRangeText(
replacement: string,
start?: number,
end?: number,
selectMode?: "select" | "start" | "end" | "preserve"
): void {
this._haInput?.setRangeText(replacement, start, end, selectMode);
}
public checkValidity(): boolean {
return this._haInput?.checkValidity() ?? true;
}
public reportValidity(): boolean {
return this._haInput?.reportValidity() ?? true;
}
public setCustomValidity(message: string): void {
this.validationMessage = message;
this.invalid = !!message;
}
/** No-op. Preserved for backward compatibility. */
public layout(): void {
// no-op — mwc-textfield needed this for notched outline recalculation
}
protected override firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
if (this.validateOnInitialRender) {
this.reportValidity();
}
}
protected override updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (
changedProperties.has("invalid") ||
changedProperties.has("errorMessage")
) {
this.setCustomValidity(
this.invalid
? this.errorMessage || this.validationMessage || "Invalid"
: ""
);
if (changedProperties.has("invalid") && this._haInput) {
if (
this.invalid ||
this.validateOnInitialRender ||
(changedProperties.has("invalid") &&
changedProperties.get("invalid") !== undefined)
(changedProperties.get("invalid") !== undefined && !this.invalid)
) {
// Only report validity if the field is invalid or the invalid state has changed from
// true to false to prevent setting empty required fields to invalid on first render
this.reportValidity();
}
}
if (changedProperties.has("autocomplete")) {
if (this.autocomplete) {
this.formElement.setAttribute("autocomplete", this.autocomplete);
} else {
this.formElement.removeAttribute("autocomplete");
}
}
if (changedProperties.has("autocorrect")) {
if (this.autocorrect === false) {
this.formElement.setAttribute("autocorrect", "off");
} else {
this.formElement.removeAttribute("autocorrect");
}
}
if (changedProperties.has("inputSpellcheck")) {
if (this.inputSpellcheck) {
this.formElement.setAttribute("spellcheck", this.inputSpellcheck);
} else {
this.formElement.removeAttribute("spellcheck");
}
}
private _mapType(
type: string
):
| "text"
| "search"
| "tel"
| "url"
| "email"
| "password"
| "date"
| "datetime-local"
| "number"
| "time" {
// mwc-textfield supports "color", "month", "week" which ha-input doesn't
switch (type) {
case "text":
case "search":
case "tel":
case "url":
case "email":
case "password":
case "date":
case "datetime-local":
case "number":
case "time":
return type;
default:
return "text";
}
}
protected override renderIcon(
_icon: string,
isTrailingIcon = false
): TemplateResult {
const type = isTrailingIcon ? "trailing" : "leading";
protected override render(): TemplateResult {
const errorMsg = this.errorMessage || this.validationMessage;
return html`
<span
class="mdc-text-field__icon mdc-text-field__icon--${type}"
tabindex=${isTrailingIcon ? 1 : -1}
<ha-input
.type=${this._mapType(this.type)}
.value=${this.value || undefined}
.label=${this.label}
.placeholder=${this.placeholder}
.disabled=${this.disabled}
.required=${this.required}
.readonly=${this.readOnly}
.pattern=${this.pattern || undefined}
.minlength=${this.minLength > 0 ? this.minLength : undefined}
.maxlength=${this.maxLength > 0 ? this.maxLength : undefined}
.min=${this.min !== "" ? this.min : undefined}
.max=${this.max !== "" ? this.max : undefined}
.step=${this.step ?? undefined}
.name=${this.name || undefined}
.autocomplete=${this.autocomplete}
.autocorrect=${this.autocorrect}
.spellcheck=${this.inputSpellcheck === "true"}
.inputmode=${this._mapInputMode(this.inputMode)}
.autocapitalize=${this.autocapitalize || ""}
.invalid=${this.invalid}
.validationMessage=${errorMsg || ""}
.autoValidate=${this.autoValidate}
.hint=${this.helper}
.withoutSpinButtons=${this.type === "number"}
@input=${this._onInput}
@change=${this._onChange}
>
<slot name="${type}Icon"></slot>
</span>
${this.icon
? html`<slot name="leadingIcon" slot="start"></slot>`
: nothing}
${this.prefix
? html`<span class="prefix" slot="start">${this.prefix}</span>`
: nothing}
${this.suffix
? html`<span class="suffix" slot="end">${this.suffix}</span>`
: nothing}
${this.iconTrailing
? html`<slot name="trailingIcon" slot="end"></slot>`
: nothing}
</ha-input>
`;
}
static override styles = [
styles,
css`
.mdc-text-field__input {
width: var(--ha-textfield-input-width, 100%);
}
.mdc-text-field:not(.mdc-text-field--with-leading-icon) {
padding: var(--text-field-padding, 0px 16px);
}
.mdc-text-field__affix--suffix {
padding-left: var(--text-field-suffix-padding-left, 12px);
padding-right: var(--text-field-suffix-padding-right, 0px);
padding-inline-start: var(--text-field-suffix-padding-left, 12px);
padding-inline-end: var(--text-field-suffix-padding-right, 0px);
direction: ltr;
}
.mdc-text-field--with-leading-icon {
padding-inline-start: var(--text-field-suffix-padding-left, 0px);
padding-inline-end: var(--text-field-suffix-padding-right, 16px);
direction: var(--direction);
}
private _mapInputMode(
mode: string
):
| "none"
| "text"
| "decimal"
| "numeric"
| "tel"
| "search"
| "email"
| "url"
| "" {
switch (mode) {
case "none":
case "text":
case "decimal":
case "numeric":
case "tel":
case "search":
case "email":
case "url":
return mode;
default:
return "";
}
}
.mdc-text-field--with-leading-icon.mdc-text-field--with-trailing-icon {
padding-left: var(--text-field-suffix-padding-left, 0px);
padding-right: var(--text-field-suffix-padding-right, 0px);
padding-inline-start: var(--text-field-suffix-padding-left, 0px);
padding-inline-end: var(--text-field-suffix-padding-right, 0px);
}
.mdc-text-field:not(.mdc-text-field--disabled)
.mdc-text-field__affix--suffix {
color: var(--secondary-text-color);
}
private _onInput(): void {
this.value = this._haInput?.value ?? "";
}
.mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon {
color: var(--secondary-text-color);
}
private _onChange(): void {
this.value = this._haInput?.value ?? "";
}
.mdc-text-field__icon--leading {
margin-inline-start: 16px;
margin-inline-end: 8px;
direction: var(--direction);
}
static override styles = css`
:host {
display: inline-flex;
flex-direction: column;
outline: none;
}
.mdc-text-field__icon--trailing {
padding: var(--textfield-icon-trailing-padding, 12px);
}
ha-input {
--ha-input-padding-bottom: 0;
width: 100%;
}
.mdc-floating-label:not(.mdc-floating-label--float-above) {
max-width: calc(100% - 16px);
}
.prefix,
.suffix {
color: var(--secondary-text-color);
}
.mdc-floating-label--float-above {
max-width: calc((100% - 16px) / 0.75);
transition: none;
}
.prefix {
margin-inline-end: var(--text-field-prefix-padding-right);
}
input {
text-align: var(--text-field-text-align, start);
}
input[type="color"] {
height: 20px;
}
/* Edge, hide reveal password icon */
::-ms-reveal {
display: none;
}
/* Chrome, Safari, Edge, Opera */
:host([no-spinner]) input::-webkit-outer-spin-button,
:host([no-spinner]) input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
/* Firefox */
:host([no-spinner]) input[type="number"] {
-moz-appearance: textfield;
}
.mdc-text-field__ripple {
overflow: hidden;
}
.mdc-text-field {
overflow: var(--text-field-overflow);
}
.mdc-floating-label {
padding-inline-end: 16px;
padding-inline-start: initial;
inset-inline-start: 16px !important;
inset-inline-end: initial !important;
transform-origin: var(--float-start);
direction: var(--direction);
text-align: var(--float-start);
box-sizing: border-box;
text-overflow: ellipsis;
}
.mdc-text-field--with-leading-icon.mdc-text-field--filled
.mdc-floating-label {
max-width: calc(
100% - 48px - var(--text-field-suffix-padding-left, 0px)
);
inset-inline-start: calc(
48px + var(--text-field-suffix-padding-left, 0px)
) !important;
inset-inline-end: initial !important;
direction: var(--direction);
}
.mdc-text-field__input[type="number"] {
direction: var(--direction);
}
.mdc-text-field__affix--prefix {
padding-right: var(--text-field-prefix-padding-right, 2px);
padding-inline-end: var(--text-field-prefix-padding-right, 2px);
padding-inline-start: initial;
}
.mdc-text-field:not(.mdc-text-field--disabled)
.mdc-text-field__affix--prefix {
color: var(--mdc-text-field-label-ink-color);
}
#helper-text ha-markdown {
display: inline-block;
}
`,
// safari workaround - must be explicit
mainWindow.document.dir === "rtl"
? css`
.mdc-text-field--with-leading-icon,
.mdc-text-field__icon--leading,
.mdc-floating-label,
.mdc-text-field--with-leading-icon.mdc-text-field--filled
.mdc-floating-label,
.mdc-text-field__input[type="number"] {
direction: rtl;
--direction: rtl;
}
`
: css``,
];
/* Edge, hide reveal password icon */
::-ms-reveal {
display: none;
}
`;
}
declare global {

View File

@@ -4,11 +4,9 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { navigate } from "../../../common/navigate";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-password-field";
import type { HaPasswordField } from "../../../components/ha-password-field";
import "../../../components/ha-input";
import type { HaInput } from "../../../components/ha-input";
import "../../../components/ha-svg-icon";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import { cloudLogin } from "../../../data/cloud";
import { showCloudAlreadyConnectedDialog } from "../../../panels/config/cloud/dialog-cloud-already-connected/show-dialog-cloud-already-connected";
import type { HomeAssistant } from "../../../types";
@@ -28,9 +26,9 @@ export class CloudStepSignin extends LitElement {
@state() private _checkConnection = true;
@query("#email", true) private _emailField!: HaTextField;
@query("#email", true) private _emailField!: HaInput;
@query("#password", true) private _passwordField!: HaPasswordField;
@query("#password", true) private _passwordField!: HaInput;
render() {
return html`<div class="content">
@@ -42,7 +40,7 @@ export class CloudStepSignin extends LitElement {
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<ha-textfield
<ha-input
autofocus
id="email"
name="email"
@@ -54,12 +52,14 @@ export class CloudStepSignin extends LitElement {
autocomplete="email"
required
@keydown=${this._keyDown}
validationMessage=${this.hass.localize(
.validationMessage=${this.hass.localize(
"ui.panel.config.cloud.register.email_error_msg"
)}
></ha-textfield>
<ha-password-field
></ha-input>
<ha-input
id="password"
type="password"
password-toggle
name="password"
.label=${this.hass.localize(
"ui.panel.config.cloud.register.password"
@@ -69,10 +69,10 @@ export class CloudStepSignin extends LitElement {
minlength="8"
required
@keydown=${this._keyDown}
validationMessage=${this.hass.localize(
.validationMessage=${this.hass.localize(
"ui.panel.config.cloud.register.password_error_msg"
)}
></ha-password-field>
></ha-input>
</div>
<div class="footer">
<ha-button
@@ -95,8 +95,8 @@ export class CloudStepSignin extends LitElement {
const emailField = this._emailField;
const passwordField = this._passwordField;
const email = emailField.value;
const password = passwordField.value;
const email = emailField.value as string;
const password = passwordField.value as string;
if (!emailField.reportValidity()) {
passwordField.reportValidity();
@@ -216,8 +216,7 @@ export class CloudStepSignin extends LitElement {
:host {
display: block;
}
ha-textfield,
ha-password-field {
ha-textfield {
display: block;
}
`,

View File

@@ -3,11 +3,9 @@ import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-password-field";
import type { HaPasswordField } from "../../../components/ha-password-field";
import "../../../components/ha-input";
import type { HaInput } from "../../../components/ha-input";
import "../../../components/ha-svg-icon";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import {
cloudLogin,
cloudRegister,
@@ -30,9 +28,9 @@ export class CloudStepSignup extends LitElement {
@state() private _state?: "VERIFY";
@query("#email", true) private _emailField!: HaTextField;
@query("#email", true) private _emailField!: HaInput;
@query("#password", true) private _passwordField!: HaPasswordField;
@query("#password", true) private _passwordField!: HaInput;
render() {
return html`<div class="content">
@@ -53,7 +51,7 @@ export class CloudStepSignup extends LitElement {
{ email: this._email }
)}
</p>`
: html`<ha-textfield
: html`<ha-input
autofocus
id="email"
name="email"
@@ -65,12 +63,14 @@ export class CloudStepSignup extends LitElement {
autocomplete="email"
required
@keydown=${this._keyDown}
validationMessage=${this.hass.localize(
.validationMessage=${this.hass.localize(
"ui.panel.config.cloud.register.email_error_msg"
)}
></ha-textfield>
<ha-password-field
></ha-input>
<ha-input
id="password"
type="password"
password-toggle
name="password"
.label=${this.hass.localize(
"ui.panel.config.cloud.register.password"
@@ -80,10 +80,10 @@ export class CloudStepSignup extends LitElement {
minlength="8"
required
@keydown=${this._keyDown}
validationMessage=${this.hass.localize(
.validationMessage=${this.hass.localize(
"ui.panel.config.cloud.register.password_error_msg"
)}
></ha-password-field>`}
></ha-input>`}
</div>
<div class="footer side-by-side">
${this._state === "VERIFY"
@@ -131,19 +131,26 @@ export class CloudStepSignup extends LitElement {
const emailField = this._emailField;
const passwordField = this._passwordField;
let invalid = false;
if (!emailField.reportValidity()) {
passwordField.reportValidity();
invalid = true;
emailField.focus();
return;
}
if (!passwordField.reportValidity()) {
passwordField.focus();
if (!invalid) {
passwordField.focus();
}
invalid = true;
}
if (invalid) {
return;
}
const email = emailField.value.toLowerCase();
const password = passwordField.value;
const email = emailField.value!.toLowerCase();
const password = passwordField.value!;
this._requestInProgress = true;
@@ -211,10 +218,6 @@ export class CloudStepSignup extends LitElement {
.content {
width: 100%;
}
ha-textfield,
ha-password-field {
display: block;
}
`,
];
}

View File

@@ -1,25 +1,25 @@
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
import { customElement, property, state, query } from "lit/decorators";
import "../../components/ha-button";
import { customElement, property, query, state } from "lit/decorators";
import { formatDateTimeWithBrowserDefaults } from "../../common/datetime/format_date_time";
import { fireEvent } from "../../common/dom/fire_event";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../../components/buttons/ha-progress-button";
import type { HaProgressButton } from "../../components/buttons/ha-progress-button";
import "../../components/ha-alert";
import "../../components/ha-button";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-input";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import "../../components/buttons/ha-progress-button";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-password-field";
import "../../panels/config/backup/components/ha-backup-data-picker";
import "../../panels/config/backup/components/ha-backup-formfield-label";
import type { LocalizeFunc } from "../../common/translations/localize";
import {
getPreferredAgentForDownload,
type BackupContentExtended,
type BackupData,
} from "../../data/backup";
import { restoreOnboardingBackup } from "../../data/backup_onboarding";
import type { HaProgressButton } from "../../components/buttons/ha-progress-button";
import { fireEvent } from "../../common/dom/fire_event";
import "../../panels/config/backup/components/ha-backup-data-picker";
import "../../panels/config/backup/components/ha-backup-formfield-label";
import { onBoardingStyles } from "../styles";
import { formatDateTimeWithBrowserDefaults } from "../../common/datetime/format_date_time";
@customElement("onboarding-restore-backup-restore")
class OnboardingRestoreBackupRestore extends LitElement {
@@ -170,7 +170,7 @@ class OnboardingRestoreBackupRestore extends LitElement {
`ui.panel.page-onboarding.restore.details.restore.encryption.description${this.mode === "cloud" ? "_cloud" : ""}`
)}
</span>
<ha-password-field
<ha-input
.disabled=${this._loading}
@input=${this._encryptionKeyChanged}
.label=${this.localize(
@@ -178,13 +178,11 @@ class OnboardingRestoreBackupRestore extends LitElement {
)}
.value=${this._encryptionKey}
@keydown=${this._keyDown}
.errorMessage=${this._encryptionKeyWrong
? this.localize(
"ui.panel.page-onboarding.restore.details.restore.encryption.incorrect_key"
)
: ""}
.validationMessage=${this.localize(
"ui.panel.page-onboarding.restore.details.restore.encryption.incorrect_key"
)}
.invalid=${this._encryptionKeyWrong}
></ha-password-field>
></ha-input>
</div>`
: nothing}
@@ -353,7 +351,7 @@ class OnboardingRestoreBackupRestore extends LitElement {
.encryption {
margin-bottom: 32px;
}
.encryption ha-password-field {
.encryption ha-input {
margin-top: 24px;
}
.actions {

View File

@@ -5,15 +5,14 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-dialog";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-fade-in";
import "../../../components/ha-generic-picker";
import "../../../components/ha-input";
import "../../../components/ha-markdown";
import "../../../components/ha-password-field";
import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box";
import "../../../components/ha-spinner";
import "../../../components/ha-textfield";
import "../../../components/ha-dialog";
import type {
ApplicationCredential,
ApplicationCredentialsConfig,
@@ -69,6 +68,7 @@ export class DialogAddApplicationCredential extends LitElement {
this._params = params;
this._domain = params.selectedDomain;
this._manifest = params.manifest;
this._invalid = false;
this._name = "";
this._description = "";
this._clientId = "";
@@ -195,7 +195,7 @@ export class DialogAddApplicationCredential extends LitElement {
.content=${this._description}
></ha-markdown>`
: nothing}
<ha-textfield
<ha-input
class="name"
name="name"
.label=${this.hass.localize(
@@ -205,12 +205,12 @@ export class DialogAddApplicationCredential extends LitElement {
.invalid=${this._invalid && !this._name}
required
@input=${this._handleValueChanged}
.errorMessage=${this.hass.localize(
.validationMessage=${this.hass.localize(
"ui.common.error_required"
)}
dialogInitialFocus
></ha-textfield>
<ha-textfield
></ha-input>
<ha-input
class="clientId"
name="clientId"
.label=${this.hass.localize(
@@ -220,16 +220,17 @@ export class DialogAddApplicationCredential extends LitElement {
.invalid=${this._invalid && !this._clientId}
required
@input=${this._handleValueChanged}
.errorMessage=${this.hass.localize(
.validationMessage=${this.hass.localize(
"ui.common.error_required"
)}
dialogInitialFocus
.helper=${this.hass.localize(
.hint=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_id_helper"
)}
helperPersistent
></ha-textfield>
<ha-password-field
></ha-input>
<ha-input
type="password"
password-toggle
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_secret"
)}
@@ -238,14 +239,13 @@ export class DialogAddApplicationCredential extends LitElement {
.invalid=${this._invalid && !this._clientSecret}
required
@input=${this._handleValueChanged}
.errorMessage=${this.hass.localize(
.validationMessage=${this.hass.localize(
"ui.common.error_required"
)}
.helper=${this.hass.localize(
.hint=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_secret_helper"
)}
helperPersistent
></ha-password-field>
></ha-input>
</div>
<ha-dialog-footer slot="footer">
@@ -377,11 +377,6 @@ export class DialogAddApplicationCredential extends LitElement {
display: flex;
padding: var(--ha-space-2) 0;
}
ha-textfield {
display: block;
margin-top: var(--ha-space-4);
margin-bottom: var(--ha-space-4);
}
a {
text-decoration: none;
}

View File

@@ -27,15 +27,19 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
import { transform } from "../../../common/decorators/transform";
import { fireEvent } from "../../../common/dom/fire_event";
import { goBack, navigate } from "../../../common/navigate";
import { promiseTimeout } from "../../../common/util/promise-timeout";
import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-fade-in";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-spinner";
import "../../../components/ha-svg-icon";
import "../../../components/ha-yaml-editor";
import type {
@@ -68,22 +72,27 @@ import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import "../../../layouts/hass-subpage";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
import { haStyle } from "../../../resources/styles";
import type { Entries, ValueChangedEvent } from "../../../types";
import type {
Entries,
HomeAssistant,
Route,
ValueChangedEvent,
} from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode";
import { showAutomationSaveDialog } from "./automation-save-dialog/show-dialog-automation-save";
import {
type EntityRegistryUpdate,
showAutomationSaveDialog,
} from "./automation-save-dialog/show-dialog-automation-save";
import { showAutomationSaveTimeoutDialog } from "./automation-save-timeout-dialog/show-dialog-automation-save-timeout";
import "./blueprint-automation-editor";
import {
AutomationScriptEditorMixin,
automationScriptEditorStyles,
} from "./ha-automation-script-editor-mixin";
import "./manual-automation-editor";
import type { HaManualAutomationEditor } from "./manual-automation-editor";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@@ -110,13 +119,53 @@ declare global {
}
@customElement("ha-automation-editor")
export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationConfig>(
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
export class HaAutomationEditor extends PreventUnsavedMixin(
KeyboardShortcutMixin(LitElement)
) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public automationId: string | null = null;
@property({ attribute: false }) public entityId: string | null = null;
@property({ attribute: false }) public automations!: AutomationEntity[];
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@state() private _config?: AutomationConfig;
@state() private _dirty = false;
@state() private _errors?: string;
@state() private _yamlErrors?: string;
@state() private _entityId?: string;
@state() private _mode: "gui" | "yaml" = "gui";
@state() private _readOnly = false;
@state() private _validationErrors?: (string | TemplateResult)[];
@state() private _blueprintConfig?: BlueprintAutomationConfig;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
transformer: function (this: HaAutomationEditor, value) {
return value.find(({ entity_id }) => entity_id === this._entityId);
},
watch: ["_entityId"],
})
private _registryEntry?: EntityRegistryEntry;
@state() private _saving = false;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityRegistry!: EntityRegistryEntry[];
@@ -131,18 +180,24 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
private _configSubscriptionsId = 1;
private _entityRegistryUpdate?: EntityRegistryUpdate;
private _newAutomationId?: string;
private _entityRegCreated?: (
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
) => void;
private _undoRedoController = new UndoRedoController<AutomationConfig>(this, {
apply: (config) => this._applyUndoRedo(config),
currentConfig: () => this.config!,
currentConfig: () => this._config!,
});
protected willUpdate(changedProps) {
super.willUpdate(changedProps);
if (
this.entityRegCreated &&
this._entityRegCreated &&
this._newAutomationId &&
changedProps.has("_entityRegistry")
) {
@@ -152,22 +207,26 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
entity.unique_id === this._newAutomationId
);
if (automation) {
this.entityRegCreated(automation);
this.entityRegCreated = undefined;
this._entityRegCreated(automation);
this._entityRegCreated = undefined;
}
}
}
protected render(): TemplateResult | typeof nothing {
if (!this.config) {
return this.renderLoading();
if (!this._config) {
return html`
<ha-fade-in .delay=${500}>
<ha-spinner size="large"></ha-spinner>
</ha-fade-in>
`;
}
const stateObj = this.currentEntityId
? this.hass.states[this.currentEntityId]
const stateObj = this._entityId
? this.hass.states[this._entityId]
: undefined;
const useBlueprint = "use_blueprint" in this.config;
const useBlueprint = "use_blueprint" in this._config;
const shortcutIcon = isMac
? html`<ha-svg-icon .path=${mdiAppleKeyboardCommand}></ha-svg-icon>`
: this.hass.localize("ui.panel.config.automation.editor.ctrl");
@@ -177,11 +236,11 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.backCallback=${this.backTapped}
.header=${this.config.alias ||
.backCallback=${this._backTapped}
.header=${this._config.alias ||
this.hass.localize("ui.panel.config.automation.editor.default_name")}
>
${this.mode === "gui" && !this.narrow
${this._mode === "gui" && !this.narrow
? html`<ha-icon-button
slot="toolbar-icon"
.label=${this.hass.localize("ui.common.undo")}
@@ -225,7 +284,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
</span>
</ha-tooltip>`
: nothing}
${this.config?.id && !this.narrow
${this._config?.id && !this.narrow
? html`
<ha-button
appearance="plain"
@@ -249,7 +308,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
.path=${mdiDotsVertical}
></ha-icon-button>
${this.mode === "gui" && this.narrow
${this._mode === "gui" && this.narrow
? html`<ha-dropdown-item
value="undo"
.disabled=${!this._undoRedoController.canUndo}
@@ -283,7 +342,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
<ha-dropdown-item .disabled=${!stateObj} value="category">
${this.hass.localize(
`ui.panel.config.scene.picker.${this.registryEntry?.categories?.automation ? "edit_category" : "assign_category"}`
`ui.panel.config.scene.picker.${this._registryEntry?.categories?.automation ? "edit_category" : "assign_category"}`
)}
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
</ha-dropdown-item>
@@ -307,9 +366,9 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
<ha-dropdown-item
value="rename"
.disabled=${this.readOnly ||
.disabled=${this._readOnly ||
!this.automationId ||
this.mode === "yaml"}
this._mode === "yaml"}
>
${this.hass.localize("ui.panel.config.automation.editor.rename")}
<ha-svg-icon slot="icon" .path=${mdiRenameBox}></ha-svg-icon>
@@ -318,7 +377,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
? html`
<ha-dropdown-item
@click=${this._promptAutomationMode}
.disabled=${this.readOnly || this.mode === "yaml"}
.disabled=${this._readOnly || this._mode === "yaml"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.change_mode"
@@ -332,12 +391,12 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
: nothing}
<ha-dropdown-item
.disabled=${!!this.blueprintConfig ||
(!this.readOnly && !this.automationId)}
.disabled=${!!this._blueprintConfig ||
(!this._readOnly && !this.automationId)}
value="duplicate"
>
${this.hass.localize(
this.readOnly
this._readOnly
? "ui.panel.config.automation.editor.migrate"
: "ui.panel.config.automation.editor.duplicate"
)}
@@ -351,7 +410,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
? html`
<ha-dropdown-item
value="take_control"
.disabled=${this.readOnly}
.disabled=${this._readOnly}
>
${this.hass.localize(
"ui.panel.config.automation.editor.take_control"
@@ -363,7 +422,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
<ha-dropdown-item value="toggle_yaml_mode">
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${this.mode === "gui" ? "yaml" : "ui"}`
`ui.panel.config.automation.editor.edit_${this._mode === "gui" ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-dropdown-item>
@@ -397,10 +456,10 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
</ha-dropdown-item>
</ha-dropdown>
<div
class=${this.mode === "yaml" ? "yaml-mode" : ""}
class=${this._mode === "yaml" ? "yaml-mode" : ""}
@subscribe-automation-config=${this._subscribeAutomationConfig}
>
${this.mode === "gui"
${this._mode === "gui"
? html`
<div>
${useBlueprint
@@ -410,10 +469,10 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
.narrow=${this.narrow}
.isWide=${this.isWide}
.stateObj=${stateObj}
.config=${this.config}
.disabled=${this.readOnly}
.saving=${this.saving}
.dirty=${this.dirty}
.config=${this._config}
.disabled=${this._readOnly}
.saving=${this._saving}
.dirty=${this._dirty}
@value-changed=${this._valueChanged}
@save-automation=${this._handleSaveAutomation}
></blueprint-automation-editor>
@@ -424,16 +483,16 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
.narrow=${this.narrow}
.isWide=${this.isWide}
.stateObj=${stateObj}
.config=${this.config}
.disabled=${this.readOnly}
.dirty=${this.dirty}
.saving=${this.saving}
.config=${this._config}
.disabled=${this._readOnly}
.dirty=${this._dirty}
.saving=${this._saving}
@value-changed=${this._valueChanged}
@save-automation=${this._handleSaveAutomation}
@editor-save=${this._handleSaveAutomation}
>
<div class="alert-wrapper" slot="alerts">
${this.errors || stateObj?.state === UNAVAILABLE
${this._errors || stateObj?.state === UNAVAILABLE
? html`<ha-alert
alert-type="error"
.title=${stateObj?.state === UNAVAILABLE
@@ -442,7 +501,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
)
: undefined}
>
${this.errors || this.validationErrors}
${this._errors || this._validationErrors}
${stateObj?.state === UNAVAILABLE
? html`<ha-svg-icon
slot="icon"
@@ -451,7 +510,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
: nothing}
</ha-alert>`
: nothing}
${this.blueprintConfig
${this._blueprintConfig
? html`<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.automation.editor.confirm_take_control"
@@ -459,21 +518,21 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
<div slot="action" style="display: flex;">
<ha-button
appearance="plain"
@click=${this.takeControlSave}
@click=${this._takeControlSave}
>${this.hass.localize(
"ui.common.yes"
)}</ha-button
>
<ha-button
appearance="plain"
@click=${this.revertBlueprint}
@click=${this._revertBlueprint}
>${this.hass.localize(
"ui.common.no"
)}</ha-button
>
</div>
</ha-alert>`
: this.readOnly
: this._readOnly
? html`<ha-alert
alert-type="warning"
dismissable
@@ -516,7 +575,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
`}
</div>
`
: this.mode === "yaml"
: this._mode === "yaml"
? html`${stateObj?.state === "off"
? html`
<ha-alert alert-type="info">
@@ -539,7 +598,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
.readOnly=${this.readOnly}
.readOnly=${this._readOnly}
@value-changed=${this._yamlChanged}
@editor-save=${this._handleSaveAutomation}
.showErrors=${false}
@@ -547,9 +606,9 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
></ha-yaml-editor>
<ha-fab
slot="fab"
class=${this.dirty ? "dirty" : ""}
class=${this._dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.common.save")}
.disabled=${this.saving}
.disabled=${this._saving}
extended
@click=${this._handleSaveAutomation}
>
@@ -586,7 +645,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
this.hass
) {
const initData = getAutomationEditorInitData();
this.dirty = !!initData;
this._dirty = !!initData;
let baseConfig: Partial<AutomationConfig> = { description: "" };
if (!initData || !("use_blueprint" in initData)) {
baseConfig = {
@@ -597,35 +656,35 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
actions: [],
};
}
this.config = {
this._config = {
...baseConfig,
...(initData ? normalizeAutomationConfig(initData) : initData),
} as AutomationConfig;
this.currentEntityId = undefined;
this.readOnly = false;
this._entityId = undefined;
this._readOnly = false;
}
if (changedProps.has("entityId") && this.entityId) {
getAutomationStateConfig(this.hass, this.entityId).then((c) => {
this.config = normalizeAutomationConfig(c.config);
this._config = normalizeAutomationConfig(c.config);
this._checkValidation();
});
this.currentEntityId = this.entityId;
this.dirty = false;
this.readOnly = true;
this._entityId = this.entityId;
this._dirty = false;
this._readOnly = true;
}
if (
changedProps.has("automations") &&
this.automationId &&
!this.currentEntityId
!this._entityId
) {
this._setEntityId();
}
if (changedProps.has("config")) {
if (changedProps.has("_config")) {
Object.values(this._configSubscriptions).forEach((sub) =>
sub(this.config)
sub(this._config)
);
}
}
@@ -634,24 +693,24 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
const automation = this.automations.find(
(entity: AutomationEntity) => entity.attributes.id === this.automationId
);
this.currentEntityId = automation?.entity_id;
this._entityId = automation?.entity_id;
}
private async _checkValidation() {
this.validationErrors = undefined;
if (!this.currentEntityId || !this.config) {
this._validationErrors = undefined;
if (!this._entityId || !this._config) {
return;
}
const stateObj = this.hass.states[this.currentEntityId];
const stateObj = this.hass.states[this._entityId];
if (stateObj?.state !== UNAVAILABLE) {
return;
}
const validation = await validateConfig(this.hass, {
triggers: this.config.triggers,
conditions: this.config.conditions,
actions: this.config.actions,
triggers: this._config.triggers,
conditions: this._config.conditions,
actions: this._config.actions,
});
this.validationErrors = (
this._validationErrors = (
Object.entries(validation) as Entries<typeof validation>
).map(([key, value]) =>
value.valid
@@ -669,9 +728,9 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
this.hass,
this.automationId as string
);
this.dirty = false;
this.readOnly = false;
this.config = normalizeAutomationConfig(config);
this._dirty = false;
this._readOnly = false;
this._config = normalizeAutomationConfig(config);
this._checkValidation();
} catch (err: any) {
const entity = this._entityRegistry.find(
@@ -702,27 +761,34 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
private _valueChanged(ev: ValueChangedEvent<AutomationConfig>) {
ev.stopPropagation();
if (this.config) {
this._undoRedoController.commit(this.config);
if (this._config) {
this._undoRedoController.commit(this._config);
}
this.config = ev.detail.value;
if (this.readOnly) {
this._config = ev.detail.value;
if (this._readOnly) {
return;
}
this.dirty = true;
this.errors = undefined;
this._dirty = true;
this._errors = undefined;
}
private _showInfo() {
if (!this.hass || !this.currentEntityId) {
if (!this.hass || !this._entityId) {
return;
}
fireEvent(this, "hass-more-info", { entityId: this.currentEntityId });
fireEvent(this, "hass-more-info", { entityId: this._entityId });
}
private _showSettings() {
showMoreInfoDialog(this, {
entityId: this._entityId!,
view: "settings",
});
}
private _editCategory() {
if (!this.registryEntry) {
if (!this._registryEntry) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.scene.picker.no_category_support"
@@ -735,36 +801,36 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
}
showAssignCategoryDialog(this, {
scope: "automation",
entityReg: this.registryEntry,
entityReg: this._registryEntry,
});
}
private async _showTrace() {
if (this.config?.id) {
const result = await this.confirmUnsavedChanged();
if (this._config?.id) {
const result = await this._confirmUnsavedChanged();
if (result) {
navigate(
`/config/automation/trace/${encodeURIComponent(this.config.id)}`
`/config/automation/trace/${encodeURIComponent(this._config.id)}`
);
}
}
}
private _runActions() {
if (!this.hass || !this.currentEntityId) {
if (!this.hass || !this._entityId) {
return;
}
triggerAutomationActions(
this.hass,
this.hass.states[this.currentEntityId].entity_id
this.hass.states[this._entityId].entity_id
);
}
private async _toggle(): Promise<void> {
if (!this.hass || !this.currentEntityId) {
if (!this.hass || !this._entityId) {
return;
}
const stateObj = this.hass.states[this.currentEntityId];
const stateObj = this.hass.states[this._entityId];
const service = stateObj.state === "off" ? "turn_on" : "turn_off";
await this.hass.callService("automation", service, {
entity_id: stateObj.entity_id,
@@ -772,42 +838,42 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
}
private _preprocessYaml() {
if (!this.config) {
if (!this._config) {
return {};
}
const cleanConfig: AutomationConfig = { ...this.config };
const cleanConfig: AutomationConfig = { ...this._config };
delete cleanConfig.id;
return cleanConfig;
}
private _yamlChanged(ev: CustomEvent) {
ev.stopPropagation();
this.dirty = true;
this._dirty = true;
if (!ev.detail.isValid) {
this.yamlErrors = ev.detail.errorMsg;
this._yamlErrors = ev.detail.errorMsg;
return;
}
this.yamlErrors = undefined;
this.config = {
id: this.config?.id,
this._yamlErrors = undefined;
this._config = {
id: this._config?.id,
...normalizeAutomationConfig(ev.detail.value),
};
this.errors = undefined;
this._errors = undefined;
}
protected async confirmUnsavedChanged(): Promise<boolean> {
if (!this.dirty) {
private async _confirmUnsavedChanged(): Promise<boolean> {
if (!this._dirty) {
return true;
}
return new Promise<boolean>((resolve) => {
showAutomationSaveDialog(this, {
config: this.config!,
config: this._config!,
domain: "automation",
updateConfig: async (config, entityRegistryUpdate) => {
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this.requestUpdate();
const id = this.automationId || String(Date.now());
@@ -823,8 +889,8 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
},
onClose: () => resolve(false),
onDiscard: () => resolve(true),
entityRegistryUpdate: this.entityRegistryUpdate,
entityRegistryEntry: this.registryEntry,
entityRegistryUpdate: this._entityRegistryUpdate,
entityRegistryEntry: this._registryEntry,
title: this.hass.localize(
this.automationId
? "ui.panel.config.automation.editor.leave.unsaved_confirm_title"
@@ -840,8 +906,15 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
});
}
private _backTapped = async () => {
const result = await this._confirmUnsavedChanged();
if (result) {
afterNextRender(() => goBack("/config"));
}
};
private async _takeControl() {
const config = this.config as BlueprintAutomationConfig;
const config = this._config as BlueprintAutomationConfig;
try {
const result = await substituteBlueprint(
@@ -858,20 +931,35 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
description: config.description,
};
this.blueprintConfig = config;
this.config = newConfig;
if (this.mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
this._blueprintConfig = config;
this._config = newConfig;
if (this._mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
}
this.readOnly = true;
this.errors = undefined;
this._readOnly = true;
this._errors = undefined;
} catch (err: any) {
this.errors = err.message;
this._errors = err.message;
}
}
private _revertBlueprint() {
this._config = this._blueprintConfig;
if (this._mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
}
this._blueprintConfig = undefined;
this._readOnly = false;
}
private _takeControlSave() {
this._readOnly = false;
this._dirty = true;
this._blueprintConfig = undefined;
}
private async _duplicate() {
const result = this.readOnly
const result = this._readOnly
? await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.picker.migrate_automation"
@@ -880,12 +968,12 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
"ui.panel.config.automation.picker.migrate_automation_description"
),
})
: await this.confirmUnsavedChanged();
: await this._confirmUnsavedChanged();
if (result) {
showAutomationEditor({
...this.config,
...this._config,
id: undefined,
alias: this.readOnly ? this.config?.alias : undefined,
alias: this._readOnly ? this._config?.alias : undefined,
});
}
}
@@ -897,7 +985,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
),
text: this.hass.localize(
"ui.panel.config.automation.picker.delete_confirm_text",
{ name: this.config?.alias }
{ name: this._config?.alias }
),
confirmText: this.hass!.localize("ui.common.delete"),
destructive: true,
@@ -913,21 +1001,43 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
}
}
private async _switchUiMode() {
if (this._yamlErrors) {
const result = await showConfirmationDialog(this, {
text: html`${this.hass.localize(
"ui.panel.config.automation.editor.switch_ui_yaml_error"
)}<br /><br />${this._yamlErrors}`,
confirmText: this.hass!.localize("ui.common.continue"),
destructive: true,
dismissText: this.hass!.localize("ui.common.cancel"),
});
if (!result) {
return;
}
}
this._yamlErrors = undefined;
this._mode = "gui";
}
private _switchYamlMode() {
this._mode = "yaml";
}
private async _promptAutomationAlias(): Promise<boolean> {
return new Promise((resolve) => {
showAutomationSaveDialog(this, {
config: this.config!,
config: this._config!,
domain: "automation",
updateConfig: async (config, entityRegistryUpdate) => {
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this.requestUpdate();
resolve(true);
},
onClose: () => resolve(false),
entityRegistryUpdate: this.entityRegistryUpdate,
entityRegistryEntry: this.registryEntry,
entityRegistryUpdate: this._entityRegistryUpdate,
entityRegistryEntry: this._registryEntry,
});
});
}
@@ -935,10 +1045,10 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
private async _promptAutomationMode(): Promise<void> {
return new Promise((resolve) => {
showAutomationModeDialog(this, {
config: this.config!,
config: this._config!,
updateConfig: (config) => {
this.config = config;
this.dirty = true;
this._config = config;
this._dirty = true;
this.requestUpdate();
resolve();
},
@@ -948,9 +1058,9 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
}
private async _handleSaveAutomation(): Promise<void> {
if (this.yamlErrors) {
if (this._yamlErrors) {
showToast(this, {
message: this.yamlErrors,
message: this._yamlErrors,
});
return;
}
@@ -972,22 +1082,22 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
}
private async _saveAutomation(id): Promise<void> {
this.saving = true;
this.validationErrors = undefined;
this._saving = true;
this._validationErrors = undefined;
let entityRegPromise: Promise<EntityRegistryEntry> | undefined;
if (this.entityRegistryUpdate !== undefined && !this.currentEntityId) {
if (this._entityRegistryUpdate !== undefined && !this._entityId) {
this._newAutomationId = id;
entityRegPromise = new Promise<EntityRegistryEntry>((resolve) => {
this.entityRegCreated = resolve;
this._entityRegCreated = resolve;
});
}
try {
await saveAutomationConfig(this.hass, id, this.config!);
await saveAutomationConfig(this.hass, id, this._config!);
if (this.entityRegistryUpdate !== undefined) {
let entityId = this.currentEntityId;
if (this._entityRegistryUpdate !== undefined) {
let entityId = this._entityId;
// wait for automation to appear in entity registry when creating a new automation
if (entityRegPromise) {
@@ -1021,23 +1131,23 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
if (entityId) {
await updateEntityRegistryEntry(this.hass, entityId, {
categories: {
automation: this.entityRegistryUpdate.category || null,
automation: this._entityRegistryUpdate.category || null,
},
labels: this.entityRegistryUpdate.labels || [],
area_id: this.entityRegistryUpdate.area || null,
labels: this._entityRegistryUpdate.labels || [],
area_id: this._entityRegistryUpdate.area || null,
});
}
}
this.dirty = false;
this._dirty = false;
} catch (errors: any) {
this.errors = errors.body?.message || errors.error || errors.body;
this._errors = errors.body?.message || errors.error || errors.body;
showToast(this, {
message: errors.body?.message || errors.error || errors.body,
});
throw errors;
} finally {
this.saving = false;
this._saving = false;
}
}
@@ -1047,7 +1157,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
ev.detail.unsub = () => {
delete this._configSubscriptions[id];
};
ev.detail.callback(this.config);
ev.detail.callback(this._config);
}
protected supportedShortcuts(): SupportedShortcuts {
@@ -1063,6 +1173,14 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
};
}
protected get isDirty() {
return this._dirty;
}
protected async promptDiscardChanges() {
return this._confirmUnsavedChanged();
}
// @ts-ignore
private _collapseAll() {
this._manualEditor?.collapseAll();
@@ -1087,8 +1205,8 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
private _applyUndoRedo(config: AutomationConfig) {
this._manualEditor?.triggerCloseSidebar();
this.config = config;
this.dirty = true;
this._config = config;
this._dirty = true;
}
private _undo() {
@@ -1117,7 +1235,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
this._showInfo();
break;
case "settings":
this.showSettings();
this._showSettings();
break;
case "category":
this._editCategory();
@@ -1138,11 +1256,11 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
this._takeControl();
break;
case "toggle_yaml_mode":
if (this.mode === "gui") {
this.switchYamlMode();
if (this._mode === "gui") {
this._switchYamlMode();
break;
}
this.switchUiMode();
this._switchUiMode();
break;
case "disable":
this._toggle();
@@ -1159,8 +1277,25 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
static get styles(): CSSResultGroup {
return [
haStyle,
automationScriptEditorStyles,
css`
:host {
--ha-automation-editor-max-width: var(
--ha-automation-editor-width,
1540px
);
}
ha-fade-in {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.yaml-mode {
height: 100%;
display: flex;
flex-direction: column;
padding-bottom: 0;
}
manual-automation-editor,
blueprint-automation-editor {
margin: 0 auto;
@@ -1174,6 +1309,17 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
padding: 0 12px;
}
ha-yaml-editor {
flex-grow: 1;
--actions-border-radius: var(--ha-border-radius-square);
--code-mirror-height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
}
p {
margin-bottom: 0;
}
ha-entity-toggle {
margin-right: 8px;
margin-inline-end: 8px;
@@ -1189,6 +1335,24 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
max-width: 1040px;
padding: 28px 20px 0;
}
ha-fab {
position: fixed;
right: calc(16px + var(--safe-area-inset-right, 0px));
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-fab.dirty {
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
}
ha-tooltip ha-svg-icon {
width: 12px;
}
ha-tooltip .shortcut {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 2px;
}
`,
];
}

View File

@@ -1,199 +0,0 @@
import { consume } from "@lit/context";
import type { CSSResult, TemplateResult, LitElement } from "lit";
import { css, html } from "lit";
import { property, state } from "lit/decorators";
import { transform } from "../../../common/decorators/transform";
import { goBack } from "../../../common/navigate";
import { afterNextRender } from "../../../common/util/render-status";
import { fullEntitiesContext } from "../../../data/context";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import type { Constructor, HomeAssistant, Route } from "../../../types";
import type { EntityRegistryUpdate } from "./automation-save-dialog/show-dialog-automation-save";
import "../../../components/ha-fade-in";
import "../../../components/ha-spinner"; // used by renderLoading() provided to both editors
/** Minimum config shape shared by both AutomationConfig and ScriptConfig. */
interface BaseEditorConfig {
alias?: string;
}
/** Shared CSS styles for both automation and script editors. */
export const automationScriptEditorStyles: CSSResult = css`
:host {
--ha-automation-editor-max-width: var(--ha-automation-editor-width, 1540px);
}
ha-fade-in {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.yaml-mode {
height: 100%;
display: flex;
flex-direction: column;
padding-bottom: 0;
}
ha-yaml-editor {
flex-grow: 1;
--actions-border-radius: var(--ha-border-radius-square);
--code-mirror-height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
}
p {
margin-bottom: 0;
}
ha-fab {
position: fixed;
right: calc(16px + var(--safe-area-inset-right, 0px));
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-fab.dirty {
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
}
ha-tooltip ha-svg-icon {
width: 12px;
}
ha-tooltip .shortcut {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 2px;
}
`;
export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
superClass: Constructor<LitElement>
) => {
class AutomationScriptEditorClass extends superClass {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public entityId: string | null = null;
@state() protected dirty = false;
@state() protected errors?: string;
@state() protected yamlErrors?: string;
@state() protected currentEntityId?: string;
@state() protected mode: "gui" | "yaml" = "gui";
@state() protected readOnly = false;
@state() protected saving = false;
@state() protected validationErrors?: (string | TemplateResult)[];
@state() protected config?: TConfig;
@state() protected blueprintConfig?: TConfig;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
transformer: function (this: { currentEntityId?: string }, value) {
return value.find(
({ entity_id }) => entity_id === this.currentEntityId
);
},
watch: ["currentEntityId"],
})
protected registryEntry?: EntityRegistryEntry;
protected entityRegistryUpdate?: EntityRegistryUpdate;
protected entityRegCreated?: (
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
) => void;
protected renderLoading(): TemplateResult {
return html`
<ha-fade-in .delay=${500}>
<ha-spinner size="large"></ha-spinner>
</ha-fade-in>
`;
}
protected showSettings() {
showMoreInfoDialog(this, {
entityId: this.currentEntityId!,
view: "settings",
});
}
protected async switchUiMode() {
if (this.yamlErrors) {
const result = await showConfirmationDialog(this, {
text: html`${this.hass.localize(
"ui.panel.config.automation.editor.switch_ui_yaml_error"
)}<br /><br />${this.yamlErrors}`,
confirmText: this.hass!.localize("ui.common.continue"),
destructive: true,
dismissText: this.hass!.localize("ui.common.cancel"),
});
if (!result) {
return;
}
}
this.yamlErrors = undefined;
this.mode = "gui";
}
protected switchYamlMode() {
this.mode = "yaml";
}
protected takeControlSave() {
this.readOnly = false;
this.dirty = true;
this.blueprintConfig = undefined;
}
protected revertBlueprint() {
this.config = this.blueprintConfig;
if (this.mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
}
this.blueprintConfig = undefined;
this.readOnly = false;
}
protected backTapped = async () => {
const result = await this.confirmUnsavedChanged();
if (result) {
afterNextRender(() => goBack("/config"));
}
};
protected get isDirty() {
return this.dirty;
}
protected async promptDiscardChanges() {
return this.confirmUnsavedChanged();
}
/**
* Asks whether unsaved changes should be discarded.
* Subclasses must override this to show a confirmation dialog.
* @returns true to proceed (discard/save changes), false to cancel.
*/
protected confirmUnsavedChanged(): Promise<boolean> {
return Promise.resolve(true);
}
}
return AutomationScriptEditorClass;
};

View File

@@ -1,10 +1,10 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-input";
import type { HaInput } from "../../../../../components/ha-input";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-md-textfield";
import type { HaMdTextfield } from "../../../../../components/ha-md-textfield";
import "../../../../../components/ha-select";
import type { SupervisorUpdateConfig } from "../../../../../data/supervisor/update";
import type { HomeAssistant, ValueChangedEvent } from "../../../../../types";
@@ -62,7 +62,7 @@ class HaBackupConfigAddon extends LitElement {
`ui.panel.config.backup.settings.app_update_backup.retention_description`
)}
</span>
<ha-md-textfield
<ha-input
slot="end"
@change=${this._backupRetentionChanged}
.value=${this.supervisorUpdateConfig?.add_on_backup_retain_copies?.toString() ||
@@ -70,11 +70,13 @@ class HaBackupConfigAddon extends LitElement {
type="number"
min=${MIN_RETENTION_VALUE.toString()}
step="1"
.suffixText=${this.hass.localize(
"ui.panel.config.backup.schedule.retention_units.copies"
)}
>
</ha-md-textfield>
<span slot="end">
${this.hass.localize(
"ui.panel.config.backup.schedule.retention_units.copies"
)}
</span>
</ha-input>
</ha-md-list-item>
</ha-md-list>
`;
@@ -92,7 +94,7 @@ class HaBackupConfigAddon extends LitElement {
}
private _backupRetentionChanged(ev) {
const target = ev.currentTarget as HaMdTextfield;
const target = ev.currentTarget as HaInput;
const add_on_backup_retain_copies = Number(target.value);
if (add_on_backup_retain_copies >= MIN_RETENTION_VALUE) {
fireEvent(this, "update-config-changed", {
@@ -115,7 +117,7 @@ class HaBackupConfigAddon extends LitElement {
ha-select {
min-width: 210px;
}
ha-md-textfield {
ha-input {
width: 210px;
}
@media all and (max-width: 450px) {
@@ -123,7 +125,7 @@ class HaBackupConfigAddon extends LitElement {
min-width: 160px;
width: 160px;
}
ha-md-textfield {
ha-input {
width: 160px;
}
}

View File

@@ -3,9 +3,9 @@ import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { clamp } from "../../../../../common/number/clamp";
import "../../../../../components/ha-expansion-panel";
import "../../../../../components/ha-input";
import type { HaInput } from "../../../../../components/ha-input";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-md-textfield";
import type { HaMdTextfield } from "../../../../../components/ha-md-textfield";
import "../../../../../components/ha-select";
import type { HaSelect } from "../../../../../components/ha-select";
import type { BackupConfig, Retention } from "../../../../../data/backup";
@@ -54,7 +54,7 @@ class HaBackupConfigRetention extends LitElement {
@state() private _value = 3;
@query("#value") private _customValueField?: HaMdTextfield;
@query("#value") private _customValueField?: HaInput;
@query("#type") private _customTypeField?: HaSelect;
@@ -141,7 +141,7 @@ class HaBackupConfigRetention extends LitElement {
"ui.panel.config.backup.schedule.custom_retention_label"
)}
</span>
<ha-md-textfield
<ha-input
slot="end"
@change=${this._retentionValueChanged}
.value=${this._value.toString()}
@@ -151,7 +151,7 @@ class HaBackupConfigRetention extends LitElement {
.max=${MAX_VALUE.toString()}
step="1"
>
</ha-md-textfield>
</ha-input>
<ha-select
slot="end"
@selected=${this._retentionTypeChanged}
@@ -208,8 +208,8 @@ class HaBackupConfigRetention extends LitElement {
private _retentionValueChanged(ev) {
ev.stopPropagation();
const target = ev.currentTarget as HaMdTextfield;
const value = parseInt(target.value);
const target = ev.currentTarget as HaInput;
const value = parseInt(target.value ?? "");
const clamped = clamp(value, MIN_VALUE, MAX_VALUE);
target.value = clamped.toString();
@@ -258,14 +258,14 @@ class HaBackupConfigRetention extends LitElement {
width: 160px;
}
}
ha-md-textfield#value {
ha-input#value {
min-width: 70px;
}
ha-select#type {
min-width: 100px;
}
@media all and (max-width: 450px) {
ha-md-textfield#value {
ha-input#value {
min-width: 60px;
margin: 0 -8px;
}

View File

@@ -9,7 +9,6 @@ import "../../../../../components/ha-expansion-panel";
import "../../../../../components/ha-formfield";
import "../../../../../components/ha-md-list";
import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-md-textfield";
import "../../../../../components/ha-select";
import "../../../../../components/ha-time-input";
import "../../../../../components/ha-tip";

View File

@@ -13,7 +13,6 @@ import "../../../../components/ha-icon-next";
import "../../../../components/ha-dialog";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-password-field";
import "../../../../components/ha-svg-icon";
import type {
BackupConfig,

View File

@@ -11,7 +11,6 @@ import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-dialog";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-password-field";
import {
downloadEmergencyKit,
generateEncryptionKey,

View File

@@ -4,9 +4,9 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-dialog";
import "../../../../components/ha-password-field";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-input";
import {
canDecryptBackupOnDownload,
getPreferredAgentForDownload,
@@ -85,12 +85,14 @@ class DialogDownloadDecryptedBackup extends LitElement implements HassDialog {
)}
</p>
<ha-password-field
<ha-input
type="password"
password-toggle
.label=${this.hass.localize(
"ui.panel.config.backup.dialogs.download.encryption_key"
)}
@input=${this._keyChanged}
></ha-password-field>
></ha-input>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`

View File

@@ -5,13 +5,13 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-input";
import "../../../../components/ha-spinner";
import "../../../../components/ha-password-field";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import "../../../../components/ha-alert";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-dialog";
import "../../../../components/ha-svg-icon";
import type { RestoreBackupParams } from "../../../../data/backup";
import {
fetchBackupConfig,
@@ -23,11 +23,11 @@ import type {
RestoreBackupState,
} from "../../../../data/backup_manager";
import { subscribeBackupEvents } from "../../../../data/backup_manager";
import { waitForIntegrationSetup } from "../../../../data/integration";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { RestoreBackupDialogParams } from "./show-dialog-restore-backup";
import { waitForIntegrationSetup } from "../../../../data/integration";
interface FormData {
encryption_key_type: "config" | "custom";
@@ -211,14 +211,16 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
return html`
${this._renderEncryptionIntro()}
<ha-password-field
<ha-input
type="password"
password-toggle
autofocus
@input=${this._passwordChanged}
.label=${this.hass.localize(
"ui.panel.config.backup.dialogs.restore.encryption.input_label"
)}
.value=${this._userPassword || ""}
></ha-password-field>
></ha-input>
`;
}
@@ -387,10 +389,6 @@ class DialogRestoreBackup extends LitElement implements HassDialog {
display: block;
margin-top: 16px;
}
ha-password-field {
display: block;
margin-top: 16px;
}
`,
];
}

View File

@@ -5,12 +5,11 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-dialog";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-password-field";
import {
downloadEmergencyKit,
generateEncryptionKey,

View File

@@ -11,7 +11,6 @@ import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-dialog";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-password-field";
import { downloadEmergencyKit } from "../../../../data/backup";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";

View File

@@ -13,7 +13,6 @@ import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-password-field";
import "../../../components/ha-svg-icon";
import type { BackupAgent, BackupConfig } from "../../../data/backup";
import { updateBackupConfig } from "../../../data/backup";

View File

@@ -7,6 +7,7 @@ import { navigate } from "../../../../common/navigate";
import "../../../../components/ha-alert";
import "../../../../components/ha-card";
import "../../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-list";
@@ -24,7 +25,6 @@ import "../../ha-config-section";
import { showSupportPackageDialog } from "../account/show-dialog-cloud-support-package";
import "./cloud-login";
import type { CloudLogin } from "./cloud-login";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
@customElement("cloud-login-panel")
export class CloudLoginPanel extends LitElement {
@@ -149,7 +149,7 @@ export class CloudLoginPanel extends LitElement {
private _handleForgotPassword() {
this._dismissFlash();
fireEvent(this, "cloud-email-changed", {
value: this._cloudLoginElement.emailField.value,
value: this._cloudLoginElement.emailField.value ?? "",
});
navigate("/config/cloud/forgot-password");
}
@@ -158,7 +158,7 @@ export class CloudLoginPanel extends LitElement {
this._dismissFlash();
fireEvent(this, "cloud-email-changed", {
value: this._cloudLoginElement.emailField.value,
value: this._cloudLoginElement.emailField.value ?? "",
});
navigate("/config/cloud/register");
}

View File

@@ -2,26 +2,24 @@ import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/buttons/ha-progress-button";
import "../../../../components/ha-alert";
import "../../../../components/ha-card";
import "../../../../components/ha-button";
import "../../../../components/ha-password-field";
import type { HaPasswordField } from "../../../../components/ha-password-field";
import "../../../../components/ha-textfield";
import type { HaTextField } from "../../../../components/ha-textfield";
import { haStyle } from "../../../../resources/styles";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-card";
import "../../../../components/ha-input";
import type { HaInput } from "../../../../components/ha-input";
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
import { cloudLogin } from "../../../../data/cloud";
import { loginHaCloud } from "../../../../data/onboarding";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import {
showAlertDialog,
showConfirmationDialog,
showPromptDialog,
} from "../../../lovelace/custom-card-helpers";
import { setAssistPipelinePreferred } from "../../../../data/assist_pipeline";
import { showCloudAlreadyConnectedDialog } from "../dialog-cloud-already-connected/show-dialog-cloud-already-connected";
import type { HomeAssistant } from "../../../../types";
import { loginHaCloud } from "../../../../data/onboarding";
@customElement("cloud-login")
export class CloudLogin extends LitElement {
@@ -40,9 +38,9 @@ export class CloudLogin extends LitElement {
@property({ type: Boolean, attribute: "card-less" }) public cardLess = false;
@query("#email", true) public emailField!: HaTextField;
@query("#email", true) public emailField!: HaInput;
@query("#password", true) private _passwordField!: HaPasswordField;
@query("#password", true) private _passwordField!: HaInput;
@state() private _error?: string;
@@ -71,13 +69,14 @@ export class CloudLogin extends LitElement {
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<ha-textfield
<ha-input
.label=${this.localize(
`ui.panel.${this.translationKeyPanel}.login.email`
)}
id="email"
name="username"
type="email"
hint="This should be a real email address, not an alias. If you used an alias to register, use the email address that the alias forwards to."
autocomplete="username"
required
.value=${this.email ?? ""}
@@ -86,10 +85,13 @@ export class CloudLogin extends LitElement {
.validationMessage=${this.localize(
`ui.panel.${this.translationKeyPanel}.login.email_error_msg`
)}
></ha-textfield>
<ha-password-field
></ha-input>
<ha-input
id="password"
type="password"
password-toggle
name="password"
hint="Use your nabu casa password, not your Home Assistant password. If you don't remember it, use the forgot password link below."
.label=${this.localize(
`ui.panel.${this.translationKeyPanel}.login.password`
)}
@@ -101,7 +103,7 @@ export class CloudLogin extends LitElement {
.validationMessage=${this.localize(
`ui.panel.${this.translationKeyPanel}.login.password_error_msg`
)}
></ha-password-field>
></ha-input>
</div>
<div class="card-actions">
<ha-button
@@ -277,21 +279,29 @@ export class CloudLogin extends LitElement {
private async _handleLogin() {
if (!this._inProgress) {
let valid = true;
if (!this.emailField.reportValidity()) {
this.emailField.focus();
return;
valid = false;
}
if (!this._passwordField.reportValidity()) {
this._passwordField.focus();
if (valid) {
this._passwordField.focus();
}
valid = false;
}
if (!valid) {
return;
}
this._inProgress = true;
this._login(
this.emailField.value,
this._passwordField.value,
this.emailField.value as string,
this._passwordField.value as string,
this.checkConnection
);
}

View File

@@ -5,14 +5,13 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/buttons/ha-progress-button";
import "../../../../components/ha-alert";
import "../../../../components/ha-card";
import "../../../../components/ha-textfield";
import type { HaTextField } from "../../../../components/ha-textfield";
import "../../../../components/ha-input";
import type { HaInput } from "../../../../components/ha-input";
import { cloudRegister, cloudResendVerification } from "../../../../data/cloud";
import "../../../../layouts/hass-subpage";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "../../ha-config-section";
import "../../../../components/ha-password-field";
@customElement("cloud-register")
export class CloudRegister extends LitElement {
@@ -30,9 +29,9 @@ export class CloudRegister extends LitElement {
@state() private _error?: string;
@query("#email", true) private _emailField!: HaTextField;
@query("#email", true) private _emailField!: HaInput;
@query("#password", true) private _passwordField!: HaTextField;
@query("#password", true) private _passwordField!: HaInput;
protected render(): TemplateResult {
return html`
@@ -131,7 +130,7 @@ export class CloudRegister extends LitElement {
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<ha-textfield
<ha-input
autofocus
id="email"
name="email"
@@ -143,12 +142,14 @@ export class CloudRegister extends LitElement {
required
.value=${this.email ?? ""}
@keydown=${this._keyDown}
validationMessage=${this.hass.localize(
.validationMessage=${this.hass.localize(
"ui.panel.config.cloud.register.email_error_msg"
)}
></ha-textfield>
<ha-password-field
></ha-input>
<ha-input
id="password"
type="password"
password-toggle
name="password"
.label=${this.hass.localize(
"ui.panel.config.cloud.register.password"
@@ -158,10 +159,10 @@ export class CloudRegister extends LitElement {
minlength="8"
required
@keydown=${this._keyDown}
validationMessage=${this.hass.localize(
.validationMessage=${this.hass.localize(
"ui.panel.config.cloud.register.password_error_msg"
)}
></ha-password-field>
></ha-input>
</div>
<div class="card-actions">
<button
@@ -209,8 +210,8 @@ export class CloudRegister extends LitElement {
return;
}
const email = emailField.value.toLowerCase();
const password = passwordField.value;
const email = emailField.value?.toLowerCase() || "";
const password = passwordField.value || "";
this._requestInProgress = true;
@@ -235,7 +236,7 @@ export class CloudRegister extends LitElement {
return;
}
const email = emailField.value;
const email = emailField.value || "";
const doResend = async (username: string) => {
try {

View File

@@ -8,7 +8,7 @@ import "../../../../components/ha-duration-input";
import type { HaDurationData } from "../../../../components/ha-duration-input";
import "../../../../components/ha-formfield";
import "../../../../components/ha-icon-picker";
import "../../../../components/ha-textfield";
import "../../../../components/ha-input";
import type { ForDict } from "../../../../data/automation";
import type { DurationDict, Timer } from "../../../../data/timer";
import { haStyle } from "../../../../resources/styles";
@@ -66,21 +66,21 @@ class HaTimerForm extends LitElement {
return html`
<div class="form">
<ha-textfield
<ha-input
.value=${this._name}
.configValue=${"name"}
@input=${this._valueChanged}
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.name"
)}
autoValidate
auto-validate
required
.validationMessage=${this.hass!.localize(
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
></ha-input>
<ha-icon-picker
.hass=${this.hass}
.value=${this._icon}

View File

@@ -20,9 +20,9 @@ import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-svg-icon";
import type { ConfigEntry } from "../../../../../data/config_entries";
import { getConfigEntries } from "../../../../../data/config_entries";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
const THREAD_ICON =
"m 17.126982,8.0730792 c 0,-0.7297242 -0.593746,-1.32357 -1.323637,-1.32357 -0.729454,0 -1.323199,0.5938458 -1.323199,1.32357 v 1.3234242 l 1.323199,1.458e-4 c 0.729891,0 1.323637,-0.5937006 1.323637,-1.32357 z M 11.999709,0 C 5.3829818,0 0,5.3838955 0,12.001455 0,18.574352 5.3105455,23.927406 11.865164,24 V 12.012075 l -3.9275642,-2.91e-4 c -1.1669814,0 -2.1169453,0.949979 -2.1169453,2.118323 0,1.16718 0.9499639,2.116868 2.1169453,2.116868 v 2.615717 c -2.6093089,0 -4.732218,-2.12327 -4.732218,-4.732585 0,-2.61048 2.1229091,-4.7343308 4.732218,-4.7343308 l 3.9275642,5.82e-4 v -1.323279 c 0,-2.172296 1.766691,-3.9395777 3.938181,-3.9395777 2.171928,0 3.9392,1.7672817 3.9392,3.9395777 0,2.1721498 -1.767272,3.9395768 -3.9392,3.9395768 l -1.323199,-1.45e-4 V 23.744102 C 19.911127,22.597726 24,17.768833 24,12.001455 24,5.3838955 18.616727,0 11.999709,0 Z";
@@ -312,7 +312,8 @@ export class MatterConfigDashboard extends LitElement {
}
.container {
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
padding: var(--ha-space-2) var(--ha-space-4)
calc(var(--ha-space-16) + var(--safe-area-inset-bottom, 0px));
}
a[slot="fab"] {

View File

@@ -37,10 +37,10 @@ import {
} from "../../../../../data/zha";
import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow";
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { fileDownload } from "../../../../../util/file_download";
import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../../types";
import { fileDownload } from "../../../../../util/file_download";
@customElement("zha-config-dashboard")
class ZHAConfigDashboard extends LitElement {
@@ -520,7 +520,8 @@ class ZHAConfigDashboard extends LitElement {
}
.container {
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
padding: var(--ha-space-2) var(--ha-space-4)
calc(var(--ha-space-20) + var(--safe-area-inset-bottom, 0px));
}
`,
];

View File

@@ -18,6 +18,7 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { goBack } from "../../../../../common/navigate";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-fab";
@@ -28,7 +29,6 @@ import "../../../../../components/ha-md-list-item";
import "../../../../../components/ha-progress-ring";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-svg-icon";
import { goBack } from "../../../../../common/navigate";
import type { ConfigEntry } from "../../../../../data/config_entries";
import {
ERROR_STATES,
@@ -968,7 +968,8 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
}
.container {
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
padding: var(--ha-space-2) var(--ha-space-4)
calc(var(--ha-space-16) + var(--safe-area-inset-bottom, 0px));
}
`,
];

View File

@@ -11,16 +11,15 @@ import "../../../components/ha-dropdown-item";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-formfield";
import "../../../components/ha-icon-button";
import "../../../components/ha-input";
import type { HaInput } from "../../../components/ha-input";
import "../../../components/ha-list";
import "../../../components/ha-list-item";
import "../../../components/ha-password-field";
import "../../../components/ha-radio";
import type { HaRadio } from "../../../components/ha-radio";
import "../../../components/ha-spinner";
import "../../../components/ha-tab-group";
import "../../../components/ha-tab-group-tab";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import { extractApiErrorMessage } from "../../../data/hassio/common";
import {
type AccessPoint,
@@ -233,7 +232,9 @@ export class HassioNetwork extends LitElement {
${this._wifiConfiguration.auth === "wpa-psk" ||
this._wifiConfiguration.auth === "wep"
? html`
<ha-password-field
<ha-input
type="password"
password-toggle
id="psk"
.label=${this.hass.localize(
"ui.panel.config.network.supervisor.wifi_password"
@@ -241,7 +242,7 @@ export class HassioNetwork extends LitElement {
.version=${"wifi"}
@change=${this._handleInputValueChangedWifi}
>
</ha-password-field>
</ha-input>
`
: nothing}
`
@@ -388,7 +389,7 @@ export class HassioNetwork extends LitElement {
const { ip, mask, prefix } = parseAddress(address);
return html`
<div class="address-row">
<ha-textfield
<ha-input
id="address"
.label=${this.hass.localize(
"ui.panel.config.network.supervisor.ip"
@@ -399,10 +400,10 @@ export class HassioNetwork extends LitElement {
@change=${this._handleInputValueChanged}
.disabled=${disableInputs}
>
</ha-textfield>
</ha-input>
${version === "ipv6"
? html`
<ha-textfield
<ha-input
id="prefix"
.label=${this.hass.localize(
"ui.panel.config.network.supervisor.prefix"
@@ -413,10 +414,10 @@ export class HassioNetwork extends LitElement {
@change=${this._handleInputValueChanged}
.disabled=${disableInputs}
>
</ha-textfield>
</ha-input>
`
: html`
<ha-textfield
<ha-input
id="netmask"
.label=${this.hass.localize(
"ui.panel.config.network.supervisor.netmask"
@@ -427,7 +428,7 @@ export class HassioNetwork extends LitElement {
@change=${this._handleInputValueChanged}
.disabled=${disableInputs}
>
</ha-textfield>
</ha-input>
`}
${this._interface![version].address.length > 1 &&
!disableInputs
@@ -461,7 +462,7 @@ export class HassioNetwork extends LitElement {
</ha-button>
`
: nothing}
<ha-textfield
<ha-input
id="gateway"
.label=${this.hass.localize(
"ui.panel.config.network.supervisor.gateway"
@@ -471,12 +472,12 @@ export class HassioNetwork extends LitElement {
@change=${this._handleInputValueChanged}
.disabled=${disableInputs}
>
</ha-textfield>
</ha-input>
<div class="nameservers">
${nameservers.map(
(nameserver: string, index: number) => html`
<div class="address-row">
<ha-textfield
<ha-input
id="nameserver"
.label=${`${this.hass.localize(
"ui.panel.config.network.supervisor.dns_server"
@@ -486,10 +487,11 @@ export class HassioNetwork extends LitElement {
.index=${index}
@change=${this._handleInputValueChanged}
>
</ha-textfield>
</ha-input>
${this._interface![version].nameservers?.length > 1
? html`
<ha-icon-button
slot="end"
.label=${this.hass.localize("ui.common.delete")}
.path=${mdiDeleteOutline}
.version=${version}
@@ -677,12 +679,13 @@ export class HassioNetwork extends LitElement {
}
private _handleInputValueChanged(ev: Event): void {
const source = ev.target as HaTextField;
const source = ev.target as HaInput;
const value = source.value;
const version = (ev.target as any).version as "ipv4" | "ipv6";
const id = source.id;
if (!value || !this._interface?.[version]) {
source.reportValidity();
return;
}
@@ -718,7 +721,7 @@ export class HassioNetwork extends LitElement {
}
private _handleInputValueChangedWifi(ev: Event): void {
const source = ev.target as HaTextField;
const source = ev.target as HaInput;
const value = source.value;
const id = source.id;
@@ -727,6 +730,7 @@ export class HassioNetwork extends LitElement {
!this._wifiConfiguration ||
this._wifiConfiguration![id] === value
) {
source.reportValidity();
return;
}
this._dirty = true;
@@ -819,26 +823,25 @@ export class HassioNetwork extends LitElement {
--expansion-panel-summary-padding: 0 16px;
margin: 4px 0;
}
ha-textfield {
display: block;
margin-top: 16px;
}
.address-row {
display: flex;
flex-direction: row;
gap: var(--ha-space-2);
align-items: center;
}
.address-row ha-textfield {
.address-row ha-input {
flex: 1;
}
.address-row #prefix {
flex: none;
width: 95px;
}
ha-icon-button {
color: var(--secondary-text-color);
}
.address-row ha-icon-button {
--ha-icon-button-size: 36px;
margin-top: 16px;
margin-top: var(--ha-space-5);
}
ha-dropdown {
display: block;

View File

@@ -1,4 +1,5 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import {
mdiAppleKeyboardCommand,
mdiCog,
@@ -21,13 +22,15 @@ import {
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
import { transform } from "../../../common/decorators/transform";
import { fireEvent } from "../../../common/dom/fire_event";
import { goBack, navigate } from "../../../common/navigate";
import { slugify } from "../../../common/string/slugify";
import { promiseTimeout } from "../../../common/util/promise-timeout";
import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
@@ -37,6 +40,7 @@ import "../../../components/ha-svg-icon";
import "../../../components/ha-yaml-editor";
import { substituteBlueprint } from "../../../data/blueprint";
import { validateConfig } from "../../../data/config";
import { fullEntitiesContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity/entity";
import {
type EntityRegistryEntry,
@@ -63,47 +67,88 @@ import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import type { Entries } from "../../../types";
import type { Entries, HomeAssistant, Route } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import { showAutomationModeDialog } from "../automation/automation-mode-dialog/show-dialog-automation-mode";
import type { EntityRegistryUpdate } from "../automation/automation-save-dialog/show-dialog-automation-save";
import { showAutomationSaveDialog } from "../automation/automation-save-dialog/show-dialog-automation-save";
import { showAutomationSaveTimeoutDialog } from "../automation/automation-save-timeout-dialog/show-dialog-automation-save-timeout";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import "./blueprint-script-editor";
import {
AutomationScriptEditorMixin,
automationScriptEditorStyles,
} from "../automation/ha-automation-script-editor-mixin";
import "./manual-script-editor";
import type { HaManualScriptEditor } from "./manual-script-editor";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
@customElement("ha-script-editor")
export class HaScriptEditor extends SubscribeMixin(
AutomationScriptEditorMixin<ScriptConfig>(
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
)
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public scriptId: string | null = null;
@property({ attribute: false }) public entityId: string | null = null;
@property({ attribute: false }) public entityRegistry!: EntityRegistryEntry[];
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@state() private _config?: ScriptConfig;
@state() private _dirty = false;
@state() private _errors?: string;
@state() private _yamlErrors?: string;
@state() private _entityId?: string;
@state() private _mode: "gui" | "yaml" = "gui";
@state() private _readOnly = false;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
transformer: function (this: HaScriptEditor, value) {
return value.find(({ entity_id }) => entity_id === this._entityId);
},
watch: ["_entityId"],
})
private _registryEntry?: EntityRegistryEntry;
@query("manual-script-editor")
private _manualEditor?: HaManualScriptEditor;
@state() private _validationErrors?: (string | TemplateResult)[];
@state() private _blueprintConfig?: BlueprintScriptConfig;
@state() private _saving = false;
private _entityRegistryUpdate?: EntityRegistryUpdate;
private _newScriptId?: string;
private _entityRegCreated?: (
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
) => void;
private _undoRedoController = new UndoRedoController<ScriptConfig>(this, {
apply: (config) => this._applyUndoRedo(config),
currentConfig: () => this.config!,
currentConfig: () => this._config!,
});
protected willUpdate(changedProps) {
super.willUpdate(changedProps);
if (
this.entityRegCreated &&
this._entityRegCreated &&
this._newScriptId &&
changedProps.has("entityRegistry")
) {
@@ -112,22 +157,22 @@ export class HaScriptEditor extends SubscribeMixin(
entity.platform === "script" && entity.unique_id === this._newScriptId
);
if (script) {
this.entityRegCreated(script);
this.entityRegCreated = undefined;
this._entityRegCreated(script);
this._entityRegCreated = undefined;
}
}
}
protected render(): TemplateResult | typeof nothing {
if (!this.config) {
return this.renderLoading();
if (!this._config) {
return nothing;
}
const stateObj = this.currentEntityId
? this.hass.states[this.currentEntityId]
const stateObj = this._entityId
? this.hass.states[this._entityId]
: undefined;
const useBlueprint = "use_blueprint" in this.config;
const useBlueprint = "use_blueprint" in this._config;
const shortcutIcon = isMac
? html`<ha-svg-icon .path=${mdiAppleKeyboardCommand}></ha-svg-icon>`
: this.hass.localize("ui.panel.config.automation.editor.ctrl");
@@ -137,11 +182,11 @@ export class HaScriptEditor extends SubscribeMixin(
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.backCallback=${this.backTapped}
.header=${this.config.alias ||
.backCallback=${this._backTapped}
.header=${this._config.alias ||
this.hass.localize("ui.panel.config.script.editor.default_name")}
>
${this.mode === "gui" && !this.narrow
${this._mode === "gui" && !this.narrow
? html`<ha-icon-button
slot="toolbar-icon"
.label=${this.hass.localize("ui.common.undo")}
@@ -207,7 +252,7 @@ export class HaScriptEditor extends SubscribeMixin(
.path=${mdiDotsVertical}
></ha-icon-button>
${this.mode === "gui" && this.narrow
${this._mode === "gui" && this.narrow
? html`<ha-dropdown-item
value="undo"
.disabled=${!this._undoRedoController.canUndo}
@@ -241,7 +286,7 @@ export class HaScriptEditor extends SubscribeMixin(
<ha-dropdown-item .disabled=${!stateObj} value="category">
${this.hass.localize(
`ui.panel.config.scene.picker.${this.registryEntry?.categories?.script ? "edit_category" : "assign_category"}`
`ui.panel.config.scene.picker.${this._registryEntry?.categories?.script ? "edit_category" : "assign_category"}`
)}
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
</ha-dropdown-item>
@@ -262,10 +307,10 @@ export class HaScriptEditor extends SubscribeMixin(
></ha-svg-icon>
</ha-dropdown-item>`
: nothing}
${!useBlueprint && !("fields" in this.config)
${!useBlueprint && !("fields" in this._config)
? html`
<ha-dropdown-item
.disabled=${this.readOnly || this.mode === "yaml"}
.disabled=${this._readOnly || this._mode === "yaml"}
value="add_fields"
>
${this.hass.localize(
@@ -281,7 +326,9 @@ export class HaScriptEditor extends SubscribeMixin(
<ha-dropdown-item
value="rename"
.disabled=${!this.scriptId || this.readOnly || this.mode === "yaml"}
.disabled=${!this.scriptId ||
this._readOnly ||
this._mode === "yaml"}
>
${this.hass.localize("ui.panel.config.script.editor.rename")}
<ha-svg-icon slot="icon" .path=${mdiRenameBox}></ha-svg-icon>
@@ -290,7 +337,7 @@ export class HaScriptEditor extends SubscribeMixin(
? html`
<ha-dropdown-item
value="change_mode"
.disabled=${this.readOnly || this.mode === "yaml"}
.disabled=${this._readOnly || this._mode === "yaml"}
>
${this.hass.localize(
"ui.panel.config.script.editor.change_mode"
@@ -304,12 +351,12 @@ export class HaScriptEditor extends SubscribeMixin(
: nothing}
<ha-dropdown-item
.disabled=${!!this.blueprintConfig ||
(!this.readOnly && !this.scriptId)}
.disabled=${!!this._blueprintConfig ||
(!this._readOnly && !this.scriptId)}
value="duplicate"
>
${this.hass.localize(
this.readOnly
this._readOnly
? "ui.panel.config.script.editor.migrate"
: "ui.panel.config.script.editor.duplicate"
)}
@@ -323,7 +370,7 @@ export class HaScriptEditor extends SubscribeMixin(
? html`
<ha-dropdown-item
value="take_control"
.disabled=${this.readOnly}
.disabled=${this._readOnly}
>
${this.hass.localize(
"ui.panel.config.script.editor.take_control"
@@ -335,7 +382,7 @@ export class HaScriptEditor extends SubscribeMixin(
<ha-dropdown-item value="toggle_yaml_mode">
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${this.mode === "gui" ? "yaml" : "ui"}`
`ui.panel.config.automation.editor.edit_${this._mode === "gui" ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-dropdown-item>
@@ -343,7 +390,7 @@ export class HaScriptEditor extends SubscribeMixin(
<wa-divider></wa-divider>
<ha-dropdown-item
.disabled=${this.readOnly || !this.scriptId}
.disabled=${this._readOnly || !this.scriptId}
value="delete"
.variant=${this.scriptId ? "danger" : "default"}
>
@@ -356,8 +403,8 @@ export class HaScriptEditor extends SubscribeMixin(
</ha-svg-icon>
</ha-dropdown-item>
</ha-dropdown>
<div class=${this.mode === "yaml" ? "yaml-mode" : ""}>
${this.mode === "gui"
<div class=${this._mode === "yaml" ? "yaml-mode" : ""}>
${this._mode === "gui"
? html`
<div>
${useBlueprint
@@ -366,10 +413,10 @@ export class HaScriptEditor extends SubscribeMixin(
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
.config=${this.config}
.disabled=${this.readOnly}
.saving=${this.saving}
.dirty=${this.dirty}
.config=${this._config}
.disabled=${this._readOnly}
.saving=${this._saving}
.dirty=${this._dirty}
@value-changed=${this._valueChanged}
@save-script=${this._handleSaveScript}
></blueprint-script-editor>
@@ -379,16 +426,16 @@ export class HaScriptEditor extends SubscribeMixin(
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
.config=${this.config}
.disabled=${this.readOnly}
.dirty=${this.dirty}
.saving=${this.saving}
.config=${this._config}
.disabled=${this._readOnly}
.dirty=${this._dirty}
.saving=${this._saving}
@value-changed=${this._valueChanged}
@editor-save=${this._handleSaveScript}
@save-script=${this._handleSaveScript}
>
<div class="alert-wrapper" slot="alerts">
${this.errors || stateObj?.state === UNAVAILABLE
${this._errors || stateObj?.state === UNAVAILABLE
? html`<ha-alert
alert-type="error"
.title=${stateObj?.state === UNAVAILABLE
@@ -397,7 +444,7 @@ export class HaScriptEditor extends SubscribeMixin(
)
: undefined}
>
${this.errors || this.validationErrors}
${this._errors || this._validationErrors}
${stateObj?.state === UNAVAILABLE
? html`<ha-svg-icon
slot="icon"
@@ -406,7 +453,7 @@ export class HaScriptEditor extends SubscribeMixin(
: nothing}
</ha-alert>`
: nothing}
${this.blueprintConfig
${this._blueprintConfig
? html`<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.script.editor.confirm_take_control"
@@ -414,21 +461,21 @@ export class HaScriptEditor extends SubscribeMixin(
<div slot="action" style="display: flex;">
<ha-button
appearance="plain"
@click=${this.takeControlSave}
@click=${this._takeControlSave}
>${this.hass.localize(
"ui.common.yes"
)}</ha-button
>
<ha-button
appearance="plain"
@click=${this.revertBlueprint}
@click=${this._revertBlueprint}
>${this.hass.localize(
"ui.common.no"
)}</ha-button
>
</div>
</ha-alert>`
: this.readOnly
: this._readOnly
? html`<ha-alert
alert-type="warning"
dismissable
@@ -451,11 +498,11 @@ export class HaScriptEditor extends SubscribeMixin(
`}
</div>
`
: this.mode === "yaml"
: this._mode === "yaml"
? html`<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this._preprocessYaml()}
.readOnly=${this.readOnly}
.readOnly=${this._readOnly}
disable-fullscreen
@value-changed=${this._yamlChanged}
@editor-save=${this._handleSaveScript}
@@ -463,9 +510,9 @@ export class HaScriptEditor extends SubscribeMixin(
></ha-yaml-editor>
<ha-fab
slot="fab"
class=${!this.readOnly && this.dirty ? "dirty" : ""}
class=${!this._readOnly && this._dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.common.save")}
.disabled=${this.saving}
.disabled=${this._saving}
extended
@click=${this._handleSaveScript}
>
@@ -504,26 +551,26 @@ export class HaScriptEditor extends SubscribeMixin(
const entity = this.entityRegistry.find(
(ent) => ent.platform === "script" && ent.unique_id === this.scriptId
);
this.currentEntityId = entity?.entity_id;
this._entityId = entity?.entity_id;
}
if (changedProps.has("scriptId") && !this.scriptId && this.hass) {
const initData = getScriptEditorInitData();
this.dirty = !!initData;
this._dirty = !!initData;
const baseConfig: Partial<ScriptConfig> = {};
if (!initData || !("use_blueprint" in initData)) {
baseConfig.sequence = [];
}
this.config = {
this._config = {
...baseConfig,
...initData,
} as ScriptConfig;
this.readOnly = false;
this._readOnly = false;
}
if (changedProps.has("entityId") && this.entityId) {
getScriptStateConfig(this.hass, this.entityId).then((c) => {
this.config = normalizeScriptConfig(c.config);
this._config = normalizeScriptConfig(c.config);
this._checkValidation();
});
const regEntry = this.entityRegistry.find(
@@ -532,25 +579,25 @@ export class HaScriptEditor extends SubscribeMixin(
if (regEntry?.unique_id) {
this.scriptId = regEntry.unique_id;
}
this.currentEntityId = this.entityId;
this.dirty = false;
this.readOnly = true;
this._entityId = this.entityId;
this._dirty = false;
this._readOnly = true;
}
}
private async _checkValidation() {
this.validationErrors = undefined;
if (!this.currentEntityId || !this.config) {
this._validationErrors = undefined;
if (!this._entityId || !this._config) {
return;
}
const stateObj = this.hass.states[this.currentEntityId];
const stateObj = this.hass.states[this._entityId];
if (stateObj?.state !== UNAVAILABLE) {
return;
}
const validation = await validateConfig(this.hass, {
actions: this.config.sequence,
actions: this._config.sequence,
});
this.validationErrors = (
this._validationErrors = (
Object.entries(validation) as Entries<typeof validation>
).map(([key, value]) =>
value.valid
@@ -565,13 +612,13 @@ export class HaScriptEditor extends SubscribeMixin(
private async _loadConfig() {
fetchScriptFileConfig(this.hass, this.scriptId!).then(
(config) => {
this.dirty = false;
this.readOnly = false;
this.config = normalizeScriptConfig(config);
this._dirty = false;
this._readOnly = false;
this._config = normalizeScriptConfig(config);
const entity = this.entityRegistry.find(
(ent) => ent.platform === "script" && ent.unique_id === this.scriptId
);
this.currentEntityId = entity?.entity_id;
this._entityId = entity?.entity_id;
this._checkValidation();
},
(resp) => {
@@ -600,19 +647,19 @@ export class HaScriptEditor extends SubscribeMixin(
}
private _valueChanged(ev) {
if (this.config) {
this._undoRedoController.commit(this.config);
if (this._config) {
this._undoRedoController.commit(this._config);
}
this.config = ev.detail.value;
this.errors = undefined;
this.dirty = true;
this._config = ev.detail.value;
this._errors = undefined;
this._dirty = true;
}
private async _runScript() {
if (hasScriptFields(this.hass, this.currentEntityId!)) {
if (hasScriptFields(this.hass, this._entityId!)) {
showMoreInfoDialog(this, {
entityId: this.currentEntityId!,
entityId: this._entityId!,
});
return;
}
@@ -620,13 +667,20 @@ export class HaScriptEditor extends SubscribeMixin(
await triggerScript(this.hass, this.scriptId!);
showToast(this, {
message: this.hass.localize("ui.notification_toast.triggered", {
name: this.config!.alias,
name: this._config!.alias,
}),
});
}
private _showSettings() {
showMoreInfoDialog(this, {
entityId: this._entityId!,
view: "settings",
});
}
private _editCategory() {
if (!this.registryEntry) {
if (!this._registryEntry) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.scene.picker.no_category_support"
@@ -639,7 +693,7 @@ export class HaScriptEditor extends SubscribeMixin(
}
showAssignCategoryDialog(this, {
scope: "script",
entityReg: this.registryEntry,
entityReg: this._registryEntry,
});
}
@@ -676,7 +730,7 @@ export class HaScriptEditor extends SubscribeMixin(
private async _showTrace() {
if (this.scriptId) {
const result = await this.confirmUnsavedChanged();
const result = await this._confirmUnsavedChanged();
if (result) {
navigate(`/config/script/trace/${this.scriptId}`);
}
@@ -684,47 +738,47 @@ export class HaScriptEditor extends SubscribeMixin(
}
private _addFields() {
if ("fields" in this.config!) {
if ("fields" in this._config!) {
return;
}
if (this.config) {
this._undoRedoController.commit(this.config);
if (this._config) {
this._undoRedoController.commit(this._config);
}
this._manualEditor?.addFields();
this.dirty = true;
this._dirty = true;
}
private _preprocessYaml() {
return this.config;
return this._config;
}
private _yamlChanged(ev: CustomEvent) {
ev.stopPropagation();
this.dirty = true;
this._dirty = true;
if (!ev.detail.isValid) {
this.yamlErrors = ev.detail.errorMsg;
this._yamlErrors = ev.detail.errorMsg;
return;
}
this.yamlErrors = undefined;
this.config = ev.detail.value;
this.errors = undefined;
this._yamlErrors = undefined;
this._config = ev.detail.value;
this._errors = undefined;
}
protected async confirmUnsavedChanged(): Promise<boolean> {
if (!this.dirty) {
private async _confirmUnsavedChanged(): Promise<boolean> {
if (!this._dirty) {
return true;
}
return new Promise<boolean>((resolve) => {
showAutomationSaveDialog(this, {
config: this.config!,
config: this._config!,
domain: "script",
updateConfig: async (config, entityRegistryUpdate) => {
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this.requestUpdate();
const id = this.scriptId || String(Date.now());
@@ -740,8 +794,8 @@ export class HaScriptEditor extends SubscribeMixin(
},
onClose: () => resolve(false),
onDiscard: () => resolve(true),
entityRegistryUpdate: this.entityRegistryUpdate,
entityRegistryEntry: this.registryEntry,
entityRegistryUpdate: this._entityRegistryUpdate,
entityRegistryEntry: this._registryEntry,
title: this.hass.localize(
this.scriptId
? "ui.panel.config.script.editor.leave.unsaved_confirm_title"
@@ -757,8 +811,15 @@ export class HaScriptEditor extends SubscribeMixin(
});
}
private _backTapped = async () => {
const result = await this._confirmUnsavedChanged();
if (result) {
afterNextRender(() => goBack("/config"));
}
};
private async _takeControl() {
const config = this.config as BlueprintScriptConfig;
const config = this._config as BlueprintScriptConfig;
try {
const result = await substituteBlueprint(
@@ -774,20 +835,35 @@ export class HaScriptEditor extends SubscribeMixin(
description: config.description,
};
this.blueprintConfig = config;
this.config = newConfig;
if (this.mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
this._blueprintConfig = config;
this._config = newConfig;
if (this._mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
}
this.readOnly = true;
this.errors = undefined;
this._readOnly = true;
this._errors = undefined;
} catch (err: any) {
this.errors = err.message;
this._errors = err.message;
}
}
private _revertBlueprint() {
this._config = this._blueprintConfig;
if (this._mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
}
this._blueprintConfig = undefined;
this._readOnly = false;
}
private _takeControlSave() {
this._readOnly = false;
this._dirty = true;
this._blueprintConfig = undefined;
}
private async _duplicate() {
const result = this.readOnly
const result = this._readOnly
? await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.script.picker.migrate_script"
@@ -796,14 +872,14 @@ export class HaScriptEditor extends SubscribeMixin(
"ui.panel.config.script.picker.migrate_script_description"
),
})
: await this.confirmUnsavedChanged();
: await this._confirmUnsavedChanged();
if (result) {
this.currentEntityId = undefined;
this._entityId = undefined;
showScriptEditor({
...this.config,
alias: this.readOnly
? this.config?.alias
: `${this.config?.alias} (${this.hass.localize(
...this._config,
alias: this._readOnly
? this._config?.alias
: `${this._config?.alias} (${this.hass.localize(
"ui.panel.config.script.picker.duplicate"
)})`,
});
@@ -817,7 +893,7 @@ export class HaScriptEditor extends SubscribeMixin(
),
text: this.hass.localize(
"ui.panel.config.script.editor.delete_confirm_text",
{ name: this.config?.alias }
{ name: this._config?.alias }
),
confirmText: this.hass!.localize("ui.common.delete"),
destructive: true,
@@ -831,20 +907,42 @@ export class HaScriptEditor extends SubscribeMixin(
goBack("/config");
}
private async _switchUiMode() {
if (this._yamlErrors) {
const result = await showConfirmationDialog(this, {
text: html`${this.hass.localize(
"ui.panel.config.automation.editor.switch_ui_yaml_error"
)}<br /><br />${this._yamlErrors}`,
confirmText: this.hass!.localize("ui.common.continue"),
destructive: true,
dismissText: this.hass!.localize("ui.common.cancel"),
});
if (!result) {
return;
}
}
this._yamlErrors = undefined;
this._mode = "gui";
}
private _switchYamlMode() {
this._mode = "yaml";
}
private async _promptScriptAlias(): Promise<boolean> {
return new Promise((resolve) => {
showAutomationSaveDialog(this, {
config: this.config!,
config: this._config!,
domain: "script",
updateConfig: async (config, entityRegistryUpdate) => {
this.config = config;
this.entityRegistryUpdate = entityRegistryUpdate;
this.dirty = true;
this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this.requestUpdate();
resolve(true);
},
onClose: () => resolve(false),
entityRegistryUpdate: this.entityRegistryUpdate,
entityRegistryUpdate: this._entityRegistryUpdate,
entityRegistryEntry: this.entityRegistry.find(
(entry) => entry.unique_id === this.scriptId
),
@@ -855,10 +953,10 @@ export class HaScriptEditor extends SubscribeMixin(
private async _promptScriptMode(): Promise<void> {
return new Promise((resolve) => {
showAutomationModeDialog(this, {
config: this.config!,
config: this._config!,
updateConfig: (config) => {
this.config = config;
this.dirty = true;
this._config = config;
this._dirty = true;
this.requestUpdate();
resolve();
},
@@ -868,9 +966,9 @@ export class HaScriptEditor extends SubscribeMixin(
}
private async _handleSaveScript() {
if (this.yamlErrors) {
if (this._yamlErrors) {
showToast(this, {
message: this.yamlErrors,
message: this._yamlErrors,
});
return;
}
@@ -882,9 +980,9 @@ export class HaScriptEditor extends SubscribeMixin(
if (!saved) {
return;
}
this.currentEntityId = this._computeEntityIdFromAlias(this.config!.alias);
this._entityId = this._computeEntityIdFromAlias(this._config!.alias);
}
const id = this.scriptId || this.currentEntityId || Date.now();
const id = this.scriptId || this._entityId || Date.now();
await this._saveScript(id);
if (!this.scriptId) {
@@ -893,13 +991,13 @@ export class HaScriptEditor extends SubscribeMixin(
}
private async _saveScript(id): Promise<void> {
this.saving = true;
this._saving = true;
let entityRegPromise: Promise<EntityRegistryEntry> | undefined;
if (this.entityRegistryUpdate !== undefined && !this.scriptId) {
if (this._entityRegistryUpdate !== undefined && !this.scriptId) {
this._newScriptId = id.toString();
entityRegPromise = new Promise<EntityRegistryEntry>((resolve) => {
this.entityRegCreated = resolve;
this._entityRegCreated = resolve;
});
}
@@ -907,11 +1005,11 @@ export class HaScriptEditor extends SubscribeMixin(
await this.hass!.callApi(
"POST",
"config/script/config/" + id,
this.config
this._config
);
if (this.entityRegistryUpdate !== undefined) {
let entityId = this.currentEntityId;
if (this._entityRegistryUpdate !== undefined) {
let entityId = this._entityId;
// wait for new script to appear in entity registry
if (entityRegPromise) {
@@ -946,23 +1044,23 @@ export class HaScriptEditor extends SubscribeMixin(
if (entityId) {
await updateEntityRegistryEntry(this.hass, entityId, {
categories: {
script: this.entityRegistryUpdate.category || null,
script: this._entityRegistryUpdate.category || null,
},
labels: this.entityRegistryUpdate.labels || [],
area_id: this.entityRegistryUpdate.area || null,
labels: this._entityRegistryUpdate.labels || [],
area_id: this._entityRegistryUpdate.area || null,
});
}
}
this.dirty = false;
this._dirty = false;
} catch (errors: any) {
this.errors = errors.body?.message || errors.error || errors.body;
this._errors = errors.body?.message || errors.error || errors.body;
showToast(this, {
message: errors.body?.message || errors.error || errors.body,
});
throw errors;
} finally {
this.saving = false;
this._saving = false;
}
}
@@ -979,6 +1077,14 @@ export class HaScriptEditor extends SubscribeMixin(
};
}
protected get isDirty() {
return this._dirty;
}
protected async promptDiscardChanges() {
return this._confirmUnsavedChanged();
}
// @ts-ignore
private _collapseAll() {
this._manualEditor?.collapseAll();
@@ -1003,8 +1109,8 @@ export class HaScriptEditor extends SubscribeMixin(
private _applyUndoRedo(config: ScriptConfig) {
this._manualEditor?.triggerCloseSidebar();
this.config = config;
this.dirty = true;
this._config = config;
this._dirty = true;
}
private _undo() {
@@ -1033,7 +1139,7 @@ export class HaScriptEditor extends SubscribeMixin(
this._showInfo();
break;
case "settings":
this.showSettings();
this._showSettings();
break;
case "category":
this._editCategory();
@@ -1057,11 +1163,11 @@ export class HaScriptEditor extends SubscribeMixin(
this._takeControl();
break;
case "toggle_yaml_mode":
if (this.mode === "gui") {
this.switchYamlMode();
if (this._mode === "gui") {
this._switchYamlMode();
break;
}
this.switchUiMode();
this._switchUiMode();
break;
case "delete":
this._deleteConfirm();
@@ -1075,8 +1181,19 @@ export class HaScriptEditor extends SubscribeMixin(
static get styles(): CSSResultGroup {
return [
haStyle,
automationScriptEditorStyles,
css`
:host {
--ha-automation-editor-max-width: var(
--ha-automation-editor-width,
1540px
);
}
.yaml-mode {
height: 100%;
display: flex;
flex-direction: column;
padding-bottom: 0;
}
manual-script-editor,
blueprint-script-editor {
margin: 0 auto;
@@ -1127,9 +1244,29 @@ export class HaScriptEditor extends SubscribeMixin(
padding: 0 12px;
}
ha-yaml-editor {
flex-grow: 1;
--actions-border-radius: var(--ha-border-radius-square);
--code-mirror-height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
}
p {
margin-bottom: 0;
}
span[slot="introduction"] a {
color: var(--primary-color);
}
ha-fab {
position: fixed;
right: 16px;
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-fab.dirty {
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
}
.header {
display: flex;
margin: 16px 0;
@@ -1143,6 +1280,15 @@ export class HaScriptEditor extends SubscribeMixin(
.header a {
color: var(--secondary-text-color);
}
ha-tooltip ha-svg-icon {
width: 12px;
}
ha-tooltip .shortcut {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 2px;
}
`,
];
}

View File

@@ -4,14 +4,14 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-dialog";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-icon-button";
import "../../../components/ha-input";
import type { HaInput } from "../../../components/ha-input";
import "../../../components/ha-md-list-item";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import "../../../components/ha-dialog";
import { createAuthForUser } from "../../../data/auth";
import type { User } from "../../../data/user";
import {
@@ -23,7 +23,6 @@ import {
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import type { AddUserDialogParams } from "./show-dialog-add-user";
import "../../../components/ha-password-field";
@customElement("dialog-add-user")
export class DialogAddUser extends LitElement {
@@ -100,7 +99,7 @@ export class DialogAddUser extends LitElement {
<div>
${this._error ? html` <div class="error">${this._error}</div> ` : ""}
${this._allowChangeName
? html`<ha-textfield
? html`<ha-input
class="name"
name="name"
.label=${this.hass.localize(
@@ -114,9 +113,9 @@ export class DialogAddUser extends LitElement {
@input=${this._handleValueChanged}
@blur=${this._maybePopulateUsername}
autofocus
></ha-textfield>`
></ha-input>`
: ""}
<ha-textfield
<ha-input
class="username"
name="username"
.label=${this.hass.localize(
@@ -127,9 +126,11 @@ export class DialogAddUser extends LitElement {
@input=${this._handleValueChanged}
.validationMessage=${this.hass.localize("ui.common.error_required")}
?autofocus=${!this._allowChangeName}
></ha-textfield>
></ha-input>
<ha-password-field
<ha-input
type="password"
password-toggle
.label=${this.hass.localize(
"ui.panel.config.users.add_user.password"
)}
@@ -138,9 +139,11 @@ export class DialogAddUser extends LitElement {
required
@input=${this._handleValueChanged}
.validationMessage=${this.hass.localize("ui.common.error_required")}
></ha-password-field>
></ha-input>
<ha-password-field
<ha-input
type="password"
password-toggle
.label=${this.hass.localize(
"ui.panel.config.users.add_user.password_confirm"
)}
@@ -154,7 +157,7 @@ export class DialogAddUser extends LitElement {
.errorMessage=${this.hass.localize(
"ui.panel.config.users.add_user.password_not_match"
)}
></ha-password-field>
></ha-input>
<ha-md-list-item>
<span slot="headline"
>${this.hass.localize(
@@ -245,7 +248,7 @@ export class DialogAddUser extends LitElement {
private _handleValueChanged(ev: ValueChangedEvent<string>): void {
this._error = undefined;
const target = ev.target as HaTextField;
const target = ev.target as HaInput;
this[`_${target.name}`] = target.value;
}
@@ -318,11 +321,6 @@ export class DialogAddUser extends LitElement {
display: flex;
padding: 8px 0;
}
ha-textfield,
ha-password-field {
display: block;
margin-bottom: 8px;
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;

View File

@@ -1,20 +1,19 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../components/ha-card";
import "../../components/ha-button";
import "../../components/ha-spinner";
import "../../components/ha-textfield";
import "../../components/ha-password-field";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../../components/ha-alert";
import "../../components/ha-button";
import "../../components/ha-card";
import "../../components/ha-input";
import "../../components/ha-spinner";
import { changePassword, deleteAllRefreshTokens } from "../../data/auth";
import type { RefreshToken } from "../../data/refresh_token";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../dialogs/generic/show-dialog-box";
import type { RefreshToken } from "../../data/refresh_token";
import { changePassword, deleteAllRefreshTokens } from "../../data/auth";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
@customElement("ha-change-password-card")
class HaChangePasswordCard extends LitElement {
@@ -47,8 +46,10 @@ class HaChangePasswordCard extends LitElement {
? html`<ha-alert alert-type="success">${this._statusMsg}</ha-alert>`
: ""}
<ha-password-field
<ha-input
id="currentPassword"
type="password"
password-toggle
name="currentPassword"
.label=${this.hass.localize(
"ui.panel.profile.change_password.current_password"
@@ -58,10 +59,12 @@ class HaChangePasswordCard extends LitElement {
@input=${this._currentPasswordChanged}
@change=${this._currentPasswordChanged}
required
></ha-password-field>
></ha-input>
${this._currentPassword
? html`<ha-password-field
? html`<ha-input
type="password"
password-toggle
.label=${this.hass.localize(
"ui.panel.profile.change_password.new_password"
)}
@@ -72,8 +75,10 @@ class HaChangePasswordCard extends LitElement {
@change=${this._newPasswordChanged}
required
autoValidate
></ha-password-field>
<ha-password-field
></ha-input>
<ha-input
type="password"
password-toggle
.label=${this.hass.localize(
"ui.panel.profile.change_password.confirm_new_password"
)}
@@ -84,7 +89,7 @@ class HaChangePasswordCard extends LitElement {
@change=${this._newPasswordConfirmChanged}
required
autoValidate
></ha-password-field>`
></ha-input>`
: ""}
</div>
@@ -195,10 +200,6 @@ class HaChangePasswordCard extends LitElement {
return [
haStyle,
css`
ha-textfield {
margin-top: 8px;
display: block;
}
#currentPassword {
margin-top: 0;
}

View File

@@ -64,5 +64,10 @@ export const waColorStyles = css`
--wa-focus-ring-color: var(--ha-color-neutral-60);
--wa-shadow-l: 4px 8px 12px 0 rgba(0, 0, 0, 0.3);
--wa-form-control-background-color: var(--wa-color-surface-raised);
--wa-form-control-border-color: var(--ha-color-border-neutral-quiet);
--wa-form-control-value-color: var(--primary-text-color);
--wa-form-control-placeholder-color: var(--ha-color-text-secondary);
}
`;

View File

@@ -14,10 +14,8 @@ export const waMainStyles = css`
--wa-space-l: var(--ha-space-6);
--wa-space-xl: var(--ha-space-8);
--wa-form-control-padding-block: 0.75em;
--wa-form-control-value-line-height: var(--ha-line-height-condensed);
--wa-font-weight-action: var(--ha-font-weight-medium);
--wa-font-weight-body: var(--ha-font-weight-normal);
--wa-transition-normal: 150ms;
--wa-transition-fast: 75ms;
--wa-transition-easing: ease;
@@ -29,13 +27,25 @@ export const waMainStyles = css`
--wa-border-radius-s: var(--ha-border-radius-sm);
--wa-border-radius-m: var(--ha-border-radius-md);
--wa-border-radius-l: var(--ha-border-radius-lg);
--wa-border-radius-pill: var(--ha-border-radius-pill);
--wa-line-height-condensed: var(--ha-line-height-condensed);
--wa-font-size-s: var(--ha-font-size-s);
--wa-font-size-m: var(--ha-font-size-m);
--wa-font-size-l: var(--ha-font-size-l);
--wa-shadow-s: var(--ha-box-shadow-s);
--wa-shadow-m: var(--ha-box-shadow-m);
--wa-shadow-l: var(--ha-box-shadow-l);
--wa-form-control-padding-block: 0.75em;
--wa-form-control-value-line-height: var(--wa-line-height-condensed);
--wa-form-control-value-font-weight: var(--wa-font-weight-body);
--wa-form-control-border-radius: var(--wa-border-radius-l);
--wa-form-control-border-style: var(--wa-border-style);
--wa-form-control-border-width: var(--wa-border-width-s);
--wa-form-control-height: 40px;
--wa-form-control-padding-inline: var(--ha-space-3);
}
${scrollLockStyles}