Merge pull request #5862 from home-assistant/dev

This commit is contained in:
Bram Kragten 2020-05-14 01:44:59 +02:00 committed by GitHub
commit 6853db693a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 602 additions and 561 deletions

View File

@ -19,6 +19,8 @@ gulp.task("gen-service-worker-app-dev", (done) => {
console.debug('Service worker disabled in development');
self.addEventListener('install', (event) => {
// This will activate the dev service worker,
// removing any prod service worker the dev might have running
self.skipWaiting();
});
`
@ -27,6 +29,28 @@ self.addEventListener('install', (event) => {
});
gulp.task("gen-service-worker-app-prod", async () => {
// Read bundled source file
const bundleManifestLatest = require(path.resolve(
paths.output,
"manifest.json"
));
let serviceWorkerContent = fs.readFileSync(
paths.root + bundleManifestLatest["service_worker.js"],
"utf-8"
);
// Delete old file from frontend_latest so manifest won't pick it up
fs.removeSync(paths.root + bundleManifestLatest["service_worker.js"]);
fs.removeSync(paths.root + bundleManifestLatest["service_worker.js.map"]);
// Remove ES5
const bundleManifestES5 = require(path.resolve(
paths.output_es5,
"manifest.json"
));
fs.removeSync(paths.root + bundleManifestES5["service_worker.js"]);
fs.removeSync(paths.root + bundleManifestES5["service_worker.js.map"]);
const workboxManifest = await workboxBuild.getManifest({
// Files that mach this pattern will be considered unique and skip revision check
// ignore JS files + translation files
@ -37,7 +61,8 @@ gulp.task("gen-service-worker-app-prod", async () => {
"frontend_latest/*.js",
// Cache all English translations because we catch them as fallback
// Using pattern to match hash instead of * to avoid caching en-GB
"static/translations/**/en-+([a-f0-9]).json",
// 'v' added as valid hash letter because in dev we hash with 'dev'
"static/translations/**/en-+([a-fv0-9]).json",
// Icon shown on splash screen
"static/icons/favicon-192x192.png",
"static/icons/favicon.ico",
@ -53,20 +78,6 @@ gulp.task("gen-service-worker-app-prod", async () => {
console.warn(warning);
}
// Replace `null` with 0 for better compression
for (const entry of workboxManifest.manifestEntries) {
if (entry.revision === null) {
entry.revision = 0;
}
}
const manifest = require(path.resolve(paths.output, "manifest.json"));
// Write bundled source file
let serviceWorkerContent = fs.readFileSync(
paths.root + manifest["service_worker.js"],
"utf-8"
);
// remove source map and add WB manifest
serviceWorkerContent = sourceMapUrl.removeFrom(serviceWorkerContent);
serviceWorkerContent = serviceWorkerContent.replace(
@ -76,8 +87,4 @@ gulp.task("gen-service-worker-app-prod", async () => {
// Write new file to root
fs.writeFileSync(swDest, serviceWorkerContent);
// Delete old file from frontend_latest
fs.removeSync(paths.root + manifest["service_worker.js"]);
fs.removeSync(paths.root + manifest["service_worker.js.map"]);
});

View File

@ -20,7 +20,9 @@ const createWebpackConfig = ({
}
return {
mode: isProdBuild ? "production" : "development",
devtool: isProdBuild ? "source-map" : "inline-cheap-module-source-map",
devtool: isProdBuild
? "cheap-module-source-map"
: "eval-cheap-module-source-map",
entry,
module: {
rules: [
@ -74,6 +76,10 @@ const createWebpackConfig = ({
/@polymer\/font-roboto\/roboto\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
new webpack.NormalModuleReplacementPlugin(
/@vaadin\/vaadin-material-styles\/font-roboto\.js$/,
path.resolve(paths.polymer_dir, "src/util/empty.js")
),
// Ignore mwc icons pointing at CDN.
new webpack.NormalModuleReplacementPlugin(
/@material\/mwc-icon\/mwc-icon-font\.js$/,

View File

@ -184,7 +184,7 @@ export class HcConnect extends LitElement {
this.castManager = null;
}
);
registerServiceWorker(false);
registerServiceWorker(this, false);
}
private async _handleDemo() {

View File

@ -7,5 +7,5 @@ set -e
cd "$(dirname "$0")/.."
STATS=1 NODE_ENV=production ../node_modules/.bin/webpack --profile --json > compilation-stats.json
npx webpack-bundle-analyzer compilation-stats.json dist
npx webpack-bundle-analyzer compilation-stats.json dist/frontend_latest
rm compilation-stats.json

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20200513.0",
version="20200514.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",

View File

@ -121,7 +121,7 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
const tempA = document.createElement("a");
tempA.href = this.redirectUri!;
if (tempA.host === location.host) {
registerServiceWorker(false);
registerServiceWorker(this, false);
}
}

View File

@ -30,7 +30,7 @@ class HaCoverControls extends PolymerElement {
icon="hass:stop"
on-click="onStopTap"
invisible$="[[!entityObj.supportsStop]]"
disabled="[[computStopDisabled(stateObj)]]"
disabled="[[computeStopDisabled(stateObj)]]"
></ha-icon-button>
<ha-icon-button
aria-label="Close cover"

View File

@ -31,7 +31,7 @@ class HaCoverTiltControls extends PolymerElement {
icon="hass:stop"
on-click="onStopTiltTap"
invisible$="[[!entityObj.supportsStopTilt]]"
disabled="[[computStopDisabled(stateObj)]]"
disabled="[[computeStopDisabled(stateObj)]]"
title="Stop tilt"
></ha-icon-button>
<ha-icon-button

View File

@ -164,10 +164,18 @@ self.addEventListener("install", (event) => {
event.waitUntil(caches.delete(cacheName));
});
self.addEventListener("activate", () => {
// Attach the service worker to any page of the app
// that didn't have a service worker loaded.
// Happens the first time they open the app without any
// service worker registered.
// This will serve code splitted bundles from SW.
clients.claim();
});
self.addEventListener("message", (message) => {
if (message.data.type === "skipWaiting") {
self.skipWaiting();
clients.claim();
}
});

View File

@ -46,7 +46,7 @@ export class HomeAssistantAppEl extends HassElement {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._initialize();
setTimeout(registerServiceWorker, 1000);
setTimeout(() => registerServiceWorker(this), 1000);
/* polyfill for paper-dropdown */
import(
/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"

View File

@ -96,7 +96,7 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
import(
/* webpackChunkName: "onboarding-core-config" */ "./onboarding-core-config"
);
registerServiceWorker(false);
registerServiceWorker(this, false);
this.addEventListener("onboarding-step", (ev) => this._handleStepDone(ev));
}

View File

@ -14,10 +14,7 @@ import memoizeOne from "memoize-one";
import * as Fuse from "fuse.js";
import { caseInsensitiveCompare } from "../../../common/string/compare";
import { computeRTL } from "../../../common/util/compute_rtl";
import {
afterNextRender,
nextRender,
} from "../../../common/util/render-status";
import { nextRender } from "../../../common/util/render-status";
import "../../../components/entity/ha-state-icon";
import "../../../components/ha-card";
import "@material/mwc-fab";
@ -46,6 +43,7 @@ import { domainToName } from "../../../data/integration";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-tabs-subpage";
import "../../../layouts/hass-loading-screen";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
@ -96,7 +94,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
@property() public route!: Route;
@property() private _configEntries: ConfigEntryExtended[] = [];
@property() private _configEntries?: ConfigEntryExtended[];
@property()
private _configEntriesInProgress: DataEntryFlowProgressExtended[] = [];
@ -217,32 +215,17 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
if (
this._searchParms.has("config_entry") &&
changed.has("_configEntries") &&
!(changed.get("_configEntries") as ConfigEntry[]).length &&
this._configEntries.length
!changed.get("_configEntries") &&
this._configEntries
) {
afterNextRender(() => {
const entryId = this._searchParms.get("config_entry")!;
const configEntry = this._configEntries.find(
(entry) => entry.entry_id === entryId
);
if (!configEntry) {
return;
}
const card: HaIntegrationCard = this.shadowRoot!.querySelector(
`[data-domain=${configEntry?.domain}]`
) as HaIntegrationCard;
if (card) {
card.scrollIntoView({
block: "center",
});
card.classList.add("highlight");
card.selectedConfigEntryId = entryId;
}
});
this._highlightEntry();
}
}
protected render(): TemplateResult {
if (!this._configEntries) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
const [
groupedConfigEntries,
ignoredConfigEntries,
@ -428,7 +411,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
</p>
<mwc-button @click=${this._createFlow} unelevated
>${this.hass.localize(
"ui.panel.config.integrations.add"
"ui.panel.config.integrations.add_integration"
)}</mwc-button
>
</div>
@ -491,7 +474,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
}
private _handleRemoved(ev: HASSDomEvent<ConfigEntryRemovedEvent>) {
this._configEntries = this._configEntries.filter(
this._configEntries = this._configEntries!.filter(
(entry) => entry.entry_id !== ev.detail.entryId
);
}
@ -594,6 +577,27 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
ev.target.style.visibility = "hidden";
}
private async _highlightEntry() {
await nextRender();
const entryId = this._searchParms.get("config_entry")!;
const configEntry = this._configEntries!.find(
(entry) => entry.entry_id === entryId
);
if (!configEntry) {
return;
}
const card: HaIntegrationCard = this.shadowRoot!.querySelector(
`[data-domain=${configEntry?.domain}]`
) as HaIntegrationCard;
if (card) {
card.scrollIntoView({
block: "center",
});
card.classList.add("highlight");
card.selectedConfigEntryId = entryId;
}
}
static get styles(): CSSResult[] {
return [
haStyle,

View File

@ -93,7 +93,11 @@ export class HaIntegrationCard extends LitElement {
html`<paper-item
.entryId=${item.entry_id}
@click=${this._selectConfigEntry}
><paper-item-body>${item.title}</paper-item-body
><paper-item-body
>${item.title ||
this.hass.localize(
"ui.panel.config.integrations.config_entry.unnamed_entry"
)}</paper-item-body
><ha-icon-next></ha-icon-next
></paper-item>`
)}

View File

@ -1,51 +1,407 @@
import "@material/mwc-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "../../../../components/ha-icon-button";
import "@polymer/paper-spinner/paper-spinner";
import "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { HASSDomEvent } from "../../../../common/dom/fire_event";
import { HomeAssistant } from "../../../../types";
import "./hui-edit-view";
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
import { navigate } from "../../../../common/navigate";
import "../../../../components/dialog/ha-paper-dialog";
import type { HaPaperDialog } from "../../../../components/dialog/ha-paper-dialog";
import type {
LovelaceBadgeConfig,
LovelaceCardConfig,
LovelaceViewConfig,
} from "../../../../data/lovelace";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "../../components/hui-entity-editor";
import { addView, deleteView, replaceView } from "../config-util";
import "../hui-badge-preview";
import { processEditorEntities } from "../process-editor-entities";
import {
EntitiesEditorEvent,
ViewEditEvent,
ViewVisibilityChangeEvent,
} from "../types";
import "./hui-view-editor";
import "./hui-view-visibility-editor";
import { EditViewDialogParams } from "./show-edit-view-dialog";
declare global {
// for fire event
interface HASSDomEvents {
"reload-lovelace": undefined;
}
// for add event listener
interface HTMLElementEventMap {
"reload-lovelace": HASSDomEvent<undefined>;
}
}
@customElement("hui-dialog-edit-view")
export class HuiDialogEditView extends LitElement {
@property() protected hass?: HomeAssistant;
@property() public hass?: HomeAssistant;
@property() private _params?: EditViewDialogParams;
@property() private _config?: LovelaceViewConfig;
@property() private _badges?: LovelaceBadgeConfig[];
@property() private _cards?: LovelaceCardConfig[];
@property() private _saving = false;
@property() private _curTab?: string;
private _curTabIndex = 0;
public async showDialog(params: EditViewDialogParams): Promise<void> {
// Wait till dialog is rendered.
this._params = params;
await this.updateComplete;
(this.shadowRoot!.children[0] as any).showDialog();
if (this._dialog == null) {
await this.updateComplete;
}
if (this._params.viewIndex === undefined) {
this._config = {};
this._badges = [];
this._cards = [];
} else {
const {
cards,
badges,
...viewConfig
} = this._params.lovelace!.config.views[this._params.viewIndex];
this._config = viewConfig;
this._badges = badges ? processEditorEntities(badges) : [];
this._cards = cards;
}
this._dialog.open();
}
private get _dialog(): HaPaperDialog {
return this.shadowRoot!.querySelector("ha-paper-dialog")!;
}
private get _viewConfigTitle(): string {
if (!this._config || !this._config.title) {
return this.hass!.localize("ui.panel.lovelace.editor.edit_view.header");
}
return this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.header_name",
"name",
this._config.title
);
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
}
let content;
switch (this._curTab) {
case "tab-settings":
content = html`
<hui-view-editor
.isNew=${this._params.viewIndex === undefined}
.hass=${this.hass}
.config="${this._config}"
@view-config-changed="${this._viewConfigChanged}"
></hui-view-editor>
`;
break;
case "tab-badges":
content = html`
${this._badges?.length
? html`
<div class="preview-badges">
${this._badges.map((badgeConfig) => {
return html`
<hui-badge-preview
.hass=${this.hass}
.config=${badgeConfig}
></hui-badge-preview>
`;
})}
</div>
`
: ""}
<hui-entity-editor
.hass=${this.hass}
.entities="${this._badges}"
@entities-changed="${this._badgesChanged}"
></hui-entity-editor>
`;
break;
case "tab-visibility":
content = html`
<hui-view-visibility-editor
.hass="${this.hass}"
.config="${this._config}"
@view-visibility-changed="${this._viewVisibilityChanged}"
></hui-view-visibility-editor>
`;
break;
case "tab-cards":
content = html` Cards `;
break;
}
return html`
<hui-edit-view
.hass=${this.hass}
.lovelace="${this._params.lovelace}"
.viewIndex="${this._params.viewIndex}"
>
</hui-edit-view>
<ha-paper-dialog with-backdrop modal>
<h2>
${this._viewConfigTitle}
</h2>
<paper-tabs
scrollable
hide-scroll-buttons
.selected="${this._curTabIndex}"
@selected-item-changed="${this._handleTabSelected}"
>
<paper-tab id="tab-settings"
>${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.tab_settings"
)}</paper-tab
>
<paper-tab id="tab-badges"
>${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.tab_badges"
)}</paper-tab
>
<paper-tab id="tab-visibility"
>${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.tab_visibility"
)}</paper-tab
>
</paper-tabs>
<paper-dialog-scrollable> ${content} </paper-dialog-scrollable>
<div class="paper-dialog-buttons">
${this._params.viewIndex !== undefined
? html`
<mwc-button class="warning" @click="${this._deleteConfirm}">
${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.delete"
)}
</mwc-button>
`
: ""}
<mwc-button @click="${this._closeDialog}"
>${this.hass!.localize("ui.common.cancel")}</mwc-button
>
<mwc-button
?disabled="${!this._config || this._saving}"
@click="${this._save}"
>
<paper-spinner
?active="${this._saving}"
alt="Saving"
></paper-spinner>
${this.hass!.localize("ui.common.save")}</mwc-button
>
</div>
</ha-paper-dialog>
`;
}
private async _delete(): Promise<void> {
if (!this._params) {
return;
}
try {
await this._params.lovelace!.saveConfig(
deleteView(this._params.lovelace!.config, this._params.viewIndex!)
);
this._closeDialog();
navigate(this, `/${window.location.pathname.split("/")[1]}`);
} catch (err) {
showAlertDialog(this, {
text: `Deleting failed: ${err.message}`,
});
}
}
private _deleteConfirm(): void {
showConfirmationDialog(this, {
title: this.hass!.localize(
`ui.panel.lovelace.views.confirm_delete${
this._cards?.length ? `_existing_cards` : ""
}`
),
text: this.hass!.localize(
`ui.panel.lovelace.views.confirm_delete${
this._cards?.length ? `_existing_cards` : ""
}_text`,
"name",
this._config?.title || "Unnamed view",
"number",
this._cards?.length || 0
),
confirm: () => this._delete(),
});
}
private async _resizeDialog(): Promise<void> {
await this.updateComplete;
fireEvent(this._dialog as HTMLElement, "iron-resize");
}
private _closeDialog(): void {
this._curTabIndex = 0;
this._params = undefined;
this._config = {};
this._badges = [];
this._dialog.close();
}
private _handleTabSelected(ev: CustomEvent): void {
if (!ev.detail.value) {
return;
}
this._curTab = ev.detail.value.id;
this._resizeDialog();
}
private async _save(): Promise<void> {
if (!this._params || !this._config) {
return;
}
if (!this._isConfigChanged()) {
this._closeDialog();
return;
}
this._saving = true;
const viewConf: LovelaceViewConfig = {
...this._config,
badges: this._badges,
cards: this._cards,
};
const lovelace = this._params.lovelace!;
try {
await lovelace.saveConfig(
this._creatingView
? addView(lovelace.config, viewConf)
: replaceView(lovelace.config, this._params.viewIndex!, viewConf)
);
if (this._params.saveCallback) {
this._params.saveCallback(
this._params.viewIndex || lovelace.config.views.length,
viewConf
);
}
this._closeDialog();
} catch (err) {
showAlertDialog(this, {
text: `Saving failed: ${err.message}`,
});
} finally {
this._saving = false;
}
}
private _viewConfigChanged(ev: ViewEditEvent): void {
if (ev.detail && ev.detail.config) {
this._config = ev.detail.config;
}
}
private _viewVisibilityChanged(
ev: HASSDomEvent<ViewVisibilityChangeEvent>
): void {
if (ev.detail.visible && this._config) {
this._config.visible = ev.detail.visible;
}
}
private _badgesChanged(ev: EntitiesEditorEvent): void {
if (!this._badges || !this.hass || !ev.detail || !ev.detail.entities) {
return;
}
this._badges = processEditorEntities(ev.detail.entities);
this._resizeDialog();
}
private _isConfigChanged(): boolean {
return (
this._creatingView ||
JSON.stringify(this._config) !==
JSON.stringify(
this._params!.lovelace!.config.views[this._params!.viewIndex!]
)
);
}
private get _creatingView(): boolean {
return this._params!.viewIndex === undefined;
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
@media all and (max-width: 450px), all and (max-height: 500px) {
/* overrule the ha-style-dialog max-height on small screens */
ha-paper-dialog {
max-height: 100%;
height: 100%;
}
}
@media all and (min-width: 660px) {
ha-paper-dialog {
width: 650px;
}
}
ha-paper-dialog {
max-width: 650px;
}
paper-tabs {
--paper-tabs-selection-bar-color: var(--primary-color);
text-transform: uppercase;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
mwc-button paper-spinner {
width: 14px;
height: 14px;
margin-right: 20px;
}
mwc-button.warning {
margin-right: auto;
}
paper-spinner {
display: none;
}
paper-spinner[active] {
display: block;
}
paper-dialog-scrollable {
margin-top: 0;
}
.hidden {
display: none;
}
.error {
color: var(--error-color);
border-bottom: 1px solid var(--error-color);
}
.preview-badges {
display: flex;
justify-content: center;
margin: 12px 16px;
flex-wrap: wrap;
}
`,
];
}
}
declare global {

View File

@ -1,400 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "../../../../components/ha-icon-button";
import "@polymer/paper-spinner/paper-spinner";
import "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
import { navigate } from "../../../../common/navigate";
import "../../../../components/dialog/ha-paper-dialog";
import type { HaPaperDialog } from "../../../../components/dialog/ha-paper-dialog";
import type {
LovelaceBadgeConfig,
LovelaceCardConfig,
LovelaceViewConfig,
} from "../../../../data/lovelace";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "../../components/hui-entity-editor";
import type { Lovelace } from "../../types";
import { addView, deleteView, replaceView } from "../config-util";
import "../hui-badge-preview";
import { processEditorEntities } from "../process-editor-entities";
import {
EntitiesEditorEvent,
ViewEditEvent,
ViewVisibilityChangeEvent,
} from "../types";
import "./hui-view-editor";
import "./hui-view-visibility-editor";
@customElement("hui-edit-view")
export class HuiEditView extends LitElement {
@property() public lovelace?: Lovelace;
@property() public viewIndex?: number;
@property() public hass?: HomeAssistant;
@property() private _config?: LovelaceViewConfig;
@property() private _badges?: LovelaceBadgeConfig[];
@property() private _cards?: LovelaceCardConfig[];
@property() private _saving: boolean;
@property() private _curTab?: string;
private _curTabIndex: number;
public constructor() {
super();
this._saving = false;
this._curTabIndex = 0;
}
public async showDialog(): Promise<void> {
// Wait till dialog is rendered.
if (this._dialog == null) {
await this.updateComplete;
}
if (this.viewIndex === undefined) {
this._config = {};
this._badges = [];
this._cards = [];
} else {
const { cards, badges, ...viewConfig } = this.lovelace!.config.views[
this.viewIndex
];
this._config = viewConfig;
this._badges = badges ? processEditorEntities(badges) : [];
this._cards = cards;
}
this._dialog.open();
}
private get _dialog(): HaPaperDialog {
return this.shadowRoot!.querySelector("ha-paper-dialog")!;
}
private get _viewConfigTitle(): string {
if (!this._config || !this._config.title) {
return this.hass!.localize("ui.panel.lovelace.editor.edit_view.header");
}
return this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.header_name",
"name",
this._config.title
);
}
protected render(): TemplateResult {
let content;
switch (this._curTab) {
case "tab-settings":
content = html`
<hui-view-editor
.isNew=${this.viewIndex === undefined}
.hass=${this.hass}
.config="${this._config}"
@view-config-changed="${this._viewConfigChanged}"
></hui-view-editor>
`;
break;
case "tab-badges":
content = html`
${this._badges?.length
? html`
<div class="preview-badges">
${this._badges.map((badgeConfig) => {
return html`
<hui-badge-preview
.hass=${this.hass}
.config=${badgeConfig}
></hui-badge-preview>
`;
})}
</div>
`
: ""}
<hui-entity-editor
.hass=${this.hass}
.entities="${this._badges}"
@entities-changed="${this._badgesChanged}"
></hui-entity-editor>
`;
break;
case "tab-visibility":
content = html`
<hui-view-visibility-editor
.hass="${this.hass}"
.config="${this._config}"
@view-visibility-changed="${this._viewVisibilityChanged}"
></hui-view-visibility-editor>
`;
break;
case "tab-cards":
content = html` Cards `;
break;
}
return html`
<ha-paper-dialog with-backdrop modal>
<h2>
${this._viewConfigTitle}
</h2>
<paper-tabs
scrollable
hide-scroll-buttons
.selected="${this._curTabIndex}"
@selected-item-changed="${this._handleTabSelected}"
>
<paper-tab id="tab-settings"
>${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.tab_settings"
)}</paper-tab
>
<paper-tab id="tab-badges"
>${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.tab_badges"
)}</paper-tab
>
<paper-tab id="tab-visibility"
>${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.tab_visibility"
)}</paper-tab
>
</paper-tabs>
<paper-dialog-scrollable> ${content} </paper-dialog-scrollable>
<div class="paper-dialog-buttons">
${this.viewIndex !== undefined
? html`
<mwc-button class="warning" @click="${this._deleteConfirm}">
${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.delete"
)}
</mwc-button>
`
: ""}
<mwc-button @click="${this._closeDialog}"
>${this.hass!.localize("ui.common.cancel")}</mwc-button
>
<mwc-button
?disabled="${!this._config || this._saving}"
@click="${this._save}"
>
<paper-spinner
?active="${this._saving}"
alt="Saving"
></paper-spinner>
${this.hass!.localize("ui.common.save")}</mwc-button
>
</div>
</ha-paper-dialog>
`;
}
private async _delete(): Promise<void> {
try {
await this.lovelace!.saveConfig(
deleteView(this.lovelace!.config, this.viewIndex!)
);
this._closeDialog();
navigate(this, `/${window.location.pathname.split("/")[1]}`);
} catch (err) {
showAlertDialog(this, {
text: `Deleting failed: ${err.message}`,
});
}
}
private _deleteConfirm(): void {
showConfirmationDialog(this, {
title: this.hass!.localize(
`ui.panel.lovelace.views.confirm_delete${
this._cards?.length ? `_existing_cards` : ""
}`
),
text: this.hass!.localize(
`ui.panel.lovelace.views.confirm_delete${
this._cards?.length ? `_existing_cards` : ""
}_text`,
"name",
this._config?.title || "Unnamed view",
"number",
this._cards?.length || 0
),
confirm: () => this._delete(),
});
}
private async _resizeDialog(): Promise<void> {
await this.updateComplete;
fireEvent(this._dialog as HTMLElement, "iron-resize");
}
private _closeDialog(): void {
this._curTabIndex = 0;
this.lovelace = undefined;
this._config = {};
this._badges = [];
this._dialog.close();
}
private _handleTabSelected(ev: CustomEvent): void {
if (!ev.detail.value) {
return;
}
this._curTab = ev.detail.value.id;
this._resizeDialog();
}
private async _save(): Promise<void> {
if (!this._config) {
return;
}
if (!this._isConfigChanged()) {
this._closeDialog();
return;
}
this._saving = true;
const viewConf: LovelaceViewConfig = {
...this._config,
badges: this._badges,
cards: this._cards,
};
const lovelace = this.lovelace!;
try {
await lovelace.saveConfig(
this._creatingView
? addView(lovelace.config, viewConf)
: replaceView(lovelace.config, this.viewIndex!, viewConf)
);
this._closeDialog();
} catch (err) {
showAlertDialog(this, {
text: `Saving failed: ${err.message}`,
});
} finally {
this._saving = false;
}
}
private _viewConfigChanged(ev: ViewEditEvent): void {
if (ev.detail && ev.detail.config) {
this._config = ev.detail.config;
}
}
private _viewVisibilityChanged(
ev: HASSDomEvent<ViewVisibilityChangeEvent>
): void {
if (ev.detail.visible && this._config) {
this._config.visible = ev.detail.visible;
}
}
private _badgesChanged(ev: EntitiesEditorEvent): void {
if (!this._badges || !this.hass || !ev.detail || !ev.detail.entities) {
return;
}
this._badges = processEditorEntities(ev.detail.entities);
this._resizeDialog();
}
private _isConfigChanged(): boolean {
return (
this._creatingView ||
JSON.stringify(this._config) !==
JSON.stringify(this.lovelace!.config.views[this.viewIndex!])
);
}
private get _creatingView(): boolean {
return this.viewIndex === undefined;
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
@media all and (max-width: 450px), all and (max-height: 500px) {
/* overrule the ha-style-dialog max-height on small screens */
ha-paper-dialog {
max-height: 100%;
height: 100%;
}
}
@media all and (min-width: 660px) {
ha-paper-dialog {
width: 650px;
}
}
ha-paper-dialog {
max-width: 650px;
}
paper-tabs {
--paper-tabs-selection-bar-color: var(--primary-color);
text-transform: uppercase;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
mwc-button paper-spinner {
width: 14px;
height: 14px;
margin-right: 20px;
}
mwc-button.warning {
margin-right: auto;
}
paper-spinner {
display: none;
}
paper-spinner[active] {
display: block;
}
paper-dialog-scrollable {
margin-top: 0;
}
.hidden {
display: none;
}
.error {
color: var(--error-color);
border-bottom: 1px solid var(--error-color);
}
.preview-badges {
display: flex;
justify-content: center;
margin: 12px 16px;
flex-wrap: wrap;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-edit-view": HuiEditView;
}
}

View File

@ -1,5 +1,6 @@
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
import { Lovelace } from "../../types";
import { LovelaceViewConfig } from "../../../../data/lovelace";
declare global {
// for fire event
@ -20,6 +21,7 @@ const dialogTag = "hui-dialog-edit-view";
export interface EditViewDialogParams {
lovelace: Lovelace;
viewIndex?: number;
saveCallback?: (viewIndex: number, viewConfig: LovelaceViewConfig) => void;
}
const registerEditViewDialog = (element: HTMLElement): Event =>

View File

@ -15,9 +15,14 @@ import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-generic-entity-row";
import "../components/hui-timestamp-display";
import "../components/hui-warning";
import { EntityConfig, LovelaceRow } from "./types";
import { LovelaceRow } from "./types";
import { EntitiesCardEntityConfig } from "../cards/types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { hasAction } from "../common/has-action";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { handleAction } from "../common/handle-action";
interface SensorEntityConfig extends EntityConfig {
interface SensorEntityConfig extends EntitiesCardEntityConfig {
format?: "relative" | "date" | "time" | "datetime";
}
@ -59,7 +64,14 @@ class HuiSensorEntityRow extends LitElement implements LovelaceRow {
return html`
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
<div class="text-content">
<div
class="text-content"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config.hold_action),
hasDoubleClick: hasAction(this._config.double_tap_action),
})}
>
${stateObj.attributes.device_class ===
SENSOR_DEVICE_CLASS_TIMESTAMP &&
stateObj.state !== "unavailable" &&
@ -81,6 +93,10 @@ class HuiSensorEntityRow extends LitElement implements LovelaceRow {
`;
}
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action);
}
static get styles(): CSSResult {
return css`
div {

View File

@ -31,7 +31,11 @@ import "../../components/ha-icon";
import "../../components/ha-menu-button";
import "../../components/ha-icon-button-arrow-next";
import "../../components/ha-icon-button-arrow-prev";
import type { LovelaceConfig, LovelacePanelConfig } from "../../data/lovelace";
import type {
LovelaceConfig,
LovelacePanelConfig,
LovelaceViewConfig,
} from "../../data/lovelace";
import {
showAlertDialog,
showConfirmationDialog,
@ -424,18 +428,24 @@ class HUIRoot extends LitElement {
}
if (!oldLovelace || oldLovelace.editMode !== this.lovelace!.editMode) {
const views = this.config && this.config.views;
// Adjust for higher header
if (!views || views.length < 2) {
fireEvent(this, "iron-resize");
}
// Leave unused entities when leaving edit mode
if (
this.lovelace!.mode === "storage" &&
viewPath === "hass-unused-entities"
) {
const views = this.config && this.config.views;
navigate(this, `${this.route?.prefix}/${views[0]?.path || 0}`);
newSelectView = 0;
}
}
if (!force) {
if (!force && huiView) {
huiView.lovelace = this.lovelace;
}
}
@ -552,6 +562,10 @@ class HUIRoot extends LitElement {
private _addView() {
showEditViewDialog(this, {
lovelace: this.lovelace!,
saveCallback: (viewIndex: number, viewConfig: LovelaceViewConfig) => {
const path = viewConfig.path || viewIndex;
navigate(this, `${this.route?.prefix}/${path}`);
},
});
}

View File

@ -34,7 +34,6 @@ export interface LovelaceCard extends HTMLElement {
hass?: HomeAssistant;
isPanel?: boolean;
editMode?: boolean;
index?: number;
getCardSize(): number;
setConfig(config: LovelaceCardConfig): void;
}

View File

@ -26,6 +26,7 @@ import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"
import { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
import "../../../components/ha-svg-icon";
import { mdiPlus } from "@mdi/js";
import { nextRender } from "../../../common/util/render-status";
let editCodeLoaded = false;
@ -60,6 +61,13 @@ export class HUIView extends LitElement {
@property() private _badges: LovelaceBadge[] = [];
private _createColumnsIteration = 0;
public constructor() {
super();
this.addEventListener("iron-resize", (ev) => ev.stopPropagation());
}
// Public to make demo happy
public createCardElement(cardConfig: LovelaceCardConfig) {
const element = createCardElement(cardConfig) as LovelaceCard;
@ -148,9 +156,7 @@ export class HUIView extends LitElement {
if (configChanged) {
this._createCards(lovelace.config.views[this.index!]);
} else if (editModeChanged) {
this._switchEditMode();
} else if (changedProperties.has("columns")) {
} else if (editModeChanged || changedProperties.has("columns")) {
this._recreateColumns();
}
@ -211,62 +217,75 @@ export class HUIView extends LitElement {
root.style.display = elements.length > 0 ? "block" : "none";
}
private _switchEditMode() {
if (this.lovelace!.editMode) {
const wrappedCards = this._cards.map((element) => {
const wrapper = document.createElement("hui-card-options");
wrapper.hass = this.hass;
wrapper.lovelace = this.lovelace;
wrapper.path = [this.index!, (element as LovelaceCard).index!];
(element as LovelaceCard).editMode = true;
wrapper.appendChild(element);
return wrapper;
});
this._createColumns(wrappedCards);
} else {
this._cards.forEach((card) => {
(card as LovelaceCard).editMode = false;
});
this._createColumns(this._cards);
}
private async _recreateColumns() {
this._createColumns();
}
private _recreateColumns() {
this._createColumns(this._cards);
}
private _createColumns(elements: HTMLElement[]) {
private _createColumns() {
this._createColumnsIteration++;
const iteration = this._createColumnsIteration;
const root = this.shadowRoot!.getElementById("columns")!;
while (root.lastChild) {
root.removeChild(root.lastChild);
}
let columns: HTMLElement[][] = [];
let columns: [number, number][][] = [];
const columnEntityCount: number[] = [];
for (let i = 0; i < this.columns!; i++) {
columns.push([]);
columnEntityCount.push(0);
}
elements.forEach((el) => {
this._cards.forEach((el, index) => {
const cardSize = computeCardSize(
(el.tagName === "HUI-CARD-OPTIONS" ? el.firstChild : el) as LovelaceCard
);
columns[getColumnIndex(columnEntityCount, cardSize)].push(el);
columns[getColumnIndex(columnEntityCount, cardSize)].push([
index,
cardSize,
]);
});
// Remove empty columns
columns = columns.filter((val) => val.length > 0);
columns.forEach((column) => {
columns.forEach((indexes) => {
const columnEl = document.createElement("div");
columnEl.classList.add("column");
column.forEach((el) => columnEl.appendChild(el));
this._addToColumn(columnEl, indexes, this.lovelace!.editMode, iteration);
root.appendChild(columnEl);
});
}
private async _addToColumn(columnEl, indexes, editMode, iteration) {
let i = 0;
for (const [index, cardSize] of indexes) {
const card: LovelaceCard = this._cards[index];
if (!editMode) {
card.editMode = false;
columnEl.appendChild(card);
} else {
const wrapper = document.createElement("hui-card-options");
wrapper.hass = this.hass;
wrapper.lovelace = this.lovelace;
wrapper.path = [this.index!, index];
card.editMode = true;
wrapper.appendChild(card);
columnEl.appendChild(wrapper);
}
i += cardSize;
if (i > 5) {
// eslint-disable-next-line no-await-in-loop
await nextRender();
if (iteration !== this._createColumnsIteration) {
return;
}
i = 0;
}
}
}
private _createCards(config: LovelaceViewConfig): void {
if (!config || !config.cards || !Array.isArray(config.cards)) {
this._cards = [];
@ -274,19 +293,14 @@ export class HUIView extends LitElement {
}
const elements: LovelaceCard[] = [];
config.cards.forEach((cardConfig, index) => {
config.cards.forEach((cardConfig) => {
const element = this.createCardElement(cardConfig);
element.index = index;
elements.push(element);
});
this._cards = elements;
if (this.lovelace!.editMode) {
this._switchEditMode();
} else {
this._createColumns(this._cards);
}
this._createColumns();
}
private _rebuildCard(
@ -294,7 +308,9 @@ export class HUIView extends LitElement {
config: LovelaceCardConfig
): void {
const newCardEl = this.createCardElement(config);
cardElToReplace.parentElement!.replaceChild(newCardEl, cardElToReplace);
if (cardElToReplace.parentElement) {
cardElToReplace.parentElement!.replaceChild(newCardEl, cardElToReplace);
}
this._cards = this._cards!.map((curCardEl) =>
curCardEl === cardElToReplace ? newCardEl : curCardEl
);

View File

@ -1391,6 +1391,7 @@
"manuf": "by {manufacturer}",
"hub": "Connected via",
"firmware": "Firmware: {version}",
"unnamed_entry": "Unnamed entry",
"device_unavailable": "device unavailable",
"entity_unavailable": "entity unavailable",
"area": "In {area}",

View File

@ -1,49 +1,57 @@
import { HassElement } from "../state/hass-element";
import { showToast } from "./toast";
export const supportsServiceWorker = () =>
"serviceWorker" in navigator &&
(location.protocol === "https:" || location.hostname === "localhost");
export const registerServiceWorker = (notifyUpdate = true) => {
export const registerServiceWorker = async (
rootEl: HTMLElement,
notifyUpdate = true
) => {
if (!supportsServiceWorker()) {
return;
}
navigator.serviceWorker.register("/service_worker.js").then((reg) => {
reg.addEventListener("updatefound", () => {
const installingWorker = reg.installing;
if (!installingWorker || !notifyUpdate) {
return;
}
installingWorker.addEventListener("statechange", () => {
if (
installingWorker.state === "installed" &&
navigator.serviceWorker.controller &&
!__DEV__ &&
!__DEMO__
) {
// Notify users here of a new frontend being available.
const haElement = window.document.querySelector(
"home-assistant, ha-onboarding"
)! as HassElement;
showToast(haElement, {
message: "A new version of the frontend is available.",
action: {
action: () =>
installingWorker.postMessage({ type: "skipWaiting" }),
text: "reload",
},
duration: 0,
dismissable: false,
});
}
});
});
});
// If the active service worker changes, refresh the page because the cache has changed
navigator.serviceWorker.addEventListener("controllerchange", () => {
location.reload();
});
const reg = await navigator.serviceWorker.register("/service_worker.js");
if (!notifyUpdate || __DEV__ || __DEMO__) {
return;
}
reg.addEventListener("updatefound", () => {
const installingWorker = reg.installing;
if (!installingWorker) {
return;
}
installingWorker.addEventListener("statechange", () => {
if (
installingWorker.state !== "installed" ||
!navigator.serviceWorker.controller
) {
return;
}
// Notify users a new frontend is available.
// When
showToast(rootEl, {
message: "A new version of the frontend is available.",
action: {
// We tell the service worker to call skipWaiting, which activates
// the new service worker. Above we listen for `controllerchange`
// so we reload the page once a new servic worker activates.
action: () => installingWorker.postMessage({ type: "skipWaiting" }),
text: "reload",
},
duration: 0,
dismissable: false,
});
});
});
};