diff --git a/package.json b/package.json index c1660b1bbf..7665a3f13a 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "regenerator-runtime": "^0.13.2", "resize-observer-polyfill": "^1.5.1", "roboto-fontface": "^0.10.0", - "superstruct": "^0.6.1", + "superstruct": "^0.10.12", "unfetch": "^4.1.0", "vue": "^2.6.11", "vue2-daterange-picker": "^0.5.1", diff --git a/src/data/automation.ts b/src/data/automation.ts index f495c59b48..b3e8d0b0f8 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -46,8 +46,8 @@ export interface MqttTrigger { export interface GeoLocationTrigger { platform: "geo_location"; - source: "string"; - zone: "string"; + source: string; + zone: string; event: "enter" | "leave"; } diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts index c282c9f3da..7c238b3f79 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -29,6 +29,8 @@ import "./types/ha-automation-action-event"; import "./types/ha-automation-action-scene"; import "./types/ha-automation-action-service"; import "./types/ha-automation-action-wait_template"; +import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; +import { handleStructError } from "../../../lovelace/common/structs/handle-errors"; const OPTIONS = [ "condition", @@ -87,12 +89,16 @@ export default class HaAutomationActionRow extends LitElement { @property() public totalActions!: number; + @internalProperty() private _warnings?: string[]; + + @internalProperty() private _uiModeAvailable = true; + @internalProperty() private _yamlMode = false; protected render() { const type = getType(this.action); const selected = type ? OPTIONS.indexOf(type) : -1; - const yamlMode = this._yamlMode || selected === -1; + const yamlMode = this._yamlMode; return html` @@ -137,7 +143,7 @@ export default class HaAutomationActionRow extends LitElement { ${yamlMode ? this.hass.localize( @@ -159,6 +165,16 @@ export default class HaAutomationActionRow extends LitElement { + ${this._warnings + ? html`
+ UI editor is not supported for this config: +
+ + You can still edit your config in yaml. +
` + : ""} ${yamlMode ? html`
@@ -200,7 +216,7 @@ export default class HaAutomationActionRow extends LitElement { )} -
+
${dynamicElement(`ha-automation-action-${type}`, { hass: this.hass, action: this.action, @@ -212,6 +228,13 @@ export default class HaAutomationActionRow extends LitElement { `; } + private _handleUiModeNotAvailable(ev: CustomEvent) { + this._warnings = handleStructError(ev.detail); + if (!this._yamlMode) { + this._yamlMode = true; + } + } + private _moveUp() { fireEvent(this, "move-action", { direction: "up" }); } @@ -241,6 +264,11 @@ export default class HaAutomationActionRow extends LitElement { return; } + this._uiModeAvailable = OPTIONS.includes(type); + if (!this._uiModeAvailable && !this._yamlMode) { + this._yamlMode = false; + } + if (type !== getType(this.action)) { const elClass = customElements.get(`ha-automation-action-${type}`); @@ -260,7 +288,10 @@ export default class HaAutomationActionRow extends LitElement { fireEvent(this, "value-changed", { value: ev.detail.value }); } - private _switchYamlMode() { + private _switchYamlMode(ev: CustomEvent) { + if (ev.detail.source !== "interaction") { + return; + } this._yamlMode = !this._yamlMode; } @@ -283,6 +314,13 @@ export default class HaAutomationActionRow extends LitElement { mwc-list-item[disabled] { --mdc-theme-text-primary-on-background: var(--disabled-text-color); } + .warning { + color: var(--warning-color); + margin-bottom: 8px; + } + .warning ul { + margin: 4px 0; + } `; } } diff --git a/src/panels/config/automation/action/types/ha-automation-action-service.ts b/src/panels/config/automation/action/types/ha-automation-action-service.ts index 97b2a7bf17..5ef46a6875 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-service.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-service.ts @@ -19,12 +19,20 @@ import { ServiceAction } from "../../../../../data/script"; import type { PolymerChangedEvent } from "../../../../../polymer-types"; import type { HomeAssistant } from "../../../../../types"; import { ActionElement, handleChangeEvent } from "../ha-automation-action-row"; +import { assert, optional, object, string } from "superstruct"; +import { EntityId } from "../../../../lovelace/common/structs/is-entity-id"; + +const actionStruct = object({ + service: optional(string()), + entity_id: optional(EntityId), + data: optional(object()), +}); @customElement("ha-automation-action-service") export class HaServiceAction extends LitElement implements ActionElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public action!: ServiceAction; + @property({ attribute: false }) public action!: ServiceAction; @query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor; @@ -60,6 +68,11 @@ export class HaServiceAction extends LitElement implements ActionElement { if (!changedProperties.has("action")) { return; } + try { + assert(this.action, actionStruct); + } catch (error) { + fireEvent(this, "ui-mode-not-available", error); + } if (this._actionData && this._actionData !== this.action.data) { if (this._yamlEditor) { this._yamlEditor.setValue(this.action.data); diff --git a/src/panels/config/automation/condition/ha-automation-condition-row.ts b/src/panels/config/automation/condition/ha-automation-condition-row.ts index 701c5d1b74..300338ed6b 100644 --- a/src/panels/config/automation/condition/ha-automation-condition-row.ts +++ b/src/panels/config/automation/condition/ha-automation-condition-row.ts @@ -18,6 +18,7 @@ import { Condition } from "../../../../data/automation"; import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box"; import { HomeAssistant } from "../../../../types"; import "./ha-automation-condition-editor"; +import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; export interface ConditionElement extends LitElement { condition: Condition; @@ -115,7 +116,10 @@ export default class HaAutomationConditionRow extends LitElement { }); } - private _switchYamlMode() { + private _switchYamlMode(ev: CustomEvent) { + if (ev.detail.source !== "interaction") { + return; + } this._yamlMode = !this._yamlMode; } diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index b8add47400..afcc21ae86 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -50,6 +50,13 @@ import { PaperListboxElement } from "@polymer/paper-listbox"; const MODES = ["single", "restart", "queued", "parallel"]; const MODES_MAX = ["queued", "parallel"]; +declare global { + // for fire event + interface HASSDomEvents { + "ui-mode-not-available": Error; + } +} + export class HaAutomationEditor extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; diff --git a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts index 2a15e6c35a..6a9f06a3c4 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts @@ -34,6 +34,7 @@ import "./types/ha-automation-trigger-time"; import "./types/ha-automation-trigger-time_pattern"; import "./types/ha-automation-trigger-webhook"; import "./types/ha-automation-trigger-zone"; +import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item"; const OPTIONS = [ "device", @@ -218,7 +219,10 @@ export default class HaAutomationTriggerRow extends LitElement { fireEvent(this, "value-changed", { value: ev.detail.value }); } - private _switchYamlMode() { + private _switchYamlMode(ev: CustomEvent) { + if (ev.detail.source !== "interaction") { + return; + } this._yamlMode = !this._yamlMode; } diff --git a/src/panels/lovelace/common/structs/handle-errors.ts b/src/panels/lovelace/common/structs/handle-errors.ts new file mode 100644 index 0000000000..7205025fc6 --- /dev/null +++ b/src/panels/lovelace/common/structs/handle-errors.ts @@ -0,0 +1,24 @@ +import { StructError } from "superstruct"; + +export const handleStructError = (err: Error): string[] => { + if (!(err instanceof StructError)) { + return [err.message]; + } + const errors: string[] = []; + for (const failure of err.failures()) { + if (failure.type === "never") { + errors.push( + `Key "${failure.path[0]}" is not supported by the UI editor.` + ); + } else { + errors.push( + `The value of "${failure.path.join( + "." + )}" is not supported by the UI editor, we support "${ + failure.type + }" but received "${JSON.stringify(failure.value)}".` + ); + } + } + return errors; +}; diff --git a/src/panels/lovelace/common/structs/is-entity-id.ts b/src/panels/lovelace/common/structs/is-entity-id.ts index da04c298a8..26931ab19c 100644 --- a/src/panels/lovelace/common/structs/is-entity-id.ts +++ b/src/panels/lovelace/common/structs/is-entity-id.ts @@ -1,9 +1,17 @@ -export function isEntityId(value: any): string | boolean { +import { StructResult, StructContext, struct } from "superstruct"; + +const isEntityId = (value: unknown, context: StructContext): StructResult => { if (typeof value !== "string") { - return "entity id should be a string"; + return [context.fail({ type: "string" })]; } if (!value.includes(".")) { - return "entity id should be in the format 'domain.entity'"; + return [ + context.fail({ + type: "entity id should be in the format 'domain.entity'", + }), + ]; } return true; -} +}; + +export const EntityId = struct("entity-id", isEntityId); diff --git a/src/panels/lovelace/common/structs/is-icon.ts b/src/panels/lovelace/common/structs/is-icon.ts index 30addc012e..d88dcdb593 100644 --- a/src/panels/lovelace/common/structs/is-icon.ts +++ b/src/panels/lovelace/common/structs/is-icon.ts @@ -1,9 +1,17 @@ -export function isIcon(value: any): string | boolean { +import { StructContext, StructResult, struct } from "superstruct"; + +const isIcon = (value: unknown, context: StructContext): StructResult => { if (typeof value !== "string") { - return "icon should be a string"; + return [context.fail({ type: "string" })]; } if (!value.includes(":")) { - return "icon should be in the format 'mdi:icon'"; + return [ + context.fail({ + type: "icon should be in the format 'mdi:icon'", + }), + ]; } return true; -} +}; + +export const Icon = struct("icon", isIcon); diff --git a/src/panels/lovelace/common/structs/struct.ts b/src/panels/lovelace/common/structs/struct.ts deleted file mode 100644 index 9ce805975c..0000000000 --- a/src/panels/lovelace/common/structs/struct.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { superstruct } from "superstruct"; -import { isEntityId } from "./is-entity-id"; -import { isIcon } from "./is-icon"; - -export const struct = superstruct({ - types: { - "entity-id": isEntityId, - icon: isIcon, - }, -}); diff --git a/src/panels/lovelace/editor/card-editor/hui-card-editor.ts b/src/panels/lovelace/editor/card-editor/hui-card-editor.ts index 38426daa13..f081cc65ae 100644 --- a/src/panels/lovelace/editor/card-editor/hui-card-editor.ts +++ b/src/panels/lovelace/editor/card-editor/hui-card-editor.ts @@ -26,6 +26,8 @@ import type { LovelaceCardEditor } from "../../types"; import type { GUIModeChangedEvent } from "../types"; import "../../../../components/ha-circular-progress"; import { deepEqual } from "../../../../common/util/deep-equal"; +import { handleStructError } from "../../common/structs/handle-errors"; +import { GUISupportError } from "../gui-support-error"; export interface ConfigChangedEvent { config: LovelaceCardConfig; @@ -69,7 +71,7 @@ export class HuiCardEditor extends LitElement { @internalProperty() private _error?: string; // Warning: GUI editor can't handle configuration - ok to save - @internalProperty() private _warning?: string; + @internalProperty() private _warnings?: string[]; @internalProperty() private _loading = false; @@ -121,7 +123,7 @@ export class HuiCardEditor extends LitElement { } public get hasWarning(): boolean { - return this._warning !== undefined; + return this._warnings !== undefined; } public get hasError(): boolean { @@ -194,10 +196,15 @@ export class HuiCardEditor extends LitElement {
` : ""} - ${this._warning + ${this._warnings ? html`
- ${this._warning} + UI editor is not supported for this config: +
+
    + ${this._warnings.map((warning) => html`
  • ${warning}
  • `)} +
+ You can still edit your config in yaml.
` : ""} @@ -238,7 +245,7 @@ export class HuiCardEditor extends LitElement { let configElement = this._configElement; try { this._error = undefined; - this._warning = undefined; + this._warnings = undefined; if (this._configElType !== cardType) { // If the card type has changed, we need to load a new GUI editor @@ -254,7 +261,9 @@ export class HuiCardEditor extends LitElement { configElement = await elClass.getConfigElement(); } else { configElement = undefined; - throw Error(`WARNING: No visual editor available for: ${cardType}`); + throw new GUISupportError( + `No visual editor available for: ${cardType}` + ); } this._configElement = configElement; @@ -272,11 +281,14 @@ export class HuiCardEditor extends LitElement { try { this._configElement!.setConfig(this.value); } catch (err) { - throw Error(`WARNING: ${err.message}`); + throw new GUISupportError( + "Config is not supported", + handleStructError(err) + ); } } catch (err) { - if (err.message.startsWith("WARNING:")) { - this._warning = err.message.substr(8); + if (err instanceof GUISupportError) { + this._warnings = err.warnings ?? [err.message]; } else { this._error = err; } @@ -312,6 +324,9 @@ export class HuiCardEditor extends LitElement { .warning { color: var(--warning-color); } + .warning ul { + margin: 4px 0; + } ha-circular-progress { display: block; margin: auto; diff --git a/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts index ac8bc242f3..bf9c963199 100644 --- a/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-alarm-panel-card-editor.ts @@ -16,18 +16,18 @@ import "../../../../components/entity/ha-entity-picker"; import "../../../../components/ha-icon"; import { HomeAssistant } from "../../../../types"; import { AlarmPanelCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import "../../components/hui-theme-select-editor"; import { LovelaceCardEditor } from "../../types"; import { EditorTarget, EntitiesEditorEvent } from "../types"; import { configElementStyle } from "./config-elements-style"; +import { assert, object, string, optional, array } from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - entity: "string?", - name: "string?", - states: "array?", - theme: "string?", +const cardConfigStruct = object({ + type: string(), + entity: optional(string()), + name: optional(string()), + states: optional(array()), + theme: optional(string()), }); const includeDomains = ["alarm_control_panel"]; @@ -40,7 +40,7 @@ export class HuiAlarmPanelCardEditor extends LitElement @internalProperty() private _config?: AlarmPanelCardConfig; public setConfig(config: AlarmPanelCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; } @@ -194,6 +194,7 @@ export class HuiAlarmPanelCardEditor extends LitElement } if (target.configValue) { if (target.value === "") { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { this._config = { diff --git a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts index 0e27cedcc3..36ce2aab96 100644 --- a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts @@ -13,7 +13,6 @@ import "../../../../components/ha-icon-input"; import { ActionConfig } from "../../../../data/lovelace"; import { HomeAssistant } from "../../../../types"; import { ButtonCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import "../../components/hui-action-editor"; import "../../components/hui-entity-editor"; import "../../components/hui-theme-select-editor"; @@ -27,19 +26,20 @@ import "../../../../components/ha-switch"; import "../../../../components/ha-formfield"; import { configElementStyle } from "./config-elements-style"; import { computeRTLDirection } from "../../../../common/util/compute_rtl"; +import { assert, object, string, optional, boolean } from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - entity: "string?", - name: "string?", - show_name: "boolean?", - icon: "string?", - show_icon: "boolean?", - icon_height: "string?", - tap_action: struct.optional(actionConfigStruct), - hold_action: struct.optional(actionConfigStruct), - theme: "string?", - show_state: "boolean?", +const cardConfigStruct = object({ + type: string(), + entity: optional(string()), + name: optional(string()), + show_name: optional(boolean()), + icon: optional(string()), + show_icon: optional(boolean()), + icon_height: optional(string()), + tap_action: optional(actionConfigStruct), + hold_action: optional(actionConfigStruct), + theme: optional(string()), + show_state: optional(boolean()), }); @customElement("hui-button-card-editor") @@ -50,7 +50,7 @@ export class HuiButtonCardEditor extends LitElement @internalProperty() private _config?: ButtonCardConfig; public setConfig(config: ButtonCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; } @@ -258,6 +258,7 @@ export class HuiButtonCardEditor extends LitElement } if (target.configValue) { if (target.value === "") { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { let newValue: string | undefined; diff --git a/src/panels/lovelace/editor/config-elements/hui-conditional-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-conditional-card-editor.ts index 4957008c07..eccabf4bf5 100644 --- a/src/panels/lovelace/editor/config-elements/hui-conditional-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-conditional-card-editor.ts @@ -16,7 +16,6 @@ import "../../../../components/entity/ha-entity-picker"; import { LovelaceConfig } from "../../../../data/lovelace"; import { HomeAssistant } from "../../../../types"; import { ConditionalCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import { LovelaceCardEditor } from "../../types"; import { ConfigChangedEvent, @@ -24,16 +23,17 @@ import { } from "../card-editor/hui-card-editor"; import "../card-editor/hui-card-picker"; import { GUIModeChangedEvent } from "../types"; +import { string, any, object, optional, array, assert } from "superstruct"; -const conditionStruct = struct({ - entity: "string", - state: "string?", - state_not: "string?", +const conditionStruct = object({ + entity: string(), + state: optional(string()), + state_not: optional(string()), }); -const cardConfigStruct = struct({ - type: "string", - card: "any", - conditions: struct.optional([conditionStruct]), +const cardConfigStruct = object({ + type: string(), + card: any(), + conditions: optional(array(conditionStruct)), }); @customElement("hui-conditional-card-editor") @@ -54,7 +54,8 @@ export class HuiConditionalCardEditor extends LitElement @query("hui-card-editor") private _cardEditorEl?: HuiCardEditor; public setConfig(config: ConditionalCardConfig): void { - this._config = cardConfigStruct(config); + assert(config, cardConfigStruct); + this._config = config; } public refreshYamlEditor(focus) { @@ -217,7 +218,7 @@ export class HuiConditionalCardEditor extends LitElement } this._setMode(true); this._guiModeAvailable = true; - this._config.card = ev.detail.config; + this._config = { ...this._config, card: ev.detail.config }; fireEvent(this, "config-changed", { config: this._config }); } @@ -226,7 +227,7 @@ export class HuiConditionalCardEditor extends LitElement if (!this._config) { return; } - this._config.card = ev.detail.config; + this._config = { ...this._config, card: ev.detail.config }; this._guiModeAvailable = ev.detail.guiModeAvailable; fireEvent(this, "config-changed", { config: this._config }); } @@ -236,7 +237,8 @@ export class HuiConditionalCardEditor extends LitElement return; } // @ts-ignore - this._config.card = {}; + this._config = { ...this._config, card: {} }; + // @ts-ignore fireEvent(this, "config-changed", { config: this._config }); } @@ -245,10 +247,12 @@ export class HuiConditionalCardEditor extends LitElement if (target.value === "" || !this._config) { return; } - this._config.conditions.push({ + const conditions = [...this._config.conditions]; + conditions.push({ entity: target.value, state: "", }); + this._config = { ...this._config, conditions }; target.value = ""; fireEvent(this, "config-changed", { config: this._config }); } @@ -258,10 +262,11 @@ export class HuiConditionalCardEditor extends LitElement if (!this._config || !target) { return; } + const conditions = [...this._config.conditions]; if (target.configValue === "entity" && target.value === "") { - this._config.conditions.splice(target.index, 1); + conditions.splice(target.index, 1); } else { - const condition = this._config.conditions[target.index]; + const condition = { ...conditions[target.index] }; if (target.configValue === "entity") { condition.entity = target.value; } else if (target.configValue === "state") { @@ -281,8 +286,9 @@ export class HuiConditionalCardEditor extends LitElement delete condition.state_not; } } - this._config.conditions[target.index] = condition; + conditions[target.index] = condition; } + this._config = { ...this._config, conditions }; fireEvent(this, "config-changed", { config: this._config }); } diff --git a/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts index 279337710b..b2642b2a09 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts @@ -20,7 +20,6 @@ import { EntitiesCardConfig, EntitiesCardEntityConfig, } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import "../../components/hui-entity-editor"; import "../../components/hui-theme-select-editor"; import { headerFooterConfigStructs } from "../../header-footer/types"; @@ -33,15 +32,24 @@ import { } from "../types"; import { configElementStyle } from "./config-elements-style"; import { computeRTLDirection } from "../../../../common/util/compute_rtl"; +import { + string, + optional, + object, + boolean, + array, + union, + assert, +} from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - title: "string|number?", - theme: "string?", - show_header_toggle: "boolean?", - entities: [entitiesConfigStruct], - header: struct.optional(headerFooterConfigStructs), - footer: struct.optional(headerFooterConfigStructs), +const cardConfigStruct = object({ + type: string(), + title: optional(union([string(), boolean()])), + theme: optional(string()), + show_header_toggle: optional(boolean()), + entities: array(entitiesConfigStruct), + header: optional(headerFooterConfigStructs), + footer: optional(headerFooterConfigStructs), }); @customElement("hui-entities-card-editor") @@ -54,7 +62,7 @@ export class HuiEntitiesCardEditor extends LitElement @internalProperty() private _configEntities?: EntitiesCardEntityConfig[]; public setConfig(config: EntitiesCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; this._configEntities = processEditorEntities(config.entities); } @@ -131,6 +139,7 @@ export class HuiEntitiesCardEditor extends LitElement this._configEntities = processEditorEntities(this._config.entities); } else if (target.configValue) { if (target.value === "") { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { this._config = { diff --git a/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts index 7e68423aec..cd230be535 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entity-card-editor.ts @@ -12,7 +12,6 @@ import { stateIcon } from "../../../../common/entity/state_icon"; import "../../../../components/ha-icon-input"; import { HomeAssistant } from "../../../../types"; import { EntityCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import "../../components/hui-action-editor"; import "../../components/hui-entity-editor"; import "../../components/hui-theme-select-editor"; @@ -20,16 +19,17 @@ import { headerFooterConfigStructs } from "../../header-footer/types"; import { LovelaceCardEditor } from "../../types"; import { EditorTarget, EntitiesEditorEvent } from "../types"; import { configElementStyle } from "./config-elements-style"; +import { string, object, optional, assert } from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - entity: "string?", - name: "string?", - icon: "string?", - attribute: "string?", - unit: "string?", - theme: "string?", - footer: struct.optional(headerFooterConfigStructs), +const cardConfigStruct = object({ + type: string(), + entity: optional(string()), + name: optional(string()), + icon: optional(string()), + attribute: optional(string()), + unit: optional(string()), + theme: optional(string()), + footer: optional(headerFooterConfigStructs), }); @customElement("hui-entity-card-editor") @@ -40,7 +40,7 @@ export class HuiEntityCardEditor extends LitElement @internalProperty() private _config?: EntityCardConfig; public setConfig(config: EntityCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; } @@ -158,6 +158,7 @@ export class HuiEntityCardEditor extends LitElement } if (target.configValue) { if (target.value === "") { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { let newValue: string | undefined; diff --git a/src/panels/lovelace/editor/config-elements/hui-gauge-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-gauge-card-editor.ts index 4fbb33c88e..c58d65aa6b 100644 --- a/src/panels/lovelace/editor/config-elements/hui-gauge-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-gauge-card-editor.ts @@ -14,23 +14,23 @@ import "../../../../components/ha-switch"; import "../../../../components/ha-formfield"; import { HomeAssistant } from "../../../../types"; import { GaugeCardConfig, SeverityConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import "../../components/hui-entity-editor"; import "../../components/hui-theme-select-editor"; import { LovelaceCardEditor } from "../../types"; import { EditorTarget, EntitiesEditorEvent } from "../types"; import { configElementStyle } from "./config-elements-style"; import { computeRTLDirection } from "../../../../common/util/compute_rtl"; +import { assert, object, string, optional, number } from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - name: "string?", - entity: "string?", - unit: "string?", - min: "number?", - max: "number?", - severity: "object?", - theme: "string?", +const cardConfigStruct = object({ + type: string(), + name: optional(string()), + entity: optional(string()), + unit: optional(string()), + min: optional(number()), + max: optional(number()), + severity: optional(object()), + theme: optional(string()), }); const includeDomains = ["sensor"]; @@ -43,7 +43,7 @@ export class HuiGaugeCardEditor extends LitElement @internalProperty() private _config?: GaugeCardConfig; public setConfig(config: GaugeCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; } @@ -260,6 +260,7 @@ export class HuiGaugeCardEditor extends LitElement target.value === "" || (target.type === "number" && isNaN(Number(target.value))) ) { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { let value: any = target.value; diff --git a/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts index e7688714f1..bca304e179 100644 --- a/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts @@ -17,7 +17,6 @@ import "../../../../components/ha-switch"; import "../../../../components/ha-formfield"; import { HomeAssistant } from "../../../../types"; import { ConfigEntity, GlanceCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import "../../components/hui-entity-editor"; import "../../components/hui-theme-select-editor"; import { LovelaceCardEditor } from "../../types"; @@ -29,16 +28,26 @@ import { } from "../types"; import { configElementStyle } from "./config-elements-style"; import { computeRTLDirection } from "../../../../common/util/compute_rtl"; +import { + string, + union, + object, + optional, + number, + boolean, + assert, + array, +} from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - title: "string|number?", - theme: "string?", - columns: "number?", - show_name: "boolean?", - show_state: "boolean?", - show_icon: "boolean?", - entities: [entitiesConfigStruct], +const cardConfigStruct = object({ + type: string(), + title: optional(union([string(), number()])), + theme: optional(string()), + columns: optional(number()), + show_name: optional(boolean()), + show_state: optional(boolean()), + show_icon: optional(boolean()), + entities: array(entitiesConfigStruct), }); @customElement("hui-glance-card-editor") @@ -51,7 +60,7 @@ export class HuiGlanceCardEditor extends LitElement @internalProperty() private _configEntities?: ConfigEntity[]; public setConfig(config: GlanceCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; this._configEntities = processEditorEntities(config.entities); } @@ -191,6 +200,7 @@ export class HuiGlanceCardEditor extends LitElement target.value === "" || (target.type === "number" && isNaN(Number(target.value))) ) { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { let value: any = target.value; diff --git a/src/panels/lovelace/editor/config-elements/hui-history-graph-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-history-graph-card-editor.ts index 6774dd3ef2..ed6193bf82 100644 --- a/src/panels/lovelace/editor/config-elements/hui-history-graph-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-history-graph-card-editor.ts @@ -10,28 +10,37 @@ import { import { fireEvent } from "../../../../common/dom/fire_event"; import { HomeAssistant } from "../../../../types"; import { HistoryGraphCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import "../../components/hui-entity-editor"; import { EntityConfig } from "../../entity-rows/types"; import { LovelaceCardEditor } from "../../types"; import { processEditorEntities } from "../process-editor-entities"; import { EditorTarget, EntitiesEditorEvent } from "../types"; import { configElementStyle } from "./config-elements-style"; +import { + assert, + union, + optional, + string, + object, + array, + number, +} from "superstruct"; +import { EntityId } from "../../common/structs/is-entity-id"; -const entitiesConfigStruct = struct.union([ - { - entity: "entity-id", - name: "string?", - }, - "entity-id", +const entitiesConfigStruct = union([ + object({ + entity: EntityId, + name: optional(string()), + }), + EntityId, ]); -const cardConfigStruct = struct({ - type: "string", - entities: [entitiesConfigStruct], - title: "string?", - hours_to_show: "number?", - refresh_interval: "number?", +const cardConfigStruct = object({ + type: string(), + entities: array(entitiesConfigStruct), + title: optional(string()), + hours_to_show: optional(number()), + refresh_interval: optional(number()), }); @customElement("hui-history-graph-card-editor") @@ -44,7 +53,7 @@ export class HuiHistoryGraphCardEditor extends LitElement @internalProperty() private _configEntities?: EntityConfig[]; public setConfig(config: HistoryGraphCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; this._configEntities = processEditorEntities(config.entities); } @@ -131,6 +140,7 @@ export class HuiHistoryGraphCardEditor extends LitElement this._configEntities = processEditorEntities(this._config.entities); } else if (target.configValue) { if (target.value === "") { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { let value: any = target.value; diff --git a/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts index 4f016f1ead..6183248167 100644 --- a/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts @@ -11,17 +11,17 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/entity/ha-entity-picker"; import { HomeAssistant } from "../../../../types"; import { HumidifierCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import "../../components/hui-theme-select-editor"; import { LovelaceCardEditor } from "../../types"; import { EditorTarget, EntitiesEditorEvent } from "../types"; import { configElementStyle } from "./config-elements-style"; +import { string, object, optional, assert } from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - entity: "string", - name: "string?", - theme: "string?", +const cardConfigStruct = object({ + type: string(), + entity: string(), + name: optional(string()), + theme: optional(string()), }); const includeDomains = ["humidifier"]; @@ -34,7 +34,7 @@ export class HuiHumidifierCardEditor extends LitElement @internalProperty() private _config?: HumidifierCardConfig; public setConfig(config: HumidifierCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; } @@ -102,6 +102,7 @@ export class HuiHumidifierCardEditor extends LitElement } if (target.configValue) { if (target.value === "") { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { this._config = { ...this._config, [target.configValue!]: target.value }; diff --git a/src/panels/lovelace/editor/config-elements/hui-iframe-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-iframe-card-editor.ts index fdda725d1c..07b1d2e0f4 100644 --- a/src/panels/lovelace/editor/config-elements/hui-iframe-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-iframe-card-editor.ts @@ -10,16 +10,16 @@ import { import { fireEvent } from "../../../../common/dom/fire_event"; import { HomeAssistant } from "../../../../types"; import { IframeCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import { LovelaceCardEditor } from "../../types"; import { EditorTarget, EntitiesEditorEvent } from "../types"; import { configElementStyle } from "./config-elements-style"; +import { string, assert, object, optional } from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - title: "string?", - url: "string?", - aspect_ratio: "string?", +const cardConfigStruct = object({ + type: string(), + title: optional(string()), + url: optional(string()), + aspect_ratio: optional(string()), }); @customElement("hui-iframe-card-editor") @@ -30,7 +30,7 @@ export class HuiIframeCardEditor extends LitElement @internalProperty() private _config?: IframeCardConfig; public setConfig(config: IframeCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; } @@ -102,6 +102,7 @@ export class HuiIframeCardEditor extends LitElement } if (target.configValue) { if (value === "") { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { this._config = { ...this._config, [target.configValue!]: value }; diff --git a/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts index 3e511e9be5..6e37bd0358 100644 --- a/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-light-card-editor.ts @@ -13,7 +13,6 @@ import "../../../../components/ha-icon-input"; import { ActionConfig } from "../../../../data/lovelace"; import { HomeAssistant } from "../../../../types"; import { LightCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import "../../components/hui-action-editor"; import "../../components/hui-entity-editor"; import "../../components/hui-theme-select-editor"; @@ -24,15 +23,16 @@ import { EntitiesEditorEvent, } from "../types"; import { configElementStyle } from "./config-elements-style"; +import { string, object, optional, assert } from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - name: "string?", - entity: "string?", - theme: "string?", - icon: "string?", - hold_action: struct.optional(actionConfigStruct), - double_tap_action: struct.optional(actionConfigStruct), +const cardConfigStruct = object({ + type: string(), + name: optional(string()), + entity: optional(string()), + theme: optional(string()), + icon: optional(string()), + hold_action: optional(actionConfigStruct), + double_tap_action: optional(actionConfigStruct), }); const includeDomains = ["light"]; @@ -45,7 +45,8 @@ export class HuiLightCardEditor extends LitElement @internalProperty() private _config?: LightCardConfig; public setConfig(config: LightCardConfig): void { - this._config = cardConfigStruct(config); + assert(config, cardConfigStruct); + this._config = config; } get _name(): string { @@ -177,6 +178,7 @@ export class HuiLightCardEditor extends LitElement } if (target.configValue) { if (target.value === "") { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { this._config = { diff --git a/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts index 6f3788cfad..3ec82725c5 100644 --- a/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-map-card-editor.ts @@ -13,7 +13,6 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import { PolymerChangedEvent } from "../../../../polymer-types"; import { HomeAssistant } from "../../../../types"; import { MapCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import "../../components/hui-entity-editor"; import "../../components/hui-input-list-editor"; import { EntityConfig } from "../../entity-rows/types"; @@ -28,16 +27,25 @@ import "../../../../components/ha-switch"; import "../../../../components/ha-formfield"; import { configElementStyle } from "./config-elements-style"; import { computeRTLDirection } from "../../../../common/util/compute_rtl"; +import { + string, + optional, + object, + number, + boolean, + array, + assert, +} from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - title: "string?", - aspect_ratio: "string?", - default_zoom: "number?", - dark_mode: "boolean?", - entities: [entitiesConfigStruct], - hours_to_show: "number?", - geo_location_sources: "array?", +const cardConfigStruct = object({ + type: string(), + title: optional(string()), + aspect_ratio: optional(string()), + default_zoom: optional(number()), + dark_mode: optional(boolean()), + entities: array(entitiesConfigStruct), + hours_to_show: optional(number()), + geo_location_sources: optional(array()), }); @customElement("hui-map-card-editor") @@ -49,7 +57,7 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { @internalProperty() private _configEntities?: EntityConfig[]; public setConfig(config: MapCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; this._configEntities = config.entities ? processEditorEntities(config.entities) @@ -195,6 +203,7 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { value = Number(value); } if (target.value === "" || (target.type === "number" && isNaN(value))) { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else if (target.configValue) { this._config = { diff --git a/src/panels/lovelace/editor/config-elements/hui-markdown-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-markdown-card-editor.ts index 250cd4583f..cda28b3671 100644 --- a/src/panels/lovelace/editor/config-elements/hui-markdown-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-markdown-card-editor.ts @@ -11,17 +11,17 @@ import { import { fireEvent } from "../../../../common/dom/fire_event"; import { HomeAssistant } from "../../../../types"; import { MarkdownCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import "../../components/hui-theme-select-editor"; import { LovelaceCardEditor } from "../../types"; import { EditorTarget, EntitiesEditorEvent } from "../types"; import { configElementStyle } from "./config-elements-style"; +import { string, assert, object, optional } from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - title: "string?", - content: "string", - theme: "string?", +const cardConfigStruct = object({ + type: string(), + title: optional(string()), + content: string(), + theme: optional(string()), }); @customElement("hui-markdown-card-editor") @@ -32,7 +32,7 @@ export class HuiMarkdownCardEditor extends LitElement @internalProperty() private _config?: MarkdownCardConfig; public setConfig(config: MarkdownCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; } @@ -100,6 +100,7 @@ export class HuiMarkdownCardEditor extends LitElement } if (target.configValue) { if (target.value === "" && target.configValue !== "content") { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { this._config = { diff --git a/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts index 4b80b6d36c..fa13a3c860 100644 --- a/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-media-control-card-editor.ts @@ -10,13 +10,13 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/entity/ha-entity-picker"; import { HomeAssistant } from "../../../../types"; import { MediaControlCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import { LovelaceCardEditor } from "../../types"; import { EditorTarget, EntitiesEditorEvent } from "../types"; +import { assert, object, string, optional } from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - entity: "string?", +const cardConfigStruct = object({ + type: string(), + entity: optional(string()), }); const includeDomains = ["media_player"]; @@ -29,7 +29,7 @@ export class HuiMediaControlCardEditor extends LitElement @internalProperty() private _config?: MediaControlCardConfig; public setConfig(config: MediaControlCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; } @@ -71,6 +71,7 @@ export class HuiMediaControlCardEditor extends LitElement } if (target.configValue) { if (target.value === "") { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { this._config = { diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts index 914fa0a9e4..98fbf6af9a 100644 --- a/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-picture-card-editor.ts @@ -11,7 +11,6 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import { ActionConfig } from "../../../../data/lovelace"; import { HomeAssistant } from "../../../../types"; import { PictureCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import "../../components/hui-action-editor"; import "../../components/hui-theme-select-editor"; import { LovelaceCardEditor } from "../../types"; @@ -21,13 +20,14 @@ import { EntitiesEditorEvent, } from "../types"; import { configElementStyle } from "./config-elements-style"; +import { string, object, optional, assert } from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - image: "string?", - tap_action: struct.optional(actionConfigStruct), - hold_action: struct.optional(actionConfigStruct), - theme: "string?", +const cardConfigStruct = object({ + type: string(), + image: optional(string()), + tap_action: optional(actionConfigStruct), + hold_action: optional(actionConfigStruct), + theme: optional(string()), }); @customElement("hui-picture-card-editor") @@ -38,7 +38,7 @@ export class HuiPictureCardEditor extends LitElement @internalProperty() private _config?: PictureCardConfig; public setConfig(config: PictureCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; } @@ -128,6 +128,7 @@ export class HuiPictureCardEditor extends LitElement } if (target.configValue) { if (target.value === "") { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { this._config = { diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts index 52d23ef79a..235bfdd8b6 100644 --- a/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-picture-entity-card-editor.ts @@ -16,7 +16,6 @@ import "../../../../components/ha-formfield"; import { ActionConfig } from "../../../../data/lovelace"; import { HomeAssistant } from "../../../../types"; import { PictureEntityCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import "../../components/hui-action-editor"; import "../../components/hui-entity-editor"; import "../../components/hui-theme-select-editor"; @@ -28,20 +27,21 @@ import { } from "../types"; import { configElementStyle } from "./config-elements-style"; import { computeRTLDirection } from "../../../../common/util/compute_rtl"; +import { assert, object, string, optional, boolean } from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - entity: "string", - image: "string?", - name: "string?", - camera_image: "string?", - camera_view: "string?", - aspect_ratio: "string?", - tap_action: struct.optional(actionConfigStruct), - hold_action: struct.optional(actionConfigStruct), - show_name: "boolean?", - show_state: "boolean?", - theme: "string?", +const cardConfigStruct = object({ + type: string(), + entity: string(), + image: optional(string()), + name: optional(string()), + camera_image: optional(string()), + camera_view: optional(string()), + aspect_ratio: optional(string()), + tap_action: optional(actionConfigStruct), + hold_action: optional(actionConfigStruct), + show_name: optional(boolean()), + show_state: optional(boolean()), + theme: optional(string()), }); const includeDomains = ["camera"]; @@ -54,7 +54,7 @@ export class HuiPictureEntityCardEditor extends LitElement @internalProperty() private _config?: PictureEntityCardConfig; public setConfig(config: PictureEntityCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; } @@ -275,6 +275,7 @@ export class HuiPictureEntityCardEditor extends LitElement } if (target.configValue) { if (value === "") { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { this._config = { diff --git a/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts index f569ed7c8b..1c54bac8e6 100644 --- a/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-picture-glance-card-editor.ts @@ -15,7 +15,6 @@ import "../../../../components/entity/ha-entity-picker"; import { ActionConfig } from "../../../../data/lovelace"; import { HomeAssistant } from "../../../../types"; import { PictureGlanceCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import "../../components/hui-action-editor"; import "../../components/hui-entity-editor"; import "../../components/hui-theme-select-editor"; @@ -29,19 +28,20 @@ import { EntitiesEditorEvent, } from "../types"; import { configElementStyle } from "./config-elements-style"; +import { assert, string, object, optional, array } from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - title: "string?", - entity: "string?", - image: "string?", - camera_image: "string?", - camera_view: "string?", - aspect_ratio: "string?", - tap_action: struct.optional(actionConfigStruct), - hold_action: struct.optional(actionConfigStruct), - entities: [entitiesConfigStruct], - theme: "string?", +const cardConfigStruct = object({ + type: string(), + title: optional(string()), + entity: optional(string()), + image: optional(string()), + camera_image: optional(string()), + camera_view: optional(string()), + aspect_ratio: optional(string()), + tap_action: optional(actionConfigStruct), + hold_action: optional(actionConfigStruct), + entities: array(entitiesConfigStruct), + theme: optional(string()), }); const includeDomains = ["camera"]; @@ -56,7 +56,7 @@ export class HuiPictureGlanceCardEditor extends LitElement @internalProperty() private _configEntities?: EntityConfig[]; public setConfig(config: PictureGlanceCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; this._configEntities = processEditorEntities(config.entities); } @@ -262,6 +262,7 @@ export class HuiPictureGlanceCardEditor extends LitElement } if (value === "") { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { this._config = { diff --git a/src/panels/lovelace/editor/config-elements/hui-plant-status-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-plant-status-card-editor.ts index bbb6fa187d..e24fc10f74 100644 --- a/src/panels/lovelace/editor/config-elements/hui-plant-status-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-plant-status-card-editor.ts @@ -12,17 +12,17 @@ import "../../../../components/entity/ha-entity-picker"; import "../../../../components/ha-icon"; import { HomeAssistant } from "../../../../types"; import { PlantStatusCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import "../../components/hui-theme-select-editor"; import { LovelaceCardEditor } from "../../types"; import { EditorTarget, EntitiesEditorEvent } from "../types"; import { configElementStyle } from "./config-elements-style"; +import { assert, object, string, optional } from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - entity: "string", - name: "string?", - theme: "string?", +const cardConfigStruct = object({ + type: string(), + entity: string(), + name: optional(string()), + theme: optional(string()), }); const includeDomains = ["plant"]; @@ -35,7 +35,7 @@ export class HuiPlantStatusCardEditor extends LitElement @internalProperty() private _config?: PlantStatusCardConfig; public setConfig(config: PlantStatusCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; } @@ -102,6 +102,7 @@ export class HuiPlantStatusCardEditor extends LitElement } if (target.configValue) { if (target.value === "") { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { this._config = { diff --git a/src/panels/lovelace/editor/config-elements/hui-sensor-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-sensor-card-editor.ts index 2fd1614bdc..d42fba0fb6 100644 --- a/src/panels/lovelace/editor/config-elements/hui-sensor-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-sensor-card-editor.ts @@ -16,22 +16,22 @@ import "../../../../components/entity/ha-entity-picker"; import "../../../../components/ha-icon-input"; import { HomeAssistant } from "../../../../types"; import { SensorCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import "../../components/hui-theme-select-editor"; import { LovelaceCardEditor } from "../../types"; import { EditorTarget, EntitiesEditorEvent } from "../types"; import { configElementStyle } from "./config-elements-style"; +import { string, assert, object, optional, number } from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - entity: "string?", - name: "string?", - icon: "string?", - graph: "string?", - unit: "string?", - detail: "number?", - theme: "string?", - hours_to_show: "number?", +const cardConfigStruct = object({ + type: string(), + entity: optional(string()), + name: optional(string()), + icon: optional(string()), + graph: optional(string()), + unit: optional(string()), + detail: optional(number()), + theme: optional(string()), + hours_to_show: optional(number()), }); const includeDomains = ["sensor"]; @@ -44,7 +44,7 @@ export class HuiSensorCardEditor extends LitElement @internalProperty() private _config?: SensorCardConfig; public setConfig(config: SensorCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; } @@ -205,6 +205,7 @@ export class HuiSensorCardEditor extends LitElement target.value === "" || (target.type === "number" && isNaN(Number(target.value))) ) { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { let value: any = target.value; diff --git a/src/panels/lovelace/editor/config-elements/hui-shopping-list-editor.ts b/src/panels/lovelace/editor/config-elements/hui-shopping-list-editor.ts index ac28efea07..f82fb7e9bb 100644 --- a/src/panels/lovelace/editor/config-elements/hui-shopping-list-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-shopping-list-editor.ts @@ -13,15 +13,15 @@ import { isComponentLoaded } from "../../../../common/config/is_component_loaded import { fireEvent } from "../../../../common/dom/fire_event"; import { HomeAssistant } from "../../../../types"; import { ShoppingListCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import "../../components/hui-theme-select-editor"; import { LovelaceCardEditor } from "../../types"; import { EditorTarget, EntitiesEditorEvent } from "../types"; +import { string, assert, object, optional } from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - title: "string?", - theme: "string?", +const cardConfigStruct = object({ + type: string(), + title: optional(string()), + theme: optional(string()), }); @customElement("hui-shopping-list-card-editor") @@ -32,7 +32,7 @@ export class HuiShoppingListEditor extends LitElement @internalProperty() private _config?: ShoppingListCardConfig; public setConfig(config: ShoppingListCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; } @@ -91,6 +91,7 @@ export class HuiShoppingListEditor extends LitElement } if (target.configValue) { if (target.value === "") { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { this._config = { diff --git a/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts index 3f748c1a80..999c18c66e 100644 --- a/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts @@ -16,7 +16,6 @@ import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event"; import { LovelaceConfig } from "../../../../data/lovelace"; import { HomeAssistant } from "../../../../types"; import { StackCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import { LovelaceCardEditor } from "../../types"; import { ConfigChangedEvent, @@ -24,11 +23,12 @@ import { } from "../card-editor/hui-card-editor"; import "../card-editor/hui-card-picker"; import { GUIModeChangedEvent } from "../types"; +import { assert, object, string, array, any, optional } from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - cards: ["any"], - title: "string?", +const cardConfigStruct = object({ + type: string(), + cards: array(any()), + title: optional(string()), }); @customElement("hui-stack-card-editor") @@ -48,8 +48,9 @@ export class HuiStackCardEditor extends LitElement @query("hui-card-editor") private _cardEditorEl?: HuiCardEditor; - public setConfig(config: StackCardConfig): void { - this._config = cardConfigStruct(config); + public setConfig(config: Readonly): void { + assert(config, cardConfigStruct); + this._config = config; } public refreshYamlEditor(focus) { @@ -162,7 +163,9 @@ export class HuiStackCardEditor extends LitElement if (!this._config) { return; } - this._config.cards[this._selectedCard] = ev.detail.config; + const cards = [...this._config.cards]; + cards[this._selectedCard] = ev.detail.config; + this._config = { ...this._config, cards }; this._guiModeAvailable = ev.detail.guiModeAvailable; fireEvent(this, "config-changed", { config: this._config }); } @@ -173,7 +176,8 @@ export class HuiStackCardEditor extends LitElement return; } const config = ev.detail.config; - this._config.cards.push(config); + const cards = [...this._config.cards, config]; + this._config = { ...this._config, cards }; fireEvent(this, "config-changed", { config: this._config }); } @@ -181,7 +185,9 @@ export class HuiStackCardEditor extends LitElement if (!this._config) { return; } - this._config.cards.splice(this._selectedCard, 1); + const cards = [...this._config.cards]; + cards.splice(this._selectedCard, 1); + this._config = { ...this._config, cards }; this._selectedCard = Math.max(0, this._selectedCard - 1); fireEvent(this, "config-changed", { config: this._config }); } @@ -192,8 +198,13 @@ export class HuiStackCardEditor extends LitElement } const source = this._selectedCard; const target = ev.target.id === "move-before" ? source - 1 : source + 1; - const card = this._config.cards.splice(this._selectedCard, 1)[0]; - this._config.cards.splice(target, 0, card); + const cards = [...this._config.cards]; + const card = cards.splice(this._selectedCard, 1)[0]; + cards.splice(target, 0, card); + this._config = { + ...this._config, + cards, + }; this._selectedCard = target; fireEvent(this, "config-changed", { config: this._config }); } diff --git a/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts index fadb869a02..e7cefd2e41 100644 --- a/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts @@ -11,17 +11,17 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/entity/ha-entity-picker"; import { HomeAssistant } from "../../../../types"; import { ThermostatCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import "../../components/hui-theme-select-editor"; import { LovelaceCardEditor } from "../../types"; import { EditorTarget, EntitiesEditorEvent } from "../types"; import { configElementStyle } from "./config-elements-style"; +import { object, string, optional, assert } from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - entity: "string", - name: "string?", - theme: "string?", +const cardConfigStruct = object({ + type: string(), + entity: string(), + name: optional(string()), + theme: optional(string()), }); const includeDomains = ["climate"]; @@ -34,7 +34,7 @@ export class HuiThermostatCardEditor extends LitElement @internalProperty() private _config?: ThermostatCardConfig; public setConfig(config: ThermostatCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; } @@ -102,6 +102,7 @@ export class HuiThermostatCardEditor extends LitElement } if (target.configValue) { if (target.value === "") { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { this._config = { ...this._config, [target.configValue!]: target.value }; diff --git a/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts index cca45d1ed6..8de36e2985 100644 --- a/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts @@ -12,20 +12,20 @@ import "../../../../components/ha-switch"; import "../../../../components/ha-formfield"; import { HomeAssistant } from "../../../../types"; import { WeatherForecastCardConfig } from "../../cards/types"; -import { struct } from "../../common/structs/struct"; import "../../components/hui-theme-select-editor"; import { LovelaceCardEditor } from "../../types"; import { EditorTarget, EntitiesEditorEvent } from "../types"; import { configElementStyle } from "./config-elements-style"; import { computeRTLDirection } from "../../../../common/util/compute_rtl"; +import { object, string, optional, boolean, assert } from "superstruct"; -const cardConfigStruct = struct({ - type: "string", - entity: "string?", - name: "string?", - theme: "string?", - show_forecast: "boolean?", - secondary_info_attribute: "string?", +const cardConfigStruct = object({ + type: string(), + entity: optional(string()), + name: optional(string()), + theme: optional(string()), + show_forecast: optional(boolean()), + secondary_info_attribute: optional(string()), }); const includeDomains = ["weather"]; @@ -38,7 +38,7 @@ export class HuiWeatherForecastCardEditor extends LitElement @internalProperty() private _config?: WeatherForecastCardConfig; public setConfig(config: WeatherForecastCardConfig): void { - config = cardConfigStruct(config); + assert(config, cardConfigStruct); this._config = config; } @@ -139,6 +139,7 @@ export class HuiWeatherForecastCardEditor extends LitElement } if (target.configValue) { if (target.value === "") { + this._config = { ...this._config }; delete this._config[target.configValue!]; } else { this._config = { diff --git a/src/panels/lovelace/editor/gui-support-error.ts b/src/panels/lovelace/editor/gui-support-error.ts new file mode 100644 index 0000000000..ce1aa8ff85 --- /dev/null +++ b/src/panels/lovelace/editor/gui-support-error.ts @@ -0,0 +1,9 @@ +export class GUISupportError extends Error { + public warnings?: string[] = []; + + constructor(message: string, warnings?: string[]) { + super(message); + this.name = "GUISupportError"; + this.warnings = warnings; + } +} diff --git a/src/panels/lovelace/editor/types.ts b/src/panels/lovelace/editor/types.ts index 31a3186d96..04a2755a4b 100644 --- a/src/panels/lovelace/editor/types.ts +++ b/src/panels/lovelace/editor/types.ts @@ -4,8 +4,10 @@ import { LovelaceViewConfig, ShowViewConfig, } from "../../../data/lovelace"; -import { struct } from "../common/structs/struct"; import { EntityConfig } from "../entity-rows/types"; +import { optional, string, object, union } from "superstruct"; +import { EntityId } from "../common/structs/is-entity-id"; +import { Icon } from "../common/structs/is-icon"; export interface YamlChangedEvent extends Event { detail: { @@ -66,19 +68,19 @@ export interface CardPickTarget extends EventTarget { config: LovelaceCardConfig; } -export const actionConfigStruct = struct({ - action: "string", - navigation_path: "string?", - url_path: "string?", - service: "string?", - service_data: "object?", +export const actionConfigStruct = object({ + action: string(), + navigation_path: optional(string()), + url_path: optional(string()), + service: optional(string()), + service_data: optional(object()), }); -export const entitiesConfigStruct = struct.union([ - { - entity: "entity-id", - name: "string?", - icon: "icon?", - }, - "entity-id", +export const entitiesConfigStruct = union([ + object({ + entity: EntityId, + name: optional(string()), + icon: optional(Icon), + }), + EntityId, ]); diff --git a/src/panels/lovelace/header-footer/types.ts b/src/panels/lovelace/header-footer/types.ts index a65c17720c..b0d3e28bea 100644 --- a/src/panels/lovelace/header-footer/types.ts +++ b/src/panels/lovelace/header-footer/types.ts @@ -1,5 +1,5 @@ import { ActionConfig } from "../../../data/lovelace"; -import { struct } from "../common/structs/struct"; +import { object, optional, union, string, number, array } from "superstruct"; import { actionConfigStruct, entitiesConfigStruct } from "../editor/types"; import { EntityConfig } from "../entity-rows/types"; @@ -24,27 +24,27 @@ export interface PictureHeaderFooterConfig extends LovelaceHeaderFooterConfig { double_tap_action?: ActionConfig; } -export const pictureHeaderFooterConfigStruct = struct({ - type: "string", - image: "string", - tap_action: struct.optional(actionConfigStruct), - hold_action: struct.optional(actionConfigStruct), - double_tap_action: struct.optional(actionConfigStruct), +export const pictureHeaderFooterConfigStruct = object({ + type: string(), + image: string(), + tap_action: optional(actionConfigStruct), + hold_action: optional(actionConfigStruct), + double_tap_action: optional(actionConfigStruct), }); -export const buttonsHeaderFooterConfigStruct = struct({ - type: "string", - entities: [entitiesConfigStruct], +export const buttonsHeaderFooterConfigStruct = object({ + type: string(), + entities: array(entitiesConfigStruct), }); -export const graphHeaderFooterConfigStruct = struct({ - type: "string", - entity: "string", - detail: "number?", - hours_to_show: "number?", +export const graphHeaderFooterConfigStruct = object({ + type: string(), + entity: string(), + detail: optional(string()), + hours_to_show: optional(number()), }); -export const headerFooterConfigStructs = struct.union([ +export const headerFooterConfigStructs = union([ pictureHeaderFooterConfigStruct, buttonsHeaderFooterConfigStruct, graphHeaderFooterConfigStruct, diff --git a/src/panels/lovelace/hui-editor.ts b/src/panels/lovelace/hui-editor.ts index c970e07b05..92480bd999 100644 --- a/src/panels/lovelace/hui-editor.ts +++ b/src/panels/lovelace/hui-editor.ts @@ -27,12 +27,12 @@ import { } from "../../dialogs/generic/show-dialog-box"; import { haStyle } from "../../resources/styles"; import type { HomeAssistant } from "../../types"; -import { struct } from "./common/structs/struct"; import type { Lovelace } from "./types"; +import { optional, array, string, object, type, assert } from "superstruct"; -const lovelaceStruct = struct.interface({ - title: "string?", - views: ["object"], +const lovelaceStruct = type({ + title: optional(string()), + views: array(object()), }); @customElement("hui-editor") @@ -251,7 +251,7 @@ class LovelaceFullConfigEditor extends LitElement { return; } try { - config = lovelaceStruct(config); + assert(config, lovelaceStruct); } catch (err) { showAlertDialog(this, { text: this.hass.localize( diff --git a/src/resources/ha-style.ts b/src/resources/ha-style.ts index 40c0c80ff1..ae49b37a86 100644 --- a/src/resources/ha-style.ts +++ b/src/resources/ha-style.ts @@ -34,7 +34,7 @@ documentContainer.innerHTML = ` --scrollbar-thumb-color: rgb(194, 194, 194); --error-color: #db4437; - --warning-color: #f4b400; + --warning-color: #FF9800; --success-color: #0f9d58; --info-color: #4285f4; diff --git a/yarn.lock b/yarn.lock index e08f6f02ea..5fb36d4512 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4411,16 +4411,6 @@ clone-buffer@^1.0.0: resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= -clone-deep@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713" - integrity sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ== - dependencies: - for-own "^1.0.0" - is-plain-object "^2.0.4" - kind-of "^6.0.0" - shallow-clone "^1.0.0" - clone-stats@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" @@ -6163,11 +6153,6 @@ follow-redirects@^1.0.0: dependencies: debug "^3.2.6" -for-in@^0.1.3: - version "0.1.8" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" - integrity sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE= - for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -7706,7 +7691,7 @@ kind-of@^5.0.0, kind-of@^5.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== -kind-of@^6.0.0, kind-of@^6.0.1, kind-of@^6.0.2: +kind-of@^6.0.0, kind-of@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== @@ -8628,14 +8613,6 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mixin-object@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" - integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4= - dependencies: - for-in "^0.1.3" - is-extendable "^0.1.1" - mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -10785,15 +10762,6 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" -shallow-clone@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571" - integrity sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA== - dependencies: - is-extendable "^0.1.1" - kind-of "^5.0.0" - mixin-object "^2.0.1" - shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -11300,13 +11268,10 @@ subarg@^1.0.0: dependencies: minimist "^1.1.0" -superstruct@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.6.1.tgz#148fc3d627bb59fcfe24aa1bd2a1b8c51b1db072" - integrity sha512-LDbOKL5sNbOJ00Q36iYRhSexKIptZje0/mhNznnz04wT9CmsPDZg/K/UV1dgYuCwNMuOBHTbVROZsGB9EhhK4w== - dependencies: - clone-deep "^2.0.1" - kind-of "^6.0.1" +superstruct@^0.10.12: + version "0.10.12" + resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.10.12.tgz#7b2c8adaf61b75257265eac3b588f30017f996f0" + integrity sha512-FiNhfegyytDI0QxrrEoeGknFM28SnoHqCBpkWewUm8jRNj74NVxLpiiePvkOo41Ze/aKMSHa/twWjNF81mKaQQ== supports-color@6.0.0: version "6.0.0"