Compare commits

...

22 Commits

Author SHA1 Message Date
Simon Lamon
9a45fbc488 Merge branch 'dev' into sec_pypi_publishing 2025-11-23 17:01:26 +01:00
Franck Nijhof
4674384f7d Allow deleting helpers directly from helpers panel (#28044)
* Allow deleting helpers directly from helpers panel

* Fix error handling in helper deletion logic

Address review feedback by improving the logic in _deleteHelper:
- Throw error when entity is not found in registry or component not loaded
- Move return statement outside of nested condition to ensure proper control flow
- Prevent fallthrough to config entry deletion for old-style helpers

Addresses review comment from @MindFreeze

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-23 15:20:20 +01:00
steinmn
2e7c95a8cc Translate dashboard panel url error message (#27306)
* Translate dashboard panel url error message

* Use backend provided translations
2025-11-23 14:54:21 +01:00
Simon Lamon
9630b175a1 Remove open from ha-dropdown (#28037) 2025-11-22 14:30:06 +02:00
Petar Petrov
be319503f7 Update color scheme in ZHA network visualization (#28032) 2025-11-21 19:09:37 +01:00
Paul Bottein
9b1fe28018 Rename defaultPanel to default_panel (#28035) 2025-11-21 16:24:34 +01:00
Paul Bottein
0595f722f3 Add basic editor to edit favorites entities for home panel (#28028)
* Add basic editor to edit favorites entities for home panel

* Rename favorites

* Rename favorites

* Feedbacks
2025-11-21 16:19:39 +02:00
Petar Petrov
1c0315854a Hide echarts toolbox better (#28030) 2025-11-21 14:52:20 +02:00
Bram Kragten
3b73d7c298 Dont add store token for external auth flows (#28026)
* Dont add store token for external auth flows

* Apply suggestion from @MindFreeze

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-21 12:01:02 +00:00
Aidan Timson
2955cb4956 Make use of documentationUrl over hardcoded docs links (#28022)
Make use of documentationUrl over hardcoded docs link
2025-11-21 13:43:46 +02:00
Franck Nijhof
c679e312a0 Add delete option to reauth cards on integrations dashboard (#28020)
* Add delete option to reauth cards on integrations dashboard

Users can now delete config entries directly from the reauth card that appears at the top of the integrations dashboard, instead of having to scroll down to find the original integration card.

The delete option:
- Appears in the three-dot menu on reauth cards
- Shows a confirmation dialog before deletion
- Handles application credentials cleanup
- Shows restart notifications when required
- Uses the same styling and localization as the integration entry delete

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Apply suggestions from code review

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-21 13:39:01 +02:00
Aidan Timson
37e12a83be Fix scene review switch action overflow issue (#28024)
* Fix scene review switch action overflow issue

* Be more specific
2025-11-21 10:33:49 +00:00
Aidan Timson
755c6dbb93 Add design tokens to labs feature (#28023) 2025-11-21 11:21:33 +01:00
Silas Krause
4a90331ac7 Add markdown support for assist messages (#27957)
* Add markdown support for assist messages

* Improve styles

* Refactor code

* Fix white space

* Move code

* Make css compiler happy

* Wait for render to complete before scrolling

* Revert changes

* Refactor ha-markdown to render in chunks

* Refactor and adapt scroll logic

* Fix imports

* Update styles

* Render into renderRoot

* Fix query selector

* Fix broken image style

* Implement PR feedback

* Remove unnecessary css

* Fix cache issue

* Apply suggestion from @MindFreeze

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-21 08:03:47 +00:00
Simon Lamon
aef3cb1c36 Merge branch 'dev' into sec_pypi_publishing 2025-11-20 09:53:07 +01:00
Simon Lamon
8535ee0694 Merge branch 'dev' into sec_pypi_publishing 2025-11-13 06:58:30 +01:00
Simon Lamon
b8110d1a45 Merge branch 'dev' into sec_pypi_publishing 2025-10-27 06:41:51 +01:00
Simon Lamon
19e9de39c5 Merge branch 'dev' into sec_pypi_publishing 2025-10-19 10:56:12 +02:00
Simon Lamon
f22f01e513 Merge branch 'dev' into sec_pypi_publishing 2025-10-06 20:28:38 +02:00
Simon Lamon
3f86f144b5 Merge branch 'dev' into sec_pypi_publishing 2025-10-04 17:25:20 +02:00
Simon Lamon
4efef5ed16 Update release.yaml 2025-09-24 07:04:06 +02:00
Simon Lamon
cac7ae2a40 Remove twine and introduce trusted publishing 2025-09-20 21:23:04 +02:00
33 changed files with 735 additions and 161 deletions

View File

@@ -19,8 +19,11 @@ jobs:
release:
name: Release
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: write # Required to upload release assets
id-token: write # For "Trusted Publisher" to PyPi
if: github.repository_owner == 'home-assistant'
steps:
- name: Checkout the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -46,14 +49,18 @@ jobs:
run: ./script/translations_download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build and release package
run: |
python3 -m pip install twine build
export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
python3 -m pip install build
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
script/release
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
skip-existing: true
- name: Upload release assets
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
with:

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,5 +1,4 @@
#!/bin/sh
# Pushes a new version to PyPi.
# Stop on errors
set -e
@@ -12,5 +11,4 @@ yarn install
script/build_frontend
rm -rf dist home_assistant_frontend.egg-info
python3 -m build
python3 -m twine upload dist/*.whl --skip-existing
python3 -m build -q

View File

@@ -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;
}

View File

@@ -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 },
},

View File

@@ -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;

View File

@@ -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(

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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"
>

View File

@@ -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

View File

@@ -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"
>

View File

@@ -299,7 +299,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"

View File

@@ -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

View File

@@ -5,6 +5,7 @@ import {
mdiCancel,
mdiChevronRight,
mdiCog,
mdiDelete,
mdiDotsVertical,
mdiMenuDown,
mdiPencilOff,
@@ -109,10 +110,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,
@@ -451,6 +453,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>
@@ -1280,6 +1295,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, {});
}

View File

@@ -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);
}
`;
}

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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);
}
`,
];

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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"

View 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;
}
}

View 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,
});
};

View File

@@ -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;

View File

@@ -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")}

View File

@@ -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,
});
}
}

View File

@@ -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 = {

View File

@@ -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",