Allow to remove labels in multi select (#20368)

* Allow to remove labels in multi select

* reducedTouchTarget

* fix devices

* Update ha-config-devices-dashboard.ts
This commit is contained in:
Bram Kragten 2024-04-03 14:17:21 +02:00 committed by GitHub
parent e25d4f17aa
commit eb79a1e7d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 155 additions and 41 deletions

View File

@ -380,10 +380,26 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
</ha-menu-item>`; </ha-menu-item>`;
const labelItems = html`${this._labels?.map((label) => { const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined; const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-menu-item return html`<ha-menu-item
.value=${label.label_id} .value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel} @click=${this._handleBulkLabel}
keep-open
> >
<ha-checkbox
slot="start"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label style=${color ? `--color: ${color}` : ""}> <ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon ${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
@ -391,12 +407,13 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
${label.name} ${label.name}
</ha-label> </ha-label>
</ha-menu-item>`; </ha-menu-item>`;
})} <md-divider role="separator" tabindex="-1"></md-divider> })}
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._createLabel}> <ha-menu-item @click=${this._createLabel}>
<div slot="headline"> <div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")} ${this.hass.localize("ui.panel.config.labels.add_label")}
</div> </div></ha-menu-item
</ha-menu-item>`; >`;
return html` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
@ -1078,11 +1095,17 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
private async _handleBulkLabel(ev) { private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value; const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
const promises: Promise<UpdateEntityRegistryEntryResult>[] = []; const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => { this._selected.forEach((entityId) => {
promises.push( promises.push(
updateEntityRegistryEntry(this.hass, entityId, { updateEntityRegistryEntry(this.hass, entityId, {
labels: this.hass.entities[entityId].labels.concat(label), labels:
action === "add"
? this.hass.entities[entityId].labels.concat(label)
: this.hass.entities[entityId].labels.filter(
(lbl) => lbl !== label
),
}) })
); );
}); });

View File

@ -546,10 +546,26 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
const labelItems = html`${this._labels?.map((label) => { const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined; const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((deviceId) =>
this.hass.devices[deviceId]?.labels.includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((deviceId) =>
this.hass.devices[deviceId]?.labels.includes(label.label_id)
);
return html`<ha-menu-item return html`<ha-menu-item
.value=${label.label_id} .value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel} @click=${this._handleBulkLabel}
keep-open
> >
<ha-checkbox
slot="start"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label style=${color ? `--color: ${color}` : ""}> <ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon ${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
@ -557,12 +573,13 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
${label.name} ${label.name}
</ha-label> </ha-label>
</ha-menu-item>`; </ha-menu-item>`;
})}<md-divider role="separator" tabindex="-1"></md-divider> })}
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._createLabel}> <ha-menu-item @click=${this._createLabel}>
<div slot="headline"> <div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")} ${this.hass.localize("ui.panel.config.labels.add_label")}
</div> </div></ha-menu-item
</ha-menu-item>`; >`;
return html` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
@ -783,11 +800,17 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
private async _handleBulkLabel(ev) { private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value; const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
const promises: Promise<DeviceRegistryEntry>[] = []; const promises: Promise<DeviceRegistryEntry>[] = [];
this._selected.forEach((deviceId) => { this._selected.forEach((deviceId) => {
promises.push( promises.push(
updateDeviceRegistryEntry(this.hass, deviceId, { updateDeviceRegistryEntry(this.hass, deviceId, {
labels: this.hass.devices[deviceId].labels.concat(label), labels:
action === "add"
? this.hass.devices[deviceId].labels.concat(label)
: this.hass.devices[deviceId].labels.filter(
(lbl) => lbl !== label
),
}) })
); );
}); });

View File

@ -134,7 +134,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
{ value: string[] | undefined; items: Set<string> | undefined } { value: string[] | undefined; items: Set<string> | undefined }
> = {}; > = {};
@state() private _selectedEntities: string[] = []; @state() private _selected: string[] = [];
@state() private _expandedFilter?: string; @state() private _expandedFilter?: string;
@ -518,10 +518,26 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
const labelItems = html` ${this._labels?.map((label) => { const labelItems = html` ${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined; const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-menu-item return html`<ha-menu-item
.value=${label.label_id} .value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel} @click=${this._handleBulkLabel}
keep-open
> >
<ha-checkbox
slot="start"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label style=${color ? `--color: ${color}` : ""}> <ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon ${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
@ -529,12 +545,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
${label.name} ${label.name}
</ha-label> </ha-label>
</ha-menu-item>`; </ha-menu-item>`;
})}<md-divider role="separator" tabindex="-1"></md-divider> })}
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._createLabel}> <ha-menu-item @click=${this._createLabel}>
<div slot="headline"> <div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")} ${this.hass.localize("ui.panel.config.labels.add_label")}
</div> </div></ha-menu-item
</ha-menu-item>`; >`;
return html` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
@ -561,7 +578,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
} }
.filter=${this._filter} .filter=${this._filter}
selectable selectable
.selected=${this._selectedEntities.length} .selected=${this._selected.length}
@selection-changed=${this._handleSelectionChanged} @selection-changed=${this._handleSelectionChanged}
clickable clickable
@clear-filter=${this._clearFilter} @clear-filter=${this._clearFilter}
@ -903,14 +920,14 @@ ${
private _handleSelectionChanged( private _handleSelectionChanged(
ev: HASSDomEvent<SelectionChangedEvent> ev: HASSDomEvent<SelectionChangedEvent>
): void { ): void {
this._selectedEntities = ev.detail.value; this._selected = ev.detail.value;
} }
private async _enableSelected() { private async _enableSelected() {
showConfirmationDialog(this, { showConfirmationDialog(this, {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.confirm_title", "ui.panel.config.entities.picker.enable_selected.confirm_title",
{ number: this._selectedEntities.length } { number: this._selected.length }
), ),
text: this.hass.localize( text: this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.confirm_text" "ui.panel.config.entities.picker.enable_selected.confirm_text"
@ -921,7 +938,7 @@ ${
let require_restart = false; let require_restart = false;
let reload_delay = 0; let reload_delay = 0;
await Promise.all( await Promise.all(
this._selectedEntities.map(async (entity) => { this._selected.map(async (entity) => {
const result = await updateEntityRegistryEntry(this.hass, entity, { const result = await updateEntityRegistryEntry(this.hass, entity, {
disabled_by: null, disabled_by: null,
}); });
@ -958,7 +975,7 @@ ${
showConfirmationDialog(this, { showConfirmationDialog(this, {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.confirm_title", "ui.panel.config.entities.picker.disable_selected.confirm_title",
{ number: this._selectedEntities.length } { number: this._selected.length }
), ),
text: this.hass.localize( text: this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.confirm_text" "ui.panel.config.entities.picker.disable_selected.confirm_text"
@ -966,7 +983,7 @@ ${
confirmText: this.hass.localize("ui.common.disable"), confirmText: this.hass.localize("ui.common.disable"),
dismissText: this.hass.localize("ui.common.cancel"), dismissText: this.hass.localize("ui.common.cancel"),
confirm: () => { confirm: () => {
this._selectedEntities.forEach((entity) => this._selected.forEach((entity) =>
updateEntityRegistryEntry(this.hass, entity, { updateEntityRegistryEntry(this.hass, entity, {
disabled_by: "user", disabled_by: "user",
}) })
@ -980,7 +997,7 @@ ${
showConfirmationDialog(this, { showConfirmationDialog(this, {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.confirm_title", "ui.panel.config.entities.picker.hide_selected.confirm_title",
{ number: this._selectedEntities.length } { number: this._selected.length }
), ),
text: this.hass.localize( text: this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.confirm_text" "ui.panel.config.entities.picker.hide_selected.confirm_text"
@ -988,7 +1005,7 @@ ${
confirmText: this.hass.localize("ui.common.hide"), confirmText: this.hass.localize("ui.common.hide"),
dismissText: this.hass.localize("ui.common.cancel"), dismissText: this.hass.localize("ui.common.cancel"),
confirm: () => { confirm: () => {
this._selectedEntities.forEach((entity) => this._selected.forEach((entity) =>
updateEntityRegistryEntry(this.hass, entity, { updateEntityRegistryEntry(this.hass, entity, {
hidden_by: "user", hidden_by: "user",
}) })
@ -999,7 +1016,7 @@ ${
} }
private _unhideSelected() { private _unhideSelected() {
this._selectedEntities.forEach((entity) => this._selected.forEach((entity) =>
updateEntityRegistryEntry(this.hass, entity, { updateEntityRegistryEntry(this.hass, entity, {
hidden_by: null, hidden_by: null,
}) })
@ -1009,11 +1026,17 @@ ${
private async _handleBulkLabel(ev) { private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value; const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
const promises: Promise<UpdateEntityRegistryEntryResult>[] = []; const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selectedEntities.forEach((entityId) => { this._selected.forEach((entityId) => {
promises.push( promises.push(
updateEntityRegistryEntry(this.hass, entityId, { updateEntityRegistryEntry(this.hass, entityId, {
labels: this.hass.entities[entityId].labels.concat(label), labels:
action === "add"
? this.hass.entities[entityId].labels.concat(label)
: this.hass.entities[entityId].labels.filter(
(lbl) => lbl !== label
),
}) })
); );
}); });
@ -1021,21 +1044,19 @@ ${
} }
private _removeSelected() { private _removeSelected() {
const removeableEntities = this._selectedEntities.filter((entity) => { const removeableEntities = this._selected.filter((entity) => {
const stateObj = this.hass.states[entity]; const stateObj = this.hass.states[entity];
return stateObj?.attributes.restored; return stateObj?.attributes.restored;
}); });
showConfirmationDialog(this, { showConfirmationDialog(this, {
title: this.hass.localize( title: this.hass.localize(
`ui.panel.config.entities.picker.remove_selected.confirm_${ `ui.panel.config.entities.picker.remove_selected.confirm_${
removeableEntities.length !== this._selectedEntities.length removeableEntities.length !== this._selected.length ? "partly_" : ""
? "partly_"
: ""
}title`, }title`,
{ number: removeableEntities.length } { number: removeableEntities.length }
), ),
text: text:
removeableEntities.length === this._selectedEntities.length removeableEntities.length === this._selected.length
? this.hass.localize( ? this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.confirm_text" "ui.panel.config.entities.picker.remove_selected.confirm_text"
) )
@ -1043,7 +1064,7 @@ ${
"ui.panel.config.entities.picker.remove_selected.confirm_partly_text", "ui.panel.config.entities.picker.remove_selected.confirm_partly_text",
{ {
removable: removeableEntities.length, removable: removeableEntities.length,
selected: this._selectedEntities.length, selected: this._selected.length,
} }
), ),
confirmText: this.hass.localize("ui.common.remove"), confirmText: this.hass.localize("ui.common.remove"),

View File

@ -456,6 +456,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
.value=${label.label_id} .value=${label.label_id}
.action=${selected ? "remove" : "add"} .action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel} @click=${this._handleBulkLabel}
keep-open
> >
<ha-checkbox <ha-checkbox
slot="start" slot="start"

View File

@ -374,10 +374,26 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
</ha-menu-item>`; </ha-menu-item>`;
const labelItems = html` ${this._labels?.map((label) => { const labelItems = html` ${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined; const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-menu-item return html`<ha-menu-item
.value=${label.label_id} .value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel} @click=${this._handleBulkLabel}
keep-open
> >
<ha-checkbox
slot="start"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label style=${color ? `--color: ${color}` : ""}> <ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon ${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
@ -385,12 +401,13 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
${label.name} ${label.name}
</ha-label> </ha-label>
</ha-menu-item>`; </ha-menu-item>`;
})}<md-divider role="separator" tabindex="-1"></md-divider> })}
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._createLabel}> <ha-menu-item @click=${this._createLabel}>
<div slot="headline"> <div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")} ${this.hass.localize("ui.panel.config.labels.add_label")}
</div> </div></ha-menu-item
</ha-menu-item>`; >`;
return html` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
@ -756,11 +773,17 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
private async _handleBulkLabel(ev) { private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value; const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
const promises: Promise<UpdateEntityRegistryEntryResult>[] = []; const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => { this._selected.forEach((entityId) => {
promises.push( promises.push(
updateEntityRegistryEntry(this.hass, entityId, { updateEntityRegistryEntry(this.hass, entityId, {
labels: this.hass.entities[entityId].labels.concat(label), labels:
action === "add"
? this.hass.entities[entityId].labels.concat(label)
: this.hass.entities[entityId].labels.filter(
(lbl) => lbl !== label
),
}) })
); );
}); });

View File

@ -386,10 +386,26 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
</ha-menu-item>`; </ha-menu-item>`;
const labelItems = html`${this._labels?.map((label) => { const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined; const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-menu-item return html`<ha-menu-item
.value=${label.label_id} .value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel} @click=${this._handleBulkLabel}
keep-open
reducedTouchTarget
> >
<ha-checkbox
slot="start"
.checked=${selected}
.indeterminate=${partial}
></ha-checkbox>
<ha-label style=${color ? `--color: ${color}` : ""}> <ha-label style=${color ? `--color: ${color}` : ""}>
${label.icon ${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
@ -397,12 +413,13 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
${label.name} ${label.name}
</ha-label> </ha-label>
</ha-menu-item>`; </ha-menu-item>`;
})}<md-divider role="separator" tabindex="-1"></md-divider> })}
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._createLabel}> <ha-menu-item @click=${this._createLabel}>
<div slot="headline"> <div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")} ${this.hass.localize("ui.panel.config.labels.add_label")}
</div> </div></ha-menu-item
</ha-menu-item>`; >`;
return html` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
@ -825,11 +842,17 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
private async _handleBulkLabel(ev) { private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value; const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
const promises: Promise<UpdateEntityRegistryEntryResult>[] = []; const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => { this._selected.forEach((entityId) => {
promises.push( promises.push(
updateEntityRegistryEntry(this.hass, entityId, { updateEntityRegistryEntry(this.hass, entityId, {
labels: this.hass.entities[entityId].labels.concat(label), labels:
action === "add"
? this.hass.entities[entityId].labels.concat(label)
: this.hass.entities[entityId].labels.filter(
(lbl) => lbl !== label
),
}) })
); );
}); });