mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-26 03:07:21 +00:00
Compare commits
23 Commits
color-vars
...
helpers-en
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11697f6627 | ||
|
|
648a4888a3 | ||
|
|
318452d6f6 | ||
|
|
6890823c31 | ||
|
|
ba9bab38c9 | ||
|
|
085b3884af | ||
|
|
a9c816ed9c | ||
|
|
c3201eecf3 | ||
|
|
bd67a3b90a | ||
|
|
e59ab45e60 | ||
|
|
4674384f7d | ||
|
|
2e7c95a8cc | ||
|
|
9630b175a1 | ||
|
|
be319503f7 | ||
|
|
9b1fe28018 | ||
|
|
0595f722f3 | ||
|
|
1c0315854a | ||
|
|
3b73d7c298 | ||
|
|
2955cb4956 | ||
|
|
c679e312a0 | ||
|
|
37e12a83be | ||
|
|
755c6dbb93 | ||
|
|
4a90331ac7 |
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
||||
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/autobuild@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
|
||||
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
||||
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
|
||||
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
|
||||
2
.github/workflows/nightly.yaml
vendored
2
.github/workflows/nightly.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
|
||||
|
||||
6
.github/workflows/release.yaml
vendored
6
.github/workflows/release.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
|
||||
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
||||
@@ -11,7 +11,7 @@ A compact, accessible dropdown menu for choosing actions or settings. `ha-dropdo
|
||||
### Example usage (composition)
|
||||
|
||||
```html
|
||||
<ha-dropdown open>
|
||||
<ha-dropdown>
|
||||
<ha-button slot="trigger" with-caret>Dropdown</ha-button>
|
||||
|
||||
<ha-dropdown-item>
|
||||
|
||||
@@ -28,7 +28,7 @@ export class DemoHaDropdown extends LitElement {
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-button in ${mode}">
|
||||
<div class="card-content">
|
||||
<ha-dropdown open>
|
||||
<ha-dropdown>
|
||||
<ha-button slot="trigger" with-caret>Dropdown</ha-button>
|
||||
|
||||
<ha-dropdown-item>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { genClientId } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-alert";
|
||||
import "../components/ha-button";
|
||||
@@ -59,7 +59,8 @@ export class HaAuthFlow extends LitElement {
|
||||
willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
if (!this.hasUpdated && this.clientId === genClientId()) {
|
||||
// Preselect store token when logging in to own instance
|
||||
this._storeToken = this.initStoreToken;
|
||||
}
|
||||
|
||||
@@ -117,6 +118,9 @@ export class HaAuthFlow extends LitElement {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.action ha-button {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<form>${this._renderForm()}</form>
|
||||
`;
|
||||
|
||||
@@ -597,10 +597,15 @@ export class HaChartBase extends LitElement {
|
||||
aria: { show: true },
|
||||
dataZoom: this._getDataZoomConfig(),
|
||||
toolbox: {
|
||||
top: Infinity,
|
||||
left: Infinity,
|
||||
top: Number.MAX_SAFE_INTEGER,
|
||||
left: Number.MAX_SAFE_INTEGER,
|
||||
feature: {
|
||||
dataZoom: { show: true, yAxisIndex: false, filterMode: "none" },
|
||||
dataZoom: {
|
||||
show: true,
|
||||
yAxisIndex: false,
|
||||
filterMode: "none",
|
||||
showTitle: false,
|
||||
},
|
||||
},
|
||||
iconStyle: { opacity: 0 },
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { HomeAssistant } from "../types";
|
||||
import { AudioRecorder } from "../util/audio-recorder";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import "./ha-alert";
|
||||
import "./ha-markdown";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@@ -40,7 +41,11 @@ export class HaAssistChat extends LitElement {
|
||||
|
||||
@query("#message-input") private _messageInput!: HaTextField;
|
||||
|
||||
@query("#scroll-container") private _scrollContainer!: HTMLDivElement;
|
||||
@query(".message:last-child")
|
||||
private _lastChatMessage!: LitElement;
|
||||
|
||||
@query(".message:last-child img:last-of-type")
|
||||
private _lastChatMessageImage: HTMLImageElement | undefined;
|
||||
|
||||
@state() private _conversation: AssistMessage[] = [];
|
||||
|
||||
@@ -92,10 +97,7 @@ export class HaAssistChat extends LitElement {
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._audioRecorder?.close();
|
||||
this._audioRecorder = undefined;
|
||||
this._unloadAudio();
|
||||
this._conversation = [];
|
||||
this._conversationId = null;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -112,7 +114,7 @@ export class HaAssistChat extends LitElement {
|
||||
const supportsSTT = this.pipeline?.stt_engine && !this.disableSpeech;
|
||||
|
||||
return html`
|
||||
<div class="messages" id="scroll-container">
|
||||
<div class="messages">
|
||||
${controlHA
|
||||
? nothing
|
||||
: html`
|
||||
@@ -124,11 +126,18 @@ export class HaAssistChat extends LitElement {
|
||||
`}
|
||||
<div class="spacer"></div>
|
||||
${this._conversation!.map(
|
||||
// New lines matter for messages
|
||||
// prettier-ignore
|
||||
(message) => html`
|
||||
<div class="message ${classMap({ error: !!message.error, [message.who]: true })}">${message.text}</div>
|
||||
`
|
||||
<ha-markdown
|
||||
class="message ${classMap({
|
||||
error: !!message.error,
|
||||
[message.who]: true,
|
||||
})}"
|
||||
breaks
|
||||
cache
|
||||
.content=${message.text}
|
||||
>
|
||||
</ha-markdown>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<div class="input" slot="primaryAction">
|
||||
@@ -189,12 +198,28 @@ export class HaAssistChat extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _scrollMessagesBottom() {
|
||||
const scrollContainer = this._scrollContainer;
|
||||
if (!scrollContainer) {
|
||||
return;
|
||||
private async _scrollMessagesBottom() {
|
||||
const lastChatMessage = this._lastChatMessage;
|
||||
if (!lastChatMessage.hasUpdated) {
|
||||
await lastChatMessage.updateComplete;
|
||||
}
|
||||
if (
|
||||
this._lastChatMessageImage &&
|
||||
!this._lastChatMessageImage.naturalHeight
|
||||
) {
|
||||
try {
|
||||
await this._lastChatMessageImage.decode();
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Failed to decode image:", err);
|
||||
}
|
||||
}
|
||||
const isLastMessageFullyVisible =
|
||||
lastChatMessage.getBoundingClientRect().y <
|
||||
this.getBoundingClientRect().top + 24;
|
||||
if (!isLastMessageFullyVisible) {
|
||||
lastChatMessage.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
scrollContainer.scrollTo(0, scrollContainer.scrollHeight);
|
||||
}
|
||||
|
||||
private _handleKeyUp(ev: KeyboardEvent) {
|
||||
@@ -586,42 +611,31 @@ export class HaAssistChat extends LitElement {
|
||||
flex: 1;
|
||||
}
|
||||
.message {
|
||||
white-space: pre-line;
|
||||
font-size: var(--ha-font-size-l);
|
||||
clear: both;
|
||||
max-width: -webkit-fill-available;
|
||||
overflow-wrap: break-word;
|
||||
scroll-margin-top: 24px;
|
||||
margin: 8px 0;
|
||||
padding: 8px;
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
}
|
||||
.message:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.message {
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
}
|
||||
|
||||
.message p {
|
||||
margin: 0;
|
||||
}
|
||||
.message p:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
margin-left: 24px;
|
||||
margin-inline-start: 24px;
|
||||
margin-inline-end: initial;
|
||||
align-self: flex-end;
|
||||
text-align: right;
|
||||
border-bottom-right-radius: 0px;
|
||||
--markdown-link-color: var(--text-primary-color);
|
||||
background-color: var(--chat-background-color-user, var(--primary-color));
|
||||
color: var(--text-primary-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.message.hass {
|
||||
margin-right: 24px;
|
||||
margin-inline-end: 24px;
|
||||
@@ -636,20 +650,21 @@ export class HaAssistChat extends LitElement {
|
||||
color: var(--primary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.message.user a {
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
|
||||
.message.hass a {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background-color: var(--error-color);
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
|
||||
ha-markdown {
|
||||
--markdown-image-border-radius: calc(var(--ha-border-radius-xl) / 2);
|
||||
--markdown-table-border-color: var(--divider-color);
|
||||
--markdown-code-background-color: var(--primary-background-color);
|
||||
--markdown-code-text-color: var(--primary-text-color);
|
||||
&:not(:has(ha-markdown-element)) {
|
||||
min-height: 1lh;
|
||||
min-width: 1lh;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.bouncer {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { ReactiveElement } from "lit";
|
||||
import { ReactiveElement, render, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
// eslint-disable-next-line import/extensions
|
||||
import { unsafeHTML } from "lit/directives/unsafe-html.js";
|
||||
import hash from "object-hash";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { renderMarkdown } from "../resources/render-markdown";
|
||||
import { CacheManager } from "../util/cache-manager";
|
||||
|
||||
const h = (template: ReturnType<typeof unsafeHTML>) => html`${template}`;
|
||||
|
||||
const markdownCache = new CacheManager<string>(1000);
|
||||
|
||||
const _gitHubMarkdownAlerts = {
|
||||
@@ -48,18 +52,26 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
return this;
|
||||
}
|
||||
|
||||
private _renderPromise: ReturnType<typeof this._render> = Promise.resolve();
|
||||
|
||||
protected update(changedProps) {
|
||||
super.update(changedProps);
|
||||
if (this.content !== undefined) {
|
||||
this._render();
|
||||
this._renderPromise = this._render();
|
||||
}
|
||||
}
|
||||
|
||||
protected async getUpdateComplete(): Promise<boolean> {
|
||||
await super.getUpdateComplete();
|
||||
await this._renderPromise;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected willUpdate(_changedProperties: PropertyValues): void {
|
||||
if (!this.innerHTML && this.cache) {
|
||||
const key = this._computeCacheKey();
|
||||
if (markdownCache.has(key)) {
|
||||
this.innerHTML = markdownCache.get(key)!;
|
||||
render(markdownCache.get(key)!, this.renderRoot);
|
||||
this._resize();
|
||||
}
|
||||
}
|
||||
@@ -75,7 +87,7 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
}
|
||||
|
||||
private async _render() {
|
||||
this.innerHTML = await renderMarkdown(
|
||||
const elements = await renderMarkdown(
|
||||
String(this.content),
|
||||
{
|
||||
breaks: this.breaks,
|
||||
@@ -87,6 +99,11 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
}
|
||||
);
|
||||
|
||||
render(
|
||||
elements.map((e) => h(unsafeHTML(e))),
|
||||
this.renderRoot
|
||||
);
|
||||
|
||||
this._resize();
|
||||
|
||||
const walker = document.createTreeWalker(
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
type ReactiveElement,
|
||||
type CSSResultGroup,
|
||||
} from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "./ha-markdown-element";
|
||||
|
||||
@customElement("ha-markdown")
|
||||
@@ -18,6 +25,14 @@ export class HaMarkdown extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public cache = false;
|
||||
|
||||
@query("ha-markdown-element") private _markdownElement!: ReactiveElement;
|
||||
|
||||
protected async getUpdateComplete() {
|
||||
const result = await super.getUpdateComplete();
|
||||
await this._markdownElement.updateComplete;
|
||||
return result;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.content) {
|
||||
return nothing;
|
||||
@@ -53,19 +68,46 @@ export class HaMarkdown extends LitElement {
|
||||
margin: var(--ha-space-1) 0;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
color: var(--markdown-link-color, var(--primary-color));
|
||||
}
|
||||
img {
|
||||
background-color: rgba(10, 10, 10, 0.15);
|
||||
border-radius: var(--markdown-image-border-radius);
|
||||
max-width: 100%;
|
||||
min-height: 2lh;
|
||||
height: auto;
|
||||
width: auto;
|
||||
text-indent: 4px;
|
||||
transition: height 0.2s ease-in-out;
|
||||
}
|
||||
p:first-child > img:first-child {
|
||||
vertical-align: top;
|
||||
}
|
||||
p:first-child > img:last-child {
|
||||
vertical-align: top;
|
||||
}
|
||||
ol,
|
||||
ul {
|
||||
list-style-position: inside;
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
li {
|
||||
&:has(input[type="checkbox"]) {
|
||||
list-style: none;
|
||||
& > input[type="checkbox"] {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
svg {
|
||||
background-color: var(--markdown-svg-background-color, none);
|
||||
color: var(--markdown-svg-color, none);
|
||||
}
|
||||
code,
|
||||
pre {
|
||||
background-color: var(--markdown-code-background-color, none);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
svg {
|
||||
background-color: var(--markdown-svg-background-color, none);
|
||||
color: var(--markdown-svg-color, none);
|
||||
color: var(--markdown-code-text-color, inherit);
|
||||
}
|
||||
code {
|
||||
font-size: var(--ha-font-size-s);
|
||||
@@ -97,6 +139,24 @@ export class HaMarkdown extends LitElement {
|
||||
border-bottom: none;
|
||||
margin: var(--ha-space-4) 0;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
th {
|
||||
text-align: start;
|
||||
}
|
||||
td,
|
||||
th {
|
||||
border: 1px solid var(--markdown-table-border-color, transparent);
|
||||
padding: 0.25em 0.5em;
|
||||
}
|
||||
blockquote {
|
||||
border-left: 4px solid var(--divider-color);
|
||||
margin-inline: 0;
|
||||
padding-inline: 1em;
|
||||
}
|
||||
` as CSSResultGroup;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { formatTime } from "../common/datetime/format_time";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import { fileDownload } from "../util/file_download";
|
||||
import { handleFetchPromise } from "../util/hass-call-api";
|
||||
import type { BackupManagerState, ManagerStateEvent } from "./backup_manager";
|
||||
@@ -414,7 +415,7 @@ ${hass.auth.data.hassUrl}
|
||||
${hass.localize("ui.panel.config.backup.emergency_kit_file.encryption_key")}
|
||||
${encryptionKey}
|
||||
|
||||
${hass.localize("ui.panel.config.backup.emergency_kit_file.more_info", { link: "https://www.home-assistant.io/more-info/backup-emergency-kit" })}`);
|
||||
${hass.localize("ui.panel.config.backup.emergency_kit_file.more_info", { link: documentationUrl(hass, "/more-info/backup-emergency-kit") })}`);
|
||||
|
||||
export const geneateEmergencyKitFileName = (
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Connection } from "home-assistant-js-websocket";
|
||||
export interface CoreFrontendUserData {
|
||||
showAdvanced?: boolean;
|
||||
showEntityIdPicker?: boolean;
|
||||
defaultPanel?: string;
|
||||
default_panel?: string;
|
||||
}
|
||||
|
||||
export interface SidebarFrontendUserData {
|
||||
@@ -12,7 +12,11 @@ export interface SidebarFrontendUserData {
|
||||
}
|
||||
|
||||
export interface CoreFrontendSystemData {
|
||||
defaultPanel?: string;
|
||||
default_panel?: string;
|
||||
}
|
||||
|
||||
export interface HomeFrontendSystemData {
|
||||
favorite_entities?: string[];
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -22,6 +26,7 @@ declare global {
|
||||
}
|
||||
interface FrontendSystemData {
|
||||
core: CoreFrontendSystemData;
|
||||
home: HomeFrontendSystemData;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ export const getLegacyDefaultPanelUrlPath = (): string | null => {
|
||||
};
|
||||
|
||||
export const getDefaultPanelUrlPath = (hass: HomeAssistant): string =>
|
||||
hass.userData?.defaultPanel ||
|
||||
hass.systemData?.defaultPanel ||
|
||||
hass.userData?.default_panel ||
|
||||
hass.systemData?.default_panel ||
|
||||
getLegacyDefaultPanelUrlPath() ||
|
||||
DEFAULT_PANEL;
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
import { showToast } from "../../util/toast";
|
||||
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-more-info-add-to")
|
||||
export class HaMoreInfoAddTo extends LitElement {
|
||||
@@ -51,6 +52,7 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
app_payload: action.app_payload,
|
||||
},
|
||||
});
|
||||
fireEvent(this, "add-to-action-selected");
|
||||
} catch (err: any) {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
@@ -149,4 +151,8 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-add-to": HaMoreInfoAddTo;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"add-to-action-selected": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,6 +645,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
<ha-more-info-add-to
|
||||
.hass=${this.hass}
|
||||
.entityId=${entityId}
|
||||
@add-to-action-selected=${this._goBack}
|
||||
></ha-more-info-add-to>
|
||||
`
|
||||
: nothing
|
||||
|
||||
@@ -5,6 +5,7 @@ import { atLeastVersion } from "../common/config/version";
|
||||
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
|
||||
import "../components/ha-card";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./hass-subpage";
|
||||
|
||||
@@ -57,7 +58,7 @@ class SupervisorErrorScreen extends LitElement {
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.home-assistant.io/help/"
|
||||
href=${documentationUrl(this.hass, "/help/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-card";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { showAppDialog } from "./dialogs/show-app-dialog";
|
||||
import { showCommunityDialog } from "./dialogs/show-community-dialog";
|
||||
@@ -22,7 +23,10 @@ class OnboardingWelcomeLinks extends LitElement {
|
||||
return html`<a
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://www.home-assistant.io/blog/2016/01/19/perfect-home-automation/"
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/blog/2016/01/19/perfect-home-automation/"
|
||||
)}
|
||||
>
|
||||
<onboarding-welcome-link
|
||||
noninteractive
|
||||
|
||||
@@ -188,6 +188,7 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
class="handle ${this._resizing ? "resizing" : ""}"
|
||||
@mousedown=${this._handleMouseDown}
|
||||
@touchstart=${this._handleMouseDown}
|
||||
@dblclick=${this._handleDoubleClick}
|
||||
@focus=${this._startKeyboardResizing}
|
||||
@blur=${this._stopKeyboardResizing}
|
||||
tabindex="0"
|
||||
@@ -258,6 +259,17 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
);
|
||||
};
|
||||
|
||||
private _handleDoubleClick = (ev: MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this._unregisterResizeHandlers();
|
||||
this._tinykeysUnsub?.();
|
||||
this._tinykeysUnsub = undefined;
|
||||
this._resizing = false;
|
||||
document.body.style.removeProperty("cursor");
|
||||
fireEvent(this, "sidebar-reset-size");
|
||||
};
|
||||
|
||||
private _startResizing(clientX: number) {
|
||||
// register event listeners for drag handling
|
||||
document.addEventListener("mousemove", this._handleMouseMove);
|
||||
@@ -422,5 +434,6 @@ declare global {
|
||||
deltaInPx: number;
|
||||
};
|
||||
"sidebar-resizing-stopped": undefined;
|
||||
"sidebar-reset-size": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +317,7 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
@value-changed=${this._sidebarConfigChanged}
|
||||
@sidebar-resized=${this._resizeSidebar}
|
||||
@sidebar-resizing-stopped=${this._stopResizeSidebar}
|
||||
@sidebar-reset-size=${this._resetSidebarWidth}
|
||||
></ha-automation-sidebar>
|
||||
</div>
|
||||
</div>
|
||||
@@ -700,6 +701,16 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
this._prevSidebarWidthPx = undefined;
|
||||
}
|
||||
|
||||
private _resetSidebarWidth(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._prevSidebarWidthPx = undefined;
|
||||
this._sidebarWidthPx = SIDEBAR_DEFAULT_WIDTH;
|
||||
this.style.setProperty(
|
||||
"--sidebar-dynamic-width",
|
||||
`${this._sidebarWidthPx}px`
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
saveFabStyles,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { mdiClose, mdiOpenInNew } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-code-editor";
|
||||
@@ -140,7 +141,7 @@ class DialogImportBlueprint extends LitElement {
|
||||
<ha-button
|
||||
size="small"
|
||||
appearance="plain"
|
||||
href="https://www.home-assistant.io/get-blueprints"
|
||||
href=${documentationUrl(this.hass, "/get-blueprints")}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
} from "../../../data/blueprint";
|
||||
import { showScriptEditor } from "../../../data/script";
|
||||
import { findRelated } from "../../../data/search";
|
||||
import "../../../components/chips/ha-assist-chip";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@@ -60,6 +61,7 @@ type BlueprintMetaDataPath = BlueprintMetaData & {
|
||||
error: boolean;
|
||||
type: "automation" | "script";
|
||||
fullpath: string;
|
||||
usageCount?: number;
|
||||
};
|
||||
|
||||
const createNewFunctions = {
|
||||
@@ -128,14 +130,20 @@ class HaBlueprintOverview extends LitElement {
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
@state() private _usageCounts: Record<string, number> = {};
|
||||
|
||||
private _usageCountRequest = 0;
|
||||
|
||||
private _processedBlueprints = memoizeOne(
|
||||
(
|
||||
blueprints: Record<string, Blueprints>,
|
||||
localize: LocalizeFunc
|
||||
localize: LocalizeFunc,
|
||||
usageCounts: Record<string, number>
|
||||
): BlueprintMetaDataPath[] => {
|
||||
const result: any[] = [];
|
||||
Object.entries(blueprints).forEach(([type, typeBlueprints]) =>
|
||||
Object.entries(typeBlueprints).forEach(([path, blueprint]) => {
|
||||
const fullpath = `${type}/${path}`;
|
||||
if ("error" in blueprint) {
|
||||
result.push({
|
||||
name: blueprint.error,
|
||||
@@ -145,7 +153,8 @@ class HaBlueprintOverview extends LitElement {
|
||||
),
|
||||
error: true,
|
||||
path,
|
||||
fullpath: `${type}/${path}`,
|
||||
fullpath,
|
||||
usageCount: 0,
|
||||
});
|
||||
} else {
|
||||
result.push({
|
||||
@@ -156,7 +165,8 @@ class HaBlueprintOverview extends LitElement {
|
||||
),
|
||||
error: false,
|
||||
path,
|
||||
fullpath: `${type}/${path}`,
|
||||
fullpath,
|
||||
usageCount: usageCounts[fullpath] || 0,
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -189,6 +199,34 @@ class HaBlueprintOverview extends LitElement {
|
||||
filterable: true,
|
||||
flex: 2,
|
||||
},
|
||||
usage_count: {
|
||||
title: localize(
|
||||
"ui.panel.config.blueprint.overview.headers.usage_count"
|
||||
),
|
||||
sortable: true,
|
||||
valueColumn: "usageCount",
|
||||
type: "numeric",
|
||||
minWidth: "100px",
|
||||
maxWidth: "120px",
|
||||
template: (blueprint) => {
|
||||
const count = blueprint.usageCount ?? 0;
|
||||
return html`
|
||||
<ha-assist-chip
|
||||
filled
|
||||
.active=${count > 0}
|
||||
label=${String(count)}
|
||||
title=${blueprint.error
|
||||
? String(count)
|
||||
: this.hass.localize(
|
||||
`ui.panel.config.blueprint.overview.view_${blueprint.type}`
|
||||
)}
|
||||
?disabled=${blueprint.error}
|
||||
data-fullpath=${blueprint.fullpath}
|
||||
@click=${this._handleUsageClick}
|
||||
></ha-assist-chip>
|
||||
`;
|
||||
},
|
||||
},
|
||||
fullpath: {
|
||||
title: "fullpath",
|
||||
hidden: true,
|
||||
@@ -266,6 +304,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._loadUsageCounts();
|
||||
if (this.route.path === "/import") {
|
||||
const url = extractSearchParam("blueprint_url");
|
||||
navigate("/config/blueprint/dashboard", { replace: true });
|
||||
@@ -275,6 +314,13 @@ class HaBlueprintOverview extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("blueprints")) {
|
||||
this._loadUsageCounts();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
@@ -284,7 +330,11 @@ class HaBlueprintOverview extends LitElement {
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${this._processedBlueprints(this.blueprints, this.hass.localize)}
|
||||
.data=${this._processedBlueprints(
|
||||
this.blueprints,
|
||||
this.hass.localize,
|
||||
this._usageCounts
|
||||
)}
|
||||
id="fullpath"
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.config.blueprint.overview.no_blueprints"
|
||||
@@ -299,7 +349,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
href="https://www.home-assistant.io/get-blueprints"
|
||||
href=${documentationUrl(this.hass, "/get-blueprints")}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
size="small"
|
||||
@@ -380,10 +430,51 @@ class HaBlueprintOverview extends LitElement {
|
||||
fireEvent(this, "reload-blueprints");
|
||||
}
|
||||
|
||||
private async _loadUsageCounts() {
|
||||
if (!this.blueprints) {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = ++this._usageCountRequest;
|
||||
const usageCounts: Record<string, number> = {};
|
||||
|
||||
const blueprintList = this._processedBlueprints(
|
||||
this.blueprints,
|
||||
this.hass.localize,
|
||||
{}
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
blueprintList.map(async (blueprint) => {
|
||||
if (blueprint.error) {
|
||||
usageCounts[blueprint.fullpath] = 0;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const related = await findRelated(
|
||||
this.hass,
|
||||
`${blueprint.domain}_blueprint`,
|
||||
blueprint.path
|
||||
);
|
||||
const count =
|
||||
(related.automation?.length || 0) + (related.script?.length || 0);
|
||||
usageCounts[blueprint.fullpath] = count;
|
||||
} catch (_err) {
|
||||
usageCounts[blueprint.fullpath] = 0;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (request === this._usageCountRequest) {
|
||||
this._usageCounts = usageCounts;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const blueprint = this._processedBlueprints(
|
||||
this.blueprints,
|
||||
this.hass.localize
|
||||
this.hass.localize,
|
||||
this._usageCounts
|
||||
).find((b) => b.fullpath === ev.detail.id)!;
|
||||
if (blueprint.error) {
|
||||
showAlertDialog(this, {
|
||||
@@ -397,6 +488,25 @@ class HaBlueprintOverview extends LitElement {
|
||||
this._createNew(blueprint);
|
||||
}
|
||||
|
||||
private _handleUsageClick = (ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
const target = ev.currentTarget as HTMLElement | null;
|
||||
const fullpath = target?.dataset.fullpath;
|
||||
if (!fullpath) {
|
||||
return;
|
||||
}
|
||||
const blueprint = this._processedBlueprints(
|
||||
this.blueprints,
|
||||
this.hass.localize,
|
||||
this._usageCounts
|
||||
).find((item) => item.fullpath === fullpath);
|
||||
if (!blueprint || blueprint.error) {
|
||||
return;
|
||||
}
|
||||
this._showUsed(blueprint);
|
||||
};
|
||||
|
||||
private _showUsed = (blueprint: BlueprintMetaDataPath) => {
|
||||
navigate(
|
||||
`/config/${blueprint.domain}/dashboard?blueprint=${encodeURIComponent(
|
||||
|
||||
@@ -2,9 +2,7 @@ import { mdiDotsVertical, mdiDownload } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import "../../../layouts/hass-subpage";
|
||||
@@ -14,6 +12,8 @@ import {
|
||||
downloadFileSupported,
|
||||
fileDownload,
|
||||
} from "../../../util/file_download";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-dropdown";
|
||||
|
||||
@customElement("ha-config-section-analytics")
|
||||
class HaConfigSectionAnalytics extends LitElement {
|
||||
@@ -33,22 +33,19 @@ class HaConfigSectionAnalytics extends LitElement {
|
||||
>
|
||||
${downloadFileSupported(this.hass)
|
||||
? html`
|
||||
<ha-button-menu
|
||||
@action=${this._handleOverflowAction}
|
||||
<ha-dropdown
|
||||
@wa-select=${this._handleOverflowAction}
|
||||
slot="toolbar-icon"
|
||||
>
|
||||
<ha-icon-button slot="trigger" .path=${mdiDotsVertical}>
|
||||
</ha-icon-button>
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiDownload}
|
||||
></ha-svg-icon>
|
||||
<ha-dropdown-item .value=${"download_device_info"}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.analytics.download_device_info"
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
`
|
||||
: nothing}
|
||||
<div class="content">
|
||||
@@ -58,9 +55,16 @@ class HaConfigSectionAnalytics extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleOverflowAction(): Promise<void> {
|
||||
const signedPath = await getSignedPath(this.hass, "/api/analytics/devices");
|
||||
fileDownload(signedPath.path);
|
||||
private async _handleOverflowAction(
|
||||
ev: CustomEvent<{ item: { value: string } }>
|
||||
): Promise<void> {
|
||||
if (ev.detail.item.value === "download_device_info") {
|
||||
const signedPath = await getSignedPath(
|
||||
this.hass,
|
||||
"/api/analytics/devices"
|
||||
);
|
||||
fileDownload(signedPath.path);
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDotsVertical, mdiRefresh } from "@mdi/js";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
@@ -6,13 +5,9 @@ import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-bar";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-metric";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import type {
|
||||
@@ -33,6 +28,9 @@ import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../dashboard/ha-config-updates";
|
||||
import { showJoinBetaDialog } from "./updates/show-dialog-join-beta";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
|
||||
@customElement("ha-config-section-updates")
|
||||
class HaConfigSectionUpdates extends LitElement {
|
||||
@@ -73,24 +71,25 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
.path=${mdiRefresh}
|
||||
@click=${this._checkUpdates}
|
||||
></ha-icon-button>
|
||||
<ha-button-menu multi>
|
||||
<ha-dropdown @wa-select=${this._handleOverflowAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-check-list-item
|
||||
left
|
||||
@request-selected=${this._toggleSkipped}
|
||||
.selected=${this._showSkipped}
|
||||
|
||||
<ha-dropdown-item
|
||||
type="checkbox"
|
||||
value="show_skipped"
|
||||
.checked=${this._showSkipped}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.updates.show_skipped")}
|
||||
</ha-check-list-item>
|
||||
</ha-dropdown-item>
|
||||
${this._supervisorInfo
|
||||
? html`
|
||||
<li divider role="separator"></li>
|
||||
<ha-list-item
|
||||
@request-selected=${this._toggleBeta}
|
||||
<wa-divider></wa-divider>
|
||||
<ha-dropdown-item
|
||||
value="toggle_beta"
|
||||
.disabled=${this._supervisorInfo.channel === "dev"}
|
||||
>
|
||||
${this._supervisorInfo.channel === "stable"
|
||||
@@ -98,10 +97,10 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.updates.leave_beta"
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
<div class="content">
|
||||
<ha-card outlined>
|
||||
@@ -133,27 +132,19 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
|
||||
}
|
||||
|
||||
private _toggleSkipped(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||
if (ev.detail.source !== "property") {
|
||||
return;
|
||||
}
|
||||
|
||||
this._showSkipped = !this._showSkipped;
|
||||
}
|
||||
|
||||
private async _toggleBeta(
|
||||
ev: CustomEvent<RequestSelectedDetail>
|
||||
private async _handleOverflowAction(
|
||||
ev: CustomEvent<{ item: { value: string } }>
|
||||
): Promise<void> {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._supervisorInfo!.channel === "stable") {
|
||||
showJoinBetaDialog(this, {
|
||||
join: async () => this._setChannel("beta"),
|
||||
});
|
||||
} else {
|
||||
this._setChannel("stable");
|
||||
if (ev.detail.item.value === "toggle_beta") {
|
||||
if (this._supervisorInfo!.channel === "stable") {
|
||||
showJoinBetaDialog(this, {
|
||||
join: async () => this._setChannel("beta"),
|
||||
});
|
||||
} else {
|
||||
this._setChannel("stable");
|
||||
}
|
||||
} else if (ev.detail.item.value === "show_skipped") {
|
||||
this._showSkipped = !this._showSkipped;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../components/ha-alert";
|
||||
import type { EnergyValidationIssue } from "../../../../data/energy";
|
||||
import { documentationUrl } from "../../../../util/documentation-url";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
@customElement("ha-energy-validation-result")
|
||||
@@ -29,7 +30,10 @@ class EnergyValidationMessage extends LitElement {
|
||||
)}
|
||||
${issue.type === "recorder_untracked"
|
||||
? html`(<a
|
||||
href="https://www.home-assistant.io/integrations/recorder#configure-filter"
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/integrations/recorder#configure-filter"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>${this.hass.localize("ui.panel.config.common.learn_more")}</a
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
mdiCancel,
|
||||
mdiChevronRight,
|
||||
mdiCog,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiMenuDown,
|
||||
mdiPencilOff,
|
||||
@@ -24,6 +25,10 @@ import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import {
|
||||
DEFAULT_ENTITY_NAME,
|
||||
type EntityNameItem,
|
||||
} from "../../../common/entity/compute_entity_name_display";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
@@ -109,10 +114,11 @@ import { configSections } from "../ha-panel-config";
|
||||
import "../integrations/ha-integration-overflow-menu";
|
||||
import { renderConfigEntryError } from "../integrations/ha-config-integration-page";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import { isHelperDomain } from "./const";
|
||||
import { isHelperDomain, type HelperDomain } from "./const";
|
||||
import { showHelperDetailDialog } from "./show-dialog-helper-detail";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { HELPERS_CRUD } from "../../../data/helpers_crud";
|
||||
import {
|
||||
fetchDiagnosticHandlers,
|
||||
getConfigEntryDiagnosticsDownloadUrl,
|
||||
@@ -120,6 +126,11 @@ import {
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
|
||||
const HELPER_ENTITY_NAME: EntityNameItem[] = [
|
||||
{ type: "area" },
|
||||
...DEFAULT_ENTITY_NAME,
|
||||
];
|
||||
|
||||
interface HelperItem {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -451,6 +462,19 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(helper.editable && helper.entity
|
||||
? [
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
path: mdiDelete,
|
||||
label: this.hass.localize("ui.common.delete"),
|
||||
warning: true,
|
||||
action: () => this._deleteHelper(helper),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
>
|
||||
</ha-icon-overflow-menu>
|
||||
@@ -490,7 +514,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
|
||||
return {
|
||||
id: entityState.entity_id,
|
||||
name: entityState.attributes.friendly_name || "",
|
||||
name: this._formatHelperName(entityState),
|
||||
entity_id: entityState.entity_id,
|
||||
editable:
|
||||
configEntry !== undefined || entityState.attributes.editable,
|
||||
@@ -569,6 +593,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
);
|
||||
|
||||
private _formatHelperName(stateObj: HassEntity): string {
|
||||
const formatted =
|
||||
this.hass.formatEntityName(stateObj, HELPER_ENTITY_NAME) || "";
|
||||
return (
|
||||
formatted || stateObj.attributes.friendly_name || stateObj.entity_id || ""
|
||||
);
|
||||
}
|
||||
|
||||
private _labelsForEntity(entityId: string): string[] {
|
||||
return (
|
||||
this.hass.entities[entityId]?.labels ||
|
||||
@@ -1280,6 +1312,62 @@ ${rejected
|
||||
}
|
||||
}
|
||||
|
||||
private async _deleteHelper(helper: HelperItem) {
|
||||
if (!helper.entity_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.helpers.picker.delete_confirm_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.helpers.picker.delete_confirm_text",
|
||||
{ name: helper.name }
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// For old-style helpers (input_boolean, etc.), use HELPERS_CRUD
|
||||
if (isHelperDomain(helper.type)) {
|
||||
const entityReg = this._entityReg.find(
|
||||
(e) => e.entity_id === helper.entity_id
|
||||
);
|
||||
if (
|
||||
!entityReg?.unique_id ||
|
||||
!isComponentLoaded(this.hass, helper.type)
|
||||
) {
|
||||
throw new Error(
|
||||
this.hass.localize("ui.panel.config.helpers.picker.delete_failed")
|
||||
);
|
||||
}
|
||||
await HELPERS_CRUD[helper.type as HelperDomain].delete(
|
||||
this.hass,
|
||||
entityReg.unique_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// For config entry-based helpers, delete the config entry
|
||||
if (helper.configEntry) {
|
||||
await deleteConfigEntry(this.hass, helper.configEntry.entry_id);
|
||||
}
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
text:
|
||||
err.message ||
|
||||
this.hass.localize("ui.panel.config.helpers.picker.delete_failed"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _createHelper() {
|
||||
showHelperDetailDialog(this, {});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { mdiBookshelf, mdiCog, mdiDotsVertical, mdiOpenInNew } from "@mdi/js";
|
||||
import {
|
||||
mdiBookshelf,
|
||||
mdiCog,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiOpenInNew,
|
||||
} from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@@ -7,6 +13,11 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-list-item";
|
||||
import {
|
||||
deleteApplicationCredential,
|
||||
fetchApplicationCredentialsConfigEntry,
|
||||
} from "../../../data/application_credential";
|
||||
import { deleteConfigEntry } from "../../../data/config_entries";
|
||||
import {
|
||||
ATTENTION_SOURCES,
|
||||
DISCOVERY_SOURCES,
|
||||
@@ -15,7 +26,10 @@ import {
|
||||
} from "../../../data/config_flow";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
|
||||
@@ -60,7 +74,7 @@ export class HaConfigFlowCard extends LitElement {
|
||||
: "ui.common.add"
|
||||
)}
|
||||
</ha-button>
|
||||
${this.flow.context.configuration_url || this.manifest
|
||||
${this.flow.context.configuration_url || this.manifest || attention
|
||||
? html`<ha-button-menu slot="header-button">
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
@@ -118,6 +132,22 @@ export class HaConfigFlowCard extends LitElement {
|
||||
</ha-list-item>
|
||||
</a>`
|
||||
: ""}
|
||||
${attention
|
||||
? html`<ha-list-item
|
||||
class="warning"
|
||||
graphic="icon"
|
||||
@click=${this._handleDelete}
|
||||
>
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="graphic"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete"
|
||||
)}
|
||||
</ha-list-item>`
|
||||
: ""}
|
||||
</ha-button-menu>`
|
||||
: ""}
|
||||
</ha-integration-action-card>
|
||||
@@ -175,6 +205,109 @@ export class HaConfigFlowCard extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
// Return an application credentials id for this config entry to prompt the
|
||||
// user for removal. This is best effort so we don't stop overall removal
|
||||
// if the integration isn't loaded or there is some other error.
|
||||
private async _fetchApplicationCredentials(entryId: string) {
|
||||
try {
|
||||
return (await fetchApplicationCredentialsConfigEntry(this.hass, entryId))
|
||||
.application_credentials_id;
|
||||
} catch (_err: any) {
|
||||
// We won't prompt the user to remove credentials
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async _removeApplicationCredential(applicationCredentialsId: string) {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.application_credentials.delete_title"
|
||||
),
|
||||
text: html`${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.application_credentials.delete_prompt"
|
||||
)},
|
||||
<br />
|
||||
<br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.application_credentials.delete_detail"
|
||||
)}
|
||||
<br />
|
||||
<br />
|
||||
<a
|
||||
href="https://www.home-assistant.io/integrations/application_credentials"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.application_credentials.learn_more"
|
||||
)}
|
||||
</a>`,
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteApplicationCredential(this.hass, applicationCredentialsId);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.application_credentials.delete_error_title"
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleDelete() {
|
||||
const entryId = this.flow.context.entry_id;
|
||||
|
||||
if (!entryId) {
|
||||
// This shouldn't happen for reauth flows, but handle gracefully
|
||||
return;
|
||||
}
|
||||
|
||||
const applicationCredentialsId =
|
||||
await this._fetchApplicationCredentials(entryId);
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete_confirm_title",
|
||||
{ title: localizeConfigFlowTitle(this.hass.localize, this.flow) }
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete_confirm_text"
|
||||
),
|
||||
confirmText: this.hass!.localize("ui.common.delete"),
|
||||
dismissText: this.hass!.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await deleteConfigEntry(this.hass, entryId);
|
||||
|
||||
if (result.require_restart) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.restart_confirm"
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (applicationCredentialsId) {
|
||||
this._removeApplicationCredential(applicationCredentialsId);
|
||||
}
|
||||
|
||||
this._handleFlowUpdated();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
a {
|
||||
text-decoration: none;
|
||||
@@ -191,6 +324,9 @@ export class HaConfigFlowCard extends LitElement {
|
||||
--mdc-theme-primary: var(--error-color);
|
||||
--ha-card-border-color: var(--error-color);
|
||||
}
|
||||
.warning {
|
||||
--mdc-theme-text-primary-on-background: var(--error-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -295,7 +295,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
color:
|
||||
route.route_status === "Active"
|
||||
? primaryColor
|
||||
: style.getPropertyValue("--disabled-color"),
|
||||
: style.getPropertyValue("--dark-primary-color"),
|
||||
type: ["Child", "Parent"].includes(neighbor.relationship)
|
||||
? "solid"
|
||||
: "dotted",
|
||||
@@ -335,7 +335,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
symbolSize: 5,
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
color: style.getPropertyValue("--disabled-color"),
|
||||
color: style.getPropertyValue("--dark-primary-color"),
|
||||
type: "dotted",
|
||||
},
|
||||
ignoreForceLayout: true,
|
||||
|
||||
@@ -204,7 +204,7 @@ export class DialogLabsPreviewFeatureEnable
|
||||
--md-list-item-trailing-space: var(--ha-space-6);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
border-top: var(--ha-border-width-sm) solid var(--divider-color);
|
||||
}
|
||||
|
||||
div[slot="actions"] > div {
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { HomeAssistant } from "../../../types";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { showLabsPreviewFeatureEnableDialog } from "./show-dialog-labs-preview-feature-enable";
|
||||
import {
|
||||
@@ -100,7 +101,7 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
? html`
|
||||
<a
|
||||
slot="toolbar-icon"
|
||||
href="https://www.home-assistant.io/integrations/labs/"
|
||||
href=${documentationUrl(this.hass, "/integrations/labs/")}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
.title=${this.hass.localize("ui.common.help")}
|
||||
@@ -124,7 +125,7 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.labs.empty.description"
|
||||
)}
|
||||
<a
|
||||
href="https://www.home-assistant.io/integrations/labs/"
|
||||
href=${documentationUrl(this.hass, "/integrations/labs/")}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@@ -387,7 +388,7 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
.content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 16px;
|
||||
padding: var(--ha-space-4);
|
||||
min-height: calc(100vh - 64px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -398,7 +399,7 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
ha-card {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
position: relative;
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
@@ -410,12 +411,12 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
@keyframes highlight-fade {
|
||||
0% {
|
||||
box-shadow:
|
||||
0 0 0 2px var(--primary-color),
|
||||
0 0 12px rgba(var(--rgb-primary-color), 0.4);
|
||||
0 0 0 var(--ha-border-width-md) var(--primary-color),
|
||||
0 0 var(--ha-shadow-blur-lg) rgba(var(--rgb-primary-color), 0.4);
|
||||
}
|
||||
100% {
|
||||
box-shadow:
|
||||
0 0 0 2px transparent,
|
||||
0 0 0 var(--ha-border-width-md) transparent,
|
||||
0 0 0 transparent;
|
||||
}
|
||||
}
|
||||
@@ -424,7 +425,7 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
.intro-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.intro-card h1 {
|
||||
@@ -432,18 +433,18 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
margin: 0 0 12px;
|
||||
margin: 0 0 var(--ha-space-3);
|
||||
}
|
||||
|
||||
/* Feature cards */
|
||||
.card-content {
|
||||
padding: 16px;
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
gap: var(--ha-space-3);
|
||||
margin-bottom: var(--ha-space-4);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@@ -475,7 +476,7 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
.empty {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 16px;
|
||||
padding: var(--ha-space-12) var(--ha-space-4);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -487,11 +488,11 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
.empty h1 {
|
||||
margin: 24px 0 16px;
|
||||
margin: var(--ha-space-6) 0 var(--ha-space-4);
|
||||
}
|
||||
|
||||
.empty p {
|
||||
margin: 0 0 24px;
|
||||
margin: 0 0 var(--ha-space-6);
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: var(--secondary-text-color);
|
||||
@@ -500,7 +501,7 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
.empty a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: var(--ha-space-1);
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
@@ -511,9 +512,9 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
.empty a:focus-visible {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline: var(--ha-border-width-md) solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
}
|
||||
|
||||
.empty a ha-svg-icon {
|
||||
@@ -528,15 +529,15 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-2);
|
||||
border-top: var(--ha-border-width-sm) solid var(--divider-color);
|
||||
}
|
||||
|
||||
.card-actions > div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -62,7 +62,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
const defaultPanelUrlPath =
|
||||
this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
|
||||
this.hass.systemData?.default_panel || DEFAULT_PANEL;
|
||||
const titleInvalid = !this._data.title || !this._data.title.trim();
|
||||
|
||||
return html`
|
||||
@@ -260,7 +260,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultPanel = this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
|
||||
const defaultPanel = this.hass.systemData?.default_panel || DEFAULT_PANEL;
|
||||
// Add warning dialog to saying that this will change the default dashboard for all users
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
@@ -284,7 +284,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
|
||||
saveFrontendSystemData(this.hass.connection, "core", {
|
||||
...this.hass.systemData,
|
||||
defaultPanel: urlPath === defaultPanel ? undefined : urlPath,
|
||||
default_panel: urlPath === defaultPanel ? undefined : urlPath,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -309,7 +309,20 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
}
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = { base: err?.message || "Unknown error" };
|
||||
let localizedErrorMessage: string | undefined;
|
||||
if (err?.translation_domain && err?.translation_key) {
|
||||
const localize = await this.hass.loadBackendTranslation(
|
||||
"exceptions",
|
||||
err.translation_domain
|
||||
);
|
||||
localizedErrorMessage = localize(
|
||||
`component.${err.translation_domain}.exceptions.${err.translation_key}.message`,
|
||||
err.translation_placeholders
|
||||
);
|
||||
}
|
||||
this._error = {
|
||||
base: localizedErrorMessage || err?.message || "Unknown error",
|
||||
};
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
|
||||
@@ -404,7 +404,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||
}
|
||||
|
||||
const defaultPanel = this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
|
||||
const defaultPanel = this.hass.systemData?.default_panel || DEFAULT_PANEL;
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
|
||||
@@ -1265,6 +1265,10 @@ export class HaSceneEditor extends PreventUnsavedMixin(
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
ha-alert ha-button[slot="action"] {
|
||||
width: max-content;
|
||||
white-space: nowrap;
|
||||
}
|
||||
ha-fab.dirty {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
@@ -270,6 +270,7 @@ export class HaManualScriptEditor extends LitElement {
|
||||
@value-changed=${this._sidebarConfigChanged}
|
||||
@sidebar-resized=${this._resizeSidebar}
|
||||
@sidebar-resizing-stopped=${this._stopResizeSidebar}
|
||||
@sidebar-reset-size=${this._resetSidebarWidth}
|
||||
></ha-automation-sidebar>
|
||||
</div>
|
||||
</div>
|
||||
@@ -618,6 +619,16 @@ export class HaManualScriptEditor extends LitElement {
|
||||
this._prevSidebarWidthPx = undefined;
|
||||
}
|
||||
|
||||
private _resetSidebarWidth(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._prevSidebarWidthPx = undefined;
|
||||
this._sidebarWidthPx = SIDEBAR_DEFAULT_WIDTH;
|
||||
this.style.setProperty(
|
||||
"--sidebar-dynamic-width",
|
||||
`${this._sidebarWidthPx}px`
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
saveFabStyles,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
@@ -14,8 +15,6 @@ import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { TagDetailDialogParams } from "./show-dialog-tag-detail";
|
||||
|
||||
const TAG_BASE = "https://www.home-assistant.io/tag/";
|
||||
|
||||
@customElement("dialog-tag-detail")
|
||||
class DialogTagDetail
|
||||
extends LitElement
|
||||
@@ -122,7 +121,7 @@ class DialogTagDetail
|
||||
</div>
|
||||
<div id="qr">
|
||||
<ha-qr-code
|
||||
.data=${`${TAG_BASE}${this._params!.entry!.id}`}
|
||||
.data=${`${documentationUrl(this.hass, "/tag/")}${this._params!.entry!.id}`}
|
||||
center-image="/static/icons/favicon-192x192.png"
|
||||
error-correction-level="quartile"
|
||||
scale="5"
|
||||
|
||||
151
src/panels/home/dialogs/dialog-edit-home.ts
Normal file
151
src/panels/home/dialogs/dialog-edit-home.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/entity/ha-entities-picker";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import type { HomeFrontendSystemData } from "../../../data/frontend";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { EditHomeDialogParams } from "./show-dialog-edit-home";
|
||||
|
||||
@customElement("dialog-edit-home")
|
||||
export class DialogEditHome
|
||||
extends LitElement
|
||||
implements HassDialog<EditHomeDialogParams>
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: EditHomeDialogParams;
|
||||
|
||||
@state() private _config?: HomeFrontendSystemData;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
public showDialog(params: EditHomeDialogParams): void {
|
||||
this._params = params;
|
||||
this._config = { ...params.config };
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
this._config = undefined;
|
||||
this._submitting = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
.headerTitle=${this.hass.localize("ui.panel.home.editor.title")}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<p class="description">
|
||||
${this.hass.localize("ui.panel.home.editor.description")}
|
||||
</p>
|
||||
|
||||
<ha-entities-picker
|
||||
autofocus
|
||||
.hass=${this.hass}
|
||||
.value=${this._config?.favorite_entities || []}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.strategy.home.favorite_entities"
|
||||
)}
|
||||
.placeholder=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.strategy.home.add_favorite_entity"
|
||||
)}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.home.editor.favorite_entities_helper"
|
||||
)}
|
||||
reorder
|
||||
allow-custom-entity
|
||||
@value-changed=${this._favoriteEntitiesChanged}
|
||||
></ha-entities-picker>
|
||||
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._save}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _favoriteEntitiesChanged(ev: CustomEvent): void {
|
||||
const entities = ev.detail.value as string[];
|
||||
this._config = {
|
||||
...this._config,
|
||||
favorite_entities: entities.length > 0 ? entities : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private async _save(): Promise<void> {
|
||||
if (!this._params || !this._config) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._submitting = true;
|
||||
|
||||
try {
|
||||
await this._params.saveConfig(this._config);
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to save home configuration:", err);
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-wa-dialog {
|
||||
--dialog-content-padding: var(--ha-space-6);
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0 0 var(--ha-space-4) 0;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-entities-picker {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-edit-home": DialogEditHome;
|
||||
}
|
||||
}
|
||||
20
src/panels/home/dialogs/show-dialog-edit-home.ts
Normal file
20
src/panels/home/dialogs/show-dialog-edit-home.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { HomeFrontendSystemData } from "../../../data/frontend";
|
||||
|
||||
export interface EditHomeDialogParams {
|
||||
config: HomeFrontendSystemData;
|
||||
saveConfig: (config: HomeFrontendSystemData) => Promise<void>;
|
||||
}
|
||||
|
||||
export const loadEditHomeDialog = () => import("./dialog-edit-home");
|
||||
|
||||
export const showEditHomeDialog = (
|
||||
element: HTMLElement,
|
||||
params: EditHomeDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-edit-home",
|
||||
dialogImport: loadEditHomeDialog,
|
||||
dialogParams: params,
|
||||
});
|
||||
};
|
||||
@@ -3,18 +3,18 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import {
|
||||
fetchFrontendSystemData,
|
||||
saveFrontendSystemData,
|
||||
type HomeFrontendSystemData,
|
||||
} from "../../data/frontend";
|
||||
import type { LovelaceDashboardStrategyConfig } from "../../data/lovelace/config/types";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../../types";
|
||||
import { showToast } from "../../util/toast";
|
||||
import "../lovelace/hui-root";
|
||||
import { generateLovelaceDashboardStrategy } from "../lovelace/strategies/get-strategy";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import { showAlertDialog } from "../lovelace/custom-card-helpers";
|
||||
|
||||
const HOME_LOVELACE_CONFIG: LovelaceDashboardStrategyConfig = {
|
||||
strategy: {
|
||||
type: "home",
|
||||
},
|
||||
};
|
||||
import { showEditHomeDialog } from "./dialogs/show-dialog-edit-home";
|
||||
|
||||
@customElement("ha-panel-home")
|
||||
class PanelHome extends LitElement {
|
||||
@@ -28,12 +28,14 @@ class PanelHome extends LitElement {
|
||||
|
||||
@state() private _lovelace?: Lovelace;
|
||||
|
||||
@state() private _config: FrontendSystemData["home"] = {};
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
// Initial setup
|
||||
if (!this.hasUpdated) {
|
||||
this.hass.loadFragmentTranslation("lovelace");
|
||||
this._setLovelace();
|
||||
this._loadConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,9 +97,28 @@ class PanelHome extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _loadConfig() {
|
||||
try {
|
||||
const data = await fetchFrontendSystemData(this.hass.connection, "home");
|
||||
this._config = data || {};
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to load favorites:", err);
|
||||
this._config = {};
|
||||
}
|
||||
this._setLovelace();
|
||||
}
|
||||
|
||||
private async _setLovelace() {
|
||||
const strategyConfig: LovelaceDashboardStrategyConfig = {
|
||||
strategy: {
|
||||
type: "home",
|
||||
favorite_entities: this._config.favorite_entities,
|
||||
},
|
||||
};
|
||||
|
||||
const config = await generateLovelaceDashboardStrategy(
|
||||
HOME_LOVELACE_CONFIG,
|
||||
strategyConfig,
|
||||
this.hass
|
||||
);
|
||||
|
||||
@@ -121,15 +142,34 @@ class PanelHome extends LitElement {
|
||||
}
|
||||
|
||||
private _setEditMode = () => {
|
||||
// For now, we just show an alert that edit mode is not supported.
|
||||
// This will be expanded in the future.
|
||||
showAlertDialog(this, {
|
||||
title: "Edit mode not available",
|
||||
text: "The Home panel does not support edit mode.",
|
||||
confirmText: this.hass.localize("ui.common.ok"),
|
||||
showEditHomeDialog(this, {
|
||||
config: this._config,
|
||||
saveConfig: async (config) => {
|
||||
await this._saveConfig(config);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private async _saveConfig(config: HomeFrontendSystemData): Promise<void> {
|
||||
try {
|
||||
await saveFrontendSystemData(this.hass.connection, "home", config);
|
||||
this._config = config || {};
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to save home configuration:", err);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.panel.home.editor.save_failed"),
|
||||
duration: 0,
|
||||
dismissable: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.successfully_saved"),
|
||||
});
|
||||
this._setLovelace();
|
||||
}
|
||||
|
||||
static readonly styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
@@ -7,6 +7,7 @@ import "../../components/ha-settings-row";
|
||||
import "../../components/ha-switch";
|
||||
import type { CoreFrontendUserData } from "../../data/frontend";
|
||||
import { saveFrontendUserData } from "../../data/frontend";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-advanced-mode-row")
|
||||
@@ -31,7 +32,10 @@ class AdvancedModeRow extends LitElement {
|
||||
<span slot="description">
|
||||
${this.hass.localize("ui.panel.profile.advanced_mode.description")}
|
||||
<a
|
||||
href="https://www.home-assistant.io/blog/2019/07/17/release-96/#advanced-mode"
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/blog/2019/07/17/release-96/#advanced-mode"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize("ui.panel.profile.advanced_mode.link_promo")}
|
||||
|
||||
@@ -25,7 +25,7 @@ class HaPickDashboardRow extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const value = this.hass.userData?.defaultPanel || USE_SYSTEM_VALUE;
|
||||
const value = this.hass.userData?.default_panel || USE_SYSTEM_VALUE;
|
||||
return html`
|
||||
<ha-settings-row .narrow=${this.narrow}>
|
||||
<span slot="heading">
|
||||
@@ -84,12 +84,12 @@ class HaPickDashboardRow extends LitElement {
|
||||
return;
|
||||
}
|
||||
const urlPath = value === USE_SYSTEM_VALUE ? undefined : value;
|
||||
if (urlPath === this.hass.userData?.defaultPanel) {
|
||||
if (urlPath === this.hass.userData?.default_panel) {
|
||||
return;
|
||||
}
|
||||
saveFrontendUserData(this.hass.connection, "core", {
|
||||
...this.hass.userData,
|
||||
defaultPanel: urlPath,
|
||||
default_panel: urlPath,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const renderMarkdown = async (
|
||||
allowSvg?: boolean;
|
||||
allowDataUrl?: boolean;
|
||||
} = {}
|
||||
): Promise<string> => {
|
||||
): Promise<string[]> => {
|
||||
if (!whiteListNormal) {
|
||||
whiteListNormal = {
|
||||
...getDefaultWhiteList(),
|
||||
@@ -53,38 +53,43 @@ const renderMarkdown = async (
|
||||
whiteList.a.push("download");
|
||||
}
|
||||
|
||||
return filterXSS(await marked(content, markedOptions), {
|
||||
whiteList,
|
||||
onTagAttr: (
|
||||
tag: string,
|
||||
name: string,
|
||||
value: string
|
||||
): string | undefined => {
|
||||
// Override the default `onTagAttr` behavior to only render
|
||||
// our markdown checkboxes.
|
||||
// Returning undefined causes the default measure to be taken
|
||||
// in the xss library.
|
||||
if (tag === "input") {
|
||||
if (
|
||||
(name === "type" && value === "checkbox") ||
|
||||
name === "checked" ||
|
||||
name === "disabled"
|
||||
) {
|
||||
return undefined;
|
||||
marked.setOptions(markedOptions);
|
||||
|
||||
const tokens = marked.lexer(content);
|
||||
return tokens.map((token) =>
|
||||
filterXSS(marked.parser([token]), {
|
||||
whiteList,
|
||||
onTagAttr: (
|
||||
tag: string,
|
||||
name: string,
|
||||
value: string
|
||||
): string | undefined => {
|
||||
// Override the default `onTagAttr` behavior to only render
|
||||
// our markdown checkboxes.
|
||||
// Returning undefined causes the default measure to be taken
|
||||
// in the xss library.
|
||||
if (tag === "input") {
|
||||
if (
|
||||
(name === "type" && value === "checkbox") ||
|
||||
name === "checked" ||
|
||||
name === "disabled"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
if (
|
||||
hassOptions.allowDataUrl &&
|
||||
tag === "a" &&
|
||||
name === "href" &&
|
||||
value.startsWith("data:")
|
||||
) {
|
||||
return `href="${value}"`;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
if (
|
||||
hassOptions.allowDataUrl &&
|
||||
tag === "a" &&
|
||||
name === "href" &&
|
||||
value.startsWith("data:")
|
||||
) {
|
||||
return `href="${value}"`;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const api = {
|
||||
|
||||
@@ -2220,6 +2220,14 @@
|
||||
"migrate_to_user_data": "This will change the sidebar on all the devices you are logged in to. To create a sidebar per device, you should use a different user for that device."
|
||||
},
|
||||
"panel": {
|
||||
"home": {
|
||||
"editor": {
|
||||
"title": "Edit home page",
|
||||
"description": "Configure your home page display preferences.",
|
||||
"favorite_entities_helper": "Display your favorite entities. Home Assistant will still suggest based on commonly used up to 8 slots.",
|
||||
"save_failed": "Failed to save home page configuration"
|
||||
}
|
||||
},
|
||||
"my": {
|
||||
"not_supported": "This redirect is not supported by your Home Assistant instance. Check the {link} for the supported redirects and the version they where introduced.",
|
||||
"component_not_loaded": "This redirect is not supported by your Home Assistant instance. You need the integration {integration} to use this redirect.",
|
||||
@@ -3257,7 +3265,10 @@
|
||||
"create_helper": "Create helper",
|
||||
"no_helpers": "Looks like you don't have any helpers yet!",
|
||||
"search": "Search {number} {number, plural,\n one {helper}\n other {helpers}\n}",
|
||||
"error_information": "Error information"
|
||||
"error_information": "Error information",
|
||||
"delete_confirm_title": "Delete helper?",
|
||||
"delete_confirm_text": "Are you sure you want to delete {name}?",
|
||||
"delete_failed": "Failed to delete helper"
|
||||
},
|
||||
"dialog": {
|
||||
"create": "Create",
|
||||
@@ -4785,7 +4796,8 @@
|
||||
"headers": {
|
||||
"name": "Name",
|
||||
"type": "Type",
|
||||
"file_name": "File name"
|
||||
"file_name": "File name",
|
||||
"usage_count": "In use"
|
||||
},
|
||||
"types": {
|
||||
"automation": "Automation",
|
||||
|
||||
Reference in New Issue
Block a user