Compare commits

..

16 Commits

Author SHA1 Message Date
Paul Bottein
f75d3f887e Add buttons on mobile to move sections 2024-03-01 15:37:23 +01:00
Paul Bottein
c05824c641 Revert "Transform helper to warning for edit view type"
This reverts commit 3abdffda9c.
2024-03-01 14:57:08 +01:00
Paul Bottein
3abdffda9c Transform helper to warning for edit view type 2024-03-01 14:55:34 +01:00
Paul Bottein
67da851efc Use max column count instead of max width for section grid (#19932) 2024-03-01 13:09:21 +01:00
Paul Bottein
5463a27255 Add badges support to sections view (#19929) 2024-03-01 13:09:10 +01:00
renovate[bot]
ec0434c9b0 Update dependency hls.js to v1.5.7 (#19927)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-01 11:58:00 +01:00
renovate[bot]
7d8cb5c863 Update typescript-eslint monorepo to v7.1.0 (#19922) 2024-02-29 18:32:16 -05:00
Bram Kragten
4f01348ffb Improve error display in automation/script traces (#19920) 2024-02-29 13:09:02 -05:00
Paul Bottein
2af3400464 Fix section editing after disconnect/reconnect (#19917)
* Fix section editing after disconnect/reconnect

* Update src/components/ha-sortable.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-02-29 14:12:19 +00:00
renovate[bot]
b6e220a4c5 Update vaadinWebComponents monorepo to v24.3.7 (#19919)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-29 13:54:55 +01:00
renovate[bot]
d5d45f100e Update dependency open to v10.0.4 (#19918)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-29 13:54:22 +01:00
renovate[bot]
6b9ca60c47 Update octokit monorepo to v7 (major) (#19914)
Update octokit monorepo to v7

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-29 13:54:02 +01:00
Simon Lamon
bc445a1e27 Lokalize automation trace area (#19836)
* Translate automation trace timeline area

* Fix undefined changed_variables

* change naming options in triggered_by

* Split messages for stopped_by

* remove stopped message
2024-02-29 13:51:18 +01:00
dependabot[bot]
a087b4c43e Bump ip from 1.1.8 to 1.1.9 (#19915) 2024-02-29 01:20:20 -05:00
Paul Bottein
8f67ddf968 Add allow changing type of empty views (#19912) 2024-02-28 21:51:21 +01:00
Simon Lamon
9ef07484dd Replace paper-toast with mwc-snackbar (#19579)
* toast

* Fixes

* Linting

* Remove empty styles

* PR feedback

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2024-02-28 21:50:58 +01:00
25 changed files with 1237 additions and 966 deletions

View File

@@ -17,6 +17,7 @@ export const basicTrace: DemoTrace = {
{
path: "trigger/0",
timestamp: "2021-03-25T04:36:51.223693+00:00",
changed_variables: {},
},
],
"condition/0": [

View File

@@ -17,6 +17,7 @@ export const motionLightTrace: DemoTrace = {
{
path: "trigger/0",
timestamp: "2021-03-25T04:36:51.223693+00:00",
changed_variables: {},
},
],
"action/0": [

View File

@@ -55,6 +55,7 @@ export class DemoAutomationTraceTimeline extends LitElement {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
}
static get styles() {

View File

@@ -60,6 +60,7 @@ export class DemoAutomationTrace extends LitElement {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
hass.updateTranslations("config", "en");
}
static get styles() {

View File

@@ -72,6 +72,7 @@
"@material/mwc-radio": "0.27.0",
"@material/mwc-ripple": "0.27.0",
"@material/mwc-select": "0.27.0",
"@material/mwc-snackbar": "0.27.0",
"@material/mwc-switch": "0.27.0",
"@material/mwc-tab": "0.27.0",
"@material/mwc-tab-bar": "0.27.0",
@@ -86,11 +87,10 @@
"@polymer/paper-item": "3.0.1",
"@polymer/paper-listbox": "3.0.1",
"@polymer/paper-tabs": "3.1.0",
"@polymer/paper-toast": "3.0.1",
"@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.3.6",
"@vaadin/vaadin-themable-mixin": "24.3.6",
"@vaadin/combo-box": "24.3.7",
"@vaadin/vaadin-themable-mixin": "24.3.7",
"@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@@ -110,7 +110,7 @@
"element-internals-polyfill": "1.3.10",
"fuse.js": "7.0.0",
"google-timezones-json": "1.2.0",
"hls.js": "1.5.6",
"hls.js": "1.5.7",
"home-assistant-js-websocket": "9.1.0",
"idb-keyval": "6.2.1",
"intl-messageformat": "10.5.11",
@@ -159,8 +159,8 @@
"@bundle-stats/plugin-webpack-filter": "4.10.1",
"@koa/cors": "5.0.0",
"@lokalise/node-api": "12.1.0",
"@octokit/auth-oauth-device": "6.0.1",
"@octokit/plugin-retry": "6.0.1",
"@octokit/auth-oauth-device": "7.0.0",
"@octokit/plugin-retry": "7.0.1",
"@octokit/rest": "20.0.2",
"@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.4",
@@ -185,8 +185,8 @@
"@types/tar": "6.1.11",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "7.0.2",
"@typescript-eslint/parser": "7.0.2",
"@typescript-eslint/eslint-plugin": "7.1.0",
"@typescript-eslint/parser": "7.1.0",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3",
@@ -224,7 +224,7 @@
"map-stream": "0.0.7",
"mocha": "10.3.0",
"object-hash": "3.0.0",
"open": "10.0.3",
"open": "10.0.4",
"pinst": "3.0.0",
"prettier": "3.2.5",
"rollup": "2.79.1",

View File

@@ -82,6 +82,9 @@ export class HaSortable extends LitElement {
public connectedCallback() {
super.connectedCallback();
this._shouldBeDestroy = false;
if (this.hasUpdated) {
this.requestUpdate();
}
}
protected createRenderRoot() {

View File

@@ -1,35 +1,8 @@
import "@polymer/paper-toast/paper-toast";
import type { PaperToastElement } from "@polymer/paper-toast/paper-toast";
import { customElement } from "lit/decorators";
import type { Constructor } from "../types";
const PaperToast = customElements.get(
"paper-toast"
) as Constructor<PaperToastElement>;
import { Snackbar } from "@material/mwc-snackbar/mwc-snackbar";
@customElement("ha-toast")
export class HaToast extends PaperToast {
private _resizeListener?: (obj: { matches: boolean }) => unknown;
private _mediaq?: MediaQueryList;
public connectedCallback() {
super.connectedCallback();
if (!this._resizeListener) {
this._resizeListener = (ev) =>
this.classList.toggle("fit-bottom", ev.matches);
this._mediaq = window.matchMedia("(max-width: 599px");
}
this._mediaq!.addListener(this._resizeListener);
this._resizeListener(this._mediaq!);
}
public disconnectedCallback() {
super.disconnectedCallback();
this._mediaq!.removeListener(this._resizeListener!);
}
}
export class HaToast extends Snackbar {}
declare global {
interface HTMLElementTagNameMap {

View File

@@ -163,21 +163,22 @@ export class HaTracePathDetails extends LitElement {
}
)}
<br />
${error
? html`<div class="error">
${this.hass!.localize(
"ui.panel.config.automation.trace.path.error",
{
error: error,
}
)}
</div>`
: nothing}
${result
? html`${this.hass!.localize(
"ui.panel.config.automation.trace.path.result"
)}
<pre>${dump(result)}</pre>`
: error
? html`<div class="error">
${this.hass!.localize(
"ui.panel.config.automation.trace.path.error",
{
error: error,
}
)}
</div>`
: nothing}
: nothing}
${Object.keys(rest).length === 0
? nothing
: html`<pre>${dump(rest)}</pre>`}

View File

@@ -1,15 +1,16 @@
import { mdiExclamationThick } from "@mdi/js";
import {
css,
LitElement,
PropertyValues,
html,
TemplateResult,
svg,
css,
html,
nothing,
svg,
} from "lit";
import { customElement, property } from "lit/decorators";
import { NODE_SIZE, SPACING } from "./hat-graph-const";
import { isSafari } from "../../util/is_safari";
import { NODE_SIZE, SPACING } from "./hat-graph-const";
/**
* @attribute active
@@ -21,6 +22,8 @@ export class HatGraphNode extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false;
@property({ type: Boolean }) public error = false;
@property({ reflect: true, type: Boolean }) notEnabled = false;
@property({ reflect: true, type: Boolean }) graphStart = false;
@@ -65,16 +68,28 @@ export class HatGraphNode extends LitElement {
`}
<g class="node">
<circle cx="0" cy="0" r=${NODE_SIZE / 2} />
${this.error
? svg`
<g class="error">
<circle
cx="-12"
cy=${-NODE_SIZE / 2}
r="8"
></circle>
<path transform="translate(-18 -21) scale(.5)" class="exclamation" d=${mdiExclamationThick}/>
</g>
`
: nothing}
${this.badge
? svg`
<g class="number">
<circle
cx="8"
cx="12"
cy=${-NODE_SIZE / 2}
r="8"
></circle>
<text
x="8"
x="12"
y=${-NODE_SIZE / 2}
text-anchor="middle"
alignment-baseline="middle"
@@ -82,7 +97,7 @@ export class HatGraphNode extends LitElement {
</g>
`
: nothing}
<g style="pointer-events: none" transform="translate(${-12} ${-12})">
<g style="pointer-events: none" transform="translate(-12 -12)">
${this.iconPath
? svg`<path class="icon" d=${this.iconPath}/>`
: svg`<foreignObject><span class="icon"><slot name="icon"></slot></span></foreignObject>`}
@@ -143,13 +158,22 @@ export class HatGraphNode extends LitElement {
fill: var(--background-clr);
stroke: var(--circle-clr, var(--stroke-clr));
}
.error circle {
fill: var(--error-color);
stroke: none;
stroke-width: 0;
}
.error .exclamation {
fill: var(--text-primary-color);
}
.number circle {
fill: var(--track-clr);
stroke: none;
stroke-width: 0;
}
.number text {
font-size: smaller;
font-size: 10px;
fill: var(--text-primary-color);
}
path.icon {
fill: var(--icon-clr);

View File

@@ -93,6 +93,7 @@ export class HatScriptGraph extends LitElement {
?active=${this.selected === path}
.iconPath=${mdiAsterisk}
.notEnabled=${config.enabled === false}
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
tabindex=${track ? "0" : "-1"}
></hat-graph-node>
`;
@@ -171,6 +172,7 @@ export class HatScriptGraph extends LitElement {
?track=${trace !== undefined}
?active=${this.selected === path}
.notEnabled=${disabled || config.enabled === false}
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
slot="head"
nofocus
></hat-graph-node>
@@ -424,6 +426,7 @@ export class HatScriptGraph extends LitElement {
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
>
${node.service
@@ -451,6 +454,7 @@ export class HatScriptGraph extends LitElement {
?track=${path in this.trace.trace}
?active=${this.selected === path}
.notEnabled=${disabled || node.enabled === false}
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
tabindex=${this.trace && path in this.trace.trace ? "0" : "-1"}
></hat-graph-node>
`;
@@ -517,6 +521,7 @@ export class HatScriptGraph extends LitElement {
@focus=${this.selectNode(node, path)}
?track=${path in this.trace.trace}
?active=${this.selected === path}
.error=${this.trace.trace[path]?.some((tr) => tr.error)}
.notEnabled=${disabled || node.enabled === false}
></hat-graph-node>
`;

View File

@@ -153,7 +153,7 @@ class LogbookRenderer {
const parts: TemplateResult[] = [];
let i;
let i: number;
for (
i = 0;
@@ -232,7 +232,7 @@ class ActionRenderer {
const value = this._getItem(index);
if (renderAllIterations) {
let i;
let i: number = 0;
value.forEach((item) => {
i = this._renderIteration(index, item, actionType);
});
@@ -270,7 +270,12 @@ class ActionRenderer {
} catch (err: any) {
this._renderEntry(
path,
`Unable to extract path ${path}. Download trace and report as bug`
this.hass.localize(
"ui.panel.config.automation.trace.messages.path_error",
{
path: path,
}
)
);
return index + 1;
}
@@ -324,20 +329,22 @@ class ActionRenderer {
private _handleTrigger(index: number, triggerStep: TriggerTraceStep): number {
this._renderEntry(
triggerStep.path,
`${
triggerStep.changed_variables.trigger.alias
? `${triggerStep.changed_variables.trigger.alias} triggered`
: "Triggered"
} ${
triggerStep.path === "trigger"
? "manually"
: `by the ${this.trace.trigger}`
} at
${formatDateTimeWithSeconds(
new Date(triggerStep.timestamp),
this.hass.locale,
this.hass.config
)}`,
this.hass.localize(
"ui.panel.config.automation.trace.messages.triggered_by",
{
triggeredBy: triggerStep.changed_variables.trigger?.alias
? "alias"
: "other",
alias: triggerStep.changed_variables.trigger?.alias,
triggeredPath: triggerStep.path === "trigger" ? "manual" : "trigger",
trigger: this.trace.trigger,
time: formatDateTimeWithSeconds(
new Date(triggerStep.timestamp),
this.hass.locale,
this.hass.config
),
}
),
mdiCircle
);
return index + 1;
@@ -367,12 +374,17 @@ class ActionRenderer {
this.keys[index]
) as ChooseAction;
const disabled = chooseConfig.enabled === false;
const name = chooseConfig.alias || "Choose";
const name =
chooseConfig.alias ||
this.hass.localize("ui.panel.config.automation.trace.messages.choose");
if (defaultExecuted) {
this._renderEntry(
choosePath,
`${name}: Default action executed`,
this.hass.localize(
"ui.panel.config.automation.trace.messages.default_action_executed",
{ name: name }
),
undefined,
disabled
);
@@ -385,8 +397,17 @@ class ActionRenderer {
`${this.keys[index]}/choose/${chooseTrace.result.choice}`
) as ChooseActionChoice | undefined;
const choiceName = choiceConfig
? `${choiceConfig.alias || `Option ${choiceNumeric}`} executed`
: `Error: ${chooseTrace.error}`;
? `${
choiceConfig.alias ||
this.hass.localize(
"ui.panel.config.automation.trace.messages.option_executed",
{ option: choiceNumeric }
)
}`
: this.hass.localize(
"ui.panel.config.automation.trace.messages.error",
{ error: chooseTrace.error }
);
this._renderEntry(
choosePath,
`${name}: ${choiceName}`,
@@ -396,13 +417,16 @@ class ActionRenderer {
} else {
this._renderEntry(
choosePath,
`${name}: No action taken`,
this.hass.localize(
"ui.panel.config.automation.trace.messages.no_action_executed",
{ name: name }
),
undefined,
disabled
);
}
let i;
let i: number;
// Skip over conditions
for (i = index + 1; i < this.keys.length; i++) {
@@ -479,26 +503,38 @@ class ActionRenderer {
const ifTrace = this._getItem(index)[0] as IfActionTraceStep;
const ifConfig = this._getDataFromPath(this.keys[index]) as IfAction;
const disabled = ifConfig.enabled === false;
const name = ifConfig.alias || "If";
const name =
ifConfig.alias ||
this.hass.localize("ui.panel.config.automation.trace.messages.if");
if (ifTrace.result?.choice) {
const choiceConfig = this._getDataFromPath(
`${this.keys[index]}/${ifTrace.result.choice}/`
) as any;
const choiceName = choiceConfig
? `${choiceConfig.alias || `${ifTrace.result.choice} action executed`}`
: `Error: ${ifTrace.error}`;
? choiceConfig.alias ||
this.hass.localize(
"ui.panel.config.automation.trace.messages.action_executed",
{ action: ifTrace.result.choice }
)
: this.hass.localize(
"ui.panel.config.automation.trace.messages.error",
{ error: ifTrace.error }
);
this._renderEntry(ifPath, `${name}: ${choiceName}`, undefined, disabled);
} else {
this._renderEntry(
ifPath,
`${name}: No action taken`,
this.hass.localize(
"ui.panel.config.automation.trace.messages.no_action_executed",
{ name: name }
),
undefined,
disabled
);
}
let i;
let i: number;
// Skip over conditions
for (i = index + 1; i < this.keys.length; i++) {
@@ -534,7 +570,11 @@ class ActionRenderer {
const disabled = parallelConfig.enabled === false;
const name = parallelConfig.alias || "Execute in parallel";
const name =
parallelConfig.alias ||
this.hass.localize(
"ui.panel.config.automation.trace.messages.execute_in_parallel"
);
this._renderEntry(parallelPath, name, undefined, disabled);
@@ -564,7 +604,11 @@ class ActionRenderer {
this.entries.push(html`
<ha-timeline .icon=${icon} data-path=${path} .notEnabled=${disabled}>
${description}${disabled
? html`<span class="disabled"> (disabled)</span>`
? html`<span class="disabled">
${this.hass.localize(
"ui.panel.config.automation.trace.messages.disabled"
)}</span
>`
: ""}
</ha-timeline>
`);
@@ -636,13 +680,12 @@ export class HaAutomationTracer extends LitElement {
this.hass.locale,
this.hass.config
);
const renderRuntime = () => `(runtime:
${(
const renderRuntime = () =>
(
(new Date(this.trace!.timestamp.finish!).getTime() -
new Date(this.trace!.timestamp.start).getTime()) /
1000
).toFixed(2)}
seconds)`;
).toFixed(2);
let entry: {
description: TemplateResult | string;
@@ -652,57 +695,90 @@ export class HaAutomationTracer extends LitElement {
if (this.trace.state === "running") {
entry = {
description: "Still running",
description: this.hass.localize(
"ui.panel.config.automation.trace.messages.still_running"
),
icon: mdiProgressClock,
};
} else if (this.trace.state === "debugged") {
entry = {
description: "Debugged",
description: this.hass.localize(
"ui.panel.config.automation.trace.messages.debugged"
),
icon: mdiProgressWrench,
};
} else if (this.trace.script_execution === "finished") {
entry = {
description: `Finished at ${renderFinishedAt()} ${renderRuntime()}`,
description: this.hass.localize(
"ui.panel.config.automation.trace.messages.finished",
{
time: renderFinishedAt(),
executiontime: renderRuntime(),
}
),
icon: mdiCircle,
};
} else if (this.trace.script_execution === "aborted") {
entry = {
description: `Aborted at ${renderFinishedAt()} ${renderRuntime()}`,
description: this.hass.localize(
"ui.panel.config.automation.trace.messages.aborted",
{
time: renderFinishedAt(),
executiontime: renderRuntime(),
}
),
icon: mdiAlertCircle,
};
} else if (this.trace.script_execution === "cancelled") {
entry = {
description: `Cancelled at ${renderFinishedAt()} ${renderRuntime()}`,
description: this.hass.localize(
"ui.panel.config.automation.trace.messages.cancelled",
{
time: renderFinishedAt(),
executiontime: renderRuntime(),
}
),
icon: mdiAlertCircle,
};
} else {
let reason: string;
let message:
| "stopped_failed_conditions"
| "stopped_failed_single"
| "stopped_failed_max_runs"
| "stopped_error"
| "stopped_unknown_reason";
let isError = false;
let extra: TemplateResult | undefined;
switch (this.trace.script_execution) {
case "failed_conditions":
reason = "a condition failed";
message = "stopped_failed_conditions";
break;
case "failed_single":
reason = "only a single execution is allowed";
message = "stopped_failed_single";
break;
case "failed_max_runs":
reason = "maximum number of parallel runs reached";
message = "stopped_failed_max_runs";
break;
case "error":
reason = "an error was encountered";
isError = true;
message = "stopped_error";
extra = html`<br /><br />${this.trace.error!}`;
break;
default:
reason = `of unknown reason "${this.trace.script_execution}"`;
isError = true;
message = "stopped_unknown_reason";
}
entry = {
description: html`Stopped because ${reason} at ${renderFinishedAt()}
${renderRuntime()}${extra || ""}`,
description: html`${this.hass.localize(
`ui.panel.config.automation.trace.messages.${message}`,
{
time: renderFinishedAt(),
executiontime: renderRuntime(),
}
)}
${extra || ""}`,
icon: mdiAlertCircle,
className: isError ? "error" : undefined,
};

View File

@@ -341,7 +341,7 @@ class DialogRestart extends LitElement {
showToast(this, {
message: this.hass.localize("ui.dialogs.restart.reboot.rebooting"),
duration: 0,
duration: -1,
});
try {
@@ -380,7 +380,7 @@ class DialogRestart extends LitElement {
showToast(this, {
message: this.hass.localize("ui.dialogs.restart.shutdown.shutting_down"),
duration: 0,
duration: -1,
});
try {

View File

@@ -1,10 +1,11 @@
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { property, state, query } from "lit/decorators";
import { mdiClose } from "@mdi/js";
import { computeRTL } from "../common/util/compute_rtl";
import "../components/ha-toast";
import type { HaToast } from "../components/ha-toast";
import type { HomeAssistant } from "../types";
import "../components/ha-button";
export interface ShowToastParams {
message: string;
@@ -21,72 +22,78 @@ export interface ToastActionParams {
class NotificationManager extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _action?: ToastActionParams;
@state() private _parameters?: ShowToastParams;
@state() private _noCancelOnOutsideClick = false;
@query("ha-toast") private _toast!: HaToast | undefined;
@query("ha-toast") private _toast!: HaToast;
public async showDialog({
message,
action,
duration,
dismissable,
}: ShowToastParams) {
let toast = this._toast;
// Can happen on initial load
if (!toast) {
public async showDialog(parameters: ShowToastParams) {
if (this._parameters) {
this._parameters = undefined;
await this.updateComplete;
toast = this._toast;
}
toast.setAttribute("dir", computeRTL(this.hass) ? "rtl" : "ltr");
this._action = action || undefined;
this._noCancelOnOutsideClick =
dismissable === undefined ? false : !dismissable;
toast.hide();
toast.show({
text: message,
duration: duration === undefined ? 3000 : duration,
});
if (!parameters || parameters.duration === 0) {
return;
}
this._parameters = parameters;
if (
this._parameters.duration === undefined ||
(this._parameters.duration > 0 && this._parameters.duration <= 4000)
) {
this._parameters.duration = 4000;
}
}
protected render(): TemplateResult {
public shouldUpdate(changedProperties) {
return !this._toast || changedProperties.has("_parameters");
}
private _toastClosed() {
this._parameters = undefined;
}
protected render() {
if (!this._parameters) {
return nothing;
}
return html`
<ha-toast .noCancelOnOutsideClick=${this._noCancelOnOutsideClick}>
${this._action
<ha-toast
leading
open
dir=${computeRTL(this.hass) ? "rtl" : "ltr"}
.labelText=${this._parameters.message}
.timeoutMs=${this._parameters.duration!}
@MDCSnackbar:closed=${this._toastClosed}
>
${this._parameters?.action
? html`
<mwc-button
.label=${this._action.text}
<ha-button
slot="action"
.label=${this._parameters?.action.text}
@click=${this.buttonClicked}
></mwc-button>
></ha-button>
`
: ""}
: nothing}
${this._parameters?.dismissable
? html`<ha-icon-button
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
dialogAction="close"
slot="dismiss"
></ha-icon-button>`
: nothing}
</ha-toast>
`;
}
private buttonClicked() {
this._toast.hide();
if (this._action) {
this._action.action();
this._toast?.close("action");
if (this._parameters?.action) {
this._parameters?.action.action();
}
}
static get styles(): CSSResultGroup {
return css`
ha-toast {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
}
mwc-button {
color: var(--primary-color);
font-weight: bold;
margin-left: 8px;
}
`;
}
}
customElements.define("notification-manager", NotificationManager);

View File

@@ -93,7 +93,7 @@ export class HaAutomationTrace extends LitElement {
let devButtons: TemplateResult | string = "";
if (__DEV__) {
devButtons = html`<div style="position: absolute; right: 0;">
devButtons = html`<div style="position: absolute; right: 0; z-index: 1;">
<button @click=${this._importTrace}>Import trace</button>
<button @click=${this._loadLocalStorageTrace}>Load stored trace</button>
</div>`;

View File

@@ -811,7 +811,7 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
"ui.panel.config.script.editor.id_already_exists_save_error"
),
dismissable: false,
duration: 0,
duration: -1,
action: {
action: () => {},
text: this.hass.localize("ui.dialogs.generic.ok"),

View File

@@ -95,12 +95,16 @@ export class HuiViewEditor extends LitElement {
: this._config.type || DEFAULT_VIEW_LAYOUT;
}
private get _isEmpty(): boolean {
return !this._config.sections?.length && !this._config.cards?.length;
}
protected render() {
if (!this.hass) {
return nothing;
}
const schema = this._schema(this.hass.localize, this._type, this.isNew);
const schema = this._schema(this.hass.localize, this._type, this._isEmpty);
const data = {
...this._config,
@@ -165,7 +169,7 @@ export class HuiViewEditor extends LitElement {
"ui.panel.lovelace.editor.edit_view.subview_helper"
);
case "type":
if (this.isNew) return undefined;
if (this._isEmpty) return undefined;
return this._type === "sections"
? this.hass.localize(
"ui.panel.lovelace.editor.edit_view.type_helper_others"

View File

@@ -221,7 +221,7 @@ export class LovelacePanel extends LitElement {
action: () => this._fetchConfig(false),
text: this.hass!.localize("ui.common.refresh"),
},
duration: 0,
duration: -1,
dismissable: false,
});
}

View File

@@ -132,7 +132,7 @@ class LovelaceFullConfigEditor extends LitElement {
"ui.panel.lovelace.editor.raw_editor.reload"
),
},
duration: 0,
duration: -1,
dismissable: false,
});
}

View File

@@ -2,8 +2,4 @@ export const DEFAULT_VIEW_LAYOUT = "masonry";
export const PANEL_VIEW_LAYOUT = "panel";
export const SIDEBAR_VIEW_LAYOUT = "sidebar";
export const SECTION_VIEW_LAYOUT = "sections";
export const VIEWS_NO_BADGE_SUPPORT = [
PANEL_VIEW_LAYOUT,
SIDEBAR_VIEW_LAYOUT,
SECTION_VIEW_LAYOUT,
];
export const VIEWS_NO_BADGE_SUPPORT = [PANEL_VIEW_LAYOUT, SIDEBAR_VIEW_LAYOUT];

View File

@@ -291,12 +291,6 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
padding-top: 4px;
}
.badges {
margin: 8px 16px;
font-size: 85%;
text-align: center;
}
#columns {
display: flex;
flex-direction: row;

View File

@@ -1,4 +1,11 @@
import { mdiArrowAll, mdiDelete, mdiPencil, mdiViewGridPlus } from "@mdi/js";
import {
mdiArrowAll,
mdiArrowDown,
mdiArrowUp,
mdiDelete,
mdiPencil,
mdiViewGridPlus,
} from "@mdi/js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
@@ -20,7 +27,9 @@ import {
updateLovelaceContainer,
} from "../editor/lovelace-path";
import { HuiSection } from "../sections/hui-section";
import type { Lovelace } from "../types";
import type { Lovelace, LovelaceBadge } from "../types";
import { isTouch } from "../../../util/is_touch";
import { listenMediaQuery } from "../../../common/dom/media_query";
@customElement("hui-sections-view")
export class SectionsView extends LitElement implements LovelaceViewElement {
@@ -34,8 +43,27 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
@property({ attribute: false }) public sections: HuiSection[] = [];
@property({ attribute: false }) public badges: LovelaceBadge[] = [];
@state() private _config?: LovelaceViewConfig;
@state() private _narrow = false;
private _unsubMql?: () => void;
public connectedCallback() {
super.connectedCallback();
this._unsubMql = listenMediaQuery("(max-width: 600px)", (matches) => {
this._narrow = matches;
});
}
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubMql?.();
this._unsubMql = undefined;
}
public setConfig(config: LovelaceViewConfig): void {
this._config = config;
}
@@ -56,9 +84,14 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
const editMode = this.lovelace.editMode;
const supportDnD = !(isTouch && this._narrow);
return html`
${this.badges.length > 0
? html`<div class="badges">${this.badges}</div>`
: ""}
<ha-sortable
.disabled=${!editMode}
.disabled=${!editMode && !supportDnD}
@item-moved=${this._sectionMoved}
group="section"
handle-selector=".handle"
@@ -68,8 +101,8 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
<div
class="container"
style=${styleMap({
"--cell-count": String(
(this._config?.sections?.length ?? 0) + (editMode ? 1 : 0)
"--section-count": String(
sectionsConfig.length + (editMode ? 1 : 0)
),
})}
>
@@ -85,11 +118,28 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
? html`
<div class="section-overlay">
<div class="section-actions">
<ha-svg-icon
aria-hidden="true"
class="handle"
.path=${mdiArrowAll}
></ha-svg-icon>
${supportDnD
? html`
<ha-svg-icon
aria-hidden="true"
class="handle"
.path=${mdiArrowAll}
></ha-svg-icon>
`
: html`
<ha-icon-button
.label=${"Down"}
@click=${this._moveDown}
.index=${idx}
.path=${mdiArrowDown}
></ha-icon-button>
<ha-icon-button
.label=${"Up"}
@click=${this._moveUp}
.index=${idx}
.path=${mdiArrowUp}
></ha-icon-button>
`}
<ha-icon-button
.label=${this.hass.localize("ui.common.edit")}
@click=${this._editSection}
@@ -226,38 +276,70 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
this.lovelace!.saveConfig(newConfig);
}
private _moveDown(ev) {
ev.stopPropagation();
const { index } = ev.currentTarget;
const newConfig = moveSection(
this.lovelace!.config,
[this.index!, index],
[this.index!, index + 1]
);
this.lovelace!.saveConfig(newConfig);
}
private _moveUp(ev) {
ev.stopPropagation();
const { index } = ev.currentTarget;
const newConfig = moveSection(
this.lovelace!.config,
[this.index!, index],
[this.index!, index - 1]
);
this.lovelace!.saveConfig(newConfig);
}
static get styles(): CSSResultGroup {
return css`
:host {
display: block;
}
.badges {
margin: 12px 8px 16px 8px;
font-size: 85%;
text-align: center;
}
.section {
position: relative;
border-radius: var(--ha-card-border-radius, 12px);
}
.container {
/* Inputs */
--grid-gap: 20px;
--grid-max-width: 1400px;
--grid-cell-max-width: 500px;
--grid-cell-min-width: 320px;
--grid-max-section-count: 4;
--grid-section-min-width: 320px;
/* Calculated */
--max-count: min(var(--section-count), var(--grid-max-section-count));
--grid-max-width: calc(
(var(--max-count) + 1) * var(--grid-section-min-width) +
(var(--max-count) + 2) * var(--grid-gap) - 1px
);
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(var(--grid-cell-min-width), 1fr)
minmax(var(--grid-section-min-width), 1fr)
);
grid-gap: 8px var(--grid-gap);
justify-content: center;
gap: 8px var(--grid-gap);
padding: var(--grid-gap);
box-sizing: border-box;
max-width: min(
calc(
var(--cell-count) * (var(--grid-cell-max-width) + var(--grid-gap)) +
var(--grid-gap)
),
var(--grid-max-width)
);
max-width: var(--grid-max-width);
margin: 0 auto;
}
@@ -270,7 +352,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
.section-actions {
position: absolute;
top: 0;
top: 20px;
right: 0;
opacity: 1;
display: flex;

View File

@@ -38,7 +38,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
message:
this.hass!.localize("ui.notification_toast.starting") ||
"Home Assistant is starting, not everything will be available until it is finished.",
duration: 0,
duration: -1,
dismissable: false,
action: {
text:
@@ -97,7 +97,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
}
showToast(this, {
message: "",
duration: 1,
duration: 0,
});
}
@@ -108,7 +108,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
this._disconnectedTimeout = undefined;
showToast(this, {
message: this.hass!.localize("ui.notification_toast.connection_lost"),
duration: 0,
duration: -1,
dismissable: false,
});
}, 1000);
@@ -124,7 +124,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
message:
this.hass!.localize("ui.notification_toast.wrapping_up_startup") ||
`Wrapping up startup, not everything will be available until it is finished.`,
duration: 0,
duration: -1,
dismissable: false,
action: {
text:
@@ -148,7 +148,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
integration: domainToName(this.hass!.localize, integration),
}) ||
`Starting ${integration}, not everything will be available until it is finished.`,
duration: 0,
duration: -1,
dismissable: false,
action: {
text:

View File

@@ -3195,6 +3195,29 @@
"no_logbook_entries": "No Logbook entries found for this step.",
"no_variables_changed": "No variables changed",
"unable_to_find_config": "Unable to find config"
},
"messages": {
"no_action_executed": "{name}: No action executed",
"default_action_executed": "{name}: Default action executed",
"action_executed": "{action} action executed",
"option_executed": "Option {option} executed",
"error": "Error: {error}",
"execute_in_parallel": "Execute in parallel",
"if": "If",
"choose": "Choose",
"still_running": "Still running",
"debugged": "Debugged",
"finished": "Finished at {time} (runtime: {executiontime} seconds)",
"aborted": "Aborted at {time} (runtime: {executiontime} seconds)",
"cancelled": "Cancelled at {time} (runtime: {executiontime} seconds)",
"stopped_failed_conditions": "Stopped because a condition failed at {time} (runtime: {executiontime} seconds)",
"stopped_failed_single": "Stopped because only a single execution is allowed at {time} (runtime: {executiontime} seconds)",
"stopped_failed_max_runs": "Stopped because maximum number of parallel runs reached at {time} (runtime: {executiontime} seconds)",
"stopped_error": "Stopped because an error was encountered at {time} (runtime: {executiontime} seconds)",
"stopped_unknown_reason": "Stopped because of unknown reason {reason} at {time} (runtime: {executiontime} seconds)",
"disabled": "(disabled)",
"triggered_by": "{triggeredBy, select, \n alias {{alias} triggered}\n other {Triggered} \n} {triggeredPath, select, \n trigger {by the {trigger}}\n other {manually} \n} at {time}",
"path_error": "Unable to extract path {path}. Download trace and report as bug."
}
}
},
@@ -5112,7 +5135,7 @@
"select_users": "Select which users should see this view in the navigation"
},
"type": "View type",
"type_helper_sections": "You can not change your view to use the 'sections' view type, because migration is not supported yet. Start from scratch with a new view if you want to experiment with the 'sections' view.",
"type_helper_sections": "You can not change your view to use the 'sections' view type because migration is not supported yet. Start from scratch with a new view if you want to experiment with the 'sections' view.",
"type_helper_others": "You can not change your view to an other type because migration is not supported yet. Start from scratch with a new view if you want to use another view type.",
"types": {

View File

@@ -48,7 +48,7 @@ export const registerServiceWorker = async (
action: () => installingWorker.postMessage({ type: "skipWaiting" }),
text: "reload",
},
duration: 0,
duration: -1,
dismissable: false,
});
});

1605
yarn.lock

File diff suppressed because it is too large Load Diff