Compare commits

..

5 Commits

Author SHA1 Message Date
Aidan Timson
4d77c2b60a Add support for error state 2025-11-21 10:41:49 +00:00
Aidan Timson
e4b4c809c9 Restore optimised parts code 2025-11-21 10:41:49 +00:00
Aidan Timson
0c51b100b6 memo 2025-11-21 10:41:49 +00:00
Aidan Timson
017bc712b0 Reorder 2025-11-21 10:41:49 +00:00
Aidan Timson
b034f57384 Migrate ha-icon-picker to generic picker 2025-11-21 10:41:49 +00:00
44 changed files with 233 additions and 833 deletions

View File

@@ -21,7 +21,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
ref: dev ref: dev
@@ -56,7 +56,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
ref: master ref: master

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
@@ -58,7 +58,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
@@ -76,7 +76,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
@@ -100,7 +100,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:

View File

@@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
# We must fetch at least the immediate parents so that if this is # We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head. # a pull request then we can checkout the head.
@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # 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) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3

View File

@@ -22,7 +22,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
ref: dev ref: dev
@@ -57,7 +57,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
ref: master ref: master

View File

@@ -16,7 +16,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0

View File

@@ -21,7 +21,7 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview') if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0

View File

@@ -20,7 +20,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6

View File

@@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
@@ -91,7 +91,7 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
@@ -120,7 +120,7 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Upload Translations - name: Upload Translations
run: | run: |

View File

@@ -11,7 +11,7 @@ A compact, accessible dropdown menu for choosing actions or settings. `ha-dropdo
### Example usage (composition) ### Example usage (composition)
```html ```html
<ha-dropdown> <ha-dropdown open>
<ha-button slot="trigger" with-caret>Dropdown</ha-button> <ha-button slot="trigger" with-caret>Dropdown</ha-button>
<ha-dropdown-item> <ha-dropdown-item>

View File

@@ -28,7 +28,7 @@ export class DemoHaDropdown extends LitElement {
<div class=${mode}> <div class=${mode}>
<ha-card header="ha-button in ${mode}"> <ha-card header="ha-button in ${mode}">
<div class="card-content"> <div class="card-content">
<ha-dropdown> <ha-dropdown open>
<ha-button slot="trigger" with-caret>Dropdown</ha-button> <ha-button slot="trigger" with-caret>Dropdown</ha-button>
<ha-dropdown-item> <ha-dropdown-item>

View File

@@ -2,8 +2,8 @@
import { genClientId } from "home-assistant-js-websocket"; import { genClientId } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { keyed } from "lit/directives/keyed"; import { keyed } from "lit/directives/keyed";
import { customElement, property, state } from "lit/decorators";
import type { LocalizeFunc } from "../common/translations/localize"; import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-alert"; import "../components/ha-alert";
import "../components/ha-button"; import "../components/ha-button";
@@ -59,8 +59,7 @@ export class HaAuthFlow extends LitElement {
willUpdate(changedProps: PropertyValues) { willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps); super.willUpdate(changedProps);
if (!this.hasUpdated && this.clientId === genClientId()) { if (!this.hasUpdated) {
// Preselect store token when logging in to own instance
this._storeToken = this.initStoreToken; this._storeToken = this.initStoreToken;
} }
@@ -118,9 +117,6 @@ export class HaAuthFlow extends LitElement {
display: block; display: block;
margin-top: 16px; margin-top: 16px;
} }
.action ha-button {
width: 100%;
}
</style> </style>
<form>${this._renderForm()}</form> <form>${this._renderForm()}</form>
`; `;

View File

@@ -597,15 +597,10 @@ export class HaChartBase extends LitElement {
aria: { show: true }, aria: { show: true },
dataZoom: this._getDataZoomConfig(), dataZoom: this._getDataZoomConfig(),
toolbox: { toolbox: {
top: Number.MAX_SAFE_INTEGER, top: Infinity,
left: Number.MAX_SAFE_INTEGER, left: Infinity,
feature: { feature: {
dataZoom: { dataZoom: { show: true, yAxisIndex: false, filterMode: "none" },
show: true,
yAxisIndex: false,
filterMode: "none",
showTitle: false,
},
}, },
iconStyle: { opacity: 0 }, iconStyle: { opacity: 0 },
}, },

View File

@@ -129,6 +129,10 @@ export class HaGenericPicker extends LitElement {
// helper to set new value after closing picker, to avoid flicker // helper to set new value after closing picker, to avoid flicker
private _newValue?: string; private _newValue?: string;
@property({ attribute: "error-message" }) public errorMessage?: string;
@property({ type: Boolean, reflect: true }) public invalid = false;
private _unsubscribeTinyKeys?: () => void; private _unsubscribeTinyKeys?: () => void;
protected render() { protected render() {
@@ -163,6 +167,8 @@ export class HaGenericPicker extends LitElement {
.value=${this.value} .value=${this.value}
.required=${this.required} .required=${this.required}
.disabled=${this.disabled} .disabled=${this.disabled}
.errorMessage=${this.errorMessage}
.invalid=${this.invalid}
.hideClearIcon=${this.hideClearIcon} .hideClearIcon=${this.hideClearIcon}
.valueRenderer=${this.valueRenderer} .valueRenderer=${this.valueRenderer}
> >
@@ -234,11 +240,16 @@ export class HaGenericPicker extends LitElement {
} }
private _renderHelper() { private _renderHelper() {
return this.helper const showError = this.invalid && this.errorMessage;
? html`<ha-input-helper-text .disabled=${this.disabled} const showHelper = !showError && this.helper;
>${this.helper}</ha-input-helper-text
>` if (!showError && !showHelper) {
: nothing; return nothing;
}
return html`<ha-input-helper-text .disabled=${this.disabled}>
${showError ? this.errorMessage : this.helper}
</ha-input-helper-text>`;
} }
private _dialogOpened = () => { private _dialogOpened = () => {
@@ -337,6 +348,9 @@ export class HaGenericPicker extends LitElement {
display: block; display: block;
margin: var(--ha-space-2) 0 0; margin: var(--ha-space-2) 0 0;
} }
:host([invalid]) ha-input-helper-text {
color: var(--mdc-theme-error, var(--error-color, #b00020));
}
wa-popover { wa-popover {
--wa-space-l: var(--ha-space-0); --wa-space-l: var(--ha-space-0);

View File

@@ -1,8 +1,4 @@
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import type {
ComboBoxDataProviderCallback,
ComboBoxDataProviderParams,
} from "@vaadin/combo-box/vaadin-combo-box-light";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { LitElement, css, html } from "lit"; import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
@@ -10,9 +6,10 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { customIcons } from "../data/custom_icons"; import { customIcons } from "../data/custom_icons";
import type { HomeAssistant, ValueChangedEvent } from "../types"; import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-combo-box";
import "./ha-icon";
import "./ha-combo-box-item"; import "./ha-combo-box-item";
import "./ha-generic-picker";
import "./ha-icon";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
interface IconItem { interface IconItem {
icon: string; icon: string;
@@ -21,7 +18,7 @@ interface IconItem {
} }
interface RankedIcon { interface RankedIcon {
icon: string; item: PickerComboBoxItem;
rank: number; rank: number;
} }
@@ -67,13 +64,18 @@ const loadCustomIconItems = async (iconsetPrefix: string) => {
} }
}; };
const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) => html` const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
<ha-combo-box-item type="button"> <ha-combo-box-item type="button">
<ha-icon .icon=${item.icon} slot="start"></ha-icon> <ha-icon .icon=${item.id} slot="start"></ha-icon>
${item.icon} ${item.id}
</ha-combo-box-item> </ha-combo-box-item>
`; `;
const valueRenderer = (value: string) => html`
<ha-icon .icon=${value} slot="start"></ha-icon>
<span slot="headline">${value}</span>
`;
@customElement("ha-icon-picker") @customElement("ha-icon-picker")
export class HaIconPicker extends LitElement { export class HaIconPicker extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@@ -96,13 +98,11 @@ export class HaIconPicker extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-combo-box <ha-generic-picker
.hass=${this.hass} .hass=${this.hass}
item-value-path="icon"
item-label-path="icon"
.value=${this._value} .value=${this._value}
allow-custom-value allow-custom-value
.dataProvider=${ICONS_LOADED ? this._iconProvider : undefined} .getItems=${this._getItems}
.label=${this.label} .label=${this.label}
.helper=${this.helper} .helper=${this.helper}
.disabled=${this.disabled} .disabled=${this.disabled}
@@ -110,69 +110,85 @@ export class HaIconPicker extends LitElement {
.placeholder=${this.placeholder} .placeholder=${this.placeholder}
.errorMessage=${this.errorMessage} .errorMessage=${this.errorMessage}
.invalid=${this.invalid} .invalid=${this.invalid}
.renderer=${rowRenderer} .rowRenderer=${rowRenderer}
icon .valueRenderer=${valueRenderer}
@opened-changed=${this._openedChanged} .searchFn=${this._filterIcons}
.notFoundLabel=${this.hass?.localize(
"ui.components.icon-picker.no_match"
)}
popover-placement="bottom-start"
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
> >
${this._value || this.placeholder </ha-generic-picker>
? html`
<ha-icon .icon=${this._value || this.placeholder} slot="icon">
</ha-icon>
`
: html`<slot slot="icon" name="fallback"></slot>`}
</ha-combo-box>
`; `;
} }
// Filter can take a significant chunk of frame (up to 3-5 ms) // Filter can take a significant chunk of frame (up to 3-5 ms)
private _filterIcons = memoizeOne( private _filterIcons = memoizeOne(
(filter: string, iconItems: IconItem[] = ICONS) => { (filter: string, items: PickerComboBoxItem[]): PickerComboBoxItem[] => {
if (!filter) { if (!filter) {
return iconItems; return items;
} }
const filteredItems: RankedIcon[] = []; const filteredItems: RankedIcon[] = [];
const addIcon = (icon: string, rank: number) => const addIcon = (item: PickerComboBoxItem, rank: number) =>
filteredItems.push({ icon, rank }); filteredItems.push({ item, rank });
// Filter and rank such that exact matches rank higher, and prefer icon name matches over keywords // Filter and rank such that exact matches rank higher, and prefer icon name matches over keywords
for (const item of iconItems) { for (const item of items) {
if (item.parts.has(filter)) { const iconName = item.id.split(":")[1] || item.id;
addIcon(item.icon, 1); const parts = iconName.split("-");
} else if (item.keywords.includes(filter)) { const keywords = item.search_labels?.slice(1) || [];
addIcon(item.icon, 2);
} else if (item.icon.includes(filter)) { if (parts.includes(filter)) {
addIcon(item.icon, 3); addIcon(item, 1);
} else if (item.keywords.some((word) => word.includes(filter))) { } else if (keywords.includes(filter)) {
addIcon(item.icon, 4); addIcon(item, 2);
} else if (item.id.includes(filter)) {
addIcon(item, 3);
} else if (keywords.some((word) => word.includes(filter))) {
addIcon(item, 4);
} }
} }
// Allow preview for custom icon not in list // Allow preview for custom icon not in list
if (filteredItems.length === 0) { if (filteredItems.length === 0) {
addIcon(filter, 0); addIcon(
{
id: filter,
primary: filter,
icon: filter,
search_labels: [filter],
sorting_label: filter,
},
0
);
} }
return filteredItems.sort((itemA, itemB) => itemA.rank - itemB.rank); return filteredItems
.sort((itemA, itemB) => itemA.rank - itemB.rank)
.map((item) => item.item);
} }
); );
private _iconProvider = ( private _getItems = (): PickerComboBoxItem[] =>
params: ComboBoxDataProviderParams, ICONS.map((icon: IconItem) => ({
callback: ComboBoxDataProviderCallback<IconItem | RankedIcon> id: icon.icon,
) => { primary: icon.icon,
const filteredItems = this._filterIcons(params.filter.toLowerCase(), ICONS); icon: icon.icon,
const iStart = params.page * params.pageSize; search_labels: [
const iEnd = iStart + params.pageSize; icon.icon.split(":")[1] || icon.icon,
callback(filteredItems.slice(iStart, iEnd), filteredItems.length); ...Array.from(icon.parts),
}; ...icon.keywords,
],
sorting_label: icon.icon,
}));
private async _openedChanged(ev: ValueChangedEvent<boolean>) { protected firstUpdated() {
const opened = ev.detail.value; if (!ICONS_LOADED) {
if (opened && !ICONS_LOADED) { loadIcons().then(() => {
await loadIcons();
this.requestUpdate(); this.requestUpdate();
});
} }
} }
@@ -199,15 +215,9 @@ export class HaIconPicker extends LitElement {
} }
static styles = css` static styles = css`
*[slot="icon"] { ha-generic-picker {
color: var(--primary-text-color); width: 100%;
position: relative; display: block;
bottom: 2px;
}
*[slot="prefix"] {
margin-right: 8px;
margin-inline-end: 8px;
margin-inline-start: initial;
} }
`; `;
} }

View File

@@ -39,6 +39,10 @@ export class HaPickerField extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public valueRenderer?: PickerValueRenderer; public valueRenderer?: PickerValueRenderer;
@property({ attribute: "error-message" }) public errorMessage?: string;
@property({ type: Boolean, reflect: true }) public invalid = false;
@query("ha-combo-box-item", true) public item!: HaComboBoxItem; @query("ha-combo-box-item", true) public item!: HaComboBoxItem;
public async focus() { public async focus() {
@@ -142,6 +146,11 @@ export class HaPickerField extends LitElement {
background-color: var(--mdc-theme-primary); background-color: var(--mdc-theme-primary);
} }
:host([invalid]) ha-combo-box-item:after {
height: 2px;
background-color: var(--mdc-theme-error, var(--error-color, #b00020));
}
.clear { .clear {
margin: 0 -8px; margin: 0 -8px;
--mdc-icon-button-size: 32px; --mdc-icon-button-size: 32px;

View File

@@ -10,7 +10,6 @@ import {
import { formatTime } from "../common/datetime/format_time"; import { formatTime } from "../common/datetime/format_time";
import type { LocalizeFunc } from "../common/translations/localize"; import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { documentationUrl } from "../util/documentation-url";
import { fileDownload } from "../util/file_download"; import { fileDownload } from "../util/file_download";
import { handleFetchPromise } from "../util/hass-call-api"; import { handleFetchPromise } from "../util/hass-call-api";
import type { BackupManagerState, ManagerStateEvent } from "./backup_manager"; import type { BackupManagerState, ManagerStateEvent } from "./backup_manager";
@@ -415,7 +414,7 @@ ${hass.auth.data.hassUrl}
${hass.localize("ui.panel.config.backup.emergency_kit_file.encryption_key")} ${hass.localize("ui.panel.config.backup.emergency_kit_file.encryption_key")}
${encryptionKey} ${encryptionKey}
${hass.localize("ui.panel.config.backup.emergency_kit_file.more_info", { link: documentationUrl(hass, "/more-info/backup-emergency-kit") })}`); ${hass.localize("ui.panel.config.backup.emergency_kit_file.more_info", { link: "https://www.home-assistant.io/more-info/backup-emergency-kit" })}`);
export const geneateEmergencyKitFileName = ( export const geneateEmergencyKitFileName = (
hass: HomeAssistant, hass: HomeAssistant,

View File

@@ -3,7 +3,7 @@ import type { Connection } from "home-assistant-js-websocket";
export interface CoreFrontendUserData { export interface CoreFrontendUserData {
showAdvanced?: boolean; showAdvanced?: boolean;
showEntityIdPicker?: boolean; showEntityIdPicker?: boolean;
default_panel?: string; defaultPanel?: string;
} }
export interface SidebarFrontendUserData { export interface SidebarFrontendUserData {
@@ -12,11 +12,7 @@ export interface SidebarFrontendUserData {
} }
export interface CoreFrontendSystemData { export interface CoreFrontendSystemData {
default_panel?: string; defaultPanel?: string;
}
export interface HomeFrontendSystemData {
favorite_entities?: string[];
} }
declare global { declare global {
@@ -26,7 +22,6 @@ declare global {
} }
interface FrontendSystemData { interface FrontendSystemData {
core: CoreFrontendSystemData; core: CoreFrontendSystemData;
home: HomeFrontendSystemData;
} }
} }

View File

@@ -9,8 +9,8 @@ export const getLegacyDefaultPanelUrlPath = (): string | null => {
}; };
export const getDefaultPanelUrlPath = (hass: HomeAssistant): string => export const getDefaultPanelUrlPath = (hass: HomeAssistant): string =>
hass.userData?.default_panel || hass.userData?.defaultPanel ||
hass.systemData?.default_panel || hass.systemData?.defaultPanel ||
getLegacyDefaultPanelUrlPath() || getLegacyDefaultPanelUrlPath() ||
DEFAULT_PANEL; DEFAULT_PANEL;

View File

@@ -11,7 +11,6 @@ import type {
import { showToast } from "../../util/toast"; import { showToast } from "../../util/toast";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("ha-more-info-add-to") @customElement("ha-more-info-add-to")
export class HaMoreInfoAddTo extends LitElement { export class HaMoreInfoAddTo extends LitElement {
@@ -52,7 +51,6 @@ export class HaMoreInfoAddTo extends LitElement {
app_payload: action.app_payload, app_payload: action.app_payload,
}, },
}); });
fireEvent(this, "add-to-action-selected");
} catch (err: any) { } catch (err: any) {
showToast(this, { showToast(this, {
message: this.hass.localize( message: this.hass.localize(
@@ -151,8 +149,4 @@ declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-more-info-add-to": HaMoreInfoAddTo; "ha-more-info-add-to": HaMoreInfoAddTo;
} }
interface HASSDomEvents {
"add-to-action-selected": undefined;
}
} }

View File

@@ -645,7 +645,6 @@ export class MoreInfoDialog extends LitElement {
<ha-more-info-add-to <ha-more-info-add-to
.hass=${this.hass} .hass=${this.hass}
.entityId=${entityId} .entityId=${entityId}
@add-to-action-selected=${this._goBack}
></ha-more-info-add-to> ></ha-more-info-add-to>
` `
: nothing : nothing

View File

@@ -5,7 +5,6 @@ import { atLeastVersion } from "../common/config/version";
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import "../components/ha-card"; import "../components/ha-card";
import { haStyle } from "../resources/styles"; import { haStyle } from "../resources/styles";
import { documentationUrl } from "../util/documentation-url";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import "./hass-subpage"; import "./hass-subpage";
@@ -58,7 +57,7 @@ class SupervisorErrorScreen extends LitElement {
</li> </li>
<li> <li>
<a <a
href=${documentationUrl(this.hass, "/help/")} href="https://www.home-assistant.io/help/"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >

View File

@@ -4,7 +4,6 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import type { LocalizeFunc } from "../common/translations/localize"; import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-card"; import "../components/ha-card";
import { documentationUrl } from "../util/documentation-url";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { showAppDialog } from "./dialogs/show-app-dialog"; import { showAppDialog } from "./dialogs/show-app-dialog";
import { showCommunityDialog } from "./dialogs/show-community-dialog"; import { showCommunityDialog } from "./dialogs/show-community-dialog";
@@ -23,10 +22,7 @@ class OnboardingWelcomeLinks extends LitElement {
return html`<a return html`<a
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
href=${documentationUrl( href="https://www.home-assistant.io/blog/2016/01/19/perfect-home-automation/"
this.hass,
"/blog/2016/01/19/perfect-home-automation/"
)}
> >
<onboarding-welcome-link <onboarding-welcome-link
noninteractive noninteractive

View File

@@ -188,7 +188,6 @@ export default class HaAutomationSidebar extends LitElement {
class="handle ${this._resizing ? "resizing" : ""}" class="handle ${this._resizing ? "resizing" : ""}"
@mousedown=${this._handleMouseDown} @mousedown=${this._handleMouseDown}
@touchstart=${this._handleMouseDown} @touchstart=${this._handleMouseDown}
@dblclick=${this._handleDoubleClick}
@focus=${this._startKeyboardResizing} @focus=${this._startKeyboardResizing}
@blur=${this._stopKeyboardResizing} @blur=${this._stopKeyboardResizing}
tabindex="0" tabindex="0"
@@ -259,17 +258,6 @@ 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) { private _startResizing(clientX: number) {
// register event listeners for drag handling // register event listeners for drag handling
document.addEventListener("mousemove", this._handleMouseMove); document.addEventListener("mousemove", this._handleMouseMove);
@@ -434,6 +422,5 @@ declare global {
deltaInPx: number; deltaInPx: number;
}; };
"sidebar-resizing-stopped": undefined; "sidebar-resizing-stopped": undefined;
"sidebar-reset-size": undefined;
} }
} }

View File

@@ -317,7 +317,6 @@ export class HaManualAutomationEditor extends LitElement {
@value-changed=${this._sidebarConfigChanged} @value-changed=${this._sidebarConfigChanged}
@sidebar-resized=${this._resizeSidebar} @sidebar-resized=${this._resizeSidebar}
@sidebar-resizing-stopped=${this._stopResizeSidebar} @sidebar-resizing-stopped=${this._stopResizeSidebar}
@sidebar-reset-size=${this._resetSidebarWidth}
></ha-automation-sidebar> ></ha-automation-sidebar>
</div> </div>
</div> </div>
@@ -701,16 +700,6 @@ export class HaManualAutomationEditor extends LitElement {
this._prevSidebarWidthPx = undefined; 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 { static get styles(): CSSResultGroup {
return [ return [
saveFabStyles, saveFabStyles,

View File

@@ -2,7 +2,6 @@ import { mdiClose, mdiOpenInNew } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { documentationUrl } from "../../../util/documentation-url";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-button"; import "../../../components/ha-button";
import "../../../components/ha-code-editor"; import "../../../components/ha-code-editor";
@@ -141,7 +140,7 @@ class DialogImportBlueprint extends LitElement {
<ha-button <ha-button
size="small" size="small"
appearance="plain" appearance="plain"
href=${documentationUrl(this.hass, "/get-blueprints")} href="https://www.home-assistant.io/get-blueprints"
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
> >

View File

@@ -43,7 +43,6 @@ import {
} from "../../../data/blueprint"; } from "../../../data/blueprint";
import { showScriptEditor } from "../../../data/script"; import { showScriptEditor } from "../../../data/script";
import { findRelated } from "../../../data/search"; import { findRelated } from "../../../data/search";
import "../../../components/chips/ha-assist-chip";
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
@@ -61,7 +60,6 @@ type BlueprintMetaDataPath = BlueprintMetaData & {
error: boolean; error: boolean;
type: "automation" | "script"; type: "automation" | "script";
fullpath: string; fullpath: string;
usageCount?: number;
}; };
const createNewFunctions = { const createNewFunctions = {
@@ -130,20 +128,14 @@ class HaBlueprintOverview extends LitElement {
}) })
private _filter = ""; private _filter = "";
@state() private _usageCounts: Record<string, number> = {};
private _usageCountRequest = 0;
private _processedBlueprints = memoizeOne( private _processedBlueprints = memoizeOne(
( (
blueprints: Record<string, Blueprints>, blueprints: Record<string, Blueprints>,
localize: LocalizeFunc, localize: LocalizeFunc
usageCounts: Record<string, number>
): BlueprintMetaDataPath[] => { ): BlueprintMetaDataPath[] => {
const result: any[] = []; const result: any[] = [];
Object.entries(blueprints).forEach(([type, typeBlueprints]) => Object.entries(blueprints).forEach(([type, typeBlueprints]) =>
Object.entries(typeBlueprints).forEach(([path, blueprint]) => { Object.entries(typeBlueprints).forEach(([path, blueprint]) => {
const fullpath = `${type}/${path}`;
if ("error" in blueprint) { if ("error" in blueprint) {
result.push({ result.push({
name: blueprint.error, name: blueprint.error,
@@ -153,8 +145,7 @@ class HaBlueprintOverview extends LitElement {
), ),
error: true, error: true,
path, path,
fullpath, fullpath: `${type}/${path}`,
usageCount: 0,
}); });
} else { } else {
result.push({ result.push({
@@ -165,8 +156,7 @@ class HaBlueprintOverview extends LitElement {
), ),
error: false, error: false,
path, path,
fullpath, fullpath: `${type}/${path}`,
usageCount: usageCounts[fullpath] || 0,
}); });
} }
}) })
@@ -199,34 +189,6 @@ class HaBlueprintOverview extends LitElement {
filterable: true, filterable: true,
flex: 2, 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: { fullpath: {
title: "fullpath", title: "fullpath",
hidden: true, hidden: true,
@@ -304,7 +266,6 @@ class HaBlueprintOverview extends LitElement {
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this._loadUsageCounts();
if (this.route.path === "/import") { if (this.route.path === "/import") {
const url = extractSearchParam("blueprint_url"); const url = extractSearchParam("blueprint_url");
navigate("/config/blueprint/dashboard", { replace: true }); navigate("/config/blueprint/dashboard", { replace: true });
@@ -314,13 +275,6 @@ class HaBlueprintOverview extends LitElement {
} }
} }
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("blueprints")) {
this._loadUsageCounts();
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
@@ -330,11 +284,7 @@ class HaBlueprintOverview extends LitElement {
.route=${this.route} .route=${this.route}
.tabs=${configSections.automations} .tabs=${configSections.automations}
.columns=${this._columns(this.hass.localize)} .columns=${this._columns(this.hass.localize)}
.data=${this._processedBlueprints( .data=${this._processedBlueprints(this.blueprints, this.hass.localize)}
this.blueprints,
this.hass.localize,
this._usageCounts
)}
id="fullpath" id="fullpath"
.noDataText=${this.hass.localize( .noDataText=${this.hass.localize(
"ui.panel.config.blueprint.overview.no_blueprints" "ui.panel.config.blueprint.overview.no_blueprints"
@@ -349,7 +299,7 @@ class HaBlueprintOverview extends LitElement {
> >
<ha-button <ha-button
appearance="plain" appearance="plain"
href=${documentationUrl(this.hass, "/get-blueprints")} href="https://www.home-assistant.io/get-blueprints"
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
size="small" size="small"
@@ -430,51 +380,10 @@ class HaBlueprintOverview extends LitElement {
fireEvent(this, "reload-blueprints"); 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>) { private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const blueprint = this._processedBlueprints( const blueprint = this._processedBlueprints(
this.blueprints, this.blueprints,
this.hass.localize, this.hass.localize
this._usageCounts
).find((b) => b.fullpath === ev.detail.id)!; ).find((b) => b.fullpath === ev.detail.id)!;
if (blueprint.error) { if (blueprint.error) {
showAlertDialog(this, { showAlertDialog(this, {
@@ -488,25 +397,6 @@ class HaBlueprintOverview extends LitElement {
this._createNew(blueprint); 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) => { private _showUsed = (blueprint: BlueprintMetaDataPath) => {
navigate( navigate(
`/config/${blueprint.domain}/dashboard?blueprint=${encodeURIComponent( `/config/${blueprint.domain}/dashboard?blueprint=${encodeURIComponent(

View File

@@ -2,7 +2,9 @@ import { mdiDotsVertical, mdiDownload } from "@mdi/js";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../components/ha-button-menu";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-list-item";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { getSignedPath } from "../../../data/auth"; import { getSignedPath } from "../../../data/auth";
import "../../../layouts/hass-subpage"; import "../../../layouts/hass-subpage";
@@ -12,8 +14,6 @@ import {
downloadFileSupported, downloadFileSupported,
fileDownload, fileDownload,
} from "../../../util/file_download"; } from "../../../util/file_download";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-dropdown";
@customElement("ha-config-section-analytics") @customElement("ha-config-section-analytics")
class HaConfigSectionAnalytics extends LitElement { class HaConfigSectionAnalytics extends LitElement {
@@ -33,19 +33,22 @@ class HaConfigSectionAnalytics extends LitElement {
> >
${downloadFileSupported(this.hass) ${downloadFileSupported(this.hass)
? html` ? html`
<ha-dropdown <ha-button-menu
@wa-select=${this._handleOverflowAction} @action=${this._handleOverflowAction}
slot="toolbar-icon" slot="toolbar-icon"
> >
<ha-icon-button slot="trigger" .path=${mdiDotsVertical}> <ha-icon-button slot="trigger" .path=${mdiDotsVertical}>
</ha-icon-button> </ha-icon-button>
<ha-dropdown-item .value=${"download_device_info"}> <ha-list-item graphic="icon">
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon> <ha-svg-icon
slot="graphic"
.path=${mdiDownload}
></ha-svg-icon>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.analytics.download_device_info" "ui.panel.config.analytics.download_device_info"
)} )}
</ha-dropdown-item> </ha-list-item>
</ha-dropdown> </ha-button-menu>
` `
: nothing} : nothing}
<div class="content"> <div class="content">
@@ -55,17 +58,10 @@ class HaConfigSectionAnalytics extends LitElement {
`; `;
} }
private async _handleOverflowAction( private async _handleOverflowAction(): Promise<void> {
ev: CustomEvent<{ item: { value: string } }> const signedPath = await getSignedPath(this.hass, "/api/analytics/devices");
): Promise<void> {
if (ev.detail.item.value === "download_device_info") {
const signedPath = await getSignedPath(
this.hass,
"/api/analytics/devices"
);
fileDownload(signedPath.path); fileDownload(signedPath.path);
} }
}
static styles = css` static styles = css`
.content { .content {

View File

@@ -1,3 +1,4 @@
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical, mdiRefresh } from "@mdi/js"; import { mdiDotsVertical, mdiRefresh } from "@mdi/js";
import type { HassEntities } from "home-assistant-js-websocket"; import type { HassEntities } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
@@ -5,9 +6,13 @@ import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-bar"; import "../../../components/ha-bar";
import "../../../components/ha-button-menu";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-check-list-item";
import "../../../components/ha-list-item";
import "../../../components/ha-metric"; import "../../../components/ha-metric";
import { extractApiErrorMessage } from "../../../data/hassio/common"; import { extractApiErrorMessage } from "../../../data/hassio/common";
import type { import type {
@@ -28,9 +33,6 @@ import "../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import "../dashboard/ha-config-updates"; import "../dashboard/ha-config-updates";
import { showJoinBetaDialog } from "./updates/show-dialog-join-beta"; 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") @customElement("ha-config-section-updates")
class HaConfigSectionUpdates extends LitElement { class HaConfigSectionUpdates extends LitElement {
@@ -71,25 +73,24 @@ class HaConfigSectionUpdates extends LitElement {
.path=${mdiRefresh} .path=${mdiRefresh}
@click=${this._checkUpdates} @click=${this._checkUpdates}
></ha-icon-button> ></ha-icon-button>
<ha-dropdown @wa-select=${this._handleOverflowAction}> <ha-button-menu multi>
<ha-icon-button <ha-icon-button
slot="trigger" slot="trigger"
.label=${this.hass.localize("ui.common.menu")} .label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
></ha-icon-button> ></ha-icon-button>
<ha-check-list-item
<ha-dropdown-item left
type="checkbox" @request-selected=${this._toggleSkipped}
value="show_skipped" .selected=${this._showSkipped}
.checked=${this._showSkipped}
> >
${this.hass.localize("ui.panel.config.updates.show_skipped")} ${this.hass.localize("ui.panel.config.updates.show_skipped")}
</ha-dropdown-item> </ha-check-list-item>
${this._supervisorInfo ${this._supervisorInfo
? html` ? html`
<wa-divider></wa-divider> <li divider role="separator"></li>
<ha-dropdown-item <ha-list-item
value="toggle_beta" @request-selected=${this._toggleBeta}
.disabled=${this._supervisorInfo.channel === "dev"} .disabled=${this._supervisorInfo.channel === "dev"}
> >
${this._supervisorInfo.channel === "stable" ${this._supervisorInfo.channel === "stable"
@@ -97,10 +98,10 @@ class HaConfigSectionUpdates extends LitElement {
: this.hass.localize( : this.hass.localize(
"ui.panel.config.updates.leave_beta" "ui.panel.config.updates.leave_beta"
)} )}
</ha-dropdown-item> </ha-list-item>
` `
: ""} : ""}
</ha-dropdown> </ha-button-menu>
</div> </div>
<div class="content"> <div class="content">
<ha-card outlined> <ha-card outlined>
@@ -132,10 +133,21 @@ class HaConfigSectionUpdates extends LitElement {
this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass); this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
} }
private async _handleOverflowAction( private _toggleSkipped(ev: CustomEvent<RequestSelectedDetail>): void {
ev: CustomEvent<{ item: { value: string } }> if (ev.detail.source !== "property") {
return;
}
this._showSkipped = !this._showSkipped;
}
private async _toggleBeta(
ev: CustomEvent<RequestSelectedDetail>
): Promise<void> { ): Promise<void> {
if (ev.detail.item.value === "toggle_beta") { if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
if (this._supervisorInfo!.channel === "stable") { if (this._supervisorInfo!.channel === "stable") {
showJoinBetaDialog(this, { showJoinBetaDialog(this, {
join: async () => this._setChannel("beta"), join: async () => this._setChannel("beta"),
@@ -143,9 +155,6 @@ class HaConfigSectionUpdates extends LitElement {
} else { } else {
this._setChannel("stable"); this._setChannel("stable");
} }
} else if (ev.detail.item.value === "show_skipped") {
this._showSkipped = !this._showSkipped;
}
} }
private async _setChannel( private async _setChannel(

View File

@@ -2,7 +2,6 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../../components/ha-alert"; import "../../../../components/ha-alert";
import type { EnergyValidationIssue } from "../../../../data/energy"; import type { EnergyValidationIssue } from "../../../../data/energy";
import { documentationUrl } from "../../../../util/documentation-url";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
@customElement("ha-energy-validation-result") @customElement("ha-energy-validation-result")
@@ -30,10 +29,7 @@ class EnergyValidationMessage extends LitElement {
)} )}
${issue.type === "recorder_untracked" ${issue.type === "recorder_untracked"
? html`(<a ? html`(<a
href=${documentationUrl( href="https://www.home-assistant.io/integrations/recorder#configure-filter"
this.hass,
"/integrations/recorder#configure-filter"
)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
>${this.hass.localize("ui.panel.config.common.learn_more")}</a >${this.hass.localize("ui.panel.config.common.learn_more")}</a

View File

@@ -5,7 +5,6 @@ import {
mdiCancel, mdiCancel,
mdiChevronRight, mdiChevronRight,
mdiCog, mdiCog,
mdiDelete,
mdiDotsVertical, mdiDotsVertical,
mdiMenuDown, mdiMenuDown,
mdiPencilOff, mdiPencilOff,
@@ -25,10 +24,6 @@ import { computeCssColor } from "../../../common/color/compute-color";
import { storage } from "../../../common/decorators/storage"; import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event"; import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeStateDomain } from "../../../common/entity/compute_state_domain"; 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 { navigate } from "../../../common/navigate";
import type { import type {
LocalizeFunc, LocalizeFunc,
@@ -114,11 +109,10 @@ import { configSections } from "../ha-panel-config";
import "../integrations/ha-integration-overflow-menu"; import "../integrations/ha-integration-overflow-menu";
import { renderConfigEntryError } from "../integrations/ha-config-integration-page"; import { renderConfigEntryError } from "../integrations/ha-config-integration-page";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import { isHelperDomain, type HelperDomain } from "./const"; import { isHelperDomain } from "./const";
import { showHelperDetailDialog } from "./show-dialog-helper-detail"; import { showHelperDetailDialog } from "./show-dialog-helper-detail";
import { slugify } from "../../../common/string/slugify"; import { slugify } from "../../../common/string/slugify";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { HELPERS_CRUD } from "../../../data/helpers_crud";
import { import {
fetchDiagnosticHandlers, fetchDiagnosticHandlers,
getConfigEntryDiagnosticsDownloadUrl, getConfigEntryDiagnosticsDownloadUrl,
@@ -126,11 +120,6 @@ import {
import { getSignedPath } from "../../../data/auth"; import { getSignedPath } from "../../../data/auth";
import { fileDownload } from "../../../util/file_download"; import { fileDownload } from "../../../util/file_download";
const HELPER_ENTITY_NAME: EntityNameItem[] = [
{ type: "area" },
...DEFAULT_ENTITY_NAME,
];
interface HelperItem { interface HelperItem {
id: string; id: string;
name: string; name: string;
@@ -462,19 +451,6 @@ 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> </ha-icon-overflow-menu>
@@ -514,7 +490,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
return { return {
id: entityState.entity_id, id: entityState.entity_id,
name: this._formatHelperName(entityState), name: entityState.attributes.friendly_name || "",
entity_id: entityState.entity_id, entity_id: entityState.entity_id,
editable: editable:
configEntry !== undefined || entityState.attributes.editable, configEntry !== undefined || entityState.attributes.editable,
@@ -593,14 +569,6 @@ 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[] { private _labelsForEntity(entityId: string): string[] {
return ( return (
this.hass.entities[entityId]?.labels || this.hass.entities[entityId]?.labels ||
@@ -1312,62 +1280,6 @@ ${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() { private _createHelper() {
showHelperDetailDialog(this, {}); showHelperDetailDialog(this, {});
} }

View File

@@ -1,10 +1,4 @@
import { import { mdiBookshelf, mdiCog, mdiDotsVertical, mdiOpenInNew } from "@mdi/js";
mdiBookshelf,
mdiCog,
mdiDelete,
mdiDotsVertical,
mdiOpenInNew,
} from "@mdi/js";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { LitElement, css, html } from "lit"; import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
@@ -13,11 +7,6 @@ import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-button"; import "../../../components/ha-button";
import "../../../components/ha-button-menu"; import "../../../components/ha-button-menu";
import "../../../components/ha-list-item"; import "../../../components/ha-list-item";
import {
deleteApplicationCredential,
fetchApplicationCredentialsConfigEntry,
} from "../../../data/application_credential";
import { deleteConfigEntry } from "../../../data/config_entries";
import { import {
ATTENTION_SOURCES, ATTENTION_SOURCES,
DISCOVERY_SOURCES, DISCOVERY_SOURCES,
@@ -26,10 +15,7 @@ import {
} from "../../../data/config_flow"; } from "../../../data/config_flow";
import type { IntegrationManifest } from "../../../data/integration"; import type { IntegrationManifest } from "../../../data/integration";
import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow";
import { import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations"; import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
@@ -74,7 +60,7 @@ export class HaConfigFlowCard extends LitElement {
: "ui.common.add" : "ui.common.add"
)} )}
</ha-button> </ha-button>
${this.flow.context.configuration_url || this.manifest || attention ${this.flow.context.configuration_url || this.manifest
? html`<ha-button-menu slot="header-button"> ? html`<ha-button-menu slot="header-button">
<ha-icon-button <ha-icon-button
slot="trigger" slot="trigger"
@@ -132,22 +118,6 @@ export class HaConfigFlowCard extends LitElement {
</ha-list-item> </ha-list-item>
</a>` </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-button-menu>`
: ""} : ""}
</ha-integration-action-card> </ha-integration-action-card>
@@ -205,109 +175,6 @@ 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` static styles = css`
a { a {
text-decoration: none; text-decoration: none;
@@ -324,9 +191,6 @@ export class HaConfigFlowCard extends LitElement {
--mdc-theme-primary: var(--error-color); --mdc-theme-primary: var(--error-color);
--ha-card-border-color: 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: color:
route.route_status === "Active" route.route_status === "Active"
? primaryColor ? primaryColor
: style.getPropertyValue("--dark-primary-color"), : style.getPropertyValue("--disabled-color"),
type: ["Child", "Parent"].includes(neighbor.relationship) type: ["Child", "Parent"].includes(neighbor.relationship)
? "solid" ? "solid"
: "dotted", : "dotted",
@@ -335,7 +335,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
symbolSize: 5, symbolSize: 5,
lineStyle: { lineStyle: {
width: 1, width: 1,
color: style.getPropertyValue("--dark-primary-color"), color: style.getPropertyValue("--disabled-color"),
type: "dotted", type: "dotted",
}, },
ignoreForceLayout: true, ignoreForceLayout: true,

View File

@@ -16,7 +16,6 @@ import type { HomeAssistant } from "../../../types";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { brandsUrl } from "../../../util/brands-url"; import { brandsUrl } from "../../../util/brands-url";
import { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
import { documentationUrl } from "../../../util/documentation-url";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { showLabsPreviewFeatureEnableDialog } from "./show-dialog-labs-preview-feature-enable"; import { showLabsPreviewFeatureEnableDialog } from "./show-dialog-labs-preview-feature-enable";
import { import {
@@ -101,7 +100,7 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
? html` ? html`
<a <a
slot="toolbar-icon" slot="toolbar-icon"
href=${documentationUrl(this.hass, "/integrations/labs/")} href="https://www.home-assistant.io/integrations/labs/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
.title=${this.hass.localize("ui.common.help")} .title=${this.hass.localize("ui.common.help")}
@@ -125,7 +124,7 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
"ui.panel.config.labs.empty.description" "ui.panel.config.labs.empty.description"
)} )}
<a <a
href=${documentationUrl(this.hass, "/integrations/labs/")} href="https://www.home-assistant.io/integrations/labs/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >

View File

@@ -62,7 +62,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
return nothing; return nothing;
} }
const defaultPanelUrlPath = const defaultPanelUrlPath =
this.hass.systemData?.default_panel || DEFAULT_PANEL; this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
const titleInvalid = !this._data.title || !this._data.title.trim(); const titleInvalid = !this._data.title || !this._data.title.trim();
return html` return html`
@@ -260,7 +260,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
return; return;
} }
const defaultPanel = this.hass.systemData?.default_panel || DEFAULT_PANEL; const defaultPanel = this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
// Add warning dialog to saying that this will change the default dashboard for all users // Add warning dialog to saying that this will change the default dashboard for all users
const confirm = await showConfirmationDialog(this, { const confirm = await showConfirmationDialog(this, {
title: this.hass.localize( title: this.hass.localize(
@@ -284,7 +284,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
saveFrontendSystemData(this.hass.connection, "core", { saveFrontendSystemData(this.hass.connection, "core", {
...this.hass.systemData, ...this.hass.systemData,
default_panel: urlPath === defaultPanel ? undefined : urlPath, defaultPanel: urlPath === defaultPanel ? undefined : urlPath,
}); });
} }
@@ -309,20 +309,7 @@ export class DialogLovelaceDashboardDetail extends LitElement {
} }
this.closeDialog(); this.closeDialog();
} catch (err: any) { } catch (err: any) {
let localizedErrorMessage: string | undefined; this._error = { base: err?.message || "Unknown error" };
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 { } finally {
this._submitting = false; this._submitting = false;
} }

View File

@@ -404,7 +404,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
return html` <hass-loading-screen></hass-loading-screen> `; return html` <hass-loading-screen></hass-loading-screen> `;
} }
const defaultPanel = this.hass.systemData?.default_panel || DEFAULT_PANEL; const defaultPanel = this.hass.systemData?.defaultPanel || DEFAULT_PANEL;
return html` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table

View File

@@ -270,7 +270,6 @@ export class HaManualScriptEditor extends LitElement {
@value-changed=${this._sidebarConfigChanged} @value-changed=${this._sidebarConfigChanged}
@sidebar-resized=${this._resizeSidebar} @sidebar-resized=${this._resizeSidebar}
@sidebar-resizing-stopped=${this._stopResizeSidebar} @sidebar-resizing-stopped=${this._stopResizeSidebar}
@sidebar-reset-size=${this._resetSidebarWidth}
></ha-automation-sidebar> ></ha-automation-sidebar>
</div> </div>
</div> </div>
@@ -619,16 +618,6 @@ export class HaManualScriptEditor extends LitElement {
this._prevSidebarWidthPx = undefined; 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 { static get styles(): CSSResultGroup {
return [ return [
saveFabStyles, saveFabStyles,

View File

@@ -2,7 +2,6 @@ import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { documentationUrl } from "../../../util/documentation-url";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-button"; import "../../../components/ha-button";
import { createCloseHeading } from "../../../components/ha-dialog"; import { createCloseHeading } from "../../../components/ha-dialog";
@@ -15,6 +14,8 @@ import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import type { TagDetailDialogParams } from "./show-dialog-tag-detail"; import type { TagDetailDialogParams } from "./show-dialog-tag-detail";
const TAG_BASE = "https://www.home-assistant.io/tag/";
@customElement("dialog-tag-detail") @customElement("dialog-tag-detail")
class DialogTagDetail class DialogTagDetail
extends LitElement extends LitElement
@@ -121,7 +122,7 @@ class DialogTagDetail
</div> </div>
<div id="qr"> <div id="qr">
<ha-qr-code <ha-qr-code
.data=${`${documentationUrl(this.hass, "/tag/")}${this._params!.entry!.id}`} .data=${`${TAG_BASE}${this._params!.entry!.id}`}
center-image="/static/icons/favicon-192x192.png" center-image="/static/icons/favicon-192x192.png"
error-correction-level="quartile" error-correction-level="quartile"
scale="5" scale="5"

View File

@@ -1,151 +0,0 @@
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

@@ -1,20 +0,0 @@
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 { customElement, property, state } from "lit/decorators";
import { debounce } from "../../common/util/debounce"; import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal"; 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 { LovelaceDashboardStrategyConfig } from "../../data/lovelace/config/types";
import type { HomeAssistant, PanelInfo, Route } from "../../types"; import type { HomeAssistant, PanelInfo, Route } from "../../types";
import { showToast } from "../../util/toast";
import "../lovelace/hui-root"; import "../lovelace/hui-root";
import { generateLovelaceDashboardStrategy } from "../lovelace/strategies/get-strategy"; import { generateLovelaceDashboardStrategy } from "../lovelace/strategies/get-strategy";
import type { Lovelace } from "../lovelace/types"; import type { Lovelace } from "../lovelace/types";
import { showEditHomeDialog } from "./dialogs/show-dialog-edit-home"; import { showAlertDialog } from "../lovelace/custom-card-helpers";
const HOME_LOVELACE_CONFIG: LovelaceDashboardStrategyConfig = {
strategy: {
type: "home",
},
};
@customElement("ha-panel-home") @customElement("ha-panel-home")
class PanelHome extends LitElement { class PanelHome extends LitElement {
@@ -28,14 +28,12 @@ class PanelHome extends LitElement {
@state() private _lovelace?: Lovelace; @state() private _lovelace?: Lovelace;
@state() private _config: FrontendSystemData["home"] = {};
public willUpdate(changedProps: PropertyValues) { public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps); super.willUpdate(changedProps);
// Initial setup // Initial setup
if (!this.hasUpdated) { if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace"); this.hass.loadFragmentTranslation("lovelace");
this._loadConfig(); this._setLovelace();
return; return;
} }
@@ -97,28 +95,9 @@ 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() { private async _setLovelace() {
const strategyConfig: LovelaceDashboardStrategyConfig = {
strategy: {
type: "home",
favorite_entities: this._config.favorite_entities,
},
};
const config = await generateLovelaceDashboardStrategy( const config = await generateLovelaceDashboardStrategy(
strategyConfig, HOME_LOVELACE_CONFIG,
this.hass this.hass
); );
@@ -142,34 +121,15 @@ class PanelHome extends LitElement {
} }
private _setEditMode = () => { private _setEditMode = () => {
showEditHomeDialog(this, { // For now, we just show an alert that edit mode is not supported.
config: this._config, // This will be expanded in the future.
saveConfig: async (config) => { showAlertDialog(this, {
await this._saveConfig(config); title: "Edit mode not available",
}, text: "The Home panel does not support edit mode.",
confirmText: this.hass.localize("ui.common.ok"),
}); });
}; };
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` static readonly styles: CSSResultGroup = css`
:host { :host {
display: block; display: block;

View File

@@ -7,7 +7,6 @@ import "../../components/ha-settings-row";
import "../../components/ha-switch"; import "../../components/ha-switch";
import type { CoreFrontendUserData } from "../../data/frontend"; import type { CoreFrontendUserData } from "../../data/frontend";
import { saveFrontendUserData } from "../../data/frontend"; import { saveFrontendUserData } from "../../data/frontend";
import { documentationUrl } from "../../util/documentation-url";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
@customElement("ha-advanced-mode-row") @customElement("ha-advanced-mode-row")
@@ -32,10 +31,7 @@ class AdvancedModeRow extends LitElement {
<span slot="description"> <span slot="description">
${this.hass.localize("ui.panel.profile.advanced_mode.description")} ${this.hass.localize("ui.panel.profile.advanced_mode.description")}
<a <a
href=${documentationUrl( href="https://www.home-assistant.io/blog/2019/07/17/release-96/#advanced-mode"
this.hass,
"/blog/2019/07/17/release-96/#advanced-mode"
)}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
>${this.hass.localize("ui.panel.profile.advanced_mode.link_promo")} >${this.hass.localize("ui.panel.profile.advanced_mode.link_promo")}

View File

@@ -25,7 +25,7 @@ class HaPickDashboardRow extends LitElement {
} }
protected render(): TemplateResult { protected render(): TemplateResult {
const value = this.hass.userData?.default_panel || USE_SYSTEM_VALUE; const value = this.hass.userData?.defaultPanel || USE_SYSTEM_VALUE;
return html` return html`
<ha-settings-row .narrow=${this.narrow}> <ha-settings-row .narrow=${this.narrow}>
<span slot="heading"> <span slot="heading">
@@ -84,12 +84,12 @@ class HaPickDashboardRow extends LitElement {
return; return;
} }
const urlPath = value === USE_SYSTEM_VALUE ? undefined : value; const urlPath = value === USE_SYSTEM_VALUE ? undefined : value;
if (urlPath === this.hass.userData?.default_panel) { if (urlPath === this.hass.userData?.defaultPanel) {
return; return;
} }
saveFrontendUserData(this.hass.connection, "core", { saveFrontendUserData(this.hass.connection, "core", {
...this.hass.userData, ...this.hass.userData,
default_panel: urlPath, defaultPanel: urlPath,
}); });
} }
} }

View File

@@ -767,6 +767,9 @@
"no_match": "No languages found for {term}", "no_match": "No languages found for {term}",
"no_languages": "No languages available" "no_languages": "No languages available"
}, },
"icon-picker": {
"no_match": "No matching icons found"
},
"tts-picker": { "tts-picker": {
"tts": "Text-to-speech", "tts": "Text-to-speech",
"none": "None" "none": "None"
@@ -2220,14 +2223,6 @@
"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." "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": { "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": { "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.", "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.", "component_not_loaded": "This redirect is not supported by your Home Assistant instance. You need the integration {integration} to use this redirect.",
@@ -3265,10 +3260,7 @@
"create_helper": "Create helper", "create_helper": "Create helper",
"no_helpers": "Looks like you don't have any helpers yet!", "no_helpers": "Looks like you don't have any helpers yet!",
"search": "Search {number} {number, plural,\n one {helper}\n other {helpers}\n}", "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": { "dialog": {
"create": "Create", "create": "Create",
@@ -4796,8 +4788,7 @@
"headers": { "headers": {
"name": "Name", "name": "Name",
"type": "Type", "type": "Type",
"file_name": "File name", "file_name": "File name"
"usage_count": "In use"
}, },
"types": { "types": {
"automation": "Automation", "automation": "Automation",