mirror of
https://github.com/home-assistant/frontend.git
synced 2026-01-15 03:37:28 +00:00
Compare commits
3 Commits
dev
...
scenes-dia
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
427419e0e2 | ||
|
|
37de574592 | ||
|
|
0efcca56b6 |
@@ -11,6 +11,7 @@ import {
|
||||
mdiMenuDown,
|
||||
mdiOpenInNew,
|
||||
mdiPalette,
|
||||
mdiPencil,
|
||||
mdiPencilOff,
|
||||
mdiPlay,
|
||||
mdiPlus,
|
||||
@@ -90,8 +91,10 @@ import {
|
||||
activateScene,
|
||||
deleteScene,
|
||||
getSceneConfig,
|
||||
saveScene,
|
||||
showSceneEditor,
|
||||
} from "../../../data/scene";
|
||||
import { showSceneSaveDialog } from "./scene-save-dialog/show-dialog-scene-save";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@@ -388,6 +391,14 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
),
|
||||
action: () => this._editCategory(scene),
|
||||
},
|
||||
{
|
||||
path: mdiPencil,
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.scene.editor.rename"
|
||||
),
|
||||
action: () => this._rename(scene),
|
||||
disabled: !scene.attributes.id,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
@@ -1205,6 +1216,31 @@ ${rejected
|
||||
}
|
||||
}
|
||||
|
||||
private async _rename(scene: SceneEntity): Promise<void> {
|
||||
if (!scene.attributes.id) {
|
||||
return;
|
||||
}
|
||||
const config = await getSceneConfig(this.hass, scene.attributes.id);
|
||||
const entityRegEntry = this._entityReg.find(
|
||||
(reg) => reg.entity_id === scene.entity_id
|
||||
);
|
||||
showSceneSaveDialog(this, {
|
||||
config,
|
||||
domain: "scene",
|
||||
entityRegistryEntry: entityRegEntry,
|
||||
updateConfig: async (newConfig, entityRegistryUpdate) => {
|
||||
await saveScene(this.hass, scene.attributes.id!, newConfig);
|
||||
if (entityRegEntry) {
|
||||
await updateEntityRegistryEntry(this.hass, scene.entity_id, {
|
||||
area_id: entityRegistryUpdate.area || undefined,
|
||||
labels: entityRegistryUpdate.labels,
|
||||
categories: { scene: entityRegistryUpdate.category },
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _showHelp() {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.scene.picker.header"),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { consume } from "@lit/context";
|
||||
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import {
|
||||
mdiCog,
|
||||
mdiContentDuplicate,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
mdiEye,
|
||||
mdiInformationOutline,
|
||||
mdiMotionPlayOutline,
|
||||
mdiPencil,
|
||||
mdiPlay,
|
||||
mdiPlaylistEdit,
|
||||
mdiTag,
|
||||
@@ -30,17 +31,15 @@ import { afterNextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/device/ha-device-picker";
|
||||
import "../../../components/entity/ha-entities-picker";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-area-picker";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-textfield";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
@@ -66,6 +65,7 @@ import {
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import { showSceneSaveDialog } from "./scene-save-dialog/show-dialog-scene-save";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
|
||||
@@ -227,10 +227,9 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
? computeStateName(this._scene)
|
||||
: this.hass.localize("ui.panel.config.scene.editor.default_name")}
|
||||
>
|
||||
<ha-button-menu
|
||||
<ha-dropdown
|
||||
slot="toolbar-icon"
|
||||
@action=${this._handleMenuAction}
|
||||
activatable
|
||||
@wa-select=${this._handleDropdownSelect}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
@@ -238,67 +237,66 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-dropdown-item
|
||||
value="apply"
|
||||
.disabled=${!this.sceneId || this._mode === "live"}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.scene.picker.apply")}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPlay}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon" .disabled=${!this.sceneId}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlay}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-dropdown-item value="info" .disabled=${!this.sceneId}>
|
||||
${this.hass.localize("ui.panel.config.scene.picker.show_info")}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
slot="icon"
|
||||
.path=${mdiInformationOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-list-item graphic="icon" .disabled=${!this.sceneId}>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-dropdown-item value="settings" .disabled=${!this.sceneId}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.show_settings"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-list-item graphic="icon" .disabled=${!this.sceneId}>
|
||||
<ha-dropdown-item value="category" .disabled=${!this.sceneId}>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.scene.picker.${this._getCategory(this._entityRegistryEntries, this._scene?.entity_id) ? "edit_category" : "assign_category"}`
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiTag}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-dropdown-item value="rename" .disabled=${!this.sceneId}>
|
||||
${this.hass.localize("ui.panel.config.scene.editor.rename")}
|
||||
<ha-svg-icon slot="icon" .path=${mdiPencil}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-dropdown-item value="toggle_yaml_mode">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${this._mode !== "yaml" ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<li divider role="separator"></li>
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<ha-list-item .disabled=${!this.sceneId} graphic="icon">
|
||||
<ha-dropdown-item value="duplicate" .disabled=${!this.sceneId}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.scene.picker.duplicate_scene"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentDuplicate}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
|
||||
<ha-list-item
|
||||
<ha-dropdown-item
|
||||
value="delete"
|
||||
.disabled=${!this.sceneId}
|
||||
class=${classMap({ warning: Boolean(this.sceneId) })}
|
||||
graphic="icon"
|
||||
.variant=${this.sceneId ? "danger" : "default"}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.scene.picker.delete_scene")}
|
||||
<ha-svg-icon
|
||||
class=${classMap({ warning: Boolean(this.sceneId) })}
|
||||
slot="graphic"
|
||||
.path=${mdiDelete}
|
||||
>
|
||||
</ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
${this._errors ? html` <div class="errors">${this._errors}</div> ` : ""}
|
||||
${this._mode === "yaml" ? this._renderYamlMode() : this._renderUiMode()}
|
||||
<ha-fab
|
||||
@@ -307,7 +305,10 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
extended
|
||||
.disabled=${this._saving}
|
||||
@click=${this._saveScene}
|
||||
class=${classMap({ dirty: this._dirty, saving: this._saving })}
|
||||
class=${classMap({
|
||||
dirty: this._dirty || !this.sceneId,
|
||||
saving: this._saving,
|
||||
})}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
@@ -374,38 +375,6 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-alert>
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<ha-textfield
|
||||
.value=${this._config.name}
|
||||
.name=${"name"}
|
||||
@change=${this._valueChanged}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.scene.editor.name"
|
||||
)}
|
||||
></ha-textfield>
|
||||
<ha-icon-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.scene.editor.icon"
|
||||
)}
|
||||
.name=${"icon"}
|
||||
.value=${this._config.icon}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-icon-picker>
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.scene.editor.area"
|
||||
)}
|
||||
.name=${"area"}
|
||||
.value=${this._sceneAreaIdWithUpdates || ""}
|
||||
@value-changed=${this._areaChanged}
|
||||
>
|
||||
</ha-area-picker>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
|
||||
<ha-config-section vertical .isWide=${this.isWide}>
|
||||
@@ -652,24 +621,32 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
private async _handleDropdownSelect(ev: CustomEvent) {
|
||||
const action = (ev.target as any).value;
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "apply":
|
||||
activateScene(this.hass, this._scene!.entity_id);
|
||||
break;
|
||||
case 1:
|
||||
case "info":
|
||||
fireEvent(this, "hass-more-info", { entityId: this._scene!.entity_id });
|
||||
break;
|
||||
case 2:
|
||||
case "settings":
|
||||
showMoreInfoDialog(this, {
|
||||
entityId: this._scene!.entity_id,
|
||||
view: "settings",
|
||||
});
|
||||
break;
|
||||
case 3:
|
||||
case "category":
|
||||
this._editCategory(this._scene!);
|
||||
break;
|
||||
case 4:
|
||||
case "rename":
|
||||
this._promptSceneRename();
|
||||
break;
|
||||
case "toggle_yaml_mode":
|
||||
if (this._mode === "yaml") {
|
||||
this._initEntities(this._config!);
|
||||
this._exitYamlMode();
|
||||
@@ -677,10 +654,10 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
this._enterYamlMode();
|
||||
}
|
||||
break;
|
||||
case 5:
|
||||
case "duplicate":
|
||||
this._duplicate();
|
||||
break;
|
||||
case 6:
|
||||
case "delete":
|
||||
this._deleteTapped();
|
||||
break;
|
||||
}
|
||||
@@ -930,44 +907,6 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as any;
|
||||
const name = target.name;
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
let newVal = (ev as CustomEvent).detail?.value ?? target.value;
|
||||
if (target.type === "number") {
|
||||
newVal = Number(newVal);
|
||||
}
|
||||
if ((this._config![name] || "") === newVal) {
|
||||
return;
|
||||
}
|
||||
if (!newVal) {
|
||||
delete this._config![name];
|
||||
this._config = { ...this._config! };
|
||||
} else {
|
||||
this._config = { ...this._config!, [name]: newVal };
|
||||
}
|
||||
this._dirty = true;
|
||||
}
|
||||
|
||||
private _areaChanged(ev: CustomEvent) {
|
||||
const newValue = ev.detail.value === "" ? null : ev.detail.value;
|
||||
|
||||
if (newValue === (this._sceneAreaIdWithUpdates || "")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newValue === this._sceneAreaIdCurrent) {
|
||||
this._updatedAreaId = undefined;
|
||||
} else {
|
||||
this._updatedAreaId = newValue;
|
||||
this._dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _stateChanged(event: HassEvent) {
|
||||
if (
|
||||
event.context.id !== this._activateContextId &&
|
||||
@@ -1112,10 +1051,84 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
return;
|
||||
}
|
||||
|
||||
const id = !this.sceneId ? "" + Date.now() : this.sceneId!;
|
||||
if (this._mode === "live") {
|
||||
this._generateConfigFromLive();
|
||||
}
|
||||
|
||||
const isNewScene = !this.sceneId;
|
||||
|
||||
if (isNewScene) {
|
||||
// Show dialog for new scenes
|
||||
const entityRegEntry = this._scene
|
||||
? this._entityRegistryEntries.find(
|
||||
(reg) => reg.entity_id === this._scene!.entity_id
|
||||
)
|
||||
: undefined;
|
||||
|
||||
showSceneSaveDialog(this, {
|
||||
config: this._config!,
|
||||
domain: "scene",
|
||||
entityRegistryEntry: entityRegEntry,
|
||||
entityRegistryUpdate:
|
||||
this._updatedAreaId !== undefined
|
||||
? {
|
||||
area: this._updatedAreaId || "",
|
||||
labels: entityRegEntry?.labels || [],
|
||||
category: entityRegEntry?.categories.scene || "",
|
||||
}
|
||||
: undefined,
|
||||
updateConfig: async (newConfig, entityRegistryUpdate) => {
|
||||
const id = "" + Date.now();
|
||||
try {
|
||||
this._saving = true;
|
||||
await saveScene(this.hass, id, newConfig);
|
||||
|
||||
let scene = this.scenes.find(
|
||||
(entity: SceneEntity) => entity.attributes.id === id
|
||||
);
|
||||
|
||||
if (!scene) {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
setTimeout(reject, 3000);
|
||||
this._scenesSet = resolve;
|
||||
});
|
||||
scene = this.scenes.find(
|
||||
(entity: SceneEntity) => entity.attributes.id === id
|
||||
);
|
||||
} catch (_err) {
|
||||
// We do nothing.
|
||||
} finally {
|
||||
this._scenesSet = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (scene) {
|
||||
await updateEntityRegistryEntry(this.hass, scene.entity_id, {
|
||||
area_id: entityRegistryUpdate.area || undefined,
|
||||
labels: entityRegistryUpdate.labels,
|
||||
categories: { scene: entityRegistryUpdate.category },
|
||||
});
|
||||
}
|
||||
|
||||
this._dirty = false;
|
||||
navigate(`/config/scene/edit/${id}`, { replace: true });
|
||||
} catch (err: any) {
|
||||
this._errors = err.body.message || err.message;
|
||||
showToast(this, {
|
||||
message: err.body.message || err.message,
|
||||
});
|
||||
throw err;
|
||||
} finally {
|
||||
this._saving = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Existing scene - save directly
|
||||
const id = this.sceneId!;
|
||||
try {
|
||||
this._saving = true;
|
||||
await saveScene(this.hass, id, this._config!);
|
||||
@@ -1153,10 +1166,6 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
|
||||
this._dirty = false;
|
||||
|
||||
if (!this.sceneId) {
|
||||
navigate(`/config/scene/edit/${id}`, { replace: true });
|
||||
}
|
||||
} catch (err: any) {
|
||||
this._errors = err.body.message || err.message;
|
||||
showToast(this, {
|
||||
@@ -1210,6 +1219,38 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
});
|
||||
}
|
||||
|
||||
private async _promptSceneRename(): Promise<boolean> {
|
||||
const entityRegEntry = this._scene
|
||||
? this._entityRegistryEntries.find(
|
||||
(reg) => reg.entity_id === this._scene!.entity_id
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
showSceneSaveDialog(this, {
|
||||
config: this._config!,
|
||||
domain: "scene",
|
||||
entityRegistryEntry: entityRegEntry,
|
||||
entityRegistryUpdate:
|
||||
this._updatedAreaId !== undefined
|
||||
? {
|
||||
area: this._updatedAreaId || "",
|
||||
labels: entityRegEntry?.labels || [],
|
||||
category: entityRegEntry?.categories.scene || "",
|
||||
}
|
||||
: undefined,
|
||||
updateConfig: async (newConfig, entityRegistryUpdate) => {
|
||||
this._config = newConfig;
|
||||
this._updatedAreaId = entityRegistryUpdate.area || null;
|
||||
this._dirty = true;
|
||||
this.requestUpdate();
|
||||
resolve(true);
|
||||
},
|
||||
onClose: () => resolve(false),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected get isDirty() {
|
||||
return this._dirty;
|
||||
}
|
||||
@@ -1275,15 +1316,10 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
ha-fab.saving {
|
||||
opacity: var(--light-disabled-opacity);
|
||||
}
|
||||
ha-icon-picker,
|
||||
ha-area-picker,
|
||||
ha-entity-picker {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
div[slot="meta"] {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
505
src/panels/config/scene/scene-save-dialog/dialog-scene-save.ts
Normal file
505
src/panels/config/scene/scene-save-dialog/dialog-scene-save.ts
Normal file
@@ -0,0 +1,505 @@
|
||||
import { mdiClose, mdiPlus } from "@mdi/js";
|
||||
import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/chips/ha-assist-chip";
|
||||
import "../../../../components/chips/ha-chip-set";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-area-picker";
|
||||
import "../../../../components/ha-domain-icon";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-picker";
|
||||
import "../../../../components/ha-labels-picker";
|
||||
import "../../../../components/ha-suggest-with-ai-button";
|
||||
import type { SuggestWithAIGenerateTask } from "../../../../components/ha-suggest-with-ai-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-textfield";
|
||||
import "../../category/ha-category-picker";
|
||||
|
||||
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
|
||||
import { subscribeOne } from "../../../../common/util/subscribe-one";
|
||||
import type { GenDataTaskResult } from "../../../../data/ai_task";
|
||||
import { fetchCategoryRegistry } from "../../../../data/category_registry";
|
||||
import { subscribeEntityRegistry } from "../../../../data/entity/entity_registry";
|
||||
import { subscribeLabelRegistry } from "../../../../data/label/label_registry";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type {
|
||||
EntityRegistryUpdate,
|
||||
SceneSaveDialogParams,
|
||||
} from "./show-dialog-scene-save";
|
||||
|
||||
@customElement("ha-dialog-scene-save")
|
||||
class DialogSceneSave extends LitElement implements HassDialog {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _visibleOptionals: string[] = [];
|
||||
|
||||
@state() private _entryUpdates!: EntityRegistryUpdate;
|
||||
|
||||
private _params!: SceneSaveDialogParams;
|
||||
|
||||
@state() private _newName?: string;
|
||||
|
||||
private _newIcon?: string;
|
||||
|
||||
public showDialog(params: SceneSaveDialogParams): void {
|
||||
this._opened = true;
|
||||
this._params = params;
|
||||
this._newIcon = params.config.icon;
|
||||
this._newName =
|
||||
params.config.name ||
|
||||
this.hass.localize(
|
||||
`ui.panel.config.${this._params.domain}.editor.default_name`
|
||||
);
|
||||
this._entryUpdates = params.entityRegistryUpdate || {
|
||||
area: params.entityRegistryEntry?.area_id || "",
|
||||
labels: params.entityRegistryEntry?.labels || [],
|
||||
category: params.entityRegistryEntry?.categories[params.domain] || "",
|
||||
};
|
||||
|
||||
this._visibleOptionals = [
|
||||
this._entryUpdates.category ? "category" : "",
|
||||
this._entryUpdates.labels.length > 0 ? "labels" : "",
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._params.onClose?.();
|
||||
|
||||
if (this._opened) {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
this._opened = false;
|
||||
this._visibleOptionals = [];
|
||||
return true;
|
||||
}
|
||||
|
||||
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 _renderDiscard() {
|
||||
if (!this._params.onDiscard) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-button
|
||||
@click=${this._handleDiscard}
|
||||
slot="secondaryAction"
|
||||
variant="danger"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize("ui.common.dont_save")}
|
||||
</ha-button>
|
||||
`;
|
||||
}
|
||||
|
||||
protected _renderInputs() {
|
||||
if (this._params.hideInputs) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-textfield
|
||||
dialogInitialFocus
|
||||
.value=${this._newName}
|
||||
.placeholder=${this.hass.localize(
|
||||
`ui.panel.config.${this._params.domain}.editor.default_name`
|
||||
)}
|
||||
.label=${this.hass.localize("ui.panel.config.scene.editor.name")}
|
||||
required
|
||||
type="string"
|
||||
@input=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
|
||||
<ha-icon-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize("ui.panel.config.scene.editor.icon")}
|
||||
.value=${this._newIcon}
|
||||
@value-changed=${this._iconChanged}
|
||||
>
|
||||
<ha-domain-icon
|
||||
slot="start"
|
||||
domain=${this._params.domain}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</ha-domain-icon>
|
||||
</ha-icon-picker>
|
||||
|
||||
<ha-area-picker
|
||||
id="area"
|
||||
.hass=${this.hass}
|
||||
.value=${this._entryUpdates.area}
|
||||
@value-changed=${this._registryEntryChanged}
|
||||
></ha-area-picker>
|
||||
|
||||
${this._visibleOptionals.includes("category")
|
||||
? html` <ha-category-picker
|
||||
id="category"
|
||||
.hass=${this.hass}
|
||||
.scope=${this._params.domain}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.category-picker.category"
|
||||
)}
|
||||
.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}
|
||||
|
||||
<ha-chip-set>
|
||||
${this._renderOptionalChip(
|
||||
"category",
|
||||
this.hass.localize("ui.panel.config.scene.editor.dialog.add_category")
|
||||
)}
|
||||
${this._renderOptionalChip(
|
||||
"labels",
|
||||
this.hass.localize("ui.panel.config.scene.editor.dialog.add_labels")
|
||||
)}
|
||||
</ha-chip-set>
|
||||
`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._opened) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const title = this.hass.localize(
|
||||
this._params.config.id
|
||||
? "ui.panel.config.scene.editor.rename"
|
||||
: "ui.common.save"
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${title}
|
||||
>
|
||||
<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._params.title || title}</span>
|
||||
${this._params.hideInputs
|
||||
? nothing
|
||||
: html` <ha-suggest-with-ai-button
|
||||
slot="actionItems"
|
||||
.hass=${this.hass}
|
||||
.generateTask=${this._generateTask}
|
||||
@suggestion=${this._handleSuggestion}
|
||||
></ha-suggest-with-ai-button>`}
|
||||
</ha-dialog-header>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.scene.editor.missing_name"
|
||||
)}</ha-alert
|
||||
>`
|
||||
: ""}
|
||||
${this._params.description
|
||||
? html`<p>${this._params.description}</p>`
|
||||
: nothing}
|
||||
${this._renderInputs()} ${this._renderDiscard()}
|
||||
|
||||
<div slot="primaryAction">
|
||||
<ha-button appearance="plain" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button @click=${this._save}>
|
||||
${this.hass.localize(
|
||||
this._params.config.id && !this._params.onDiscard
|
||||
? "ui.panel.config.scene.editor.rename"
|
||||
: "ui.common.save"
|
||||
)}
|
||||
</ha-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;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as any;
|
||||
this._newName = target.value;
|
||||
}
|
||||
|
||||
private _handleDiscard() {
|
||||
this._params.onDiscard?.();
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _getSuggestData() {
|
||||
return Promise.all([
|
||||
subscribeOne(this.hass.connection, subscribeLabelRegistry).then((labs) =>
|
||||
Object.fromEntries(labs.map((lab) => [lab.label_id, lab.name]))
|
||||
),
|
||||
subscribeOne(this.hass.connection, subscribeEntityRegistry).then((ents) =>
|
||||
Object.fromEntries(ents.map((ent) => [ent.entity_id, ent]))
|
||||
),
|
||||
fetchCategoryRegistry(this.hass.connection, "scene").then((cats) =>
|
||||
Object.fromEntries(cats.map((cat) => [cat.category_id, cat.name]))
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
private _generateTask = async (): Promise<SuggestWithAIGenerateTask> => {
|
||||
const [labels, entities, categories] = await this._getSuggestData();
|
||||
const inspirations: string[] = [];
|
||||
|
||||
const domain = this._params.domain;
|
||||
|
||||
for (const entity of Object.values(this.hass.states)) {
|
||||
const entityEntry = entities[entity.entity_id];
|
||||
if (
|
||||
computeStateDomain(entity) !== domain ||
|
||||
entity.attributes.restored ||
|
||||
!entity.attributes.friendly_name ||
|
||||
!entityEntry
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let inspiration = `- ${entity.attributes.friendly_name}`;
|
||||
|
||||
const category = categories[entityEntry.categories.scene];
|
||||
if (category) {
|
||||
inspiration += ` (category: ${category})`;
|
||||
}
|
||||
|
||||
if (entityEntry.labels.length) {
|
||||
inspiration += ` (labels: ${entityEntry.labels
|
||||
.map((label) => labels[label])
|
||||
.join(", ")})`;
|
||||
}
|
||||
|
||||
inspirations.push(inspiration);
|
||||
}
|
||||
|
||||
return {
|
||||
type: "data",
|
||||
task: {
|
||||
task_name: "frontend__scene__save",
|
||||
instructions: `Suggest in language "${this.hass.language}" a name, category and labels for the following Home Assistant scene.
|
||||
|
||||
The name should be relevant to the scene's purpose.
|
||||
${
|
||||
inspirations.length
|
||||
? `The name should be in same style and sentence capitalization as existing scenes.
|
||||
Suggest a category and labels if relevant to the scene's purpose.
|
||||
Only suggest category and labels that are already used by existing scenes.`
|
||||
: `The name should be short, descriptive, sentence case, and written in the language ${this.hass.language}.`
|
||||
}
|
||||
|
||||
For inspiration, here are existing scenes:
|
||||
${inspirations.join("\n")}
|
||||
|
||||
The scene configuration is as follows:
|
||||
|
||||
${dump(this._params.config)}
|
||||
`,
|
||||
structure: {
|
||||
name: {
|
||||
description: "The name of the scene",
|
||||
required: true,
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
labels: {
|
||||
description: "Labels for the scene",
|
||||
required: false,
|
||||
selector: {
|
||||
text: {
|
||||
multiple: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
category: {
|
||||
description: "The category of the scene",
|
||||
required: false,
|
||||
selector: {
|
||||
select: {
|
||||
options: Object.entries(categories).map(([id, name]) => ({
|
||||
value: id,
|
||||
label: name,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
private async _handleSuggestion(
|
||||
event: CustomEvent<
|
||||
GenDataTaskResult<{
|
||||
name: string;
|
||||
category?: string;
|
||||
labels?: string[];
|
||||
}>
|
||||
>
|
||||
) {
|
||||
const result = event.detail;
|
||||
const [labels, _entities, categories] = await this._getSuggestData();
|
||||
|
||||
this._newName = result.data.name;
|
||||
if (result.data.category) {
|
||||
// We get back category name, convert it to ID
|
||||
const categoryId = Object.entries(categories).find(
|
||||
([, name]) => name === result.data.category
|
||||
)?.[0];
|
||||
if (categoryId) {
|
||||
this._entryUpdates = {
|
||||
...this._entryUpdates,
|
||||
category: categoryId,
|
||||
};
|
||||
if (!this._visibleOptionals.includes("category")) {
|
||||
this._visibleOptionals = [...this._visibleOptionals, "category"];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.data.labels?.length) {
|
||||
// We get back label names, convert them to IDs
|
||||
const newLabels: Record<string, undefined | string> = Object.fromEntries(
|
||||
result.data.labels.map((name) => [name, undefined])
|
||||
);
|
||||
let toFind = result.data.labels.length;
|
||||
for (const [labelId, labelName] of Object.entries(labels)) {
|
||||
if (labelName in newLabels && newLabels[labelName] === undefined) {
|
||||
newLabels[labelName] = labelId;
|
||||
toFind--;
|
||||
if (toFind === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const foundLabels = Object.values(newLabels).filter(
|
||||
(labelId) => labelId !== undefined
|
||||
);
|
||||
if (foundLabels.length) {
|
||||
this._entryUpdates = {
|
||||
...this._entryUpdates,
|
||||
labels: foundLabels,
|
||||
};
|
||||
if (!this._visibleOptionals.includes("labels")) {
|
||||
this._visibleOptionals = [...this._visibleOptionals, "labels"];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
if (!this._newName) {
|
||||
this._error = "Name is required";
|
||||
return;
|
||||
}
|
||||
|
||||
await this._params.updateConfig(
|
||||
{
|
||||
...this._params.config,
|
||||
name: this._newName,
|
||||
icon: this._newIcon,
|
||||
},
|
||||
this._entryUpdates
|
||||
);
|
||||
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0 24px 24px 24px;
|
||||
}
|
||||
|
||||
@media all and (min-width: 500px) {
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: min(500px, 95vw);
|
||||
--mdc-dialog-max-width: min(500px, 95vw);
|
||||
}
|
||||
}
|
||||
|
||||
ha-textfield,
|
||||
ha-icon-picker,
|
||||
ha-category-picker,
|
||||
ha-labels-picker,
|
||||
ha-area-picker {
|
||||
display: block;
|
||||
}
|
||||
ha-icon-picker,
|
||||
ha-category-picker,
|
||||
ha-labels-picker,
|
||||
ha-area-picker,
|
||||
ha-chip-set:has(> ha-assist-chip) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
ha-suggest-with-ai-button {
|
||||
margin: 8px 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-dialog-scene-save": DialogSceneSave;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { SceneConfig } from "../../../../data/scene";
|
||||
|
||||
export const loadSceneSaveDialog = () => import("./dialog-scene-save");
|
||||
|
||||
interface BaseRenameDialogParams {
|
||||
entityRegistryUpdate?: EntityRegistryUpdate;
|
||||
entityRegistryEntry?: EntityRegistryEntry;
|
||||
onClose?: () => void;
|
||||
onDiscard?: () => void;
|
||||
saveText?: string;
|
||||
description?: string;
|
||||
title?: string;
|
||||
hideInputs?: boolean;
|
||||
}
|
||||
|
||||
export interface EntityRegistryUpdate {
|
||||
area: string;
|
||||
labels: string[];
|
||||
category: string;
|
||||
}
|
||||
|
||||
export interface SceneSaveDialogParams extends BaseRenameDialogParams {
|
||||
config: SceneConfig;
|
||||
domain: "scene";
|
||||
updateConfig: (
|
||||
config: SceneConfig,
|
||||
entityRegistryUpdate: EntityRegistryUpdate
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export const showSceneSaveDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: SceneSaveDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "ha-dialog-scene-save",
|
||||
dialogImport: loadSceneSaveDialog,
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -5152,11 +5152,19 @@
|
||||
"load_error_not_editable": "Only scenes in scenes.yaml are editable.",
|
||||
"load_error_unknown": "Error loading scene ({err_no}).",
|
||||
"save": "Save",
|
||||
"rename": "Rename",
|
||||
"missing_name": "Name is required",
|
||||
"unsaved_confirm_title": "Leave editor?",
|
||||
"unsaved_confirm_text": "Unsaved changes will be lost.",
|
||||
"name": "Name",
|
||||
"icon": "Icon",
|
||||
"area": "Area",
|
||||
"dialog": {
|
||||
"add_icon": "Add icon",
|
||||
"add_area": "Add area",
|
||||
"add_category": "Add category",
|
||||
"add_labels": "Add labels"
|
||||
},
|
||||
"devices": {
|
||||
"header": "Devices",
|
||||
"introduction": "Add the devices that you want to be included in your scene. Set all entities in each device to the state you want for this scene.",
|
||||
|
||||
Reference in New Issue
Block a user