From 4cb45d63132af6380b54a6856b8dcbb7054c7aed Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 10 Nov 2021 21:42:43 +0100 Subject: [PATCH] Add picture uploader to area (#10544) --- src/components/ha-area-picker.ts | 16 +- src/data/area_registry.ts | 2 + src/data/entity_registry.ts | 2 +- .../show-image-cropper-dialog.ts | 2 +- .../areas/dialog-area-registry-detail.ts | 41 +++- .../config/areas/ha-config-area-page.ts | 101 ++++++++-- .../config/areas/ha-config-areas-dashboard.ts | 189 +++++++++++------- .../config/entities/ha-config-entities.ts | 4 +- src/translations/en.json | 1 + 9 files changed, 254 insertions(+), 104 deletions(-) diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index b79abb66a4..4a22d3775e 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -340,7 +340,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { item-value-path="area_id" item-id-path="area_id" item-label-path="name" - .value=${this._value} + .value=${this.value} .disabled=${this.disabled} ${comboBoxRenderer(rowRenderer)} @opened-changed=${this._openedChanged} @@ -431,12 +431,24 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { name, }); this._areas = [...this._areas!, area]; + (this.comboBox as any).items = this._getAreas( + this._areas!, + this._devices!, + this._entities!, + this.includeDomains, + this.excludeDomains, + this.includeDeviceClasses, + this.deviceFilter, + this.entityFilter, + this.noAdd + ); this._setValue(area.area_id); } catch (err: any) { showAlertDialog(this, { - text: this.hass.localize( + title: this.hass.localize( "ui.components.area-picker.add_dialog.failed_create_area" ), + text: err.message, }); } }, diff --git a/src/data/area_registry.ts b/src/data/area_registry.ts index bb820261a9..3e432ce626 100644 --- a/src/data/area_registry.ts +++ b/src/data/area_registry.ts @@ -7,10 +7,12 @@ import { HomeAssistant } from "../types"; export interface AreaRegistryEntry { area_id: string; name: string; + picture?: string; } export interface AreaRegistryEntryMutableParams { name: string; + picture?: string | null; } export const createAreaRegistryEntry = ( diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 7072c44227..becf455fc0 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -66,7 +66,7 @@ export const computeEntityRegistryName = ( return entry.name; } const state = hass.states[entry.entity_id]; - return state ? computeStateName(state) : null; + return state ? computeStateName(state) : entry.entity_id; }; export const getExtendedEntityRegistryEntry = ( diff --git a/src/dialogs/image-cropper-dialog/show-image-cropper-dialog.ts b/src/dialogs/image-cropper-dialog/show-image-cropper-dialog.ts index 6ce2090070..4f7ff03d14 100644 --- a/src/dialogs/image-cropper-dialog/show-image-cropper-dialog.ts +++ b/src/dialogs/image-cropper-dialog/show-image-cropper-dialog.ts @@ -4,7 +4,7 @@ export interface CropOptions { round: boolean; type?: "image/jpeg" | "image/png"; quality?: number; - aspectRatio: number; + aspectRatio?: number; } export interface HaImageCropperDialogParams { diff --git a/src/panels/config/areas/dialog-area-registry-detail.ts b/src/panels/config/areas/dialog-area-registry-detail.ts index f2fc039470..9bbb35de18 100644 --- a/src/panels/config/areas/dialog-area-registry-detail.ts +++ b/src/panels/config/areas/dialog-area-registry-detail.ts @@ -3,19 +3,31 @@ import "@polymer/paper-input/paper-input"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { property, state } from "lit/decorators"; import { fireEvent } from "../../../common/dom/fire_event"; -import { navigate } from "../../../common/navigate"; import { createCloseHeading } from "../../../components/ha-dialog"; +import "../../../components/ha-alert"; +import "../../../components/ha-picture-upload"; +import type { HaPictureUpload } from "../../../components/ha-picture-upload"; import { AreaRegistryEntryMutableParams } from "../../../data/area_registry"; +import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog"; import { PolymerChangedEvent } from "../../../polymer-types"; import { haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry-detail"; +const cropOptions: CropOptions = { + round: false, + type: "image/jpeg", + quality: 0.75, + aspectRatio: 1.78, +}; + class DialogAreaDetail extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _name!: string; + @state() private _picture!: string | null; + @state() private _error?: string; @state() private _params?: AreaRegistryDetailDialogParams; @@ -28,6 +40,7 @@ class DialogAreaDetail extends LitElement { this._params = params; this._error = undefined; this._name = this._params.entry ? this._params.entry.name : ""; + this._picture = this._params.entry?.picture || null; await this.updateComplete; } @@ -55,7 +68,9 @@ class DialogAreaDetail extends LitElement { )} >
- ${this._error ? html`
${this._error}
` : ""} + ${this._error + ? html` ${this._error} ` + : ""}
${entry ? html` @@ -78,6 +93,13 @@ class DialogAreaDetail extends LitElement { )} .invalid=${nameInvalid} > +
${entry @@ -120,18 +142,24 @@ class DialogAreaDetail extends LitElement { this._name = ev.detail.value; } + private _pictureChanged(ev: PolymerChangedEvent) { + this._error = undefined; + this._picture = (ev.target as HaPictureUpload).value; + } + private async _updateEntry() { this._submitting = true; try { const values: AreaRegistryEntryMutableParams = { name: this._name.trim(), + picture: this._picture, }; if (this._params!.entry) { await this._params!.updateEntry!(values); } else { await this._params!.createEntry!(values); } - this._params = undefined; + this.closeDialog(); } catch (err: any) { this._error = err.message || @@ -145,13 +173,11 @@ class DialogAreaDetail extends LitElement { this._submitting = true; try { if (await this._params!.removeEntry!()) { - this._params = undefined; + this.closeDialog(); } } finally { this._submitting = false; } - - navigate("/config/areas/dashboard"); } static get styles(): CSSResultGroup { @@ -161,9 +187,6 @@ class DialogAreaDetail extends LitElement { .form { padding-bottom: 24px; } - .error { - color: var(--error-color); - } `, ]; } diff --git a/src/panels/config/areas/ha-config-area-page.ts b/src/panels/config/areas/ha-config-area-page.ts index ece1334bce..2de91d403b 100644 --- a/src/panels/config/areas/ha-config-area-page.ts +++ b/src/panels/config/areas/ha-config-area-page.ts @@ -1,13 +1,17 @@ import "@material/mwc-button"; -import { mdiCog } from "@mdi/js"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-item/paper-item-body"; +import { mdiImagePlus, mdiPencil } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { computeStateName } from "../../../common/entity/compute_state_name"; +import { afterNextRender } from "../../../common/util/render-status"; import "../../../components/ha-card"; import "../../../components/ha-icon-button"; +import "../../../components/ha-icon-next"; import { AreaRegistryEntry, deleteAreaRegistryEntry, @@ -134,25 +138,59 @@ class HaConfigAreaPage extends LitElement { .tabs=${configSections.integrations} .route=${this.route} > - ${this.narrow ? html` ${area.name} ` : ""} - - + ${this.narrow + ? html` ${area.name} + ` + : ""}
${!this.narrow ? html`
-

${area.name}

+

+ ${area.name} + +

` : ""}
+ ${area.picture + ? html`
+ +
` + : html` + + `} ${devices.length @@ -181,7 +219,8 @@ class HaConfigAreaPage extends LitElement { .header=${this.hass.localize( "ui.panel.config.areas.editor.linked_entities_caption" )} - >${entities.length + > + ${entities.length ? entities.map( (entity) => html` @@ -390,6 +429,7 @@ class HaConfigAreaPage extends LitElement { try { await deleteAreaRegistryEntry(this.hass!, entry!.area_id); + afterNextRender(() => history.back()); return true; } catch (err: any) { return false; @@ -403,7 +443,7 @@ class HaConfigAreaPage extends LitElement { haStyle, css` h1 { - margin-top: 0; + margin: 0; font-family: var(--paper-font-headline_-_font-family); -webkit-font-smoothing: var( --paper-font-headline_-_-webkit-font-smoothing @@ -413,6 +453,13 @@ class HaConfigAreaPage extends LitElement { letter-spacing: var(--paper-font-headline_-_letter-spacing); line-height: var(--paper-font-headline_-_line-height); opacity: var(--dark-primary-opacity); + display: flex; + align-items: center; + } + + img { + border-radius: var(--ha-card-border-radius, 4px); + width: 100%; } .container { @@ -458,6 +505,34 @@ class HaConfigAreaPage extends LitElement { paper-item.no-link { cursor: default; } + + ha-card > a:first-child { + display: block; + } + ha-card > *:first-child { + margin-top: -16px; + } + .img-container { + position: relative; + } + .img-edit-btn { + position: absolute; + top: 4px; + right: 4px; + display: none; + } + .img-container:hover .img-edit-btn { + display: block; + } + .img-edit-btn::before { + content: ""; + position: absolute; + width: 100%; + height: 100%; + background-color: var(--card-background-color); + opacity: 0.5; + border-radius: 50%; + } `, ]; } diff --git a/src/panels/config/areas/ha-config-areas-dashboard.ts b/src/panels/config/areas/ha-config-areas-dashboard.ts index a2d27e63b7..d694234ea6 100644 --- a/src/panels/config/areas/ha-config-areas-dashboard.ts +++ b/src/panels/config/areas/ha-config-areas-dashboard.ts @@ -1,15 +1,8 @@ import { mdiHelpCircle, mdiPlus } from "@mdi/js"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; -import { HASSDomEvent } from "../../../common/dom/fire_event"; -import { navigate } from "../../../common/navigate"; -import { - DataTableColumnContainer, - RowClickedEvent, -} from "../../../components/data-table/ha-data-table"; import "../../../components/ha-fab"; import "../../../components/ha-icon-button"; import "../../../components/ha-svg-icon"; @@ -21,7 +14,7 @@ import type { DeviceRegistryEntry } from "../../../data/device_registry"; import type { EntityRegistryEntry } from "../../../data/entity_registry"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-loading-screen"; -import "../../../layouts/hass-tabs-subpage-data-table"; +import "../../../layouts/hass-tabs-subpage"; import { HomeAssistant, Route } from "../../../types"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; @@ -53,97 +46,51 @@ export class HaConfigAreasDashboard extends LitElement { entities: EntityRegistryEntry[] ) => areas.map((area) => { + let noDevicesInArea = 0; + let noServicesInArea = 0; + let noEntitiesInArea = 0; + const devicesInArea = new Set(); for (const device of devices) { if (device.area_id === area.area_id) { devicesInArea.add(device.id); + if (device.entry_type === "service") { + noServicesInArea++; + } else { + noDevicesInArea++; + } } } - let entitiesInArea = 0; - for (const entity of entities) { if ( entity.area_id ? entity.area_id === area.area_id : devicesInArea.has(entity.device_id) ) { - entitiesInArea++; + noEntitiesInArea++; } } return { ...area, - devices: devicesInArea.size, - entities: entitiesInArea, + devices: noDevicesInArea, + services: noServicesInArea, + entities: noEntitiesInArea, }; }) ); - private _columns = memoizeOne( - (narrow: boolean): DataTableColumnContainer => - narrow - ? { - name: { - title: this.hass.localize( - "ui.panel.config.areas.data_table.area" - ), - sortable: true, - filterable: true, - grows: true, - direction: "asc", - }, - } - : { - name: { - title: this.hass.localize( - "ui.panel.config.areas.data_table.area" - ), - sortable: true, - filterable: true, - grows: true, - direction: "asc", - }, - devices: { - title: this.hass.localize( - "ui.panel.config.areas.data_table.devices" - ), - sortable: true, - type: "numeric", - width: "20%", - direction: "asc", - }, - entities: { - title: this.hass.localize( - "ui.panel.config.areas.data_table.entities" - ), - sortable: true, - type: "numeric", - width: "20%", - direction: "asc", - }, - } - ); - protected render(): TemplateResult { return html` - + - + `; } @@ -191,11 +190,6 @@ export class HaConfigAreasDashboard extends LitElement { }); } - private _handleRowClicked(ev: HASSDomEvent) { - const areaId = ev.detail.id; - navigate(`/config/areas/area/${areaId}`); - } - private _openDialog(entry?: AreaRegistryEntry) { showAreaRegistryDetailDialog(this, { entry, @@ -210,6 +204,51 @@ export class HaConfigAreasDashboard extends LitElement { --app-header-background-color: var(--sidebar-background-color); --app-header-text-color: var(--sidebar-text-color); } + .container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + grid-gap: 16px 16px; + padding: 8px 16px 16px; + margin: 0 auto 64px auto; + max-width: 1000px; + } + .container > * { + max-width: 500px; + } + ha-card { + overflow: hidden; + } + a { + text-decoration: none; + } + h1 { + padding-bottom: 0; + } + .picture { + height: 150px; + width: 100%; + background-size: cover; + background-position: center; + position: relative; + } + .picture.placeholder::before { + position: absolute; + content: ""; + width: 100%; + height: 100%; + background-color: var(--sidebar-selected-icon-color); + opacity: 0.12; + } + .card-content { + min-height: 16px; + color: var(--secondary-text-color); + } `; } } + +declare global { + interface HTMLElementTagNameMap { + "ha-config-areas-dashboard": HaConfigAreasDashboard; + } +} diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 055b770147..6f9569e6cf 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -376,9 +376,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { result.push({ ...entry, entity, - name: - computeEntityRegistryName(this.hass!, entry) || - this.hass.localize("state.default.unavailable"), + name: computeEntityRegistryName(this.hass!, entry), unavailable, restored, area: area ? area.name : undefined, diff --git a/src/translations/en.json b/src/translations/en.json index c5df32dc8a..9a565b0875 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -930,6 +930,7 @@ "caption": "Areas", "description": "Group devices and entities into areas", "edit_settings": "Area settings", + "add_picture": "Add a picture", "data_table": { "area": "Area", "devices": "Devices",