1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-02 00:27:49 +01:00

Enhance create new automation/script dialog with search and adaptive dialog (#30188)

* Enhance create new automation/script dialog with search and adaptive dialog

* Always show tip

* Address review comments

* Use multiTermSearch
This commit is contained in:
Jan-Philipp Benecke
2026-03-17 15:11:26 +01:00
committed by GitHub
parent 14615191f4
commit fe53656c7e
2 changed files with 235 additions and 100 deletions

View File

@@ -5,6 +5,7 @@ import {
mdiPencilOutline,
mdiWeb,
} from "@mdi/js";
import Fuse from "fuse.js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -12,11 +13,13 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import { stringCompare } from "../../../common/string/compare";
import "../../../components/ha-adaptive-dialog";
import "../../../components/ha-icon-next";
import "../../../components/ha-dialog";
import "../../../components/ha-list";
import "../../../components/ha-list-item";
import "../../../components/ha-spinner";
import "../../../components/ha-tip";
import "../../../components/search-input";
import { showAutomationEditor } from "../../../data/automation";
import type {
Blueprint,
@@ -28,6 +31,10 @@ import {
fetchBlueprints,
getBlueprintSourceType,
} from "../../../data/blueprint";
import {
type FuseWeightedKey,
multiTermSearch,
} from "../../../resources/fuseMultiTerm";
import { showScriptEditor } from "../../../data/script";
import { mdiHomeAssistant } from "../../../resources/home-assistant-logo-svg";
import { haStyle, haStyleDialog } from "../../../resources/styles";
@@ -41,6 +48,13 @@ const SOURCE_TYPE_ICONS: Record<BlueprintSourceType, string> = {
homeassistant: mdiHomeAssistant,
};
const BLUEPRINT_SEARCH_KEYS: FuseWeightedKey[] = [
{ name: "name", weight: 10 },
{ name: "description", weight: 7 },
{ name: "author", weight: 5 },
{ name: "sourceType", weight: 3 },
];
@customElement("ha-dialog-new-automation")
class DialogNewAutomation extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -53,14 +67,25 @@ class DialogNewAutomation extends LitElement {
@state() public blueprints?: Blueprints;
@state() private _loadingBlueprints = false;
@state() private _filter = "";
public showDialog(params: NewAutomationDialogParams): void {
this._params = params;
this._open = true;
this._mode = params?.mode || "automation";
this._filter = "";
this.blueprints = undefined;
this._loadingBlueprints = true;
fetchBlueprints(this.hass!, this._mode).then((blueprints) => {
this.blueprints = blueprints;
});
fetchBlueprints(this.hass!, this._mode)
.then((blueprints) => {
this.blueprints = blueprints;
})
.finally(() => {
this._loadingBlueprints = false;
});
}
public closeDialog(): void {
@@ -70,6 +95,8 @@ class DialogNewAutomation extends LitElement {
private _dialogClosed(): void {
this._params = undefined;
this.blueprints = undefined;
this._loadingBlueprints = false;
this._filter = "";
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -93,117 +120,182 @@ class DialogNewAutomation extends LitElement {
);
});
private _blueprintFuseIndex = memoizeOne(
(blueprints: ReturnType<DialogNewAutomation["_processedBlueprints"]>) =>
Fuse.createIndex(BLUEPRINT_SEARCH_KEYS, blueprints)
);
private _filteredBlueprints = memoizeOne(
(
blueprints: ReturnType<DialogNewAutomation["_processedBlueprints"]>,
filter: string
) =>
multiTermSearch(
blueprints,
filter,
BLUEPRINT_SEARCH_KEYS,
this._blueprintFuseIndex(blueprints)
)
);
protected render() {
if (!this._params) {
return nothing;
}
const processedBlueprints = this._processedBlueprints(this.blueprints);
const filteredBlueprints = this._filteredBlueprints(
processedBlueprints,
this._filter
);
return html`
<ha-dialog
<ha-adaptive-dialog
.hass=${this.hass}
.open=${this._open}
flexcontent
header-title=${this.hass.localize(
`ui.panel.config.${this._mode}.dialog_new.header`
)}
@closed=${this._dialogClosed}
>
<ha-list
innerRole="listbox"
itemRoles="option"
innerAriaLabel=${this.hass.localize(
`ui.panel.config.${this._mode}.dialog_new.header`
)}
rootTabbable
autofocus
>
<ha-list-item
hasmeta
twoline
graphic="icon"
@request-selected=${this._blank}
>
<ha-svg-icon slot="graphic" .path=${mdiPencilOutline}></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.${this._mode}.dialog_new.create_empty`
)}
<span slot="secondary">
<div class="content-wrapper">
<search-input
autofocus
.hass=${this.hass}
.filter=${this._filter}
.label=${this.hass.localize("ui.common.search")}
@value-changed=${this._handleSearchChange}
></search-input>
<ha-list>
<ha-list-item
hasmeta
twoline
graphic="icon"
@request-selected=${this._blank}
>
<ha-svg-icon
slot="graphic"
.path=${mdiPencilOutline}
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.${this._mode}.dialog_new.create_empty_description`
`ui.panel.config.${this._mode}.dialog_new.create_empty`
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
<li divider role="separator"></li>
${processedBlueprints.map(
(blueprint) => html`
<ha-list-item
hasmeta
twoline
graphic="icon"
@request-selected=${this._blueprint}
.path=${blueprint.path}
>
<ha-svg-icon
slot="graphic"
.path=${SOURCE_TYPE_ICONS[blueprint.sourceType]}
></ha-svg-icon>
${blueprint.name}
<span slot="secondary">
${blueprint.author
? this.hass.localize(
`ui.panel.config.${this._mode}.dialog_new.blueprint_source.author`,
{ author: blueprint.author }
)
: this.hass.localize(
`ui.panel.config.${this._mode}.dialog_new.blueprint_source.${blueprint.sourceType}`
<span slot="secondary">
${this.hass.localize(
`ui.panel.config.${this._mode}.dialog_new.create_empty_description`
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
</ha-list>
<div class="blueprints-container">
<div class="blueprints-list ha-scrollbar">
${this._loadingBlueprints
? html`<div class="spinner">
<ha-spinner></ha-spinner>
</div>`
: html`
<ha-list>
${filteredBlueprints.map(
(blueprint) => html`
<ha-list-item
hasmeta
twoline
graphic="icon"
@request-selected=${this._blueprint}
.path=${blueprint.path}
>
<ha-svg-icon
slot="graphic"
.path=${SOURCE_TYPE_ICONS[blueprint.sourceType]}
></ha-svg-icon>
${blueprint.name}
<span slot="secondary">
${blueprint.author
? this.hass.localize(
`ui.panel.config.${this._mode}.dialog_new.blueprint_source.author`,
{ author: blueprint.author }
)
: this.hass.localize(
`ui.panel.config.${this._mode}.dialog_new.blueprint_source.${blueprint.sourceType}`
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
)}
${processedBlueprints.length === 0
? html`
<a
href=${documentationUrl(this.hass, "/get-blueprints")}
target="_blank"
rel="noreferrer noopener"
class="item"
>
<ha-list-item hasmeta twoline graphic="icon">
<ha-svg-icon slot="graphic" .path=${mdiWeb}></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.${this._mode}.dialog_new.create_blueprint`
)}
<span slot="secondary">
${this.hass.localize(
`ui.panel.config.${this._mode}.dialog_new.create_blueprint_description`
)}
</span>
<ha-svg-icon slot="meta" path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item>
</a>
`
: html`
<ha-tip .hass=${this.hass}>
<a
href=${documentationUrl(this.hass, "/get-blueprints")}
target="_blank"
rel="noreferrer noopener"
>
${this.hass.localize(
`ui.panel.config.${this._mode}.dialog_new.discover_blueprint_tip`
)}
</a>
</ha-tip>
`}
</ha-list>
</ha-dialog>
</ha-list>
${processedBlueprints.length === 0
? html`
<a
href=${documentationUrl(
this.hass,
"/get-blueprints"
)}
target="_blank"
rel="noreferrer noopener"
class="item"
>
<ha-list-item hasmeta twoline graphic="icon">
<ha-svg-icon
slot="graphic"
.path=${mdiWeb}
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.${this._mode}.dialog_new.create_blueprint`
)}
<span slot="secondary">
${this.hass.localize(
`ui.panel.config.${this._mode}.dialog_new.create_blueprint_description`
)}
</span>
<ha-svg-icon
slot="meta"
path=${mdiOpenInNew}
></ha-svg-icon>
</ha-list-item>
</a>
`
: filteredBlueprints.length === 0
? html`
<div class="empty-search">
${this.hass.localize(
`ui.panel.config.${this._mode}.dialog_new.no_blueprints_match_search`
)}
</div>
`
: nothing}
${processedBlueprints.length > 0
? html`
<ha-tip .hass=${this.hass}>
<a
href=${documentationUrl(
this.hass,
"/get-blueprints"
)}
target="_blank"
rel="noreferrer noopener"
>
${this.hass.localize(
`ui.panel.config.${this._mode}.dialog_new.discover_blueprint_tip`
)}
</a>
</ha-tip>
`
: nothing}
`}
</div>
</div>
</div>
</ha-adaptive-dialog>
`;
}
private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value;
}
private async _blueprint(ev) {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
@@ -232,22 +324,63 @@ class DialogNewAutomation extends LitElement {
haStyle,
haStyleDialog,
css`
ha-dialog {
ha-adaptive-dialog {
--dialog-content-padding: 0;
--mdc-dialog-max-height: 60vh;
--mdc-dialog-max-height: 60dvh;
--ha-dialog-min-height: min(
720px,
calc(
100dvh - max(
var(--safe-area-inset-bottom),
var(--ha-space-4)
) - max(var(--safe-area-inset-top), var(--ha-space-4))
)
);
--ha-dialog-max-height: var(--ha-dialog-min-height);
}
:host {
--ha-bottom-sheet-height: min(85vh, 85dvh);
--ha-bottom-sheet-max-height: min(85vh, 85dvh);
}
@media all and (min-width: 550px) {
ha-dialog {
ha-adaptive-dialog {
--mdc-dialog-min-width: 500px;
}
}
.content-wrapper {
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
}
.blueprints-container {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
border-top: 1px solid var(--divider-color);
}
search-input {
display: block;
}
.blueprints-list {
overflow-y: auto;
min-height: 0;
padding-bottom: var(--ha-space-2);
}
.spinner {
display: flex;
justify-content: center;
padding: var(--ha-space-8) 0;
}
ha-icon-next {
width: 24px;
}
ha-tip {
margin-top: 8px;
margin-bottom: 4px;
margin: var(--ha-space-2) var(--ha-space-4);
}
.empty-search {
color: var(--secondary-text-color);
padding: var(--ha-space-4);
}
a.item {
text-decoration: unset;

View File

@@ -4743,6 +4743,7 @@
"header": "Create automation",
"create_empty": "Create new automation",
"create_empty_description": "Start with an empty automation from scratch",
"no_blueprints_match_search": "No matching blueprints",
"create_blueprint": "Create from blueprint",
"create_blueprint_description": "Discover community blueprints",
"blueprint_source": {
@@ -5762,6 +5763,7 @@
"header": "Create script",
"create_empty": "Create new script",
"create_empty_description": "Start with an empty script from scratch",
"no_blueprints_match_search": "[%key:ui::panel::config::automation::dialog_new::no_blueprints_match_search%]",
"create_blueprint": "[%key:ui::panel::config::automation::dialog_new::create_blueprint%]",
"create_blueprint_description": "[%key:ui::panel::config::automation::dialog_new::create_blueprint_description%]",
"blueprint_source": {