mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-01 13:37:47 +00:00
Merge pull request #5862 from home-assistant/dev
This commit is contained in:
commit
6853db693a
@ -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"]);
|
||||
});
|
||||
|
@ -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$/,
|
||||
|
@ -184,7 +184,7 @@ export class HcConnect extends LitElement {
|
||||
this.castManager = null;
|
||||
}
|
||||
);
|
||||
registerServiceWorker(false);
|
||||
registerServiceWorker(this, false);
|
||||
}
|
||||
|
||||
private async _handleDemo() {
|
||||
|
@ -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
|
||||
|
2
setup.py
2
setup.py
@ -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",
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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>`
|
||||
)}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 =>
|
||||
|
@ -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 {
|
||||
|
@ -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}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,6 @@ export interface LovelaceCard extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
isPanel?: boolean;
|
||||
editMode?: boolean;
|
||||
index?: number;
|
||||
getCardSize(): number;
|
||||
setConfig(config: LovelaceCardConfig): void;
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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}",
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user