20240626.0 (#21171)

This commit is contained in:
Bram Kragten 2024-06-26 12:49:50 +02:00 committed by GitHub
commit 8d0c4e4a52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
138 changed files with 4877 additions and 2809 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@v4.1.6 uses: actions/checkout@v4.1.7
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@v4.1.6 uses: actions/checkout@v4.1.7
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@v4.1.6 uses: actions/checkout@v4.1.7
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2
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@v4.1.6 uses: actions/checkout@v4.1.7
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2
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@v4.1.6 uses: actions/checkout@v4.1.7
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2
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@v4.1.6 uses: actions/checkout@v4.1.7
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2
with: with:

View File

@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.7
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.

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@v4.1.6 uses: actions/checkout@v4.1.7
with: with:
ref: dev ref: dev
@ -58,7 +58,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@v4.1.6 uses: actions/checkout@v4.1.7
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@v4.1.6 uses: actions/checkout@v4.1.7
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2

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@v4.1.6 uses: actions/checkout@v4.1.7
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2

View File

@ -20,7 +20,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.7
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5 uses: actions/setup-python@v5

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@v4.1.6 uses: actions/checkout@v4.1.7
- name: Verify version - name: Verify version
uses: home-assistant/actions/helpers/verify-version@master uses: home-assistant/actions/helpers/verify-version@master
@ -55,7 +55,7 @@ jobs:
script/release script/release
- name: Upload release assets - name: Upload release assets
uses: softprops/action-gh-release@v2.0.5 uses: softprops/action-gh-release@v2.0.6
with: with:
files: | files: |
dist/*.whl dist/*.whl

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@v4.1.6 uses: actions/checkout@v4.1.7
- name: Upload Translations - name: Upload Translations
run: | run: |

File diff suppressed because one or more lines are too long

View File

@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.2.2.cjs yarnPath: .yarn/releases/yarn-4.3.1.cjs

View File

@ -92,8 +92,8 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
[ [
"@babel/preset-env", "@babel/preset-env",
{ {
useBuiltIns: latestBuild ? false : "usage", useBuiltIns: "usage",
corejs: latestBuild ? false : dependencies["core-js"], corejs: dependencies["core-js"],
bugfixes: true, bugfixes: true,
shippedProposals: true, shippedProposals: true,
}, },

View File

@ -1,7 +1,6 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { ActionDetail } from "@material/mwc-list/mwc-list";
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js"; import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-listbox/paper-listbox";
import { Auth, Connection } from "home-assistant-js-websocket"; import { Auth, Connection } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@ -28,6 +27,7 @@ import { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
import "../../../../src/layouts/hass-loading-screen"; import "../../../../src/layouts/hass-loading-screen";
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config"; import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
import "./hc-layout"; import "./hc-layout";
import "../../../../src/components/ha-list-item";
@customElement("hc-cast") @customElement("hc-cast")
class HcCast extends LitElement { class HcCast extends LitElement {
@ -83,37 +83,37 @@ class HcCast extends LitElement {
` `
: html` : html`
<div class="section-header">PICK A VIEW</div> <div class="section-header">PICK A VIEW</div>
<paper-listbox <mwc-list @action=${this._handlePickView} activatable>
attr-for-selected="data-path"
.selected=${this.castManager.status.lovelacePath || ""}
>
${( ${(
this.lovelaceViews ?? [ this.lovelaceViews ?? [
generateDefaultViewConfig({}, {}, {}, {}, () => ""), generateDefaultViewConfig({}, {}, {}, {}, () => ""),
] ]
).map( ).map(
(view, idx) => html` (view, idx) =>
<paper-icon-item html`<ha-list-item
@click=${this._handlePickView} graphic="avatar"
data-path=${view.path || idx} .activated=${this.castManager.status?.lovelacePath ===
(view.path ?? idx)}
.selected=${this.castManager.status?.lovelacePath ===
(view.path ?? idx)}
> >
${view.title || view.path || "Unnamed view"}
${view.icon ${view.icon
? html` ? html`
<ha-icon <ha-icon
.icon=${view.icon} .icon=${view.icon}
slot="item-icon" slot="graphic"
></ha-icon> ></ha-icon>
` `
: html`<ha-svg-icon : html`<ha-svg-icon
slot="item-icon" slot="item-icon"
.path=${mdiViewDashboard} .path=${mdiViewDashboard}
></ha-svg-icon>`} ></ha-svg-icon>`}</ha-list-item
${view.title || view.path || "Unnamed view"} > `
</paper-icon-item> )}</mwc-list
` >
)}
</paper-listbox>
`} `}
<div class="card-actions"> <div class="card-actions">
${this.castManager.status ${this.castManager.status
? html` ? html`
@ -185,8 +185,8 @@ class HcCast extends LitElement {
this.castManager.requestSession(); this.castManager.requestSession();
} }
private async _handlePickView(ev: Event) { private async _handlePickView(ev: CustomEvent<ActionDetail>) {
const path = (ev.currentTarget as any).getAttribute("data-path"); const path = this.lovelaceViews![ev.detail.index].path ?? ev.detail.index;
await ensureConnectedCastSession(this.castManager!, this.auth!); await ensureConnectedCastSession(this.castManager!, this.auth!);
castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path); castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path);
} }
@ -249,26 +249,14 @@ class HcCast extends LitElement {
height: 18px; height: 18px;
} }
paper-listbox { ha-list-item ha-icon,
padding-top: 0; ha-list-item ha-svg-icon {
}
paper-listbox ha-icon,
paper-listbox ha-svg-icon {
padding: 12px; padding: 12px;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
paper-icon-item { :host([hide-icons]) ha-icon {
cursor: pointer; display: none;
}
paper-icon-item[disabled] {
cursor: initial;
}
:host([hide-icons]) paper-icon-item {
--paper-item-icon-width: 0px;
} }
.spacer { .spacer {

View File

@ -1,8 +1,9 @@
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until"; import { until } from "lit/directives/until";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-button";
import "../../../src/components/ha-circular-progress"; import "../../../src/components/ha-circular-progress";
import { LovelaceCardConfig } from "../../../src/data/lovelace/config/card"; import { LovelaceCardConfig } from "../../../src/data/lovelace/config/card";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
@ -11,7 +12,6 @@ import {
demoConfigs, demoConfigs,
selectedDemoConfig, selectedDemoConfig,
selectedDemoConfigIndex, selectedDemoConfigIndex,
setDemoConfig,
} from "../configs/demo-configs"; } from "../configs/demo-configs";
@customElement("ha-demo-card") @customElement("ha-demo-card")
@ -64,9 +64,9 @@ export class HADemoCard extends LitElement implements LovelaceCard {
)} )}
</div> </div>
<mwc-button @click=${this._nextConfig} .disabled=${this._switching}> <ha-button @click=${this._nextConfig} .disabled=${this._switching}>
${this.hass.localize("ui.panel.page-demo.cards.demo.next_demo")} ${this.hass.localize("ui.panel.page-demo.cards.demo.next_demo")}
</mwc-button> </ha-button>
</div> </div>
<div class="content"> <div class="content">
<p class="small-hidden"> <p class="small-hidden">
@ -87,9 +87,9 @@ export class HADemoCard extends LitElement implements LovelaceCard {
</div> </div>
<div class="actions small-hidden"> <div class="actions small-hidden">
<a href="https://www.home-assistant.io" target="_blank"> <a href="https://www.home-assistant.io" target="_blank">
<mwc-button> <ha-button>
${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")} ${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")}
</mwc-button> </ha-button>
</a> </a>
</div> </div>
</ha-card> </ha-card>
@ -113,13 +113,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
private async _updateConfig(index: number) { private async _updateConfig(index: number) {
this._switching = true; this._switching = true;
try { fireEvent(this, "set-demo-config" as any, { index });
await setDemoConfig(this.hass, this.lovelace!, index);
} catch (err: any) {
alert("Failed to switch config :-(");
} finally {
this._switching = false;
}
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
@ -149,7 +143,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
height: 60px; height: 60px;
} }
.picker mwc-button { .picker ha-button {
margin-right: 8px; margin-right: 8px;
} }

View File

@ -1,9 +1,12 @@
import type { LocalizeFunc } from "../../../src/common/translations/localize"; import type { LocalizeFunc } from "../../../src/common/translations/localize";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import { selectedDemoConfig } from "../configs/demo-configs"; import {
selectedDemoConfig,
selectedDemoConfigIndex,
setDemoConfig,
} from "../configs/demo-configs";
import "../custom-cards/cast-demo-row"; import "../custom-cards/cast-demo-row";
import "../custom-cards/ha-demo-card"; import "../custom-cards/ha-demo-card";
import type { HADemoCard } from "../custom-cards/ha-demo-card";
export const mockLovelace = ( export const mockLovelace = (
hass: MockHomeAssistant, hass: MockHomeAssistant,
@ -19,17 +22,22 @@ export const mockLovelace = (
hass.mockWS("lovelace/resources", () => Promise.resolve([])); hass.mockWS("lovelace/resources", () => Promise.resolve([]));
}; };
customElements.whenDefined("hui-card").then(() => { customElements.whenDefined("hui-root").then(() => {
// eslint-disable-next-line // eslint-disable-next-line
const HUIView = customElements.get("hui-card"); const HUIRoot = customElements.get("hui-root")!;
// Patch HUI-VIEW to make the lovelace object available to the demo card
const oldCreateCard = HUIView!.prototype.createElement;
HUIView!.prototype.createElement = function (config) { const oldFirstUpdated = HUIRoot.prototype.firstUpdated;
const el = oldCreateCard.call(this, config);
if (config.type === "custom:ha-demo-card") { HUIRoot.prototype.firstUpdated = function (changedProperties) {
(el as HADemoCard).lovelace = this.lovelace; oldFirstUpdated.call(this, changedProperties);
this.addEventListener("set-demo-config", async (ev) => {
const index = (ev as CustomEvent).detail.index;
try {
await setDemoConfig(this.hass, this.lovelace!, index);
} catch (err: any) {
setDemoConfig(this.hass, this.lovelace!, selectedDemoConfigIndex);
alert("Failed to switch config :-(");
} }
return el; });
}; };
}); });

View File

@ -1,7 +1,9 @@
import { load } from "js-yaml"; import { load } from "js-yaml";
import { html, css, LitElement, PropertyValues } from "lit"; import { LitElement, PropertyValueMap, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { createCardElement } from "../../../src/panels/lovelace/create-element/create-card-element"; import memoizeOne from "memoize-one";
import "../../../src/panels/lovelace/cards/hui-card";
import type { HuiCard } from "../../../src/panels/lovelace/cards/hui-card";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
export interface DemoCardConfig { export interface DemoCardConfig {
@ -19,7 +21,12 @@ class DemoCard extends LitElement {
@state() private _size?: number; @state() private _size?: number;
@query("#card") private _card!: HTMLElement; @query("hui-card", false) private _card?: HuiCard;
private _config = memoizeOne((config: string) => {
const c = (load(config) as any)[0];
return c;
});
render() { render() {
return html` return html`
@ -30,63 +37,32 @@ class DemoCard extends LitElement {
: ""} : ""}
</h2> </h2>
<div class="root"> <div class="root">
<div id="card"></div> <hui-card
${this.showConfig ? html`<pre>${this.config.config.trim()}</pre>` : ""} .config=${this._config(this.config.config)}
.hass=${this.hass}
@card-updated=${this._cardUpdated}
></hui-card>
${this.showConfig
? html`<pre>${this.config.config.trim()}</pre>`
: nothing}
</div> </div>
`; `;
} }
updated(changedProps: PropertyValues) { private async _cardUpdated(ev) {
super.updated(changedProps);
if (changedProps.has("config")) {
const card = this._card;
while (card.lastChild) {
card.removeChild(card.lastChild);
}
const el = this._createCardElement((load(this.config.config) as any)[0]);
card.appendChild(el);
this._getSize(el);
}
if (changedProps.has("hass")) {
const card = this._card.lastChild;
if (card) {
(card as any).hass = this.hass;
}
}
}
async _getSize(el) {
await customElements.whenDefined(el.localName);
if (!("getCardSize" in el)) {
this._size = undefined;
return;
}
this._size = await el.getCardSize();
}
_createCardElement(cardConfig) {
const element = createCardElement(cardConfig);
if (this.hass) {
element.hass = this.hass;
}
element.addEventListener(
"ll-rebuild",
(ev) => {
ev.stopPropagation(); ev.stopPropagation();
this._rebuildCard(element, cardConfig); this._updateSize();
},
{ once: true }
);
return element;
} }
_rebuildCard(cardElToReplace, config) { private async _updateSize() {
const newCardEl = this._createCardElement(config); this._size = await this._card?.getCardSize();
cardElToReplace.parentElement.replaceChild(newCardEl, cardElToReplace); }
protected update(
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
): void {
super.update(_changedProperties);
this._updateSize();
} }
static styles = css` static styles = css`
@ -101,7 +77,7 @@ class DemoCard extends LitElement {
font-size: 0.5em; font-size: 0.5em;
color: var(--primary-text-color); color: var(--primary-text-color);
} }
#card { hui-card {
max-width: 400px; max-width: 400px;
width: 100vw; width: 100vw;
} }

View File

@ -1,5 +1,6 @@
import Fuse from "fuse.js";
import type { IFuseOptions } from "fuse.js"; import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import { stripDiacritics } from "../../../src/common/string/strip-diacritics";
import { StoreAddon } from "../../../src/data/supervisor/store"; import { StoreAddon } from "../../../src/data/supervisor/store";
export function filterAndSort(addons: StoreAddon[], filter: string) { export function filterAndSort(addons: StoreAddon[], filter: string) {
@ -8,7 +9,8 @@ export function filterAndSort(addons: StoreAddon[], filter: string) {
isCaseSensitive: false, isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2), minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2, threshold: 0.2,
getFn: (obj, path) => stripDiacritics(Fuse.config.getFn(obj, path)),
}; };
const fuse = new Fuse(addons, options); const fuse = new Fuse(addons, options);
return fuse.search(filter).map((result) => result.item); return fuse.search(stripDiacritics(filter)).map((result) => result.item);
} }

View File

@ -26,14 +26,14 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@babel/runtime": "7.24.7", "@babel/runtime": "7.24.7",
"@braintree/sanitize-url": "7.0.2", "@braintree/sanitize-url": "7.0.3",
"@codemirror/autocomplete": "6.16.2", "@codemirror/autocomplete": "6.16.3",
"@codemirror/commands": "6.6.0", "@codemirror/commands": "6.6.0",
"@codemirror/language": "6.10.2", "@codemirror/language": "6.10.2",
"@codemirror/legacy-modes": "6.4.0", "@codemirror/legacy-modes": "6.4.0",
"@codemirror/search": "6.5.6", "@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1", "@codemirror/state": "6.4.1",
"@codemirror/view": "6.27.0", "@codemirror/view": "6.28.2",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.5", "@formatjs/intl-datetimeformat": "6.12.5",
"@formatjs/intl-displaynames": "6.6.8", "@formatjs/intl-displaynames": "6.6.8",
@ -88,8 +88,8 @@
"@polymer/paper-tabs": "3.1.0", "@polymer/paper-tabs": "3.1.0",
"@polymer/polymer": "3.5.1", "@polymer/polymer": "3.5.1",
"@thomasloven/round-slider": "0.6.0", "@thomasloven/round-slider": "0.6.0",
"@vaadin/combo-box": "24.3.13", "@vaadin/combo-box": "24.4.0",
"@vaadin/vaadin-themable-mixin": "24.3.13", "@vaadin/vaadin-themable-mixin": "24.4.0",
"@vibrant/color": "3.2.1-alpha.1", "@vibrant/color": "3.2.1-alpha.1",
"@vibrant/core": "3.2.1-alpha.1", "@vibrant/core": "3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1", "@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
@ -110,7 +110,7 @@
"fuse.js": "7.0.0", "fuse.js": "7.0.0",
"google-timezones-json": "1.2.0", "google-timezones-json": "1.2.0",
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch", "hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
"home-assistant-js-websocket": "9.3.0", "home-assistant-js-websocket": "9.4.0",
"idb-keyval": "6.2.1", "idb-keyval": "6.2.1",
"intl-messageformat": "10.5.14", "intl-messageformat": "10.5.14",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
@ -160,15 +160,15 @@
"@lokalise/node-api": "12.5.0", "@lokalise/node-api": "12.5.0",
"@octokit/auth-oauth-device": "7.1.1", "@octokit/auth-oauth-device": "7.1.1",
"@octokit/plugin-retry": "7.1.1", "@octokit/plugin-retry": "7.1.1",
"@octokit/rest": "20.1.1", "@octokit/rest": "21.0.0",
"@open-wc/dev-server-hmr": "0.1.4", "@open-wc/dev-server-hmr": "0.1.4",
"@rollup/plugin-babel": "6.0.4", "@rollup/plugin-babel": "6.0.4",
"@rollup/plugin-commonjs": "25.0.8", "@rollup/plugin-commonjs": "26.0.1",
"@rollup/plugin-json": "6.1.0", "@rollup/plugin-json": "6.1.0",
"@rollup/plugin-node-resolve": "15.2.3", "@rollup/plugin-node-resolve": "15.2.3",
"@rollup/plugin-replace": "5.0.7", "@rollup/plugin-replace": "5.0.7",
"@types/babel__plugin-transform-runtime": "7.9.5", "@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.14", "@types/chromecast-caf-receiver": "6.0.15",
"@types/chromecast-caf-sender": "1.0.10", "@types/chromecast-caf-sender": "1.0.10",
"@types/color-name": "1.1.4", "@types/color-name": "1.1.4",
"@types/glob": "8.1.0", "@types/glob": "8.1.0",
@ -178,15 +178,15 @@
"@types/leaflet-draw": "1.0.11", "@types/leaflet-draw": "1.0.11",
"@types/lodash.merge": "4.6.9", "@types/lodash.merge": "4.6.9",
"@types/luxon": "3.4.2", "@types/luxon": "3.4.2",
"@types/mocha": "10.0.6", "@types/mocha": "10.0.7",
"@types/qrcode": "1.5.5", "@types/qrcode": "1.5.5",
"@types/serve-handler": "6.1.4", "@types/serve-handler": "6.1.4",
"@types/sortablejs": "1.15.8", "@types/sortablejs": "1.15.8",
"@types/tar": "6.1.13", "@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39", "@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29", "@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "7.12.0", "@typescript-eslint/eslint-plugin": "7.13.1",
"@typescript-eslint/parser": "7.12.0", "@typescript-eslint/parser": "7.13.1",
"@web/dev-server": "0.1.38", "@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1", "@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3", "babel-loader": "9.1.3",
@ -205,7 +205,7 @@
"eslint-plugin-wc": "2.1.0", "eslint-plugin-wc": "2.1.0",
"fancy-log": "2.0.0", "fancy-log": "2.0.0",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"glob": "10.4.1", "glob": "10.4.2",
"gulp": "5.0.0", "gulp": "5.0.0",
"gulp-json-transform": "0.5.0", "gulp-json-transform": "0.5.0",
"gulp-rename": "2.0.0", "gulp-rename": "2.0.0",
@ -214,7 +214,7 @@
"husky": "9.0.11", "husky": "9.0.11",
"instant-mocha": "1.5.2", "instant-mocha": "1.5.2",
"jszip": "3.10.1", "jszip": "3.10.1",
"lint-staged": "15.2.5", "lint-staged": "15.2.7",
"lit-analyzer": "2.0.3", "lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2", "lodash.merge": "4.6.2",
"lodash.template": "4.5.0", "lodash.template": "4.5.0",
@ -224,7 +224,7 @@
"object-hash": "3.0.0", "object-hash": "3.0.0",
"open": "10.1.0", "open": "10.1.0",
"pinst": "3.0.0", "pinst": "3.0.0",
"prettier": "3.3.1", "prettier": "3.3.2",
"rollup": "2.79.1", "rollup": "2.79.1",
"rollup-plugin-string": "3.0.0", "rollup-plugin-string": "3.0.0",
"rollup-plugin-terser": "7.0.2", "rollup-plugin-terser": "7.0.2",
@ -233,12 +233,12 @@
"sinon": "18.0.0", "sinon": "18.0.0",
"source-map-url": "0.4.1", "source-map-url": "0.4.1",
"systemjs": "6.15.1", "systemjs": "6.15.1",
"tar": "7.2.0", "tar": "7.4.0",
"terser-webpack-plugin": "5.3.10", "terser-webpack-plugin": "5.3.10",
"transform-async-modules-webpack-plugin": "1.1.1", "transform-async-modules-webpack-plugin": "1.1.1",
"ts-lit-plugin": "2.0.2", "ts-lit-plugin": "2.0.2",
"typescript": "5.4.5", "typescript": "5.5.2",
"webpack": "5.91.0", "webpack": "5.92.1",
"webpack-cli": "5.1.4", "webpack-cli": "5.1.4",
"webpack-dev-server": "5.0.4", "webpack-dev-server": "5.0.4",
"webpack-manifest-plugin": "5.0.0", "webpack-manifest-plugin": "5.0.0",
@ -257,5 +257,5 @@
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch", "sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch" "leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
}, },
"packageManager": "yarn@4.2.2" "packageManager": "yarn@4.3.1"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,3 @@
<svg width="1200" height="1227" viewBox="0 0 1200 1227" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 430 B

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20240610.1" version = "20240626.0"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@ -31,6 +31,7 @@ import {
mdiFormatListBulleted, mdiFormatListBulleted,
mdiFormatListCheckbox, mdiFormatListCheckbox,
mdiFormTextbox, mdiFormTextbox,
mdiForumOutline,
mdiGauge, mdiGauge,
mdiGoogleAssistant, mdiGoogleAssistant,
mdiGoogleCirclesCommunities, mdiGoogleCirclesCommunities,
@ -98,7 +99,7 @@ export const FIXED_DOMAIN_ICONS = {
calendar: mdiCalendar, calendar: mdiCalendar,
climate: mdiThermostat, climate: mdiThermostat,
configurator: mdiCog, configurator: mdiCog,
conversation: mdiMicrophoneMessage, conversation: mdiForumOutline,
counter: mdiCounter, counter: mdiCounter,
date: mdiCalendar, date: mdiCalendar,
datetime: mdiCalendarClock, datetime: mdiCalendarClock,
@ -235,6 +236,8 @@ export const SENSOR_ENTITIES = [
"weather", "weather",
]; ];
export const ASSIST_ENTITIES = ["conversation", "stt", "tts"];
/** Domains that render an input element instead of a text value when displayed in a row. /** Domains that render an input element instead of a text value when displayed in a row.
* Those rows should then not show a cursor pointer when hovered (which would normally * Those rows should then not show a cursor pointer when hovered (which would normally
* be the default) unless the element itself enforces it (e.g. a button). Also those elements * be the default) unless the element itself enforces it (e.g. a button). Also those elements

View File

@ -0,0 +1 @@
export const preventDefault = (ev) => ev.preventDefault();

View File

@ -1,3 +1,4 @@
import { stripDiacritics } from "../strip-diacritics";
import { fuzzyScore } from "./filter"; import { fuzzyScore } from "./filter";
/** /**
@ -19,10 +20,10 @@ export const fuzzySequentialMatch = (
for (const word of item.strings) { for (const word of item.strings) {
const scores = fuzzyScore( const scores = fuzzyScore(
filter, filter,
filter.toLowerCase(), stripDiacritics(filter.toLowerCase()),
0, 0,
word, word,
word.toLowerCase(), stripDiacritics(word.toLowerCase()),
0, 0,
true true
); );

View File

@ -0,0 +1,2 @@
export const stripDiacritics = (str) =>
str.normalize("NFD").replace(/[\u0300-\u036F]/g, "");

View File

@ -0,0 +1,280 @@
import "@material/mwc-list";
import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { haStyleDialog } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import { createCloseHeading } from "../ha-dialog";
import "../ha-list-item";
import "../ha-sortable";
import "../ha-button";
import { DataTableColumnContainer, DataTableColumnData } from "./ha-data-table";
import { DataTableSettingsDialogParams } from "./show-dialog-data-table-settings";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("dialog-data-table-settings")
export class DialogDataTableSettings extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: DataTableSettingsDialogParams;
@state() private _columnOrder?: string[];
@state() private _hiddenColumns?: string[];
public showDialog(params: DataTableSettingsDialogParams) {
this._params = params;
this._columnOrder = params.columnOrder;
this._hiddenColumns = params.hiddenColumns;
}
public closeDialog() {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private _sortedColumns = memoizeOne(
(
columns: DataTableColumnContainer,
columnOrder: string[] | undefined,
hiddenColumns: string[] | undefined
) =>
Object.keys(columns)
.filter((col) => !columns[col].hidden)
.sort((a, b) => {
const orderA = columnOrder?.indexOf(a) ?? -1;
const orderB = columnOrder?.indexOf(b) ?? -1;
const hiddenA =
hiddenColumns?.includes(a) ?? Boolean(columns[a].defaultHidden);
const hiddenB =
hiddenColumns?.includes(b) ?? Boolean(columns[b].defaultHidden);
if (hiddenA !== hiddenB) {
return hiddenA ? 1 : -1;
}
if (orderA !== orderB) {
if (orderA === -1) {
return 1;
}
if (orderB === -1) {
return -1;
}
}
return orderA - orderB;
})
.reduce(
(arr, key) => {
arr.push({ key, ...columns[key] });
return arr;
},
[] as (DataTableColumnData & { key: string })[]
)
);
protected render() {
if (!this._params) {
return nothing;
}
const columns = this._sortedColumns(
this._params.columns,
this._columnOrder,
this._hiddenColumns
);
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.components.data-table.settings.header")
)}
>
<ha-sortable
@item-moved=${this._columnMoved}
draggable-selector=".draggable"
handle-selector=".handle"
>
<mwc-list>
${repeat(
columns,
(col) => col.key,
(col, _idx) => {
const canMove = !col.main && col.moveable !== false;
const canHide = !col.main && col.hideable !== false;
const isVisible = !(this._columnOrder &&
this._columnOrder.includes(col.key)
? this._hiddenColumns?.includes(col.key) ?? col.defaultHidden
: col.defaultHidden);
return html`<ha-list-item
hasMeta
class=${classMap({
hidden: !isVisible,
draggable: canMove && isVisible,
})}
graphic="icon"
noninteractive
>${col.title || col.label || col.key}
${canMove && isVisible
? html`<ha-svg-icon
class="handle"
.path=${mdiDrag}
slot="graphic"
></ha-svg-icon>`
: nothing}
<ha-icon-button
tabindex="0"
class="action"
.disabled=${!canHide}
.hidden=${!isVisible}
.path=${isVisible ? mdiEye : mdiEyeOff}
slot="meta"
.label=${this.hass!.localize(
`ui.components.data-table.settings.${isVisible ? "hide" : "show"}`,
{ title: typeof col.title === "string" ? col.title : "" }
)}
.column=${col.key}
@click=${this._toggle}
></ha-icon-button>
</ha-list-item>`;
}
)}
</mwc-list>
</ha-sortable>
<ha-button slot="secondaryAction" @click=${this._reset}
>${this.hass.localize(
"ui.components.data-table.settings.restore"
)}</ha-button
>
<ha-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.components.data-table.settings.done")}
</ha-button>
</ha-dialog>
`;
}
private _columnMoved(ev: CustomEvent): void {
ev.stopPropagation();
if (!this._params) {
return;
}
const { oldIndex, newIndex } = ev.detail;
const columns = this._sortedColumns(
this._params.columns,
this._columnOrder,
this._hiddenColumns
);
const columnOrder = columns.map((column) => column.key);
const option = columnOrder.splice(oldIndex, 1)[0];
columnOrder.splice(newIndex, 0, option);
this._columnOrder = columnOrder;
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
}
_toggle(ev) {
if (!this._params) {
return;
}
const column = ev.target.column;
const wasHidden = ev.target.hidden;
const hidden = [
...(this._hiddenColumns ??
Object.entries(this._params.columns)
.filter(([_key, col]) => col.defaultHidden)
.map(([key]) => key)),
];
if (wasHidden && hidden.includes(column)) {
hidden.splice(hidden.indexOf(column), 1);
} else if (!wasHidden) {
hidden.push(column);
}
const columns = this._sortedColumns(
this._params.columns,
this._columnOrder,
this._hiddenColumns
);
if (!this._columnOrder) {
this._columnOrder = columns.map((col) => col.key);
} else {
columns.forEach((col) => {
if (!this._columnOrder!.includes(col.key)) {
this._columnOrder!.push(col.key);
if (col.defaultHidden) {
hidden.push(col.key);
}
}
});
}
this._hiddenColumns = hidden;
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
}
_reset() {
this._columnOrder = undefined;
this._hiddenColumns = undefined;
this._params!.onUpdate(this._columnOrder, this._hiddenColumns);
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 500px;
--dialog-z-index: 10;
--dialog-content-padding: 0 8px;
}
@media all and (max-width: 451px) {
ha-dialog {
--vertical-align-dialog: flex-start;
--dialog-surface-margin-top: 250px;
--ha-dialog-border-radius: 28px 28px 0 0;
--mdc-dialog-min-height: calc(100% - 250px);
--mdc-dialog-max-height: calc(100% - 250px);
}
}
ha-list-item {
--mdc-list-side-padding: 12px;
overflow: visible;
}
.hidden {
color: var(--disabled-text-color);
}
.handle {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
}
.actions {
display: flex;
flex-direction: row;
}
ha-icon-button {
display: block;
margin: -12px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-data-table-settings": DialogDataTableSettings;
}
}

View File

@ -65,6 +65,10 @@ export interface DataTableSortColumnData {
valueColumn?: string; valueColumn?: string;
direction?: SortingDirection; direction?: SortingDirection;
groupable?: boolean; groupable?: boolean;
moveable?: boolean;
hideable?: boolean;
defaultHidden?: boolean;
showNarrow?: boolean;
} }
export interface DataTableColumnData<T = any> extends DataTableSortColumnData { export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
@ -79,6 +83,7 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
| "overflow-menu" | "overflow-menu"
| "flex"; | "flex";
template?: (row: T) => TemplateResult | string | typeof nothing; template?: (row: T) => TemplateResult | string | typeof nothing;
extraTemplate?: (row: T) => TemplateResult | string | typeof nothing;
width?: string; width?: string;
maxWidth?: string; maxWidth?: string;
grows?: boolean; grows?: boolean;
@ -105,6 +110,8 @@ const UNDEFINED_GROUP_KEY = "zzzzz_undefined";
export class HaDataTable extends LitElement { export class HaDataTable extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@property({ type: Object }) public columns: DataTableColumnContainer = {}; @property({ type: Object }) public columns: DataTableColumnContainer = {};
@property({ type: Array }) public data: DataTableRowData[] = []; @property({ type: Array }) public data: DataTableRowData[] = [];
@ -145,6 +152,10 @@ export class HaDataTable extends LitElement {
@property({ attribute: false }) public initialCollapsedGroups?: string[]; @property({ attribute: false }) public initialCollapsedGroups?: string[];
@property({ attribute: false }) public hiddenColumns?: string[];
@property({ attribute: false }) public columnOrder?: string[];
@state() private _filterable = false; @state() private _filterable = false;
@state() private _filter = ""; @state() private _filter = "";
@ -235,6 +246,7 @@ export class HaDataTable extends LitElement {
(column: ClonedDataTableColumnData) => { (column: ClonedDataTableColumnData) => {
delete column.title; delete column.title;
delete column.template; delete column.template;
delete column.extraTemplate;
} }
); );
@ -272,12 +284,44 @@ export class HaDataTable extends LitElement {
this._sortFilterData(); this._sortFilterData();
} }
if (properties.has("selectable")) { if (properties.has("selectable") || properties.has("hiddenColumns")) {
this._items = [...this._items]; this._items = [...this._items];
} }
} }
private _sortedColumns = memoizeOne(
(columns: DataTableColumnContainer, columnOrder?: string[]) => {
if (!columnOrder || !columnOrder.length) {
return columns;
}
return Object.keys(columns)
.sort((a, b) => {
const orderA = columnOrder!.indexOf(a);
const orderB = columnOrder!.indexOf(b);
if (orderA !== orderB) {
if (orderA === -1) {
return 1;
}
if (orderB === -1) {
return -1;
}
}
return orderA - orderB;
})
.reduce((obj, key) => {
obj[key] = columns[key];
return obj;
}, {}) as DataTableColumnContainer;
}
);
protected render() { protected render() {
const columns = this._sortedColumns(this.columns, this.columnOrder);
const renderRow = (row: DataTableRowData, index: number) =>
this._renderRow(columns, this.narrow, row, index);
return html` return html`
<div class="mdc-data-table"> <div class="mdc-data-table">
<slot name="header" @slotchange=${this._calcTableHeight}> <slot name="header" @slotchange=${this._calcTableHeight}>
@ -326,9 +370,14 @@ export class HaDataTable extends LitElement {
</div> </div>
` `
: ""} : ""}
${Object.entries(this.columns).map(([key, column]) => { ${Object.entries(columns).map(([key, column]) => {
if (column.hidden) { if (
return ""; column.hidden ||
(this.columnOrder && this.columnOrder.includes(key)
? this.hiddenColumns?.includes(key) ?? column.defaultHidden
: column.defaultHidden)
) {
return nothing;
} }
const sorted = key === this.sortColumn; const sorted = key === this.sortColumn;
const classes = { const classes = {
@ -399,7 +448,7 @@ export class HaDataTable extends LitElement {
@scroll=${this._saveScrollPos} @scroll=${this._saveScrollPos}
.items=${this._items} .items=${this._items}
.keyFunction=${this._keyFunction} .keyFunction=${this._keyFunction}
.renderItem=${this._renderRow} .renderItem=${renderRow}
></lit-virtualizer> ></lit-virtualizer>
`} `}
</div> </div>
@ -409,7 +458,12 @@ export class HaDataTable extends LitElement {
private _keyFunction = (row: DataTableRowData) => row?.[this.id] || row; private _keyFunction = (row: DataTableRowData) => row?.[this.id] || row;
private _renderRow = (row: DataTableRowData, index: number) => { private _renderRow = (
columns: DataTableColumnContainer,
narrow: boolean,
row: DataTableRowData,
index: number
) => {
// not sure how this happens... // not sure how this happens...
if (!row) { if (!row) {
return nothing; return nothing;
@ -454,8 +508,14 @@ export class HaDataTable extends LitElement {
</div> </div>
` `
: ""} : ""}
${Object.entries(this.columns).map(([key, column]) => { ${Object.entries(columns).map(([key, column]) => {
if (column.hidden) { if (
(narrow && !column.main && !column.showNarrow) ||
column.hidden ||
(this.columnOrder && this.columnOrder.includes(key)
? this.hiddenColumns?.includes(key) ?? column.defaultHidden
: column.defaultHidden)
) {
return nothing; return nothing;
} }
return html` return html`
@ -482,7 +542,38 @@ export class HaDataTable extends LitElement {
}) })
: ""} : ""}
> >
${column.template ? column.template(row) : row[key]} ${column.template
? column.template(row)
: narrow && column.main
? html`<div class="primary">${row[key]}</div>
<div class="secondary">
${Object.entries(columns)
.filter(
([key2, column2]) =>
!column2.hidden &&
!column2.main &&
!column2.showNarrow &&
!(this.columnOrder &&
this.columnOrder.includes(key2)
? this.hiddenColumns?.includes(key2) ??
column2.defaultHidden
: column2.defaultHidden)
)
.map(
([key2, column2], i) =>
html`${i !== 0
? " ⸱ "
: nothing}${column2.template
? column2.template(row)
: row[key2]}`
)}
</div>
${column.extraTemplate
? column.extraTemplate(row)
: nothing}`
: html`${row[key]}${column.extraTemplate
? column.extraTemplate(row)
: nothing}`}
</div> </div>
`; `;
})} })}
@ -861,6 +952,7 @@ export class HaDataTable extends LitElement {
width: 100%; width: 100%;
border: 0; border: 0;
white-space: nowrap; white-space: nowrap;
position: relative;
} }
.mdc-data-table__cell { .mdc-data-table__cell {

View File

@ -0,0 +1,26 @@
import { fireEvent } from "../../common/dom/fire_event";
import { DataTableColumnContainer } from "./ha-data-table";
export interface DataTableSettingsDialogParams {
columns: DataTableColumnContainer;
onUpdate: (
columnOrder: string[] | undefined,
hiddenColumns: string[] | undefined
) => void;
hiddenColumns?: string[];
columnOrder?: string[];
}
export const loadDataTableSettingsDialog = () =>
import("./dialog-data-table-settings");
export const showDataTableSettingsDialog = (
element: HTMLElement,
dialogParams: DataTableSettingsDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-data-table-settings",
dialogImport: loadDataTableSettingsDialog,
dialogParams,
});
};

View File

@ -1,5 +1,6 @@
import { expose } from "comlink"; import { expose } from "comlink";
import { stringCompare } from "../../common/string/compare"; import { stringCompare } from "../../common/string/compare";
import { stripDiacritics } from "../../common/string/strip-diacritics";
import type { import type {
ClonedDataTableColumnData, ClonedDataTableColumnData,
DataTableRowData, DataTableRowData,
@ -12,20 +13,18 @@ const filterData = (
columns: SortableColumnContainer, columns: SortableColumnContainer,
filter: string filter: string
) => { ) => {
filter = filter.toUpperCase(); filter = stripDiacritics(filter.toLowerCase());
return data.filter((row) => return data.filter((row) =>
Object.entries(columns).some((columnEntry) => { Object.entries(columns).some((columnEntry) => {
const [key, column] = columnEntry; const [key, column] = columnEntry;
if (column.filterable) { if (column.filterable) {
if ( const value = String(
String(
column.filterKey column.filterKey
? row[column.valueColumn || key][column.filterKey] ? row[column.valueColumn || key][column.filterKey]
: row[column.valueColumn || key] : row[column.valueColumn || key]
) );
.toUpperCase()
.includes(filter) if (stripDiacritics(value).toLowerCase().includes(filter)) {
) {
return true; return true;
} }
} }

View File

@ -90,7 +90,8 @@ class HaAnsiToHtml extends LitElement {
private _parseTextToColoredPre(text) { private _parseTextToColoredPre(text) {
const pre = document.createElement("pre"); const pre = document.createElement("pre");
const re = /\033(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g; // eslint-disable-next-line no-control-regex
const re = /\x1b(?:\[(.*?)[@-~]|\].*?(?:\x07|\x1b\\))/g;
let i = 0; let i = 0;
const state: State = { const state: State = {

View File

@ -47,6 +47,8 @@ export class HaCodeEditor extends ReactiveElement {
@property({ type: Boolean }) public readOnly = false; @property({ type: Boolean }) public readOnly = false;
@property({ type: Boolean }) public linewrap = false;
@property({ type: Boolean, attribute: "autocomplete-entities" }) @property({ type: Boolean, attribute: "autocomplete-entities" })
public autocompleteEntities = false; public autocompleteEntities = false;
@ -134,6 +136,13 @@ export class HaCodeEditor extends ReactiveElement {
), ),
}); });
} }
if (changedProps.has("linewrap")) {
transactions.push({
effects: this._loadedCodeMirror!.linewrapCompartment!.reconfigure(
this.linewrap ? this._loadedCodeMirror!.EditorView.lineWrapping : []
),
});
}
if (changedProps.has("_value") && this._value !== this.value) { if (changedProps.has("_value") && this._value !== this.value) {
transactions.push({ transactions.push({
changes: { changes: {
@ -181,6 +190,9 @@ export class HaCodeEditor extends ReactiveElement {
this._loadedCodeMirror.readonlyCompartment.of( this._loadedCodeMirror.readonlyCompartment.of(
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly) this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
), ),
this._loadedCodeMirror.linewrapCompartment.of(
this.linewrap ? this._loadedCodeMirror.EditorView.lineWrapping : []
),
this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate), this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate),
]; ];

View File

@ -89,13 +89,18 @@ export class HaFilterDomains extends LitElement {
}); });
return Array.from(domains.values()) return Array.from(domains.values())
.map((domain) => ({
domain,
name: domainToName(this.hass.localize, domain),
}))
.filter( .filter(
(entry) => (entry) =>
!filter || !filter ||
entry.toLowerCase().includes(filter) || entry.domain.toLowerCase().includes(filter) ||
domainToName(this.hass.localize, entry).toLowerCase().includes(filter) entry.name.toLowerCase().includes(filter)
) )
.sort((a, b) => stringCompare(a, b, this.hass.locale.language)); .sort((a, b) => stringCompare(a.name, b.name, this.hass.locale.language))
.map((entry) => entry.domain);
}); });
protected updated(changed) { protected updated(changed) {

View File

@ -2,7 +2,7 @@ import type { Selector } from "../../data/selector";
import type { HaFormSchema } from "./types"; import type { HaFormSchema } from "./types";
export const computeInitialHaFormData = ( export const computeInitialHaFormData = (
schema: HaFormSchema[] schema: HaFormSchema[] | readonly HaFormSchema[]
): Record<string, any> => { ): Record<string, any> => {
const data = {}; const data = {};
schema.forEach((field) => { schema.forEach((field) => {
@ -36,6 +36,8 @@ export const computeInitialHaFormData = (
minutes: 0, minutes: 0,
seconds: 0, seconds: 0,
}; };
} else if (field.type === "expandable") {
data[field.name] = computeInitialHaFormData(field.schema);
} else if ("selector" in field) { } else if ("selector" in field) {
const selector: Selector = field.selector; const selector: Selector = field.selector;

View File

@ -0,0 +1,233 @@
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "./ha-icon-button";
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
import { mdiRestore } from "@mdi/js";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import { HomeAssistant } from "../types";
type GridSizeValue = {
rows?: number;
columns?: number;
};
@customElement("ha-grid-size-picker")
export class HaGridSizeEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: GridSizeValue;
@property({ attribute: false }) public rows = 6;
@property({ attribute: false }) public columns = 4;
@property({ attribute: false }) public rowMin?: number;
@property({ attribute: false }) public rowMax?: number;
@property({ attribute: false }) public columnMin?: number;
@property({ attribute: false }) public columnMax?: number;
@property({ attribute: false }) public isDefault?: boolean;
@state() public _localValue?: GridSizeValue = undefined;
protected willUpdate(changedProperties) {
if (changedProperties.has("value")) {
this._localValue = this.value;
}
}
protected render() {
return html`
<div class="grid">
<ha-grid-layout-slider
aria-label=${this.hass.localize(
"ui.components.grid-size-picker.columns"
)}
id="columns"
.min=${this.columnMin ?? 1}
.max=${this.columnMax ?? this.columns}
.range=${this.columns}
.value=${this.value?.columns}
@value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved}
></ha-grid-layout-slider>
<ha-grid-layout-slider
aria-label=${this.hass.localize(
"ui.components.grid-size-picker.rows"
)}
id="rows"
.min=${this.rowMin ?? 1}
.max=${this.rowMax ?? this.rows}
.range=${this.rows}
vertical
.value=${this.value?.rows}
@value-changed=${this._valueChanged}
@slider-moved=${this._sliderMoved}
></ha-grid-layout-slider>
${!this.isDefault
? html`
<ha-icon-button
@click=${this._reset}
class="reset"
.path=${mdiRestore}
label=${this.hass.localize(
"ui.components.grid-size-picker.reset_default"
)}
title=${this.hass.localize(
"ui.components.grid-size-picker.reset_default"
)}
>
</ha-icon-button>
`
: nothing}
<div
class="preview"
style=${styleMap({
"--total-rows": this.rows,
"--total-columns": this.columns,
"--rows": this._localValue?.rows,
"--columns": this._localValue?.columns,
})}
>
<div>
${Array(this.rows * this.columns)
.fill(0)
.map((_, index) => {
const row = Math.floor(index / this.columns) + 1;
const column = (index % this.columns) + 1;
const disabled =
(this.rowMin !== undefined && row < this.rowMin) ||
(this.rowMax !== undefined && row > this.rowMax) ||
(this.columnMin !== undefined && column < this.columnMin) ||
(this.columnMax !== undefined && column > this.columnMax);
return html`
<div
class="cell"
data-row=${row}
data-column=${column}
?disabled=${disabled}
@click=${this._cellClick}
></div>
`;
})}
</div>
<div class="selected">
<div class="cell"></div>
</div>
</div>
</div>
`;
}
_cellClick(ev) {
const cell = ev.currentTarget as HTMLElement;
if (cell.getAttribute("disabled") !== null) return;
const rows = Number(cell.getAttribute("data-row"));
const columns = Number(cell.getAttribute("data-column"));
fireEvent(this, "value-changed", {
value: { rows, columns },
});
}
private _valueChanged(ev) {
ev.stopPropagation();
const key = ev.currentTarget.id;
const newValue = {
...this.value,
[key]: ev.detail.value,
};
fireEvent(this, "value-changed", { value: newValue });
}
private _reset(ev) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
rows: undefined,
columns: undefined,
},
});
}
private _sliderMoved(ev) {
ev.stopPropagation();
const key = ev.currentTarget.id;
const value = ev.detail.value;
if (value === undefined) return;
this._localValue = {
...this.value,
[key]: ev.detail.value,
};
}
static styles = [
css`
.grid {
display: grid;
grid-template-areas:
"reset column-slider"
"row-slider preview";
grid-template-rows: auto 1fr;
grid-template-columns: auto 1fr;
gap: 8px;
}
#columns {
grid-area: column-slider;
}
#rows {
grid-area: row-slider;
}
.reset {
grid-area: reset;
}
.preview {
position: relative;
grid-area: preview;
aspect-ratio: 1 / 1;
}
.preview > div {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: grid;
grid-template-columns: repeat(var(--total-columns), 1fr);
grid-template-rows: repeat(var(--total-rows), 1fr);
gap: 4px;
}
.preview .cell {
background-color: var(--disabled-color);
grid-column: span 1;
grid-row: span 1;
border-radius: 4px;
opacity: 0.2;
cursor: pointer;
}
.preview .cell[disabled] {
opacity: 0.05;
cursor: initial;
}
.selected {
pointer-events: none;
}
.selected .cell {
background-color: var(--primary-color);
grid-column: 1 / span var(--columns, 0);
grid-row: 1 / span var(--rows, 0);
opacity: 0.5;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-grid-size-picker": HaGridSizeEditor;
}
}

View File

@ -32,6 +32,7 @@ export class HaTemplateSelector extends LitElement {
autocomplete-icons autocomplete-icons
@value-changed=${this._handleChange} @value-changed=${this._handleChange}
dir="ltr" dir="ltr"
linewrap
></ha-code-editor> ></ha-code-editor>
${this.helper ${this.helper
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>` ? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`

View File

@ -65,6 +65,8 @@ interface ExtHassService extends Omit<HassService, "fields"> {
Omit<HassService["fields"][string], "selector"> & { Omit<HassService["fields"][string], "selector"> & {
key: string; key: string;
selector?: Selector; selector?: Selector;
fields?: Record<string, Omit<HassService["fields"][string], "selector">>;
collapsed?: boolean;
} }
>; >;
hasSelector: string[]; hasSelector: string[];
@ -247,20 +249,7 @@ export class HaServiceControl extends LitElement {
} }
); );
private _filterFields = memoizeOne( private _getTargetedEntities = memoizeOne((target, value) => {
(serviceData: ExtHassService | undefined, value: this["value"]) =>
serviceData?.fields?.filter(
(field) =>
!field.filter ||
this._filterField(serviceData.target, field.filter, value)
)
);
private _filterField(
target: ExtHassService["target"],
filter: ExtHassService["fields"][number]["filter"],
value: this["value"]
) {
const targetSelector = target ? { target } : { target: {} }; const targetSelector = target ? { target } : { target: {} };
const targetEntities = const targetEntities =
ensureArray( ensureArray(
@ -330,6 +319,13 @@ export class HaServiceControl extends LitElement {
); );
}); });
} }
return targetEntities;
});
private _filterField(
filter: ExtHassService["fields"][number]["filter"],
targetEntities: string[]
) {
if (!targetEntities.length) { if (!targetEntities.length) {
return false; return false;
} }
@ -391,7 +387,10 @@ export class HaServiceControl extends LitElement {
serviceData?.fields.some((field) => showOptionalToggle(field)) serviceData?.fields.some((field) => showOptionalToggle(field))
); );
const filteredFields = this._filterFields(serviceData, this._value); const targetEntities = this._getTargetedEntities(
serviceData?.target,
this._value
);
const domain = this._value?.service const domain = this._value?.service
? computeDomain(this._value.service) ? computeDomain(this._value.service)
@ -485,12 +484,54 @@ export class HaServiceControl extends LitElement {
.defaultValue=${this._value?.data} .defaultValue=${this._value?.data}
@value-changed=${this._dataChanged} @value-changed=${this._dataChanged}
></ha-yaml-editor>` ></ha-yaml-editor>`
: filteredFields?.map((dataField) => { : serviceData?.fields.map((dataField) =>
dataField.fields
? html`<ha-expansion-panel
leftChevron
.expanded=${!dataField.collapsed}
.header=${this.hass.localize(
`component.${domain}.services.${serviceName}.sections.${dataField.key}.name`
) ||
dataField.name ||
dataField.key}
>
${Object.entries(dataField.fields).map(([key, field]) =>
this._renderField(
{ key, ...field },
hasOptional,
domain,
serviceName,
targetEntities
)
)}
</ha-expansion-panel>`
: this._renderField(
dataField,
hasOptional,
domain,
serviceName,
targetEntities
)
)} `;
}
private _renderField = (
dataField: ExtHassService["fields"][number],
hasOptional: boolean,
domain: string | undefined,
serviceName: string | undefined,
targetEntities: string[]
) => {
if (
dataField.filter &&
!this._filterField(dataField.filter, targetEntities)
) {
return nothing;
}
const selector = dataField?.selector ?? { text: undefined }; const selector = dataField?.selector ?? { text: undefined };
const type = Object.keys(selector)[0]; const type = Object.keys(selector)[0];
const enhancedSelector = ["action", "condition", "trigger"].includes( const enhancedSelector = ["action", "condition", "trigger"].includes(type)
type
)
? { ? {
[type]: { [type]: {
...selector[type], ...selector[type],
@ -504,8 +545,7 @@ export class HaServiceControl extends LitElement {
return dataField.selector && return dataField.selector &&
(!dataField.advanced || (!dataField.advanced ||
this.showAdvanced || this.showAdvanced ||
(this._value?.data && (this._value?.data && this._value.data[dataField.key] !== undefined))
this._value.data[dataField.key] !== undefined))
? html`<ha-settings-row .narrow=${this.narrow}> ? html`<ha-settings-row .narrow=${this.narrow}>
${!showOptional ${!showOptional
? hasOptional ? hasOptional
@ -551,8 +591,7 @@ export class HaServiceControl extends LitElement {
></ha-selector> ></ha-selector>
</ha-settings-row>` </ha-settings-row>`
: ""; : "";
})} `; };
}
private _localizeValueCallback = (key: string) => { private _localizeValueCallback = (key: string) => {
if (!this._value?.service) { if (!this._value?.service) {
@ -839,6 +878,11 @@ export class HaServiceControl extends LitElement {
.description p { .description p {
direction: ltr; direction: ltr;
} }
ha-expansion-panel {
--ha-card-border-radius: 0;
--expansion-panel-summary-padding: 0 16px;
--expansion-panel-content-padding: 0;
}
`; `;
} }
} }

View File

@ -327,6 +327,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
for (const entityId of Object.keys(this.hass.states)) { for (const entityId of Object.keys(this.hass.states)) {
if ( if (
entityId.startsWith("update.") && entityId.startsWith("update.") &&
!this.hass.entities[entityId]?.hidden &&
updateCanInstall(this.hass.states[entityId] as UpdateEntity) updateCanInstall(this.hass.states[entityId] as UpdateEntity)
) { ) {
updateCount++; updateCount++;

View File

@ -138,6 +138,17 @@ export const adminChangePassword = (
password, password,
}); });
export const adminChangeUsername = (
hass: HomeAssistant,
userId: string,
username: string
) =>
hass.callWS<void>({
type: "config/auth_provider/homeassistant/admin_change_username",
user_id: userId,
username,
});
export const deleteAllRefreshTokens = ( export const deleteAllRefreshTokens = (
hass: HomeAssistant, hass: HomeAssistant,
token_type?: RefreshTokenType, token_type?: RefreshTokenType,

View File

@ -352,6 +352,22 @@ export const saveAutomationConfig = (
config: AutomationConfig config: AutomationConfig
) => hass.callApi<void>("POST", `config/automation/config/${id}`, config); ) => hass.callApi<void>("POST", `config/automation/config/${id}`, config);
export const normalizeAutomationConfig = <
T extends Partial<AutomationConfig> | AutomationConfig,
>(
config: T
): T => {
// Normalize data: ensure trigger, action and condition are lists
// Happens when people copy paste their automations into the config
for (const key of ["trigger", "condition", "action"]) {
const value = config[key];
if (value && !Array.isArray(value)) {
config[key] = [value];
}
}
return config;
};
export const showAutomationEditor = (data?: Partial<AutomationConfig>) => { export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
initialAutomationEditorData = data; initialAutomationEditorData = data;
navigate("/config/automation/edit/new"); navigate("/config/automation/edit/new");

View File

@ -1,4 +1,6 @@
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { ManualAutomationConfig } from "./automation";
import { ManualScriptConfig } from "./script";
import { Selector } from "./selector"; import { Selector } from "./selector";
export type BlueprintDomain = "automation" | "script"; export type BlueprintDomain = "automation" | "script";
@ -42,6 +44,11 @@ export interface BlueprintImportResult {
validation_errors: string[] | null; validation_errors: string[] | null;
} }
export interface BlueprintSubstituteResults {
automation: { substituted_config: ManualAutomationConfig };
script: { substituted_config: ManualScriptConfig };
}
export const fetchBlueprints = (hass: HomeAssistant, domain: BlueprintDomain) => export const fetchBlueprints = (hass: HomeAssistant, domain: BlueprintDomain) =>
hass.callWS<Blueprints>({ type: "blueprint/list", domain }); hass.callWS<Blueprints>({ type: "blueprint/list", domain });
@ -91,3 +98,18 @@ export const getBlueprintSourceType = (
} }
return "community"; return "community";
}; };
export const substituteBlueprint = <
T extends BlueprintDomain = BlueprintDomain,
>(
hass: HomeAssistant,
domain: T,
path: string,
input: Record<string, any>
) =>
hass.callWS<BlueprintSubstituteResults[T]>({
type: "blueprint/substitute",
domain,
path,
input,
});

View File

@ -6,6 +6,7 @@ export interface ConfigUpdateValues {
latitude: number; latitude: number;
longitude: number; longitude: number;
elevation: number; elevation: number;
radius: number;
unit_system: "metric" | "us_customary"; unit_system: "metric" | "us_customary";
time_zone: string; time_zone: string;
external_url?: string | null; external_url?: string | null;

View File

@ -249,6 +249,22 @@ export const localizeDeviceAutomationTrigger = (
) || ) ||
(trigger.subtype ? `"${trigger.subtype}" ${trigger.type}` : trigger.type!); (trigger.subtype ? `"${trigger.subtype}" ${trigger.type}` : trigger.type!);
export const localizeExtraFieldsComputeLabelCallback =
(hass: HomeAssistant, deviceAutomation: DeviceAutomation) =>
// Returns a callback for ha-form to calculate labels per schema object
(schema): string =>
hass.localize(
`component.${deviceAutomation.domain}.device_automation.extra_fields.${schema.name}`
) || schema.name;
export const localizeExtraFieldsComputeHelperCallback =
(hass: HomeAssistant, deviceAutomation: DeviceAutomation) =>
// Returns a callback for ha-form to calculate helper texts per schema object
(schema): string | undefined =>
hass.localize(
`component.${deviceAutomation.domain}.device_automation.extra_fields_descriptions.${schema.name}`
);
export const sortDeviceAutomations = ( export const sortDeviceAutomations = (
automationA: DeviceAutomation, automationA: DeviceAutomation,
automationB: DeviceAutomation automationB: DeviceAutomation

View File

@ -30,6 +30,7 @@ export interface LovelaceViewElement extends HTMLElement {
export interface LovelaceSectionElement extends HTMLElement { export interface LovelaceSectionElement extends HTMLElement {
hass?: HomeAssistant; hass?: HomeAssistant;
lovelace?: Lovelace; lovelace?: Lovelace;
preview?: boolean;
viewIndex?: number; viewIndex?: number;
index?: number; index?: number;
cards?: HuiCard[]; cards?: HuiCard[];

View File

@ -2,6 +2,7 @@ import { HomeAssistant } from "../types";
export interface OTBRInfo { export interface OTBRInfo {
active_dataset_tlvs: string; active_dataset_tlvs: string;
border_agent_id: string;
channel: number; channel: number;
extended_address: string; extended_address: string;
url: string; url: string;

21
src/data/threshold.ts Normal file
View File

@ -0,0 +1,21 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
export interface ThresholdPreview {
state: string;
attributes: Record<string, any>;
}
export const subscribePreviewThreshold = (
hass: HomeAssistant,
flow_id: string,
flow_type: "config_flow" | "options_flow",
user_input: Record<string, any>,
callback: (preview: ThresholdPreview) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(callback, {
type: "threshold/start_preview",
flow_id,
flow_type,
user_input,
});

View File

@ -1,6 +1,9 @@
import { import {
mdiAlertCircleOutline, mdiAlertCircleOutline,
mdiGauge, mdiGauge,
mdiThermometer,
mdiThermometerWater,
mdiSunWireless,
mdiWaterPercent, mdiWaterPercent,
mdiWeatherCloudy, mdiWeatherCloudy,
mdiWeatherFog, mdiWeatherFog,
@ -114,10 +117,15 @@ export const weatherIcons = {
}; };
export const weatherAttrIcons = { export const weatherAttrIcons = {
apparent_temperature: mdiThermometer,
cloud_coverage: mdiWeatherCloudy,
dew_point: mdiThermometerWater,
humidity: mdiWaterPercent, humidity: mdiWaterPercent,
wind_bearing: mdiWeatherWindy, wind_bearing: mdiWeatherWindy,
wind_speed: mdiWeatherWindy, wind_speed: mdiWeatherWindy,
pressure: mdiGauge, pressure: mdiGauge,
temperature: mdiThermometer,
uv_index: mdiSunWireless,
visibility: mdiWeatherFog, visibility: mdiWeatherFog,
precipitation: mdiWeatherRainy, precipitation: mdiWeatherRainy,
}; };
@ -221,6 +229,8 @@ export const getWeatherUnit = (
stateObj.attributes.pressure_unit || stateObj.attributes.pressure_unit ||
(lengthUnit === "km" ? "hPa" : "inHg") (lengthUnit === "km" ? "hPa" : "inHg")
); );
case "apparent_temperature":
case "dew_point":
case "temperature": case "temperature":
case "templow": case "templow":
return ( return (
@ -228,6 +238,7 @@ export const getWeatherUnit = (
); );
case "wind_speed": case "wind_speed":
return stateObj.attributes.wind_speed_unit || `${lengthUnit}/h`; return stateObj.attributes.wind_speed_unit || `${lengthUnit}/h`;
case "cloud_coverage":
case "humidity": case "humidity":
case "precipitation_probability": case "precipitation_probability":
return "%"; return "%";

View File

@ -14,6 +14,7 @@ export interface Zone {
export interface HomeZoneMutableParams { export interface HomeZoneMutableParams {
latitude: number; latitude: number;
longitude: number; longitude: number;
radius: number;
} }
export interface ZoneMutableParams { export interface ZoneMutableParams {

View File

@ -0,0 +1,108 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { FlowType } from "../../../data/data_entry_flow";
import {
ThresholdPreview,
subscribePreviewThreshold,
} from "../../../data/threshold";
import { HomeAssistant } from "../../../types";
import "./entity-preview-row";
import { debounce } from "../../../common/util/debounce";
import { fireEvent } from "../../../common/dom/fire_event";
@customElement("flow-preview-threshold")
class FlowPreviewThreshold extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public flowType!: FlowType;
public handler!: string;
@property() public stepId!: string;
@property() public flowId!: string;
@property() public stepData!: Record<string, any>;
@state() private _preview?: HassEntity;
@state() private _error?: string;
private _unsub?: Promise<UnsubscribeFunc>;
disconnectedCallback(): void {
super.disconnectedCallback();
if (this._unsub) {
this._unsub.then((unsub) => unsub());
this._unsub = undefined;
}
}
willUpdate(changedProps) {
if (changedProps.has("stepData")) {
this._debouncedSubscribePreview();
}
}
protected render() {
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}
return html`<entity-preview-row
.hass=${this.hass}
.stateObj=${this._preview}
></entity-preview-row>`;
}
private _setPreview = (preview: ThresholdPreview) => {
const now = new Date().toISOString();
this._preview = {
entity_id: `${this.stepId}.___flow_preview___`,
last_changed: now,
last_updated: now,
context: { id: "", parent_id: null, user_id: null },
...preview,
};
};
private _debouncedSubscribePreview = debounce(() => {
this._subscribePreview();
}, 250);
private async _subscribePreview() {
if (this._unsub) {
(await this._unsub)();
this._unsub = undefined;
}
if (this.flowType === "repair_flow") {
return;
}
try {
this._unsub = subscribePreviewThreshold(
this.hass,
this.flowId,
this.flowType,
this.stepData,
this._setPreview
);
await this._unsub;
fireEvent(this, "set-flow-errors", { errors: {} });
} catch (err: any) {
if (typeof err.message === "string") {
this._error = err.message;
} else {
this._error = undefined;
fireEvent(this, "set-flow-errors", err.message);
}
this._unsub = undefined;
this._preview = undefined;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"flow-preview-threshold": FlowPreviewThreshold;
}
}

View File

@ -126,7 +126,6 @@ class MoreInfoUpdate extends LitElement {
></ha-checkbox> ></ha-checkbox>
</ha-formfield> ` </ha-formfield> `
: ""} : ""}
<hr />
<div class="actions"> <div class="actions">
${this.stateObj.attributes.auto_update ${this.stateObj.attributes.auto_update
? "" ? ""
@ -240,10 +239,20 @@ class MoreInfoUpdate extends LitElement {
justify-content: space-between; justify-content: space-between;
} }
.actions { .actions {
border-top: 1px solid var(--divider-color);
background: var(
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
margin: 8px 0 0; margin: 8px 0 0;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
position: sticky;
bottom: 0;
padding: 12px 0;
margin-bottom: -24px;
z-index: 1;
} }
.actions mwc-button { .actions mwc-button {

View File

@ -2,7 +2,6 @@
import "../resources/compatibility"; import "../resources/compatibility";
import "../auth/ha-authorize"; import "../auth/ha-authorize";
import "../resources/safari-14-attachshadow-patch"; import "../resources/safari-14-attachshadow-patch";
import "../resources/array.flat.polyfill";
import("../resources/ha-style"); import("../resources/ha-style");
import("@polymer/polymer/lib/utils/settings").then( import("@polymer/polymer/lib/utils/settings").then(

View File

@ -25,7 +25,6 @@ import { subscribePanels } from "../data/ws-panels";
import { subscribeThemes } from "../data/ws-themes"; import { subscribeThemes } from "../data/ws-themes";
import { subscribeUser } from "../data/ws-user"; import { subscribeUser } from "../data/ws-user";
import type { ExternalAuth } from "../external_app/external_auth"; import type { ExternalAuth } from "../external_app/external_auth";
import "../resources/array.flat.polyfill";
import "../resources/safari-14-attachshadow-patch"; import "../resources/safari-14-attachshadow-patch";
window.name = MAIN_WINDOW_NAME; window.name = MAIN_WINDOW_NAME;

View File

@ -2,7 +2,6 @@
import "../resources/compatibility"; import "../resources/compatibility";
import "../onboarding/ha-onboarding"; import "../onboarding/ha-onboarding";
import "../resources/safari-14-attachshadow-patch"; import "../resources/safari-14-attachshadow-patch";
import "../resources/array.flat.polyfill";
import("../resources/ha-style"); import("../resources/ha-style");
import("@polymer/polymer/lib/utils/settings").then( import("@polymer/polymer/lib/utils/settings").then(

View File

@ -5,6 +5,7 @@ export const demoConfig: HassConfig = {
elevation: 300, elevation: 300,
latitude: 52.3731339, latitude: 52.3731339,
longitude: 4.8903147, longitude: 4.8903147,
radius: 100,
unit_system: { unit_system: {
length: "km", length: "km",
mass: "kg", mass: "kg",

View File

@ -6,6 +6,7 @@ import {
mdiArrowDown, mdiArrowDown,
mdiArrowUp, mdiArrowUp,
mdiClose, mdiClose,
mdiCog,
mdiFilterVariant, mdiFilterVariant,
mdiFilterVariantRemove, mdiFilterVariantRemove,
mdiFormatListChecks, mdiFormatListChecks,
@ -42,6 +43,7 @@ import "../components/search-input-outlined";
import type { HomeAssistant, Route } from "../types"; import type { HomeAssistant, Route } from "../types";
import "./hass-tabs-subpage"; import "./hass-tabs-subpage";
import type { PageNavigation } from "./hass-tabs-subpage"; import type { PageNavigation } from "./hass-tabs-subpage";
import { showDataTableSettingsDialog } from "../components/data-table/show-dialog-data-table-settings";
@customElement("hass-tabs-subpage-data-table") @customElement("hass-tabs-subpage-data-table")
export class HaTabsSubpageDataTable extends LitElement { export class HaTabsSubpageDataTable extends LitElement {
@ -171,6 +173,10 @@ export class HaTabsSubpageDataTable extends LitElement {
@property({ attribute: false }) public groupOrder?: string[]; @property({ attribute: false }) public groupOrder?: string[];
@property({ attribute: false }) public columnOrder?: string[];
@property({ attribute: false }) public hiddenColumns?: string[];
@state() private _sortColumn?: string; @state() private _sortColumn?: string;
@state() private _sortDirection: SortingDirection = null; @state() private _sortDirection: SortingDirection = null;
@ -290,6 +296,14 @@ export class HaTabsSubpageDataTable extends LitElement {
` `
: nothing; : nothing;
const settingsButton = html`<ha-assist-chip
class="has-dropdown select-mode-chip"
@click=${this._openSettings}
.title=${localize("ui.components.subpage-data-table.settings")}
>
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
</ha-assist-chip>`;
return html` return html`
<hass-tabs-subpage <hass-tabs-subpage
.hass=${this.hass} .hass=${this.hass}
@ -416,6 +430,7 @@ export class HaTabsSubpageDataTable extends LitElement {
: ""} : ""}
<ha-data-table <ha-data-table
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow}
.columns=${this.columns} .columns=${this.columns}
.data=${this.data} .data=${this.data}
.noDataText=${this.noDataText} .noDataText=${this.noDataText}
@ -430,6 +445,8 @@ export class HaTabsSubpageDataTable extends LitElement {
.groupColumn=${this._groupColumn} .groupColumn=${this._groupColumn}
.groupOrder=${this.groupOrder} .groupOrder=${this.groupOrder}
.initialCollapsedGroups=${this.initialCollapsedGroups} .initialCollapsedGroups=${this.initialCollapsedGroups}
.columnOrder=${this.columnOrder}
.hiddenColumns=${this.hiddenColumns}
> >
${!this.narrow ${!this.narrow
? html` ? html`
@ -438,7 +455,7 @@ export class HaTabsSubpageDataTable extends LitElement {
<div class="table-header"> <div class="table-header">
${this.hasFilters && !this.showFilters ${this.hasFilters && !this.showFilters
? html`${filterButton}` ? html`${filterButton}`
: nothing}${selectModeBtn}${searchBar}${groupByMenu}${sortByMenu} : nothing}${selectModeBtn}${searchBar}${groupByMenu}${sortByMenu}${settingsButton}
</div> </div>
</slot> </slot>
</div> </div>
@ -448,7 +465,7 @@ export class HaTabsSubpageDataTable extends LitElement {
${this.hasFilters && !this.showFilters ${this.hasFilters && !this.showFilters
? html`${filterButton}` ? html`${filterButton}`
: nothing} : nothing}
${selectModeBtn}${groupByMenu}${sortByMenu} ${selectModeBtn}${groupByMenu}${sortByMenu}${settingsButton}
</div>`} </div>`}
</ha-data-table>`} </ha-data-table>`}
<div slot="fab"><slot name="fab"></slot></div> <div slot="fab"><slot name="fab"></slot></div>
@ -608,6 +625,22 @@ export class HaTabsSubpageDataTable extends LitElement {
fireEvent(this, "grouping-changed", { value: columnId }); fireEvent(this, "grouping-changed", { value: columnId });
} }
private _openSettings() {
showDataTableSettingsDialog(this, {
columns: this.columns,
hiddenColumns: this.hiddenColumns,
columnOrder: this.columnOrder,
onUpdate: (
columnOrder: string[] | undefined,
hiddenColumns: string[] | undefined
) => {
this.columnOrder = columnOrder;
this.hiddenColumns = hiddenColumns;
fireEvent(this, "columns-changed", { columnOrder, hiddenColumns });
},
});
}
private _collapseAllGroups() { private _collapseAllGroups() {
this._dataTable.collapseAllGroups(); this._dataTable.collapseAllGroups();
} }
@ -874,6 +907,10 @@ declare global {
interface HASSDomEvents { interface HASSDomEvents {
"search-changed": { value: string }; "search-changed": { value: string };
"grouping-changed": { value: string }; "grouping-changed": { value: string };
"columns-changed": {
columnOrder: string[] | undefined;
hiddenColumns: string[] | undefined;
};
"clear-filter": undefined; "clear-filter": undefined;
} }
} }

View File

@ -72,11 +72,11 @@ class DialogCommunity extends LitElement {
<a <a
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
href="https://twitter.com/home_assistant" href="https://x.com/home_assistant"
> >
<ha-list-item hasMeta graphic="icon"> <ha-list-item hasMeta graphic="icon">
<img src="/static/images/logo_twitter.png" slot="graphic" /> <img class="x" src="/static/images/logo_x.svg" slot="graphic" />
${this.localize("ui.panel.page-onboarding.welcome.twitter")} ${this.localize("ui.panel.page-onboarding.welcome.x")}
<ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon> <ha-svg-icon slot="meta" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-list-item> </ha-list-item>
</a> </a>
@ -96,6 +96,12 @@ class DialogCommunity extends LitElement {
a { a {
text-decoration: none; text-decoration: none;
} }
@media (prefers-color-scheme: light) {
img.x {
filter: invert(1) hue-rotate(180deg);
}
}
`; `;
} }

View File

@ -11,6 +11,7 @@ import {
import "../../../components/ha-fab"; import "../../../components/ha-fab";
import "../../../components/ha-help-tooltip"; import "../../../components/ha-help-tooltip";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import "../../../components/ha-icon-overflow-menu";
import { import {
ApplicationCredential, ApplicationCredential,
deleteApplicationCredential, deleteApplicationCredential,
@ -70,6 +71,26 @@ export class HaConfigApplicationCredentials extends LitElement {
width: "30%", width: "30%",
direction: "asc", direction: "asc",
}, },
actions: {
title: "",
width: "64px",
type: "overflow-menu",
template: (credential) => html`
<ha-icon-overflow-menu
.hass=${this.hass}
narrow
.items=${[
{
path: mdiDelete,
warning: true,
label: this.hass.localize("ui.common.delete"),
action: () => this._removeCredential(credential),
},
]}
>
</ha-icon-overflow-menu>
`,
},
}; };
return columns; return columns;
@ -96,7 +117,7 @@ export class HaConfigApplicationCredentials extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.route=${this.route} .route=${this.route}
backPath="/config" back-path="/config"
.tabs=${configSections.devices} .tabs=${configSections.devices}
.columns=${this._columns(this.narrow, this.hass.localize)} .columns=${this._columns(this.narrow, this.hass.localize)}
.data=${this._getApplicationCredentials( .data=${this._getApplicationCredentials(
@ -153,6 +174,24 @@ export class HaConfigApplicationCredentials extends LitElement {
this._selected = ev.detail.value; this._selected = ev.detail.value;
} }
private _removeCredential = async (credential) => {
const confirm = await showConfirmationDialog(this, {
title: this.hass.localize(
`ui.panel.config.application_credentials.picker.remove.confirm_title`
),
text: this.hass.localize(
"ui.panel.config.application_credentials.picker.remove_selected.confirm_text"
),
confirmText: this.hass.localize("ui.common.delete"),
dismissText: this.hass.localize("ui.common.cancel"),
destructive: true,
});
if (!confirm) {
return;
}
await deleteApplicationCredential(this.hass, credential.id);
};
private _removeSelected() { private _removeSelected() {
showConfirmationDialog(this, { showConfirmationDialog(this, {
title: this.hass.localize( title: this.hass.localize(
@ -162,8 +201,9 @@ export class HaConfigApplicationCredentials extends LitElement {
text: this.hass.localize( text: this.hass.localize(
"ui.panel.config.application_credentials.picker.remove_selected.confirm_text" "ui.panel.config.application_credentials.picker.remove_selected.confirm_text"
), ),
confirmText: this.hass.localize("ui.common.remove"), confirmText: this.hass.localize("ui.common.delete"),
dismissText: this.hass.localize("ui.common.cancel"), dismissText: this.hass.localize("ui.common.cancel"),
destructive: true,
confirm: async () => { confirm: async () => {
try { try {
await Promise.all( await Promise.all(

View File

@ -12,6 +12,8 @@ import {
deviceAutomationsEqual, deviceAutomationsEqual,
DeviceCapabilities, DeviceCapabilities,
fetchDeviceActionCapabilities, fetchDeviceActionCapabilities,
localizeExtraFieldsComputeLabelCallback,
localizeExtraFieldsComputeHelperCallback,
} from "../../../../../data/device_automation"; } from "../../../../../data/device_automation";
import { EntityRegistryEntry } from "../../../../../data/entity_registry"; import { EntityRegistryEntry } from "../../../../../data/entity_registry";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
@ -84,8 +86,13 @@ export class HaDeviceAction extends LitElement {
.data=${this._extraFieldsData(this.action, this._capabilities)} .data=${this._extraFieldsData(this.action, this._capabilities)}
.schema=${this._capabilities.extra_fields} .schema=${this._capabilities.extra_fields}
.disabled=${this.disabled} .disabled=${this.disabled}
.computeLabel=${this._extraFieldsComputeLabelCallback( .computeLabel=${localizeExtraFieldsComputeLabelCallback(
this.hass.localize this.hass,
this.action
)}
.computeHelper=${localizeExtraFieldsComputeHelperCallback(
this.hass,
this.action
)} )}
@value-changed=${this._extraFieldsChanged} @value-changed=${this._extraFieldsChanged}
></ha-form> ></ha-form>
@ -152,14 +159,6 @@ export class HaDeviceAction extends LitElement {
}); });
} }
private _extraFieldsComputeLabelCallback(localize) {
// Returns a callback for ha-form to calculate labels per schema object
return (schema) =>
localize(
`ui.panel.config.automation.editor.actions.type.device_id.extra_fields.${schema.name}`
) || schema.name;
}
static styles = css` static styles = css`
ha-device-picker { ha-device-picker {
display: block; display: block;

View File

@ -54,6 +54,7 @@ import {
AddAutomationElementDialogParams, AddAutomationElementDialogParams,
PASTE_VALUE, PASTE_VALUE,
} from "./show-add-automation-element-dialog"; } from "./show-add-automation-element-dialog";
import { stripDiacritics } from "../../../common/string/strip-diacritics";
const TYPES = { const TYPES = {
trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS }, trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS },
@ -208,9 +209,10 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
isCaseSensitive: false, isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2), minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2, threshold: 0.2,
getFn: (obj, path) => stripDiacritics(Fuse.config.getFn(obj, path)),
}; };
const fuse = new Fuse(items, options); const fuse = new Fuse(items, options);
return fuse.search(filter).map((result) => result.item); return fuse.search(stripDiacritics(filter)).map((result) => result.item);
} }
); );

View File

@ -3,10 +3,10 @@ import { HassEntity } from "home-assistant-js-websocket";
import { html, nothing } from "lit"; import { html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-markdown";
import { BlueprintAutomationConfig } from "../../../data/automation"; import { BlueprintAutomationConfig } from "../../../data/automation";
import { fetchBlueprints } from "../../../data/blueprint"; import { fetchBlueprints } from "../../../data/blueprint";
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor"; import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
import "../../../components/ha-markdown";
@customElement("blueprint-automation-editor") @customElement("blueprint-automation-editor")
export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor { export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {

View File

@ -12,6 +12,8 @@ import {
DeviceCapabilities, DeviceCapabilities,
DeviceCondition, DeviceCondition,
fetchDeviceConditionCapabilities, fetchDeviceConditionCapabilities,
localizeExtraFieldsComputeLabelCallback,
localizeExtraFieldsComputeHelperCallback,
} from "../../../../../data/device_automation"; } from "../../../../../data/device_automation";
import { EntityRegistryEntry } from "../../../../../data/entity_registry"; import { EntityRegistryEntry } from "../../../../../data/entity_registry";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
@ -84,8 +86,13 @@ export class HaDeviceCondition extends LitElement {
.data=${this._extraFieldsData(this.condition, this._capabilities)} .data=${this._extraFieldsData(this.condition, this._capabilities)}
.schema=${this._capabilities.extra_fields} .schema=${this._capabilities.extra_fields}
.disabled=${this.disabled} .disabled=${this.disabled}
.computeLabel=${this._extraFieldsComputeLabelCallback( .computeLabel=${localizeExtraFieldsComputeLabelCallback(
this.hass.localize this.hass,
this.condition
)}
.computeHelper=${localizeExtraFieldsComputeHelperCallback(
this.hass,
this.condition
)} )}
@value-changed=${this._extraFieldsChanged} @value-changed=${this._extraFieldsChanged}
></ha-form> ></ha-form>
@ -153,14 +160,6 @@ export class HaDeviceCondition extends LitElement {
}); });
} }
private _extraFieldsComputeLabelCallback(localize) {
// Returns a callback for ha-form to calculate labels per schema object
return (schema) =>
localize(
`ui.panel.config.automation.editor.conditions.type.device.extra_fields.${schema.name}`
) || schema.name;
}
static styles = css` static styles = css`
ha-device-picker { ha-device-picker {
display: block; display: block;

View File

@ -6,6 +6,7 @@ import {
mdiDebugStepOver, mdiDebugStepOver,
mdiDelete, mdiDelete,
mdiDotsVertical, mdiDotsVertical,
mdiFileEdit,
mdiInformationOutline, mdiInformationOutline,
mdiPlay, mdiPlay,
mdiPlayCircleOutline, mdiPlayCircleOutline,
@ -40,10 +41,12 @@ import "../../../components/ha-yaml-editor";
import { import {
AutomationConfig, AutomationConfig,
AutomationEntity, AutomationEntity,
BlueprintAutomationConfig,
deleteAutomation, deleteAutomation,
fetchAutomationFileConfig, fetchAutomationFileConfig,
getAutomationEditorInitData, getAutomationEditorInitData,
getAutomationStateConfig, getAutomationStateConfig,
normalizeAutomationConfig,
saveAutomationConfig, saveAutomationConfig,
showAutomationEditor, showAutomationEditor,
triggerAutomationActions, triggerAutomationActions,
@ -65,6 +68,7 @@ import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-a
import { showAutomationRenameDialog } from "./automation-rename-dialog/show-dialog-automation-rename"; import { showAutomationRenameDialog } from "./automation-rename-dialog/show-dialog-automation-rename";
import "./blueprint-automation-editor"; import "./blueprint-automation-editor";
import "./manual-automation-editor"; import "./manual-automation-editor";
import { substituteBlueprint } from "../../../data/blueprint";
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@ -235,6 +239,24 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
></ha-svg-icon> ></ha-svg-icon>
</ha-list-item> </ha-list-item>
${useBlueprint
? html`
<ha-list-item
graphic="icon"
@click=${this._takeControl}
.disabled=${this._readOnly || this._mode === "yaml"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.take_control"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiFileEdit}
></ha-svg-icon>
</ha-list-item>
`
: nothing}
<li divider role="separator"></li> <li divider role="separator"></li>
<ha-list-item graphic="icon" @click=${this._switchUiMode}> <ha-list-item graphic="icon" @click=${this._switchUiMode}>
@ -432,7 +454,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
} }
this._config = { this._config = {
...baseConfig, ...baseConfig,
...initData, ...(initData ? normalizeAutomationConfig(initData) : initData),
} as AutomationConfig; } as AutomationConfig;
this._entityId = undefined; this._entityId = undefined;
this._readOnly = false; this._readOnly = false;
@ -441,7 +463,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
if (changedProps.has("entityId") && this.entityId) { if (changedProps.has("entityId") && this.entityId) {
getAutomationStateConfig(this.hass, this.entityId).then((c) => { getAutomationStateConfig(this.hass, this.entityId).then((c) => {
this._config = this._normalizeConfig(c.config); this._config = normalizeAutomationConfig(c.config);
this._checkValidation(); this._checkValidation();
}); });
this._entityId = this.entityId; this._entityId = this.entityId;
@ -497,18 +519,6 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
); );
} }
private _normalizeConfig(config: AutomationConfig): AutomationConfig {
// Normalize data: ensure trigger, action and condition are lists
// Happens when people copy paste their automations into the config
for (const key of ["trigger", "condition", "action"]) {
const value = config[key];
if (value && !Array.isArray(value)) {
config[key] = [value];
}
}
return config;
}
private async _loadConfig() { private async _loadConfig() {
try { try {
const config = await fetchAutomationFileConfig( const config = await fetchAutomationFileConfig(
@ -517,7 +527,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
); );
this._dirty = false; this._dirty = false;
this._readOnly = false; this._readOnly = false;
this._config = this._normalizeConfig(config); this._config = normalizeAutomationConfig(config);
this._checkValidation(); this._checkValidation();
} catch (err: any) { } catch (err: any) {
const entityRegistry = await fetchEntityRegistry(this.hass.connection); const entityRegistry = await fetchEntityRegistry(this.hass.connection);
@ -638,6 +648,45 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
} }
}; };
private async _takeControl() {
const config = this._config as BlueprintAutomationConfig;
const confirmation = await showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.config.automation.editor.take_control_confirmation.title"
),
text: this.hass!.localize(
"ui.panel.config.automation.editor.take_control_confirmation.text"
),
confirmText: this.hass!.localize(
"ui.panel.config.automation.editor.take_control_confirmation.action"
),
});
if (!confirmation) return;
try {
const result = await substituteBlueprint(
this.hass,
"automation",
config.use_blueprint.path,
config.use_blueprint.input || {}
);
const newConfig = {
...normalizeAutomationConfig(result.substituted_config),
alias: config.alias,
description: config.description,
};
this._config = newConfig;
this._dirty = true;
this._errors = undefined;
} catch (err: any) {
this._errors = err.message;
}
}
private async _duplicate() { private async _duplicate() {
const result = this._readOnly const result = this._readOnly
? await showConfirmationDialog(this, { ? await showConfirmationDialog(this, {

View File

@ -192,6 +192,20 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
}) })
private _activeCollapsed?: string; private _activeCollapsed?: string;
@storage({
key: "automation-table-column-order",
state: false,
subscribe: false,
})
private _activeColumnOrder?: string[];
@storage({
key: "automation-table-hidden-columns",
state: false,
subscribe: false,
})
private _activeHiddenColumns?: string[];
@query("#overflow-menu") private _overflowMenu!: HaMenu; @query("#overflow-menu") private _overflowMenu!: HaMenu;
private _sizeController = new ResizeController(this, { private _sizeController = new ResizeController(this, {
@ -253,6 +267,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
title: "", title: "",
label: localize("ui.panel.config.automation.picker.headers.state"), label: localize("ui.panel.config.automation.picker.headers.state"),
type: "icon", type: "icon",
moveable: false,
showNarrow: true,
template: (automation) => template: (automation) =>
html`<ha-state-icon html`<ha-state-icon
.hass=${this.hass} .hass=${this.hass}
@ -272,30 +288,13 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true, grows: true,
template: (automation) => { extraTemplate: (automation) =>
const date = new Date(automation.attributes.last_triggered); automation.labels.length
const now = new Date();
const dayDifference = differenceInDays(now, date);
return html`
<div style="font-size: 14px;">${automation.name}</div>
${narrow
? html`<div class="secondary">
${this.hass.localize("ui.card.automation.last_triggered")}:
${automation.attributes.last_triggered
? dayDifference > 3
? formatShortDateTime(date, locale, this.hass.config)
: relativeTime(date, locale)
: localize("ui.components.relative_time.never")}
</div>`
: nothing}
${automation.labels.length
? html`<ha-data-table-labels ? html`<ha-data-table-labels
@label-clicked=${this._labelClicked} @label-clicked=${this._labelClicked}
.labels=${automation.labels} .labels=${automation.labels}
></ha-data-table-labels>` ></ha-data-table-labels>`
: nothing} : nothing,
`;
},
}, },
area: { area: {
title: localize("ui.panel.config.automation.picker.headers.area"), title: localize("ui.panel.config.automation.picker.headers.area"),
@ -322,7 +321,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
sortable: true, sortable: true,
width: "130px", width: "130px",
title: localize("ui.card.automation.last_triggered"), title: localize("ui.card.automation.last_triggered"),
hidden: narrow,
template: (automation) => { template: (automation) => {
if (!automation.last_triggered) { if (!automation.last_triggered) {
return this.hass.localize("ui.components.relative_time.never"); return this.hass.localize("ui.components.relative_time.never");
@ -341,9 +339,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
width: "82px", width: "82px",
sortable: true, sortable: true,
groupable: true, groupable: true,
hidden: narrow,
title: "", title: "",
type: "overflow", type: "overflow",
hidden: narrow,
label: this.hass.localize("ui.panel.config.automation.picker.state"), label: this.hass.localize("ui.panel.config.automation.picker.state"),
template: (automation) => html` template: (automation) => html`
<ha-entity-toggle <ha-entity-toggle
@ -356,6 +354,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
title: "", title: "",
width: "64px", width: "64px",
type: "icon-button", type: "icon-button",
showNarrow: true,
moveable: false,
hideable: false,
template: (automation) => html` template: (automation) => html`
<ha-icon-button <ha-icon-button
.automation=${automation} .automation=${automation}
@ -545,6 +546,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
.initialGroupColumn=${this._activeGrouping || "category"} .initialGroupColumn=${this._activeGrouping || "category"}
.initialCollapsedGroups=${this._activeCollapsed} .initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting} .initialSorting=${this._activeSorting}
.columnOrder=${this._activeColumnOrder}
.hiddenColumns=${this._activeHiddenColumns}
@columns-changed=${this._handleColumnsChanged}
@sorting-changed=${this._handleSortingChanged} @sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged} @grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged} @collapsed-changed=${this._handleCollapseChanged}
@ -1415,6 +1419,11 @@ ${rejected
this._activeCollapsed = ev.detail.value; this._activeCollapsed = ev.detail.value;
} }
private _handleColumnsChanged(ev: CustomEvent) {
this._activeColumnOrder = ev.detail.columnOrder;
this._activeHiddenColumns = ev.detail.hiddenColumns;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,

View File

@ -14,6 +14,8 @@ import {
DeviceCapabilities, DeviceCapabilities,
DeviceTrigger, DeviceTrigger,
fetchDeviceTriggerCapabilities, fetchDeviceTriggerCapabilities,
localizeExtraFieldsComputeLabelCallback,
localizeExtraFieldsComputeHelperCallback,
} from "../../../../../data/device_automation"; } from "../../../../../data/device_automation";
import { EntityRegistryEntry } from "../../../../../data/entity_registry"; import { EntityRegistryEntry } from "../../../../../data/entity_registry";
import { HomeAssistant } from "../../../../../types"; import { HomeAssistant } from "../../../../../types";
@ -88,8 +90,13 @@ export class HaDeviceTrigger extends LitElement {
.data=${this._extraFieldsData(this.trigger, this._capabilities)} .data=${this._extraFieldsData(this.trigger, this._capabilities)}
.schema=${this._capabilities.extra_fields} .schema=${this._capabilities.extra_fields}
.disabled=${this.disabled} .disabled=${this.disabled}
.computeLabel=${this._extraFieldsComputeLabelCallback( .computeLabel=${localizeExtraFieldsComputeLabelCallback(
this.hass.localize this.hass,
this.trigger
)}
.computeHelper=${localizeExtraFieldsComputeHelperCallback(
this.hass,
this.trigger
)} )}
@value-changed=${this._extraFieldsChanged} @value-changed=${this._extraFieldsChanged}
></ha-form> ></ha-form>
@ -177,14 +184,6 @@ export class HaDeviceTrigger extends LitElement {
}); });
} }
private _extraFieldsComputeLabelCallback(localize) {
// Returns a callback for ha-form to calculate labels per schema object
return (schema) =>
localize(
`ui.panel.config.automation.editor.triggers.type.device.extra_fields.${schema.name}`
) || schema.name;
}
static styles = css` static styles = css`
ha-device-picker { ha-device-picker {
display: block; display: block;

View File

@ -60,14 +60,19 @@ class HaConfigBackup extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
grows: true, grows: true,
template: (backup) => template: narrow
? undefined
: (backup) =>
html`${backup.name} html`${backup.name}
<div class="secondary">${backup.path}</div>`, <div class="secondary">${backup.path}</div>`,
}, },
path: {
title: localize("ui.panel.config.backup.path"),
hidden: !narrow,
},
size: { size: {
title: localize("ui.panel.config.backup.size"), title: localize("ui.panel.config.backup.size"),
width: "15%", width: "15%",
hidden: narrow,
filterable: true, filterable: true,
sortable: true, sortable: true,
template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB", template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB",
@ -76,7 +81,6 @@ class HaConfigBackup extends LitElement {
title: localize("ui.panel.config.backup.created"), title: localize("ui.panel.config.backup.created"),
width: "15%", width: "15%",
direction: "desc", direction: "desc",
hidden: narrow,
filterable: true, filterable: true,
sortable: true, sortable: true,
template: (backup) => template: (backup) =>
@ -87,6 +91,9 @@ class HaConfigBackup extends LitElement {
title: "", title: "",
width: "15%", width: "15%",
type: "overflow-menu", type: "overflow-menu",
showNarrow: true,
hideable: false,
moveable: false,
template: (backup) => template: (backup) =>
html`<ha-icon-overflow-menu html`<ha-icon-overflow-menu
.hass=${this.hass} .hass=${this.hass}

View File

@ -107,6 +107,20 @@ class HaBlueprintOverview extends LitElement {
}) })
private _activeCollapsed?: string; private _activeCollapsed?: string;
@storage({
key: "blueprint-table-column-order",
state: false,
subscribe: false,
})
private _activeColumnOrder?: string[];
@storage({
key: "blueprint-table-hidden-columns",
state: false,
subscribe: false,
})
private _activeHiddenColumns?: string[];
@storage({ @storage({
storage: "sessionStorage", storage: "sessionStorage",
key: "blueprint-table-search", key: "blueprint-table-search",
@ -154,8 +168,6 @@ class HaBlueprintOverview extends LitElement {
private _columns = memoizeOne( private _columns = memoizeOne(
( (
narrow,
_language,
localize: LocalizeFunc localize: LocalizeFunc
): DataTableColumnContainer<BlueprintMetaDataPath> => ({ ): DataTableColumnContainer<BlueprintMetaDataPath> => ({
name: { name: {
@ -165,19 +177,12 @@ class HaBlueprintOverview extends LitElement {
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true, grows: true,
template: narrow
? (blueprint) => html`
${blueprint.name}<br />
<div class="secondary">${blueprint.path}</div>
`
: undefined,
}, },
translated_type: { translated_type: {
title: localize("ui.panel.config.blueprint.overview.headers.type"), title: localize("ui.panel.config.blueprint.overview.headers.type"),
sortable: true, sortable: true,
filterable: true, filterable: true,
groupable: true, groupable: true,
hidden: narrow,
direction: "asc", direction: "asc",
width: "10%", width: "10%",
}, },
@ -185,7 +190,6 @@ class HaBlueprintOverview extends LitElement {
title: localize("ui.panel.config.blueprint.overview.headers.file_name"), title: localize("ui.panel.config.blueprint.overview.headers.file_name"),
sortable: true, sortable: true,
filterable: true, filterable: true,
hidden: narrow,
direction: "asc", direction: "asc",
width: "25%", width: "25%",
}, },
@ -197,6 +201,9 @@ class HaBlueprintOverview extends LitElement {
title: "", title: "",
width: this.narrow ? undefined : "10%", width: this.narrow ? undefined : "10%",
type: "overflow-menu", type: "overflow-menu",
showNarrow: true,
moveable: false,
hideable: false,
template: (blueprint) => template: (blueprint) =>
blueprint.error blueprint.error
? html`<ha-svg-icon ? html`<ha-svg-icon
@ -280,11 +287,7 @@ class HaBlueprintOverview extends LitElement {
back-path="/config" back-path="/config"
.route=${this.route} .route=${this.route}
.tabs=${configSections.automations} .tabs=${configSections.automations}
.columns=${this._columns( .columns=${this._columns(this.hass.localize)}
this.narrow,
this.hass.language,
this.hass.localize
)}
.data=${this._processedBlueprints(this.blueprints, this.hass.localize)} .data=${this._processedBlueprints(this.blueprints, this.hass.localize)}
id="fullpath" id="fullpath"
.noDataText=${this.hass.localize( .noDataText=${this.hass.localize(
@ -313,6 +316,9 @@ class HaBlueprintOverview extends LitElement {
.initialGroupColumn=${this._activeGrouping} .initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed} .initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting} .initialSorting=${this._activeSorting}
.columnOrder=${this._activeColumnOrder}
.hiddenColumns=${this._activeHiddenColumns}
@columns-changed=${this._handleColumnsChanged}
@sorting-changed=${this._handleSortingChanged} @sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged} @grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged} @collapsed-changed=${this._handleCollapseChanged}
@ -556,6 +562,11 @@ class HaBlueprintOverview extends LitElement {
this._filter = ev.detail.value; this._filter = ev.detail.value;
} }
private _handleColumnsChanged(ev: CustomEvent) {
this._activeColumnOrder = ev.detail.columnOrder;
this._activeHiddenColumns = ev.detail.hiddenColumns;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return haStyle; return haStyle;
} }

View File

@ -6,7 +6,7 @@ import {
mdiPower, mdiPower,
mdiRefresh, mdiRefresh,
} from "@mdi/js"; } from "@mdi/js";
import { HassEntities, UnsubscribeFunc } from "home-assistant-js-websocket"; import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { import {
CSSResultGroup, CSSResultGroup,
LitElement, LitElement,
@ -61,32 +61,32 @@ const randomTip = (hass: HomeAssistant, narrow: boolean) => {
href="https://community.home-assistant.io" href="https://community.home-assistant.io"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
>Forums</a >${hass.localize("ui.panel.config.tips.join_forums")}</a
>`, >`,
twitter: html`<a twitter: html`<a
href=${documentationUrl(hass, `/twitter`)} href=${documentationUrl(hass, `/twitter`)}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
>Twitter</a >${hass.localize("ui.panel.config.tips.join_x")}</a
>`, >`,
discord: html`<a discord: html`<a
href=${documentationUrl(hass, `/join-chat`)} href=${documentationUrl(hass, `/join-chat`)}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
>Chat</a >${hass.localize("ui.panel.config.tips.join_chat")}</a
>`, >`,
blog: html`<a blog: html`<a
href=${documentationUrl(hass, `/blog`)} href=${documentationUrl(hass, `/blog`)}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
>Blog</a >${hass.localize("ui.panel.config.tips.join_blog")}</a
>`, >`,
newsletter: html`<span class="keep-together" newsletter: html`<span class="keep-together"
><a ><a
href="https://newsletter.openhomefoundation.org/" href="https://newsletter.openhomefoundation.org/"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
>Newsletter</a >${hass.localize("ui.panel.config.tips.join_newsletter")}</a
> >
</span>`, </span>`,
}), }),
@ -177,7 +177,10 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
protected render(): TemplateResult { protected render(): TemplateResult {
const { updates: canInstallUpdates, total: totalUpdates } = const { updates: canInstallUpdates, total: totalUpdates } =
this._filterUpdateEntitiesWithInstall(this.hass.states); this._filterUpdateEntitiesWithInstall(
this.hass.states,
this.hass.entities
);
const { issues: repairsIssues, total: totalRepairIssues } = const { issues: repairsIssues, total: totalRepairIssues } =
this._repairsIssues; this._repairsIssues;
@ -306,8 +309,13 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
} }
private _filterUpdateEntitiesWithInstall = memoizeOne( private _filterUpdateEntitiesWithInstall = memoizeOne(
(entities: HassEntities): { updates: UpdateEntity[]; total: number } => { (
const updates = filterUpdateEntitiesWithInstall(entities); entities: HomeAssistant["states"],
entityRegistry: HomeAssistant["entities"]
): { updates: UpdateEntity[]; total: number } => {
const updates = filterUpdateEntitiesWithInstall(entities).filter(
(entity) => !entityRegistry[entity.entity_id]?.hidden
);
return { return {
updates: updates.slice(0, updates.length === 3 ? updates.length : 2), updates: updates.slice(0, updates.length === 3 ? updates.length : 2),

View File

@ -24,7 +24,7 @@ import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
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 { SENSOR_ENTITIES } from "../../../common/const"; import { SENSOR_ENTITIES, ASSIST_ENTITIES } from "../../../common/const";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
@ -190,26 +190,42 @@ export class HaConfigDevicePage extends LitElement {
private _entitiesByCategory = memoizeOne( private _entitiesByCategory = memoizeOne(
(entities: EntityRegistryEntry[]) => { (entities: EntityRegistryEntry[]) => {
const result = groupBy(entities, (entry) => const result = groupBy(entities, (entry) => {
entry.entity_category const domain = computeDomain(entry.entity_id);
? entry.entity_category
: computeDomain(entry.entity_id) === "event" if (entry.entity_category) {
? "event" return entry.entity_category;
: SENSOR_ENTITIES.includes(computeDomain(entry.entity_id)) }
? "sensor"
: "control" if (domain === "event" || domain === "notify") {
) as Record< return domain;
}
if (SENSOR_ENTITIES.includes(domain)) {
return "sensor";
}
if (ASSIST_ENTITIES.includes(domain)) {
return "assist";
}
return "control";
}) as Record<
| "control" | "control"
| "event" | "event"
| "sensor" | "sensor"
| "assist"
| "notify"
| NonNullable<EntityRegistryEntry["entity_category"]>, | NonNullable<EntityRegistryEntry["entity_category"]>,
EntityRegistryStateEntry[] EntityRegistryStateEntry[]
>; >;
for (const key of [ for (const key of [
"assist",
"config", "config",
"control", "control",
"diagnostic", "diagnostic",
"event", "event",
"notify",
"sensor", "sensor",
]) { ]) {
if (!(key in result)) { if (!(key in result)) {
@ -854,7 +870,15 @@ export class HaConfigDevicePage extends LitElement {
</div> </div>
<div class="column"> <div class="column">
${( ${(
["control", "sensor", "event", "config", "diagnostic"] as const [
"control",
"sensor",
"notify",
"event",
"assist",
"config",
"diagnostic",
] as const
).map((category) => ).map((category) =>
// Make sure we render controls if no other cards will be rendered // Make sure we render controls if no other cards will be rendered
entitiesByCategory[category].length > 0 || entitiesByCategory[category].length > 0 ||
@ -1004,6 +1028,9 @@ export class HaConfigDevicePage extends LitElement {
: this.hass.localize( : this.hass.localize(
`ui.panel.config.devices.confirm_delete` `ui.panel.config.devices.confirm_delete`
), ),
confirmText: this.hass.localize("ui.common.delete"),
dismissText: this.hass.localize("ui.common.cancel"),
destructive: true,
}); });
if (!confirmed) { if (!confirmed) {

View File

@ -154,6 +154,20 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
@storage({ key: "devices-table-collapsed", state: false, subscribe: false }) @storage({ key: "devices-table-collapsed", state: false, subscribe: false })
private _activeCollapsed?: string; private _activeCollapsed?: string;
@storage({
key: "devices-table-column-order",
state: false,
subscribe: false,
})
private _activeColumnOrder?: string[];
@storage({
key: "devices-table-hidden-columns",
state: false,
subscribe: false,
})
private _activeHiddenColumns?: string[];
private _sizeController = new ResizeController(this, { private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width, callback: (entries) => entries[0]?.contentRect.width,
}); });
@ -434,10 +448,13 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
typeof this._devicesAndFilterDomains typeof this._devicesAndFilterDomains
>["devicesOutput"][number]; >["devicesOutput"][number];
const columns: DataTableColumnContainer<DeviceItem> = { return {
icon: { icon: {
title: "", title: "",
label: localize("ui.panel.config.devices.data_table.icon"),
type: "icon", type: "icon",
moveable: false,
showNarrow: true,
template: (device) => template: (device) =>
device.domains.length device.domains.length
? html`<img ? html`<img
@ -452,19 +469,14 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
/>` />`
: "", : "",
}, },
}; name: {
if (narrow) {
columns.name = {
title: localize("ui.panel.config.devices.data_table.device"), title: localize("ui.panel.config.devices.data_table.device"),
main: true, main: true,
sortable: true, sortable: true,
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true, grows: true,
template: (device) => html` extraTemplate: (device) => html`
<div style="font-size: 14px;">${device.name}</div>
<div class="secondary">${device.area} | ${device.integration}</div>
${device.label_entries.length ${device.label_entries.length
? html` ? html`
<ha-data-table-labels <ha-data-table-labels
@ -473,61 +485,37 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
` `
: nothing} : nothing}
`, `,
}; },
} else { manufacturer: {
columns.name = {
title: localize("ui.panel.config.devices.data_table.device"),
main: true,
sortable: true,
filterable: true,
direction: "asc",
grows: true,
template: (device) => html`
<div style="font-size: 14px;">${device.name}</div>
${device.label_entries.length
? html`
<ha-data-table-labels
.labels=${device.label_entries}
></ha-data-table-labels>
`
: nothing}
`,
};
}
columns.manufacturer = {
title: localize("ui.panel.config.devices.data_table.manufacturer"), title: localize("ui.panel.config.devices.data_table.manufacturer"),
sortable: true, sortable: true,
hidden: narrow,
filterable: true, filterable: true,
groupable: true, groupable: true,
width: "15%", width: "15%",
}; },
columns.model = { model: {
title: localize("ui.panel.config.devices.data_table.model"), title: localize("ui.panel.config.devices.data_table.model"),
sortable: true, sortable: true,
hidden: narrow,
filterable: true, filterable: true,
width: "15%", width: "15%",
}; },
columns.area = { area: {
title: localize("ui.panel.config.devices.data_table.area"), title: localize("ui.panel.config.devices.data_table.area"),
sortable: true, sortable: true,
hidden: narrow,
filterable: true, filterable: true,
groupable: true, groupable: true,
width: "15%", width: "15%",
}; },
columns.integration = { integration: {
title: localize("ui.panel.config.devices.data_table.integration"), title: localize("ui.panel.config.devices.data_table.integration"),
sortable: true, sortable: true,
hidden: narrow,
filterable: true, filterable: true,
groupable: true, groupable: true,
width: "15%", width: "15%",
}; },
columns.battery_entity = { battery_entity: {
title: localize("ui.panel.config.devices.data_table.battery"), title: localize("ui.panel.config.devices.data_table.battery"),
showNarrow: true,
sortable: true, sortable: true,
filterable: true, filterable: true,
type: "numeric", type: "numeric",
@ -540,7 +528,9 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
batteryEntityPair && batteryEntityPair[0] batteryEntityPair && batteryEntityPair[0]
? this.hass.states[batteryEntityPair[0]] ? this.hass.states[batteryEntityPair[0]]
: undefined; : undefined;
const batteryDomain = battery ? computeStateDomain(battery) : undefined; const batteryDomain = battery
? computeStateDomain(battery)
: undefined;
const batteryCharging = const batteryCharging =
batteryEntityPair && batteryEntityPair[1] batteryEntityPair && batteryEntityPair[1]
? this.hass.states[batteryEntityPair[1]] ? this.hass.states[batteryEntityPair[1]]
@ -560,8 +550,8 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
` `
: html``; : html``;
}, },
}; },
columns.disabled_by = { disabled_by: {
title: "", title: "",
label: localize("ui.panel.config.devices.data_table.disabled_by"), label: localize("ui.panel.config.devices.data_table.disabled_by"),
hidden: true, hidden: true,
@ -569,16 +559,15 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
device.disabled_by device.disabled_by
? this.hass.localize("ui.panel.config.devices.disabled") ? this.hass.localize("ui.panel.config.devices.disabled")
: "", : "",
}; },
columns.labels = { labels: {
title: "", title: "",
hidden: true, hidden: true,
filterable: true, filterable: true,
template: (device) => template: (device) =>
device.label_entries.map((lbl) => lbl.name).join(" "), device.label_entries.map((lbl) => lbl.name).join(" "),
}; },
} as DataTableColumnContainer<DeviceItem>;
return columns;
}); });
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] { protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
@ -704,6 +693,9 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
.initialGroupColumn=${this._activeGrouping} .initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed} .initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting} .initialSorting=${this._activeSorting}
.columnOrder=${this._activeColumnOrder}
.hiddenColumns=${this._activeHiddenColumns}
@columns-changed=${this._handleColumnsChanged}
@clear-filter=${this._clearFilter} @clear-filter=${this._clearFilter}
@search-changed=${this._handleSearchChange} @search-changed=${this._handleSearchChange}
@sorting-changed=${this._handleSortingChanged} @sorting-changed=${this._handleSortingChanged}
@ -1043,6 +1035,11 @@ ${rejected
this._activeCollapsed = ev.detail.value; this._activeCollapsed = ev.detail.value;
} }
private _handleColumnsChanged(ev: CustomEvent) {
this._activeColumnOrder = ev.detail.columnOrder;
this._activeHiddenColumns = ev.detail.hiddenColumns;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
css` css`

View File

@ -164,6 +164,9 @@ export class EntitySettingsHelperTab extends LitElement {
text: this.hass.localize( text: this.hass.localize(
"ui.dialogs.entity_registry.editor.confirm_delete" "ui.dialogs.entity_registry.editor.confirm_delete"
), ),
confirmText: this.hass.localize("ui.common.delete"),
dismissText: this.hass.localize("ui.common.cancel"),
destructive: true,
})) }))
) { ) {
return; return;

View File

@ -215,6 +215,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
text: this.hass.localize( text: this.hass.localize(
"ui.dialogs.entity_registry.editor.confirm_delete" "ui.dialogs.entity_registry.editor.confirm_delete"
), ),
confirmText: this.hass.localize("ui.common.delete"),
dismissText: this.hass.localize("ui.common.cancel"),
destructive: true,
})) }))
) { ) {
return; return;

View File

@ -186,6 +186,20 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
}) })
private _activeCollapsed?: string; private _activeCollapsed?: string;
@storage({
key: "entities-table-column-order",
state: false,
subscribe: false,
})
private _activeColumnOrder?: string[];
@storage({
key: "entities-table-hidden-columns",
state: false,
subscribe: false,
})
private _activeHiddenColumns?: string[];
@query("hass-tabs-subpage-data-table", true) @query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable; private _dataTable!: HaTabsSubpageDataTable;
@ -251,15 +265,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
]); ]);
private _columns = memoize( private _columns = memoize(
( (localize: LocalizeFunc): DataTableColumnContainer<EntityRow> => ({
localize: LocalizeFunc,
narrow,
_language
): DataTableColumnContainer<EntityRow> => ({
icon: { icon: {
title: "", title: "",
label: localize("ui.panel.config.entities.picker.headers.state_icon"), label: localize("ui.panel.config.entities.picker.headers.state_icon"),
type: "icon", type: "icon",
showNarrow: true,
moveable: false,
template: (entry) => template: (entry) =>
entry.icon entry.icon
? html`<ha-icon .icon=${entry.icon}></ha-icon>` ? html`<ha-icon .icon=${entry.icon}></ha-icon>`
@ -283,32 +295,23 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true, grows: true,
template: (entry) => html` extraTemplate: (entry) =>
<div style="font-size: 14px;">${entry.name}</div> entry.label_entries.length
${narrow
? html`<div class="secondary">
${entry.entity_id} | ${entry.localized_platform}
</div>`
: nothing}
${entry.label_entries.length
? html` ? html`
<ha-data-table-labels <ha-data-table-labels
.labels=${entry.label_entries} .labels=${entry.label_entries}
></ha-data-table-labels> ></ha-data-table-labels>
` `
: nothing} : nothing,
`,
}, },
entity_id: { entity_id: {
title: localize("ui.panel.config.entities.picker.headers.entity_id"), title: localize("ui.panel.config.entities.picker.headers.entity_id"),
hidden: narrow,
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "25%", width: "25%",
}, },
localized_platform: { localized_platform: {
title: localize("ui.panel.config.entities.picker.headers.integration"), title: localize("ui.panel.config.entities.picker.headers.integration"),
hidden: narrow,
sortable: true, sortable: true,
groupable: true, groupable: true,
filterable: true, filterable: true,
@ -324,7 +327,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
area: { area: {
title: localize("ui.panel.config.entities.picker.headers.area"), title: localize("ui.panel.config.entities.picker.headers.area"),
sortable: true, sortable: true,
hidden: narrow,
filterable: true, filterable: true,
groupable: true, groupable: true,
width: "15%", width: "15%",
@ -343,6 +345,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
status: { status: {
title: localize("ui.panel.config.entities.picker.headers.status"), title: localize("ui.panel.config.entities.picker.headers.status"),
type: "icon", type: "icon",
showNarrow: true,
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "68px", width: "68px",
@ -688,11 +691,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
} }
.route=${this.route} .route=${this.route}
.tabs=${configSections.devices} .tabs=${configSections.devices}
.columns=${this._columns( .columns=${this._columns(this.hass.localize)}
this.hass.localize,
this.narrow,
this.hass.language
)}
.data=${filteredEntities} .data=${filteredEntities}
.searchLabel=${this.hass.localize( .searchLabel=${this.hass.localize(
"ui.panel.config.entities.picker.search", "ui.panel.config.entities.picker.search",
@ -714,6 +713,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
.initialGroupColumn=${this._activeGrouping} .initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed} .initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting} .initialSorting=${this._activeSorting}
.columnOrder=${this._activeColumnOrder}
.hiddenColumns=${this._activeHiddenColumns}
@columns-changed=${this._handleColumnsChanged}
@sorting-changed=${this._handleSortingChanged} @sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged} @grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged} @collapsed-changed=${this._handleCollapseChanged}
@ -839,7 +841,7 @@ ${
></ha-svg-icon> ></ha-svg-icon>
<div slot="headline"> <div slot="headline">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.button" "ui.panel.config.entities.picker.delete_selected.button"
)} )}
</div> </div>
</ha-menu-item> </ha-menu-item>
@ -1256,25 +1258,23 @@ ${rejected
}); });
showConfirmationDialog(this, { showConfirmationDialog(this, {
title: this.hass.localize( title: this.hass.localize(
`ui.panel.config.entities.picker.remove_selected.confirm_${ `ui.panel.config.entities.picker.delete_selected.confirm_title`
removeableEntities.length !== this._selected.length ? "partly_" : ""
}title`,
{ number: removeableEntities.length }
), ),
text: text:
removeableEntities.length === this._selected.length removeableEntities.length === this._selected.length
? this.hass.localize( ? this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.confirm_text" "ui.panel.config.entities.picker.delete_selected.confirm_text"
) )
: this.hass.localize( : this.hass.localize(
"ui.panel.config.entities.picker.remove_selected.confirm_partly_text", "ui.panel.config.entities.picker.delete_selected.confirm_partly_text",
{ {
removable: removeableEntities.length, deletable: removeableEntities.length,
selected: this._selected.length, selected: this._selected.length,
} }
), ),
confirmText: this.hass.localize("ui.common.remove"), confirmText: this.hass.localize("ui.common.delete"),
dismissText: this.hass.localize("ui.common.cancel"), dismissText: this.hass.localize("ui.common.cancel"),
destructive: true,
confirm: () => { confirm: () => {
removeableEntities.forEach((entity) => removeableEntities.forEach((entity) =>
removeEntityRegistryEntry(this.hass, entity) removeEntityRegistryEntry(this.hass, entity)
@ -1337,6 +1337,11 @@ ${rejected
this._activeCollapsed = ev.detail.value; this._activeCollapsed = ev.detail.value;
} }
private _handleColumnsChanged(ev: CustomEvent) {
this._activeColumnOrder = ev.detail.columnOrder;
this._activeHiddenColumns = ev.detail.hiddenColumns;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,

View File

@ -167,6 +167,20 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
}) })
private _filter = ""; private _filter = "";
@storage({
key: "helpers-table-column-order",
state: false,
subscribe: false,
})
private _activeColumnOrder?: string[];
@storage({
key: "helpers-table-hidden-columns",
state: false,
subscribe: false,
})
private _activeHiddenColumns?: string[];
@state() private _stateItems: HassEntity[] = []; @state() private _stateItems: HassEntity[] = [];
@state() private _entityEntries?: Record<string, EntityRegistryEntry>; @state() private _entityEntries?: Record<string, EntityRegistryEntry>;
@ -243,14 +257,13 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
} }
private _columns = memoizeOne( private _columns = memoizeOne(
( (localize: LocalizeFunc): DataTableColumnContainer<HelperItem> => ({
narrow: boolean,
localize: LocalizeFunc
): DataTableColumnContainer<HelperItem> => ({
icon: { icon: {
title: "", title: "",
label: localize("ui.panel.config.helpers.picker.headers.icon"), label: localize("ui.panel.config.helpers.picker.headers.icon"),
type: "icon", type: "icon",
showNarrow: true,
moveable: false,
template: (helper) => template: (helper) =>
helper.entity helper.entity
? html`<ha-state-icon ? html`<ha-state-icon
@ -269,23 +282,17 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
filterable: true, filterable: true,
grows: true, grows: true,
direction: "asc", direction: "asc",
template: (helper) => html` extraTemplate: (helper) =>
<div style="font-size: 14px;">${helper.name}</div> helper.label_entries.length
${narrow
? html`<div class="secondary">${helper.entity_id}</div> `
: nothing}
${helper.label_entries.length
? html` ? html`
<ha-data-table-labels <ha-data-table-labels
.labels=${helper.label_entries} .labels=${helper.label_entries}
></ha-data-table-labels> ></ha-data-table-labels>
` `
: nothing} : nothing,
`,
}, },
entity_id: { entity_id: {
title: localize("ui.panel.config.helpers.picker.headers.entity_id"), title: localize("ui.panel.config.helpers.picker.headers.entity_id"),
hidden: this.narrow,
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "25%", width: "25%",
@ -313,10 +320,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
}, },
editable: { editable: {
title: "", title: "",
label: this.hass.localize( label: localize("ui.panel.config.helpers.picker.headers.editable"),
"ui.panel.config.helpers.picker.headers.editable"
),
type: "icon", type: "icon",
showNarrow: true,
template: (helper) => html` template: (helper) => html`
${!helper.editable ${!helper.editable
? html` ? html`
@ -337,8 +343,12 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
}, },
actions: { actions: {
title: "", title: "",
label: "Actions",
width: "64px", width: "64px",
type: "overflow-menu", type: "overflow-menu",
hideable: false,
moveable: false,
showNarrow: true,
template: (helper) => html` template: (helper) => html`
<ha-icon-overflow-menu <ha-icon-overflow-menu
.hass=${this.hass} .hass=${this.hass}
@ -556,11 +566,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
Array.isArray(val) ? val.length : val Array.isArray(val) ? val.length : val
) )
).length} ).length}
.columns=${this._columns(this.narrow, this.hass.localize)} .columns=${this._columns(this.hass.localize)}
.data=${helpers} .data=${helpers}
.initialGroupColumn=${this._activeGrouping || "category"} .initialGroupColumn=${this._activeGrouping || "category"}
.initialCollapsedGroups=${this._activeCollapsed} .initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting} .initialSorting=${this._activeSorting}
.columnOrder=${this._activeColumnOrder}
.hiddenColumns=${this._activeHiddenColumns}
@columns-changed=${this._handleColumnsChanged}
@sorting-changed=${this._handleSortingChanged} @sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged} @grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged} @collapsed-changed=${this._handleCollapseChanged}
@ -1084,6 +1097,11 @@ ${rejected
this._filter = ev.detail.value; this._filter = ev.detail.value;
} }
private _handleColumnsChanged(ev: CustomEvent) {
this._activeColumnOrder = ev.detail.columnOrder;
this._activeHiddenColumns = ev.detail.hiddenColumns;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,

View File

@ -55,6 +55,7 @@ import {
showYamlIntegrationDialog, showYamlIntegrationDialog,
} from "./show-add-integration-dialog"; } from "./show-add-integration-dialog";
import { getConfigEntries } from "../../../data/config_entries"; import { getConfigEntries } from "../../../data/config_entries";
import { stripDiacritics } from "../../../common/string/strip-diacritics";
export interface IntegrationListItem { export interface IntegrationListItem {
name: string; name: string;
@ -255,6 +256,7 @@ class AddIntegrationDialog extends LitElement {
isCaseSensitive: false, isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2), minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2, threshold: 0.2,
getFn: (obj, path) => stripDiacritics(Fuse.config.getFn(obj, path)),
}; };
const helpers = Object.entries(h).map(([domain, integration]) => ({ const helpers = Object.entries(h).map(([domain, integration]) => ({
domain, domain,
@ -264,15 +266,16 @@ class AddIntegrationDialog extends LitElement {
is_built_in: integration.is_built_in !== false, is_built_in: integration.is_built_in !== false,
cloud: integration.iot_class?.startsWith("cloud_"), cloud: integration.iot_class?.startsWith("cloud_"),
})); }));
const normalizedFilter = stripDiacritics(filter);
return [ return [
...new Fuse(integrations, options) ...new Fuse(integrations, options)
.search(filter) .search(normalizedFilter)
.map((result) => result.item), .map((result) => result.item),
...new Fuse(yamlIntegrations, options) ...new Fuse(yamlIntegrations, options)
.search(filter) .search(normalizedFilter)
.map((result) => result.item), .map((result) => result.item),
...new Fuse(helpers, options) ...new Fuse(helpers, options)
.search(filter) .search(normalizedFilter)
.map((result) => result.item), .map((result) => result.item),
]; ];
} }

View File

@ -71,6 +71,7 @@ import { showAddIntegrationDialog } from "./show-add-integration-dialog";
import "./ha-disabled-config-entry-card"; import "./ha-disabled-config-entry-card";
import { caseInsensitiveStringCompare } from "../../../common/string/compare"; import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import "../../../components/search-input-outlined"; import "../../../components/search-input-outlined";
import { stripDiacritics } from "../../../common/string/strip-diacritics";
export interface ConfigEntryExtended extends ConfigEntry { export interface ConfigEntryExtended extends ConfigEntry {
localized_domain_name?: string; localized_domain_name?: string;
@ -208,9 +209,12 @@ class HaConfigIntegrationsDashboard extends SubscribeMixin(LitElement) {
isCaseSensitive: false, isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2), minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2, threshold: 0.2,
getFn: (obj, path) => stripDiacritics(Fuse.config.getFn(obj, path)),
}; };
const fuse = new Fuse(configEntriesInProgress, options); const fuse = new Fuse(configEntriesInProgress, options);
filteredEntries = fuse.search(filter).map((result) => result.item); filteredEntries = fuse
.search(stripDiacritics(filter))
.map((result) => result.item);
} else { } else {
filteredEntries = configEntriesInProgress; filteredEntries = configEntriesInProgress;
} }

View File

@ -36,14 +36,8 @@ class DialogThreadDataset extends LitElement implements HassDialog {
dataset.extended_pan_id && dataset.extended_pan_id &&
otbrInfo.active_dataset_tlvs?.includes(dataset.extended_pan_id); otbrInfo.active_dataset_tlvs?.includes(dataset.extended_pan_id);
const canImportKeychain =
hasOTBR &&
!this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain &&
network.routers?.length;
return html`<ha-dialog return html`<ha-dialog
open open
.hideActions=${!canImportKeychain}
@closed=${this.closeDialog} @closed=${this.closeDialog}
.heading=${createCloseHeading(this.hass, network.name)} .heading=${createCloseHeading(this.hass, network.name)}
> >
@ -59,28 +53,8 @@ class DialogThreadDataset extends LitElement implements HassDialog {
Active dataset TLVs: ${otbrInfo.active_dataset_tlvs}` Active dataset TLVs: ${otbrInfo.active_dataset_tlvs}`
: nothing} : nothing}
</div> </div>
${canImportKeychain
? html`<ha-button slot="primary-action" @click=${this._sendCredentials}
>Send credentials to phone</ha-button
>`
: nothing}
</ha-dialog>`; </ha-dialog>`;
} }
private _sendCredentials() {
this.hass.auth.external!.fireMessage({
type: "thread/store_in_platform_keychain",
payload: {
mac_extended_address:
this._params?.network.dataset?.preferred_extended_address ||
this._params!.network.routers![0]!.extended_address,
border_agent_id:
this._params?.network.dataset?.preferred_border_agent_id ||
this._params!.network.routers![0]!.border_agent_id,
active_operational_dataset: this._params!.otbrInfo!.active_dataset_tlvs,
},
});
}
} }
declare global { declare global {

View File

@ -151,7 +151,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
slot="fab" slot="fab"
@click=${this._importExternalThreadCredentials} @click=${this._importExternalThreadCredentials}
extended extended
label="Import credentials" label="Send credentials to Home Assistant"
><ha-svg-icon slot="icon" .path=${mdiCellphoneKey}></ha-svg-icon ><ha-svg-icon slot="icon" .path=${mdiCellphoneKey}></ha-svg-icon
></ha-fab>` ></ha-fab>`
: nothing} : nothing}
@ -160,6 +160,14 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
} }
private _renderNetwork(network: ThreadNetwork) { private _renderNetwork(network: ThreadNetwork) {
const canImportKeychain =
this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain &&
network.dataset?.extended_pan_id &&
this._otbrInfo &&
this._otbrInfo?.active_dataset_tlvs?.includes(
network.dataset.extended_pan_id
);
return html`<ha-card> return html`<ha-card>
<div class="card-header"> <div class="card-header">
${network.name}${network.dataset ${network.name}${network.dataset
@ -303,9 +311,30 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
> >
</div>` </div>`
: ""} : ""}
${canImportKeychain
? html`<div class="card-actions">
<mwc-button @click=${this._sendCredentials}
>Send credentials to phone</mwc-button
>
</div>`
: ""}
</ha-card>`; </ha-card>`;
} }
private _sendCredentials() {
if (!this._otbrInfo) {
return;
}
this.hass.auth.external!.fireMessage({
type: "thread/store_in_platform_keychain",
payload: {
mac_extended_address: this._otbrInfo.extended_address,
border_agent_id: this._otbrInfo.border_agent_id ?? "",
active_operational_dataset: this._otbrInfo.active_dataset_tlvs ?? "",
},
});
}
private async _showDatasetInfo(ev: Event) { private async _showDatasetInfo(ev: Event) {
const network = (ev.currentTarget as any).network as ThreadNetwork; const network = (ev.currentTarget as any).network as ThreadNetwork;
showThreadDatasetDialog(this, { network, otbrInfo: this._otbrInfo }); showThreadDatasetDialog(this, { network, otbrInfo: this._otbrInfo });

View File

@ -3,6 +3,7 @@ import { mdiAlertCircle, mdiCheckCircle, mdiQrcodeScan } from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-alert"; import "../../../../../components/ha-alert";
import type { HaCheckbox } from "../../../../../components/ha-checkbox"; import type { HaCheckbox } from "../../../../../components/ha-checkbox";
@ -60,7 +61,8 @@ class DialogZWaveJSAddNode extends LitElement {
| "finished" | "finished"
| "provisioned" | "provisioned"
| "validate_dsk_enter_pin" | "validate_dsk_enter_pin"
| "grant_security_classes"; | "grant_security_classes"
| "waiting_for_device";
@state() private _device?: ZWaveJSAddNodeDevice; @state() private _device?: ZWaveJSAddNodeDevice;
@ -86,6 +88,11 @@ class DialogZWaveJSAddNode extends LitElement {
private _qrProcessing = false; private _qrProcessing = false;
public connectedCallback(): void {
super.connectedCallback();
window.addEventListener("beforeunload", this._onBeforeUnload);
}
public disconnectedCallback(): void { public disconnectedCallback(): void {
super.disconnectedCallback(); super.disconnectedCallback();
this._unsubscribe(); this._unsubscribe();
@ -106,14 +113,22 @@ class DialogZWaveJSAddNode extends LitElement {
return nothing; return nothing;
} }
// Prevent accidentally closing the dialog in certain stages
const preventClose = this._shouldPreventClose();
const heading = this.hass.localize(
"ui.panel.config.zwave_js.add_node.title"
);
return html` return html`
<ha-dialog <ha-dialog
open open
@closed=${this.closeDialog} @closed=${this.closeDialog}
.heading=${createCloseHeading( .heading=${preventClose
this.hass, ? heading
this.hass.localize("ui.panel.config.zwave_js.add_node.title") : createCloseHeading(this.hass, heading)}
)} scrimClickAction=${ifDefined(preventClose ? "" : undefined)}
escapeKeyAction=${ifDefined(preventClose ? "" : undefined)}
> >
${this._status === "loading" ${this._status === "loading"
? html`<div style="display: flex; justify-content: center;"> ? html`<div style="display: flex; justify-content: center;">
@ -122,6 +137,15 @@ class DialogZWaveJSAddNode extends LitElement {
indeterminate indeterminate
></ha-circular-progress> ></ha-circular-progress>
</div>` </div>`
: this._status === "waiting_for_device"
? html`<div class="flex-container">
<ha-circular-progress indeterminate></ha-circular-progress>
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.waiting_for_device"
)}
</p>
</div>`
: this._status === "choose_strategy" : this._status === "choose_strategy"
? html`<h3>Choose strategy</h3> ? html`<h3>Choose strategy</h3>
<div class="flex-column"> <div class="flex-column">
@ -129,8 +153,8 @@ class DialogZWaveJSAddNode extends LitElement {
.label=${html`<b>Secure if possible</b> .label=${html`<b>Secure if possible</b>
<div class="secondary"> <div class="secondary">
Requires user interaction during inclusion. Fast and Requires user interaction during inclusion. Fast and
secure with S2 when supported. Fallback to legacy S0 or secure with S2 when supported. Fallback to legacy S0
no encryption when necessary. or no encryption when necessary.
</div>`} </div>`}
> >
<ha-radio <ha-radio
@ -146,8 +170,8 @@ class DialogZWaveJSAddNode extends LitElement {
<ha-formfield <ha-formfield
.label=${html`<b>Legacy Secure</b> .label=${html`<b>Legacy Secure</b>
<div class="secondary"> <div class="secondary">
Uses the older S0 security that is secure, but slow due Uses the older S0 security that is secure, but slow
to a lot of overhead. Allows securely including S2 due to a lot of overhead. Allows securely including S2
capable devices which fail to be included with S2. capable devices which fail to be included with S2.
</div>`} </div>`}
> >
@ -190,7 +214,10 @@ class DialogZWaveJSAddNode extends LitElement {
.localize=${this.hass.localize} .localize=${this.hass.localize}
@qr-code-scanned=${this._qrCodeScanned} @qr-code-scanned=${this._qrCodeScanned}
></ha-qr-scanner> ></ha-qr-scanner>
<mwc-button slot="secondaryAction" @click=${this._startOver}> <mwc-button
slot="secondaryAction"
@click=${this._startOver}
>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.zwave_js.common.back" "ui.panel.config.zwave_js.common.back"
)} )}
@ -228,7 +255,8 @@ class DialogZWaveJSAddNode extends LitElement {
: this._status === "grant_security_classes" : this._status === "grant_security_classes"
? html` ? html`
<h3> <h3>
The device has requested the following security classes: The device has requested the following security
classes:
</h3> </h3>
${this._error ${this._error
? html`<ha-alert alert-type="error" ? html`<ha-alert alert-type="error"
@ -471,7 +499,7 @@ class DialogZWaveJSAddNode extends LitElement {
: ""} : ""}
<a <a
href=${`/config/devices/device/${ href=${`/config/devices/device/${
this._device!.id this._device?.id
}`} }`}
> >
<mwc-button> <mwc-button>
@ -529,6 +557,15 @@ class DialogZWaveJSAddNode extends LitElement {
`; `;
} }
private _shouldPreventClose(): boolean {
return (
this._status === "started_specific" ||
this._status === "validate_dsk_enter_pin" ||
this._status === "grant_security_classes" ||
this._status === "waiting_for_device"
);
}
private _chooseInclusionStrategy(): void { private _chooseInclusionStrategy(): void {
this._unsubscribe(); this._unsubscribe();
this._status = "choose_strategy"; this._status = "choose_strategy";
@ -639,7 +676,7 @@ class DialogZWaveJSAddNode extends LitElement {
} }
private async _validateDskAndEnterPin(): Promise<void> { private async _validateDskAndEnterPin(): Promise<void> {
this._status = "loading"; this._status = "waiting_for_device";
this._error = undefined; this._error = undefined;
try { try {
await zwaveValidateDskAndEnterPin( await zwaveValidateDskAndEnterPin(
@ -656,7 +693,7 @@ class DialogZWaveJSAddNode extends LitElement {
} }
private async _grantSecurityClasses(): Promise<void> { private async _grantSecurityClasses(): Promise<void> {
this._status = "loading"; this._status = "waiting_for_device";
this._error = undefined; this._error = undefined;
try { try {
await zwaveGrantSecurityClasses( await zwaveGrantSecurityClasses(
@ -719,6 +756,12 @@ class DialogZWaveJSAddNode extends LitElement {
this._addNodeTimeoutHandle = undefined; this._addNodeTimeoutHandle = undefined;
} }
if (message.event === "node found") {
// The user may have to enter a PIN. Until then prevent accidentally
// closing the dialog
this._status = "waiting_for_device";
}
if (message.event === "validate dsk and enter pin") { if (message.event === "validate dsk and enter pin") {
this._status = "validate_dsk_enter_pin"; this._status = "validate_dsk_enter_pin";
this._dsk = message.dsk; this._dsk = message.dsk;
@ -775,6 +818,13 @@ class DialogZWaveJSAddNode extends LitElement {
}, 90000); }, 90000);
} }
private _onBeforeUnload = (event: BeforeUnloadEvent) => {
if (this._shouldPreventClose()) {
event.preventDefault();
}
event.returnValue = true;
};
private _unsubscribe(): void { private _unsubscribe(): void {
if (this._subscribed) { if (this._subscribed) {
this._subscribed.then((unsub) => unsub()); this._subscribed.then((unsub) => unsub());
@ -791,6 +841,7 @@ class DialogZWaveJSAddNode extends LitElement {
clearTimeout(this._addNodeTimeoutHandle); clearTimeout(this._addNodeTimeoutHandle);
} }
this._addNodeTimeoutHandle = undefined; this._addNodeTimeoutHandle = undefined;
window.removeEventListener("beforeunload", this._onBeforeUnload);
} }
public closeDialog(): void { public closeDialog(): void {

View File

@ -66,10 +66,26 @@ export class HaConfigLabels extends LitElement {
}) })
private _activeSorting?: SortingChangedEvent; private _activeSorting?: SortingChangedEvent;
@storage({
key: "labels-table-column-order",
state: false,
subscribe: false,
})
private _activeColumnOrder?: string[];
@storage({
key: "labels-table-hidden-columns",
state: false,
subscribe: false,
})
private _activeHiddenColumns?: string[];
private _columns = memoizeOne((localize: LocalizeFunc) => { private _columns = memoizeOne((localize: LocalizeFunc) => {
const columns: DataTableColumnContainer<LabelRegistryEntry> = { const columns: DataTableColumnContainer<LabelRegistryEntry> = {
icon: { icon: {
title: "", title: "",
moveable: false,
showNarrow: true,
label: localize("ui.panel.config.labels.headers.icon"), label: localize("ui.panel.config.labels.headers.icon"),
type: "icon", type: "icon",
template: (label) => template: (label) =>
@ -77,6 +93,7 @@ export class HaConfigLabels extends LitElement {
}, },
color: { color: {
title: "", title: "",
showNarrow: true,
label: localize("ui.panel.config.labels.headers.color"), label: localize("ui.panel.config.labels.headers.color"),
type: "icon", type: "icon",
template: (label) => template: (label) =>
@ -105,6 +122,9 @@ export class HaConfigLabels extends LitElement {
}, },
actions: { actions: {
title: "", title: "",
showNarrow: true,
moveable: false,
hideable: false,
width: "64px", width: "64px",
type: "overflow-menu", type: "overflow-menu",
template: (label) => html` template: (label) => html`
@ -167,6 +187,9 @@ export class HaConfigLabels extends LitElement {
.noDataText=${this.hass.localize("ui.panel.config.labels.no_labels")} .noDataText=${this.hass.localize("ui.panel.config.labels.no_labels")}
hasFab hasFab
.initialSorting=${this._activeSorting} .initialSorting=${this._activeSorting}
.columnOrder=${this._activeColumnOrder}
.hiddenColumns=${this._activeHiddenColumns}
@columns-changed=${this._handleColumnsChanged}
@sorting-changed=${this._handleSortingChanged} @sorting-changed=${this._handleSortingChanged}
.filter=${this._filter} .filter=${this._filter}
@search-changed=${this._handleSearchChange} @search-changed=${this._handleSearchChange}
@ -297,6 +320,11 @@ export class HaConfigLabels extends LitElement {
private _handleSearchChange(ev: CustomEvent) { private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value; this._filter = ev.detail.value;
} }
private _handleColumnsChanged(ev: CustomEvent) {
this._activeColumnOrder = ev.detail.columnOrder;
this._activeHiddenColumns = ev.detail.hiddenColumns;
}
} }
declare global { declare global {

View File

@ -85,6 +85,20 @@ export class HaConfigLovelaceDashboards extends LitElement {
}) })
private _activeSorting?: SortingChangedEvent; private _activeSorting?: SortingChangedEvent;
@storage({
key: "lovelace-dashboards-table-column-order",
state: false,
subscribe: false,
})
private _activeColumnOrder?: string[];
@storage({
key: "lovelace-dashboards-table-hidden-columns",
state: false,
subscribe: false,
})
private _activeHiddenColumns?: string[];
public willUpdate() { public willUpdate() {
if (!this.hasUpdated) { if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace"); this.hass.loadFragmentTranslation("lovelace");
@ -101,6 +115,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
const columns: DataTableColumnContainer<DataTableItem> = { const columns: DataTableColumnContainer<DataTableItem> = {
icon: { icon: {
title: "", title: "",
moveable: false,
showNarrow: true,
label: localize( label: localize(
"ui.panel.config.lovelace.dashboards.picker.headers.icon" "ui.panel.config.lovelace.dashboards.picker.headers.icon"
), ),
@ -128,8 +144,9 @@ export class HaConfigLovelaceDashboards extends LitElement {
sortable: true, sortable: true,
filterable: true, filterable: true,
grows: true, grows: true,
template: (dashboard) => { template: narrow
const titleTemplate = html` ? undefined
: (dashboard) => html`
${dashboard.title} ${dashboard.title}
${dashboard.default ${dashboard.default
? html` ? html`
@ -144,24 +161,10 @@ export class HaConfigLovelaceDashboards extends LitElement {
</simple-tooltip> </simple-tooltip>
` `
: ""} : ""}
`; `,
return narrow
? html`
${titleTemplate}
<div class="secondary">
${this.hass.localize(
`ui.panel.config.lovelace.dashboards.conf_mode.${dashboard.mode}`
)}${dashboard.filename
? html` ${dashboard.filename} `
: ""}
</div>
`
: titleTemplate;
},
}, },
}; };
if (!narrow) {
columns.mode = { columns.mode = {
title: localize( title: localize(
"ui.panel.config.lovelace.dashboards.picker.headers.conf_mode" "ui.panel.config.lovelace.dashboards.picker.headers.conf_mode"
@ -191,6 +194,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
), ),
sortable: true, sortable: true,
type: "icon", type: "icon",
hidden: narrow,
width: "100px", width: "100px",
template: (dashboard) => template: (dashboard) =>
dashboard.require_admin dashboard.require_admin
@ -202,13 +206,13 @@ export class HaConfigLovelaceDashboards extends LitElement {
"ui.panel.config.lovelace.dashboards.picker.headers.sidebar" "ui.panel.config.lovelace.dashboards.picker.headers.sidebar"
), ),
type: "icon", type: "icon",
hidden: narrow,
width: "121px", width: "121px",
template: (dashboard) => template: (dashboard) =>
dashboard.show_in_sidebar dashboard.show_in_sidebar
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>` ? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
: html``, : html``,
}; };
}
columns.url_path = { columns.url_path = {
title: "", title: "",
@ -216,6 +220,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
"ui.panel.config.lovelace.dashboards.picker.headers.url" "ui.panel.config.lovelace.dashboards.picker.headers.url"
), ),
filterable: true, filterable: true,
showNarrow: true,
width: "100px", width: "100px",
template: (dashboard) => template: (dashboard) =>
narrow narrow
@ -311,6 +316,9 @@ export class HaConfigLovelaceDashboards extends LitElement {
)} )}
.data=${this._getItems(this._dashboards)} .data=${this._getItems(this._dashboards)}
.initialSorting=${this._activeSorting} .initialSorting=${this._activeSorting}
.columnOrder=${this._activeColumnOrder}
.hiddenColumns=${this._activeHiddenColumns}
@columns-changed=${this._handleColumnsChanged}
@sorting-changed=${this._handleSortingChanged} @sorting-changed=${this._handleSortingChanged}
.filter=${this._filter} .filter=${this._filter}
@search-changed=${this._handleSearchChange} @search-changed=${this._handleSearchChange}
@ -467,6 +475,11 @@ export class HaConfigLovelaceDashboards extends LitElement {
private _handleSearchChange(ev: CustomEvent) { private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value; this._filter = ev.detail.value;
} }
private _handleColumnsChanged(ev: CustomEvent) {
this._activeColumnOrder = ev.detail.columnOrder;
this._activeHiddenColumns = ev.detail.hiddenColumns;
}
} }
declare global { declare global {

View File

@ -67,12 +67,27 @@ export class HaConfigLovelaceRescources extends LitElement {
}) })
private _activeSorting?: SortingChangedEvent; private _activeSorting?: SortingChangedEvent;
@storage({
key: "lovelace-resources-table-column-order",
state: false,
subscribe: false,
})
private _activeColumnOrder?: string[];
@storage({
key: "lovelace-resources-table-hidden-columns",
state: false,
subscribe: false,
})
private _activeHiddenColumns?: string[];
private _columns = memoize( private _columns = memoize(
( (
_language, _language,
localize: LocalizeFunc localize: LocalizeFunc
): DataTableColumnContainer<LovelaceResource> => ({ ): DataTableColumnContainer<LovelaceResource> => ({
url: { url: {
main: true,
title: localize( title: localize(
"ui.panel.config.lovelace.resources.picker.headers.url" "ui.panel.config.lovelace.resources.picker.headers.url"
), ),
@ -145,6 +160,9 @@ export class HaConfigLovelaceRescources extends LitElement {
"ui.panel.config.lovelace.resources.picker.no_resources" "ui.panel.config.lovelace.resources.picker.no_resources"
)} )}
.initialSorting=${this._activeSorting} .initialSorting=${this._activeSorting}
.columnOrder=${this._activeColumnOrder}
.hiddenColumns=${this._activeHiddenColumns}
@columns-changed=${this._handleColumnsChanged}
@sorting-changed=${this._handleSortingChanged} @sorting-changed=${this._handleSortingChanged}
.filter=${this._filter} .filter=${this._filter}
@search-changed=${this._handleSearchChange} @search-changed=${this._handleSearchChange}
@ -266,6 +284,11 @@ export class HaConfigLovelaceRescources extends LitElement {
this._filter = ev.detail.value; this._filter = ev.detail.value;
} }
private _handleColumnsChanged(ev: CustomEvent) {
this._activeColumnOrder = ev.detail.columnOrder;
this._activeHiddenColumns = ev.detail.hiddenColumns;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,

View File

@ -1,13 +1,17 @@
import "@material/mwc-button"; import { mdiPencil } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import "../../../components/entity/ha-entities-picker"; import "../../../components/entity/ha-entities-picker";
import "../../../components/ha-button";
import { createCloseHeading } from "../../../components/ha-dialog"; import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-formfield"; import "../../../components/ha-formfield";
import "../../../components/ha-icon-button";
import "../../../components/ha-picture-upload"; import "../../../components/ha-picture-upload";
import type { HaPictureUpload } from "../../../components/ha-picture-upload"; import type { HaPictureUpload } from "../../../components/ha-picture-upload";
import "../../../components/ha-settings-row";
import "../../../components/ha-textfield"; import "../../../components/ha-textfield";
import { adminChangeUsername } from "../../../data/auth";
import { PersonMutableParams } from "../../../data/person"; import { PersonMutableParams } from "../../../data/person";
import { import {
deleteUser, deleteUser,
@ -19,10 +23,11 @@ import {
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
showPromptDialog,
} from "../../../dialogs/generic/show-dialog-box"; } from "../../../dialogs/generic/show-dialog-box";
import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog"; import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { ValueChangedEvent, HomeAssistant } from "../../../types";
import { haStyleDialog } from "../../../resources/styles"; import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant, ValueChangedEvent } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import { showAddUserDialog } from "../users/show-dialog-add-user"; import { showAddUserDialog } from "../users/show-dialog-add-user";
import { showAdminChangePasswordDialog } from "../users/show-dialog-admin-change-password"; import { showAdminChangePasswordDialog } from "../users/show-dialog-admin-change-password";
@ -135,11 +140,17 @@ class DialogPersonDetail extends LitElement {
@change=${this._pictureChanged} @change=${this._pictureChanged}
></ha-picture-upload> ></ha-picture-upload>
<ha-formfield <ha-settings-row>
.label=${this.hass!.localize( <span slot="heading">
${this.hass!.localize(
"ui.panel.config.person.detail.allow_login" "ui.panel.config.person.detail.allow_login"
)} )}
> </span>
<span slot="description">
${this.hass!.localize(
"ui.panel.config.person.detail.allow_login_description"
)}
</span>
<ha-switch <ha-switch
@change=${this._allowLoginChanged} @change=${this._allowLoginChanged}
.disabled=${this._user && .disabled=${this._user &&
@ -148,34 +159,9 @@ class DialogPersonDetail extends LitElement {
this._user.is_owner)} this._user.is_owner)}
.checked=${this._userId} .checked=${this._userId}
></ha-switch> ></ha-switch>
</ha-formfield> </ha-settings-row>
${this._user ${this._renderUserFields()}
? html`<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.person.detail.local_only"
)}
>
<ha-switch
.checked=${this._localOnly}
@change=${this._localOnlyChanged}
>
</ha-switch>
</ha-formfield>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.person.detail.admin"
)}
>
<ha-switch
.disabled=${this._user.system_generated ||
this._user.is_owner}
.checked=${this._isAdmin}
@change=${this._adminChanged}
>
</ha-switch>
</ha-formfield>`
: ""}
${this._deviceTrackersAvailable(this.hass) ${this._deviceTrackersAvailable(this.hass)
? html` ? html`
<p> <p>
@ -233,7 +219,7 @@ class DialogPersonDetail extends LitElement {
</div> </div>
${this._params.entry ${this._params.entry
? html` ? html`
<mwc-button <ha-button
slot="secondaryAction" slot="secondaryAction"
class="warning" class="warning"
@click=${this._deleteEntry} @click=${this._deleteEntry}
@ -241,20 +227,10 @@ class DialogPersonDetail extends LitElement {
this._submitting} this._submitting}
> >
${this.hass!.localize("ui.panel.config.person.detail.delete")} ${this.hass!.localize("ui.panel.config.person.detail.delete")}
</mwc-button> </ha-button>
${this._user && this.hass.user?.is_owner
? html`<mwc-button
slot="secondaryAction"
@click=${this._changePassword}
>
${this.hass.localize(
"ui.panel.config.users.editor.change_password"
)}
</mwc-button>`
: ""}
` `
: nothing} : nothing}
<mwc-button <ha-button
slot="primaryAction" slot="primaryAction"
@click=${this._updateEntry} @click=${this._updateEntry}
.disabled=${nameInvalid || this._submitting} .disabled=${nameInvalid || this._submitting}
@ -262,11 +238,96 @@ class DialogPersonDetail extends LitElement {
${this._params.entry ${this._params.entry
? this.hass!.localize("ui.panel.config.person.detail.update") ? this.hass!.localize("ui.panel.config.person.detail.update")
: this.hass!.localize("ui.panel.config.person.detail.create")} : this.hass!.localize("ui.panel.config.person.detail.create")}
</mwc-button> </ha-button>
</ha-dialog> </ha-dialog>
`; `;
} }
private _renderUserFields() {
const user = this._user;
if (!user) return nothing;
return html`
${!user.system_generated
? html`
<ha-settings-row>
<span slot="heading">
${this.hass.localize("ui.panel.config.person.detail.username")}
</span>
<span slot="description">${user.username}</span>
${this.hass.user?.is_owner
? html`
<ha-icon-button
.path=${mdiPencil}
@click=${this._changeUsername}
.label=${this.hass.localize(
"ui.panel.config.person.detail.change_username"
)}
>
</ha-icon-button>
`
: nothing}
</ha-settings-row>
`
: nothing}
${!user.system_generated && this.hass.user?.is_owner
? html`
<ha-settings-row>
<span slot="heading">
${this.hass.localize("ui.panel.config.person.detail.password")}
</span>
<span slot="description">************</span>
${this.hass.user?.is_owner
? html`
<ha-icon-button
.path=${mdiPencil}
@click=${this._changePassword}
.label=${this.hass.localize(
"ui.panel.config.person.detail.change_password"
)}
>
</ha-icon-button>
`
: nothing}
</ha-settings-row>
`
: nothing}
<ha-settings-row>
<span slot="heading">
${this.hass.localize(
"ui.panel.config.person.detail.local_access_only"
)}
</span>
<span slot="description">
${this.hass.localize(
"ui.panel.config.person.detail.local_access_only_description"
)}
</span>
<ha-switch
.disabled=${user.system_generated}
.checked=${this._localOnly}
@change=${this._localOnlyChanged}
>
</ha-switch>
</ha-settings-row>
<ha-settings-row>
<span slot="heading">
${this.hass.localize("ui.panel.config.person.detail.admin")}
</span>
<span slot="description">
${this.hass.localize(
"ui.panel.config.person.detail.admin_description"
)}
</span>
<ha-switch
.disabled=${user.system_generated || user.is_owner}
.checked=${this._isAdmin}
@change=${this._adminChanged}
>
</ha-switch>
</ha-settings-row>
`;
}
private _closeDialog() { private _closeDialog() {
this._params = undefined; this._params = undefined;
} }
@ -292,11 +353,14 @@ class DialogPersonDetail extends LitElement {
userAddedCallback: async (user?: User) => { userAddedCallback: async (user?: User) => {
if (user) { if (user) {
target.checked = true; target.checked = true;
if (this._params!.entry) {
await this._params!.updateEntry({ user_id: user.id });
}
this._params?.refreshUsers();
this._user = user; this._user = user;
this._userId = user.id; this._userId = user.id;
this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN); this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
this._localOnly = user.local_only; this._localOnly = user.local_only;
this._params?.refreshUsers();
} }
}, },
name: this._name, name: this._name,
@ -304,14 +368,16 @@ class DialogPersonDetail extends LitElement {
} else if (this._userId) { } else if (this._userId) {
if ( if (
!(await showConfirmationDialog(this, { !(await showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.config.person.detail.confirm_delete_user_title"
),
text: this.hass!.localize( text: this.hass!.localize(
"ui.panel.config.person.detail.confirm_delete_user", "ui.panel.config.person.detail.confirm_delete_user_text",
{ name: this._name } { name: this._name }
), ),
confirmText: this.hass!.localize( confirmText: this.hass!.localize("ui.common.delete"),
"ui.panel.config.person.detail.delete"
),
dismissText: this.hass!.localize("ui.common.cancel"), dismissText: this.hass!.localize("ui.common.cancel"),
destructive: true,
})) }))
) { ) {
target.checked = true; target.checked = true;
@ -320,6 +386,9 @@ class DialogPersonDetail extends LitElement {
await deleteUser(this.hass, this._userId); await deleteUser(this.hass, this._userId);
this._params?.refreshUsers(); this._params?.refreshUsers();
this._userId = undefined; this._userId = undefined;
this._user = undefined;
this._isAdmin = undefined;
this._localOnly = undefined;
} }
} }
@ -349,6 +418,53 @@ class DialogPersonDetail extends LitElement {
showAdminChangePasswordDialog(this, { userId: this._user.id }); showAdminChangePasswordDialog(this, { userId: this._user.id });
} }
private async _changeUsername() {
if (!this._user) {
return;
}
const credential = this._user.credentials.find(
(cred) => cred.type === "homeassistant"
);
if (!credential) {
showAlertDialog(this, {
title: "No Home Assistant credentials found.",
});
return;
}
const newUsername = await showPromptDialog(this, {
inputLabel: this.hass.localize(
"ui.panel.config.users.change_username.new_username"
),
confirmText: this.hass.localize(
"ui.panel.config.users.change_username.change"
),
title: this.hass.localize(
"ui.panel.config.users.change_username.caption"
),
defaultValue: this._user.username!,
});
if (newUsername) {
try {
await adminChangeUsername(this.hass, this._user.id, newUsername);
this._params?.refreshUsers();
this._user = { ...this._user, username: newUsername };
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.users.change_username.username_changed"
),
});
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.users.change_username.failed"
),
text: err.message,
});
}
}
}
private async _updateEntry() { private async _updateEntry() {
this._submitting = true; this._submitting = true;
try { try {
@ -425,9 +541,8 @@ class DialogPersonDetail extends LitElement {
margin-bottom: 16px; margin-bottom: 16px;
--file-upload-image-border-radius: 50%; --file-upload-image-border-radius: 50%;
} }
ha-formfield { ha-settings-row {
display: block; padding: 0;
padding: 16px 0;
} }
a { a {
color: var(--primary-color); color: var(--primary-color);

View File

@ -31,8 +31,6 @@ class DialogIntegrationStartup extends LitElement {
return html` return html`
<ha-dialog <ha-dialog
open open
scrimClickAction
escapeKeyAction
hideActions hideActions
.heading=${createCloseHeading( .heading=${createCloseHeading(
this.hass, this.hass,

View File

@ -143,8 +143,6 @@ class DialogSystemInformation extends LitElement {
<ha-dialog <ha-dialog
open open
@closed=${this.closeDialog} @closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading( .heading=${createCloseHeading(
this.hass, this.hass,
this.hass.localize("ui.panel.config.repairs.system_information") this.hass.localize("ui.panel.config.repairs.system_information")

View File

@ -180,6 +180,20 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
}) })
private _activeCollapsed?: string; private _activeCollapsed?: string;
@storage({
key: "scene-table-column-order",
state: false,
subscribe: false,
})
private _activeColumnOrder?: string[];
@storage({
key: "scene-table-hidden-columns",
state: false,
subscribe: false,
})
private _activeHiddenColumns?: string[];
private _sizeController = new ResizeController(this, { private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width, callback: (entries) => entries[0]?.contentRect.width,
}); });
@ -225,11 +239,13 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
); );
private _columns = memoizeOne( private _columns = memoizeOne(
(narrow, localize: LocalizeFunc): DataTableColumnContainer => { (localize: LocalizeFunc): DataTableColumnContainer => {
const columns: DataTableColumnContainer<SceneItem> = { const columns: DataTableColumnContainer<SceneItem> = {
icon: { icon: {
title: "", title: "",
label: localize("ui.panel.config.scene.picker.headers.state"), label: localize("ui.panel.config.scene.picker.headers.state"),
moveable: false,
showNarrow: true,
type: "icon", type: "icon",
template: (scene) => html` template: (scene) => html`
<ha-state-icon <ha-state-icon
@ -245,15 +261,13 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true, grows: true,
template: (scene) => html` extraTemplate: (scene) =>
<div style="font-size: 14px;">${scene.name}</div> scene.labels.length
${scene.labels.length
? html`<ha-data-table-labels ? html`<ha-data-table-labels
@label-clicked=${this._labelClicked} @label-clicked=${this._labelClicked}
.labels=${scene.labels} .labels=${scene.labels}
></ha-data-table-labels>` ></ha-data-table-labels>`
: nothing} : nothing,
`,
}, },
area: { area: {
title: localize("ui.panel.config.scene.picker.headers.area"), title: localize("ui.panel.config.scene.picker.headers.area"),
@ -281,7 +295,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
), ),
sortable: true, sortable: true,
width: "30%", width: "30%",
hidden: narrow,
template: (scene) => { template: (scene) => {
const lastActivated = scene.state; const lastActivated = scene.state;
if (!lastActivated || isUnavailableState(lastActivated)) { if (!lastActivated || isUnavailableState(lastActivated)) {
@ -300,6 +313,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
only_editable: { only_editable: {
title: "", title: "",
width: "56px", width: "56px",
showNarrow: true,
template: (scene) => template: (scene) =>
!scene.attributes.id !scene.attributes.id
? html` ? html`
@ -319,6 +333,9 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
title: "", title: "",
width: "64px", width: "64px",
type: "overflow-menu", type: "overflow-menu",
showNarrow: true,
moveable: false,
hideable: false,
template: (scene) => html` template: (scene) => html`
<ha-icon-overflow-menu <ha-icon-overflow-menu
.hass=${this.hass} .hass=${this.hass}
@ -536,11 +553,14 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
Array.isArray(val) ? val.length : val Array.isArray(val) ? val.length : val
) )
).length} ).length}
.columns=${this._columns(this.narrow, this.hass.localize)} .columns=${this._columns(this.hass.localize)}
id="entity_id" id="entity_id"
.initialGroupColumn=${this._activeGrouping || "category"} .initialGroupColumn=${this._activeGrouping || "category"}
.initialCollapsedGroups=${this._activeCollapsed} .initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting} .initialSorting=${this._activeSorting}
.columnOrder=${this._activeColumnOrder}
.hiddenColumns=${this._activeHiddenColumns}
@columns-changed=${this._handleColumnsChanged}
@sorting-changed=${this._handleSortingChanged} @sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged} @grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged} @collapsed-changed=${this._handleCollapseChanged}
@ -1155,6 +1175,11 @@ ${rejected
this._filter = ev.detail.value; this._filter = ev.detail.value;
} }
private _handleColumnsChanged(ev: CustomEvent) {
this._activeColumnOrder = ev.detail.columnOrder;
this._activeHiddenColumns = ev.detail.hiddenColumns;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,

View File

@ -2,10 +2,10 @@ import "@material/mwc-button/mwc-button";
import { html, nothing } from "lit"; import { html, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import { BlueprintScriptConfig } from "../../../data/script";
import { fetchBlueprints } from "../../../data/blueprint";
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
import "../../../components/ha-markdown"; import "../../../components/ha-markdown";
import { fetchBlueprints } from "../../../data/blueprint";
import { BlueprintScriptConfig } from "../../../data/script";
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
@customElement("blueprint-script-editor") @customElement("blueprint-script-editor")
export class HaBlueprintScriptEditor extends HaBlueprintGenericEditor { export class HaBlueprintScriptEditor extends HaBlueprintGenericEditor {

View File

@ -6,6 +6,7 @@ import {
mdiDebugStepOver, mdiDebugStepOver,
mdiDelete, mdiDelete,
mdiDotsVertical, mdiDotsVertical,
mdiFileEdit,
mdiFormTextbox, mdiFormTextbox,
mdiInformationOutline, mdiInformationOutline,
mdiPlay, mdiPlay,
@ -40,6 +41,7 @@ import { validateConfig } from "../../../data/config";
import { UNAVAILABLE } from "../../../data/entity"; import { UNAVAILABLE } from "../../../data/entity";
import { EntityRegistryEntry } from "../../../data/entity_registry"; import { EntityRegistryEntry } from "../../../data/entity_registry";
import { import {
BlueprintScriptConfig,
ScriptConfig, ScriptConfig,
deleteScript, deleteScript,
fetchScriptFileConfig, fetchScriptFileConfig,
@ -61,6 +63,7 @@ import { showAutomationRenameDialog } from "../automation/automation-rename-dial
import "./blueprint-script-editor"; import "./blueprint-script-editor";
import "./manual-script-editor"; import "./manual-script-editor";
import type { HaManualScriptEditor } from "./manual-script-editor"; import type { HaManualScriptEditor } from "./manual-script-editor";
import { substituteBlueprint } from "../../../data/blueprint";
export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) { export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -228,6 +231,24 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
></ha-svg-icon> ></ha-svg-icon>
</ha-list-item> </ha-list-item>
${useBlueprint
? html`
<ha-list-item
graphic="icon"
@click=${this._takeControl}
.disabled=${this._readOnly || this._mode === "yaml"}
>
${this.hass.localize(
"ui.panel.config.script.editor.take_control"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiFileEdit}
></ha-svg-icon>
</ha-list-item>
`
: nothing}
<li divider role="separator"></li> <li divider role="separator"></li>
<ha-list-item graphic="icon" @click=${this._switchUiMode}> <ha-list-item graphic="icon" @click=${this._switchUiMode}>
@ -601,6 +622,45 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
} }
}; };
private async _takeControl() {
const config = this._config as BlueprintScriptConfig;
const confirmation = await showConfirmationDialog(this, {
title: this.hass!.localize(
"ui.panel.config.script.editor.take_control_confirmation.title"
),
text: this.hass!.localize(
"ui.panel.config.script.editor.take_control_confirmation.text"
),
confirmText: this.hass!.localize(
"ui.panel.config.script.editor.take_control_confirmation.action"
),
});
if (!confirmation) return;
try {
const result = await substituteBlueprint(
this.hass,
"script",
config.use_blueprint.path,
config.use_blueprint.input || {}
);
const newConfig = {
...this._normalizeConfig(result.substituted_config),
alias: config.alias,
description: config.description,
};
this._config = newConfig;
this._dirty = true;
this._errors = undefined;
} catch (err: any) {
this._errors = err.message;
}
}
private async _duplicate() { private async _duplicate() {
const result = this._readOnly const result = this._readOnly
? await showConfirmationDialog(this, { ? await showConfirmationDialog(this, {

View File

@ -184,6 +184,20 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
}) })
private _activeCollapsed?: string; private _activeCollapsed?: string;
@storage({
key: "script-table-column-order",
state: false,
subscribe: false,
})
private _activeColumnOrder?: string[];
@storage({
key: "script-table-hidden-columns",
state: false,
subscribe: false,
})
private _activeHiddenColumns?: string[];
private _sizeController = new ResizeController(this, { private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width, callback: (entries) => entries[0]?.contentRect.width,
}); });
@ -232,14 +246,12 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
); );
private _columns = memoizeOne( private _columns = memoizeOne(
( (localize: LocalizeFunc): DataTableColumnContainer<ScriptItem> => {
narrow,
localize: LocalizeFunc,
locale: HomeAssistant["locale"]
): DataTableColumnContainer<ScriptItem> => {
const columns: DataTableColumnContainer = { const columns: DataTableColumnContainer = {
icon: { icon: {
title: "", title: "",
showNarrow: true,
moveable: false,
label: localize("ui.panel.config.script.picker.headers.state"), label: localize("ui.panel.config.script.picker.headers.state"),
type: "icon", type: "icon",
template: (script) => template: (script) =>
@ -259,30 +271,13 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true, grows: true,
template: (script) => { extraTemplate: (script) =>
const date = new Date(script.last_triggered); script.labels.length
const now = new Date();
const dayDifference = differenceInDays(now, date);
return html`
<div style="font-size: 14px;">${script.name}</div>
${narrow
? html`<div class="secondary">
${this.hass.localize("ui.card.automation.last_triggered")}:
${script.attributes.last_triggered
? dayDifference > 3
? formatShortDateTime(date, locale, this.hass.config)
: relativeTime(date, locale)
: localize("ui.components.relative_time.never")}
</div>`
: nothing}
${script.labels.length
? html`<ha-data-table-labels ? html`<ha-data-table-labels
@label-clicked=${this._labelClicked} @label-clicked=${this._labelClicked}
.labels=${script.labels} .labels=${script.labels}
></ha-data-table-labels>` ></ha-data-table-labels>`
: nothing} : nothing,
`;
},
}, },
area: { area: {
title: localize("ui.panel.config.script.picker.headers.area"), title: localize("ui.panel.config.script.picker.headers.area"),
@ -305,7 +300,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
template: (script) => script.labels.map((lbl) => lbl.name).join(" "), template: (script) => script.labels.map((lbl) => lbl.name).join(" "),
}, },
last_triggered: { last_triggered: {
hidden: narrow,
sortable: true, sortable: true,
width: "40%", width: "40%",
title: localize("ui.card.automation.last_triggered"), title: localize("ui.card.automation.last_triggered"),
@ -330,6 +324,9 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
title: "", title: "",
width: "64px", width: "64px",
type: "overflow-menu", type: "overflow-menu",
showNarrow: true,
moveable: false,
hideable: false,
template: (script) => html` template: (script) => html`
<ha-icon-overflow-menu <ha-icon-overflow-menu
.hass=${this.hass} .hass=${this.hass}
@ -539,6 +536,9 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
.initialGroupColumn=${this._activeGrouping || "category"} .initialGroupColumn=${this._activeGrouping || "category"}
.initialCollapsedGroups=${this._activeCollapsed} .initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting} .initialSorting=${this._activeSorting}
.columnOrder=${this._activeColumnOrder}
.hiddenColumns=${this._activeHiddenColumns}
@columns-changed=${this._handleColumnsChanged}
@sorting-changed=${this._handleSortingChanged} @sorting-changed=${this._handleSortingChanged}
@grouping-changed=${this._handleGroupingChanged} @grouping-changed=${this._handleGroupingChanged}
@collapsed-changed=${this._handleCollapseChanged} @collapsed-changed=${this._handleCollapseChanged}
@ -553,11 +553,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
Array.isArray(val) ? val.length : val Array.isArray(val) ? val.length : val
) )
).length} ).length}
.columns=${this._columns( .columns=${this._columns(this.hass.localize)}
this.narrow,
this.hass.localize,
this.hass.locale
)}
.data=${scripts} .data=${scripts}
.empty=${!this.scripts.length} .empty=${!this.scripts.length}
.activeFilters=${this._activeFilters} .activeFilters=${this._activeFilters}
@ -1270,6 +1266,11 @@ ${rejected
this._activeCollapsed = ev.detail.value; this._activeCollapsed = ev.detail.value;
} }
private _handleColumnsChanged(ev: CustomEvent) {
this._activeColumnOrder = ev.detail.columnOrder;
this._activeHiddenColumns = ev.detail.hiddenColumns;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,

View File

@ -66,11 +66,12 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
}) })
private _filter = ""; private _filter = "";
private _columns = memoizeOne( private _columns = memoizeOne((localize: LocalizeFunc) => {
(narrow: boolean, _language, localize: LocalizeFunc) => {
const columns: DataTableColumnContainer<TagRowData> = { const columns: DataTableColumnContainer<TagRowData> = {
icon: { icon: {
title: "", title: "",
moveable: false,
showNarrow: true,
label: localize("ui.panel.config.tag.headers.icon"), label: localize("ui.panel.config.tag.headers.icon"),
type: "icon", type: "icon",
template: (tag) => html`<tag-image .tag=${tag}></tag-image>`, template: (tag) => html`<tag-image .tag=${tag}></tag-image>`,
@ -81,24 +82,10 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
sortable: true, sortable: true,
filterable: true, filterable: true,
grows: true, grows: true,
template: (tag) =>
html`${tag.display_name}
${narrow
? html`<div class="secondary">
${tag.last_scanned_datetime
? html`<ha-relative-time
.hass=${this.hass}
.datetime=${tag.last_scanned_datetime}
capitalize
></ha-relative-time>`
: this.hass.localize("ui.panel.config.tag.never_scanned")}
</div>`
: ""}`,
}, },
last_scanned_datetime: { last_scanned_datetime: {
title: localize("ui.panel.config.tag.headers.last_scanned"), title: localize("ui.panel.config.tag.headers.last_scanned"),
sortable: true, sortable: true,
hidden: narrow,
direction: "desc", direction: "desc",
width: "20%", width: "20%",
template: (tag) => html` template: (tag) => html`
@ -117,8 +104,9 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
title: "", title: "",
label: localize("ui.panel.config.tag.headers.write"), label: localize("ui.panel.config.tag.headers.write"),
type: "icon-button", type: "icon-button",
showNarrow: true,
template: (tag) => template: (tag) =>
html` <ha-icon-button html`<ha-icon-button
.tag=${tag} .tag=${tag}
@click=${this._handleWriteClick} @click=${this._handleWriteClick}
.label=${this.hass.localize("ui.panel.config.tag.write")} .label=${this.hass.localize("ui.panel.config.tag.write")}
@ -129,21 +117,23 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
columns.automation = { columns.automation = {
title: "", title: "",
type: "icon-button", type: "icon-button",
showNarrow: true,
template: (tag) => template: (tag) =>
html` <ha-icon-button html`<ha-icon-button
.tag=${tag} .tag=${tag}
@click=${this._handleAutomationClick} @click=${this._handleAutomationClick}
.label=${this.hass.localize( .label=${this.hass.localize("ui.panel.config.tag.create_automation")}
"ui.panel.config.tag.create_automation"
)}
.path=${mdiRobot} .path=${mdiRobot}
></ha-icon-button>`, ></ha-icon-button>`,
}; };
columns.edit = { columns.edit = {
title: "", title: "",
type: "icon-button", type: "icon-button",
showNarrow: true,
hideable: false,
moveable: false,
template: (tag) => template: (tag) =>
html` <ha-icon-button html`<ha-icon-button
.tag=${tag} .tag=${tag}
@click=${this._handleEditClick} @click=${this._handleEditClick}
.label=${this.hass.localize("ui.panel.config.tag.edit")} .label=${this.hass.localize("ui.panel.config.tag.edit")}
@ -151,8 +141,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
></ha-icon-button>`, ></ha-icon-button>`,
}; };
return columns; return columns;
} });
);
private _data = memoizeOne((tags: Tag[]): TagRowData[] => private _data = memoizeOne((tags: Tag[]): TagRowData[] =>
tags.map((tag) => ({ tags.map((tag) => ({
@ -191,11 +180,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
back-path="/config" back-path="/config"
.route=${this.route} .route=${this.route}
.tabs=${configSections.tags} .tabs=${configSections.tags}
.columns=${this._columns( .columns=${this._columns(this.hass.localize)}
this.narrow,
this.hass.language,
this.hass.localize
)}
.data=${this._data(this._tags)} .data=${this._data(this._tags)}
.noDataText=${this.hass.localize("ui.panel.config.tag.no_tags")} .noDataText=${this.hass.localize("ui.panel.config.tag.no_tags")}
.filter=${this._filter} .filter=${this._filter}

View File

@ -1,31 +1,34 @@
import "@material/mwc-button";
import { import {
css,
CSSResultGroup, CSSResultGroup,
html,
LitElement, LitElement,
PropertyValues, PropertyValues,
css,
html,
nothing, nothing,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-circular-progress"; import "../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../components/ha-dialog"; import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-formfield"; import "../../../components/ha-formfield";
import "../../../components/ha-icon-button";
import "../../../components/ha-settings-row";
import "../../../components/ha-switch"; import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch"; import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
import { createAuthForUser } from "../../../data/auth"; import { createAuthForUser } from "../../../data/auth";
import { import {
createUser,
deleteUser,
SYSTEM_GROUP_ID_ADMIN, SYSTEM_GROUP_ID_ADMIN,
SYSTEM_GROUP_ID_USER, SYSTEM_GROUP_ID_USER,
User, User,
createUser,
deleteUser,
} from "../../../data/user"; } from "../../../data/user";
import { ValueChangedEvent, HomeAssistant } from "../../../types";
import { haStyleDialog } from "../../../resources/styles"; import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant, ValueChangedEvent } from "../../../types";
import { AddUserDialogParams } from "./show-dialog-add-user"; import { AddUserDialogParams } from "./show-dialog-add-user";
import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield";
@customElement("dialog-add-user") @customElement("dialog-add-user")
export class DialogAddUser extends LitElement { export class DialogAddUser extends LitElement {
@ -155,38 +158,44 @@ export class DialogAddUser extends LitElement {
"ui.panel.config.users.add_user.password_not_match" "ui.panel.config.users.add_user.password_not_match"
)} )}
></ha-textfield> ></ha-textfield>
<div class="row"> <ha-settings-row>
<ha-formfield <span slot="heading">
.label=${this.hass.localize( ${this.hass.localize(
"ui.panel.config.users.editor.local_only" "ui.panel.config.users.editor.local_access_only"
)} )}
> </span>
<span slot="description">
${this.hass.localize(
"ui.panel.config.users.editor.local_access_only_description"
)}
</span>
<ha-switch <ha-switch
.checked=${this._localOnly} .checked=${this._localOnly}
@change=${this._localOnlyChanged} @change=${this._localOnlyChanged}
> >
</ha-switch> </ha-switch>
</ha-formfield> </ha-settings-row>
</div> <ha-settings-row>
<div class="row"> <span slot="heading">
<ha-formfield ${this.hass.localize("ui.panel.config.users.editor.admin")}
.label=${this.hass.localize("ui.panel.config.users.editor.admin")} </span>
> <span slot="description">
<ha-switch ${this.hass.localize(
.checked=${this._isAdmin} "ui.panel.config.users.editor.admin_description"
@change=${this._adminChanged} )}
> </span>
<ha-switch .checked=${this._isAdmin} @change=${this._adminChanged}>
</ha-switch> </ha-switch>
</ha-formfield> </ha-settings-row>
</div>
${!this._isAdmin ${!this._isAdmin
? html` ? html`
<br /> <ha-alert alert-type="info">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.users.users_privileges_note" "ui.panel.config.users.users_privileges_note"
)} )}
</ha-alert>
` `
: ""} : nothing}
</div> </div>
${this._loading ${this._loading
? html` ? html`
@ -195,7 +204,7 @@ export class DialogAddUser extends LitElement {
</div> </div>
` `
: html` : html`
<mwc-button <ha-button
slot="primaryAction" slot="primaryAction"
.disabled=${!this._name || .disabled=${!this._name ||
!this._username || !this._username ||
@ -204,7 +213,7 @@ export class DialogAddUser extends LitElement {
@click=${this._createUser} @click=${this._createUser}
> >
${this.hass.localize("ui.panel.config.users.add_user.create")} ${this.hass.localize("ui.panel.config.users.add_user.create")}
</mwc-button> </ha-button>
`} `}
</ha-dialog> </ha-dialog>
`; `;
@ -299,7 +308,10 @@ export class DialogAddUser extends LitElement {
} }
ha-textfield { ha-textfield {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 8px;
}
ha-settings-row {
padding: 0;
} }
`, `,
]; ];

View File

@ -132,7 +132,7 @@ class DialogAdminChangePassword extends LitElement {
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
.disabled=${this._submitting} .disabled=${this._submitting}
></ha-form> ></ha-form>
<mwc-button slot="primaryAction" @click=${this.closeDialog}> <mwc-button slot="secondaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")} ${this.hass.localize("ui.common.cancel")}
</mwc-button> </mwc-button>
<mwc-button <mwc-button

View File

@ -1,21 +1,26 @@
import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; import { mdiPencil } from "@mdi/js";
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import { createCloseHeading } from "../../../components/ha-dialog"; import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-formfield"; import "../../../components/ha-formfield";
import "../../../components/ha-help-tooltip"; import "../../../components/ha-icon-button";
import "../../../components/ha-label"; import "../../../components/ha-label";
import "../../../components/ha-settings-row";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import "../../../components/ha-switch"; import "../../../components/ha-switch";
import "../../../components/ha-textfield"; import "../../../components/ha-textfield";
import { adminChangeUsername } from "../../../data/auth";
import { import {
computeUserBadges, computeUserBadges,
SYSTEM_GROUP_ID_ADMIN, SYSTEM_GROUP_ID_ADMIN,
SYSTEM_GROUP_ID_USER, SYSTEM_GROUP_ID_USER,
} from "../../../data/user"; } from "../../../data/user";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import {
showAlertDialog,
showPromptDialog,
} from "../../../dialogs/generic/show-dialog-box";
import { haStyleDialog } from "../../../resources/styles"; import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { showAdminChangePasswordDialog } from "./show-dialog-admin-change-password"; import { showAdminChangePasswordDialog } from "./show-dialog-admin-change-password";
@ -64,15 +69,15 @@ class DialogUserDetail extends LitElement {
.heading=${createCloseHeading(this.hass, user.name)} .heading=${createCloseHeading(this.hass, user.name)}
> >
<div> <div>
${this._error ? html` <div class="error">${this._error}</div> ` : ""} ${this._error
? html`<div class="error">${this._error}</div>`
: nothing}
<div class="secondary"> <div class="secondary">
${this.hass.localize("ui.panel.config.users.editor.id")}: ${this.hass.localize("ui.panel.config.users.editor.id")}:
${user.id}<br /> ${user.id}<br />
${this.hass.localize("ui.panel.config.users.editor.username")}:
${user.username}
</div> </div>
${badges.length === 0 ${badges.length === 0
? "" ? nothing
: html` : html`
<div class="badge-container"> <div class="badge-container">
${badges.map( ${badges.map(
@ -86,74 +91,136 @@ class DialogUserDetail extends LitElement {
</div> </div>
`} `}
<div class="form"> <div class="form">
${!user.system_generated
? html`
<ha-textfield <ha-textfield
dialogInitialFocus dialogInitialFocus
.value=${this._name} .value=${this._name}
.disabled=${user.system_generated}
@input=${this._nameChanged} @input=${this._nameChanged}
.label=${this.hass!.localize("ui.panel.config.users.editor.name")} .label=${this.hass!.localize(
"ui.panel.config.users.editor.name"
)}
></ha-textfield> ></ha-textfield>
<div class="row"> <ha-settings-row>
<ha-formfield <span slot="heading">
.label=${this.hass.localize(
"ui.panel.config.users.editor.local_only"
)}
>
<ha-switch
.disabled=${user.system_generated}
.checked=${this._localOnly}
@change=${this._localOnlyChanged}
>
</ha-switch>
</ha-formfield>
</div>
<div class="row">
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.users.editor.admin"
)}
>
<ha-switch
.disabled=${user.system_generated || user.is_owner}
.checked=${this._isAdmin}
@change=${this._adminChanged}
>
</ha-switch>
</ha-formfield>
</div>
${!this._isAdmin
? html`
<br />
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.users.users_privileges_note" "ui.panel.config.users.editor.username"
)} )}
` </span>
: ""} <span slot="description">${user.username}</span>
<div class="row"> ${this.hass.user?.is_owner
<ha-formfield ? html`
<ha-icon-button
.path=${mdiPencil}
@click=${this._changeUsername}
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.users.editor.active" "ui.panel.config.users.editor.change_username"
)} )}
> >
</ha-icon-button>
`
: nothing}
</ha-settings-row>
`
: nothing}
${!user.system_generated && this.hass.user?.is_owner
? html`
<ha-settings-row>
<span slot="heading">
${this.hass.localize(
"ui.panel.config.users.editor.password"
)}
</span>
<span slot="description">************</span>
${this.hass.user?.is_owner
? html`
<ha-icon-button
.path=${mdiPencil}
@click=${this._changePassword}
.label=${this.hass.localize(
"ui.panel.config.users.editor.change_password"
)}
>
</ha-icon-button>
`
: nothing}
</ha-settings-row>
`
: nothing}
<ha-settings-row>
<span slot="heading">
${this.hass.localize("ui.panel.config.users.editor.active")}
</span>
<span slot="description">
${this.hass.localize(
"ui.panel.config.users.editor.active_description"
)}
</span>
<ha-switch <ha-switch
.disabled=${user.system_generated || user.is_owner} .disabled=${user.system_generated || user.is_owner}
.checked=${this._isActive} .checked=${this._isActive}
@change=${this._activeChanged} @change=${this._activeChanged}
> >
</ha-switch> </ha-switch>
</ha-formfield> </ha-settings-row>
<ha-help-tooltip <ha-settings-row>
.label=${this.hass.localize( <span slot="heading">
"ui.panel.config.users.editor.active_tooltip" ${this.hass.localize(
"ui.panel.config.users.editor.local_access_only"
)} )}
</span>
<span slot="description">
${this.hass.localize(
"ui.panel.config.users.editor.local_access_only_description"
)}
</span>
<ha-switch
.disabled=${user.system_generated}
.checked=${this._localOnly}
@change=${this._localOnlyChanged}
> >
</ha-help-tooltip> </ha-switch>
</div> </ha-settings-row>
<ha-settings-row>
<span slot="heading">
${this.hass.localize("ui.panel.config.users.editor.admin")}
</span>
<span slot="description">
${this.hass.localize(
"ui.panel.config.users.editor.admin_description"
)}
</span>
<ha-switch
.disabled=${user.system_generated || user.is_owner}
.checked=${this._isAdmin}
@change=${this._adminChanged}
>
</ha-switch>
</ha-settings-row>
${!this._isAdmin && !user.system_generated
? html`
<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.users.users_privileges_note"
)}
</ha-alert>
`
: nothing}
</div> </div>
${user.system_generated
? html`
<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.users.editor.system_generated_read_only_users"
)}
</ha-alert>
`
: nothing}
</div> </div>
<div slot="secondaryAction"> <div slot="secondaryAction">
<mwc-button <ha-button
class="warning" class="warning"
@click=${this._deleteEntry} @click=${this._deleteEntry}
.disabled=${this._submitting || .disabled=${this._submitting ||
@ -161,43 +228,18 @@ class DialogUserDetail extends LitElement {
user.is_owner} user.is_owner}
> >
${this.hass!.localize("ui.panel.config.users.editor.delete_user")} ${this.hass!.localize("ui.panel.config.users.editor.delete_user")}
</mwc-button> </ha-button>
${user.system_generated
? html`
<simple-tooltip animation-delay="0" position="right">
${this.hass.localize(
"ui.panel.config.users.editor.system_generated_users_not_removable"
)}
</simple-tooltip>
`
: ""}
${!user.system_generated && this.hass.user?.is_owner
? html`<mwc-button @click=${this._changePassword}>
${this.hass.localize(
"ui.panel.config.users.editor.change_password"
)}
</mwc-button>`
: ""}
</div> </div>
<div slot="primaryAction"> <div slot="primaryAction">
<mwc-button <ha-button
@click=${this._updateEntry} @click=${this._updateEntry}
.disabled=${!this._name || .disabled=${!this._name ||
this._submitting || this._submitting ||
user.system_generated} user.system_generated}
> >
${this.hass!.localize("ui.panel.config.users.editor.update_user")} ${this.hass!.localize("ui.panel.config.users.editor.update_user")}
</mwc-button> </ha-button>
${user.system_generated
? html`
<simple-tooltip animation-delay="0" position="left">
${this.hass.localize(
"ui.panel.config.users.editor.system_generated_users_not_editable"
)}
</simple-tooltip>
`
: ""}
</div> </div>
</ha-dialog> </ha-dialog>
`; `;
@ -250,6 +292,56 @@ class DialogUserDetail extends LitElement {
} }
} }
private async _changeUsername() {
const credential = this._params?.entry.credentials.find(
(cred) => cred.type === "homeassistant"
);
if (!credential) {
showAlertDialog(this, {
title: "No Home Assistant credentials found.",
});
return;
}
const newUsername = await showPromptDialog(this, {
inputLabel: this.hass.localize(
"ui.panel.config.users.change_username.new_username"
),
confirmText: this.hass.localize(
"ui.panel.config.users.change_username.change"
),
title: this.hass.localize(
"ui.panel.config.users.change_username.caption"
),
defaultValue: this._params!.entry.username!,
});
if (newUsername) {
try {
await adminChangeUsername(
this.hass,
this._params!.entry.id,
newUsername
);
this._params = {
...this._params!,
entry: { ...this._params!.entry, username: newUsername },
};
this._params.replaceEntry(this._params.entry);
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.users.change_username.username_changed"
),
});
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.users.change_username.failed"
),
text: err.message,
});
}
}
}
private async _changePassword() { private async _changePassword() {
const credential = this._params?.entry.credentials.find( const credential = this._params?.entry.credentials.find(
(cred) => cred.type === "homeassistant" (cred) => cred.type === "homeassistant"
@ -295,27 +387,8 @@ class DialogUserDetail extends LitElement {
margin-inline-end: 4px; margin-inline-end: 4px;
margin-inline-start: 0; margin-inline-start: 0;
} }
.state { ha-settings-row {
background-color: rgba(var(--rgb-primary-text-color), 0.15); padding: 0;
border-radius: 16px;
padding: 4px 8px;
margin-top: 8px;
display: inline-block;
}
.state:not(:first-child) {
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
}
.row {
display: flex;
padding: 8px 0;
}
ha-help-tooltip {
margin-left: 4px;
margin-inline-start: 4px;
margin-inline-end: initial;
position: relative;
} }
`, `,
]; ];

View File

@ -46,6 +46,20 @@ export class HaConfigUsers extends LitElement {
@storage({ key: "users-table-grouping", state: false, subscribe: false }) @storage({ key: "users-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string; private _activeGrouping?: string;
@storage({
key: "users-table-column-order",
state: false,
subscribe: false,
})
private _activeColumnOrder?: string[];
@storage({
key: "users-table-hidden-columns",
state: false,
subscribe: false,
})
private _activeHiddenColumns?: string[];
@storage({ @storage({
storage: "sessionStorage", storage: "sessionStorage",
key: "users-table-search", key: "users-table-search",
@ -72,17 +86,6 @@ export class HaConfigUsers extends LitElement {
width: "25%", width: "25%",
direction: "asc", direction: "asc",
grows: true, grows: true,
template: (user) =>
narrow
? html` ${user.name}<br />
<div class="secondary">
${user.username ? `${user.username} |` : ""}
${localize(`groups.${user.group_ids[0]}`)}
</div>`
: html` ${user.name ||
this.hass!.localize(
"ui.panel.config.users.editor.unnamed_user"
)}`,
}, },
username: { username: {
title: localize("ui.panel.config.users.picker.headers.username"), title: localize("ui.panel.config.users.picker.headers.username"),
@ -90,7 +93,6 @@ export class HaConfigUsers extends LitElement {
filterable: true, filterable: true,
width: "20%", width: "20%",
direction: "asc", direction: "asc",
hidden: narrow,
template: (user) => html`${user.username || "—"}`, template: (user) => html`${user.username || "—"}`,
}, },
group: { group: {
@ -100,7 +102,6 @@ export class HaConfigUsers extends LitElement {
groupable: true, groupable: true,
width: "20%", width: "20%",
direction: "asc", direction: "asc",
hidden: narrow,
}, },
is_active: { is_active: {
title: this.hass.localize( title: this.hass.localize(
@ -154,6 +155,7 @@ export class HaConfigUsers extends LitElement {
filterable: false, filterable: false,
width: "104px", width: "104px",
hidden: !narrow, hidden: !narrow,
showNarrow: true,
template: (user) => { template: (user) => {
const badges = computeUserBadges(this.hass, user, false); const badges = computeUserBadges(this.hass, user, false);
return html`${badges.map( return html`${badges.map(
@ -182,10 +184,13 @@ export class HaConfigUsers extends LitElement {
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.route=${this.route} .route=${this.route}
backPath="/config" back-path="/config"
.tabs=${configSections.persons} .tabs=${configSections.persons}
.columns=${this._columns(this.narrow, this.hass.localize)} .columns=${this._columns(this.narrow, this.hass.localize)}
.data=${this._userData(this._users, this.hass.localize)} .data=${this._userData(this._users, this.hass.localize)}
.columnOrder=${this._activeColumnOrder}
.hiddenColumns=${this._activeHiddenColumns}
@columns-changed=${this._handleColumnsChanged}
.initialGroupColumn=${this._activeGrouping} .initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed} .initialCollapsedGroups=${this._activeCollapsed}
.initialSorting=${this._activeSorting} .initialSorting=${this._activeSorting}
@ -213,6 +218,7 @@ export class HaConfigUsers extends LitElement {
private _userData = memoizeOne((users: User[], localize: LocalizeFunc) => private _userData = memoizeOne((users: User[], localize: LocalizeFunc) =>
users.map((user) => ({ users.map((user) => ({
...user, ...user,
name: user.name || localize("ui.panel.config.users.editor.unnamed_user"),
group: localize(`groups.${user.group_ids[0]}`), group: localize(`groups.${user.group_ids[0]}`),
})) }))
); );
@ -237,6 +243,11 @@ export class HaConfigUsers extends LitElement {
showUserDetailDialog(this, { showUserDetailDialog(this, {
entry, entry,
replaceEntry: (newEntry: User) => {
this._users = this._users!.map((ent) =>
ent.id === newEntry.id ? newEntry : ent
);
},
updateEntry: async (values) => { updateEntry: async (values) => {
const updated = await updateUser(this.hass!, entry!.id, values); const updated = await updateUser(this.hass!, entry!.id, values);
this._users = this._users!.map((ent) => this._users = this._users!.map((ent) =>
@ -297,6 +308,11 @@ export class HaConfigUsers extends LitElement {
private _handleSearchChange(ev: CustomEvent) { private _handleSearchChange(ev: CustomEvent) {
this._filter = ev.detail.value; this._filter = ev.detail.value;
} }
private _handleColumnsChanged(ev: CustomEvent) {
this._activeColumnOrder = ev.detail.columnOrder;
this._activeHiddenColumns = ev.detail.hiddenColumns;
}
} }
declare global { declare global {

View File

@ -4,6 +4,7 @@ import { UpdateUserParams, User } from "../../../data/user";
export interface UserDetailDialogParams { export interface UserDetailDialogParams {
entry: User; entry: User;
updateEntry: (updates: Partial<UpdateUserParams>) => Promise<unknown>; updateEntry: (updates: Partial<UpdateUserParams>) => Promise<unknown>;
replaceEntry: (entry: User) => void;
removeEntry: () => Promise<boolean>; removeEntry: () => Promise<boolean>;
} }

View File

@ -118,6 +118,20 @@ export class VoiceAssistantsExpose extends LitElement {
}) })
private _activeCollapsed?: string; private _activeCollapsed?: string;
@storage({
key: "voice-expose-table-column-order",
state: false,
subscribe: false,
})
private _activeColumnOrder?: string[];
@storage({
key: "voice-expose-table-hidden-columns",
state: false,
subscribe: false,
})
private _activeHiddenColumns?: string[];
@query("hass-tabs-subpage-data-table", true) @query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable; private _dataTable!: HaTabsSubpageDataTable;
@ -137,6 +151,7 @@ export class VoiceAssistantsExpose extends LitElement {
icon: { icon: {
title: "", title: "",
type: "icon", type: "icon",
moveable: false,
hidden: narrow, hidden: narrow,
template: (entry) => html` template: (entry) => html`
<ha-state-icon <ha-state-icon
@ -153,11 +168,21 @@ export class VoiceAssistantsExpose extends LitElement {
filterable: true, filterable: true,
direction: "asc", direction: "asc",
grows: true, grows: true,
template: (entry) => html` template: narrow
? undefined
: (entry) => html`
${entry.name}<br /> ${entry.name}<br />
<div class="secondary">${entry.entity_id}</div> <div class="secondary">${entry.entity_id}</div>
`, `,
}, },
// For search & narrow
entity_id: {
title: localize(
"ui.panel.config.voice_assistants.expose.headers.entity_id"
),
hidden: !narrow,
filterable: true,
},
domain: { domain: {
title: localize( title: localize(
"ui.panel.config.voice_assistants.expose.headers.domain" "ui.panel.config.voice_assistants.expose.headers.domain"
@ -171,7 +196,6 @@ export class VoiceAssistantsExpose extends LitElement {
title: localize("ui.panel.config.voice_assistants.expose.headers.area"), title: localize("ui.panel.config.voice_assistants.expose.headers.area"),
sortable: true, sortable: true,
groupable: true, groupable: true,
hidden: narrow,
filterable: true, filterable: true,
width: "15%", width: "15%",
}, },
@ -179,6 +203,7 @@ export class VoiceAssistantsExpose extends LitElement {
title: localize( title: localize(
"ui.panel.config.voice_assistants.expose.headers.assistants" "ui.panel.config.voice_assistants.expose.headers.assistants"
), ),
showNarrow: true,
sortable: true, sortable: true,
filterable: true, filterable: true,
width: "160px", width: "160px",
@ -208,7 +233,6 @@ export class VoiceAssistantsExpose extends LitElement {
), ),
sortable: true, sortable: true,
filterable: true, filterable: true,
hidden: narrow,
width: "15%", width: "15%",
template: (entry) => template: (entry) =>
entry.aliases.length === 0 entry.aliases.length === 0
@ -230,12 +254,6 @@ export class VoiceAssistantsExpose extends LitElement {
.path=${mdiCloseCircleOutline} .path=${mdiCloseCircleOutline}
></ha-icon-button>`, ></ha-icon-button>`,
}, },
// For search
entity_id: {
title: "",
hidden: true,
filterable: true,
},
}) })
); );
@ -552,6 +570,9 @@ export class VoiceAssistantsExpose extends LitElement {
.initialSorting=${this._activeSorting} .initialSorting=${this._activeSorting}
.initialGroupColumn=${this._activeGrouping} .initialGroupColumn=${this._activeGrouping}
.initialCollapsedGroups=${this._activeCollapsed} .initialCollapsedGroups=${this._activeCollapsed}
.columnOrder=${this._activeColumnOrder}
.hiddenColumns=${this._activeHiddenColumns}
@columns-changed=${this._handleColumnsChanged}
@sorting-changed=${this._handleSortingChanged} @sorting-changed=${this._handleSortingChanged}
@selection-changed=${this._handleSelectionChanged} @selection-changed=${this._handleSelectionChanged}
@grouping-changed=${this._handleGroupingChanged} @grouping-changed=${this._handleGroupingChanged}
@ -757,6 +778,11 @@ export class VoiceAssistantsExpose extends LitElement {
this._activeCollapsed = ev.detail.value; this._activeCollapsed = ev.detail.value;
} }
private _handleColumnsChanged(ev: CustomEvent) {
this._activeColumnOrder = ev.detail.columnOrder;
this._activeHiddenColumns = ev.detail.hiddenColumns;
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
haStyle, haStyle,

View File

@ -14,7 +14,7 @@ const SCHEMA = [
{ {
name: "location", name: "location",
required: true, required: true,
selector: { location: { radius: true, radius_readonly: true } }, selector: { location: { radius: true } },
}, },
]; ];
@ -35,6 +35,7 @@ class DialogHomeZoneDetail extends LitElement {
this._data = { this._data = {
latitude: this.hass.config.latitude, latitude: this.hass.config.latitude,
longitude: this.hass.config.longitude, longitude: this.hass.config.longitude,
radius: this.hass.config.radius,
}; };
} }
@ -73,11 +74,6 @@ class DialogHomeZoneDetail extends LitElement {
.computeLabel=${this._computeLabel} .computeLabel=${this._computeLabel}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-form> ></ha-form>
<p>
${this.hass!.localize(
"ui.panel.config.zone.detail.no_edit_home_zone_radius"
)}
</p>
</div> </div>
<mwc-button <mwc-button
slot="primaryAction" slot="primaryAction"
@ -95,7 +91,7 @@ class DialogHomeZoneDetail extends LitElement {
location: { location: {
latitude: data.latitude, latitude: data.latitude,
longitude: data.longitude, longitude: data.longitude,
radius: this.hass.states["zone.home"]?.attributes?.radius || 100, radius: data.radius || 100,
}, },
})); }));
@ -104,6 +100,7 @@ class DialogHomeZoneDetail extends LitElement {
const value = { ...ev.detail.value }; const value = { ...ev.detail.value };
value.latitude = value.location.latitude; value.latitude = value.location.latitude;
value.longitude = value.location.longitude; value.longitude = value.location.longitude;
value.radius = value.location.radius;
delete value.location; delete value.location;
this._data = value; this._data = value;
} }

View File

@ -101,7 +101,8 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
: zoneRadiusColor, : zoneRadiusColor,
location_editable: location_editable:
entityState.entity_id === "zone.home" && this._canEditCore, entityState.entity_id === "zone.home" && this._canEditCore,
radius_editable: false, radius_editable:
entityState.entity_id === "zone.home" && this._canEditCore,
}) })
); );
const storageLocations: MarkerLocation[] = storageItems.map((zone) => ({ const storageLocations: MarkerLocation[] = storageItems.map((zone) => ({
@ -381,8 +382,14 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
}); });
} }
private _radiusUpdated(ev: CustomEvent) { private async _radiusUpdated(ev: CustomEvent) {
this._activeEntry = ev.detail.id; this._activeEntry = ev.detail.id;
if (ev.detail.id === "zone.home" && this._canEditCore) {
await saveCoreConfig(this.hass, {
radius: Math.round(ev.detail.radius),
});
return;
}
const entry = this._storageItems!.find((item) => item.id === ev.detail.id); const entry = this._storageItems!.find((item) => item.id === ev.detail.id);
if (!entry) { if (!entry) {
return; return;
@ -478,6 +485,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
await saveCoreConfig(this.hass, { await saveCoreConfig(this.hass, {
latitude: values.latitude, latitude: values.latitude,
longitude: values.longitude, longitude: values.longitude,
radius: values.radius,
}); });
this._zoomZone("zone.home"); this._zoomZone("zone.home");
} }

View File

@ -282,6 +282,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
{ statistic_id: issue.data.statistic_id } { statistic_id: issue.data.statistic_id }
)}`, )}`,
confirmText: this.hass.localize("ui.common.delete"), confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: async () => { confirm: async () => {
await clearStatistics(this.hass, [issue.data.statistic_id]); await clearStatistics(this.hass, [issue.data.statistic_id]);
this._deletedStatistics.add(issue.data.statistic_id); this._deletedStatistics.add(issue.data.statistic_id);
@ -314,7 +315,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
}); });
break; break;
case "entity_no_longer_recorded": case "entity_no_longer_recorded":
showAlertDialog(this, { showConfirmationDialog(this, {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.title" "ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.title"
), ),
@ -335,7 +336,17 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
${this.hass.localize( ${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_3_link" "ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_3_link"
)}</a )}</a
>`, ><br /><br />
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_4"
)}`,
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: async () => {
await clearStatistics(this.hass, [issue.data.statistic_id]);
this._deletedStatistics.add(issue.data.statistic_id);
this._validateStatistics();
},
}); });
break; break;
case "unsupported_state_class": case "unsupported_state_class":
@ -381,6 +392,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
{ statistic_id: issue.data.statistic_id } { statistic_id: issue.data.statistic_id }
)}`, )}`,
confirmText: this.hass.localize("ui.common.delete"), confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: async () => { confirm: async () => {
await clearStatistics(this.hass, [issue.data.statistic_id]); await clearStatistics(this.hass, [issue.data.statistic_id]);
this._deletedStatistics.add(issue.data.statistic_id); this._deletedStatistics.add(issue.data.statistic_id);

View File

@ -24,7 +24,7 @@ import { calculateStatisticsSumGrowth } from "../../../../data/recorder";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import type { LovelaceCard } from "../../types"; import type { LovelaceCard } from "../../types";
import type { EnergyGridGaugeCardConfig } from "../types"; import type { EnergyGridNeutralityGaugeCardConfig } from "../types";
import { hasConfigChanged } from "../../common/has-changed"; import { hasConfigChanged } from "../../common/has-changed";
const LEVELS: LevelDefinition[] = [ const LEVELS: LevelDefinition[] = [
@ -39,7 +39,7 @@ class HuiEnergyGridGaugeCard
{ {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: EnergyGridGaugeCardConfig; @state() private _config?: EnergyGridNeutralityGaugeCardConfig;
@state() private _data?: EnergyData; @state() private _data?: EnergyData;
@ -59,7 +59,7 @@ class HuiEnergyGridGaugeCard
return 4; return 4;
} }
public setConfig(config: EnergyGridGaugeCardConfig): void { public setConfig(config: EnergyGridNeutralityGaugeCardConfig): void {
this._config = config; this._config = config;
} }

View File

@ -1,5 +1,6 @@
import { PropertyValues, ReactiveElement } from "lit"; import { PropertyValueMap, PropertyValues, ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { MediaQueriesListener } from "../../../common/dom/media_query"; import { MediaQueriesListener } from "../../../common/dom/media_query";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
@ -10,23 +11,41 @@ import {
checkConditionsMet, checkConditionsMet,
} from "../common/validate-condition"; } from "../common/validate-condition";
import { createCardElement } from "../create-element/create-card-element"; import { createCardElement } from "../create-element/create-card-element";
import type { Lovelace, LovelaceCard, LovelaceLayoutOptions } from "../types"; import { createErrorCardConfig } from "../create-element/create-element-base";
import type { LovelaceCard, LovelaceLayoutOptions } from "../types";
declare global { declare global {
interface HASSDomEvents { interface HASSDomEvents {
"card-visibility-changed": { value: boolean }; "card-visibility-changed": { value: boolean };
"card-updated": undefined;
} }
} }
@customElement("hui-card") @customElement("hui-card")
export class HuiCard extends ReactiveElement { export class HuiCard extends ReactiveElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public lovelace?: Lovelace; @property({ type: Boolean }) public preview = false;
@property({ attribute: false }) public isPanel = false; @property({ type: Boolean }) public isPanel = false;
@state() public _config?: LovelaceCardConfig; set config(config: LovelaceCardConfig | undefined) {
if (!config) return;
if (config.type !== this._config?.type) {
this._buildElement(config);
} else if (config !== this.config) {
this._element?.setConfig(config);
fireEvent(this, "card-updated");
}
this._config = config;
}
@property({ attribute: false })
public get config() {
return this._config;
}
private _config?: LovelaceCardConfig;
private _element?: LovelaceCard; private _element?: LovelaceCard;
@ -44,7 +63,7 @@ export class HuiCard extends ReactiveElement {
public connectedCallback() { public connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this._listenMediaQueries(); this._listenMediaQueries();
this._updateElement(); this._updateVisibility();
} }
public getCardSize(): number | Promise<number> { public getCardSize(): number | Promise<number> {
@ -56,7 +75,7 @@ export class HuiCard extends ReactiveElement {
} }
public getLayoutOptions(): LovelaceLayoutOptions { public getLayoutOptions(): LovelaceLayoutOptions {
const configOptions = this._config?.layout_options ?? {}; const configOptions = this.config?.layout_options ?? {};
if (this._element) { if (this._element) {
const cardOptions = this._element.getLayoutOptions?.() ?? {}; const cardOptions = this._element.getLayoutOptions?.() ?? {};
return { return {
@ -67,51 +86,84 @@ export class HuiCard extends ReactiveElement {
return configOptions; return configOptions;
} }
// Public to make demo happy public getElementLayoutOptions(): LovelaceLayoutOptions {
public createElement(config: LovelaceCardConfig) { return this._element?.getLayoutOptions?.() ?? {};
const element = createCardElement(config) as LovelaceCard; }
private _createElement(config: LovelaceCardConfig) {
const element = createCardElement(config);
element.hass = this.hass; element.hass = this.hass;
element.editMode = this.lovelace?.editMode; element.preview = this.preview;
// For backwards compatibility
(element as any).editMode = this.preview;
// Update element when the visibility of the card changes (e.g. conditional card or filter card) // Update element when the visibility of the card changes (e.g. conditional card or filter card)
element.addEventListener("card-visibility-changed", (ev) => { element.addEventListener("card-visibility-changed", (ev: Event) => {
ev.stopPropagation(); ev.stopPropagation();
this._updateElement(); this._updateVisibility();
}); });
element.addEventListener(
"ll-upgrade",
(ev: Event) => {
ev.stopPropagation();
fireEvent(this, "card-updated");
},
{ once: true }
);
element.addEventListener(
"ll-rebuild",
(ev: Event) => {
ev.stopPropagation();
this._buildElement(config);
fireEvent(this, "card-updated");
},
{ once: true }
);
return element; return element;
} }
public setConfig(config: LovelaceCardConfig): void { private _buildElement(config: LovelaceCardConfig) {
if (this._config === config) { this._element = this._createElement(config);
return;
}
this._config = config;
this._element = this.createElement(config);
while (this.lastChild) { while (this.lastChild) {
this.removeChild(this.lastChild); this.removeChild(this.lastChild);
} }
this.appendChild(this._element!); this._updateVisibility();
} }
protected update(changedProperties: PropertyValues<typeof this>) { protected update(changedProps: PropertyValues<typeof this>) {
super.update(changedProperties); super.update(changedProps);
if (this._element) { if (this._element) {
if (changedProperties.has("hass")) { if (changedProps.has("hass")) {
try {
this._element.hass = this.hass; this._element.hass = this.hass;
} catch (e: any) {
this._buildElement(createErrorCardConfig(e.message, null));
} }
if (changedProperties.has("lovelace")) {
this._element.editMode = this.lovelace?.editMode;
} }
if (changedProperties.has("hass") || changedProperties.has("lovelace")) { if (changedProps.has("preview")) {
this._updateElement(); try {
this._element.preview = this.preview;
// For backwards compatibility
(this._element as any).editMode = this.preview;
} catch (e: any) {
this._buildElement(createErrorCardConfig(e.message, null));
} }
if (changedProperties.has("isPanel")) { }
if (changedProps.has("isPanel")) {
this._element.isPanel = this.isPanel; this._element.isPanel = this.isPanel;
} }
} }
} }
protected willUpdate(
changedProps: PropertyValueMap<any> | Map<PropertyKey, unknown>
): void {
if (changedProps.has("hass") || changedProps.has("preview")) {
this._updateVisibility();
}
}
private _clearMediaQueries() { private _clearMediaQueries() {
this._listeners.forEach((unsub) => unsub()); this._listeners.forEach((unsub) => unsub());
this._listeners = []; this._listeners = [];
@ -119,42 +171,50 @@ export class HuiCard extends ReactiveElement {
private _listenMediaQueries() { private _listenMediaQueries() {
this._clearMediaQueries(); this._clearMediaQueries();
if (!this._config?.visibility) { if (!this.config?.visibility) {
return; return;
} }
const conditions = this._config.visibility; const conditions = this.config.visibility;
const hasOnlyMediaQuery = const hasOnlyMediaQuery =
conditions.length === 1 && conditions.length === 1 &&
conditions[0].condition === "screen" && conditions[0].condition === "screen" &&
!!conditions[0].media_query; !!conditions[0].media_query;
this._listeners = attachConditionMediaQueriesListeners( this._listeners = attachConditionMediaQueriesListeners(
this._config.visibility, this.config.visibility,
(matches) => { (matches) => {
this._updateElement(hasOnlyMediaQuery && matches); this._updateVisibility(hasOnlyMediaQuery && matches);
} }
); );
} }
private _updateElement(forceVisible?: boolean) { private _updateVisibility(forceVisible?: boolean) {
if (!this._element) { if (!this._element || !this.hass) {
return; return;
} }
if (this._element.hidden) { if (this._element.hidden) {
this.style.setProperty("display", "none"); this._setElementVisibility(false);
this.toggleAttribute("hidden", true);
return; return;
} }
const visible = const visible =
forceVisible || forceVisible ||
this.lovelace?.editMode || this.preview ||
!this._config?.visibility || !this.config?.visibility ||
checkConditionsMet(this._config.visibility, this.hass); checkConditionsMet(this.config.visibility, this.hass);
this._setElementVisibility(visible);
}
private _setElementVisibility(visible: boolean) {
if (!this._element) return;
if (this.hidden !== !visible) {
this.style.setProperty("display", visible ? "" : "none"); this.style.setProperty("display", visible ? "" : "none");
this.toggleAttribute("hidden", !visible); this.toggleAttribute("hidden", !visible);
fireEvent(this, "card-visibility-changed", { value: visible });
}
if (!visible && this._element.parentElement) { if (!visible && this._element.parentElement) {
this.removeChild(this._element); this.removeChild(this._element);
} else if (visible && !this._element.parentElement) { } else if (visible && !this._element.parentElement) {

View File

@ -3,7 +3,6 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { LovelaceCardConfig } from "../../../data/lovelace/config/card"; import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import { computeCardSize } from "../common/compute-card-size"; import { computeCardSize } from "../common/compute-card-size";
import { HuiConditionalBase } from "../components/hui-conditional-base"; import { HuiConditionalBase } from "../components/hui-conditional-base";
import { createCardElement } from "../create-element/create-card-element";
import { LovelaceCard, LovelaceCardEditor } from "../types"; import { LovelaceCard, LovelaceCardEditor } from "../types";
import { ConditionalCardConfig } from "./types"; import { ConditionalCardConfig } from "./types";
@ -38,30 +37,15 @@ class HuiConditionalCard extends HuiConditionalBase implements LovelaceCard {
} }
private _createCardElement(cardConfig: LovelaceCardConfig) { private _createCardElement(cardConfig: LovelaceCardConfig) {
const element = createCardElement(cardConfig) as LovelaceCard; const element = document.createElement("hui-card");
if (this.hass) {
element.hass = this.hass; element.hass = this.hass;
} element.preview = this.preview;
element.addEventListener( element.config = cardConfig;
"ll-rebuild",
(ev) => {
ev.stopPropagation();
this._rebuildCard(cardConfig);
},
{ once: true }
);
return element; return element;
} }
private _rebuildCard(config: LovelaceCardConfig): void {
this._element = this._createCardElement(config);
if (this.lastChild) {
this.replaceChild(this._element, this.lastChild);
}
}
protected setVisibility(conditionMet: boolean): void { protected setVisibility(conditionMet: boolean): void {
const visible = this.editMode || conditionMet; const visible = this.preview || conditionMet;
const previouslyHidden = this.hidden; const previouslyHidden = this.hidden;
super.setVisibility(conditionMet); super.setVisibility(conditionMet);
if (previouslyHidden !== this.hidden) { if (previouslyHidden !== this.hidden) {

Some files were not shown because too many files have changed in this diff Show More