diff --git a/src/data/script.ts b/src/data/script.ts index c6c78fc163..6b83157bde 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -21,7 +21,9 @@ export interface ScriptEntity extends HassEntityBase { }; } -export interface ScriptConfig { +export type ScriptConfig = ManualScriptConfig | BlueprintScriptConfig; + +export interface ManualScriptConfig { alias: string; sequence: Action | Action[]; icon?: string; @@ -29,7 +31,7 @@ export interface ScriptConfig { max?: number; } -export interface BlueprintScriptConfig extends ScriptConfig { +export interface BlueprintScriptConfig extends ManualScriptConfig { use_blueprint: { path: string; input?: BlueprintInput }; } diff --git a/src/panels/config/blueprint/ha-blueprint-overview.ts b/src/panels/config/blueprint/ha-blueprint-overview.ts index 6b2b207e0b..35123dceb0 100644 --- a/src/panels/config/blueprint/ha-blueprint-overview.ts +++ b/src/panels/config/blueprint/ha-blueprint-overview.ts @@ -29,6 +29,7 @@ import { Blueprints, deleteBlueprint, } from "../../../data/blueprint"; +import { showScriptEditor } from "../../../data/script"; import { showAlertDialog, showConfirmationDialog, @@ -52,6 +53,12 @@ const createNewFunctions = { use_blueprint: { path: blueprintMeta.path }, }); }, + script: (blueprintMeta: BlueprintMetaDataPath) => { + showScriptEditor({ + alias: blueprintMeta.name, + use_blueprint: { path: blueprintMeta.path }, + }); + }, }; @customElement("ha-blueprint-overview") @@ -62,27 +69,38 @@ class HaBlueprintOverview extends LitElement { @property({ type: Boolean }) public narrow!: boolean; - @property() public route!: Route; + @property({ attribute: false }) public route!: Route; - @property() public blueprints!: Blueprints; + @property({ attribute: false }) public blueprints!: Record< + string, + Blueprints + >; - private _processedBlueprints = memoizeOne((blueprints: Blueprints) => { - const result = Object.entries(blueprints).map(([path, blueprint]) => { - if ("error" in blueprint) { - return { - name: blueprint.error, - error: true, - path, - }; - } - return { - ...blueprint.metadata, - error: false, - path, - }; - }); - return result; - }); + private _processedBlueprints = memoizeOne( + (blueprints: Record) => { + const result: any[] = []; + Object.entries(blueprints).forEach(([type, typeBlueprints]) => + Object.entries(typeBlueprints).forEach(([path, blueprint]) => { + if ("error" in blueprint) { + result.push({ + name: blueprint.error, + type, + error: true, + path, + }); + } else { + result.push({ + ...blueprint.metadata, + type, + error: false, + path, + }); + } + }) + ); + return result; + } + ); private _columns = memoizeOne( (narrow, _language): DataTableColumnContainer => ({ @@ -102,6 +120,20 @@ class HaBlueprintOverview extends LitElement { ` : undefined, }, + type: { + title: this.hass.localize( + "ui.panel.config.blueprint.overview.headers.type" + ), + template: (type: string) => + html`${this.hass.localize( + `ui.panel.config.blueprint.overview.types.${type}` + )}`, + sortable: true, + filterable: true, + hidden: narrow, + direction: "asc", + width: "10%", + }, path: { title: this.hass.localize( "ui.panel.config.blueprint.overview.headers.file_name" @@ -114,25 +146,27 @@ class HaBlueprintOverview extends LitElement { }, create: { title: "", + width: narrow ? undefined : "20%", type: narrow ? "icon-button" : undefined, template: (_, blueprint: any) => blueprint.error ? "" : narrow - ? html` ` + .path=${mdiRobot} + > + ` : html` ${this.hass.localize( - "ui.panel.config.blueprint.overview.use_blueprint" + `ui.panel.config.blueprint.overview.create_${blueprint.domain}` )} `, }, diff --git a/src/panels/config/blueprint/ha-config-blueprint.ts b/src/panels/config/blueprint/ha-config-blueprint.ts index 3ea73842da..68f50c94b4 100644 --- a/src/panels/config/blueprint/ha-config-blueprint.ts +++ b/src/panels/config/blueprint/ha-config-blueprint.ts @@ -25,7 +25,7 @@ class HaConfigBlueprint extends HassRouterPage { @property() public showAdvanced!: boolean; - @property() public blueprints: Blueprints = {}; + @property() public blueprints: Record = {}; protected routerOptions: RouterOptions = { defaultPage: "dashboard", @@ -41,7 +41,11 @@ class HaConfigBlueprint extends HassRouterPage { }; private async _getBlueprints() { - this.blueprints = await fetchBlueprints(this.hass, "automation"); + const [automation, script] = await Promise.all([ + fetchBlueprints(this.hass, "automation"), + fetchBlueprints(this.hass, "script"), + ]); + this.blueprints = { automation, script }; } protected firstUpdated(changedProps) { diff --git a/src/panels/config/script/blueprint-script-editor.ts b/src/panels/config/script/blueprint-script-editor.ts new file mode 100644 index 0000000000..8ea1d163fd --- /dev/null +++ b/src/panels/config/script/blueprint-script-editor.ts @@ -0,0 +1,205 @@ +import "@polymer/paper-input/paper-input"; +import { css, CSSResultGroup, html, LitElement } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/ha-blueprint-picker"; +import "../../../components/ha-card"; +import "../../../components/ha-circular-progress"; +import "../../../components/ha-markdown"; +import "../../../components/ha-selector/ha-selector"; +import "../../../components/ha-settings-row"; + +import { + BlueprintOrError, + Blueprints, + fetchBlueprints, +} from "../../../data/blueprint"; +import { BlueprintScriptConfig } from "../../../data/script"; +import { haStyle } from "../../../resources/styles"; +import { HomeAssistant } from "../../../types"; +import "../ha-config-section"; + +@customElement("blueprint-script-editor") +export class HaBlueprintScriptEditor extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public isWide!: boolean; + + @property({ reflect: true, type: Boolean }) public narrow!: boolean; + + @property({ attribute: false }) public config!: BlueprintScriptConfig; + + @state() private _blueprints?: Blueprints; + + protected firstUpdated(changedProps) { + super.firstUpdated(changedProps); + this._getBlueprints(); + } + + private get _blueprint(): BlueprintOrError | undefined { + if (!this._blueprints) { + return undefined; + } + return this._blueprints[this.config.use_blueprint.path]; + } + + protected render() { + const blueprint = this._blueprint; + return html` + ${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.config.use_blueprint.path + ? blueprint && "error" in blueprint + ? html`

+ There is an error in this Blueprint: ${blueprint.error} +

` + : html`${blueprint?.metadata.description + ? html`` + : ""} + ${blueprint?.metadata?.input && + Object.keys(blueprint.metadata.input).length + ? Object.entries(blueprint.metadata.input).map( + ([key, value]) => + html` + ${value?.name || key} + ${value?.description} + ${value?.selector + ? html`` + : html``} + ` + ) + : html`

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

`}` + : ""} +
+
`; + } + + private async _getBlueprints() { + this._blueprints = await fetchBlueprints(this.hass, "script"); + } + + 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 = ev.detail.value; + if ( + (this.config.use_blueprint.input && + this.config.use_blueprint.input[key] === value) || + (!this.config.use_blueprint.input && value === "") + ) { + return; + } + const input = { ...this.config.use_blueprint.input, [key]: value }; + + if (value === "" || value === undefined) { + delete input[key]; + } + + fireEvent(this, "value-changed", { + value: { + ...this.config, + use_blueprint: { + ...this.config.use_blueprint, + input, + }, + }, + }); + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + .padding { + padding: 16px; + } + .blueprint-picker-container { + padding: 16px; + } + p { + margin-bottom: 0; + } + ha-settings-row { + --paper-time-input-justify-content: flex-end; + border-top: 1px solid var(--divider-color); + } + :host(:not([narrow])) ha-settings-row paper-input { + width: 60%; + } + :host(:not([narrow])) ha-settings-row ha-selector { + width: 60%; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "blueprint-script-editor": HaBlueprintScriptEditor; + } +} diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index e6d617990e..0674226b53 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -38,6 +38,7 @@ import { Action, deleteScript, getScriptEditorInitData, + ManualScriptConfig, MODES, MODES_MAX, ScriptConfig, @@ -55,6 +56,7 @@ import "../automation/action/ha-automation-action"; import { HaDeviceAction } from "../automation/action/types/ha-automation-action-device_id"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; +import "./blueprint-script-editor"; export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @@ -236,60 +238,62 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { > ` : ""} -

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

- - - ${MODES.map( - (mode) => html` - - ${this.hass.localize( - `ui.panel.config.script.editor.modes.${mode}` - ) || mode} - - ` - )} - - - ${this._config.mode && - MODES_MAX.includes(this._config.mode) - ? html` - ` - : html``} + ${"use_blueprint" in this._config + ? "" + : html`

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

+ + + ${MODES.map( + (mode) => html` + + ${this.hass.localize( + `ui.panel.config.script.editor.modes.${mode}` + ) || mode} + + ` + )} + + + ${this._config.mode && + MODES_MAX.includes(this._config.mode) + ? html` + ` + : html``} `} ${this.scriptEntityId ? html` @@ -323,37 +327,48 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { - - - ${this.hass.localize( - "ui.panel.config.script.editor.sequence" - )} - - -

- ${this.hass.localize( - "ui.panel.config.script.editor.sequence_sentence" - )} -

- ` + : html` - ${this.hass.localize( - "ui.panel.config.script.editor.link_available_actions" - )} - -
- -
+ + ${this.hass.localize( + "ui.panel.config.script.editor.sequence" + )} + + +

+ ${this.hass.localize( + "ui.panel.config.script.editor.sequence_sentence" + )} +

+ + ${this.hass.localize( + "ui.panel.config.script.editor.link_available_actions" + )} + +
+ + `} ` : ""} @@ -427,7 +442,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { (!oldScript || oldScript !== this.scriptEntityId) ) { this.hass - .callApi( + .callApi( "GET", `config/script/config/${computeObjectId(this.scriptEntityId)}` ) @@ -466,11 +481,16 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { ) { const initData = getScriptEditorInitData(); this._dirty = !!initData; - this._config = { + const baseConfig: Partial = { alias: this.hass.localize("ui.panel.config.script.editor.default_name"), - sequence: [{ ...HaDeviceAction.defaultConfig }], - ...initData, }; + if (!initData || !("use_blueprint" in initData)) { + baseConfig.sequence = [{ ...HaDeviceAction.defaultConfig }]; + } + this._config = { + ...baseConfig, + ...initData, + } as ScriptConfig; } } @@ -548,6 +568,11 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { this._dirty = true; } + private _configChanged(ev) { + this._config = ev.detail.value; + this._dirty = true; + } + private _sequenceChanged(ev: CustomEvent): void { this._config = { ...this._config!, sequence: ev.detail.value as Action[] }; this._errors = undefined; @@ -749,3 +774,9 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { } customElements.define("ha-script-editor", HaScriptEditor); + +declare global { + interface HTMLElementTagNameMap { + "ha-script-editor": HaScriptEditor; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index e431546104..29660a7d7b 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1794,13 +1794,18 @@ "learn_more": "Learn more about using blueprints", "headers": { "name": "Name", - "domain": "Domain", + "type": "Type", "file_name": "File name" }, + "types": { + "automation": "Automation", + "script": "Script" + }, "confirm_delete_header": "Delete this blueprint?", "confirm_delete_text": "Are you sure you want to delete this blueprint?", "add_blueprint": "Import blueprint", - "use_blueprint": "Create automation", + "create_automation": "Create automation", + "create_script": "Create script", "delete_blueprint": "Delete blueprint", "share_blueprint": "Share blueprint", "share_blueprint_no_url": "Unable to share blueprint: no source url",