+
${this.hass!.localize(
+ "ui.panel.lovelace.editor.edit_card.edit"
+ )}
+
-
+
-
+
${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.move"
)}
- ${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.duplicate"
)}
-
+
${this.hass!.localize(
"ui.panel.lovelace.editor.edit_card.delete"
)}) {
+ switch (ev.detail.index) {
+ case 0:
+ this._moveCard();
+ break;
+ case 1:
+ this._duplicateCard();
+ break;
+ case 2:
+ this._deleteCard();
+ break;
+ }
+ }
+
private _duplicateCard(): void {
const path = this.path!;
const cardConfig = this.lovelace!.config.views[path[0]].cards![path[1]];
@@ -183,9 +188,39 @@ export class HuiCardOptions extends LitElement {
}
private _moveCard(): void {
- showMoveCardViewDialog(this, {
- path: this.path!,
- lovelace: this.lovelace!,
+ showSelectViewDialog(this, {
+ lovelaceConfig: this.lovelace!.config,
+ urlPath: this.lovelace!.urlPath,
+ allowDashboardChange: true,
+ header: this.hass!.localize("ui.panel.lovelace.editor.move_card.header"),
+ viewSelectedCallback: async (urlPath, selectedDashConfig, viewIndex) => {
+ if (urlPath === this.lovelace!.urlPath) {
+ this.lovelace!.saveConfig(
+ moveCard(this.lovelace!.config, this.path!, [viewIndex])
+ );
+ showSaveSuccessToast(this, this.hass!);
+ return;
+ }
+ try {
+ await saveConfig(
+ this.hass!,
+ urlPath,
+ addCard(
+ selectedDashConfig,
+ [viewIndex],
+ this.lovelace!.config.views[this.path![0]].cards![this.path![1]]
+ )
+ );
+ this.lovelace!.saveConfig(
+ deleteCard(this.lovelace!.config, this.path!)
+ );
+ showSaveSuccessToast(this, this.hass!);
+ } catch (err) {
+ showAlertDialog(this, {
+ text: `Moving failed: ${err.message}`,
+ });
+ }
+ },
});
}
diff --git a/src/panels/lovelace/components/hui-entity-editor.ts b/src/panels/lovelace/components/hui-entity-editor.ts
index d68b8fb9f2..23ab6eb6f1 100644
--- a/src/panels/lovelace/components/hui-entity-editor.ts
+++ b/src/panels/lovelace/components/hui-entity-editor.ts
@@ -125,9 +125,6 @@ export class HuiEntityEditor extends LitElement {
static get styles(): CSSResult {
return css`
- .entities {
- padding-left: 20px;
- }
.entity {
display: flex;
align-items: flex-end;
diff --git a/src/panels/lovelace/components/hui-views-list.ts b/src/panels/lovelace/components/hui-views-list.ts
index 6fa320eb41..71e51cccde 100644
--- a/src/panels/lovelace/components/hui-views-list.ts
+++ b/src/panels/lovelace/components/hui-views-list.ts
@@ -43,7 +43,7 @@ class HuiViewsList extends LitElement {
`
: ""}
- ${view.title || view.path}
+ ${view.title || view.path || "Unnamed view"}
`
)}
diff --git a/src/panels/lovelace/editor/add-entities-to-view.ts b/src/panels/lovelace/editor/add-entities-to-view.ts
index 686b544bf5..318df70429 100644
--- a/src/panels/lovelace/editor/add-entities-to-view.ts
+++ b/src/panels/lovelace/editor/add-entities-to-view.ts
@@ -2,69 +2,128 @@ import {
fetchConfig,
LovelaceConfig,
saveConfig,
+ fetchDashboards,
+ LovelacePanelConfig,
} from "../../../data/lovelace";
import { HomeAssistant } from "../../../types";
import { showSuggestCardDialog } from "./card-editor/show-suggest-card-dialog";
import { showSelectViewDialog } from "./select-view/show-select-view-dialog";
+import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
export const addEntitiesToLovelaceView = async (
element: HTMLElement,
hass: HomeAssistant,
- entities: string[],
- lovelaceConfig?: LovelaceConfig,
- saveConfigFunc?: (newConfig: LovelaceConfig) => void
+ entities: string[]
) => {
- if ((hass!.panels.lovelace?.config as any)?.mode === "yaml") {
+ const dashboards = await fetchDashboards(hass);
+
+ const storageDashs = dashboards.filter(
+ (dashboard) => dashboard.mode === "storage"
+ );
+
+ const mainLovelaceMode = (hass!.panels.lovelace
+ ?.config as LovelacePanelConfig)?.mode;
+
+ if (mainLovelaceMode !== "storage" && !storageDashs.length) {
+ // no storage dashboards, just show the YAML config
showSuggestCardDialog(element, {
entities,
+ yaml: true,
});
return;
}
- if (!lovelaceConfig) {
+
+ let lovelaceConfig;
+ let urlPath: string | null = null;
+ if (mainLovelaceMode === "storage") {
try {
lovelaceConfig = await fetchConfig(hass.connection, null, false);
- } catch {
- alert(
- hass.localize(
- "ui.panel.lovelace.editor.add_entities.generated_unsupported"
- )
- );
- return;
+ } catch (e) {
+ // default dashboard is in generated mode
}
}
- if (!lovelaceConfig.views.length) {
- alert(
- "You don't have any Lovelace views, first create a view in Lovelace."
- );
+
+ if (!lovelaceConfig && storageDashs.length) {
+ // find first dashoard not in generated mode
+ for (const storageDash of storageDashs) {
+ try {
+ // eslint-disable-next-line no-await-in-loop
+ lovelaceConfig = await fetchConfig(
+ hass.connection,
+ storageDash.url_path,
+ false
+ );
+ urlPath = storageDash.url_path;
+ break;
+ } catch (e) {
+ // dashboard is in generated mode
+ }
+ }
+ }
+
+ if (!lovelaceConfig) {
+ if (dashboards.length > storageDashs.length) {
+ // all storage dashboards are generated, but we have YAML dashboards just show the YAML config
+ showSuggestCardDialog(element, {
+ entities,
+ yaml: true,
+ });
+ } else {
+ // all storage dashboards are generated
+ showAlertDialog(element, {
+ text:
+ "You don't seem to be in control of any dashboard, please take control first.",
+ });
+ }
return;
}
- if (!saveConfigFunc) {
- saveConfigFunc = async (newConfig: LovelaceConfig): Promise => {
- try {
- await saveConfig(hass!, null, newConfig);
- } catch {
- alert(
- hass.localize("ui.panel.config.devices.add_entities.saving_failed")
- );
- }
- };
+
+ if (!storageDashs.length && !lovelaceConfig.views?.length) {
+ showAlertDialog(element, {
+ text:
+ "You don't have any Lovelace views, first create a view in Lovelace.",
+ });
+ return;
}
- if (lovelaceConfig.views.length === 1) {
+
+ if (!storageDashs.length && lovelaceConfig.views.length === 1) {
showSuggestCardDialog(element, {
lovelaceConfig: lovelaceConfig!,
- saveConfig: saveConfigFunc,
+ saveConfig: async (newConfig: LovelaceConfig): Promise => {
+ try {
+ await saveConfig(hass!, null, newConfig);
+ } catch (e) {
+ alert(
+ hass.localize("ui.panel.config.devices.add_entities.saving_failed")
+ );
+ }
+ },
path: [0],
entities,
});
return;
}
+
showSelectViewDialog(element, {
lovelaceConfig,
- viewSelectedCallback: (view) => {
+ urlPath,
+ allowDashboardChange: true,
+ dashboards,
+ viewSelectedCallback: (newUrlPath, selectedDashConfig, viewIndex) => {
showSuggestCardDialog(element, {
- lovelaceConfig: lovelaceConfig!,
- saveConfig: saveConfigFunc,
- path: [view],
+ lovelaceConfig: selectedDashConfig,
+ saveConfig: async (newConfig: LovelaceConfig): Promise => {
+ try {
+ await saveConfig(hass!, newUrlPath, newConfig);
+ } catch {
+ alert(
+ hass.localize(
+ "ui.panel.config.devices.add_entities.saving_failed"
+ )
+ );
+ }
+ },
+ path: [viewIndex],
entities,
});
},
diff --git a/src/panels/lovelace/editor/card-editor/hui-card-editor.ts b/src/panels/lovelace/editor/card-editor/hui-card-editor.ts
index d5cf049076..f081cc65ae 100644
--- a/src/panels/lovelace/editor/card-editor/hui-card-editor.ts
+++ b/src/panels/lovelace/editor/card-editor/hui-card-editor.ts
@@ -26,6 +26,8 @@ import type { LovelaceCardEditor } from "../../types";
import type { GUIModeChangedEvent } from "../types";
import "../../../../components/ha-circular-progress";
import { deepEqual } from "../../../../common/util/deep-equal";
+import { handleStructError } from "../../common/structs/handle-errors";
+import { GUISupportError } from "../gui-support-error";
export interface ConfigChangedEvent {
config: LovelaceCardConfig;
@@ -69,7 +71,7 @@ export class HuiCardEditor extends LitElement {
@internalProperty() private _error?: string;
// Warning: GUI editor can't handle configuration - ok to save
- @internalProperty() private _warning?: string;
+ @internalProperty() private _warnings?: string[];
@internalProperty() private _loading = false;
@@ -121,7 +123,7 @@ export class HuiCardEditor extends LitElement {
}
public get hasWarning(): boolean {
- return this._warning !== undefined;
+ return this._warnings !== undefined;
}
public get hasError(): boolean {
@@ -194,10 +196,15 @@ export class HuiCardEditor extends LitElement {
`
: ""}
- ${this._warning
+ ${this._warnings
? html`
- ${this._warning}
+ UI editor is not supported for this config:
+
+
+ ${this._warnings.map((warning) => html`- ${warning}
`)}
+
+ You can still edit your config in yaml.
`
: ""}
@@ -238,7 +245,7 @@ export class HuiCardEditor extends LitElement {
let configElement = this._configElement;
try {
this._error = undefined;
- this._warning = undefined;
+ this._warnings = undefined;
if (this._configElType !== cardType) {
// If the card type has changed, we need to load a new GUI editor
@@ -254,7 +261,9 @@ export class HuiCardEditor extends LitElement {
configElement = await elClass.getConfigElement();
} else {
configElement = undefined;
- throw Error(`WARNING: No visual editor available for: ${cardType}`);
+ throw new GUISupportError(
+ `No visual editor available for: ${cardType}`
+ );
}
this._configElement = configElement;
@@ -272,11 +281,14 @@ export class HuiCardEditor extends LitElement {
try {
this._configElement!.setConfig(this.value);
} catch (err) {
- throw Error(`WARNING: ${err.message}`);
+ throw new GUISupportError(
+ "Config is not supported",
+ handleStructError(err)
+ );
}
} catch (err) {
- if (err.message.startsWith("WARNING:")) {
- this._warning = err.message.substr(8);
+ if (err instanceof GUISupportError) {
+ this._warnings = err.warnings ?? [err.message];
} else {
this._error = err;
}
@@ -302,12 +314,19 @@ export class HuiCardEditor extends LitElement {
.yaml-editor {
padding: 8px 0px;
}
+ .error,
+ .warning {
+ word-break: break-word;
+ }
.error {
color: var(--error-color);
}
.warning {
color: var(--warning-color);
}
+ .warning ul {
+ margin: 4px 0;
+ }
ha-circular-progress {
display: block;
margin: auto;
diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts
index feac4d8ddc..0186f24508 100755
--- a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts
+++ b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts
@@ -11,7 +11,7 @@ import {
TemplateResult,
PropertyValues,
} from "lit-element";
-import type { HASSDomEvent } from "../../../../common/dom/fire_event";
+import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-dialog";
import type {
LovelaceCardConfig,
@@ -30,6 +30,9 @@ import "./hui-card-preview";
import type { EditCardDialogParams } from "./show-edit-card-dialog";
import { getCardDocumentationURL } from "../get-card-documentation-url";
import { mdiHelpCircle } from "@mdi/js";
+import { computeRTLDirection } from "../../../../common/util/compute_rtl";
+import { HassDialog } from "../../../../dialogs/make-dialog-manager";
+import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
declare global {
// for fire event
@@ -43,7 +46,7 @@ declare global {
}
@customElement("hui-dialog-edit-card")
-export class HuiDialogEditCard extends LitElement {
+export class HuiDialogEditCard extends LitElement implements HassDialog {
@property() protected hass!: HomeAssistant;
@internalProperty() private _params?: EditCardDialogParams;
@@ -64,6 +67,8 @@ export class HuiDialogEditCard extends LitElement {
@internalProperty() private _documentationURL?: string;
+ @internalProperty() private _dirty = false;
+
public async showDialog(params: EditCardDialogParams): Promise
{
this._params = params;
this._GUImode = true;
@@ -77,6 +82,20 @@ export class HuiDialogEditCard extends LitElement {
}
}
+ public closeDialog(): boolean {
+ if (this._dirty) {
+ this._confirmCancel();
+ return false;
+ }
+ this._params = undefined;
+ this._cardConfig = undefined;
+ this._error = undefined;
+ this._documentationURL = undefined;
+ this._dirty = false;
+ fireEvent(this, "dialog-closed", { dialog: this.localName });
+ return true;
+ }
+
protected updated(changedProps: PropertyValues): void {
if (
!this._cardConfig ||
@@ -100,9 +119,13 @@ export class HuiDialogEditCard extends LitElement {
let heading: string;
if (this._cardConfig && this._cardConfig.type) {
- heading = `${this.hass!.localize(
- `ui.panel.lovelace.editor.card.${this._cardConfig.type}.name`
- )} ${this.hass!.localize("ui.panel.lovelace.editor.edit_card.header")}`;
+ heading = this.hass!.localize(
+ "ui.panel.lovelace.editor.edit_card.typed_header",
+ "type",
+ this.hass!.localize(
+ `ui.panel.lovelace.editor.card.${this._cardConfig.type}.name`
+ )
+ );
} else if (!this._cardConfig) {
heading = this._viewConfig.title
? this.hass!.localize(
@@ -122,7 +145,7 @@ export class HuiDialogEditCard extends LitElement {
open
scrimClickAction
@keydown=${this._ignoreKeydown}
- @closed=${this._close}
+ @closed=${this._cancel}
@opened=${this._opened}
.heading=${html`${heading}
${this._documentationURL !== undefined
@@ -133,6 +156,7 @@ export class HuiDialogEditCard extends LitElement {
title=${this.hass!.localize("ui.panel.lovelace.menu.help")}
target="_blank"
rel="noreferrer"
+ dir=${computeRTLDirection(this.hass)}
>
@@ -197,7 +221,7 @@ export class HuiDialogEditCard extends LitElement {
`
: ""}
-
+
${this.hass!.localize("ui.common.cancel")}
${this._cardConfig !== undefined
@@ -214,7 +238,9 @@ export class HuiDialogEditCard extends LitElement {
size="small"
>
`
- : this.hass!.localize("ui.common.save")}
+ : this._dirty
+ ? this.hass!.localize("ui.common.save")
+ : this.hass!.localize("ui.common.close")}
`
: ``}
@@ -344,12 +370,14 @@ export class HuiDialogEditCard extends LitElement {
}
this._cardConfig = deepFreeze(config);
this._error = ev.detail.error;
+ this._dirty = true;
}
private _handleConfigChanged(ev: HASSDomEvent) {
this._cardConfig = deepFreeze(ev.detail.config);
this._error = ev.detail.error;
this._guiModeAvailable = ev.detail.guiModeAvailable;
+ this._dirty = true;
}
private _handleGUIModeChanged(ev: HASSDomEvent): void {
@@ -366,13 +394,6 @@ export class HuiDialogEditCard extends LitElement {
this._cardEditorEl?.refreshYamlEditor();
}
- private _close(): void {
- this._params = undefined;
- this._cardConfig = undefined;
- this._error = undefined;
- this._documentationURL = undefined;
- }
-
private get _canSave(): boolean {
if (this._saving) {
return false;
@@ -386,8 +407,38 @@ export class HuiDialogEditCard extends LitElement {
return true;
}
+ private async _confirmCancel() {
+ // Make sure the open state of this dialog is handled before the open state of confirm dialog
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ const confirm = await showConfirmationDialog(this, {
+ title: this.hass!.localize(
+ "ui.panel.lovelace.editor.edit_card.unsaved_changes"
+ ),
+ text: this.hass!.localize(
+ "ui.panel.lovelace.editor.edit_card.confirm_cancel"
+ ),
+ dismissText: this.hass!.localize("ui.common.no"),
+ confirmText: this.hass!.localize("ui.common.yes"),
+ });
+ if (confirm) {
+ this._cancel();
+ }
+ }
+
+ private _cancel(ev?: Event) {
+ if (ev) {
+ ev.stopPropagation();
+ }
+ this._dirty = false;
+ this.closeDialog();
+ }
+
private async _save(): Promise {
- if (!this._canSave || this._saving) {
+ if (!this._canSave) {
+ return;
+ }
+ if (!this._dirty) {
+ this.closeDialog();
return;
}
this._saving = true;
@@ -405,8 +456,9 @@ export class HuiDialogEditCard extends LitElement {
)
);
this._saving = false;
+ this._dirty = false;
showSaveSuccessToast(this, this.hass);
- this._close();
+ this.closeDialog();
}
}
diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-move-card-view.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-move-card-view.ts
deleted file mode 100644
index c0e733c531..0000000000
--- a/src/panels/lovelace/editor/card-editor/hui-dialog-move-card-view.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import "@polymer/paper-item/paper-item";
-import {
- css,
- CSSResult,
- customElement,
- html,
- LitElement,
- internalProperty,
- TemplateResult,
-} from "lit-element";
-import "../../../../components/dialog/ha-paper-dialog";
-import type { HaPaperDialog } from "../../../../components/dialog/ha-paper-dialog";
-import type { PolymerChangedEvent } from "../../../../polymer-types";
-import "../../components/hui-views-list";
-import { moveCard } from "../config-util";
-import type { MoveCardViewDialogParams } from "./show-move-card-view-dialog";
-
-@customElement("hui-dialog-move-card-view")
-export class HuiDialogMoveCardView extends LitElement {
- @internalProperty() private _params?: MoveCardViewDialogParams;
-
- public async showDialog(params: MoveCardViewDialogParams): Promise {
- this._params = params;
- await this.updateComplete;
- }
-
- protected render(): TemplateResult {
- if (!this._params) {
- return html``;
- }
- return html`
-
- Choose view to move card
-
-
-
- `;
- }
-
- static get styles(): CSSResult {
- return css`
- paper-item {
- margin: 8px;
- cursor: pointer;
- }
- paper-item[active] {
- color: var(--primary-color);
- }
- paper-item[active]:before {
- border-radius: 4px;
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- pointer-events: none;
- content: "";
- background-color: var(--primary-color);
- opacity: 0.12;
- transition: opacity 15ms linear;
- will-change: opacity;
- }
- `;
- }
-
- private get _dialog(): HaPaperDialog {
- return this.shadowRoot!.querySelector("ha-paper-dialog")!;
- }
-
- private _moveCard(e: CustomEvent): void {
- const newView = e.detail.view;
- const path = this._params!.path!;
- if (newView === path[0]) {
- return;
- }
-
- const lovelace = this._params!.lovelace!;
- lovelace.saveConfig(moveCard(lovelace.config, path, [newView!]));
- this._dialog.close();
- }
-
- private _openedChanged(ev: PolymerChangedEvent): void {
- if (!(ev.detail as any).value) {
- this._params = undefined;
- }
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- "hui-dialog-move-card-view": HuiDialogMoveCardView;
- }
-}
diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts
index 1a3075b86a..a4f222200c 100755
--- a/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts
+++ b/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts
@@ -12,7 +12,6 @@ import {
TemplateResult,
} from "lit-element";
import "../../../../components/dialog/ha-paper-dialog";
-import type { HaPaperDialog } from "../../../../components/dialog/ha-paper-dialog";
import "../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { LovelaceCardConfig } from "../../../../data/lovelace";
@@ -27,7 +26,7 @@ import { SuggestCardDialogParams } from "./show-suggest-card-dialog";
@customElement("hui-dialog-suggest-card")
export class HuiDialogSuggestCard extends LitElement {
- @property() protected hass!: HomeAssistant;
+ @property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _params?: SuggestCardDialogParams;
@@ -35,16 +34,10 @@ export class HuiDialogSuggestCard extends LitElement {
@internalProperty() private _saving = false;
- @internalProperty() private _yamlMode = false;
-
- @query("ha-paper-dialog") private _dialog?: HaPaperDialog;
-
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
- public async showDialog(params: SuggestCardDialogParams): Promise {
+ public showDialog(params: SuggestCardDialogParams): void {
this._params = params;
- this._yamlMode =
- (this.hass.panels.lovelace?.config as any)?.mode === "yaml";
this._cardConfig =
params.cardConfig ||
computeCards(
@@ -58,15 +51,15 @@ export class HuiDialogSuggestCard extends LitElement {
if (!Object.isFrozen(this._cardConfig)) {
this._cardConfig = deepFreeze(this._cardConfig);
}
- if (this._dialog) {
- this._dialog.open();
- }
if (this._yamlEditor) {
this._yamlEditor.setValue(this._cardConfig);
}
}
protected render(): TemplateResult {
+ if (!this._params) {
+ return html``;
+ }
return html`
@@ -87,7 +80,7 @@ export class HuiDialogSuggestCard extends LitElement {
`
: ""}
- ${this._yamlMode && this._cardConfig
+ ${this._params.yaml && this._cardConfig
? html`
-
-
-
+
+
+
+
${this.hass!.localize(
"ui.panel.lovelace.unused_entities.title"
)}
`}
-
+
${this.hass!.localize(
"ui.panel.lovelace.editor.menu.raw_editor"
)}
@@ -210,7 +217,7 @@ class HUIRoot extends LitElement {
aria-label=${this.hass!.localize(
"ui.panel.lovelace.menu.refresh"
)}
- @tap="${this._handleRefresh}"
+ @request-selected="${this._handleRefresh}"
>
${this.hass!.localize(
"ui.panel.lovelace.menu.refresh"
@@ -220,7 +227,7 @@ class HUIRoot extends LitElement {
aria-label=${this.hass!.localize(
"ui.panel.lovelace.unused_entities.title"
)}
- @tap="${this._handleUnusedEntities}"
+ @request-selected="${this._handleUnusedEntities}"
>
${this.hass!.localize(
"ui.panel.lovelace.unused_entities.title"
@@ -235,7 +242,7 @@ class HUIRoot extends LitElement {
aria-label=${this.hass!.localize(
"ui.panel.lovelace.menu.reload_resources"
)}
- @tap="${this._handleReloadResources}"
+ @request-selected=${this._handleReloadResources}
>
${this.hass!.localize(
"ui.panel.lovelace.menu.reload_resources"
@@ -243,13 +250,13 @@ class HUIRoot extends LitElement {
`
: ""}
- ${this.hass!.user!.is_admin && !this.hass!.config.safe_mode
+ ${this.hass!.user?.is_admin && !this.hass!.config.safe_mode
? html`
${this.hass!.localize(
"ui.panel.lovelace.menu.configure_ui"
@@ -257,14 +264,19 @@ class HUIRoot extends LitElement {
`
: ""}
-
- ${this.hass!.localize("ui.panel.lovelace.menu.help")}
-
+
+ ${this.hass!.localize("ui.panel.lovelace.menu.help")}
+
+
`}
@@ -474,11 +486,17 @@ class HUIRoot extends LitElement {
return this.shadowRoot!.getElementById("view") as HTMLDivElement;
}
- private _handleRefresh(): void {
+ private _handleRefresh(ev: CustomEvent): void {
+ if (!shouldHandleRequestSelectedEvent(ev)) {
+ return;
+ }
fireEvent(this, "config-refresh");
}
- private _handleReloadResources(): void {
+ private _handleReloadResources(ev: CustomEvent): void {
+ if (!shouldHandleRequestSelectedEvent(ev)) {
+ return;
+ }
this.hass.callService("lovelace", "reload_resources");
showConfirmationDialog(this, {
title: this.hass!.localize(
@@ -491,7 +509,17 @@ class HUIRoot extends LitElement {
});
}
- private _handleUnusedEntities(): void {
+ private _handleRawEditor(ev: CustomEvent): void {
+ if (!shouldHandleRequestSelectedEvent(ev)) {
+ return;
+ }
+ this.lovelace!.enableFullEditMode();
+ }
+
+ private _handleUnusedEntities(ev: CustomEvent): void {
+ if (!shouldHandleRequestSelectedEvent(ev)) {
+ return;
+ }
navigate(this, `${this.route?.prefix}/hass-unused-entities`);
}
@@ -499,17 +527,20 @@ class HUIRoot extends LitElement {
showVoiceCommandDialog(this);
}
- private _handleHelp(): void {
- window.open("https://www.home-assistant.io/lovelace/", "_blank");
- }
-
- private _editModeEnable(): void {
+ private _handleEnableEditMode(ev: CustomEvent): void {
+ if (!shouldHandleRequestSelectedEvent(ev)) {
+ return;
+ }
if (this._yamlMode) {
showAlertDialog(this, {
text: "The edit UI is not available when in YAML mode.",
});
return;
}
+ this._enableEditMode();
+ }
+
+ private _enableEditMode(): void {
this.lovelace!.setEditMode(true);
}
@@ -614,7 +645,7 @@ class HUIRoot extends LitElement {
const viewConfig = this.config.views[viewIndex];
if (!viewConfig) {
- this._editModeEnable();
+ this._enableEditMode();
return;
}
@@ -663,7 +694,8 @@ class HUIRoot extends LitElement {
min-height: 100%;
}
paper-tabs {
- margin-left: 12px;
+ margin-left: max(env(safe-area-inset-left), 12px);
+ margin-right: env(safe-area-inset-right);
--paper-tabs-selection-bar-color: var(--text-primary-color, #fff);
text-transform: uppercase;
}
diff --git a/src/panels/lovelace/special-rows/hui-divider-row.ts b/src/panels/lovelace/special-rows/hui-divider-row.ts
index aaf4127b3a..40a3d25061 100644
--- a/src/panels/lovelace/special-rows/hui-divider-row.ts
+++ b/src/panels/lovelace/special-rows/hui-divider-row.ts
@@ -22,7 +22,7 @@ class HuiDividerRow extends LitElement implements LovelaceRow {
this._config = {
style: {
height: "1px",
- "background-color": "var(--secondary-text-color)",
+ "background-color": "var(--divider-color)",
},
...config,
};
diff --git a/src/panels/lovelace/special-rows/hui-section-row.ts b/src/panels/lovelace/special-rows/hui-section-row.ts
index 43900825d3..1f23264560 100644
--- a/src/panels/lovelace/special-rows/hui-section-row.ts
+++ b/src/panels/lovelace/special-rows/hui-section-row.ts
@@ -48,8 +48,7 @@ class HuiSectionRow extends LitElement implements LovelaceRow {
}
.divider {
height: 1px;
- background-color: var(--secondary-text-color);
- opacity: 0.25;
+ background-color: var(--divider-color);
margin-left: -16px;
margin-right: -16px;
margin-top: 8px;
diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts
index adfcd98e01..4bd956ee1d 100644
--- a/src/panels/lovelace/types.ts
+++ b/src/panels/lovelace/types.ts
@@ -17,6 +17,7 @@ declare global {
export interface Lovelace {
config: LovelaceConfig;
editMode: boolean;
+ urlPath: string | null;
mode: "generated" | "yaml" | "storage";
language: string;
enableFullEditMode: () => void;
diff --git a/src/panels/lovelace/views/hui-view.ts b/src/panels/lovelace/views/hui-view.ts
index 49279ef253..cc96bae638 100644
--- a/src/panels/lovelace/views/hui-view.ts
+++ b/src/panels/lovelace/views/hui-view.ts
@@ -350,7 +350,7 @@ export class HUIView extends LitElement {
:host {
display: block;
box-sizing: border-box;
- padding: 4px 4px 0;
+ padding: 4px 4px env(safe-area-inset-bottom);
transform: translateZ(0);
position: relative;
color: var(--primary-text-color);
@@ -383,15 +383,15 @@ export class HUIView extends LitElement {
mwc-fab {
position: sticky;
float: right;
- bottom: 16px;
- right: 16px;
+ right: calc(16px + env(safe-area-inset-right));
+ bottom: calc(16px + env(safe-area-inset-bottom));
z-index: 1;
}
mwc-fab.rtl {
float: left;
right: auto;
- left: 16px;
+ left: calc(16px + env(safe-area-inset-left));
}
@media (max-width: 500px) {
diff --git a/src/panels/mailbox/ha-panel-mailbox.js b/src/panels/mailbox/ha-panel-mailbox.js
index 21bc6dd6e3..fb9e17acb0 100644
--- a/src/panels/mailbox/ha-panel-mailbox.js
+++ b/src/panels/mailbox/ha-panel-mailbox.js
@@ -1,5 +1,5 @@
import "@material/mwc-button";
-import "@polymer/app-layout/app-header-layout/app-header-layout";
+import "../../layouts/ha-app-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-input/paper-textarea";
@@ -76,7 +76,7 @@ class HaPanelMailbox extends EventsMixin(LocalizeMixin(PolymerElement)) {
}
-
+
-
+
`;
}
diff --git a/src/panels/map/ha-panel-map.js b/src/panels/map/ha-panel-map.js
index 3a89207227..a3c2d5103b 100644
--- a/src/panels/map/ha-panel-map.js
+++ b/src/panels/map/ha-panel-map.js
@@ -2,7 +2,10 @@ import "@polymer/app-layout/app-toolbar/app-toolbar";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
-import { setupLeafletMap } from "../../common/dom/setup-leaflet-map";
+import {
+ setupLeafletMap,
+ replaceTileLayer,
+} from "../../common/dom/setup-leaflet-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { navigate } from "../../common/navigate";
@@ -12,6 +15,7 @@ import { defaultRadiusColor } from "../../data/zone";
import LocalizeMixin from "../../mixins/localize-mixin";
import "./ha-entity-marker";
import "../../styles/polymer-ha-style";
+import "../../layouts/ha-app-layout";
/*
* @appliesMixin LocalizeMixin
@@ -21,28 +25,35 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
return html`
-
-
- [[localize('panel.map')]]
-
-
-
-
-
-
+
+
+
+
+ [[localize('panel.map')]]
+
+
+
+
+
+
+
`;
}
@@ -62,7 +73,11 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
}
async loadMap() {
- [this._map, this.Leaflet] = await setupLeafletMap(this.$.map);
+ this._darkMode = this.hass.themes.darkMode;
+ [this._map, this.Leaflet, this._tileLayer] = await setupLeafletMap(
+ this.$.map,
+ this._darkMode
+ );
this.drawEntities(this.hass);
this._map.invalidateSize();
this.fitMap();
@@ -83,7 +98,7 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
}
fitMap() {
- var bounds;
+ let bounds;
if (this._mapItems.length === 0) {
this._map.setView(
@@ -103,25 +118,35 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
drawEntities(hass) {
/* eslint-disable vars-on-top */
- var map = this._map;
+ const map = this._map;
if (!map) return;
+ if (this._darkMode !== this.hass.themes.darkMode) {
+ this._darkMode = this.hass.themes.darkMode;
+ this._tileLayer = replaceTileLayer(
+ this.Leaflet,
+ map,
+ this._tileLayer,
+ this.hass.themes.darkMode
+ );
+ }
+
if (this._mapItems) {
this._mapItems.forEach(function (marker) {
marker.remove();
});
}
- var mapItems = (this._mapItems = []);
+ const mapItems = (this._mapItems = []);
if (this._mapZones) {
this._mapZones.forEach(function (marker) {
marker.remove();
});
}
- var mapZones = (this._mapZones = []);
+ const mapZones = (this._mapZones = []);
Object.keys(hass.states).forEach((entityId) => {
- var entity = hass.states[entityId];
+ const entity = hass.states[entityId];
if (
entity.state === "home" ||
@@ -131,15 +156,15 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
return;
}
- var title = computeStateName(entity);
- var icon;
+ const title = computeStateName(entity);
+ let icon;
if (computeStateDomain(entity) === "zone") {
// DRAW ZONE
if (entity.attributes.passive) return;
// create icon
- var iconHTML = "";
+ let iconHTML = "";
if (entity.attributes.icon) {
const el = document.createElement("ha-icon");
el.setAttribute("icon", entity.attributes.icon);
@@ -153,7 +178,7 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
icon = this.Leaflet.divIcon({
html: iconHTML,
iconSize: [24, 24],
- className: "light",
+ className: "icon",
});
// create marker with the icon
@@ -185,8 +210,8 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
// DRAW ENTITY
// create icon
- var entityPicture = entity.attributes.entity_picture || "";
- var entityName = title
+ const entityPicture = entity.attributes.entity_picture || "";
+ const entityName = title
.split(" ")
.map(function (part) {
return part.substr(0, 1);
diff --git a/src/panels/profile/ha-panel-profile.ts b/src/panels/profile/ha-panel-profile.ts
index d783e37260..3c429e2e31 100644
--- a/src/panels/profile/ha-panel-profile.ts
+++ b/src/panels/profile/ha-panel-profile.ts
@@ -1,5 +1,5 @@
import "@material/mwc-button";
-import "@polymer/app-layout/app-header-layout/app-header-layout";
+import "../../layouts/ha-app-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-item/paper-item";
@@ -70,7 +70,7 @@ class HaPanelProfile extends LitElement {
protected render(): TemplateResult {
return html`
-
+
-
+
`;
}
@@ -211,6 +211,7 @@ class HaPanelProfile extends LitElement {
display: block;
max-width: 600px;
margin: 0 auto;
+ padding-bottom: env(safe-area-inset-bottom);
}
.content > * {
diff --git a/src/panels/profile/ha-pick-theme-row.js b/src/panels/profile/ha-pick-theme-row.js
deleted file mode 100644
index 9d4f62dec1..0000000000
--- a/src/panels/profile/ha-pick-theme-row.js
+++ /dev/null
@@ -1,112 +0,0 @@
-import "@polymer/paper-item/paper-item";
-import "@polymer/paper-listbox/paper-listbox";
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-/* eslint-plugin-disable lit */
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-import "../../components/ha-paper-dropdown-menu";
-import { EventsMixin } from "../../mixins/events-mixin";
-import LocalizeMixin from "../../mixins/localize-mixin";
-
-/*
- * @appliesMixin LocalizeMixin
- * @appliesMixin EventsMixin
- */
-class HaPickThemeRow extends LocalizeMixin(EventsMixin(PolymerElement)) {
- static get template() {
- return html`
-
-
- `;
- }
-
- static get properties() {
- return {
- hass: Object,
- narrow: Boolean,
- _hasThemes: {
- type: Boolean,
- computed: "_compHasThemes(hass)",
- },
- themes: {
- type: Array,
- computed: "_computeThemes(hass)",
- },
- selectedTheme: {
- type: Number,
- },
- };
- }
-
- static get observers() {
- return ["selectionChanged(hass, selectedTheme)"];
- }
-
- _compHasThemes(hass) {
- return (
- hass.themes &&
- hass.themes.themes &&
- Object.keys(hass.themes.themes).length
- );
- }
-
- ready() {
- super.ready();
- if (
- this.hass.selectedTheme &&
- this.themes.indexOf(this.hass.selectedTheme) > 0
- ) {
- this.selectedTheme = this.themes.indexOf(this.hass.selectedTheme);
- } else if (!this.hass.selectedTheme) {
- this.selectedTheme = 0;
- }
- }
-
- _computeThemes(hass) {
- if (!hass) return [];
- return ["Backend-selected", "default"].concat(
- Object.keys(hass.themes.themes).sort()
- );
- }
-
- selectionChanged(hass, selection) {
- if (selection > 0 && selection < this.themes.length) {
- if (hass.selectedTheme !== this.themes[selection]) {
- this.fire("settheme", this.themes[selection]);
- }
- } else if (selection === 0 && hass.selectedTheme !== "") {
- this.fire("settheme", "");
- }
- }
-}
-
-customElements.define("ha-pick-theme-row", HaPickThemeRow);
diff --git a/src/panels/profile/ha-pick-theme-row.ts b/src/panels/profile/ha-pick-theme-row.ts
new file mode 100644
index 0000000000..84d5dd4992
--- /dev/null
+++ b/src/panels/profile/ha-pick-theme-row.ts
@@ -0,0 +1,240 @@
+import "@polymer/paper-item/paper-item";
+import "@polymer/paper-listbox/paper-listbox";
+import "../../components/ha-paper-dropdown-menu";
+import { TemplateResult, html } from "lit-html";
+import {
+ property,
+ internalProperty,
+ LitElement,
+ customElement,
+ PropertyValues,
+ CSSResult,
+ css,
+} from "lit-element";
+import { HomeAssistant } from "../../types";
+import "./ha-settings-row";
+import { fireEvent } from "../../common/dom/fire_event";
+import "../../components/ha-formfield";
+import "../../components/ha-radio";
+import "@polymer/paper-input/paper-input";
+import type { HaRadio } from "../../components/ha-radio";
+import "@material/mwc-button/mwc-button";
+
+@customElement("ha-pick-theme-row")
+export class HaPickThemeRow extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ type: Boolean }) public narrow!: boolean;
+
+ @internalProperty() _themes: string[] = [];
+
+ @internalProperty() _selectedTheme = 0;
+
+ protected render(): TemplateResult {
+ const hasThemes =
+ this.hass.themes?.themes && Object.keys(this.hass.themes.themes).length;
+ const curTheme =
+ this.hass!.selectedTheme?.theme || this.hass!.themes.default_theme;
+ return html`
+
`
+ : ""}
+ `;
+ }
+
+ protected updated(changedProperties: PropertyValues) {
+ const oldHass = changedProperties.get("hass") as undefined | HomeAssistant;
+ const themesChanged =
+ changedProperties.has("hass") &&
+ (!oldHass || oldHass.themes?.themes !== this.hass.themes?.themes);
+ const selectedThemeChanged =
+ changedProperties.has("hass") &&
+ (!oldHass || oldHass.selectedTheme !== this.hass.selectedTheme);
+
+ if (themesChanged) {
+ this._themes = ["Backend-selected", "default"].concat(
+ Object.keys(this.hass.themes.themes).sort()
+ );
+ }
+
+ if (selectedThemeChanged) {
+ if (
+ this.hass.selectedTheme &&
+ this._themes.indexOf(this.hass.selectedTheme.theme) > 0
+ ) {
+ this._selectedTheme = this._themes.indexOf(
+ this.hass.selectedTheme.theme
+ );
+ } else if (!this.hass.selectedTheme) {
+ this._selectedTheme = 0;
+ }
+ }
+ }
+
+ private _handleColorChange(ev: CustomEvent) {
+ const target = ev.target as any;
+ fireEvent(this, "settheme", { [target.name]: target.value });
+ }
+
+ private _resetColors() {
+ fireEvent(this, "settheme", {
+ primaryColor: undefined,
+ accentColor: undefined,
+ });
+ }
+
+ private _handleDarkMode(ev: CustomEvent) {
+ let dark: boolean | undefined;
+ switch ((ev.target as HaRadio).value) {
+ case "light":
+ dark = false;
+ break;
+ case "dark":
+ dark = true;
+ break;
+ }
+ fireEvent(this, "settheme", { dark });
+ }
+
+ private _handleThemeSelection(ev: CustomEvent) {
+ const theme = ev.detail.item.theme;
+ if (theme === "Backend-selected") {
+ if (this.hass.selectedTheme?.theme) {
+ fireEvent(this, "settheme", { theme: "" });
+ }
+ return;
+ }
+ fireEvent(this, "settheme", { theme });
+ }
+
+ static get styles(): CSSResult {
+ return css`
+ a {
+ color: var(--primary-color);
+ }
+ .inputs {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ margin: 0 12px;
+ }
+ ha-formfield {
+ margin: 0 4px;
+ }
+ .color-pickers {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ flex-grow: 1;
+ }
+ paper-input {
+ min-width: 75px;
+ flex-grow: 1;
+ margin: 0 4px;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-pick-theme-row": HaPickThemeRow;
+ }
+}
diff --git a/src/panels/shopping-list/ha-panel-shopping-list.js b/src/panels/shopping-list/ha-panel-shopping-list.js
index 1adc188439..2229607672 100644
--- a/src/panels/shopping-list/ha-panel-shopping-list.js
+++ b/src/panels/shopping-list/ha-panel-shopping-list.js
@@ -1,4 +1,4 @@
-import "@polymer/app-layout/app-header-layout/app-header-layout";
+import "../../layouts/ha-app-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-checkbox/paper-checkbox";
@@ -68,7 +68,7 @@ class HaPanelShoppingList extends LocalizeMixin(PolymerElement) {
}
-