diff --git a/gallery/src/pages/components/ha-control-select.ts b/gallery/src/pages/components/ha-control-select.ts index 1b18d0e5ab20..8ab5f0ea891a 100644 --- a/gallery/src/pages/components/ha-control-select.ts +++ b/gallery/src/pages/components/ha-control-select.ts @@ -187,7 +187,7 @@ export class DemoHaControlSelect extends LitElement { --mdc-icon-size: 24px; --control-select-color: var(--state-fan-active-color); --control-select-thickness: 130px; - --control-select-border-radius: 48px; + --control-select-border-radius: 36px; } .vertical-selects { height: 300px; diff --git a/gallery/src/pages/components/ha-control-slider.ts b/gallery/src/pages/components/ha-control-slider.ts index 1b14a666cc68..ee1ef8a66288 100644 --- a/gallery/src/pages/components/ha-control-slider.ts +++ b/gallery/src/pages/components/ha-control-slider.ts @@ -151,7 +151,7 @@ export class DemoHaBarSlider extends LitElement { --control-slider-background: #ffcf4c; --control-slider-background-opacity: 0.2; --control-slider-thickness: 130px; - --control-slider-border-radius: 48px; + --control-slider-border-radius: 36px; } .vertical-sliders { height: 300px; diff --git a/gallery/src/pages/components/ha-control-switch.ts b/gallery/src/pages/components/ha-control-switch.ts index 662c9d13426c..dc154b725d7e 100644 --- a/gallery/src/pages/components/ha-control-switch.ts +++ b/gallery/src/pages/components/ha-control-switch.ts @@ -118,7 +118,7 @@ export class DemoHaControlSwitch extends LitElement { --control-switch-on-color: var(--green-color); --control-switch-off-color: var(--red-color); --control-switch-thickness: 130px; - --control-switch-border-radius: 48px; + --control-switch-border-radius: 36px; --control-switch-padding: 6px; --mdc-icon-size: 24px; } diff --git a/pyproject.toml b/pyproject.toml index caf1247abc3b..f19e51af4f57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20240402.0" +version = "20240402.1" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" diff --git a/src/components/data-table/ha-data-table-labels.ts b/src/components/data-table/ha-data-table-labels.ts index 1c47f0bf64d3..66c137e92a6a 100644 --- a/src/components/data-table/ha-data-table-labels.ts +++ b/src/components/data-table/ha-data-table-labels.ts @@ -5,20 +5,22 @@ import { LabelRegistryEntry } from "../../data/label_registry"; import { computeCssColor } from "../../common/color/compute-color"; import { fireEvent } from "../../common/dom/fire_event"; import "../ha-label"; +import { stringCompare } from "../../common/string/compare"; @customElement("ha-data-table-labels") class HaDataTableLabels extends LitElement { @property({ attribute: false }) public labels!: LabelRegistryEntry[]; protected render(): TemplateResult { + const labels = this.labels.sort((a, b) => stringCompare(a.name, b.name)); return html` ${repeat( - this.labels.slice(0, 2), + labels.slice(0, 2), (label) => label.label_id, (label) => this._renderLabel(label, true) )} - ${this.labels.length > 2 + ${labels.length > 2 ? html` - +${this.labels.length - 2} + +${labels.length - 2} ${repeat( - this.labels.slice(2), + labels.slice(2), (label) => label.label_id, (label) => html` diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index a7b404f29abe..5fe9b63d5eec 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -21,10 +21,8 @@ import { getDeviceEntityDisplayLookup, } from "../data/device_registry"; import { EntityRegistryDisplayEntry } from "../data/entity_registry"; -import { - showAlertDialog, - showPromptDialog, -} from "../dialogs/generic/show-dialog-box"; +import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; +import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail"; import { HomeAssistant, ValueChangedEvent } from "../types"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; import "./ha-combo-box"; @@ -38,7 +36,7 @@ type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry; const rowRenderer: ComboBoxLitRenderer = (item) => html` ${item.icon ? html`` @@ -46,6 +44,10 @@ const rowRenderer: ComboBoxLitRenderer = (item) => ${item.name} `; +const ADD_NEW_ID = "___ADD_NEW___"; +const NO_ITEMS_ID = "___NO_ITEMS___"; +const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___"; + @customElement("ha-area-picker") export class HaAreaPicker extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -134,20 +136,6 @@ export class HaAreaPicker extends LitElement { noAdd: this["noAdd"], excludeAreas: this["excludeAreas"] ): AreaRegistryEntry[] => { - if (!areas.length) { - return [ - { - area_id: "no_areas", - floor_id: null, - name: this.hass.localize("ui.components.area-picker.no_areas"), - picture: null, - icon: null, - aliases: [], - labels: [], - }, - ]; - } - let deviceEntityLookup: DeviceEntityDisplayLookup = {}; let inputDevices: DeviceRegistryEntry[] | undefined; let inputEntities: EntityRegistryDisplayEntry[] | undefined; @@ -284,9 +272,9 @@ export class HaAreaPicker extends LitElement { if (!outputAreas.length) { outputAreas = [ { - area_id: "no_areas", + area_id: NO_ITEMS_ID, floor_id: null, - name: this.hass.localize("ui.components.area-picker.no_match"), + name: this.hass.localize("ui.components.area-picker.no_areas"), picture: null, icon: null, aliases: [], @@ -300,7 +288,7 @@ export class HaAreaPicker extends LitElement { : [ ...outputAreas, { - area_id: "add_new", + area_id: ADD_NEW_ID, floor_id: null, name: this.hass.localize("ui.components.area-picker.add_new"), picture: null, @@ -374,20 +362,40 @@ export class HaAreaPicker extends LitElement { const filteredItems = fuzzyFilterSort( filterString, - target.items || [] + target.items?.filter( + (item) => ![NO_ITEMS_ID, ADD_NEW_ID].includes(item.label_id) + ) || [] ); - if (!this.noAdd && filteredItems?.length === 0) { - this._suggestion = filterString; - this.comboBox.filteredItems = [ - { - area_id: "add_new_suggestion", - name: this.hass.localize( - "ui.components.area-picker.add_new_sugestion", - { name: this._suggestion } - ), - picture: null, - }, - ]; + if (filteredItems.length === 0) { + if (!this.noAdd) { + this.comboBox.filteredItems = [ + { + area_id: NO_ITEMS_ID, + floor_id: null, + name: this.hass.localize("ui.components.area-picker.no_match"), + icon: null, + picture: null, + labels: [], + aliases: [], + }, + ] as AreaRegistryEntry[]; + } else { + this._suggestion = filterString; + this.comboBox.filteredItems = [ + { + area_id: ADD_NEW_SUGGESTION_ID, + floor_id: null, + name: this.hass.localize( + "ui.components.area-picker.add_new_sugestion", + { name: this._suggestion } + ), + icon: "mdi:plus", + picture: null, + labels: [], + aliases: [], + }, + ] as AreaRegistryEntry[]; + } } else { this.comboBox.filteredItems = filteredItems; } @@ -405,11 +413,13 @@ export class HaAreaPicker extends LitElement { ev.stopPropagation(); let newValue = ev.detail.value; - if (newValue === "no_areas") { + if (newValue === NO_ITEMS_ID) { newValue = ""; + this.comboBox.setInputValue(""); + return; } - if (!["add_new_suggestion", "add_new"].includes(newValue)) { + if (![ADD_NEW_SUGGESTION_ID, ADD_NEW_ID].includes(newValue)) { if (newValue !== this._value) { this._setValue(newValue); } @@ -417,25 +427,14 @@ export class HaAreaPicker extends LitElement { } (ev.target as any).value = this._value; - showPromptDialog(this, { - title: this.hass.localize("ui.components.area-picker.add_dialog.title"), - text: this.hass.localize("ui.components.area-picker.add_dialog.text"), - confirmText: this.hass.localize( - "ui.components.area-picker.add_dialog.add" - ), - inputLabel: this.hass.localize( - "ui.components.area-picker.add_dialog.name" - ), - defaultValue: - newValue === "add_new_suggestion" ? this._suggestion : undefined, - confirm: async (name) => { - if (!name) { - return; - } + + this.hass.loadFragmentTranslation("config"); + + showAreaRegistryDetailDialog(this, { + suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "", + createEntry: async (values) => { try { - const area = await createAreaRegistryEntry(this.hass, { - name, - }); + const area = await createAreaRegistryEntry(this.hass, values); const areas = [...Object.values(this.hass.areas), area]; this.comboBox.filteredItems = this._getAreas( areas, @@ -455,18 +454,16 @@ export class HaAreaPicker extends LitElement { } catch (err: any) { showAlertDialog(this, { title: this.hass.localize( - "ui.components.area-picker.add_dialog.failed_create_area" + "ui.components.area-picker.failed_create_area" ), text: err.message, }); } }, - cancel: () => { - this._setValue(undefined); - this._suggestion = undefined; - this.comboBox.setInputValue(""); - }, }); + + this._suggestion = undefined; + this.comboBox.setInputValue(""); } private _setValue(value?: string) { diff --git a/src/components/ha-floor-picker.ts b/src/components/ha-floor-picker.ts index 9ac37cf746b1..f8e29dc7ef48 100644 --- a/src/components/ha-floor-picker.ts +++ b/src/components/ha-floor-picker.ts @@ -23,11 +23,9 @@ import { getFloorAreaLookup, subscribeFloorRegistry, } from "../data/floor_registry"; -import { - showAlertDialog, - showPromptDialog, -} from "../dialogs/generic/show-dialog-box"; +import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; import { SubscribeMixin } from "../mixins/subscribe-mixin"; +import { showFloorRegistryDetailDialog } from "../panels/config/areas/show-dialog-floor-registry-detail"; import { HomeAssistant, ValueChangedEvent } from "../types"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; import "./ha-combo-box"; @@ -386,7 +384,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { this.comboBox.filteredItems = [ { floor_id: NO_FLOORS_ID, - name: this.hass.localize("ui.components.floor-picker.no_floors"), + name: this.hass.localize("ui.components.floor-picker.no_match"), icon: null, level: null, aliases: [], @@ -438,25 +436,14 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { } (ev.target as any).value = this._value; - showPromptDialog(this, { - title: this.hass.localize("ui.components.floor-picker.add_dialog.title"), - text: this.hass.localize("ui.components.floor-picker.add_dialog.text"), - confirmText: this.hass.localize( - "ui.components.floor-picker.add_dialog.add" - ), - inputLabel: this.hass.localize( - "ui.components.floor-picker.add_dialog.name" - ), - defaultValue: - newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : undefined, - confirm: async (name) => { - if (!name) { - return; - } + + this.hass.loadFragmentTranslation("config"); + + showFloorRegistryDetailDialog(this, { + suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "", + createEntry: async (values) => { try { - const floor = await createFloorRegistryEntry(this.hass, { - name, - }); + const floor = await createFloorRegistryEntry(this.hass, values); const floors = [...this._floors!, floor]; this.comboBox.filteredItems = this._getFloors( floors, @@ -477,18 +464,16 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { } catch (err: any) { showAlertDialog(this, { title: this.hass.localize( - "ui.components.floor-picker.add_dialog.failed_create_floor" + "ui.components.floor-picker.failed_create_floor" ), text: err.message, }); } }, - cancel: () => { - this._setValue(undefined); - this._suggestion = undefined; - this.comboBox.setInputValue(""); - }, }); + + this._suggestion = undefined; + this.comboBox.setInputValue(""); } private _setValue(value?: string) { diff --git a/src/components/ha-label-picker.ts b/src/components/ha-label-picker.ts index 15b42c07b266..289b7ba269c1 100644 --- a/src/components/ha-label-picker.ts +++ b/src/components/ha-label-picker.ts @@ -445,6 +445,8 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) { (ev.target as any).value = this._value; + this.hass.loadFragmentTranslation("config"); + showLabelDetailDialog(this, { entry: undefined, suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "", diff --git a/src/components/ha-label.ts b/src/components/ha-label.ts index e542b06be531..d9d82052015b 100644 --- a/src/components/ha-label.ts +++ b/src/components/ha-label.ts @@ -43,6 +43,7 @@ class HaLabel extends LitElement { border-radius: 18px; color: var(--ha-label-text-color); --mdc-icon-size: 12px; + text-wrap: nowrap; } .content > * { position: relative; diff --git a/src/components/ha-labels-picker.ts b/src/components/ha-labels-picker.ts index ccf4511a6119..2cc592e26efd 100644 --- a/src/components/ha-labels-picker.ts +++ b/src/components/ha-labels-picker.ts @@ -17,6 +17,7 @@ import "./chips/ha-input-chip"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; import "./ha-label-picker"; import type { HaLabelPicker } from "./ha-label-picker"; +import { stringCompare } from "../common/string/compare"; @customElement("ha-labels-picker") export class HaLabelsPicker extends SubscribeMixin(LitElement) { @@ -75,7 +76,7 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) { @property({ type: Boolean }) public required = false; - @state() private _labels?: LabelRegistryEntry[]; + @state() private _labels?: { [id: string]: LabelRegistryEntry }; @query("ha-label-picker", true) public labelPicker!: HaLabelPicker; @@ -92,22 +93,28 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) { protected hassSubscribe(): (UnsubscribeFunc | Promise)[] { return [ subscribeLabelRegistry(this.hass.connection, (labels) => { - this._labels = labels; + const lookUp = {}; + labels.forEach((label) => { + lookUp[label.label_id] = label; + }); + this._labels = lookUp; }), ]; } protected render(): TemplateResult { + const labels = this.value + ?.map((id) => this._labels?.[id]) + .sort((a, b) => + stringCompare(a?.name || "", b?.name || "", this.hass.locale.language) + ); return html` - ${this.value?.length + ${labels?.length ? html` ${repeat( - this.value, - (item) => item, - (item, idx) => { - const label = this._labels?.find( - (lbl) => lbl.label_id === item - ); + labels, + (label) => label?.label_id, + (label, idx) => { const color = label?.color ? computeCssColor(label.color) : undefined; @@ -168,9 +175,6 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) { label.label_id, values ); - this._labels = this._labels!.map((lbl) => - lbl.label_id === updated.label_id ? updated : lbl - ); return updated; }, }); diff --git a/src/components/ha-menu-item.ts b/src/components/ha-menu-item.ts index e96e867f6f83..9c03e4e7f567 100644 --- a/src/components/ha-menu-item.ts +++ b/src/components/ha-menu-item.ts @@ -1,7 +1,7 @@ -import { customElement } from "lit/decorators"; +import { MdMenuItem } from "@material/web/menu/menu-item"; import "element-internals-polyfill"; import { CSSResult, css } from "lit"; -import { MdMenuItem } from "@material/web/menu/menu-item"; +import { customElement } from "lit/decorators"; @customElement("ha-menu-item") export class HaMenuItem extends MdMenuItem { @@ -26,6 +26,13 @@ export class HaMenuItem extends MdMenuItem { --md-sys-color-on-primary-container: var(--primary-text-color); --md-sys-color-on-secondary-container: var(--primary-text-color); } + :host(.warning) { + --md-menu-item-label-text-color: var(--error-color); + --md-menu-item-leading-icon-color: var(--error-color); + } + ::slotted([slot="headline"]) { + text-wrap: nowrap; + } `, ]; } diff --git a/src/components/ha-selector/ha-selector-label.ts b/src/components/ha-selector/ha-selector-label.ts index 14e90e8f7d69..f8a091769214 100644 --- a/src/components/ha-selector/ha-selector-label.ts +++ b/src/components/ha-selector/ha-selector-label.ts @@ -30,6 +30,7 @@ export class HaLabelSelector extends LitElement { if (this.selector.label.multiple) { return html` -
- - ${this.hass.localize("ui.card.alarm_control_panel.disarm")} - - ` : html` @@ -76,7 +70,15 @@ class MoreInfoAlarmControlPanel extends LitElement { `} - +
+ ${["triggered", "arming", "pending"].includes(this.stateObj.state) + ? html` + + ${this.hass.localize("ui.card.alarm_control_panel.disarm")} + + ` + : nothing} +
`; } @@ -127,8 +129,12 @@ class MoreInfoAlarmControlPanel extends LitElement { transition: background-color 180ms ease-in-out; opacity: 0.2; } - .status ha-outlined-button { - margin-top: 32px; + ha-control-button.disarm { + height: 60px; + min-width: 130px; + max-width: 200px; + margin: 0 auto; + --control-button-border-radius: 24px; } `, ]; diff --git a/src/dialogs/more-info/controls/more-info-lock.ts b/src/dialogs/more-info/controls/more-info-lock.ts index fccc841c9014..ac0f45e43ad9 100644 --- a/src/dialogs/more-info/controls/more-info-lock.ts +++ b/src/dialogs/more-info/controls/more-info-lock.ts @@ -170,7 +170,7 @@ class MoreInfoLock extends LitElement { --control-button-border-radius: 24px; } .open-button { - width: 100px; + width: 130px; --control-button-background-color: var(--state-color); } .open-button.confirm { diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index ecc3411f08ac..33413df92be3 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -77,6 +77,8 @@ declare global { } } +const DEFAULT_VIEW: View = "info"; + @customElement("ha-more-info-dialog") export class MoreInfoDialog extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -85,7 +87,9 @@ export class MoreInfoDialog extends LitElement { @state() private _entityId?: string | null; - @state() private _currView: View = "info"; + @state() private _currView: View = DEFAULT_VIEW; + + @state() private _initialView: View = DEFAULT_VIEW; @state() private _childView?: ChildView; @@ -102,7 +106,8 @@ export class MoreInfoDialog extends LitElement { this.closeDialog(); return; } - this._currView = params.view || "info"; + this._currView = params.view || DEFAULT_VIEW; + this._initialView = params.view || DEFAULT_VIEW; this._childView = undefined; this.large = false; this._loadEntityRegistryEntry(); @@ -127,6 +132,7 @@ export class MoreInfoDialog extends LitElement { this._entry = undefined; this._childView = undefined; this._infoEditMode = false; + this._initialView = DEFAULT_VIEW; fireEvent(this, "dialog-closed", { dialog: this.localName }); } @@ -183,10 +189,15 @@ export class MoreInfoDialog extends LitElement { if (this._childView) { this._childView = undefined; } else { - this.setView("info"); + this.setView(this._initialView); } } + private _resetInitialView() { + this._initialView = DEFAULT_VIEW; + this.setView(DEFAULT_VIEW); + } + private _goToHistory() { this.setView("history"); } @@ -262,7 +273,10 @@ export class MoreInfoDialog extends LitElement { const title = this._childView?.viewTitle ?? name; - const isInfoView = this._currView === "info" && !this._childView; + const isDefaultView = this._currView === DEFAULT_VIEW && !this._childView; + const isSpecificInitialView = + this._initialView !== DEFAULT_VIEW && !this._childView; + const showCloseIcon = isDefaultView || isSpecificInitialView; return html` - ${isInfoView + ${showCloseIcon ? html` ${title} - ${isInfoView + ${isDefaultView ? html` ${this.shouldShowHistory(domain) ? html` @@ -407,7 +421,34 @@ export class MoreInfoDialog extends LitElement { ` : nothing} ` - : nothing} + : isSpecificInitialView + ? html` + + + + + ${this.hass.localize("ui.dialogs.more_info_control.info")} + + + + ` + : nothing}
{ this._params = params; this._error = undefined; - this._name = this._params.entry ? this._params.entry.name : ""; + this._name = this._params.entry + ? this._params.entry.name + : this._params.suggestedName || ""; this._aliases = this._params.entry ? this._params.entry.aliases : []; this._labels = this._params.entry ? this._params.entry.labels : []; this._picture = this._params.entry?.picture || null; diff --git a/src/panels/config/areas/dialog-floor-registry-detail.ts b/src/panels/config/areas/dialog-floor-registry-detail.ts index 775d18399382..12e666873f3b 100644 --- a/src/panels/config/areas/dialog-floor-registry-detail.ts +++ b/src/panels/config/areas/dialog-floor-registry-detail.ts @@ -38,7 +38,9 @@ class DialogFloorDetail extends LitElement { ): Promise { this._params = params; this._error = undefined; - this._name = this._params.entry ? this._params.entry.name : ""; + this._name = this._params.entry + ? this._params.entry.name + : this._params.suggestedName || ""; this._aliases = this._params.entry?.aliases || []; this._icon = this._params.entry?.icon || null; this._level = this._params.entry?.level ?? null; diff --git a/src/panels/config/areas/show-dialog-area-registry-detail.ts b/src/panels/config/areas/show-dialog-area-registry-detail.ts index 0f0eb422fa4b..19d56e52c1fc 100644 --- a/src/panels/config/areas/show-dialog-area-registry-detail.ts +++ b/src/panels/config/areas/show-dialog-area-registry-detail.ts @@ -6,6 +6,7 @@ import { export interface AreaRegistryDetailDialogParams { entry?: AreaRegistryEntry; + suggestedName?: string; createEntry?: (values: AreaRegistryEntryMutableParams) => Promise; updateEntry?: ( updates: Partial diff --git a/src/panels/config/areas/show-dialog-floor-registry-detail.ts b/src/panels/config/areas/show-dialog-floor-registry-detail.ts index 507d10346999..ecaa74786bb5 100644 --- a/src/panels/config/areas/show-dialog-floor-registry-detail.ts +++ b/src/panels/config/areas/show-dialog-floor-registry-detail.ts @@ -6,6 +6,7 @@ import { export interface FloorRegistryDetailDialogParams { entry?: FloorRegistryEntry; + suggestedName?: string; createEntry?: (values: FloorRegistryEntryMutableParams) => Promise; updateEntry?: ( updates: Partial diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index b5208b944e3c..24232a69f860 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -11,10 +11,8 @@ import { mdiInformationOutline, mdiMenuDown, mdiPlay, - mdiPlayCircleOutline, mdiPlus, mdiRobotHappy, - mdiStopCircleOutline, mdiTag, mdiToggleSwitch, mdiToggleSwitchOffOutline, @@ -60,6 +58,7 @@ import "../../../components/ha-filter-labels"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-overflow-menu"; import "../../../components/ha-menu"; +import type { HaMenu } from "../../../components/ha-menu"; import "../../../components/ha-menu-item"; import "../../../components/ha-sub-menu"; import "../../../components/ha-svg-icon"; @@ -101,7 +100,6 @@ import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { configSections } from "../ha-panel-config"; import { showNewAutomationDialog } from "./show-dialog-new-automation"; -import type { HaMenu } from "../../../components/ha-menu"; type AutomationItem = AutomationEntity & { name: string; @@ -379,7 +377,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { @@ -728,6 +728,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { if (this._searchParms.has("blueprint")) { this._filterBlueprint(); } + if (this._searchParms.has("label")) { + this._filterLabel(); + } } private _filterExpanded(ev) { @@ -815,6 +818,21 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { this._filteredAutomations = items ? [...items] : undefined; } + private _filterLabel() { + const label = this._searchParms.get("label"); + if (!label) { + return; + } + this._filters = { + ...this._filters, + "ha-filter-labels": { + value: [label], + items: undefined, + }, + }; + this._applyFilters(); + } + private async _filterBlueprint() { const blueprint = this._searchParms.get("blueprint"); if (!blueprint) { diff --git a/src/panels/config/category/ha-category-picker.ts b/src/panels/config/category/ha-category-picker.ts index 3cd495935a96..686ed907fb03 100644 --- a/src/panels/config/category/ha-category-picker.ts +++ b/src/panels/config/category/ha-category-picker.ts @@ -188,9 +188,7 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) { this.comboBox.filteredItems = [ { category_id: NO_CATEGORIES_ID, - name: this.hass.localize( - "ui.components.category-picker.no_categories" - ), + name: this.hass.localize("ui.components.category-picker.no_match"), icon: null, }, ] as ScorableCategoryRegistryEntry[]; @@ -239,6 +237,8 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) { (ev.target as any).value = this._value; + this.hass.loadFragmentTranslation("config"); + showCategoryRegistryDetailDialog(this, { scope: this.scope!, suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "", diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index ce3e0b26e74d..76e086cba941 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -1,6 +1,6 @@ import { consume } from "@lit-labs/context"; import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; -import { mdiPlus } from "@mdi/js"; +import { mdiChevronRight, mdiMenuDown, mdiPlus } from "@mdi/js"; import { CSSResultGroup, LitElement, @@ -13,6 +13,7 @@ import { import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; +import { computeCssColor } from "../../../common/color/compute-color"; import { HASSDomEvent } from "../../../common/dom/fire_event"; import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { @@ -24,6 +25,7 @@ import { LocalizeFunc } from "../../../common/translations/localize"; import { DataTableColumnContainer, RowClickedEvent, + SelectionChangedEvent, } from "../../../components/data-table/ha-data-table"; import "../../../components/data-table/ha-data-table-labels"; import "../../../components/entity/ha-battery-icon"; @@ -37,12 +39,15 @@ import "../../../components/ha-filter-integrations"; import "../../../components/ha-filter-labels"; import "../../../components/ha-filter-states"; import "../../../components/ha-icon-button"; +import "../../../components/ha-menu-item"; +import "../../../components/ha-sub-menu"; import { ConfigEntry, sortConfigEntries } from "../../../data/config_entries"; import { fullEntitiesContext } from "../../../data/context"; import { DeviceEntityLookup, DeviceRegistryEntry, computeDeviceName, + updateDeviceRegistryEntry, } from "../../../data/device_registry"; import { EntityRegistryEntry, @@ -91,6 +96,8 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { @state() private _searchParms = new URLSearchParams(window.location.search); + @state() private _selected: string[] = []; + @state() private _filter: string = history.state?.filter || ""; @state() private _filters: Record< @@ -185,6 +192,23 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { }, }; } + if (this._searchParms.has("label")) { + this._filterLabel(); + } + } + + private _filterLabel() { + const label = this._searchParms.get("label"); + if (!label) { + return; + } + this._filters = { + ...this._filters, + "ha-filter-labels": { + value: [label], + items: undefined, + }, + }; } private _clearFilter() { @@ -518,6 +542,21 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { this._labels ); + const labelItems = html` ${this._labels?.map((label) => { + const color = label.color ? computeCssColor(label.color) : undefined; + return html` + + ${label.icon + ? html`` + : nothing} + ${label.name} + + `; + })}`; + return html` + + ${!this.narrow + ? html` + + + + ${labelItems} + ` + : html` + + + + +
+ ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.add_label" + )} +
+ +
+ ${labelItems} +
+
`}
`; } @@ -683,6 +768,25 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { }); } + private _handleSelectionChanged( + ev: HASSDomEvent + ): void { + this._selected = ev.detail.value; + } + + private async _handleBulkLabel(ev) { + const label = ev.currentTarget.value; + const promises: Promise[] = []; + this._selected.forEach((deviceId) => { + promises.push( + updateDeviceRegistryEntry(this.hass, deviceId, { + labels: this.hass.devices[deviceId].labels.concat(label), + }) + ); + }); + await Promise.all(promises); + } + static get styles(): CSSResultGroup { return [ css` @@ -704,6 +808,16 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { text-transform: uppercase; direction: var(--direction); } + 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; + } `, haStyle, ]; diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index c0d92b96b794..80adf624d587 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -3,12 +3,17 @@ import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; import { mdiAlertCircle, mdiCancel, + mdiChevronRight, mdiDelete, + mdiDotsVertical, + mdiEye, mdiEyeOff, + mdiMenuDown, mdiPencilOff, mdiPlus, mdiRestoreAlert, - mdiUndo, + mdiToggleSwitch, + mdiToggleSwitchOffOutline, } from "@mdi/js"; import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { @@ -24,6 +29,7 @@ import { ifDefined } from "lit/directives/if-defined"; import { styleMap } from "lit/directives/style-map"; import { until } from "lit/directives/until"; import memoize from "memoize-one"; +import { computeCssColor } from "../../../common/color/compute-color"; import type { HASSDomEvent } from "../../../common/dom/fire_event"; import { computeDomain } from "../../../common/entity/compute_domain"; import { computeStateName } from "../../../common/entity/compute_state_name"; @@ -44,16 +50,19 @@ import "../../../components/ha-check-list-item"; import "../../../components/ha-filter-devices"; import "../../../components/ha-filter-floor-areas"; import "../../../components/ha-filter-integrations"; -import "../../../components/ha-filter-states"; import "../../../components/ha-filter-labels"; +import "../../../components/ha-filter-states"; import "../../../components/ha-icon"; import "../../../components/ha-icon-button"; +import "../../../components/ha-menu-item"; +import "../../../components/ha-sub-menu"; import "../../../components/ha-svg-icon"; import { ConfigEntry, getConfigEntries } from "../../../data/config_entries"; import { fullEntitiesContext } from "../../../data/context"; import { UNAVAILABLE } from "../../../data/entity"; import { EntityRegistryEntry, + UpdateEntityRegistryEntryResult, computeEntityRegistryName, removeEntityRegistryEntry, updateEntityRegistryEntry, @@ -505,13 +514,28 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { [...filteredDomains][0] ); + const labelItems = html` ${this._labels?.map((label) => { + const color = label.color ? computeCssColor(label.color) : undefined; + return html` + + ${label.icon + ? html`` + : nothing} + ${label.name} + + `; + })}`; + return html` filter.value?.length - ).length} + .filters=${ + Object.values(this._filters).filter((filter) => filter.value?.length) + .length + } .filter=${this._filter} selectable .selected=${this._selectedEntities.length} @@ -543,100 +568,131 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { .hass=${this.hass} slot="toolbar-icon" > -
- ${!this.narrow - ? html` - ${this.hass.localize( - "ui.panel.config.entities.picker.enable_selected.button" - )} - ${this.hass.localize( - "ui.panel.config.entities.picker.disable_selected.button" - )} - ${this.hass.localize( - "ui.panel.config.entities.picker.hide_selected.button" - )} - ${this.hass.localize( - "ui.panel.config.entities.picker.remove_selected.button" - )} - ` - : html` - - - ${this.hass.localize( - "ui.panel.config.entities.picker.enable_selected.button" - )} - - - - ${this.hass.localize( - "ui.panel.config.entities.picker.disable_selected.button" - )} - - - - ${this.hass.localize( - "ui.panel.config.entities.picker.hide_selected.button" - )} - - - - ${this.hass.localize( - "ui.panel.config.entities.picker.remove_selected.button" - )} - - `} -
- ${this._filters.config_entry?.value?.length - ? html` - Filtering by config entry - ${this._entries?.find( - (entry) => - entry.entry_id === this._filters.config_entry!.value![0] - )?.title || this._filters.config_entry.value[0]} - ` - : nothing} + + +${ + !this.narrow + ? html` + + + + ${labelItems} + ` + : nothing +} + + ${ + this.narrow + ? html` + + ` + : html`` + } + + ${ + this.narrow + ? html` + +
+ ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.add_label" + )} +
+ +
+ ${labelItems} +
+ ` + : nothing + } + + + +
+ ${this.hass.localize( + "ui.panel.config.entities.picker.enable_selected.button" + )} +
+
+ + +
+ ${this.hass.localize( + "ui.panel.config.entities.picker.disable_selected.button" + )} +
+
+ + + + +
+ ${this.hass.localize( + "ui.panel.config.entities.picker.unhide_selected.button" + )} +
+
+ + +
+ ${this.hass.localize( + "ui.panel.config.entities.picker.hide_selected.button" + )} +
+
+ + + + +
+ ${this.hass.localize( + "ui.panel.config.entities.picker.remove_selected.button" + )} +
+
+ +
+ ${ + this._filters.config_entry?.value?.length + ? html` + Filtering by config entry + ${this._entries?.find( + (entry) => + entry.entry_id === this._filters.config_entry!.value![0] + )?.title || this._filters.config_entry.value[0]} + ` + : nothing + } - ${includeAddDeviceFab - ? html` - - ` - : nothing} + ${ + includeAddDeviceFab + ? html` + + ` + : nothing + }
`; } @@ -758,6 +818,23 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { }, }; } + if (this._searchParms.has("label")) { + this._filterLabel(); + } + } + + private _filterLabel() { + const label = this._searchParms.get("label"); + if (!label) { + return; + } + this._filters = { + ...this._filters, + "ha-filter-labels": { + value: [label], + items: undefined, + }, + }; } private _clearFilter() { @@ -914,6 +991,28 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { }); } + private _unhideSelected() { + this._selectedEntities.forEach((entity) => + updateEntityRegistryEntry(this.hass, entity, { + hidden_by: null, + }) + ); + this._clearSelection(); + } + + private async _handleBulkLabel(ev) { + const label = ev.currentTarget.value; + const promises: Promise[] = []; + this._selectedEntities.forEach((entityId) => { + promises.push( + updateEntityRegistryEntry(this.hass, entityId, { + labels: this.hass.entities[entityId].labels.concat(label), + }) + ); + }); + await Promise.all(promises); + } + private _removeSelected() { const removeableEntities = this._selectedEntities.filter((entity) => { const stateObj = this.hass.states[entity]; @@ -1063,6 +1162,17 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { text-transform: uppercase; direction: var(--direction); } + + 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; + } `, ]; } diff --git a/src/panels/config/labels/ha-config-labels.ts b/src/panels/config/labels/ha-config-labels.ts index 193dc4fc064b..f8396ada5e52 100644 --- a/src/panels/config/labels/ha-config-labels.ts +++ b/src/panels/config/labels/ha-config-labels.ts @@ -1,4 +1,11 @@ -import { mdiDelete, mdiHelpCircle, mdiPlus } from "@mdi/js"; +import { + mdiDelete, + mdiDevices, + mdiHelpCircle, + mdiPlus, + mdiRobot, + mdiShape, +} from "@mdi/js"; import { LitElement, PropertyValues, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; @@ -28,6 +35,7 @@ import "../../../layouts/hass-tabs-subpage-data-table"; import { HomeAssistant, Route } from "../../../types"; import { configSections } from "../ha-panel-config"; import { showLabelDetailDialog } from "./show-dialog-label-detail"; +import { navigate } from "../../../common/navigate"; @customElement("ha-config-labels") export class HaConfigLabels extends LitElement { @@ -81,6 +89,21 @@ export class HaConfigLabels extends LitElement { .hass=${this.hass} narrow .items=${[ + { + label: this.hass.localize("ui.panel.config.entities.caption"), + path: mdiShape, + action: () => this._navigateEntities(label), + }, + { + label: this.hass.localize("ui.panel.config.devices.caption"), + path: mdiDevices, + action: () => this._navigateDevices(label), + }, + { + label: this.hass.localize("ui.panel.config.automation.caption"), + path: mdiRobot, + action: () => this._navigateAutomations(label), + }, { label: this.hass.localize("ui.common.delete"), path: mdiDelete, @@ -225,6 +248,20 @@ export class HaConfigLabels extends LitElement { return false; } } + + private _navigateEntities(label: LabelRegistryEntry) { + navigate(`/config/entities?historyBack=1&label=${label.label_id}`); + } + + private _navigateDevices(label: LabelRegistryEntry) { + navigate(`/config/devices/dashboard?historyBack=1&label=${label.label_id}`); + } + + private _navigateAutomations(label: LabelRegistryEntry) { + navigate( + `/config/automation/dashboard?historyBack=1&label=${label.label_id}` + ); + } } declare global { diff --git a/src/panels/config/scene/ha-scene-dashboard.ts b/src/panels/config/scene/ha-scene-dashboard.ts index 36826e56df26..cb8bebaaf177 100644 --- a/src/panels/config/scene/ha-scene-dashboard.ts +++ b/src/panels/config/scene/ha-scene-dashboard.ts @@ -1,10 +1,13 @@ import { consume } from "@lit-labs/context"; import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; import { + mdiChevronRight, mdiContentDuplicate, mdiDelete, + mdiDotsVertical, mdiHelpCircle, mdiInformationOutline, + mdiMenuDown, mdiPalette, mdiPencilOff, mdiPlay, @@ -33,6 +36,7 @@ import { LocalizeFunc } from "../../../common/translations/localize"; import { DataTableColumnContainer, RowClickedEvent, + SelectionChangedEvent, } from "../../../components/data-table/ha-data-table"; import "../../../components/data-table/ha-data-table-labels"; import "../../../components/ha-button"; @@ -46,13 +50,19 @@ import "../../../components/ha-icon-button"; import "../../../components/ha-icon-overflow-menu"; import "../../../components/ha-state-icon"; import "../../../components/ha-svg-icon"; +import "../../../components/ha-menu-item"; +import "../../../components/ha-sub-menu"; import { CategoryRegistryEntry, subscribeCategoryRegistry, } from "../../../data/category_registry"; import { fullEntitiesContext } from "../../../data/context"; 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 { LabelRegistryEntry, @@ -77,6 +87,7 @@ import { documentationUrl } from "../../../util/documentation-url"; import { showToast } from "../../../util/toast"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { configSections } from "../ha-panel-config"; +import { computeCssColor } from "../../../common/color/compute-color"; type SceneItem = SceneEntity & { name: string; @@ -96,6 +107,10 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { @property({ attribute: false }) public scenes!: SceneEntity[]; + @state() private _searchParms = new URLSearchParams(window.location.search); + + @state() private _selected: string[] = []; + @state() private _activeFilters?: string[]; @state() private _filteredScenes?: string[] | null; @@ -317,6 +332,40 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { } protected render(): TemplateResult { + const categoryItems = html`${this._categories?.map( + (category) => + html` + ${category.icon + ? html`` + : html``} +
${category.name}
+
` + )} + +
+ ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.no_category" + )} +
+
`; + const labelItems = html` ${this._labels?.map((label) => { + const color = label.color ? computeCssColor(label.color) : undefined; + return html` + + ${label.icon + ? html`` + : nothing} + ${label.name} + + `; + })}`; + return html` filter.value?.length @@ -405,6 +457,103 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { @expanded-changed=${this._filterExpanded} > + ${!this.narrow + ? html` + + + + ${categoryItems} + + ${this.hass.dockedSidebar === "docked" + ? nothing + : html` + + + + ${labelItems} + `}` + : nothing} + ${this.narrow || this.hass.dockedSidebar === "docked" + ? html` + + ${ + this.narrow + ? html` + + ` + : html`` + } + + ${ + this.narrow + ? html` + +
+ ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.move_category" + )} +
+ +
+ ${categoryItems} +
` + : nothing + } + ${ + this.narrow || this.hass.dockedSidebar === "docked" + ? html` + +
+ ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.add_label" + )} +
+ +
+ ${labelItems} +
` + : nothing + } +
` + : nothing} ${!this.scenes.length ? html`
@@ -530,6 +679,33 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { this._applyFilters(); } + firstUpdated() { + if (this._searchParms.has("label")) { + this._filterLabel(); + } + } + + private _filterLabel() { + const label = this._searchParms.get("label"); + if (!label) { + return; + } + this._filters = { + ...this._filters, + "ha-filter-labels": { + value: [label], + items: undefined, + }, + }; + this._applyFilters(); + } + + private _handleSelectionChanged( + ev: HASSDomEvent + ): void { + this._selected = ev.detail.value; + } + private _handleRowClicked(ev: HASSDomEvent) { const scene = this.scenes.find((a) => a.entity_id === ev.detail.id); @@ -538,6 +714,32 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { } } + private async _handleBulkCategory(ev) { + const category = ev.currentTarget.value; + const promises: Promise[] = []; + 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[] = []; + 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) { const entityReg = this._entityReg.find( (reg) => reg.entity_id === scene.entity_id @@ -641,6 +843,16 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { --mdc-icon-size: 80px; 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; + } `, ]; } diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index fa9a4b920838..15441b45da11 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -1,9 +1,12 @@ import { consume } from "@lit-labs/context"; import { + mdiChevronRight, mdiContentDuplicate, mdiDelete, + mdiDotsVertical, mdiHelpCircle, mdiInformationOutline, + mdiMenuDown, mdiPlay, mdiPlus, mdiScriptText, @@ -34,6 +37,7 @@ import { LocalizeFunc } from "../../../common/translations/localize"; import { DataTableColumnContainer, RowClickedEvent, + SelectionChangedEvent, } from "../../../components/data-table/ha-data-table"; import "../../../components/data-table/ha-data-table-labels"; import "../../../components/ha-fab"; @@ -46,13 +50,19 @@ import "../../../components/ha-filter-labels"; import "../../../components/ha-icon-button"; import "../../../components/ha-icon-overflow-menu"; import "../../../components/ha-svg-icon"; +import "../../../components/ha-menu-item"; +import "../../../components/ha-sub-menu"; import { CategoryRegistryEntry, subscribeCategoryRegistry, } from "../../../data/category_registry"; import { fullEntitiesContext } from "../../../data/context"; import { UNAVAILABLE } from "../../../data/entity"; -import { EntityRegistryEntry } from "../../../data/entity_registry"; +import { + EntityRegistryEntry, + UpdateEntityRegistryEntryResult, + updateEntityRegistryEntry, +} from "../../../data/entity_registry"; import { LabelRegistryEntry, subscribeLabelRegistry, @@ -79,6 +89,7 @@ import { showToast } from "../../../util/toast"; import { showNewAutomationDialog } from "../automation/show-dialog-new-automation"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { configSections } from "../ha-panel-config"; +import { computeCssColor } from "../../../common/color/compute-color"; type ScriptItem = ScriptEntity & { name: string; @@ -102,6 +113,8 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { @state() private _searchParms = new URLSearchParams(window.location.search); + @state() private _selected: string[] = []; + @state() private _activeFilters?: string[]; @state() private _filteredScripts?: string[] | null; @@ -331,6 +344,40 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { } protected render(): TemplateResult { + const categoryItems = html`${this._categories?.map( + (category) => + html` + ${category.icon + ? html`` + : html``} +
${category.name}
+
` + )} + +
+ ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.no_category" + )} +
+
`; + const labelItems = html` ${this._labels?.map((label) => { + const color = label.color ? computeCssColor(label.color) : undefined; + return html` + + ${label.icon + ? html`` + : nothing} + ${label.name} + + `; + })}`; + return html` filter.value?.length ).length} @@ -432,6 +482,104 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { .narrow=${this.narrow} @expanded-changed=${this._filterExpanded} > + + ${!this.narrow + ? html` + + + + ${categoryItems} + + ${this.hass.dockedSidebar === "docked" + ? nothing + : html` + + + + ${labelItems} + `}` + : nothing} + ${this.narrow || this.hass.dockedSidebar === "docked" + ? html` + + ${ + this.narrow + ? html` + + ` + : html`` + } + + ${ + this.narrow + ? html` + +
+ ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.move_category" + )} +
+ +
+ ${categoryItems} +
` + : nothing + } + ${ + this.narrow || this.hass.dockedSidebar === "docked" + ? html` + +
+ ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.add_label" + )} +
+ +
+ ${labelItems} +
` + : nothing + } +
` + : nothing} ${!this.scripts.length ? html`
@@ -572,6 +720,24 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { if (this._searchParms.has("blueprint")) { this._filterBlueprint(); } + if (this._searchParms.has("label")) { + this._filterLabel(); + } + } + + private _filterLabel() { + const label = this._searchParms.get("label"); + if (!label) { + return; + } + this._filters = { + ...this._filters, + "ha-filter-labels": { + value: [label], + items: undefined, + }, + }; + this._applyFilters(); } private async _filterBlueprint() { @@ -611,6 +777,38 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { }); } + private _handleSelectionChanged( + ev: HASSDomEvent + ): void { + this._selected = ev.detail.value; + } + + private async _handleBulkCategory(ev) { + const category = ev.currentTarget.value; + const promises: Promise[] = []; + 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[] = []; + 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) { const entry = this.entityRegistry.find((e) => e.entity_id === ev.detail.id); if (entry) { @@ -764,6 +962,16 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { --mdc-icon-size: 80px; 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; + } `, ]; } diff --git a/src/state-control/alarm_control_panel/ha-state-control-alarm_control_panel-modes.ts b/src/state-control/alarm_control_panel/ha-state-control-alarm_control_panel-modes.ts index 10d5b465638a..585d14d9ca81 100644 --- a/src/state-control/alarm_control_panel/ha-state-control-alarm_control_panel-modes.ts +++ b/src/state-control/alarm_control_panel/ha-state-control-alarm_control_panel-modes.ts @@ -129,7 +129,7 @@ export class HaStateControlAlarmControlPanelModes extends LitElement { max-height: max(320px, var(--modes-count, 1) * 80px); min-height: max(200px, var(--modes-count, 1) * 80px); --control-select-thickness: 130px; - --control-select-border-radius: 48px; + --control-select-border-radius: 36px; --control-select-color: var(--primary-color); --control-select-background: var(--disabled-color); --control-select-background-opacity: 0.2; diff --git a/src/state-control/cover/ha-state-control-cover-position.ts b/src/state-control/cover/ha-state-control-cover-position.ts index 9f64c4b017f4..0f5652004e17 100644 --- a/src/state-control/cover/ha-state-control-cover-position.ts +++ b/src/state-control/cover/ha-state-control-cover-position.ts @@ -75,7 +75,7 @@ export class HaStateControlCoverPosition extends LitElement { max-height: 320px; min-height: 200px; --control-slider-thickness: 130px; - --control-slider-border-radius: 48px; + --control-slider-border-radius: 36px; --control-slider-color: var(--primary-color); --control-slider-background: var(--disabled-color); --control-slider-background-opacity: 0.2; diff --git a/src/state-control/cover/ha-state-control-cover-tilt-position.ts b/src/state-control/cover/ha-state-control-cover-tilt-position.ts index a3dccdf363a6..30d7160531d2 100644 --- a/src/state-control/cover/ha-state-control-cover-tilt-position.ts +++ b/src/state-control/cover/ha-state-control-cover-tilt-position.ts @@ -112,7 +112,7 @@ export class HaStateControlInfoCoverTiltPosition extends LitElement { max-height: 320px; min-height: 200px; --control-slider-thickness: 130px; - --control-slider-border-radius: 48px; + --control-slider-border-radius: 36px; --control-slider-color: var(--primary-color); --control-slider-background: var(--disabled-color); --control-slider-background-opacity: 0.2; diff --git a/src/state-control/cover/ha-state-control-cover-toggle.ts b/src/state-control/cover/ha-state-control-cover-toggle.ts index f743f774cd84..328a56352b55 100644 --- a/src/state-control/cover/ha-state-control-cover-toggle.ts +++ b/src/state-control/cover/ha-state-control-cover-toggle.ts @@ -142,7 +142,7 @@ export class HaStateControlCoverToggle extends LitElement { max-height: 320px; min-height: 200px; --control-switch-thickness: 130px; - --control-switch-border-radius: 48px; + --control-switch-border-radius: 36px; --control-switch-padding: 6px; --mdc-icon-size: 24px; } @@ -159,7 +159,7 @@ export class HaStateControlCoverToggle extends LitElement { ha-control-button { flex: 1; width: 100%; - --control-button-border-radius: 48px; + --control-button-border-radius: 36px; --mdc-icon-size: 24px; } ha-control-button.active { diff --git a/src/state-control/fan/ha-state-control-fan-speed.ts b/src/state-control/fan/ha-state-control-fan-speed.ts index f6bbb286b555..16e6d3a30ae1 100644 --- a/src/state-control/fan/ha-state-control-fan-speed.ts +++ b/src/state-control/fan/ha-state-control-fan-speed.ts @@ -142,7 +142,7 @@ export class HaStateControlFanSpeed extends LitElement { max-height: 320px; min-height: 200px; --control-slider-thickness: 130px; - --control-slider-border-radius: 48px; + --control-slider-border-radius: 36px; --control-slider-color: var(--primary-color); --control-slider-background: var(--disabled-color); --control-slider-background-opacity: 0.2; @@ -153,7 +153,7 @@ export class HaStateControlFanSpeed extends LitElement { max-height: 320px; min-height: 200px; --control-select-thickness: 130px; - --control-select-border-radius: 48px; + --control-select-border-radius: 36px; --control-select-color: var(--primary-color); --control-select-background: var(--disabled-color); --control-select-background-opacity: 0.2; diff --git a/src/state-control/ha-state-control-toggle.ts b/src/state-control/ha-state-control-toggle.ts index 8614f1d5055c..7d6c6971897d 100644 --- a/src/state-control/ha-state-control-toggle.ts +++ b/src/state-control/ha-state-control-toggle.ts @@ -133,7 +133,7 @@ export class HaStateControlToggle extends LitElement { max-height: 320px; min-height: 200px; --control-switch-thickness: 130px; - --control-switch-border-radius: 48px; + --control-switch-border-radius: 36px; --control-switch-padding: 6px; --mdc-icon-size: 24px; } @@ -150,7 +150,7 @@ export class HaStateControlToggle extends LitElement { ha-control-button { flex: 1; width: 100%; - --control-button-border-radius: 48px; + --control-button-border-radius: 36px; --mdc-icon-size: 24px; } ha-control-button.active { diff --git a/src/state-control/light/ha-state-control-light-brightness.ts b/src/state-control/light/ha-state-control-light-brightness.ts index 444dd1d7f04b..0205514cfa5e 100644 --- a/src/state-control/light/ha-state-control-light-brightness.ts +++ b/src/state-control/light/ha-state-control-light-brightness.ts @@ -89,7 +89,7 @@ export class HaStateControlLightBrightness extends LitElement { max-height: 320px; min-height: 200px; --control-slider-thickness: 130px; - --control-slider-border-radius: 48px; + --control-slider-border-radius: 36px; --control-slider-color: var(--primary-color); --control-slider-background: var(--disabled-color); --control-slider-background-opacity: 0.2; diff --git a/src/state-control/lock/ha-state-control-lock-toggle.ts b/src/state-control/lock/ha-state-control-lock-toggle.ts index d05eb393e2e6..bbc7c42ff0f0 100644 --- a/src/state-control/lock/ha-state-control-lock-toggle.ts +++ b/src/state-control/lock/ha-state-control-lock-toggle.ts @@ -167,7 +167,7 @@ export class HaStateControlLockToggle extends LitElement { max-height: 320px; min-height: 200px; --control-switch-thickness: 130px; - --control-switch-border-radius: 48px; + --control-switch-border-radius: 36px; --control-switch-padding: 6px; --mdc-icon-size: 24px; } @@ -187,7 +187,7 @@ export class HaStateControlLockToggle extends LitElement { ha-control-button { flex: 1; width: 100%; - --control-button-border-radius: 48px; + --control-button-border-radius: 36px; --mdc-icon-size: 24px; } ha-control-button.active { diff --git a/src/state-control/valve/ha-state-control-valve-position.ts b/src/state-control/valve/ha-state-control-valve-position.ts index af7a356f4191..a9b9714ca913 100644 --- a/src/state-control/valve/ha-state-control-valve-position.ts +++ b/src/state-control/valve/ha-state-control-valve-position.ts @@ -71,7 +71,7 @@ export class HaStateControlValvePosition extends LitElement { max-height: 320px; min-height: 200px; --control-slider-thickness: 130px; - --control-slider-border-radius: 48px; + --control-slider-border-radius: 36px; --control-slider-color: var(--primary-color); --control-slider-background: var(--disabled-color); --control-slider-background-opacity: 0.2; diff --git a/src/state-control/valve/ha-state-control-valve-toggle.ts b/src/state-control/valve/ha-state-control-valve-toggle.ts index cc74e52170d0..907a86e6f42c 100644 --- a/src/state-control/valve/ha-state-control-valve-toggle.ts +++ b/src/state-control/valve/ha-state-control-valve-toggle.ts @@ -142,7 +142,7 @@ export class HaStateControlValveToggle extends LitElement { max-height: 320px; min-height: 200px; --control-switch-thickness: 130px; - --control-switch-border-radius: 48px; + --control-switch-border-radius: 36px; --control-switch-padding: 6px; --mdc-icon-size: 24px; } @@ -159,7 +159,7 @@ export class HaStateControlValveToggle extends LitElement { ha-control-button { flex: 1; width: 100%; - --control-button-border-radius: 48px; + --control-button-border-radius: 36px; --mdc-icon-size: 24px; } ha-control-button.active { diff --git a/src/translations/en.json b/src/translations/en.json index 9a68c368b7b1..2f0ed5d46c36 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -571,6 +571,7 @@ "add_new_sugestion": "Add new category ''{name}''", "add_new": "Add new category…", "no_categories": "You don't have any categories", + "no_match": "No matching categories found", "add_dialog": { "title": "Add new category", "text": "Enter the name of the new category.", @@ -599,13 +600,7 @@ "no_areas": "You don't have any areas", "no_match": "No matching areas found", "unassigned_areas": "Unassigned areas", - "add_dialog": { - "title": "Add new area", - "text": "Enter the name of the new area.", - "name": "Name", - "add": "Add", - "failed_create_area": "Failed to create area." - } + "failed_create_area": "Failed to create area." }, "floor-picker": { "clear": "Clear", @@ -615,13 +610,7 @@ "add_new": "Add new floor…", "no_floors": "You don't have any floors", "no_match": "No matching floors found", - "add_dialog": { - "title": "Add new floor", - "text": "Enter the name of the new floor.", - "name": "Name", - "add": "Add", - "failed_create_floor": "Failed to create floor." - } + "failed_create_floor": "Failed to create floor." }, "area-filter": { "title": "Areas", @@ -1125,6 +1114,7 @@ "edit": "Edit entity", "details": "Details", "back_to_info": "Back to info", + "info": "Information", "related": "Related", "history": "History", "logbook": "Logbook", @@ -4062,6 +4052,9 @@ "button": "Hide selected", "confirm_title": "Do you want to hide {number} {number, plural,\n one {entity}\n other {entities}\n}?", "confirm_text": "Hidden entities will not be shown on your dashboard. Their history is still tracked and you can still interact with them with services." + }, + "unhide_selected": { + "button": "Unhide selected" } } },