diff --git a/gallery/src/pages/Text/remove-delete-add-create.markdown b/gallery/src/pages/Text/remove-delete-add-create.markdown new file mode 100644 index 0000000000..395adc673c --- /dev/null +++ b/gallery/src/pages/Text/remove-delete-add-create.markdown @@ -0,0 +1,56 @@ +--- +title: When to use remove, delete, add and create +subtitle: The difference between remove/delete and add/create. +--- + +# Remove vs Delete +Remove and Delete are quite similar, but can be frustrating if used inconsistently. + +## Remove +Take away and set aside, but kept in existence. + +For example: +* Removing a user's permission +* Removing a user from a group +* Removing links between items +* Removing a widget +* Removing a link +* Removing an item from a cart + +## Delete +Erase, rendered nonexistent or nonrecoverable. + +For example: +* Deleting a field +* Deleting a value in a field +* Deleting a task +* Deleting a group +* Deleting a permission +* Deleting a calendar event + +# Add vs Create +In most cases, Create can be paired with Delete, and Add can be paired with Remove. + +## Add +An already-exisiting item. + +For example: +* Adding a permission to a user +* Adding a user to a group +* Adding links between items +* Adding a widget +* Adding a link +* Adding an item to a cart + +## Create +Something made from scratch. + +For example: +* Creating a new field +* Creating a new value in a field +* Creating a new task +* Creating a new group +* Creating a new permission +* Creating a new calendar event + +Based on this is [UX magazine article](https://uxmag.com/articles/ui-copy-remove-vs-delete2-banner). diff --git a/gallery/src/pages/components/dialogs.markdown b/gallery/src/pages/components/dialogs.markdown index 69d6edf758..472265b091 100644 --- a/gallery/src/pages/components/dialogs.markdown +++ b/gallery/src/pages/components/dialogs.markdown @@ -1,5 +1,5 @@ --- -title: Dialgos +title: Dialogs subtitle: Dialogs provide important prompts in a user flow. --- diff --git a/pyproject.toml b/pyproject.toml index 38b0d8ad8a..399da64a98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20220902.0" +version = "20220905.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index ba759b197f..de3acbabe8 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -2,17 +2,18 @@ import { HassEntity } from "home-assistant-js-websocket"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; import { FrontendLocaleData } from "../../data/translation"; import { - UPDATE_SUPPORT_PROGRESS, updateIsInstallingFromAttributes, + UPDATE_SUPPORT_PROGRESS, } from "../../data/update"; +import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration"; import { formatDate } from "../datetime/format_date"; import { formatDateTime } from "../datetime/format_date_time"; import { formatTime } from "../datetime/format_time"; import { formatNumber, isNumericFromAttributes } from "../number/format_number"; +import { blankBeforePercent } from "../translations/blank_before_percent"; import { LocalizeFunc } from "../translations/localize"; -import { supportsFeatureFromAttributes } from "./supports-feature"; -import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration"; import { computeDomain } from "./compute_domain"; +import { supportsFeatureFromAttributes } from "./supports-feature"; export const computeStateDisplay = ( localize: LocalizeFunc, @@ -67,7 +68,7 @@ export const computeStateDisplayFromEntityAttributes = ( const unit = !attributes.unit_of_measurement ? "" : attributes.unit_of_measurement === "%" - ? "%" + ? blankBeforePercent(locale) + "%" : ` ${attributes.unit_of_measurement}`; return `${formatNumber(state, locale)}${unit}`; } diff --git a/src/common/translations/blank_before_percent.ts b/src/common/translations/blank_before_percent.ts new file mode 100644 index 0000000000..4c489c96c8 --- /dev/null +++ b/src/common/translations/blank_before_percent.ts @@ -0,0 +1,18 @@ +import { FrontendLocaleData } from "../../data/translation"; + +// Logic based on https://en.wikipedia.org/wiki/Percent_sign#Form_and_spacing +export const blankBeforePercent = ( + localeOptions: FrontendLocaleData +): string => { + switch (localeOptions.language) { + case "cz": + case "de": + case "fi": + case "fr": + case "sk": + case "sv": + return " "; + default: + return ""; + } +}; diff --git a/src/components/ha-gauge.ts b/src/components/ha-gauge.ts index e37d80a0b1..6aaf31e3d5 100644 --- a/src/components/ha-gauge.ts +++ b/src/components/ha-gauge.ts @@ -2,6 +2,7 @@ import { css, LitElement, PropertyValues, svg, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; import { formatNumber } from "../common/number/format_number"; +import { blankBeforePercent } from "../common/translations/blank_before_percent"; import { afterNextRender } from "../common/util/render-status"; import { FrontendLocaleData } from "../data/translation"; import { getValueInPercentage, normalize } from "../util/calculate"; @@ -133,7 +134,11 @@ export class Gauge extends LitElement { ? this._segment_label : this.valueText || formatNumber(this.value, this.locale) }${ - this._segment_label ? "" : this.label === "%" ? "%" : ` ${this.label}` + this._segment_label + ? "" + : this.label === "%" + ? blankBeforePercent(this.locale) + "%" + : ` ${this.label}` } `; diff --git a/src/components/ha-icon-picker.ts b/src/components/ha-icon-picker.ts index 871dc3e477..657953e816 100644 --- a/src/components/ha-icon-picker.ts +++ b/src/components/ha-icon-picker.ts @@ -1,4 +1,4 @@ -import { css, html, LitElement, TemplateResult } from "lit"; +import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { customElement, property, query, state } from "lit/decorators"; import { fireEvent } from "../common/dom/fire_event"; @@ -123,6 +123,10 @@ export class HaIconPicker extends LitElement { } } + protected shouldUpdate(changedProps: PropertyValues) { + return !this._opened || changedProps.has("_opened"); + } + private _valueChanged(ev: PolymerChangedEvent) { ev.stopPropagation(); this._setValue(ev.detail.value); diff --git a/src/data/automation.ts b/src/data/automation.ts index 91591a595b..4f7d31d1eb 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -9,6 +9,7 @@ import { DeviceCondition, DeviceTrigger } from "./device_automation"; import { Action, MODES } from "./script"; export const AUTOMATION_DEFAULT_MODE: typeof MODES[number] = "single"; +export const AUTOMATION_DEFAULT_MAX = 10; export interface AutomationEntity extends HassEntityBase { attributes: HassEntityAttributeBase & { diff --git a/src/data/media-player.ts b/src/data/media-player.ts index 38a288d17e..4470156588 100644 --- a/src/data/media-player.ts +++ b/src/data/media-player.ts @@ -51,6 +51,7 @@ interface MediaPlayerEntityAttributes extends HassEntityAttributeBase { media_duration?: number; media_position?: number; media_title?: string; + media_channel?: string; icon?: string; entity_picture_local?: string; is_volume_muted?: boolean; @@ -235,6 +236,9 @@ export const computeMediaDescription = ( } } break; + case "channel": + secondaryTitle = stateObj.attributes.media_channel!; + break; default: secondaryTitle = stateObj.attributes.app_name || ""; } diff --git a/src/dialogs/more-info/controls/more-info-media_player.ts b/src/dialogs/more-info/controls/more-info-media_player.ts index afd1854fcc..395254d4d4 100644 --- a/src/dialogs/more-info/controls/more-info-media_player.ts +++ b/src/dialogs/more-info/controls/more-info-media_player.ts @@ -167,7 +167,8 @@ class MoreInfoMediaPlayer extends LitElement { ` : ""} - ${supportsFeature(stateObj, SUPPORT_SELECT_SOUND_MODE) && + ${![UNAVAILABLE, UNKNOWN, "off"].includes(stateObj.state) && + supportsFeature(stateObj, SUPPORT_SELECT_SOUND_MODE) && stateObj.attributes.sound_mode_list?.length ? html`
diff --git a/src/panels/config/areas/ha-config-area-page.ts b/src/panels/config/areas/ha-config-area-page.ts index 74aec4c709..c211dccd9b 100644 --- a/src/panels/config/areas/ha-config-area-page.ts +++ b/src/panels/config/areas/ha-config-area-page.ts @@ -45,13 +45,14 @@ import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; -import { HomeAssistant, Route } from "../../../types"; +import { HomeAssistant } from "../../../types"; import "../../logbook/ha-logbook"; -import { configSections } from "../ha-panel-config"; import { loadAreaRegistryDetailDialog, showAreaRegistryDetailDialog, } from "./show-dialog-area-registry-detail"; +import "../../../layouts/hass-error-screen"; +import "../../../layouts/hass-subpage"; declare type NameAndEntity = { name: string; @@ -66,11 +67,9 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) { @property({ type: Boolean, reflect: true }) public narrow!: boolean; - @property() public isWide!: boolean; + @property({ type: Boolean }) public isWide!: boolean; - @property() public showAdvanced!: boolean; - - @property() public route!: Route; + @property({ type: Boolean }) public showAdvanced!: boolean; @state() public _areas!: AreaRegistryEntry[]; @@ -242,43 +241,20 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) { } return html` - - ${this.narrow - ? html` ${area.name} - ` - : ""} +
- ${!this.narrow - ? html` -
-

- ${area.name} - -

-
- ` - : ""}
${area.picture ? html`
@@ -504,7 +480,7 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) { : ""}
- + `; } 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 310b38c90d..c742eefc01 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -1,8 +1,6 @@ import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; import "@material/mwc-list/mwc-list-item"; import { - mdiArrowDown, - mdiArrowUp, mdiCheck, mdiContentDuplicate, mdiDelete, @@ -17,13 +15,15 @@ import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; import { fireEvent } from "../../../../common/dom/fire_event"; +import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter"; import { handleStructError } from "../../../../common/structs/handle-errors"; import "../../../../components/ha-alert"; import "../../../../components/ha-button-menu"; import "../../../../components/ha-card"; -import "../../../../components/ha-icon-button"; import "../../../../components/ha-expansion-panel"; +import "../../../../components/ha-icon-button"; import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; +import { ACTION_TYPES } from "../../../../data/action"; import { validateConfig } from "../../../../data/config"; import { Action, getActionType } from "../../../../data/script"; import { describeAction } from "../../../../data/script_i18n"; @@ -50,8 +50,6 @@ import "./types/ha-automation-action-service"; import "./types/ha-automation-action-stop"; import "./types/ha-automation-action-wait_for_trigger"; import "./types/ha-automation-action-wait_template"; -import { ACTION_TYPES } from "../../../../data/action"; -import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter"; const getType = (action: Action | undefined) => { if (!action) { @@ -66,13 +64,6 @@ const getType = (action: Action | undefined) => { return Object.keys(ACTION_TYPES).find((option) => option in action); }; -declare global { - // for fire event - interface HASSDomEvents { - "move-action": { direction: "up" | "down" }; - } -} - export interface ActionElement extends LitElement { action: Action; } @@ -107,12 +98,12 @@ export default class HaAutomationActionRow extends LitElement { @property() public action!: Action; - @property() public index!: number; - - @property() public totalActions!: number; - @property({ type: Boolean }) public narrow = false; + @property({ type: Boolean }) public hideMenu = false; + + @property({ type: Boolean }) public reOrderMode = false; + @state() private _warnings?: string[]; @state() private _uiModeAvailable = true; @@ -165,119 +156,112 @@ export default class HaAutomationActionRow extends LitElement { ${capitalizeFirstLetter(describeAction(this.hass, this.action))} - ${this.index !== 0 - ? html` - + ${this.hideMenu + ? "" + : html` + - ` - : ""} - ${this.index !== this.totalActions - 1 - ? html` - - ` - : ""} - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.run" - )} - - + fixed + corner="BOTTOM_START" + @action=${this._handleAction} + @click=${preventDefault} + > + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.run" + )} + + - - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.rename" - )} - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.duplicate" - )} - - + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.rename" + )} + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.duplicate" + )} + + -
  • +
  • - - ${this.hass.localize("ui.panel.config.automation.editor.edit_ui")} - ${!yamlMode - ? html`` - : ``} - + + ${this.hass.localize( + "ui.panel.config.automation.editor.edit_ui" + )} + ${!yamlMode + ? html`` + : ``} + - - ${this.hass.localize( - "ui.panel.config.automation.editor.edit_yaml" - )} - ${yamlMode - ? html`` - : ``} - + + ${this.hass.localize( + "ui.panel.config.automation.editor.edit_yaml" + )} + ${yamlMode + ? html`` + : ``} + -
  • +
  • + + + ${this.action.enabled === false + ? this.hass.localize( + "ui.panel.config.automation.editor.actions.enable" + ) + : this.hass.localize( + "ui.panel.config.automation.editor.actions.disable" + )} + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.delete" + )} + + +
    + `} - - ${this.action.enabled === false - ? this.hass.localize( - "ui.panel.config.automation.editor.actions.enable" - ) - : this.hass.localize( - "ui.panel.config.automation.editor.actions.disable" - )} - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.delete" - )} - - -
    `} @@ -346,16 +331,6 @@ export default class HaAutomationActionRow extends LitElement { } } - private _moveUp(ev) { - ev.preventDefault(); - fireEvent(this, "move-action", { direction: "up" }); - } - - private _moveDown(ev) { - ev.preventDefault(); - fireEvent(this, "move-action", { direction: "down" }); - } - private async _handleAction(ev: CustomEvent) { switch (ev.detail.index) { case 0: diff --git a/src/panels/config/automation/action/ha-automation-action.ts b/src/panels/config/automation/action/ha-automation-action.ts index 9c65fa1fba..dd7b802aa1 100644 --- a/src/panels/config/automation/action/ha-automation-action.ts +++ b/src/panels/config/automation/action/ha-automation-action.ts @@ -1,15 +1,25 @@ -import { repeat } from "lit/directives/repeat"; -import { mdiPlus } from "@mdi/js"; -import deepClone from "deep-clone-simple"; import "@material/mwc-button"; import type { ActionDetail } from "@material/mwc-list"; -import memoizeOne from "memoize-one"; +import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js"; +import deepClone from "deep-clone-simple"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { customElement, property } from "lit/decorators"; +import { repeat } from "lit/directives/repeat"; +import memoizeOne from "memoize-one"; +import type { SortableEvent } from "sortablejs"; import { fireEvent } from "../../../../common/dom/fire_event"; -import "../../../../components/ha-svg-icon"; +import { stringCompare } from "../../../../common/string/compare"; +import { LocalizeFunc } from "../../../../common/translations/localize"; import "../../../../components/ha-button-menu"; +import type { HaSelect } from "../../../../components/ha-select"; +import "../../../../components/ha-svg-icon"; +import { ACTION_TYPES } from "../../../../data/action"; import { Action } from "../../../../data/script"; +import { sortableStyles } from "../../../../resources/ha-sortable-style"; +import { + loadSortable, + SortableInstance, +} from "../../../../resources/sortable.ondemand"; import { HomeAssistant } from "../../../../types"; import "./ha-automation-action-row"; import type HaAutomationActionRow from "./ha-automation-action-row"; @@ -27,10 +37,6 @@ import "./types/ha-automation-action-service"; import "./types/ha-automation-action-stop"; import "./types/ha-automation-action-wait_for_trigger"; import "./types/ha-automation-action-wait_template"; -import { ACTION_TYPES } from "../../../../data/action"; -import { stringCompare } from "../../../../common/string/compare"; -import { LocalizeFunc } from "../../../../common/translations/localize"; -import type { HaSelect } from "../../../../components/ha-select"; @customElement("ha-automation-action") export default class HaAutomationAction extends LitElement { @@ -40,28 +46,62 @@ export default class HaAutomationAction extends LitElement { @property() public actions!: Action[]; + @property({ type: Boolean }) public reOrderMode = false; + private _focusLastActionOnChange = false; private _actionKeys = new WeakMap(); + private _sortable?: SortableInstance; + protected render() { return html` - ${repeat( - this.actions, - (action) => this._getKey(action), - (action, idx) => html` - - ` - )} +
    + ${repeat( + this.actions, + (action) => this._getKey(action), + (action, idx) => html` + + ${this.reOrderMode + ? html` + + +
    + +
    + ` + : ""} +
    + ` + )} +
    { + (evt.item as any).placeholder = + document.createComment("sort-placeholder"); + evt.item.after((evt.item as any).placeholder); + }, + onEnd: (evt: SortableEvent) => { + // put back in original location + if ((evt.item as any).placeholder) { + (evt.item as any).placeholder.replaceWith(evt.item); + delete (evt.item as any).placeholder; + } + this._dragged(evt); + }, + }); + } + + private _destroySortable() { + this._sortable?.destroy(); + this._sortable = undefined; + } + private _getKey(action: Action) { if (!this._actionKeys.has(action)) { this._actionKeys.set(action, Math.random().toString()); @@ -121,12 +195,24 @@ export default class HaAutomationAction extends LitElement { fireEvent(this, "value-changed", { value: actions }); } - private _move(ev: CustomEvent) { - // Prevent possible parent action-row from also moving - ev.stopPropagation(); - + private _moveUp(ev) { const index = (ev.target as any).index; - const newIndex = ev.detail.direction === "up" ? index - 1 : index + 1; + const newIndex = index - 1; + this._move(index, newIndex); + } + + private _moveDown(ev) { + const index = (ev.target as any).index; + const newIndex = index + 1; + this._move(index, newIndex); + } + + private _dragged(ev: SortableEvent): void { + if (ev.oldIndex === ev.newIndex) return; + this._move(ev.oldIndex!, ev.newIndex!); + } + + private _move(index: number, newIndex: number) { const actions = this.actions.concat(); const action = actions.splice(index, 1)[0]; actions.splice(newIndex, 0, action); @@ -177,16 +263,27 @@ export default class HaAutomationAction extends LitElement { ); static get styles(): CSSResultGroup { - return css` - ha-automation-action-row { - display: block; - margin-bottom: 16px; - scroll-margin-top: 48px; - } - ha-svg-icon { - height: 20px; - } - `; + return [ + sortableStyles, + css` + ha-automation-action-row { + display: block; + margin-bottom: 16px; + scroll-margin-top: 48px; + } + ha-svg-icon { + height: 20px; + } + .handle { + cursor: move; + padding: 12px; + } + .handle ha-svg-icon { + pointer-events: none; + height: 24px; + } + `, + ]; } } diff --git a/src/panels/config/automation/action/types/ha-automation-action-choose.ts b/src/panels/config/automation/action/types/ha-automation-action-choose.ts index 620d8e39ad..67a70e4304 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-choose.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-choose.ts @@ -17,6 +17,8 @@ export class HaChooseAction extends LitElement implements ActionElement { @property() public action!: ChooseAction; + @property({ type: Boolean }) public reOrderMode = false; + @state() private _showDefault = false; public static get defaultConfig() { @@ -52,6 +54,7 @@ export class HaChooseAction extends LitElement implements ActionElement { diff --git a/src/panels/config/automation/action/types/ha-automation-action-if.ts b/src/panels/config/automation/action/types/ha-automation-action-if.ts index 0424b8cf79..8725e52dc0 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-if.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-if.ts @@ -15,6 +15,8 @@ export class HaIfAction extends LitElement implements ActionElement { @property({ attribute: false }) public action!: IfAction; + @property({ type: Boolean }) public reOrderMode = false; + @state() private _showElse = false; public static get defaultConfig() { @@ -35,8 +37,9 @@ export class HaIfAction extends LitElement implements ActionElement {

    @@ -46,6 +49,7 @@ export class HaIfAction extends LitElement implements ActionElement {

    @@ -58,6 +62,7 @@ export class HaIfAction extends LitElement implements ActionElement { diff --git a/src/panels/config/automation/action/types/ha-automation-action-parallel.ts b/src/panels/config/automation/action/types/ha-automation-action-parallel.ts index 37dbfb429e..f4d56bbf91 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-parallel.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-parallel.ts @@ -14,6 +14,8 @@ export class HaParallelAction extends LitElement implements ActionElement { @property({ attribute: false }) public action!: ParallelAction; + @property({ type: Boolean }) public reOrderMode = false; + public static get defaultConfig() { return { parallel: [], @@ -26,6 +28,7 @@ export class HaParallelAction extends LitElement implements ActionElement { return html` diff --git a/src/panels/config/automation/action/types/ha-automation-action-repeat.ts b/src/panels/config/automation/action/types/ha-automation-action-repeat.ts index 6b78e6c0cf..c7dc7c8eab 100644 --- a/src/panels/config/automation/action/types/ha-automation-action-repeat.ts +++ b/src/panels/config/automation/action/types/ha-automation-action-repeat.ts @@ -25,6 +25,8 @@ export class HaRepeatAction extends LitElement implements ActionElement { @property({ attribute: false }) public action!: RepeatAction; + @property({ type: Boolean }) public reOrderMode = false; + public static get defaultConfig() { return { repeat: { count: 2, sequence: [] } }; } @@ -95,6 +97,7 @@ export class HaRepeatAction extends LitElement implements ActionElement { diff --git a/src/panels/config/automation/automation-mode-dialog/dialog-automation-mode.ts b/src/panels/config/automation/automation-mode-dialog/dialog-automation-mode.ts new file mode 100644 index 0000000000..270eaed01e --- /dev/null +++ b/src/panels/config/automation/automation-mode-dialog/dialog-automation-mode.ts @@ -0,0 +1,167 @@ +import "@material/mwc-button"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { createCloseHeading } from "../../../../components/ha-dialog"; +import "../../../../components/ha-textfield"; +import "../../../../components/ha-select"; +import { HassDialog } from "../../../../dialogs/make-dialog-manager"; +import { haStyle, haStyleDialog } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import type { AutomationModeDialog } from "./show-dialog-automation-mode"; +import { + AUTOMATION_DEFAULT_MAX, + AUTOMATION_DEFAULT_MODE, +} from "../../../../data/automation"; +import { documentationUrl } from "../../../../util/documentation-url"; +import { isMaxMode, MODES } from "../../../../data/script"; +import "@material/mwc-list/mwc-list-item"; +import { stopPropagation } from "../../../../common/dom/stop_propagation"; + +@customElement("ha-dialog-automation-mode") +class DialogAutomationMode extends LitElement implements HassDialog { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _opened = false; + + private _params!: AutomationModeDialog; + + @state() private _newMode: typeof MODES[number] = AUTOMATION_DEFAULT_MODE; + + @state() private _newMax?: number; + + public showDialog(params: AutomationModeDialog): void { + this._opened = true; + this._params = params; + this._newMode = params.config.mode || AUTOMATION_DEFAULT_MODE; + this._newMax = isMaxMode(this._newMode) + ? params.config.max || AUTOMATION_DEFAULT_MAX + : undefined; + } + + public closeDialog(): void { + this._params.onClose(); + + if (this._opened) { + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + this._opened = false; + } + + protected render(): TemplateResult { + if (!this._opened) { + return html``; + } + + return html` + + ${this.hass.localize( + "ui.panel.config.automation.editor.modes.learn_more" + )} + `} + > + ${MODES.map( + (mode) => html` + + ${this.hass.localize( + `ui.panel.config.automation.editor.modes.${mode}` + ) || mode} + + ` + )} + + ${isMaxMode(this._newMode) + ? html` +
    + + ` + : html``} + + + ${this.hass.localize("ui.dialogs.generic.cancel")} + + + ${this.hass.localize("ui.panel.config.automation.editor.change_mode")} + +
    + `; + } + + private _modeChanged(ev) { + const mode = ev.target.value; + this._newMode = mode; + if (!isMaxMode(mode)) { + this._newMax = undefined; + } else if (!this._newMax) { + this._newMax = AUTOMATION_DEFAULT_MAX; + } + } + + private _valueChanged(ev: CustomEvent) { + ev.stopPropagation(); + const target = ev.target as any; + if (target.name === "max") { + this._newMax = Number(target.value); + } + } + + private _save(): void { + this._params.updateAutomation({ + ...this._params.config, + mode: this._newMode, + max: this._newMax, + }); + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + haStyleDialog, + css` + ha-select, + ha-textfield { + display: block; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-dialog-automation-mode": DialogAutomationMode; + } +} diff --git a/src/panels/config/automation/automation-mode-dialog/show-dialog-automation-mode.ts b/src/panels/config/automation/automation-mode-dialog/show-dialog-automation-mode.ts new file mode 100644 index 0000000000..3847ec252d --- /dev/null +++ b/src/panels/config/automation/automation-mode-dialog/show-dialog-automation-mode.ts @@ -0,0 +1,22 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; +import type { AutomationConfig } from "../../../../data/automation"; + +export const loadAutomationModeDialog = () => + import("./dialog-automation-mode"); + +export interface AutomationModeDialog { + config: AutomationConfig; + updateAutomation: (config: AutomationConfig) => void; + onClose: () => void; +} + +export const showAutomationModeDialog = ( + element: HTMLElement, + dialogParams: AutomationModeDialog +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "ha-dialog-automation-mode", + dialogImport: loadAutomationModeDialog, + dialogParams, + }); +}; diff --git a/src/panels/config/automation/automation-rename-dialog/dialog-automation-rename.ts b/src/panels/config/automation/automation-rename-dialog/dialog-automation-rename.ts new file mode 100644 index 0000000000..e97976e3f7 --- /dev/null +++ b/src/panels/config/automation/automation-rename-dialog/dialog-automation-rename.ts @@ -0,0 +1,157 @@ +import "@material/mwc-button"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { createCloseHeading } from "../../../../components/ha-dialog"; +import { HassDialog } from "../../../../dialogs/make-dialog-manager"; +import { haStyle, haStyleDialog } from "../../../../resources/styles"; +import type { HomeAssistant } from "../../../../types"; +import type { AutomationRenameDialog } from "./show-dialog-automation-rename"; +import "../../../../components/ha-textarea"; +import "../../../../components/ha-alert"; +import "../../../../components/ha-textfield"; + +@customElement("ha-dialog-automation-rename") +class DialogAutomationRename extends LitElement implements HassDialog { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _opened = false; + + @state() private _error?: string; + + private _params!: AutomationRenameDialog; + + private _newName?: string; + + private _newDescription?: string; + + public showDialog(params: AutomationRenameDialog): void { + this._opened = true; + this._params = params; + this._newName = + params.config.alias || + this.hass.localize("ui.panel.config.automation.editor.default_name"); + this._newDescription = params.config.description || ""; + } + + public closeDialog(): void { + this._params.onClose(); + + if (this._opened) { + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + this._opened = false; + } + + protected render(): TemplateResult { + if (!this._opened) { + return html``; + } + return html` + + ${this._error + ? html`${this.hass.localize( + "ui.panel.config.automation.editor.missing_name" + )}` + : ""} + + + + + + ${this.hass.localize("ui.dialogs.generic.cancel")} + + + ${this.hass.localize( + this._params.config.alias + ? "ui.panel.config.automation.editor.rename" + : "ui.panel.config.automation.editor.save" + )} + + + `; + } + + private _valueChanged(ev: CustomEvent) { + ev.stopPropagation(); + const target = ev.target as any; + if (target.name === "description") { + this._newDescription = target.value; + } else { + this._newName = target.value; + } + } + + private _save(): void { + if (!this._newName) { + this._error = "Name is required"; + return; + } + this._params.updateAutomation({ + ...this._params.config, + alias: this._newName, + description: this._newDescription, + }); + this.closeDialog(); + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + haStyleDialog, + css` + ha-textfield, + ha-textarea { + display: block; + } + ha-alert { + display: block; + margin-bottom: 16px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-dialog-automation-rename": DialogAutomationRename; + } +} diff --git a/src/panels/config/automation/automation-rename-dialog/show-dialog-automation-rename.ts b/src/panels/config/automation/automation-rename-dialog/show-dialog-automation-rename.ts new file mode 100644 index 0000000000..cf0fd4dd36 --- /dev/null +++ b/src/panels/config/automation/automation-rename-dialog/show-dialog-automation-rename.ts @@ -0,0 +1,22 @@ +import { fireEvent } from "../../../../common/dom/fire_event"; +import type { AutomationConfig } from "../../../../data/automation"; + +export const loadAutomationRenameDialog = () => + import("./dialog-automation-rename"); + +export interface AutomationRenameDialog { + config: AutomationConfig; + updateAutomation: (config: AutomationConfig) => void; + onClose: () => void; +} + +export const showAutomationRenameDialog = ( + element: HTMLElement, + dialogParams: AutomationRenameDialog +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "ha-dialog-automation-rename", + dialogImport: loadAutomationRenameDialog, + dialogParams, + }); +}; diff --git a/src/panels/config/automation/blueprint-automation-editor.ts b/src/panels/config/automation/blueprint-automation-editor.ts index be9c0ec9c1..cb69701510 100644 --- a/src/panels/config/automation/blueprint-automation-editor.ts +++ b/src/panels/config/automation/blueprint-automation-editor.ts @@ -10,6 +10,7 @@ import "../../../components/ha-markdown"; import "../../../components/ha-selector/ha-selector"; import "../../../components/ha-settings-row"; import "../../../components/ha-textfield"; +import "../../../components/ha-alert"; import { BlueprintAutomationConfig } from "../../../data/automation"; import { BlueprintOrError, @@ -49,26 +50,23 @@ export class HaBlueprintAutomationEditor extends LitElement { protected render() { const blueprint = this._blueprint; return html` -

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

    - -
    - -
    -
    - + ${this.stateObj?.state === "off" + ? html` + + ${this.hass.localize( + "ui.panel.config.automation.editor.disabled" + )} + + ${this.hass.localize( + "ui.panel.config.automation.editor.enable" + )} + + + ` + : ""} + ${this.config.description + ? html`

    ${this.config.description}

    ` + : ""} { + if (!this.hass || !this.stateObj) { return; } - const newVal = target.value; - if ((this.config![name] || "") === newVal) { - return; - } - fireEvent(this, "value-changed", { - value: { ...this.config!, [name]: newVal }, + await this.hass.callService("automation", "turn_on", { + entity_id: this.stateObj.entity_id, }); } @@ -222,7 +213,7 @@ export class HaBlueprintAutomationEditor extends LitElement { display: block; } ha-card.blueprint { - margin: 24px auto; + margin: 0 auto; } .padding { padding: 16px; @@ -233,7 +224,6 @@ export class HaBlueprintAutomationEditor extends LitElement { .blueprint-picker-container { padding: 0 16px 16px; } - ha-textarea, ha-textfield, ha-blueprint-picker { display: block; @@ -251,12 +241,19 @@ export class HaBlueprintAutomationEditor extends LitElement { p { margin-bottom: 0; } + .description { + margin-bottom: 16px; + } ha-settings-row { --paper-time-input-justify-content: flex-end; --settings-row-content-width: 100%; --settings-row-prefix-display: contents; border-top: 1px solid var(--divider-color); } + ha-alert { + margin-bottom: 16px; + display: block; + } `, ]; } diff --git a/src/panels/config/automation/condition/ha-automation-condition-editor.ts b/src/panels/config/automation/condition/ha-automation-condition-editor.ts index bcb3727209..3cd54473cb 100644 --- a/src/panels/config/automation/condition/ha-automation-condition-editor.ts +++ b/src/panels/config/automation/condition/ha-automation-condition-editor.ts @@ -28,6 +28,8 @@ export default class HaAutomationConditionEditor extends LitElement { @property({ type: Boolean }) public yamlMode = false; + @property({ type: Boolean }) public reOrderMode = false; + private _processedCondition = memoizeOne((condition) => expandConditionWithShorthand(condition) ); @@ -60,7 +62,11 @@ export default class HaAutomationConditionEditor extends LitElement {
    ${dynamicElement( `ha-automation-condition-${condition.condition}`, - { hass: this.hass, condition: condition } + { + hass: this.hass, + condition: condition, + reOrderMode: this.reOrderMode, + } )}
    `} 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 8c2f1204b5..dcc93b8015 100644 --- a/src/panels/config/automation/condition/ha-automation-condition-row.ts +++ b/src/panels/config/automation/condition/ha-automation-condition-row.ts @@ -70,6 +70,10 @@ export default class HaAutomationConditionRow extends LitElement { @property() public condition!: Condition; + @property({ type: Boolean }) public hideMenu = false; + + @property({ type: Boolean }) public reOrderMode = false; + @state() private _yamlMode = false; @state() private _warnings?: string[]; @@ -103,96 +107,106 @@ export default class HaAutomationConditionRow extends LitElement { )} - - - + + ${this.hideMenu + ? "" + : html` + + + - - ${this.hass.localize( - "ui.panel.config.automation.editor.conditions.test" - )} - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.conditions.rename" - )} - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.duplicate" - )} - - + + ${this.hass.localize( + "ui.panel.config.automation.editor.conditions.test" + )} + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.conditions.rename" + )} + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.duplicate" + )} + + -
  • +
  • - - ${this.hass.localize("ui.panel.config.automation.editor.edit_ui")} - ${!this._yamlMode - ? html`` - : ``} - + + ${this.hass.localize( + "ui.panel.config.automation.editor.edit_ui" + )} + ${!this._yamlMode + ? html`` + : ``} + - - ${this.hass.localize( - "ui.panel.config.automation.editor.edit_yaml" - )} - ${this._yamlMode - ? html`` - : ``} - + + ${this.hass.localize( + "ui.panel.config.automation.editor.edit_yaml" + )} + ${this._yamlMode + ? html`` + : ``} + -
  • +
  • - - ${this.condition.enabled === false - ? this.hass.localize( - "ui.panel.config.automation.editor.actions.enable" - ) - : this.hass.localize( - "ui.panel.config.automation.editor.actions.disable" - )} - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.delete" - )} - - -
    + + ${this.condition.enabled === false + ? this.hass.localize( + "ui.panel.config.automation.editor.actions.enable" + ) + : this.hass.localize( + "ui.panel.config.automation.editor.actions.disable" + )} + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.delete" + )} + + +
    + `}
    diff --git a/src/panels/config/automation/condition/ha-automation-condition.ts b/src/panels/config/automation/condition/ha-automation-condition.ts index 67233bbfe2..8815d15cb1 100644 --- a/src/panels/config/automation/condition/ha-automation-condition.ts +++ b/src/panels/config/automation/condition/ha-automation-condition.ts @@ -1,14 +1,15 @@ -import { mdiPlus } from "@mdi/js"; -import { repeat } from "lit/directives/repeat"; -import deepClone from "deep-clone-simple"; import "@material/mwc-button"; +import type { ActionDetail } from "@material/mwc-list"; +import { mdiArrowDown, mdiArrowUp, mdiDrag, mdiPlus } from "@mdi/js"; +import deepClone from "deep-clone-simple"; import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { customElement, property } from "lit/decorators"; +import { repeat } from "lit/directives/repeat"; import memoizeOne from "memoize-one"; -import type { ActionDetail } from "@material/mwc-list"; +import type { SortableEvent } from "sortablejs"; import { fireEvent } from "../../../../common/dom/fire_event"; -import "../../../../components/ha-svg-icon"; import "../../../../components/ha-button-menu"; +import "../../../../components/ha-svg-icon"; import type { Condition } from "../../../../data/automation"; import type { HomeAssistant } from "../../../../types"; import "./ha-automation-condition-row"; @@ -16,6 +17,14 @@ import type HaAutomationConditionRow from "./ha-automation-condition-row"; // Uncommenting these and this element doesn't load // import "./types/ha-automation-condition-not"; // import "./types/ha-automation-condition-or"; +import { stringCompare } from "../../../../common/string/compare"; +import type { LocalizeFunc } from "../../../../common/translations/localize"; +import type { HaSelect } from "../../../../components/ha-select"; +import { CONDITION_TYPES } from "../../../../data/condition"; +import { + loadSortable, + SortableInstance, +} from "../../../../resources/sortable.ondemand"; import "./types/ha-automation-condition-and"; import "./types/ha-automation-condition-device"; import "./types/ha-automation-condition-numeric_state"; @@ -25,10 +34,7 @@ import "./types/ha-automation-condition-template"; import "./types/ha-automation-condition-time"; import "./types/ha-automation-condition-trigger"; import "./types/ha-automation-condition-zone"; -import { CONDITION_TYPES } from "../../../../data/condition"; -import { stringCompare } from "../../../../common/string/compare"; -import type { LocalizeFunc } from "../../../../common/translations/localize"; -import type { HaSelect } from "../../../../components/ha-select"; +import { sortableStyles } from "../../../../resources/ha-sortable-style"; @customElement("ha-automation-condition") export default class HaAutomationCondition extends LitElement { @@ -36,11 +42,23 @@ export default class HaAutomationCondition extends LitElement { @property() public conditions!: Condition[]; + @property({ type: Boolean }) public reOrderMode = false; + private _focusLastConditionOnChange = false; private _conditionKeys = new WeakMap(); + private _sortable?: SortableInstance; + protected updated(changedProperties: PropertyValues) { + if (changedProperties.has("reOrderMode")) { + if (this.reOrderMode) { + this._createSortable(); + } else { + this._destroySortable(); + } + } + if (!changedProperties.has("conditions")) { return; } @@ -82,19 +100,53 @@ export default class HaAutomationCondition extends LitElement { return html``; } return html` - ${repeat( - this.conditions, - (condition) => this._getKey(condition), - (cond, idx) => html` - - ` - )} +
    + ${repeat( + this.conditions, + (condition) => this._getKey(condition), + (cond, idx) => html` + + ${this.reOrderMode + ? html` + + +
    + +
    + ` + : ""} +
    + ` + )} +
    { + (evt.item as any).placeholder = + document.createComment("sort-placeholder"); + evt.item.after((evt.item as any).placeholder); + }, + onEnd: (evt: SortableEvent) => { + // put back in original location + if ((evt.item as any).placeholder) { + (evt.item as any).placeholder.replaceWith(evt.item); + delete (evt.item as any).placeholder; + } + this._dragged(evt); + }, + } + ); + } + + private _destroySortable() { + this._sortable?.destroy(); + this._sortable = undefined; + } + private _getKey(condition: Condition) { if (!this._conditionKeys.has(condition)) { this._conditionKeys.set(condition, Math.random().toString()); @@ -142,6 +224,30 @@ export default class HaAutomationCondition extends LitElement { fireEvent(this, "value-changed", { value: conditions }); } + private _moveUp(ev) { + const index = (ev.target as any).index; + const newIndex = index - 1; + this._move(index, newIndex); + } + + private _moveDown(ev) { + const index = (ev.target as any).index; + const newIndex = index + 1; + this._move(index, newIndex); + } + + private _dragged(ev: SortableEvent): void { + if (ev.oldIndex === ev.newIndex) return; + this._move(ev.oldIndex!, ev.newIndex!); + } + + private _move(index: number, newIndex: number) { + const conditions = this.conditions.concat(); + const condition = conditions.splice(index, 1)[0]; + conditions.splice(newIndex, 0, condition); + fireEvent(this, "value-changed", { value: conditions }); + } + private _conditionChanged(ev: CustomEvent) { ev.stopPropagation(); const conditions = [...this.conditions]; @@ -186,16 +292,27 @@ export default class HaAutomationCondition extends LitElement { ); static get styles(): CSSResultGroup { - return css` - ha-automation-condition-row { - display: block; - margin-bottom: 16px; - scroll-margin-top: 48px; - } - ha-svg-icon { - height: 20px; - } - `; + return [ + sortableStyles, + css` + ha-automation-condition-row { + display: block; + margin-bottom: 16px; + scroll-margin-top: 48px; + } + ha-svg-icon { + height: 20px; + } + .handle { + cursor: move; + padding: 12px; + } + .handle ha-svg-icon { + pointer-events: none; + height: 24px; + } + `, + ]; } } diff --git a/src/panels/config/automation/condition/types/ha-automation-condition-logical.ts b/src/panels/config/automation/condition/types/ha-automation-condition-logical.ts index 628c050663..0838feffc3 100644 --- a/src/panels/config/automation/condition/types/ha-automation-condition-logical.ts +++ b/src/panels/config/automation/condition/types/ha-automation-condition-logical.ts @@ -12,6 +12,8 @@ export class HaLogicalCondition extends LitElement implements ConditionElement { @property({ attribute: false }) public condition!: LogicalCondition; + @property({ type: Boolean }) public reOrderMode = false; + public static get defaultConfig() { return { conditions: [], @@ -24,6 +26,7 @@ export class HaLogicalCondition extends LitElement implements ConditionElement { .conditions=${this.condition.conditions || []} @value-changed=${this._valueChanged} .hass=${this.hass} + .reOrderMode=${this.reOrderMode} >
    `; } diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index 77cd37c48c..936c45eb3c 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -1,15 +1,17 @@ +import "@material/mwc-button"; import "@material/mwc-list/mwc-list-item"; import { mdiCheck, mdiContentDuplicate, mdiContentSave, + mdiDebugStepOver, mdiDelete, mdiDotsVertical, mdiInformationOutline, - mdiPencil, mdiPlay, mdiPlayCircleOutline, mdiRenameBox, + mdiSort, mdiStopCircleOutline, mdiTransitConnection, } from "@mdi/js"; @@ -24,7 +26,7 @@ import { PropertyValues, TemplateResult, } from "lit"; -import { property, state, query } from "lit/decorators"; +import { property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { fireEvent } from "../../../common/dom/fire_event"; import { navigate } from "../../../common/navigate"; @@ -48,7 +50,6 @@ import { import { showAlertDialog, showConfirmationDialog, - showPromptDialog, } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/ha-app-layout"; import "../../../layouts/hass-subpage"; @@ -57,9 +58,11 @@ import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; import { showToast } from "../../../util/toast"; import "../ha-config-section"; -import { configSections } from "../ha-panel-config"; +import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode"; +import { showAutomationRenameDialog } from "./automation-rename-dialog/show-dialog-automation-rename"; import "./blueprint-automation-editor"; import "./manual-automation-editor"; +import type { HaManualAutomationEditor } from "./manual-automation-editor"; declare global { interface HTMLElementTagNameMap { @@ -99,7 +102,10 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { @state() private _mode: "gui" | "yaml" = "gui"; - @query("ha-yaml-editor", true) private _editor?: HaYamlEditor; + @query("ha-yaml-editor", true) private _yamlEditor?: HaYamlEditor; + + @query("manual-automation-editor") + private _manualEditor?: HaManualAutomationEditor; private _configSubscriptions: Record< string, @@ -118,8 +124,28 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { .narrow=${this.narrow} .route=${this.route} .backCallback=${this._backTapped} - .tabs=${configSections.automations} + .header=${!this._config + ? "" + : this._config.alias || + this.hass.localize( + "ui.panel.config.automation.editor.default_name" + )} > + ${this._config?.id && !this.narrow + ? html` + + + ${this.hass.localize( + "ui.panel.config.automation.editor.show_trace" + )} + + + ` + : ""} - ${stateObj && this._config + ${stateObj && this._config && this.narrow ? html` ${this.hass.localize( @@ -158,11 +184,42 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { ` : ""} - + ${this.hass.localize("ui.panel.config.automation.editor.rename")} + ${this._config && !("use_blueprint" in this._config) + ? html` + + ${this.hass.localize( + "ui.panel.config.automation.editor.change_mode" + )} + + + ` + : ""} + + + ${this.hass.localize("ui.panel.config.automation.editor.re_order")} + + + - ${!stateObj || stateObj.state === "off" + ${stateObj?.state === "off" ? this.hass.localize("ui.panel.config.automation.editor.enable") : this.hass.localize("ui.panel.config.automation.editor.disable")} @@ -234,14 +291,6 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { ${this._config ? html` - ${this.narrow - ? html`${this._config!.alias || - this.hass.localize( - "ui.panel.config.automation.editor.default_name" - )}` - : ""}
    ${this._errors - ? html`
    ${this._errors}
    ` + ? html` + ${this._errors} + ` : ""} ${this._mode === "gui" - ? html` - ${this.narrow - ? "" - : html` -
    -

    - ${this._config!.alias || - this.hass.localize( - "ui.panel.config.automation.editor.default_name" - )} -

    - -
    - `} - ${"use_blueprint" in this._config - ? html` - - ` - : html` - - `} - ` + ? "use_blueprint" in this._config + ? html` + + ` + : html` + + ` : this._mode === "yaml" ? html` - ${!this.narrow + ${stateObj?.state === "off" ? html` - -
    - ${this._config.alias || - this.hass.localize( - "ui.panel.config.automation.editor.default_name" + + ${this.hass.localize( + "ui.panel.config.automation.editor.disabled" + )} + + ${this.hass.localize( + "ui.panel.config.automation.editor.enable" )} -
    -
    + + ` - : ``} + : ""} { - if (this._editor?.yaml) { - await copyToClipboard(this._editor.yaml); + if (this._yamlEditor?.yaml) { + await copyToClipboard(this._yamlEditor.yaml); showToast(this, { message: this.hass.localize("ui.common.copied_clipboard"), }); @@ -559,40 +591,46 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { this._mode = "yaml"; } - private async _promptAutomationAlias(): Promise { - const result = await showPromptDialog(this, { - title: this.hass.localize( - "ui.panel.config.automation.editor.automation_alias" - ), - inputLabel: this.hass.localize("ui.panel.config.automation.editor.alias"), - inputType: "string", - placeholder: this.hass.localize( - "ui.panel.config.automation.editor.default_name" - ), - defaultValue: this._config!.alias, - confirmText: this.hass.localize("ui.common.submit"), - }); - if (result) { - this._config!.alias = result; - this._dirty = true; - this.requestUpdate(); + private _toggleReOrderMode() { + if (this._manualEditor) { + this._manualEditor.reOrderMode = !this._manualEditor.reOrderMode; } - return result; + } + + private async _promptAutomationAlias(): Promise { + return new Promise((resolve) => { + showAutomationRenameDialog(this, { + config: this._config!, + updateAutomation: (config) => { + this._config = config; + this._dirty = true; + this.requestUpdate(); + resolve(); + }, + onClose: () => resolve(), + }); + }); + } + + private async _promptAutomationMode(): Promise { + return new Promise((resolve) => { + showAutomationModeDialog(this, { + config: this._config!, + updateAutomation: (config) => { + this._config = config; + this._dirty = true; + this.requestUpdate(); + resolve(); + }, + onClose: () => resolve(), + }); + }); } private async _saveAutomation(): Promise { const id = this.automationId || String(Date.now()); - if (!this._config!.alias) { - const alias = await this._promptAutomationAlias(); - if (!alias) { - showAlertDialog(this, { - text: this.hass.localize( - "ui.panel.config.automation.editor.missing_name" - ), - }); - return; - } - this._config!.alias = alias; + if (!this.automationId) { + await this._promptAutomationAlias(); } this.hass!.callApi( @@ -637,11 +675,6 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { ha-card { overflow: hidden; } - .errors { - padding: 20px; - font-weight: bold; - color: var(--error-color); - } .content { padding-bottom: 20px; } @@ -651,6 +684,9 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { flex-direction: column; padding-bottom: 0; } + .trace-link { + text-decoration: none; + } manual-automation-editor, blueprint-automation-editor { margin: 0 auto; diff --git a/src/panels/config/automation/manual-automation-editor.ts b/src/panels/config/automation/manual-automation-editor.ts index 2ac1affe3a..ebe14f3e1d 100644 --- a/src/panels/config/automation/manual-automation-editor.ts +++ b/src/panels/config/automation/manual-automation-editor.ts @@ -1,21 +1,21 @@ import "@material/mwc-button/mwc-button"; -import { mdiHelpCircle, mdiRobot } from "@mdi/js"; +import { mdiHelpCircle } from "@mdi/js"; import { HassEntity } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; import "../../../components/entity/ha-entity-toggle"; import "../../../components/ha-card"; +import "../../../components/ha-icon-button"; +import "../../../components/ha-alert"; import "../../../components/ha-textarea"; import "../../../components/ha-textfield"; -import "../../../components/ha-icon-button"; import { - AUTOMATION_DEFAULT_MODE, Condition, ManualAutomationConfig, Trigger, } from "../../../data/automation"; -import { Action, isMaxMode, MODES } from "../../../data/script"; +import { Action } from "../../../data/script"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; @@ -35,85 +35,47 @@ export class HaManualAutomationEditor extends LitElement { @property({ attribute: false }) public stateObj?: HassEntity; + @property({ type: Boolean, reflect: true, attribute: "re-order-mode" }) + public reOrderMode = false; + protected render() { return html` - - ${this.stateObj && this.stateObj.state === "off" - ? html`
    + ${this.stateObj?.state === "off" + ? html` + ${this.hass.localize( "ui.panel.config.automation.editor.disabled" )} -
    ` - : ""} - - -

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

    -
    - + ${this.hass.localize( + "ui.panel.config.automation.editor.enable" + )} + + + ` + : ""} + ${this.reOrderMode + ? html` + - ${this.hass.localize( - "ui.panel.config.automation.editor.modes.learn_more" - )} - `} > - ${MODES.map( - (mode) => html` - - ${this.hass.localize( - `ui.panel.config.automation.editor.modes.${mode}` - ) || mode} - - ` + ${this.hass.localize( + "ui.panel.config.automation.editor.re_order_mode.description" )} - - ${this.config.mode && isMaxMode(this.config.mode) - ? html` -
    - - ` - : html``} -
    -
    -
    - + + ${this.hass.localize( + "ui.panel.config.automation.editor.re_order_mode.exit" + )} + + + ` + : ""} + ${this.config.description + ? html`

    ${this.config.description}

    ` + : ""}

    ${this.hass.localize( @@ -140,6 +102,7 @@ export class HaManualAutomationEditor extends LitElement { .triggers=${this.config.trigger} @value-changed=${this._triggerChanged} .hass=${this.hass} + .reOrderMode=${this.reOrderMode} >
    @@ -168,6 +131,7 @@ export class HaManualAutomationEditor extends LitElement { .conditions=${this.config.condition || []} @value-changed=${this._conditionChanged} .hass=${this.hass} + .reOrderMode=${this.reOrderMode} >
    @@ -176,18 +140,20 @@ export class HaManualAutomationEditor extends LitElement { "ui.panel.config.automation.editor.actions.header" )}

    - - - +
    + + + +
    `; } - private _valueChanged(ev: CustomEvent) { - ev.stopPropagation(); - const target = ev.target as any; - const name = target.name; - if (!name) { - return; - } - let newVal = target.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) { - const mode = ev.target.value; - - if ( - mode === this.config!.mode || - (!this.config!.mode && mode === MODES[0]) - ) { - return; - } - const value = { - ...this.config!, - mode, - }; - - if (!isMaxMode(mode)) { - delete value.max; - } - - fireEvent(this, "value-changed", { - value, - }); + private _exitReOrderMode() { + this.reOrderMode = !this.reOrderMode; } private _triggerChanged(ev: CustomEvent): void { @@ -267,6 +196,15 @@ export class HaManualAutomationEditor extends LitElement { }); } + private async _enable(): Promise { + if (!this.hass || !this.stateObj) { + return; + } + await this.hass.callService("automation", "turn_on", { + entity_id: this.stateObj.entity_id, + }); + } + static get styles(): CSSResultGroup { return [ haStyle, @@ -280,11 +218,9 @@ export class HaManualAutomationEditor extends LitElement { .link-button-row { padding: 14px; } - ha-textarea, - ha-textfield { - display: block; + .description { + margin: 0; } - p { margin-bottom: 0; } @@ -300,6 +236,9 @@ export class HaManualAutomationEditor extends LitElement { display: flex; align-items: center; } + .header:first-child { + margin-top: -16px; + } .header .name { font-size: 20px; font-weight: 400; @@ -320,9 +259,6 @@ export class HaManualAutomationEditor extends LitElement { .card-content { padding: 16px; } - .card-content ha-textarea:first-child { - margin-top: -16px; - } .settings-icon { display: none; } @@ -340,6 +276,10 @@ export class HaManualAutomationEditor extends LitElement { border-top-right-radius: var(--ha-card-border-radius); border-top-left-radius: var(--ha-card-border-radius); } + ha-alert { + display: block; + margin-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 b3c32cdc92..2674664af9 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts @@ -87,6 +87,8 @@ export default class HaAutomationTriggerRow extends LitElement { @property({ attribute: false }) public trigger!: Trigger; + @property({ type: Boolean }) public hideMenu = false; + @state() private _warnings?: string[]; @state() private _yamlMode = false; @@ -128,97 +130,110 @@ export default class HaAutomationTriggerRow extends LitElement { > ${capitalizeFirstLetter(describeTrigger(this.trigger, this.hass))} - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.triggers.rename" - )} - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.duplicate" - )} - - + + ${this.hideMenu + ? "" + : html` + + - - ${this.hass.localize( - "ui.panel.config.automation.editor.triggers.edit_id" - )} - - + + ${this.hass.localize( + "ui.panel.config.automation.editor.triggers.rename" + )} + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.duplicate" + )} + + -
  • + + ${this.hass.localize( + "ui.panel.config.automation.editor.triggers.edit_id" + )} + + - - ${this.hass.localize("ui.panel.config.automation.editor.edit_ui")} - ${!yamlMode - ? html`` - : ``} - +
  • - - ${this.hass.localize( - "ui.panel.config.automation.editor.edit_yaml" - )} - ${yamlMode - ? html`` - : ``} - + + ${this.hass.localize( + "ui.panel.config.automation.editor.edit_ui" + )} + ${!yamlMode + ? html`` + : ``} + -
  • + + ${this.hass.localize( + "ui.panel.config.automation.editor.edit_yaml" + )} + ${yamlMode + ? html`` + : ``} + - - ${this.trigger.enabled === false - ? this.hass.localize( - "ui.panel.config.automation.editor.actions.enable" - ) - : this.hass.localize( - "ui.panel.config.automation.editor.actions.disable" - )} - - - - ${this.hass.localize( - "ui.panel.config.automation.editor.actions.delete" - )} - - -
    +
  • + + ${this.trigger.enabled === false + ? this.hass.localize( + "ui.panel.config.automation.editor.actions.enable" + ) + : this.hass.localize( + "ui.panel.config.automation.editor.actions.disable" + )} + + + + ${this.hass.localize( + "ui.panel.config.automation.editor.actions.delete" + )} + + +
    + `}
    (); + private _sortable?: SortableInstance; + protected render() { return html` - ${repeat( - this.triggers, - (trigger) => this._getKey(trigger), - (trg, idx) => html` - - ` - )} - - - - - ${this._processedTypes(this.hass.localize).map( - ([opt, label, icon]) => html` - - ${label} +
    + ${repeat( + this.triggers, + (trigger) => this._getKey(trigger), + (trg, idx) => html` + + ${this.reOrderMode + ? html` + + +
    + +
    + ` + : ""} +
    ` )} - +
    + + + + + ${this._processedTypes(this.hass.localize).map( + ([opt, label, icon]) => html` + + ${label} + ` + )} + +
    `; } protected updated(changedProps: PropertyValues) { super.updated(changedProps); + if (changedProps.has("reOrderMode")) { + if (this.reOrderMode) { + this._createSortable(); + } else { + this._destroySortable(); + } + } + if (changedProps.has("triggers") && this._focusLastTriggerOnChange) { this._focusLastTriggerOnChange = false; @@ -96,6 +144,36 @@ export default class HaAutomationTrigger extends LitElement { } } + private async _createSortable() { + const Sortable = await loadSortable(); + this._sortable = new Sortable( + this.shadowRoot!.querySelector(".triggers")!, + { + animation: 150, + fallbackClass: "sortable-fallback", + handle: ".handle", + onChoose: (evt: SortableEvent) => { + (evt.item as any).placeholder = + document.createComment("sort-placeholder"); + evt.item.after((evt.item as any).placeholder); + }, + onEnd: (evt: SortableEvent) => { + // put back in original location + if ((evt.item as any).placeholder) { + (evt.item as any).placeholder.replaceWith(evt.item); + delete (evt.item as any).placeholder; + } + this._dragged(evt); + }, + } + ); + } + + private _destroySortable() { + this._sortable?.destroy(); + this._sortable = undefined; + } + private _getKey(action: Trigger) { if (!this._triggerKeys.has(action)) { this._triggerKeys.set(action, Math.random().toString()); @@ -122,6 +200,30 @@ export default class HaAutomationTrigger extends LitElement { fireEvent(this, "value-changed", { value: triggers }); } + private _moveUp(ev) { + const index = (ev.target as any).index; + const newIndex = index - 1; + this._move(index, newIndex); + } + + private _moveDown(ev) { + const index = (ev.target as any).index; + const newIndex = index + 1; + this._move(index, newIndex); + } + + private _dragged(ev: SortableEvent): void { + if (ev.oldIndex === ev.newIndex) return; + this._move(ev.oldIndex!, ev.newIndex!); + } + + private _move(index: number, newIndex: number) { + const triggers = this.triggers.concat(); + const trigger = triggers.splice(index, 1)[0]; + triggers.splice(newIndex, 0, trigger); + fireEvent(this, "value-changed", { value: triggers }); + } + private _triggerChanged(ev: CustomEvent) { ev.stopPropagation(); const triggers = [...this.triggers]; @@ -166,16 +268,27 @@ export default class HaAutomationTrigger extends LitElement { ); static get styles(): CSSResultGroup { - return css` - ha-automation-trigger-row { - display: block; - margin-bottom: 16px; - scroll-margin-top: 48px; - } - ha-svg-icon { - height: 20px; - } - `; + return [ + sortableStyles, + css` + ha-automation-trigger-row { + display: block; + margin-bottom: 16px; + scroll-margin-top: 48px; + } + ha-svg-icon { + height: 20px; + } + .handle { + cursor: move; + padding: 12px; + } + .handle ha-svg-icon { + pointer-events: none; + height: 24px; + } + `, + ]; } } diff --git a/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts b/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts index 12cb696d80..18c702bf16 100644 --- a/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts +++ b/src/panels/config/automation/trigger/types/ha-automation-trigger-tag.ts @@ -58,9 +58,9 @@ export class HaTagTrigger extends LitElement implements TriggerElement { private _tagChanged(ev) { if ( - !ev.detail.value || + !ev.target.value || !this._tags || - this.trigger.tag_id === ev.detail.value + this.trigger.tag_id === ev.target.value ) { return; } diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index ddb82c4ca6..dae63448dd 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -60,12 +60,11 @@ import { import "../../../layouts/hass-error-screen"; import "../../../layouts/hass-tabs-subpage"; import { haStyle } from "../../../resources/styles"; -import type { HomeAssistant, Route } from "../../../types"; +import type { HomeAssistant } from "../../../types"; import { brandsUrl } from "../../../util/brands-url"; import { fileDownload } from "../../../util/file_download"; import "../../logbook/ha-logbook"; import "../ha-config-section"; -import { configSections } from "../ha-panel-config"; import "./device-detail/ha-device-entities-card"; import "./device-detail/ha-device-info-card"; import { showDeviceAutomationDialog } from "./device-detail/show-dialog-device-automation"; @@ -73,6 +72,7 @@ import { loadDeviceRegistryDetailDialog, showDeviceRegistryDetailDialog, } from "./device-registry-detail/show-dialog-device-registry-detail"; +import "../../../layouts/hass-subpage"; export interface EntityRegistryStateEntry extends EntityRegistryEntry { stateName?: string | null; @@ -96,23 +96,21 @@ export interface DeviceAlert { export class HaConfigDevicePage extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() public devices!: DeviceRegistryEntry[]; + @property({ attribute: false }) public devices!: DeviceRegistryEntry[]; - @property() public entries!: ConfigEntry[]; + @property({ attribute: false }) public entries!: ConfigEntry[]; - @property() public entities!: EntityRegistryEntry[]; + @property({ attribute: false }) public entities!: EntityRegistryEntry[]; - @property() public areas!: AreaRegistryEntry[]; + @property({ attribute: false }) public areas!: AreaRegistryEntry[]; @property() public deviceId!: string; @property({ type: Boolean, reflect: true }) public narrow!: boolean; - @property() public isWide!: boolean; + @property({ type: Boolean }) public isWide!: boolean; - @property() public showAdvanced!: boolean; - - @property() public route!: Route; + @property({ type: Boolean }) public showAdvanced!: boolean; @state() private _related?: RelatedResult; @@ -609,16 +607,12 @@ export class HaConfigDevicePage extends LitElement { : ""; return html` - - ${ - this.narrow - ? html` - ${deviceName} + - ` - : "" - }
    ${ - this.narrow - ? "" - : html` - - ` + area + ? html`` + : "" }
    ${ @@ -859,7 +834,7 @@ export class HaConfigDevicePage extends LitElement {
    - `; + `; } private async _getDiagnosticButtons(requestId: number): Promise { diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 61687ed80c..eb952f4b6c 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -319,7 +319,7 @@ export const configSections: { [name: string]: PageNavigation[] } = { translationKey: "hardware", iconPath: mdiMemory, iconColor: "#301A8E", - component: "hassio", + components: ["hassio", "hardware"], }, ], about: [ diff --git a/src/panels/config/hardware/ha-config-hardware.ts b/src/panels/config/hardware/ha-config-hardware.ts index 14795a97e3..6806eb5cb0 100644 --- a/src/panels/config/hardware/ha-config-hardware.ts +++ b/src/panels/config/hardware/ha-config-hardware.ts @@ -284,38 +284,38 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { ` : ""} - - -
    -
    - ${this.hass.localize("ui.panel.config.hardware.processor")} -
    -
    - ${this._systemStatusData?.cpu_percent || "-"}% -
    -
    -
    - -
    -
    - -
    -
    - ${this.hass.localize("ui.panel.config.hardware.memory")} -
    -
    - ${this._systemStatusData - ? html` + ${this._systemStatusData + ? html` +
    +
    + ${this.hass.localize( + "ui.panel.config.hardware.processor" + )} +
    +
    + ${this._systemStatusData.cpu_percent || "-"}% +
    +
    +
    + +
    +
    + +
    +
    + ${this.hass.localize("ui.panel.config.hardware.memory")} +
    +
    ${round(this._systemStatusData.memory_used_mb / 1024, 1)} GB / ${round( @@ -325,24 +325,23 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { 0 )} GB - ` - : "- GB / - GB"} -
    -
    -
    - -
    -
    +
    +
    +
    + +
    +
    ` + : ""}
    `; diff --git a/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard-router.ts b/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard-router.ts index 8b0963b190..758f8acf0e 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard-router.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard-router.ts @@ -60,7 +60,7 @@ class ZHAConfigDashboardRouter extends HassRouterPage { } else if (this._currentPage === "device") { el.ieee = this.routeTail.path.substr(1); } else if (this._currentPage === "visualization") { - el.zoomedDeviceId = this.routeTail.path.substr(1); + el.zoomedDeviceIdFromURL = this.routeTail.path.substr(1); } const searchParams = new URLSearchParams(window.location.search); diff --git a/src/panels/config/integrations/integration-panels/zha/zha-network-visualization-page.ts b/src/panels/config/integrations/integration-panels/zha/zha-network-visualization-page.ts index 82af56463b..b321222084 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-network-visualization-page.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-network-visualization-page.ts @@ -37,7 +37,10 @@ export class ZHANetworkVisualizationPage extends LitElement { @property({ type: Boolean }) public isWide!: boolean; @property() - public zoomedDeviceId?: string; + public zoomedDeviceIdFromURL?: string; + + @state() + private zoomedDeviceId?: string; @query("#visualization", true) private _visualization?: HTMLElement; @@ -64,6 +67,11 @@ export class ZHANetworkVisualizationPage extends LitElement { protected firstUpdated(changedProperties: PropertyValues): void { super.firstUpdated(changedProperties); + // prevent zoomedDeviceIdFromURL from being restored to zoomedDeviceId after the user clears it + if (this.zoomedDeviceIdFromURL) { + this.zoomedDeviceId = this.zoomedDeviceIdFromURL; + } + if (this.hass) { this._fetchData(); } diff --git a/src/panels/config/scene/ha-scene-dashboard.ts b/src/panels/config/scene/ha-scene-dashboard.ts index 07ca40e110..b112b0047c 100644 --- a/src/panels/config/scene/ha-scene-dashboard.ts +++ b/src/panels/config/scene/ha-scene-dashboard.ts @@ -11,9 +11,13 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import memoizeOne from "memoize-one"; -import { fireEvent } from "../../../common/dom/fire_event"; +import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event"; import { computeStateName } from "../../../common/entity/compute_state_name"; -import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; +import { navigate } from "../../../common/navigate"; +import { + DataTableColumnContainer, + RowClickedEvent, +} from "../../../components/data-table/ha-data-table"; import "../../../components/ha-button-related-filter-menu"; import "../../../components/ha-fab"; import "../../../components/ha-icon-button"; @@ -165,6 +169,8 @@ class HaSceneDashboard extends LitElement { )} @clear-filter=${this._clearFilter} hasFab + clickable + @row-click=${this._handleRowClicked} > ) { + const scene = this.scenes.find((a) => a.entity_id === ev.detail.id); + + if (scene?.attributes.id) { + navigate(`/config/scene/edit/${scene?.attributes.id}`); + } + } + private _relatedFilterChanged(ev: CustomEvent) { this._filterValue = ev.detail.value; if (!this._filterValue) { diff --git a/src/panels/config/scene/ha-scene-editor.ts b/src/panels/config/scene/ha-scene-editor.ts index 584792533f..d693bf2417 100644 --- a/src/panels/config/scene/ha-scene-editor.ts +++ b/src/panels/config/scene/ha-scene-editor.ts @@ -63,13 +63,13 @@ import { showAlertDialog, showConfirmationDialog, } from "../../../dialogs/generic/show-dialog-box"; +import "../../../layouts/hass-subpage"; import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; import { showToast } from "../../../util/toast"; import "../ha-config-section"; -import { configSections } from "../ha-panel-config"; interface DeviceEntities { id: string; @@ -214,17 +214,16 @@ export class HaSceneEditor extends SubscribeMixin( this._deviceEntityLookup, this._deviceRegistryEntries ); - const name = this._scene - ? computeStateName(this._scene) - : this.hass.localize("ui.panel.config.scene.editor.default_name"); return html` - ${this._errors ? html`
    ${this._errors}
    ` : ""} - ${this.narrow ? html` ${name} ` : ""}
    ${this._config ? html` - - ${!this.narrow - ? html` ${name} ` - : ""} -
    - ${this.hass.localize( - "ui.panel.config.scene.editor.introduction" - )} -
    +
    - +
    @@ -486,7 +476,7 @@ export class HaSceneEditor extends SubscribeMixin( > - + `; } @@ -963,6 +953,16 @@ export class HaSceneEditor extends SubscribeMixin( ha-card { overflow: hidden; } + .container { + display: flex; + justify-content: center; + margin-top: 24px; + } + .container > * { + max-width: 1040px; + flex: 1 1 auto; + } + .errors { padding: 20px; font-weight: bold; diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts index f740f48421..acfee65f3f 100644 --- a/src/panels/config/script/ha-script-editor.ts +++ b/src/panels/config/script/ha-script-editor.ts @@ -18,7 +18,7 @@ import { PropertyValues, TemplateResult, } from "lit"; -import { property, state, query } from "lit/decorators"; +import { property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; import { computeObjectId } from "../../../common/entity/compute_object_id"; @@ -51,13 +51,13 @@ import { } from "../../../data/script"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/ha-app-layout"; +import "../../../layouts/hass-subpage"; import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant, Route } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; import { showToast } from "../../../util/toast"; import { HaDeviceAction } from "../automation/action/types/ha-automation-action-device_id"; -import { configSections } from "../ha-panel-config"; import "./blueprint-script-editor"; export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { @@ -168,12 +168,12 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { }; return html` - ${this.hass.localize("ui.panel.config.automation.editor.edit_ui")} ${this._mode === "gui" @@ -228,13 +227,11 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { - ${this.hass.localize( - "ui.panel.config.script.picker.duplicate_script" - )} + ${this.hass.localize("ui.panel.config.script.picker.duplicate")} - ${this.hass.localize("ui.panel.config.script.editor.delete_script")} + ${this.hass.localize("ui.panel.config.script.picker.delete")} - ${this.narrow - ? html`${this._config?.alias}` - : ""}
    - + `; } diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index 8131ca19e8..ab828534bd 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -11,10 +11,14 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { formatDateTime } from "../../../common/datetime/format_date_time"; -import { fireEvent } from "../../../common/dom/fire_event"; +import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event"; import { computeStateName } from "../../../common/entity/compute_state_name"; +import { navigate } from "../../../common/navigate"; import { computeRTL } from "../../../common/util/compute_rtl"; -import { DataTableColumnContainer } from "../../../components/data-table/ha-data-table"; +import { + DataTableColumnContainer, + RowClickedEvent, +} from "../../../components/data-table/ha-data-table"; import "../../../components/ha-button-related-filter-menu"; import "../../../components/ha-fab"; import "../../../components/ha-icon-button"; @@ -191,6 +195,8 @@ class HaScriptPicker extends LitElement { )} @clear-filter=${this._clearFilter} hasFab + clickable + @row-click=${this._handleRowClicked} > ) { + navigate(`/config/script/edit/${ev.detail.id}`); + } + private _runScript = async (ev) => { ev.stopPropagation(); const script = ev.currentTarget.script as HassEntity; diff --git a/src/panels/logbook/ha-logbook-renderer.ts b/src/panels/logbook/ha-logbook-renderer.ts index 81d4424974..c79bdc9e34 100644 --- a/src/panels/logbook/ha-logbook-renderer.ts +++ b/src/panels/logbook/ha-logbook-renderer.ts @@ -34,6 +34,8 @@ import { } from "../../resources/styles"; import { HomeAssistant } from "../../types"; import { brandsUrl } from "../../util/brands-url"; +import "../../components/ha-icon-next"; +import { navigate } from "../../common/navigate"; declare global { interface HASSDomEvents { @@ -156,8 +158,26 @@ class HaLogbookRenderer extends LitElement { }) : undefined; + const traceContext = + triggerDomains.includes(item.domain!) && + item.context_id! in this.traceContexts + ? this.traceContexts[item.context_id!] + : undefined; + + const hasTrace = traceContext !== undefined; + return html` -
    +
    ${index === 0 || (item?.when && previous?.when && @@ -186,15 +206,16 @@ class HaLogbookRenderer extends LitElement {
    ${!this.noName // Used for more-info panel (single entity case) - ? this._renderEntity(item.entity_id, item.name) + ? this._renderEntity(item.entity_id, item.name, hasTrace) : ""} ${this._renderMessage( item, seenEntityIds, domain, - historicStateObj + historicStateObj, + hasTrace )} - ${this._renderContextMessage(item, seenEntityIds)} + ${this._renderContextMessage(item, seenEntityIds, hasTrace)}
    ${item.context_user_id ? html`${this._renderUser(item)}` : ""} - ${triggerDomains.includes(item.domain!) && - item.context_id! in this.traceContexts - ? html` - - - ${this.hass.localize( - "ui.components.logbook.show_trace" - )} - ` + ${hasTrace + ? `- ${this.hass.localize( + "ui.components.logbook.show_trace" + )}` : ""}
    + ${hasTrace ? html`` : ""}
    `; @@ -258,7 +261,8 @@ class HaLogbookRenderer extends LitElement { item: LogbookEntry, seenEntityIds: string[], domain?: string, - historicStateObj?: HassEntity + historicStateObj?: HassEntity, + noLink?: boolean ) { if (item.entity_id) { if (item.state) { @@ -291,7 +295,8 @@ class HaLogbookRenderer extends LitElement { ? stripEntityId(message, item.context_entity_id) : message, seenEntityIds, - undefined + undefined, + noLink ) : ""; } @@ -307,7 +312,8 @@ class HaLogbookRenderer extends LitElement { private _renderUnseenContextSourceEntity( item: LogbookEntry, - seenEntityIds: string[] + seenEntityIds: string[], + noLink: boolean ) { if ( !item.context_entity_id || @@ -320,11 +326,16 @@ class HaLogbookRenderer extends LitElement { // described event. return html` (${this._renderEntity( item.context_entity_id, - item.context_entity_id_name + item.context_entity_id_name, + noLink )})`; } - private _renderContextMessage(item: LogbookEntry, seenEntityIds: string[]) { + private _renderContextMessage( + item: LogbookEntry, + seenEntityIds: string[], + noLink: boolean + ) { // State change if (item.context_state) { const historicStateObj = @@ -337,7 +348,11 @@ class HaLogbookRenderer extends LitElement { return html`${this.hass.localize( "ui.components.logbook.triggered_by_state_of" )} - ${this._renderEntity(item.context_entity_id, item.context_entity_id_name)} + ${this._renderEntity( + item.context_entity_id, + item.context_entity_id_name, + noLink + )} ${historicStateObj ? localizeStateMessage( this.hass, @@ -379,11 +394,17 @@ class HaLogbookRenderer extends LitElement { ? "ui.components.logbook.triggered_by_automation" : "ui.components.logbook.triggered_by_script" )} - ${this._renderEntity(item.context_entity_id, item.context_entity_id_name)} + ${this._renderEntity( + item.context_entity_id, + item.context_entity_id_name, + noLink + )} ${item.context_message ? this._formatMessageWithPossibleEntity( contextTriggerSource, - seenEntityIds + seenEntityIds, + undefined, + noLink ) : ""}`; } @@ -394,14 +415,16 @@ class HaLogbookRenderer extends LitElement { ${this._formatMessageWithPossibleEntity( item.context_message, seenEntityIds, - item.context_entity_id + item.context_entity_id, + noLink )} - ${this._renderUnseenContextSourceEntity(item, seenEntityIds)}`; + ${this._renderUnseenContextSourceEntity(item, seenEntityIds, noLink)}`; } private _renderEntity( entityId: string | undefined, - entityName: string | undefined + entityName: string | undefined, + noLink?: boolean ) { const hasState = entityId && entityId in this.hass.states; const displayName = @@ -412,19 +435,22 @@ class HaLogbookRenderer extends LitElement { if (!hasState) { return displayName; } - return html``; + return noLink + ? displayName + : html``; } private _formatMessageWithPossibleEntity( message: string, seenEntities: string[], - possibleEntity?: string + possibleEntity?: string, + noLink?: boolean ) { // // As we are looking at a log(book), we are doing entity_id @@ -449,7 +475,8 @@ class HaLogbookRenderer extends LitElement { return html`${messageParts.join(" ")} ${this._renderEntity( entityId, - this.hass.states[entityId].attributes.friendly_name + this.hass.states[entityId].attributes.friendly_name, + noLink )} ${messageEnd.join(" ")}`; } @@ -475,7 +502,7 @@ class HaLogbookRenderer extends LitElement { message.length - possibleEntityName.length ); return html`${message} - ${this._renderEntity(possibleEntity, possibleEntityName)}`; + ${this._renderEntity(possibleEntity, possibleEntityName, noLink)}`; } } return message; @@ -494,8 +521,12 @@ class HaLogbookRenderer extends LitElement { }); } - private _close(): void { - setTimeout(() => fireEvent(this, "closed"), 500); + _handleClick(ev) { + if (!ev.currentTarget.traceLink) { + return; + } + navigate(ev.currentTarget.traceLink); + fireEvent(this, "closed"); } static get styles(): CSSResultGroup { @@ -520,10 +551,20 @@ class HaLogbookRenderer extends LitElement { padding: 8px 16px; box-sizing: border-box; border-top: 1px solid var(--divider-color); + justify-content: space-between; + align-items: center; } - .entry.no-entity, - .no-name .entry { + ha-icon-next { + color: var(--secondary-text-color); + } + + .clickable { + cursor: pointer; + } + + :not(.clickable) .entry.no-entity, + :not(.clickable) .no-name .entry { cursor: default; } diff --git a/src/panels/logbook/ha-logbook.ts b/src/panels/logbook/ha-logbook.ts index f0885b4ca7..57ae5ac00a 100644 --- a/src/panels/logbook/ha-logbook.ts +++ b/src/panels/logbook/ha-logbook.ts @@ -377,16 +377,32 @@ export class HaLogbook extends LitElement { return; } const nonExpiredRecords = this._nonExpiredRecords(purgeBeforePythonTime); - this._logbookEntries = !nonExpiredRecords.length - ? // All existing entries expired - newEntries - : newEntries[0].when >= nonExpiredRecords[0].when - ? // The new records are newer than the old records - // append the old records to the end of the new records - newEntries.concat(nonExpiredRecords) - : // The new records are older than the old records - // append the new records to the end of the old records - nonExpiredRecords.concat(newEntries); + + // Entries are sorted in descending order with newest first. + if (!nonExpiredRecords.length) { + // We have no records left, so we can just replace the list + this._logbookEntries = newEntries; + } else if ( + newEntries[newEntries.length - 1].when > // oldest new entry + nonExpiredRecords[0].when // newest old entry + ) { + // The new records are newer than the old records + // append the old records to the end of the new records + this._logbookEntries = newEntries.concat(nonExpiredRecords); + } else if ( + nonExpiredRecords[nonExpiredRecords.length - 1].when > // oldest old entry + newEntries[0].when // newest new entry + ) { + // The new records are older than the old records + // append the new records to the end of the old records + this._logbookEntries = nonExpiredRecords.concat(newEntries); + } else { + // The new records are in the middle of the old records + // so we need to re-sort them + this._logbookEntries = nonExpiredRecords + .concat(newEntries) + .sort((a, b) => b.when - a.when); + } }; private _updateTraceContexts = throttle(async () => { diff --git a/src/translations/en.json b/src/translations/en.json index e38adf3c96..c73f2ac15a 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -301,6 +301,7 @@ "refresh": "Refresh", "cancel": "Cancel", "delete": "Delete", + "duplicate": "Duplicate", "remove": "Remove", "enable": "Enable", "disable": "Disable", @@ -1790,9 +1791,9 @@ "edit_automation": "Edit automation", "dev_automation": "Debug automation", "show_info_automation": "Show info about automation", - "delete": "Delete", + "delete": "[%key:ui::common::delete%]", "delete_confirm": "Are you sure you want to delete this automation?", - "duplicate": "Duplicate", + "duplicate": "[%key:ui::common::duplicate%]", "disabled": "Disabled", "headers": { "toggle": "Enable/disable", @@ -1824,7 +1825,6 @@ "rename": "[%key:ui::panel::config::automation::editor::triggers::rename%]", "show_trace": "Traces", "show_info": "Information", - "introduction": "Use automations to bring your home to life.", "default_name": "New Automation", "missing_name": "Cannot save automation without a name", "load_error_not_editable": "Only automations in automations.yaml are editable.", @@ -1836,6 +1836,12 @@ "automation_settings": "Automation settings", "move_up": "Move up", "move_down": "Move down", + "re_order": "Re-order", + "re_order_mode": { + "title": "Re-order mode", + "description": "You are in re-order mode, you can re-order your triggers, conditions and actions.", + "exit": "Exit" + }, "description": { "label": "Description", "placeholder": "Optional description", @@ -1847,6 +1853,7 @@ "no_blueprints": "You don't have any blueprints", "no_inputs": "This blueprint doesn't have any inputs." }, + "change_mode": "Change mode", "modes": { "label": "Mode", "learn_more": "Learn about modes", @@ -1869,11 +1876,11 @@ "add": "Add trigger", "id": "Trigger ID", "edit_id": "Edit ID", - "duplicate": "Duplicate", + "duplicate": "[%key:ui::common::duplicate%]", "rename": "Rename", "change_alias": "Rename trigger", "alias": "Trigger name", - "delete": "[%key:ui::panel::mailbox::delete_button%]", + "delete": "[%key:ui::common::delete%]", "delete_confirm": "Are you sure you want to delete this?", "unsupported_platform": "No visual editor support for platform: {platform}", "type_select": "Trigger type", @@ -1989,11 +1996,11 @@ "testing_pass": "Condition passes", "invalid_condition": "Invalid condition configuration", "test_failed": "Error occurred while testing condition", - "duplicate": "[%key:ui::panel::config::automation::editor::triggers::duplicate%]", + "duplicate": "[%key:ui::common::duplicate%]", "rename": "[%key:ui::panel::config::automation::editor::triggers::rename%]", "change_alias": "Rename condition", "alias": "Condition name", - "delete": "[%key:ui::panel::mailbox::delete_button%]", + "delete": "[%key:ui::common::delete%]", "delete_confirm": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm%]", "unsupported_condition": "No visual editor support for condition: {condition}", "type_select": "Condition type", @@ -2080,14 +2087,14 @@ "run": "Run", "run_action_error": "Error running action", "run_action_success": "Action run successfully", - "duplicate": "[%key:ui::panel::config::automation::editor::triggers::duplicate%]", + "duplicate": "[%key:ui::common::duplicate%]", "rename": "[%key:ui::panel::config::automation::editor::triggers::rename%]", "change_alias": "Rename action", "alias": "Action name", "enable": "Enable", "disable": "Disable", "disabled": "Disabled", - "delete": "[%key:ui::panel::mailbox::delete_button%]", + "delete": "[%key:ui::common::delete%]", "delete_confirm": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm%]", "unsupported_action": "No visual editor support for action: {action}", "type_select": "Action type", @@ -2260,7 +2267,7 @@ "header": "Script Editor", "introduction": "The script editor allows you to create and edit scripts. Please follow the link below to read the instructions to make sure that you have configured Home Assistant correctly.", "learn_more": "Learn more about scripts", - "no_scripts": "We couldn’t find any scripts", + "no_scripts": "We couldn't find any scripts", "add_script": "Add script", "show_info": "Show info about script", "run_script": "Run script", @@ -2270,8 +2277,8 @@ "name": "Name", "state": "State" }, - "duplicate_script": "Duplicate script", - "duplicate": "[%key:ui::panel::config::automation::picker::duplicate%]" + "delete": "[%key:ui::common::delete%]", + "duplicate": "[%key:ui::common::duplicate%]" }, "editor": { "alias": "Name", @@ -2298,7 +2305,6 @@ "load_error_not_editable": "Only scripts inside scripts.yaml are editable.", "load_error_unknown": "Error loading script ({err_no}).", "delete_confirm": "Are you sure you want to delete this script?", - "delete_script": "Delete script", "save_script": "Save script", "sequence": "Sequence", "sequence_sentence": "The sequence of actions of this script.", @@ -2314,7 +2320,7 @@ "introduction": "The scene editor allows you to create and edit scenes. Please follow the link below to read the instructions to make sure that you have configured Home Assistant correctly.", "learn_more": "Learn more about scenes", "pick_scene": "Pick scene to edit", - "no_scenes": "We couldn’t find any scenes", + "no_scenes": "We couldn't find any scenes", "add_scene": "Add scene", "only_editable": "Only scenes defined in scenes.yaml are editable.", "edit_scene": "Edit scene", @@ -2322,7 +2328,7 @@ "delete_scene": "Delete scene", "delete_confirm": "Are you sure you want to delete this scene?", "duplicate_scene": "Duplicate scene", - "duplicate": "Duplicate", + "duplicate": "[%key:ui::common::duplicate%]", "headers": { "activate": "Activate", "state": "State", @@ -2332,7 +2338,6 @@ } }, "editor": { - "introduction": "Use scenes to bring your home to life.", "default_name": "New Scene", "load_error_not_editable": "Only scenes in scenes.yaml are editable.", "load_error_unknown": "Error loading scene ({err_no}).", @@ -3855,7 +3860,7 @@ }, "entity": { "name": "Entity", - "description": "The Entity card gives you a quick overview of your entity’s state." + "description": "The Entity card gives you a quick overview of your entity's state." }, "button": { "name": "Button",