diff --git a/src/components/ha-automation-row.ts b/src/components/ha-automation-row.ts new file mode 100644 index 0000000000..1d9c64201a --- /dev/null +++ b/src/components/ha-automation-row.ts @@ -0,0 +1,138 @@ +import type { TemplateResult } from "lit"; +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import "./ha-svg-icon"; + +@customElement("ha-automation-row") +export class HaAutomationRow extends LitElement { + @property() header?: string; + + @property() secondary?: string; + + protected render(): TemplateResult { + return html` +
+
+ + +
+ ${this.header} + ${this.secondary} +
+
+ +
+
+ `; + } + + private async _toggleContainer(ev): Promise { + if (ev.defaultPrevented) { + return; + } + if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") { + return; + } + ev.preventDefault(); + + this.click(); + } + + static styles = css` + :host { + display: block; + } + + .top { + display: flex; + align-items: center; + border-radius: var(--ha-card-border-radius, 12px); + } + + .top.expanded { + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; + } + + .top.focused { + background: var(--input-fill-color); + } + + :host([outlined]) { + box-shadow: none; + border-width: 1px; + border-style: solid; + border-color: var(--outline-color); + border-radius: var(--ha-card-border-radius, 12px); + } + + .summary-icon { + transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1); + direction: var(--direction); + margin-left: 8px; + margin-inline-start: 8px; + margin-inline-end: initial; + } + + :host([left-chevron]) .summary-icon, + ::slotted([slot="leading-icon"]) { + margin-left: 0; + margin-right: 8px; + margin-inline-start: 0; + margin-inline-end: 8px; + } + + #summary { + flex: 1; + display: flex; + padding: var(--expansion-panel-summary-padding, 0 8px); + min-height: 48px; + align-items: center; + cursor: pointer; + overflow: hidden; + font-weight: var(--ha-font-weight-medium); + outline: none; + } + #summary.noCollapse { + cursor: default; + } + + .summary-icon.expanded { + transform: rotate(180deg); + } + + .header, + ::slotted([slot="header"]) { + flex: 1; + overflow-wrap: anywhere; + } + + .container { + padding: var(--expansion-panel-content-padding, 0 8px); + overflow: hidden; + transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1); + height: 0px; + } + + .container.expanded { + height: auto; + } + + .secondary { + display: block; + color: var(--secondary-text-color); + font-size: var(--ha-font-size-s); + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-row": HaAutomationRow; + } +} diff --git a/src/layouts/hass-subpage.ts b/src/layouts/hass-subpage.ts index 11be4952a6..2374ad5ff9 100644 --- a/src/layouts/hass-subpage.ts +++ b/src/layouts/hass-subpage.ts @@ -55,7 +55,11 @@ class HassSubpage extends LitElement {
${this.header}
-
+
@@ -69,6 +73,15 @@ class HassSubpage extends LitElement { this._savedScrollPos = (e.target as HTMLDivElement).scrollTop; } + private _scrollTo(e: CustomEvent<{ up: number }>): void { + this.renderRoot + .querySelector(".content")! + .scrollTo( + 0, + e.detail.up + this.renderRoot.querySelector(".content")?.scrollTop + ); + } + private _backTapped(): void { if (this.backCallback) { this.backCallback(); 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 4302972e52..98fc848b74 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -203,7 +203,7 @@ export default class HaAutomationActionRow extends LitElement {
` : nothing} - + ${type === "service" && "action" in this.action && this.action.action ? html` - - ${this.hass.localize( - `ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}` - )} - - - `}
- + `; } @@ -676,8 +666,8 @@ export default class HaAutomationActionRow extends LitElement { } :host([highlight]) ha-card { --shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent); - --shadow-focus: 0 0 0 1px var(--state-inactive-color); - border-color: var(--state-inactive-color); + --shadow-focus: 0 0 0 1px var(--primary-color); + border-color: var(--primary-color); box-shadow: var(--shadow-default), var(--shadow-focus); } `, diff --git a/src/panels/config/automation/action/ha-automation-action.ts b/src/panels/config/automation/action/ha-automation-action.ts index 678f829bcb..1934adf0be 100644 --- a/src/panels/config/automation/action/ha-automation-action.ts +++ b/src/panels/config/automation/action/ha-automation-action.ts @@ -93,6 +93,7 @@ export default class HaAutomationAction extends LitElement { @move-down=${this._moveDown} @move-up=${this._moveUp} @value-changed=${this._actionChanged} + @click=${this._actionClicked} .hass=${this.hass} ?highlight=${this.highlightedActions?.includes(action)} > @@ -102,8 +103,74 @@ export default class HaAutomationAction extends LitElement { ` - : nothing} - + : nothing} ${Object.keys(action)[0] === "choose" + ? html`
+ +

+ Option 1: +

+
+
+ + + + + Actions
+ +
+
+ + + + Default actions
+ +
+
` + : nothing} ` )}
@@ -132,6 +199,15 @@ export default class HaAutomationAction extends LitElement { `; } + private _actionClicked(ev: MouseEvent) { + fireEvent(this, "element-selected", { + type: "action", + element: (ev.currentTarget as HaAutomationActionRow).action, + index: (ev.currentTarget as HaAutomationActionRow).index, + path: (ev.currentTarget as HaAutomationActionRow).path, + }); + } + protected updated(changedProps: PropertyValues) { super.updated(changedProps); 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 5fe57a14e5..e2fb1d620f 100644 --- a/src/panels/config/automation/condition/ha-automation-condition-row.ts +++ b/src/panels/config/automation/condition/ha-automation-condition-row.ts @@ -128,7 +128,7 @@ export default class HaAutomationConditionRow extends LitElement { ` : ""} - + - - ${this.hass.localize( - `ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}` - )} - - - ` : ""} -
- +
ha-alert { margin: 0 auto; @@ -1100,6 +1099,12 @@ export class HaAutomationEditor extends PreventUnsavedMixin( padding: 28px 20px 0; display: block; } + manual-automation-editor { + margin: 0 auto; + max-width: 1540px; + padding: 28px 20px 0; + display: block; + } ha-yaml-editor { flex-grow: 1; --actions-border-radius: 0; diff --git a/src/panels/config/automation/manual-automation-editor.ts b/src/panels/config/automation/manual-automation-editor.ts index 3d69ee8752..60f8638afd 100644 --- a/src/panels/config/automation/manual-automation-editor.ts +++ b/src/panels/config/automation/manual-automation-editor.ts @@ -1,5 +1,11 @@ import "@material/mwc-button/mwc-button"; -import { mdiHelpCircle } from "@mdi/js"; +import { + mdiClose, + mdiDotsVertical, + mdiHelpCircle, + mdiIdentifier, + mdiPlaylistEdit, +} from "@mdi/js"; import type { HassEntity } from "home-assistant-js-websocket"; import type { CSSResultGroup, PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit"; @@ -49,6 +55,51 @@ import { constructUrlCurrentPath } from "../../../common/url/construct-url"; import { canOverrideAlphanumericInput } from "../../../common/dom/can-override-input"; import { showToast } from "../../../util/toast"; import { showPasteReplaceDialog } from "./paste-replace-dialog/show-dialog-paste-replace"; +import "@shoelace-style/shoelace/dist/components/split-panel/split-panel"; +import "@shoelace-style/shoelace/dist/components/drawer/drawer"; +import { dynamicElement } from "../../../common/dom/dynamic-element-directive"; +import { classMap } from "lit/directives/class-map"; +import { getType } from "./action/ha-automation-action-row"; +import { storage } from "../../../common/decorators/storage"; +import { nextRender } from "../../../common/util/render-status"; +import { + DIRECTION_ALL, + DIRECTION_VERTICAL, + Manager, + Pan, + Swipe, +} from "@egjs/hammerjs"; + +function findNestedItem( + obj: any, + path: ItemPath, + createNonExistingPath?: boolean +): any { + return path.reduce((ac, p, index, array) => { + if (ac === undefined) return undefined; + if (!ac[p] && createNonExistingPath) { + const nextP = array[index + 1]; + // Create object or array depending on next path + if (nextP === undefined || typeof nextP === "number") { + ac[p] = []; + } else { + ac[p] = {}; + } + } + return ac[p]; + }, obj); +} + +function updateNestedItem(obj: any, path: ItemPath, newValue): any { + const lastKey = path.pop()!; + const parent = findNestedItem(obj, path); + parent[lastKey] = newValue + ? newValue + : Array.isArray(parent[lastKey]) + ? [...parent[lastKey]] + : [parent[lastKey]]; + return obj; +} const baseConfigStruct = object({ alias: optional(string()), @@ -85,6 +136,14 @@ export class HaManualAutomationEditor extends LitElement { @state() private _pastedConfig?: ManualAutomationConfig; + @state() private _selectedElement?: any; + + @state() + @storage({ key: "automationSidebarPosition" }) + private _sidebarWidth = 99999; + + @state() private _yamlMode = false; + private _previousConfig?: ManualAutomationConfig; public connectedCallback() { @@ -114,6 +173,13 @@ export class HaManualAutomationEditor extends LitElement { } } + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if (changedProps.has("narrow") && this.narrow && this._selectedElement) { + this.renderRoot.querySelector("sl-drawer").show(); + } + } + private _clearParam(param: string) { window.history.replaceState( null, @@ -123,151 +189,411 @@ export class HaManualAutomationEditor extends LitElement { } protected render() { - return html` - ${this.stateObj?.state === "off" - ? html` - - ${this.hass.localize( - "ui.panel.config.automation.editor.disabled" - )} - - ${this.hass.localize( - "ui.panel.config.automation.editor.enable" - )} - - - ` - : nothing} - ${this.config.description - ? html`` - : nothing} -
-

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

- - - -
- ${!ensureArray(this.config.triggers)?.length - ? html`

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

` - : nothing} + const selectedElement = this._selectedElement?.element; + const selectedElementType = this._selectedElement?.type; + const path = this._selectedElement?.path || []; - + const type = ""; + const supported = true; + const yamlMode = this._yamlMode; -
-

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

- - - -
- ${!ensureArray(this.config.conditions)?.length - ? html`

- ${this.hass.localize( - "ui.panel.config.automation.editor.conditions.description", - { user: this.hass.user?.name || "Alice" } - )} -

` - : nothing} - - - -
-

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

-
- + const sidePanel = this._selectedElement + ? html` - -
-
- ${!ensureArray(this.config.actions)?.length - ? html`

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

` - : nothing} + ${`Edit ${selectedElementType}`} + + + ${selectedElementType === "trigger" + ? html` + ${this.hass.localize( + "ui.panel.config.automation.editor.triggers.edit_id" + )} + + ` + : nothing} + + ${this.hass.localize( + `ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}` + )} + + + + +
+ ${this._yamlMode + ? html`` + : selectedElementType === "trigger" + ? html`
+ ${dynamicElement( + `ha-automation-trigger-${selectedElement.trigger}`, + { + hass: this.hass, + trigger: selectedElement, + disabled: this.disabled, + } + )} +
` + : selectedElementType === "condition" + ? html`` + : selectedElementType === "action" + ? html`
+ ${dynamicElement( + `ha-automation-action-${getType(selectedElement)}`, + { + hass: this.hass, + action: selectedElement, + narrow: true, + disabled: this.disabled, + } + )} +
` + : nothing} +
` + : nothing; - + return html` + ${this.narrow + ? html` + ${sidePanel} + ` + : nothing} + +
+ ${this.stateObj?.state === "off" + ? html` + + ${this.hass.localize( + "ui.panel.config.automation.editor.disabled" + )} + + ${this.hass.localize( + "ui.panel.config.automation.editor.enable" + )} + + + ` + : nothing} + ${this.config.description + ? html`` + : nothing} +
+

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

+ + + +
+ ${!ensureArray(this.config.triggers)?.length + ? html`

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

` + : nothing} + + + +
+

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

+ + + +
+ ${!ensureArray(this.config.conditions)?.length + ? html`

+ ${this.hass.localize( + "ui.panel.config.automation.editor.conditions.description", + { user: this.hass.user?.name || "Alice" } + )} +

` + : nothing} + + + +
+

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

+
+ + + +
+
+ ${!ensureArray(this.config.actions)?.length + ? html`

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

` + : nothing} + + +
+ ${!this.narrow && selectedElement + ? html` + ${sidePanel} + ` + : nothing} +
`; } + private _onUiChanged(ev: CustomEvent): void { + ev.stopPropagation(); + const path = ev.currentTarget?.path || []; + + const newConfig = updateNestedItem( + { ...this.config }, + path, + ev.detail.value + ); + + console.log(newConfig); + + fireEvent(this, "value-changed", { value: newConfig }); + } + + private async _toggleYamlMode() { + this._yamlMode = !this._yamlMode; + if (this._yamlMode) { + await this.updateComplete; + // this.renderRoot.querySelector("ha-yaml-editor").positionInPixels = 0; + } + } + + private async _elementSelected(ev) { + console.log(ev); + this._selectedElement = ev.detail; + console.log("repo", this._sidebarWidth); + const target = ev.target; + await this.updateComplete; + this.renderRoot.querySelector("sl-split-panel").positionInPixels = + this.clientWidth - 40 - this._sidebarWidth; + if (this.narrow) { + this.renderRoot.querySelector("sl-drawer").show(); + console.log(target); + this._targetEl = target; + } + } + + private _splitPanelRepositioned(ev: CustomEvent): void { + if (!this._selectedElement) { + return; + } + console.log(ev); + console.log("reposition", ev.target.positionInPixels); + let sidebarWidth = ev.target.clientWidth - ev.target.positionInPixels; + if (this._oldClientWidth && this._oldClientWidth !== this.clientWidth) { + // If the client width has changed, we need to subtract the difference + sidebarWidth = sidebarWidth + (this._oldClientWidth - this.clientWidth); + } + this._oldClientWidth = this.clientWidth; + console.log(sidebarWidth); + console.log(this.clientWidth); + console.log(this.clientWidth - 40 - sidebarWidth); + // if (Math.abs(sidebarWidth - this._sidebarWidth) > 20) { + // this._sidebarWidth = sidebarWidth; + // } + this._sidebarWidth = sidebarWidth; + } + + private _closeSidebar() { + if (this.narrow) { + this.renderRoot.querySelector("sl-drawer").hide(); + } + this._selectedElement = undefined; + } + + private async _drawerOpen() { + // this._oldScrollPosition = window.scrollY; + this.renderRoot.querySelector("div[slot='start']").style.paddingBottom = + "66vh"; + await nextRender(); + fireEvent(this, "scroll-to", { + up: this._targetEl.getBoundingClientRect().top, + }); + this._setupListeners(); + } + + private _setupListeners() { + const mc = new Manager(this.renderRoot.querySelector("ha-dialog-header"), { + touchAction: "pan-y", + }); + + mc.add( + new Swipe({ + direction: DIRECTION_VERTICAL, + }) + ); + mc.on("swipeup", (e) => { + console.log("up", e); + this.toggleAttribute("big-drawer", true); + }); + + mc.on("swipedown", (e) => { + console.log("down", e); + if (this.hasAttribute("big-drawer")) { + this.toggleAttribute("big-drawer", false); + } else { + this.renderRoot.querySelector("sl-drawer").hide(); + } + }); + + this._manager = mc; + } + + private _drawerClose() { + this.renderRoot.querySelector("div[slot='start']").style.paddingBottom = + "0"; + } + private _triggerChanged(ev: CustomEvent): void { ev.stopPropagation(); this.resetPastedConfig(); @@ -552,6 +878,45 @@ export class HaManualAutomationEditor extends LitElement { font-weight: var(--ha-font-weight-normal); line-height: 0; } + + sl-split-panel { + height: calc(100vh - var(--header-height, 64px) - 28px - 20px - 1px); + } + + sl-drawer { + --sl-z-index-drawer: 9999; + --size: 66vh; + --sl-panel-background-color: var(--ha-card-background, white); + --sl-overlay-background-color: rgba(0, 0, 0, 0.32); + --sl-shadow-x-large: var( + --ha-card-box-shadow, + 0px -1px 4px 1px rgba(0, 0, 0, 0.2), + 0px 1px 1px 0px rgba(0, 0, 0, 0.14), + 0px 1px 3px 0px rgba(0, 0, 0, 0.12) + ); + --sl-panel-border-color: var(--ha-card-border-color, #e0e0e0); + } + :host([big-drawer]) sl-drawer { + --size: 90vh; + } + sl-drawer::part(panel) { + border-radius: 12px 12px 0 0; + border: 1px solid var(--ha-card-border-color, #e0e0e0); + } + sl-drawer .card-content { + padding: 12px; + } + sl-drawer ha-dialog-header { + position: sticky; + top: 0; + background: var(--card-background-color); + z-index: 999; + } + .card-content { + overflow: auto; + height: 100%; + padding-bottom: 16px; + } `, ]; } 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 f9f4c28ec2..c7caa764dc 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts @@ -70,6 +70,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 "../../../../components/ha-automation-row"; export interface TriggerElement extends LitElement { trigger: Trigger; @@ -158,7 +159,7 @@ export default class HaAutomationTriggerRow extends LitElement { ` : nothing} - + - - ${this.hass.localize( - "ui.panel.config.automation.editor.triggers.edit_id" - )} - - - - - ${this.hass.localize( - `ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}` - )} - - - - -
- ${this._warnings - ? html` - ${this._warnings.length && this._warnings[0] !== undefined - ? html`
    - ${this._warnings.map( - (warning) => html`
  • ${warning}
  • ` - )} -
` - : ""} - ${this.hass.localize( - "ui.errors.config.edit_in_yaml_supported" - )} -
` - : ""} - ${yamlMode - ? html` - ${!supported - ? html` - ${this.hass.localize( - "ui.panel.config.automation.editor.triggers.unsupported_platform", - { platform: type } - )} - ` - : ""} - - ` - : html` - ${showId && !isTriggerList(this.trigger) - ? html` - - - ` - : ""} -
- ${dynamicElement(`ha-automation-trigger-${type}`, { - hass: this.hass, - trigger: this.trigger, - disabled: this.disabled, - })} -
- `} -
-
+
{ let triggers: Trigger[]; if (value === PASTE_VALUE) { diff --git a/src/panels/config/integrations/integration-panels/zha/zha-device-card.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-card.ts index 4e0908e746..1c94eb18ca 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-device-card.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-device-card.ts @@ -156,7 +156,7 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) { newName = name.replace(oldDeviceName, newDeviceName); } - if (newName !== undefined && !newEntityId) { + if (newName === undefined && !newEntityId) { return undefined; } diff --git a/src/panels/config/script/ha-script-field-row.ts b/src/panels/config/script/ha-script-field-row.ts index 7c3d68fd23..7dbc207f25 100644 --- a/src/panels/config/script/ha-script-field-row.ts +++ b/src/panels/config/script/ha-script-field-row.ts @@ -355,8 +355,8 @@ export default class HaScriptFieldRow extends LitElement { } :host([highlight]) ha-card { --shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent); - --shadow-focus: 0 0 0 1px var(--state-inactive-color); - border-color: var(--state-inactive-color); + --shadow-focus: 0 0 0 1px var(--primary-color); + border-color: var(--primary-color); box-shadow: var(--shadow-default), var(--shadow-focus); } `,