Add category and labels to automation/script save and rename dialog (#23240)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Jan-Philipp Benecke 2024-12-22 16:59:04 +01:00 committed by GitHub
parent 523c38a83e
commit 5cd6f22e99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 357 additions and 76 deletions

View File

@ -3,7 +3,7 @@
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
// leading edge and on the trailing.
export const debounce = <T extends any[]>(
func: (...args: T) => void,
@ -14,9 +14,7 @@ export const debounce = <T extends any[]>(
const debouncedFunc = (...args: T): void => {
const later = () => {
timeout = undefined;
if (!immediate) {
func(...args);
}
func(...args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);

View File

@ -19,6 +19,9 @@ export class HaFab extends FabBase {
margin-inline-end: 12px;
direction: var(--direction);
}
:disabled {
opacity: var(--light-disabled-opacity);
}
`,
// safari workaround - must be explicit
mainWindow.document.dir === "rtl"

View File

@ -30,7 +30,7 @@ export const PreventUnsavedMixin = <T extends Constructor<LitElement>>(
window.removeEventListener("beforeunload", this._handleUnload);
}
public willUpdate(changedProperties: PropertyValues): void {
protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (this.isDirty) {

View File

@ -2,19 +2,25 @@ import "@material/mwc-button";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { mdiClose, mdiPlus } from "@mdi/js";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-domain-icon";
import "../../../../components/ha-icon-picker";
import "../../../../components/ha-textarea";
import "../../../../components/ha-textfield";
import "../../../../components/ha-labels-picker";
import "../../category/ha-category-picker";
import "../../../../components/ha-expansion-panel";
import "../../../../components/chips/ha-chip-set";
import "../../../../components/chips/ha-assist-chip";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type {
AutomationRenameDialogParams,
EntityRegistryUpdate,
ScriptRenameDialogParams,
} from "./show-dialog-automation-rename";
@ -26,6 +32,10 @@ class DialogAutomationRename extends LitElement implements HassDialog {
@state() private _error?: string;
@state() private _visibleOptionals: string[] = [];
@state() private _entryUpdates!: EntityRegistryUpdate;
private _params!: AutomationRenameDialogParams | ScriptRenameDialogParams;
private _newName?: string;
@ -46,6 +56,17 @@ class DialogAutomationRename extends LitElement implements HassDialog {
`ui.panel.config.${this._params.domain}.editor.default_name`
);
this._newDescription = params.config.description || "";
this._entryUpdates = params.entityRegistryUpdate || {
labels: params.entityRegistryEntry?.labels || [],
category: params.entityRegistryEntry?.categories[params.domain] || "",
};
this._visibleOptionals = [
this._newDescription ? "description" : "",
this._newIcon ? "icon" : "",
this._entryUpdates.category ? "category" : "",
this._entryUpdates.labels.length > 0 ? "labels" : "",
];
}
public closeDialog(): void {
@ -55,6 +76,19 @@ class DialogAutomationRename extends LitElement implements HassDialog {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
this._opened = false;
this._visibleOptionals = [];
}
protected _renderOptionalChip(id: string, label: string) {
if (this._visibleOptionals.includes(id)) {
return nothing;
}
return html`
<ha-assist-chip id=${id} @click=${this._addOptional} label=${label}>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-assist-chip>
`;
}
protected render() {
@ -66,15 +100,27 @@ class DialogAutomationRename extends LitElement implements HassDialog {
open
scrimClickAction
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize(
this._params.config.alias
? "ui.panel.config.automation.editor.rename"
: "ui.panel.config.automation.editor.save"
)
.heading=${this.hass.localize(
this._params.config.alias
? "ui.panel.config.automation.editor.rename"
: "ui.panel.config.automation.editor.save"
)}
>
<ha-dialog-header slot="heading">
<ha-icon-button
slot="navigationIcon"
dialogAction="cancel"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title"
>${this.hass.localize(
this._params.config.alias
? "ui.panel.config.automation.editor.rename"
: "ui.panel.config.automation.editor.save"
)}</span
>
</ha-dialog-header>
${this._error
? html`<ha-alert alert-type="error"
>${this.hass.localize(
@ -96,7 +142,8 @@ class DialogAutomationRename extends LitElement implements HassDialog {
@input=${this._valueChanged}
></ha-textfield>
${this._params.domain === "script"
${this._params.domain === "script" &&
this._visibleOptionals.includes("icon")
? html`
<ha-icon-picker
.hass=${this.hass}
@ -115,33 +162,97 @@ class DialogAutomationRename extends LitElement implements HassDialog {
</ha-icon-picker>
`
: nothing}
<ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label"
)}
.placeholder=${this.hass.localize(
"ui.panel.config.automation.editor.description.placeholder"
)}
name="description"
autogrow
.value=${this._newDescription}
@input=${this._valueChanged}
></ha-textarea>
${this._visibleOptionals.includes("description")
? html` <ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label"
)}
.placeholder=${this.hass.localize(
"ui.panel.config.automation.editor.description.placeholder"
)}
name="description"
autogrow
.value=${this._newDescription}
@input=${this._valueChanged}
></ha-textarea>`
: nothing}
${this._visibleOptionals.includes("category")
? html` <ha-category-picker
id="category"
.hass=${this.hass}
.scope=${this._params.domain}
.value=${this._entryUpdates.category}
@value-changed=${this._registryEntryChanged}
></ha-category-picker>`
: nothing}
${this._visibleOptionals.includes("labels")
? html` <ha-labels-picker
id="labels"
.hass=${this.hass}
.value=${this._entryUpdates.labels}
@value-changed=${this._registryEntryChanged}
></ha-labels-picker>`
: nothing}
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
${this.hass.localize("ui.dialogs.generic.cancel")}
</mwc-button>
<mwc-button @click=${this._save} slot="primaryAction">
${this.hass.localize(
this._params.config.alias
? "ui.panel.config.automation.editor.rename"
: "ui.panel.config.automation.editor.save"
<ha-chip-set>
${this._renderOptionalChip(
"description",
this.hass.localize(
"ui.panel.config.automation.editor.dialog.add_description"
)
)}
</mwc-button>
${this._params.domain === "script"
? this._renderOptionalChip(
"icon",
this.hass.localize(
"ui.panel.config.automation.editor.dialog.add_icon"
)
)
: nothing}
${this._renderOptionalChip(
"category",
this.hass.localize(
"ui.panel.config.automation.editor.dialog.add_category"
)
)}
${this._renderOptionalChip(
"labels",
this.hass.localize(
"ui.panel.config.automation.editor.dialog.add_labels"
)
)}
</ha-chip-set>
<div slot="primaryAction">
<mwc-button @click=${this.closeDialog}>
${this.hass.localize("ui.dialogs.generic.cancel")}
</mwc-button>
<mwc-button @click=${this._save}>
${this.hass.localize(
this._params.config.alias
? "ui.panel.config.automation.editor.rename"
: "ui.panel.config.automation.editor.save"
)}
</mwc-button>
</div>
</ha-dialog>
`;
}
private _addOptional(ev) {
ev.stopPropagation();
const option: string = ev.target.id;
this._visibleOptionals = [...this._visibleOptionals, option];
}
private _registryEntryChanged(ev) {
ev.stopPropagation();
const id: string = ev.target.id;
const value = ev.detail.value;
this._entryUpdates = { ...this._entryUpdates, [id]: value };
}
private _iconChanged(ev: CustomEvent) {
ev.stopPropagation();
this._newIcon = ev.detail.value || undefined;
@ -162,19 +273,26 @@ class DialogAutomationRename extends LitElement implements HassDialog {
this._error = "Name is required";
return;
}
if (this._params.domain === "script") {
this._params.updateConfig({
...this._params.config,
alias: this._newName,
description: this._newDescription,
icon: this._newIcon,
});
this._params.updateConfig(
{
...this._params.config,
alias: this._newName,
description: this._newDescription,
icon: this._newIcon,
},
this._entryUpdates
);
} else {
this._params.updateConfig({
...this._params.config,
alias: this._newName,
description: this._newDescription,
});
this._params.updateConfig(
{
...this._params.config,
alias: this._newName,
description: this._newDescription,
},
this._entryUpdates
);
}
this.closeDialog();
@ -185,12 +303,21 @@ class DialogAutomationRename extends LitElement implements HassDialog {
haStyle,
haStyleDialog,
css`
ha-dialog {
--dialog-content-padding: 0 24px 24px 24px;
}
ha-textfield,
ha-textarea,
ha-icon-picker {
ha-icon-picker,
ha-category-picker,
ha-labels-picker,
ha-chip-set {
display: block;
}
ha-icon-picker {
ha-icon-picker,
ha-category-picker,
ha-labels-picker,
ha-chip-set {
margin-top: 16px;
}
ha-alert {

View File

@ -1,22 +1,38 @@
import { fireEvent } from "../../../../common/dom/fire_event";
import type { AutomationConfig } from "../../../../data/automation";
import type { ScriptConfig } from "../../../../data/script";
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
export const loadAutomationRenameDialog = () =>
import("./dialog-automation-rename");
export interface AutomationRenameDialogParams {
config: AutomationConfig;
domain: "automation";
updateConfig: (config: AutomationConfig) => void;
interface BaseRenameDialogParams {
entityRegistryUpdate?: EntityRegistryUpdate;
entityRegistryEntry?: EntityRegistryEntry;
onClose: () => void;
}
export interface ScriptRenameDialogParams {
export interface EntityRegistryUpdate {
labels: string[];
category: string;
}
export interface AutomationRenameDialogParams extends BaseRenameDialogParams {
config: AutomationConfig;
domain: "automation";
updateConfig: (
config: AutomationConfig,
entityRegistryUpdate: EntityRegistryUpdate
) => void;
}
export interface ScriptRenameDialogParams extends BaseRenameDialogParams {
config: ScriptConfig;
domain: "script";
updateConfig: (config: ScriptConfig) => void;
onClose: () => void;
updateConfig: (
config: ScriptConfig,
entityRegistryUpdate: EntityRegistryUpdate
) => void;
}
export const showAutomationRenameDialog = (

View File

@ -53,8 +53,8 @@ import { substituteBlueprint } from "../../../data/blueprint";
import { validateConfig } from "../../../data/config";
import { UNAVAILABLE } from "../../../data/entity";
import {
fetchEntityRegistry,
type EntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import {
showAlertDialog,
@ -67,7 +67,10 @@ import type { Entries, HomeAssistant, Route } from "../../../types";
import { showToast } from "../../../util/toast";
import "../ha-config-section";
import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode";
import { showAutomationRenameDialog } from "./automation-rename-dialog/show-dialog-automation-rename";
import {
type EntityRegistryUpdate,
showAutomationRenameDialog,
} from "./automation-rename-dialog/show-dialog-automation-rename";
import "./blueprint-automation-editor";
import "./manual-automation-editor";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
@ -137,6 +140,12 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
})
private _registryEntry?: EntityRegistryEntry;
@state() private _saving = false;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityRegistry!: EntityRegistryEntry[];
private _configSubscriptions: Record<
string,
(config?: AutomationConfig) => void
@ -144,6 +153,33 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
private _configSubscriptionsId = 1;
private _entityRegistryUpdate?: EntityRegistryUpdate;
private _newAutomationId?: string;
private _entityRegCreated?: (
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
) => void;
protected willUpdate(changedProps) {
super.willUpdate(changedProps);
if (
this._entityRegCreated &&
this._newAutomationId &&
changedProps.has("entityRegistry")
) {
const automation = this._entityRegistry.find(
(entity: EntityRegistryEntry) =>
entity.unique_id === this._newAutomationId
);
if (automation) {
this._entityRegCreated(automation);
this._entityRegCreated = undefined;
}
}
}
protected render(): TemplateResult | typeof nothing {
if (!this._config) {
return nothing;
@ -456,8 +492,11 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
</div>
<ha-fab
slot="fab"
class=${classMap({ dirty: !this._readOnly && this._dirty })}
class=${classMap({
dirty: !this._readOnly && this._dirty,
})}
.label=${this.hass.localize("ui.panel.config.automation.editor.save")}
.disabled=${this._saving}
extended
@click=${this._saveAutomation}
>
@ -577,8 +616,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
this._config = normalizeAutomationConfig(config);
this._checkValidation();
} catch (err: any) {
const entityRegistry = await fetchEntityRegistry(this.hass.connection);
const entity = entityRegistry.find(
const entity = this._entityRegistry.find(
(ent) =>
ent.platform === "automation" && ent.unique_id === this.automationId
);
@ -841,13 +879,16 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
showAutomationRenameDialog(this, {
config: this._config!,
domain: "automation",
updateConfig: (config) => {
updateConfig: (config, entityRegistryUpdate) => {
this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this.requestUpdate();
resolve(true);
},
onClose: () => resolve(false),
entityRegistryUpdate: this._entityRegistryUpdate,
entityRegistryEntry: this._registryEntry,
});
});
}
@ -883,21 +924,49 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
}
}
this._saving = true;
this._validationErrors = undefined;
try {
await saveAutomationConfig(this.hass, id, this._config!);
if (this._entityRegistryUpdate !== undefined) {
let entityId = this._entityId;
// wait for automation to appear in entity registry when creating a new automation
if (!entityId) {
this._newAutomationId = id;
const automation = await new Promise<EntityRegistryEntry>(
(resolve) => {
this._entityRegCreated = resolve;
}
);
entityId = automation.entity_id;
}
if (entityId) {
await updateEntityRegistryEntry(this.hass, entityId, {
categories: {
automation: this._entityRegistryUpdate.category || null,
},
labels: this._entityRegistryUpdate.labels || [],
});
}
}
this._dirty = false;
if (!this.automationId) {
navigate(`/config/automation/edit/${id}`, { replace: true });
}
} catch (errors: any) {
this._errors = errors.body.message || errors.error || errors.body;
showToast(this, {
message: errors.body.message || errors.error || errors.body,
});
throw errors;
}
this._dirty = false;
if (!this.automationId) {
navigate(`/config/automation/edit/${id}`, { replace: true });
} finally {
this._saving = false;
}
}

View File

@ -35,7 +35,10 @@ import "../../../components/ha-svg-icon";
import "../../../components/ha-yaml-editor";
import { validateConfig } from "../../../data/config";
import { UNAVAILABLE } from "../../../data/entity";
import type { EntityRegistryEntry } from "../../../data/entity_registry";
import {
type EntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import type { BlueprintScriptConfig, ScriptConfig } from "../../../data/script";
import {
deleteScript,
@ -58,6 +61,7 @@ import { haStyle } from "../../../resources/styles";
import type { Entries, HomeAssistant, Route } from "../../../types";
import { showToast } from "../../../util/toast";
import { showAutomationModeDialog } from "../automation/automation-mode-dialog/show-dialog-automation-mode";
import type { EntityRegistryUpdate } from "../automation/automation-rename-dialog/show-dialog-automation-rename";
import { showAutomationRenameDialog } from "../automation/automation-rename-dialog/show-dialog-automation-rename";
import "./blueprint-script-editor";
import "./manual-script-editor";
@ -116,6 +120,34 @@ export class HaScriptEditor extends SubscribeMixin(
@state() private _blueprintConfig?: BlueprintScriptConfig;
@state() private _saving = false;
private _entityRegistryUpdate?: EntityRegistryUpdate;
private _newScriptId?: string;
private _entityRegCreated?: (
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
) => void;
protected willUpdate(changedProps) {
super.willUpdate(changedProps);
if (
this._entityRegCreated &&
this._newScriptId &&
changedProps.has("entityRegistry")
) {
const script = this.entityRegistry.find(
(entity: EntityRegistryEntry) => entity.unique_id === this._newScriptId
);
if (script) {
this._entityRegCreated(script);
this._entityRegCreated = undefined;
}
}
}
protected render(): TemplateResult | typeof nothing {
if (!this._config) {
return nothing;
@ -410,11 +442,12 @@ export class HaScriptEditor extends SubscribeMixin(
<ha-fab
slot="fab"
class=${classMap({
dirty: this._dirty,
dirty: !this._readOnly && this._dirty,
})}
.label=${this.hass.localize(
"ui.panel.config.script.editor.save_script"
)}
.disabled=${this._saving}
extended
@click=${this._saveScript}
>
@ -812,13 +845,18 @@ export class HaScriptEditor extends SubscribeMixin(
showAutomationRenameDialog(this, {
config: this._config!,
domain: "script",
updateConfig: (config) => {
updateConfig: (config, entityRegistryUpdate) => {
this._config = config;
this._entityRegistryUpdate = entityRegistryUpdate;
this._dirty = true;
this.requestUpdate();
resolve(true);
},
onClose: () => resolve(false),
entityRegistryUpdate: this._entityRegistryUpdate,
entityRegistryEntry: this.entityRegistry.find(
(entry) => entry.unique_id === this.scriptId
),
});
});
}
@ -855,24 +893,48 @@ export class HaScriptEditor extends SubscribeMixin(
}
const id = this.scriptId || this._entityId || Date.now();
this._saving = true;
try {
await this.hass!.callApi(
"POST",
"config/script/config/" + id,
this._config
);
if (this._entityRegistryUpdate !== undefined) {
let entityId = id.toString().startsWith("script.")
? id.toString()
: `script.${id}`;
// wait for new script to appear in entity registry
if (!this.scriptId) {
const script = await new Promise<EntityRegistryEntry>((resolve) => {
this._entityRegCreated = resolve;
});
entityId = script.entity_id;
}
await updateEntityRegistryEntry(this.hass, entityId, {
categories: {
script: this._entityRegistryUpdate.category || null,
},
labels: this._entityRegistryUpdate.labels || [],
});
}
this._dirty = false;
if (!this.scriptId) {
navigate(`/config/script/edit/${id}`, { replace: true });
}
} catch (errors: any) {
this._errors = errors.body.message || errors.error || errors.body;
showToast(this, {
message: errors.body.message || errors.error || errors.body,
});
throw errors;
}
this._dirty = false;
if (!this.scriptId) {
navigate(`/config/script/edit/${id}`, { replace: true });
} finally {
this._saving = false;
}
}

View File

@ -3708,6 +3708,12 @@
"label": "Unknown"
}
}
},
"dialog": {
"add_description": "Add description",
"add_icon": "Add icon",
"add_category": "Add category",
"add_labels": "Add labels"
}
},
"trace": {