Add multi select to scripts and scenes (#20318)

This commit is contained in:
Bram Kragten 2024-04-02 15:16:10 +02:00 committed by GitHub
parent 6301bc713c
commit cbb08c6202
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 381 additions and 2 deletions

View File

@ -1,10 +1,13 @@
import { consume } from "@lit-labs/context"; import { consume } from "@lit-labs/context";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import { import {
mdiChevronRight,
mdiContentDuplicate, mdiContentDuplicate,
mdiDelete, mdiDelete,
mdiDotsVertical,
mdiHelpCircle, mdiHelpCircle,
mdiInformationOutline, mdiInformationOutline,
mdiMenuDown,
mdiPalette, mdiPalette,
mdiPencilOff, mdiPencilOff,
mdiPlay, mdiPlay,
@ -33,6 +36,7 @@ import { LocalizeFunc } from "../../../common/translations/localize";
import { import {
DataTableColumnContainer, DataTableColumnContainer,
RowClickedEvent, RowClickedEvent,
SelectionChangedEvent,
} from "../../../components/data-table/ha-data-table"; } from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels"; import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-button"; import "../../../components/ha-button";
@ -46,13 +50,19 @@ import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu"; import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-state-icon"; import "../../../components/ha-state-icon";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import "../../../components/ha-menu-item";
import "../../../components/ha-sub-menu";
import { import {
CategoryRegistryEntry, CategoryRegistryEntry,
subscribeCategoryRegistry, subscribeCategoryRegistry,
} from "../../../data/category_registry"; } from "../../../data/category_registry";
import { fullEntitiesContext } from "../../../data/context"; import { fullEntitiesContext } from "../../../data/context";
import { isUnavailableState } from "../../../data/entity"; import { isUnavailableState } from "../../../data/entity";
import { EntityRegistryEntry } from "../../../data/entity_registry"; import {
EntityRegistryEntry,
UpdateEntityRegistryEntryResult,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import { forwardHaptic } from "../../../data/haptics"; import { forwardHaptic } from "../../../data/haptics";
import { import {
LabelRegistryEntry, LabelRegistryEntry,
@ -77,6 +87,7 @@ import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import { computeCssColor } from "../../../common/color/compute-color";
type SceneItem = SceneEntity & { type SceneItem = SceneEntity & {
name: string; name: string;
@ -98,6 +109,8 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
@state() private _searchParms = new URLSearchParams(window.location.search); @state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _selected: string[] = [];
@state() private _activeFilters?: string[]; @state() private _activeFilters?: string[];
@state() private _filteredScenes?: string[] | null; @state() private _filteredScenes?: string[] | null;
@ -319,6 +332,40 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
const categoryItems = html`${this._categories?.map(
(category) =>
html`<ha-menu-item
.value=${category.category_id}
@click=${this._handleBulkCategory}
>
${category.icon
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
<div slot="headline">${category.name}</div>
</ha-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkCategory}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category"
)}
</div>
</ha-menu-item>`;
const labelItems = html` ${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
return html`<ha-menu-item
.value=${label.label_id}
@click=${this._handleBulkLabel}
>
<ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
${label.name}
</ha-label>
</ha-menu-item>`;
})}`;
return html` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
.hass=${this.hass} .hass=${this.hass}
@ -326,6 +373,9 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
back-path="/config" back-path="/config"
.route=${this.route} .route=${this.route}
.tabs=${configSections.automations} .tabs=${configSections.automations}
selectable
.selected=${this._selected.length}
@selection-changed=${this._handleSelectionChanged}
hasFilters hasFilters
.filters=${Object.values(this._filters).filter( .filters=${Object.values(this._filters).filter(
(filter) => filter.value?.length (filter) => filter.value?.length
@ -407,6 +457,103 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
@expanded-changed=${this._filterExpanded} @expanded-changed=${this._filterExpanded}
></ha-filter-categories> ></ha-filter-categories>
${!this.narrow
? html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${categoryItems}
</ha-button-menu-new>
${this.hass.dockedSidebar === "docked"
? nothing
: html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-button-menu-new>`}`
: nothing}
${this.narrow || this.hass.dockedSidebar === "docked"
? html`
<ha-button-menu-new has-overflow slot="selection-bar">
${
this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>`
: html`<ha-icon-button
.path=${mdiDotsVertical}
.label=${"ui.panel.config.automation.picker.bulk_action"}
slot="trigger"
></ha-icon-button>`
}
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
${
this.narrow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${categoryItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
${
this.narrow || this.hass.dockedSidebar === "docked"
? html` <ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${labelItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
</ha-button-menu-new>`
: nothing}
${!this.scenes.length ${!this.scenes.length
? html`<div class="empty" slot="empty"> ? html`<div class="empty" slot="empty">
<ha-svg-icon .path=${mdiPalette}></ha-svg-icon> <ha-svg-icon .path=${mdiPalette}></ha-svg-icon>
@ -553,6 +700,12 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
this._applyFilters(); this._applyFilters();
} }
private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selected = ev.detail.value;
}
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) { private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const scene = this.scenes.find((a) => a.entity_id === ev.detail.id); const scene = this.scenes.find((a) => a.entity_id === ev.detail.id);
@ -561,6 +714,32 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
} }
} }
private async _handleBulkCategory(ev) {
const category = ev.currentTarget.value;
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
categories: { scene: category },
})
);
});
await Promise.all(promises);
}
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
labels: this.hass.entities[entityId].labels.concat(label),
})
);
});
await Promise.all(promises);
}
private _editCategory(scene: any) { private _editCategory(scene: any) {
const entityReg = this._entityReg.find( const entityReg = this._entityReg.find(
(reg) => reg.entity_id === scene.entity_id (reg) => reg.entity_id === scene.entity_id
@ -664,6 +843,16 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
--mdc-icon-size: 80px; --mdc-icon-size: 80px;
max-width: 500px; max-width: 500px;
} }
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-button-menu-new ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
`, `,
]; ];
} }

View File

@ -1,9 +1,12 @@
import { consume } from "@lit-labs/context"; import { consume } from "@lit-labs/context";
import { import {
mdiChevronRight,
mdiContentDuplicate, mdiContentDuplicate,
mdiDelete, mdiDelete,
mdiDotsVertical,
mdiHelpCircle, mdiHelpCircle,
mdiInformationOutline, mdiInformationOutline,
mdiMenuDown,
mdiPlay, mdiPlay,
mdiPlus, mdiPlus,
mdiScriptText, mdiScriptText,
@ -34,6 +37,7 @@ import { LocalizeFunc } from "../../../common/translations/localize";
import { import {
DataTableColumnContainer, DataTableColumnContainer,
RowClickedEvent, RowClickedEvent,
SelectionChangedEvent,
} from "../../../components/data-table/ha-data-table"; } from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels"; import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-fab"; import "../../../components/ha-fab";
@ -46,13 +50,19 @@ import "../../../components/ha-filter-labels";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu"; import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import "../../../components/ha-menu-item";
import "../../../components/ha-sub-menu";
import { import {
CategoryRegistryEntry, CategoryRegistryEntry,
subscribeCategoryRegistry, subscribeCategoryRegistry,
} from "../../../data/category_registry"; } from "../../../data/category_registry";
import { fullEntitiesContext } from "../../../data/context"; import { fullEntitiesContext } from "../../../data/context";
import { UNAVAILABLE } from "../../../data/entity"; import { UNAVAILABLE } from "../../../data/entity";
import { EntityRegistryEntry } from "../../../data/entity_registry"; import {
EntityRegistryEntry,
UpdateEntityRegistryEntryResult,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import { import {
LabelRegistryEntry, LabelRegistryEntry,
subscribeLabelRegistry, subscribeLabelRegistry,
@ -79,6 +89,7 @@ import { showToast } from "../../../util/toast";
import { showNewAutomationDialog } from "../automation/show-dialog-new-automation"; import { showNewAutomationDialog } from "../automation/show-dialog-new-automation";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import { computeCssColor } from "../../../common/color/compute-color";
type ScriptItem = ScriptEntity & { type ScriptItem = ScriptEntity & {
name: string; name: string;
@ -102,6 +113,8 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
@state() private _searchParms = new URLSearchParams(window.location.search); @state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _selected: string[] = [];
@state() private _activeFilters?: string[]; @state() private _activeFilters?: string[];
@state() private _filteredScripts?: string[] | null; @state() private _filteredScripts?: string[] | null;
@ -331,6 +344,40 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
const categoryItems = html`${this._categories?.map(
(category) =>
html`<ha-menu-item
.value=${category.category_id}
@click=${this._handleBulkCategory}
>
${category.icon
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
<div slot="headline">${category.name}</div>
</ha-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkCategory}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category"
)}
</div>
</ha-menu-item>`;
const labelItems = html` ${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
return html`<ha-menu-item
.value=${label.label_id}
@click=${this._handleBulkLabel}
>
<ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
${label.name}
</ha-label>
</ha-menu-item>`;
})}`;
return html` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
.hass=${this.hass} .hass=${this.hass}
@ -340,6 +387,9 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
.tabs=${configSections.automations} .tabs=${configSections.automations}
hasFilters hasFilters
initialGroupColumn="category" initialGroupColumn="category"
selectable
.selected=${this._selected.length}
@selection-changed=${this._handleSelectionChanged}
.filters=${Object.values(this._filters).filter( .filters=${Object.values(this._filters).filter(
(filter) => filter.value?.length (filter) => filter.value?.length
).length} ).length}
@ -432,6 +482,104 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
.narrow=${this.narrow} .narrow=${this.narrow}
@expanded-changed=${this._filterExpanded} @expanded-changed=${this._filterExpanded}
></ha-filter-blueprints> ></ha-filter-blueprints>
${!this.narrow
? html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${categoryItems}
</ha-button-menu-new>
${this.hass.dockedSidebar === "docked"
? nothing
: html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-button-menu-new>`}`
: nothing}
${this.narrow || this.hass.dockedSidebar === "docked"
? html`
<ha-button-menu-new has-overflow slot="selection-bar">
${
this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>`
: html`<ha-icon-button
.path=${mdiDotsVertical}
.label=${"ui.panel.config.automation.picker.bulk_action"}
slot="trigger"
></ha-icon-button>`
}
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
${
this.narrow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${categoryItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
${
this.narrow || this.hass.dockedSidebar === "docked"
? html` <ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${labelItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
</ha-button-menu-new>`
: nothing}
${!this.scripts.length ${!this.scripts.length
? html` <div class="empty" slot="empty"> ? html` <div class="empty" slot="empty">
<ha-svg-icon .path=${mdiScriptText}></ha-svg-icon> <ha-svg-icon .path=${mdiScriptText}></ha-svg-icon>
@ -629,6 +777,38 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
}); });
} }
private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent>
): void {
this._selected = ev.detail.value;
}
private async _handleBulkCategory(ev) {
const category = ev.currentTarget.value;
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
categories: { script: category },
})
);
});
await Promise.all(promises);
}
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
labels: this.hass.entities[entityId].labels.concat(label),
})
);
});
await Promise.all(promises);
}
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) { private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const entry = this.entityRegistry.find((e) => e.entity_id === ev.detail.id); const entry = this.entityRegistry.find((e) => e.entity_id === ev.detail.id);
if (entry) { if (entry) {
@ -782,6 +962,16 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
--mdc-icon-size: 80px; --mdc-icon-size: 80px;
max-width: 500px; max-width: 500px;
} }
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-button-menu-new ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {
--ha-label-background-color: var(--color, var(--grey-color));
--ha-label-background-opacity: 0.5;
}
`, `,
]; ];
} }