From 4973d8f629c3619f7e113b6fb6888aaf3027ede6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 18 Nov 2020 12:19:59 +0100 Subject: [PATCH] WIP initial Blueprint UI (#7695) * WIP initial Blueprint UI * Review comments * localize --- src/components/ha-blueprint-picker.ts | 119 ++++++ src/data/automation.ts | 15 +- src/data/blueprint.ts | 54 +++ .../automation/blueprint-automation-editor.ts | 306 +++++++++++++++ .../automation/dialog-new-automation.ts | 175 +++++++++ .../config/automation/ha-automation-editor.ts | 309 ++-------------- .../config/automation/ha-automation-picker.ts | 26 +- .../automation/manual-automation-editor.ts | 349 ++++++++++++++++++ .../automation/show-dialog-new-automation.ts | 12 + .../automation/thingtalk/dialog-thingtalk.ts | 14 +- .../{ => thingtalk}/show-dialog-thingtalk.ts | 9 +- .../blueprint/dialog-import-blueprint.ts | 186 ++++++++++ .../config/blueprint/ha-blueprint-overview.ts | 212 +++++++++++ .../config/blueprint/ha-config-blueprint.ts | 76 ++++ .../blueprint/show-dialog-import-blueprint.ts | 17 + src/panels/config/ha-panel-config.ts | 16 + src/translations/en.json | 51 +++ 17 files changed, 1645 insertions(+), 301 deletions(-) create mode 100644 src/components/ha-blueprint-picker.ts create mode 100644 src/data/blueprint.ts create mode 100644 src/panels/config/automation/blueprint-automation-editor.ts create mode 100644 src/panels/config/automation/dialog-new-automation.ts create mode 100644 src/panels/config/automation/manual-automation-editor.ts create mode 100644 src/panels/config/automation/show-dialog-new-automation.ts rename src/panels/config/automation/{ => thingtalk}/show-dialog-thingtalk.ts (65%) create mode 100644 src/panels/config/blueprint/dialog-import-blueprint.ts create mode 100644 src/panels/config/blueprint/ha-blueprint-overview.ts create mode 100644 src/panels/config/blueprint/ha-config-blueprint.ts create mode 100644 src/panels/config/blueprint/show-dialog-import-blueprint.ts diff --git a/src/components/ha-blueprint-picker.ts b/src/components/ha-blueprint-picker.ts new file mode 100644 index 0000000000..e5ee91fb9d --- /dev/null +++ b/src/components/ha-blueprint-picker.ts @@ -0,0 +1,119 @@ +import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-listbox/paper-listbox"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../common/dom/fire_event"; +import { compare } from "../common/string/compare"; +import { Blueprints, fetchBlueprints } from "../data/blueprint"; +import { HomeAssistant } from "../types"; + +@customElement("ha-blueprint-picker") +class HaBluePrintPicker extends LitElement { + public hass?: HomeAssistant; + + @property() public label?: string; + + @property() public value = ""; + + @property() public domain = "automation"; + + @property() public blueprints?: Blueprints; + + @property({ type: Boolean }) public disabled = false; + + private _processedBlueprints = memoizeOne((blueprints?: Blueprints) => { + if (!blueprints) { + return []; + } + const result = Object.entries(blueprints).map(([path, blueprint]) => ({ + ...blueprint.metadata, + path, + })); + return result.sort((a, b) => compare(a.name, b.name)); + }); + + protected render(): TemplateResult { + if (!this.hass) { + return html``; + } + return html` + + + + ${this.hass.localize( + "ui.components.blueprint-picker.select_blueprint" + )} + + ${this._processedBlueprints(this.blueprints).map( + (blueprint) => html` + + ${blueprint.name} + + ` + )} + + + `; + } + + protected firstUpdated(changedProps) { + super.firstUpdated(changedProps); + if (this.blueprints === undefined) { + fetchBlueprints(this.hass!, this.domain).then((blueprints) => { + this.blueprints = blueprints; + }); + } + } + + private _blueprintChanged(ev) { + const newValue = ev.detail.item.dataset.blueprintPath; + + if (newValue !== this.value) { + this.value = ev.detail.value; + setTimeout(() => { + fireEvent(this, "value-changed", { value: newValue }); + fireEvent(this, "change"); + }, 0); + } + } + + static get styles(): CSSResult { + return css` + :host { + display: inline-block; + } + paper-dropdown-menu-light { + width: 100%; + min-width: 200px; + display: block; + } + paper-listbox { + min-width: 200px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-blueprint-picker": HaBluePrintPicker; + } +} diff --git a/src/data/automation.ts b/src/data/automation.ts index bb35448575..88ee3f847c 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -4,6 +4,7 @@ import { } from "home-assistant-js-websocket"; import { navigate } from "../common/navigate"; import { Context, HomeAssistant } from "../types"; +import { BlueprintInput } from "./blueprint"; import { DeviceCondition, DeviceTrigger } from "./device_automation"; import { Action } from "./script"; @@ -14,10 +15,14 @@ export interface AutomationEntity extends HassEntityBase { }; } -export interface AutomationConfig { +export type AutomationConfig = + | ManualAutomationConfig + | BlueprintAutomationConfig; + +export interface ManualAutomationConfig { id?: string; - alias: string; - description: string; + alias?: string; + description?: string; trigger: Trigger[]; condition?: Condition[]; action: Action[]; @@ -25,6 +30,10 @@ export interface AutomationConfig { max?: number; } +export interface BlueprintAutomationConfig extends ManualAutomationConfig { + use_blueprint: { path: string; input?: BlueprintInput }; +} + export interface ForDict { hours?: number | string; minutes?: number | string; diff --git a/src/data/blueprint.ts b/src/data/blueprint.ts new file mode 100644 index 0000000000..b2dd307592 --- /dev/null +++ b/src/data/blueprint.ts @@ -0,0 +1,54 @@ +import { HomeAssistant } from "../types"; + +export type Blueprints = Record; + +export interface Blueprint { + metadata: BlueprintMetaData; +} + +export interface BlueprintMetaData { + domain: string; + name: string; + input: BlueprintInput; +} + +export type BlueprintInput = Record; + +export interface BlueprintImportResult { + url: string; + suggested_filename: string; + raw_data: string; + blueprint: Blueprint; +} + +export const fetchBlueprints = (hass: HomeAssistant, domain: string) => + hass.callWS({ type: "blueprint/list", domain }); + +export const importBlueprint = (hass: HomeAssistant, url: string) => + hass.callWS({ type: "blueprint/import", url }); + +export const saveBlueprint = ( + hass: HomeAssistant, + domain: string, + path: string, + yaml: string, + source_url?: string +) => + hass.callWS({ + type: "blueprint/save", + domain, + path, + yaml, + source_url, + }); + +export const deleteBlueprint = ( + hass: HomeAssistant, + domain: string, + path: string +) => + hass.callWS({ + type: "blueprint/delete", + domain, + path, + }); diff --git a/src/panels/config/automation/blueprint-automation-editor.ts b/src/panels/config/automation/blueprint-automation-editor.ts new file mode 100644 index 0000000000..78bcf332fd --- /dev/null +++ b/src/panels/config/automation/blueprint-automation-editor.ts @@ -0,0 +1,306 @@ +import { + css, + CSSResult, + customElement, + internalProperty, + LitElement, + property, +} from "lit-element"; +import { html } from "lit-html"; +import { + BlueprintAutomationConfig, + triggerAutomation, +} from "../../../data/automation"; +import { HomeAssistant } from "../../../types"; +import "../ha-config-section"; +import "../../../components/ha-card"; +import "@polymer/paper-input/paper-textarea"; +import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; +import "../../../components/entity/ha-entity-toggle"; +import "@material/mwc-button/mwc-button"; +import "./trigger/ha-automation-trigger"; +import "./condition/ha-automation-condition"; +import "./action/ha-automation-action"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { haStyle } from "../../../resources/styles"; +import { HassEntity } from "home-assistant-js-websocket"; +import { navigate } from "../../../common/navigate"; +import { + Blueprint, + Blueprints, + fetchBlueprints, +} from "../../../data/blueprint"; +import "../../../components/ha-blueprint-picker"; +import "../../../components/ha-circular-progress"; + +@customElement("blueprint-automation-editor") +export class HaBlueprintAutomationEditor extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public isWide!: boolean; + + @property() public narrow!: boolean; + + @property() public config!: BlueprintAutomationConfig; + + @property() public stateObj?: HassEntity; + + @internalProperty() private _blueprints?: Blueprints; + + protected firstUpdated(changedProps) { + super.firstUpdated(changedProps); + this._getBlueprints(); + } + + private get _blueprint(): Blueprint | undefined { + if (!this._blueprints) { + return undefined; + } + return this._blueprints[this.config.use_blueprint.path]; + } + + protected render() { + const blueprint = this._blueprint; + return html` + ${!this.narrow + ? html` ${this.config.alias} ` + : ""} + + ${this.hass.localize( + "ui.panel.config.automation.editor.introduction" + )} + + +
+ + + +
+ ${this.stateObj + ? html` +
+
+ + ${this.hass.localize( + "ui.panel.config.automation.editor.enable_disable" + )} +
+ + ${this.hass.localize("ui.card.automation.trigger")} + +
+ ` + : ""} +
+
+ + + ${this.hass.localize( + "ui.panel.config.automation.editor.blueprint.header" + )} + +
+
+ ${this._blueprints + ? Object.keys(this._blueprints).length + ? html` + + ` + : this.hass.localize( + "ui.panel.config.automation.editor.blueprint.no_blueprints" + ) + : html``} + + ${this.hass.localize( + "ui.panel.config.automation.editor.blueprint.manage_blueprints" + )} + +
+ ${this.config.use_blueprint.path + ? blueprint?.metadata?.input && + Object.keys(blueprint.metadata.input).length + ? html`

+ ${this.hass.localize( + "ui.panel.config.automation.editor.blueprint.inputs" + )} +

+ ${Object.entries(blueprint.metadata.input).map( + ([key, value]) => + html`
+ ${value?.description} + +
` + )}` + : this.hass.localize( + "ui.panel.config.automation.editor.blueprint.no_inputs" + ) + : ""} +
+
+
`; + } + + private async _getBlueprints() { + this._blueprints = await fetchBlueprints(this.hass, "automation"); + } + + private _excuteAutomation(ev: Event) { + triggerAutomation(this.hass, (ev.target as any).stateObj.entity_id); + } + + private _blueprintChanged(ev) { + ev.stopPropagation(); + if (this.config.use_blueprint.path === ev.detail.value) { + return; + } + fireEvent(this, "value-changed", { + value: { + ...this.config!, + use_blueprint: { + path: ev.detail.value, + }, + }, + }); + } + + private _inputChanged(ev) { + ev.stopPropagation(); + const target = ev.target as any; + const key = target.key; + const value = target.value; + if ( + (this.config.use_blueprint.input && + this.config.use_blueprint.input[key] === value) || + (!this.config.use_blueprint.input && value === "") + ) { + return; + } + fireEvent(this, "value-changed", { + value: { + ...this.config!, + use_blueprint: { + ...this.config.use_blueprint, + input: { ...this.config.use_blueprint.input, [key]: value }, + }, + }, + }); + } + + private _valueChanged(ev: CustomEvent) { + ev.stopPropagation(); + const target = ev.target as any; + const name = target.name; + if (!name) { + return; + } + let newVal = ev.detail.value; + if (target.type === "number") { + newVal = Number(newVal); + } + if ((this.config![name] || "") === newVal) { + return; + } + fireEvent(this, "value-changed", { + value: { ...this.config!, [name]: newVal }, + }); + } + + private _navigateBlueprints() { + navigate(this, "/config/blueprint"); + } + + static get styles(): CSSResult[] { + return [ + haStyle, + css` + ha-card { + overflow: hidden; + } + .errors { + padding: 20px; + font-weight: bold; + color: var(--error-color); + } + .content { + padding-bottom: 20px; + } + .blueprint-picker-container { + display: flex; + align-items: center; + justify-content: space-between; + } + h3 { + margin-top: 16px; + } + span[slot="introduction"] a { + color: var(--primary-color); + } + p { + margin-bottom: 0; + } + ha-entity-toggle { + margin-right: 8px; + } + mwc-fab { + position: relative; + bottom: calc(-80px - env(safe-area-inset-bottom)); + transition: bottom 0.3s; + } + mwc-fab.dirty { + bottom: 0; + } + .selected_menu_item { + color: var(--primary-color); + } + li[role="separator"] { + border-bottom-color: var(--divider-color); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "blueprint-automation-editor": HaBlueprintAutomationEditor; + } +} diff --git a/src/panels/config/automation/dialog-new-automation.ts b/src/panels/config/automation/dialog-new-automation.ts new file mode 100644 index 0000000000..cf9a5c965d --- /dev/null +++ b/src/panels/config/automation/dialog-new-automation.ts @@ -0,0 +1,175 @@ +import "@material/mwc-button"; +import "../../../components/ha-circular-progress"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + internalProperty, + TemplateResult, +} from "lit-element"; +import "../../../components/ha-dialog"; +import { haStyle, haStyleDialog } from "../../../resources/styles"; +import type { HomeAssistant } from "../../../types"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; +import { + AutomationConfig, + showAutomationEditor, +} from "../../../data/automation"; +import { showThingtalkDialog } from "./thingtalk/show-dialog-thingtalk"; +import "../../../components/ha-card"; +import "../../../components/ha-blueprint-picker"; + +@customElement("ha-dialog-new-automation") +class DialogNewAutomation extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @internalProperty() private _opened = false; + + public showDialog(): void { + this._opened = true; + } + + public closeDialog(): void { + this._opened = false; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._opened) { + return html``; + } + return html` + +
+ ${this.hass.localize("ui.panel.config.automation.dialog_new.how")} +
+ ${isComponentLoaded(this.hass, "cloud") + ? html` +
+

+ ${this.hass.localize( + "ui.panel.config.automation.dialog_new.thingtalk.header" + )} +

+ ${this.hass.localize( + "ui.panel.config.automation.dialog_new.thingtalk.intro" + )} +
+ + ${this.hass.localize( + "ui.panel.config.automation.dialog_new.thingtalk.create" + )} +
+
+
` + : html``} + ${isComponentLoaded(this.hass, "blueprint") + ? html` +
+

+ ${this.hass.localize( + "ui.panel.config.automation.dialog_new.blueprint.use_blueprint" + )} +

+ +
+
` + : html``} +
+
+ + ${this.hass.localize( + "ui.panel.config.automation.dialog_new.start_empty" + )} + +
+ `; + } + + private _thingTalk() { + this.closeDialog(); + showThingtalkDialog(this, { + callback: (config: Partial | undefined) => + showAutomationEditor(this, config), + input: this.shadowRoot!.querySelector("paper-input")!.value as string, + }); + } + + private _blueprintPicked(ev: CustomEvent) { + showAutomationEditor(this, { use_blueprint: { path: ev.detail.value } }); + this.closeDialog(); + } + + private _blank() { + showAutomationEditor(this); + this.closeDialog(); + } + + static get styles(): CSSResult[] { + return [ + haStyle, + haStyleDialog, + css` + .container { + display: flex; + } + ha-card { + width: calc(50% - 8px); + margin: 4px; + } + ha-card div { + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + } + ha-card { + box-sizing: border-box; + padding: 8px; + } + ha-blueprint-picker { + width: 100%; + } + .side-by-side { + display: flex; + flex-direction: row; + align-items: flex-end; + } + @media all and (max-width: 500px) { + .container { + flex-direction: column; + } + ha-card { + width: 100%; + } + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-dialog-new-automation": DialogNewAutomation; + } +} diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index 47d8160e86..e6b785e01f 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -12,7 +12,6 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; import "@polymer/paper-input/paper-textarea"; import "@material/mwc-list/mwc-list-item"; import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; -import { PaperListboxElement } from "@polymer/paper-listbox"; import { css, CSSResult, @@ -36,14 +35,11 @@ import type { HaYamlEditor } from "../../../components/ha-yaml-editor"; import { AutomationConfig, AutomationEntity, - Condition, deleteAutomation, getAutomationEditorInitData, showAutomationEditor, - Trigger, triggerAutomation, } from "../../../data/automation"; -import { Action } from "../../../data/script"; import { showAlertDialog, showConfirmationDialog, @@ -53,7 +49,6 @@ import "../../../layouts/hass-tabs-subpage"; import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; -import { documentationUrl } from "../../../util/documentation-url"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; import "./action/ha-automation-action"; @@ -61,11 +56,13 @@ import { HaDeviceAction } from "./action/types/ha-automation-action-device_id"; import "./condition/ha-automation-condition"; import "./trigger/ha-automation-trigger"; import { HaDeviceTrigger } from "./trigger/types/ha-automation-trigger-device"; - -const MODES = ["single", "restart", "queued", "parallel"]; -const MODES_MAX = ["queued", "parallel"]; +import "./manual-automation-editor"; +import "./blueprint-automation-editor"; declare global { + interface HTMLElementTagNameMap { + "ha-automation-editor": HaAutomationEditor; + } // for fire event interface HASSDomEvents { "ui-mode-not-available": Error; @@ -193,6 +190,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { + ${this._config ? html` ${this.narrow @@ -204,217 +202,19 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { : ""} ${this._mode === "gui" ? html` - - ${!this.narrow - ? html` - ${this._config.alias} - ` - : ""} - - ${this.hass.localize( - "ui.panel.config.automation.editor.introduction" - )} - - -
- - - -

- ${this.hass.localize( - "ui.panel.config.automation.editor.modes.description", - "documentation_link", - html`${this.hass.localize( - "ui.panel.config.automation.editor.modes.documentation" - )}` - )} -

- - - ${MODES.map( - (mode) => html` - - ${this.hass.localize( - `ui.panel.config.automation.editor.modes.${mode}` - ) || mode} - - ` - )} - - - ${this._config.mode && - MODES_MAX.includes(this._config.mode) - ? html` - ` - : html``} -
- ${stateObj - ? html` -
-
- - ${this.hass.localize( - "ui.panel.config.automation.editor.enable_disable" - )} -
- - ${this.hass.localize( - "ui.card.automation.trigger" - )} - -
- ` - : ""} -
-
- - - - ${this.hass.localize( - "ui.panel.config.automation.editor.triggers.header" - )} - - -

- ${this.hass.localize( - "ui.panel.config.automation.editor.triggers.introduction" - )} -

- - ${this.hass.localize( - "ui.panel.config.automation.editor.triggers.learn_more" - )} - -
- -
- - - - ${this.hass.localize( - "ui.panel.config.automation.editor.conditions.header" - )} - - -

- ${this.hass.localize( - "ui.panel.config.automation.editor.conditions.introduction" - )} -

- - ${this.hass.localize( - "ui.panel.config.automation.editor.conditions.learn_more" - )} - -
- -
- - - - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.header" - )} - - -

- ${this.hass.localize( - "ui.panel.config.automation.editor.actions.introduction" - )} -

- - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.learn_more" - )} - -
- -
+ ${"use_blueprint" in this._config + ? html`` + : html``} ` : this._mode === "yaml" ? html` @@ -531,17 +331,25 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { if (changedProps.has("automationId") && !this.automationId && this.hass) { const initData = getAutomationEditorInitData(); - this._dirty = !!initData; - this._config = { + let baseConfig: Partial = { alias: this.hass.localize( "ui.panel.config.automation.editor.default_name" ), description: "", - trigger: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }], - condition: [], - action: [{ ...HaDeviceAction.defaultConfig }], - ...initData, }; + if (!initData || !("use_blueprint" in initData)) { + baseConfig = { + ...baseConfig, + mode: "single", + trigger: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }], + condition: [], + action: [{ ...HaDeviceAction.defaultConfig }], + }; + } + this._config = { + ...baseConfig, + ...initData, + } as AutomationConfig; } if ( @@ -560,58 +368,11 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { this._entityId = automation?.entity_id; } - private _modeChanged(ev: CustomEvent) { - const mode = ((ev.target as PaperListboxElement)?.selectedItem as any) - ?.mode; - - if (mode === this._config!.mode) { - return; - } - - this._config = { ...this._config!, mode }; - if (!MODES_MAX.includes(mode)) { - delete this._config.max; - } - this._dirty = true; - } - - private _valueChanged(ev: CustomEvent) { + private _valueChanged(ev: CustomEvent<{ value: AutomationConfig }>) { ev.stopPropagation(); - const target = ev.target as any; - const name = target.name; - if (!name) { - return; - } - let newVal = ev.detail.value; - if (target.type === "number") { - newVal = Number(newVal); - } - if ((this._config![name] || "") === newVal) { - return; - } - this._config = { ...this._config!, [name]: newVal }; + this._config = ev.detail.value; this._dirty = true; - } - - private _triggerChanged(ev: CustomEvent): void { - this._config = { ...this._config!, trigger: ev.detail.value as Trigger[] }; this._errors = undefined; - this._dirty = true; - } - - private _conditionChanged(ev: CustomEvent): void { - this._config = { - ...this._config!, - condition: ev.detail.value as Condition[], - }; - this._errors = undefined; - this._dirty = true; - } - - private _actionChanged(ev: CustomEvent): void { - this._config = { ...this._config!, action: ev.detail.value as Action[] }; - this._errors = undefined; - this._dirty = true; } private _excuteAutomation(ev: Event) { diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index 9c4acf92cc..1b72642178 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -12,7 +12,6 @@ import { } from "lit-element"; import { ifDefined } from "lit-html/directives/if-defined"; import memoizeOne from "memoize-one"; -import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { formatDateTime } from "../../../common/datetime/format_date_time"; import { fireEvent } from "../../../common/dom/fire_event"; import { computeStateName } from "../../../common/entity/compute_state_name"; @@ -20,19 +19,16 @@ import { DataTableColumnContainer } from "../../../components/data-table/ha-data import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../components/entity/ha-entity-toggle"; import "../../../components/ha-svg-icon"; -import { - AutomationConfig, - AutomationEntity, - showAutomationEditor, - triggerAutomation, -} from "../../../data/automation"; +import { AutomationEntity, triggerAutomation } from "../../../data/automation"; import { UNAVAILABLE_STATES } from "../../../data/entity"; import "../../../layouts/hass-tabs-subpage-data-table"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; import { configSections } from "../ha-panel-config"; -import { showThingtalkDialog } from "./show-dialog-thingtalk"; import { documentationUrl } from "../../../util/documentation-url"; +import { showNewAutomationDialog } from "./show-dialog-new-automation"; +import { navigate } from "../../../common/navigate"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; @customElement("ha-automation-picker") class HaAutomationPicker extends LitElement { @@ -220,14 +216,14 @@ class HaAutomationPicker extends LitElement { } private _createNew() { - if (!isComponentLoaded(this.hass, "cloud")) { - showAutomationEditor(this); - return; + if ( + isComponentLoaded(this.hass, "cloud") || + isComponentLoaded(this.hass, "blueprint") + ) { + showNewAutomationDialog(this); + } else { + navigate(this, "/config/automation/edit/new"); } - showThingtalkDialog(this, { - callback: (config: Partial | undefined) => - showAutomationEditor(this, config), - }); } static get styles(): CSSResult { diff --git a/src/panels/config/automation/manual-automation-editor.ts b/src/panels/config/automation/manual-automation-editor.ts new file mode 100644 index 0000000000..addd7f25f2 --- /dev/null +++ b/src/panels/config/automation/manual-automation-editor.ts @@ -0,0 +1,349 @@ +import { + css, + CSSResult, + customElement, + LitElement, + property, +} from "lit-element"; +import { html } from "lit-html"; +import { + Condition, + ManualAutomationConfig, + Trigger, + triggerAutomation, +} from "../../../data/automation"; +import { Action, MODES, MODES_MAX } from "../../../data/script"; +import { HomeAssistant } from "../../../types"; +import { documentationUrl } from "../../../util/documentation-url"; +import "../ha-config-section"; +import "../../../components/ha-card"; +import "@polymer/paper-input/paper-textarea"; +import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; +import "../../../components/entity/ha-entity-toggle"; +import "@material/mwc-button/mwc-button"; +import "./trigger/ha-automation-trigger"; +import "./condition/ha-automation-condition"; +import "./action/ha-automation-action"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { PaperListboxElement } from "@polymer/paper-listbox"; +import { haStyle } from "../../../resources/styles"; +import { HassEntity } from "home-assistant-js-websocket"; + +@customElement("manual-automation-editor") +export class HaManualAutomationEditor extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public isWide!: boolean; + + @property() public narrow!: boolean; + + @property() public config!: ManualAutomationConfig; + + @property() public stateObj?: HassEntity; + + protected render() { + return html` + ${!this.narrow + ? html` ${this.config.alias} ` + : ""} + + ${this.hass.localize( + "ui.panel.config.automation.editor.introduction" + )} + + +
+ + + +

+ ${this.hass.localize( + "ui.panel.config.automation.editor.modes.description", + "documentation_link", + html`${this.hass.localize( + "ui.panel.config.automation.editor.modes.documentation" + )}` + )} +

+ + + ${MODES.map( + (mode) => html` + + ${this.hass.localize( + `ui.panel.config.automation.editor.modes.${mode}` + ) || mode} + + ` + )} + + + ${this.config.mode && MODES_MAX.includes(this.config.mode) + ? html` + ` + : html``} +
+ ${this.stateObj + ? html` +
+
+ + ${this.hass.localize( + "ui.panel.config.automation.editor.enable_disable" + )} +
+ + ${this.hass.localize("ui.card.automation.trigger")} + +
+ ` + : ""} +
+
+ + + + ${this.hass.localize( + "ui.panel.config.automation.editor.triggers.header" + )} + + +

+ ${this.hass.localize( + "ui.panel.config.automation.editor.triggers.introduction" + )} +

+ + ${this.hass.localize( + "ui.panel.config.automation.editor.triggers.learn_more" + )} + +
+ +
+ + + + ${this.hass.localize( + "ui.panel.config.automation.editor.conditions.header" + )} + + +

+ ${this.hass.localize( + "ui.panel.config.automation.editor.conditions.introduction" + )} +

+ + ${this.hass.localize( + "ui.panel.config.automation.editor.conditions.learn_more" + )} + +
+ +
+ + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.header" + )} + + +

+ ${this.hass.localize( + "ui.panel.config.automation.editor.actions.introduction" + )} +

+ + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.learn_more" + )} + +
+ +
`; + } + + private _excuteAutomation(ev: Event) { + triggerAutomation(this.hass, (ev.target as any).stateObj.entity_id); + } + + private _valueChanged(ev: CustomEvent) { + ev.stopPropagation(); + const target = ev.target as any; + const name = target.name; + if (!name) { + return; + } + let newVal = ev.detail.value; + if (target.type === "number") { + newVal = Number(newVal); + } + if ((this.config![name] || "") === newVal) { + return; + } + fireEvent(this, "value-changed", { + value: { ...this.config!, [name]: newVal }, + }); + } + + private _modeChanged(ev: CustomEvent) { + const mode = ((ev.target as PaperListboxElement)?.selectedItem as any) + ?.mode; + + if (mode === this.config!.mode) { + return; + } + fireEvent(this, "value-changed", { + value: { + ...this.config!, + mode, + max: !MODES_MAX.includes(mode) ? undefined : this.config.max, + }, + }); + } + + private _triggerChanged(ev: CustomEvent): void { + fireEvent(this, "value-changed", { + value: { ...this.config!, trigger: ev.detail.value as Trigger[] }, + }); + } + + private _conditionChanged(ev: CustomEvent): void { + fireEvent(this, "value-changed", { + value: { + ...this.config!, + condition: ev.detail.value as Condition[], + }, + }); + } + + private _actionChanged(ev: CustomEvent): void { + fireEvent(this, "value-changed", { + value: { ...this.config!, action: ev.detail.value as Action[] }, + }); + } + + static get styles(): CSSResult[] { + return [ + haStyle, + css` + ha-card { + overflow: hidden; + } + .errors { + padding: 20px; + font-weight: bold; + color: var(--error-color); + } + .content { + padding-bottom: 20px; + } + span[slot="introduction"] a { + color: var(--primary-color); + } + p { + margin-bottom: 0; + } + ha-entity-toggle { + margin-right: 8px; + } + mwc-fab { + position: relative; + bottom: calc(-80px - env(safe-area-inset-bottom)); + transition: bottom 0.3s; + } + mwc-fab.dirty { + bottom: 0; + } + .selected_menu_item { + color: var(--primary-color); + } + li[role="separator"] { + border-bottom-color: var(--divider-color); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "manual-automation-editor": HaManualAutomationEditor; + } +} diff --git a/src/panels/config/automation/show-dialog-new-automation.ts b/src/panels/config/automation/show-dialog-new-automation.ts new file mode 100644 index 0000000000..6d39eab972 --- /dev/null +++ b/src/panels/config/automation/show-dialog-new-automation.ts @@ -0,0 +1,12 @@ +import { fireEvent } from "../../../common/dom/fire_event"; + +export const loadNewAutomationDialog = () => + import(/* webpackChunkName: "thingtalk-dialog" */ "./dialog-new-automation"); + +export const showNewAutomationDialog = (element: HTMLElement): void => { + fireEvent(element, "show-dialog", { + dialogTag: "ha-dialog-new-automation", + dialogImport: loadNewAutomationDialog, + dialogParams: {}, + }); +}; diff --git a/src/panels/config/automation/thingtalk/dialog-thingtalk.ts b/src/panels/config/automation/thingtalk/dialog-thingtalk.ts index 6659c03ac8..1e19024d0b 100644 --- a/src/panels/config/automation/thingtalk/dialog-thingtalk.ts +++ b/src/panels/config/automation/thingtalk/dialog-thingtalk.ts @@ -20,7 +20,7 @@ import { convertThingTalk } from "../../../../data/cloud"; import type { PolymerChangedEvent } from "../../../../polymer-types"; import { haStyle, haStyleDialog } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; -import type { ThingtalkDialogParams } from "../show-dialog-thingtalk"; +import type { ThingtalkDialogParams } from "./show-dialog-thingtalk"; import "./ha-thingtalk-placeholders"; import type { PlaceholderValues } from "./ha-thingtalk-placeholders"; @@ -50,16 +50,21 @@ class DialogThingtalk extends LitElement { @internalProperty() private _placeholders?: PlaceholderContainer; - @query("#input", true) private _input?: PaperInputElement; + @query("#input") private _input?: PaperInputElement; - private _value!: string; + private _value?: string; private _config!: Partial; - public showDialog(params: ThingtalkDialogParams): void { + public async showDialog(params: ThingtalkDialogParams): Promise { this._params = params; this._error = undefined; this._opened = true; + if (params.input) { + this._value = params.input; + await this.updateComplete; + this._generate(); + } } protected render(): TemplateResult { @@ -126,6 +131,7 @@ class DialogThingtalk extends LitElement { diff --git a/src/panels/config/automation/show-dialog-thingtalk.ts b/src/panels/config/automation/thingtalk/show-dialog-thingtalk.ts similarity index 65% rename from src/panels/config/automation/show-dialog-thingtalk.ts rename to src/panels/config/automation/thingtalk/show-dialog-thingtalk.ts index 1c051a3bec..4452cae671 100644 --- a/src/panels/config/automation/show-dialog-thingtalk.ts +++ b/src/panels/config/automation/thingtalk/show-dialog-thingtalk.ts @@ -1,14 +1,13 @@ -import { fireEvent } from "../../../common/dom/fire_event"; -import { AutomationConfig } from "../../../data/automation"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { AutomationConfig } from "../../../../data/automation"; export interface ThingtalkDialogParams { callback: (config: Partial | undefined) => void; + input?: string; } export const loadThingtalkDialog = () => - import( - /* webpackChunkName: "thingtalk-dialog" */ "./thingtalk/dialog-thingtalk" - ); + import(/* webpackChunkName: "thingtalk-dialog" */ "./dialog-thingtalk"); export const showThingtalkDialog = ( element: HTMLElement, diff --git a/src/panels/config/blueprint/dialog-import-blueprint.ts b/src/panels/config/blueprint/dialog-import-blueprint.ts new file mode 100644 index 0000000000..cc257b8179 --- /dev/null +++ b/src/panels/config/blueprint/dialog-import-blueprint.ts @@ -0,0 +1,186 @@ +import "@material/mwc-button"; +import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; +import "@polymer/paper-input/paper-input"; +import type { PaperInputElement } from "@polymer/paper-input/paper-input"; +import "../../../components/ha-circular-progress"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + internalProperty, + query, + TemplateResult, +} from "lit-element"; +import "../../../components/ha-dialog"; +import { haStyleDialog } from "../../../resources/styles"; +import type { HomeAssistant } from "../../../types"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { + BlueprintImportResult, + importBlueprint, + saveBlueprint, +} from "../../../data/blueprint"; + +@customElement("ha-dialog-import-blueprint") +class DialogImportBlueprint extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @internalProperty() private _params?; + + @internalProperty() private _importing = false; + + @internalProperty() private _saving = false; + + @internalProperty() private _error?: string; + + @internalProperty() private _result?: BlueprintImportResult; + + @query("#input") private _input?: PaperInputElement; + + public showDialog(params): void { + this._params = params; + this._error = undefined; + } + + public closeDialog(): void { + this._error = undefined; + this._result = undefined; + this._params = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._params) { + return html``; + } + return html` + +
+ ${this._error ? html`
${this._error}
` : ""} + ${this._result + ? html`${this.hass.localize( + "ui.panel.config.blueprint.add.import", + "name", + "domain", + html`${this._result.blueprint.metadata.name}`, + this._result.blueprint.metadata.domain + )} + +
${this._result.raw_data}
` + : html`${this.hass.localize( + "ui.panel.config.blueprint.add.import_introduction" + )}`} +
+ ${!this._result + ? html` + ${this._importing + ? html`` + : ""} + ${this.hass.localize("ui.panel.config.blueprint.add.import_btn")} + ` + : html` + ${this.hass.localize("ui.common.cancel")} + + + ${this._saving + ? html`` + : ""} + ${this.hass.localize("ui.panel.config.blueprint.add.save_btn")} + `} +
+ `; + } + + private async _import() { + this._importing = true; + this._error = undefined; + try { + const url = this._input?.value; + if (!url) { + this._error = this.hass.localize( + "ui.panel.config.blueprint.add.error_no_url" + ); + return; + } + this._result = await importBlueprint(this.hass, url); + } catch (e) { + this._error = e.message; + } finally { + this._importing = false; + } + } + + private async _save() { + this._saving = true; + try { + const filename = this._input?.value; + if (!filename) { + return; + } + await saveBlueprint( + this.hass, + this._result!.blueprint.metadata.domain, + filename, + this._result!.raw_data, + this._result!.url + ); + this._params.importedCallback(); + this.closeDialog(); + } catch (e) { + this._error = e.message; + } finally { + this._saving = false; + } + } + + static get styles(): CSSResult[] { + return [haStyleDialog, css``]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-dialog-import-blueprint": DialogImportBlueprint; + } +} diff --git a/src/panels/config/blueprint/ha-blueprint-overview.ts b/src/panels/config/blueprint/ha-blueprint-overview.ts new file mode 100644 index 0000000000..73d97fb8c4 --- /dev/null +++ b/src/panels/config/blueprint/ha-blueprint-overview.ts @@ -0,0 +1,212 @@ +import "@material/mwc-fab"; +import "@material/mwc-icon-button"; +import { mdiPlus, mdiHelpCircle, mdiDelete } from "@mdi/js"; +import "@polymer/paper-tooltip/paper-tooltip"; +import { + CSSResult, + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import memoizeOne from "memoize-one"; +import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; +import { + showAlertDialog, + showConfirmationDialog, +} from "../../../dialogs/generic/show-dialog-box"; +import "../../../components/entity/ha-entity-toggle"; +import "../../../components/ha-svg-icon"; +import "../../../layouts/hass-tabs-subpage-data-table"; +import { haStyle } from "../../../resources/styles"; +import { HomeAssistant, Route } from "../../../types"; +import { configSections } from "../ha-panel-config"; +import { documentationUrl } from "../../../util/documentation-url"; +import { + BlueprintMetaData, + Blueprints, + deleteBlueprint, +} from "../../../data/blueprint"; +import { showAddBlueprintDialog } from "./show-dialog-import-blueprint"; +import { showAutomationEditor } from "../../../data/automation"; +import { fireEvent } from "../../../common/dom/fire_event"; + +interface BlueprintMetaDataPath extends BlueprintMetaData { + path: string; +} + +const createNewFunctions = { + automation: ( + context: HaBlueprintOverview, + blueprintMeta: BlueprintMetaDataPath + ) => { + showAutomationEditor(context, { + alias: blueprintMeta.name, + use_blueprint: { path: blueprintMeta.path }, + }); + }, +}; + +@customElement("ha-blueprint-overview") +class HaBlueprintOverview extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public isWide!: boolean; + + @property({ type: Boolean }) public narrow!: boolean; + + @property() public route!: Route; + + @property() public blueprints!: Blueprints; + + private _processedBlueprints = memoizeOne((blueprints: Blueprints) => { + const result = Object.entries(blueprints).map(([path, blueprint]) => ({ + ...blueprint.metadata, + path, + })); + return result; + }); + + private _columns = memoizeOne( + (_language): DataTableColumnContainer => { + const columns: DataTableColumnContainer = { + name: { + title: this.hass.localize( + "ui.panel.config.blueprint.overview.headers.name" + ), + sortable: true, + filterable: true, + direction: "asc", + grows: true, + }, + }; + columns.domain = { + title: "Domain", + sortable: true, + filterable: true, + direction: "asc", + width: "20%", + }; + columns.path = { + title: "Path", + sortable: true, + filterable: true, + direction: "asc", + width: "20%", + }; + columns.create = { + title: "", + type: "icon-button", + template: (_, blueprint) => html` this._createNew(ev)} + >`, + }; + columns.delete = { + title: "", + type: "icon-button", + template: (_, blueprint) => html` this._delete(ev)} + >`, + }; + return columns; + } + ); + + protected render(): TemplateResult { + return html` + + + + + + + + + `; + } + + private _showHelp() { + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.blueprint.caption"), + text: html` + ${this.hass.localize("ui.panel.config.blueprint.overview.introduction")} +

+ + ${this.hass.localize( + "ui.panel.config.blueprint.overview.learn_more" + )} + +

+ `, + }); + } + + private _addBlueprint() { + showAddBlueprintDialog(this, { importedCallback: () => this._reload() }); + } + + private _reload() { + fireEvent(this, "reload-blueprints"); + } + + private _createNew(ev) { + const blueprint = ev.currentTarget.blueprint as BlueprintMetaDataPath; + createNewFunctions[blueprint.domain](this, blueprint); + } + + private async _delete(ev) { + const blueprint = ev.currentTarget.blueprint; + if ( + !(await showConfirmationDialog(this, { + title: this.hass.localize( + "ui.panel.config.blueprint.overview.confirm_delete_header" + ), + text: this.hass.localize( + "ui.panel.config.blueprint.overview.confirm_delete_text" + ), + })) + ) { + return; + } + await deleteBlueprint(this.hass, blueprint.domain, blueprint.path); + fireEvent(this, "reload-blueprints"); + } + + static get styles(): CSSResult { + return haStyle; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-blueprint-overview": HaBlueprintOverview; + } +} diff --git a/src/panels/config/blueprint/ha-config-blueprint.ts b/src/panels/config/blueprint/ha-config-blueprint.ts new file mode 100644 index 0000000000..12e3bbc35f --- /dev/null +++ b/src/panels/config/blueprint/ha-config-blueprint.ts @@ -0,0 +1,76 @@ +import { customElement, property, PropertyValues } from "lit-element"; +import { + HassRouterPage, + RouterOptions, +} from "../../../layouts/hass-router-page"; +import "./ha-blueprint-overview"; +import { HomeAssistant } from "../../../types"; +import { Blueprints, fetchBlueprints } from "../../../data/blueprint"; + +declare global { + // for fire event + interface HASSDomEvents { + "reload-blueprints": undefined; + } +} + +@customElement("ha-config-blueprint") +class HaConfigBlueprint extends HassRouterPage { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public narrow!: boolean; + + @property() public isWide!: boolean; + + @property() public showAdvanced!: boolean; + + @property() public blueprints: Blueprints = {}; + + protected routerOptions: RouterOptions = { + defaultPage: "dashboard", + routes: { + dashboard: { + tag: "ha-blueprint-overview", + cache: true, + }, + edit: { + tag: "ha-blueprint-editor", + }, + }, + }; + + private async _getBlueprints() { + this.blueprints = await fetchBlueprints(this.hass, "automation"); + } + + protected firstUpdated(changedProps) { + super.firstUpdated(changedProps); + this.addEventListener("reload-blueprints", () => { + this._getBlueprints(); + }); + this._getBlueprints(); + } + + protected updatePageEl(pageEl, changedProps: PropertyValues) { + pageEl.hass = this.hass; + pageEl.narrow = this.narrow; + pageEl.isWide = this.isWide; + pageEl.route = this.routeTail; + pageEl.showAdvanced = this.showAdvanced; + pageEl.blueprints = this.blueprints; + + if ( + (!changedProps || changedProps.has("route")) && + this._currentPage === "edit" + ) { + const blueprintId = this.routeTail.path.substr(1); + pageEl.blueprintId = blueprintId === "new" ? null : blueprintId; + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-config-blueprint": HaConfigBlueprint; + } +} diff --git a/src/panels/config/blueprint/show-dialog-import-blueprint.ts b/src/panels/config/blueprint/show-dialog-import-blueprint.ts new file mode 100644 index 0000000000..0fec45cf4c --- /dev/null +++ b/src/panels/config/blueprint/show-dialog-import-blueprint.ts @@ -0,0 +1,17 @@ +import { fireEvent } from "../../../common/dom/fire_event"; + +export const loadImportBlueprintDialog = () => + import( + /* webpackChunkName: "add-blueprint-dialog" */ "./dialog-import-blueprint" + ); + +export const showAddBlueprintDialog = ( + element: HTMLElement, + dialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "ha-dialog-import-blueprint", + dialogImport: loadImportBlueprintDialog, + dialogParams, + }); +}; diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 457f9bf1c8..b57805382e 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -33,6 +33,7 @@ import { mdiMathLog, mdiPencil, mdiNfcVariant, + mdiPaletteSwatch, } from "@mdi/js"; declare global { @@ -74,6 +75,12 @@ export const configSections: { [name: string]: PageNavigation[] } = { }, ], automation: [ + { + component: "blueprint", + path: "/config/blueprint", + translationKey: "ui.panel.config.blueprint.caption", + iconPath: mdiPaletteSwatch, + }, { component: "automation", path: "/config/automation", @@ -92,6 +99,8 @@ export const configSections: { [name: string]: PageNavigation[] } = { translationKey: "ui.panel.config.script.caption", iconPath: mdiScriptText, }, + ], + helpers: [ { component: "helpers", path: "/config/helpers", @@ -206,6 +215,13 @@ class HaPanelConfig extends HassRouterPage { /* webpackChunkName: "panel-config-automation" */ "./automation/ha-config-automation" ), }, + blueprint: { + tag: "ha-config-blueprint", + load: () => + import( + /* webpackChunkName: "panel-config-blueprint" */ "./blueprint/ha-config-blueprint" + ), + }, tags: { tag: "ha-config-tags", load: () => diff --git a/src/translations/en.json b/src/translations/en.json index ab628679f5..b70531caf7 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -339,6 +339,11 @@ "add_user": "Add user", "remove_user": "Remove user" }, + "blueprint-picker": { + "select_blueprint": "Select a Blueprint", + "add_user": "Add user", + "remove_user": "Remove user" + }, "device-picker": { "clear": "Clear", "toggle": "Toggle", @@ -1110,6 +1115,19 @@ "name": "Name" } }, + "dialog_new": { + "header": "Create a new automation", + "how": "How do you want to create your new automation?", + + "blueprint": { "use_blueprint": "Use a blueprint" }, + "thingtalk": { + "header": "Describe the automation you want to create", + "intro": "And we will try to create it for you. For example: Turn the lights off when I leave.", + "input_label": "What should this automation do?", + "create": "Create" + }, + "start_empty": "Start with an empty automation" + }, "editor": { "enable_disable": "Enable/Disable automation", "introduction": "Use automations to bring your home alive.", @@ -1125,6 +1143,14 @@ "label": "Description", "placeholder": "Optional description" }, + "blueprint": { + "header": "Blueprint", + "blueprint_to_use": "Blueprint to use", + "no_blueprints": "You don't have any blueprints", + "manage_blueprints": "Manage Blueprints", + "inputs": "Inputs", + "no_inputs": "This blueprint doesn't have any inputs." + }, "modes": { "label": "Mode", "description": "The mode controls what happens when the automation is triggered while the actions are still running from a previous trigger. Check the {documentation_link} for more info.", @@ -1414,6 +1440,31 @@ } } }, + "blueprint": { + "caption": "Blueprints", + "description": "Manage blueprints", + "overview": { + "header": "Blueprint Editor", + "introduction": "The blueprint editor allows you to create and edit blueprints.", + "learn_more": "Learn more about blueprints", + "headers": { + "name": "Name" + }, + "confirm_delete_header": "Delete this Blueprint?", + "confirm_delete_text": "Are you sure you want to delete this Blueprint" + }, + "add": { + "header": "Add new blueprint", + "import_header": "Import {name} ({domain})", + "import_introduction": "You can import Blueprints of other users from Github and the community forums. Enter the url of the Blueprint below.", + "url": "Url of the blueprint", + "importing": "Importing blueprint...", + "import_btn": "Import blueprint", + "saving": "Saving blueprint...", + "save_btn": "Save blueprint", + "error_no_url": "Please enter the url of the blueprint." + } + }, "script": { "caption": "Scripts", "description": "Manage scripts",