mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
20240626.0 (#21171)
This commit is contained in:
commit
8d0c4e4a52
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
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
|
||||||
|
|
||||||
|
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@ -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:
|
||||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -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.
|
||||||
|
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@ -22,7 +22,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
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
|
||||||
|
|
||||||
|
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
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
|
||||||
|
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
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
|
||||||
|
2
.github/workflows/nightly.yaml
vendored
2
.github/workflows/nightly.yaml
vendored
@ -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
|
||||||
|
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@ -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
|
||||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@ -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
@ -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
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) => {
|
||||||
return el;
|
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 :-(");
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -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);
|
ev.stopPropagation();
|
||||||
|
this._updateSize();
|
||||||
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) {
|
private async _updateSize() {
|
||||||
await customElements.whenDefined(el.localName);
|
this._size = await this._card?.getCardSize();
|
||||||
|
|
||||||
if (!("getCardSize" in el)) {
|
|
||||||
this._size = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._size = await el.getCardSize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_createCardElement(cardConfig) {
|
protected update(
|
||||||
const element = createCardElement(cardConfig);
|
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
||||||
if (this.hass) {
|
): void {
|
||||||
element.hass = this.hass;
|
super.update(_changedProperties);
|
||||||
}
|
this._updateSize();
|
||||||
element.addEventListener(
|
|
||||||
"ll-rebuild",
|
|
||||||
(ev) => {
|
|
||||||
ev.stopPropagation();
|
|
||||||
this._rebuildCard(element, cardConfig);
|
|
||||||
},
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
_rebuildCard(cardElToReplace, config) {
|
|
||||||
const newCardEl = this._createCardElement(config);
|
|
||||||
cardElToReplace.parentElement.replaceChild(newCardEl, cardElToReplace);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
38
package.json
38
package.json
@ -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 |
3
public/static/images/logo_x.svg
Normal file
3
public/static/images/logo_x.svg
Normal 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 |
@ -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"
|
||||||
|
@ -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
|
||||||
|
1
src/common/dom/prevent_default.ts
Normal file
1
src/common/dom/prevent_default.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const preventDefault = (ev) => ev.preventDefault();
|
@ -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
|
||||||
);
|
);
|
||||||
|
2
src/common/string/strip-diacritics.ts
Normal file
2
src/common/string/strip-diacritics.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const stripDiacritics = (str) =>
|
||||||
|
str.normalize("NFD").replace(/[\u0300-\u036F]/g, "");
|
280
src/components/data-table/dialog-data-table-settings.ts
Normal file
280
src/components/data-table/dialog-data-table-settings.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
26
src/components/data-table/show-dialog-data-table-settings.ts
Normal file
26
src/components/data-table/show-dialog-data-table-settings.ts
Normal 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,
|
||||||
|
});
|
||||||
|
};
|
@ -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()
|
if (stripDiacritics(value).toLowerCase().includes(filter)) {
|
||||||
.includes(filter)
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 = {
|
||||||
|
@ -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),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
233
src/components/ha-grid-size-picker.ts
Normal file
233
src/components/ha-grid-size-picker.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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>`
|
||||||
|
@ -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,75 +484,115 @@ 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) =>
|
||||||
const selector = dataField?.selector ?? { text: undefined };
|
dataField.fields
|
||||||
const type = Object.keys(selector)[0];
|
? html`<ha-expansion-panel
|
||||||
const enhancedSelector = ["action", "condition", "trigger"].includes(
|
leftChevron
|
||||||
type
|
.expanded=${!dataField.collapsed}
|
||||||
)
|
.header=${this.hass.localize(
|
||||||
? {
|
`component.${domain}.services.${serviceName}.sections.${dataField.key}.name`
|
||||||
[type]: {
|
) ||
|
||||||
...selector[type],
|
dataField.name ||
|
||||||
path: [dataField.key],
|
dataField.key}
|
||||||
},
|
>
|
||||||
}
|
${Object.entries(dataField.fields).map(([key, field]) =>
|
||||||
: selector;
|
this._renderField(
|
||||||
|
{ key, ...field },
|
||||||
const showOptional = showOptionalToggle(dataField);
|
hasOptional,
|
||||||
|
domain,
|
||||||
return dataField.selector &&
|
serviceName,
|
||||||
(!dataField.advanced ||
|
targetEntities
|
||||||
this.showAdvanced ||
|
)
|
||||||
(this._value?.data &&
|
)}
|
||||||
this._value.data[dataField.key] !== undefined))
|
</ha-expansion-panel>`
|
||||||
? html`<ha-settings-row .narrow=${this.narrow}>
|
: this._renderField(
|
||||||
${!showOptional
|
dataField,
|
||||||
? hasOptional
|
hasOptional,
|
||||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
domain,
|
||||||
: ""
|
serviceName,
|
||||||
: html`<ha-checkbox
|
targetEntities
|
||||||
.key=${dataField.key}
|
)
|
||||||
.checked=${this._checkedKeys.has(dataField.key) ||
|
)} `;
|
||||||
(this._value?.data &&
|
|
||||||
this._value.data[dataField.key] !== undefined)}
|
|
||||||
.disabled=${this.disabled}
|
|
||||||
@change=${this._checkboxChanged}
|
|
||||||
slot="prefix"
|
|
||||||
></ha-checkbox>`}
|
|
||||||
<span slot="heading"
|
|
||||||
>${this.hass.localize(
|
|
||||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.name`
|
|
||||||
) ||
|
|
||||||
dataField.name ||
|
|
||||||
dataField.key}</span
|
|
||||||
>
|
|
||||||
<span slot="description"
|
|
||||||
>${this.hass.localize(
|
|
||||||
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
|
|
||||||
) || dataField?.description}</span
|
|
||||||
>
|
|
||||||
<ha-selector
|
|
||||||
.disabled=${this.disabled ||
|
|
||||||
(showOptional &&
|
|
||||||
!this._checkedKeys.has(dataField.key) &&
|
|
||||||
(!this._value?.data ||
|
|
||||||
this._value.data[dataField.key] === undefined))}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.selector=${enhancedSelector}
|
|
||||||
.key=${dataField.key}
|
|
||||||
@value-changed=${this._serviceDataChanged}
|
|
||||||
.value=${this._value?.data
|
|
||||||
? this._value.data[dataField.key]
|
|
||||||
: undefined}
|
|
||||||
.placeholder=${dataField.default}
|
|
||||||
.localizeValue=${this._localizeValueCallback}
|
|
||||||
@item-moved=${this._itemMoved}
|
|
||||||
></ha-selector>
|
|
||||||
</ha-settings-row>`
|
|
||||||
: "";
|
|
||||||
})} `;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 type = Object.keys(selector)[0];
|
||||||
|
const enhancedSelector = ["action", "condition", "trigger"].includes(type)
|
||||||
|
? {
|
||||||
|
[type]: {
|
||||||
|
...selector[type],
|
||||||
|
path: [dataField.key],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: selector;
|
||||||
|
|
||||||
|
const showOptional = showOptionalToggle(dataField);
|
||||||
|
|
||||||
|
return dataField.selector &&
|
||||||
|
(!dataField.advanced ||
|
||||||
|
this.showAdvanced ||
|
||||||
|
(this._value?.data && this._value.data[dataField.key] !== undefined))
|
||||||
|
? html`<ha-settings-row .narrow=${this.narrow}>
|
||||||
|
${!showOptional
|
||||||
|
? hasOptional
|
||||||
|
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||||
|
: ""
|
||||||
|
: html`<ha-checkbox
|
||||||
|
.key=${dataField.key}
|
||||||
|
.checked=${this._checkedKeys.has(dataField.key) ||
|
||||||
|
(this._value?.data &&
|
||||||
|
this._value.data[dataField.key] !== undefined)}
|
||||||
|
.disabled=${this.disabled}
|
||||||
|
@change=${this._checkboxChanged}
|
||||||
|
slot="prefix"
|
||||||
|
></ha-checkbox>`}
|
||||||
|
<span slot="heading"
|
||||||
|
>${this.hass.localize(
|
||||||
|
`component.${domain}.services.${serviceName}.fields.${dataField.key}.name`
|
||||||
|
) ||
|
||||||
|
dataField.name ||
|
||||||
|
dataField.key}</span
|
||||||
|
>
|
||||||
|
<span slot="description"
|
||||||
|
>${this.hass.localize(
|
||||||
|
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
|
||||||
|
) || dataField?.description}</span
|
||||||
|
>
|
||||||
|
<ha-selector
|
||||||
|
.disabled=${this.disabled ||
|
||||||
|
(showOptional &&
|
||||||
|
!this._checkedKeys.has(dataField.key) &&
|
||||||
|
(!this._value?.data ||
|
||||||
|
this._value.data[dataField.key] === undefined))}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.selector=${enhancedSelector}
|
||||||
|
.key=${dataField.key}
|
||||||
|
@value-changed=${this._serviceDataChanged}
|
||||||
|
.value=${this._value?.data
|
||||||
|
? this._value.data[dataField.key]
|
||||||
|
: undefined}
|
||||||
|
.placeholder=${dataField.default}
|
||||||
|
.localizeValue=${this._localizeValueCallback}
|
||||||
|
@item-moved=${this._itemMoved}
|
||||||
|
></ha-selector>
|
||||||
|
</ha-settings-row>`
|
||||||
|
: "";
|
||||||
|
};
|
||||||
|
|
||||||
private _localizeValueCallback = (key: string) => {
|
private _localizeValueCallback = (key: string) => {
|
||||||
if (!this._value?.service) {
|
if (!this._value?.service) {
|
||||||
return "";
|
return "";
|
||||||
@ -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;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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++;
|
||||||
|
@ -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,
|
||||||
|
@ -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");
|
||||||
|
@ -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,
|
||||||
|
});
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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[];
|
||||||
|
@ -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
21
src/data/threshold.ts
Normal 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,
|
||||||
|
});
|
@ -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 "%";
|
||||||
|
@ -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 {
|
||||||
|
108
src/dialogs/config-flow/previews/flow-preview-threshold.ts
Normal file
108
src/dialogs/config-flow/previews/flow-preview-threshold.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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(
|
||||||
|
@ -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;
|
||||||
|
@ -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(
|
||||||
|
@ -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",
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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, {
|
||||||
|
@ -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();
|
? html`<ha-data-table-labels
|
||||||
const dayDifference = differenceInDays(now, date);
|
@label-clicked=${this._labelClicked}
|
||||||
return html`
|
.labels=${automation.labels}
|
||||||
<div style="font-size: 14px;">${automation.name}</div>
|
></ha-data-table-labels>`
|
||||||
${narrow
|
: nothing,
|
||||||
? 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
|
|
||||||
@label-clicked=${this._labelClicked}
|
|
||||||
.labels=${automation.labels}
|
|
||||||
></ha-data-table-labels>`
|
|
||||||
: 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,
|
||||||
|
@ -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;
|
||||||
|
@ -60,14 +60,19 @@ class HaConfigBackup extends LitElement {
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
grows: true,
|
grows: true,
|
||||||
template: (backup) =>
|
template: narrow
|
||||||
html`${backup.name}
|
? undefined
|
||||||
<div class="secondary">${backup.path}</div>`,
|
: (backup) =>
|
||||||
|
html`${backup.name}
|
||||||
|
<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}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -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) {
|
||||||
|
@ -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,112 +485,89 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
`,
|
`,
|
||||||
};
|
|
||||||
} else {
|
|
||||||
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"),
|
|
||||||
sortable: true,
|
|
||||||
hidden: narrow,
|
|
||||||
filterable: true,
|
|
||||||
groupable: true,
|
|
||||||
width: "15%",
|
|
||||||
};
|
|
||||||
columns.model = {
|
|
||||||
title: localize("ui.panel.config.devices.data_table.model"),
|
|
||||||
sortable: true,
|
|
||||||
hidden: narrow,
|
|
||||||
filterable: true,
|
|
||||||
width: "15%",
|
|
||||||
};
|
|
||||||
columns.area = {
|
|
||||||
title: localize("ui.panel.config.devices.data_table.area"),
|
|
||||||
sortable: true,
|
|
||||||
hidden: narrow,
|
|
||||||
filterable: true,
|
|
||||||
groupable: true,
|
|
||||||
width: "15%",
|
|
||||||
};
|
|
||||||
columns.integration = {
|
|
||||||
title: localize("ui.panel.config.devices.data_table.integration"),
|
|
||||||
sortable: true,
|
|
||||||
hidden: narrow,
|
|
||||||
filterable: true,
|
|
||||||
groupable: true,
|
|
||||||
width: "15%",
|
|
||||||
};
|
|
||||||
columns.battery_entity = {
|
|
||||||
title: localize("ui.panel.config.devices.data_table.battery"),
|
|
||||||
sortable: true,
|
|
||||||
filterable: true,
|
|
||||||
type: "numeric",
|
|
||||||
width: narrow ? "105px" : "15%",
|
|
||||||
maxWidth: "105px",
|
|
||||||
valueColumn: "battery_level",
|
|
||||||
template: (device) => {
|
|
||||||
const batteryEntityPair = device.battery_entity;
|
|
||||||
const battery =
|
|
||||||
batteryEntityPair && batteryEntityPair[0]
|
|
||||||
? this.hass.states[batteryEntityPair[0]]
|
|
||||||
: undefined;
|
|
||||||
const batteryDomain = battery ? computeStateDomain(battery) : undefined;
|
|
||||||
const batteryCharging =
|
|
||||||
batteryEntityPair && batteryEntityPair[1]
|
|
||||||
? this.hass.states[batteryEntityPair[1]]
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return battery &&
|
|
||||||
(batteryDomain === "binary_sensor" || !isNaN(battery.state as any))
|
|
||||||
? html`
|
|
||||||
${batteryDomain === "sensor"
|
|
||||||
? this.hass.formatEntityState(battery)
|
|
||||||
: nothing}
|
|
||||||
<ha-battery-icon
|
|
||||||
.hass=${this.hass}
|
|
||||||
.batteryStateObj=${battery}
|
|
||||||
.batteryChargingStateObj=${batteryCharging}
|
|
||||||
></ha-battery-icon>
|
|
||||||
`
|
|
||||||
: html`—`;
|
|
||||||
},
|
},
|
||||||
};
|
manufacturer: {
|
||||||
columns.disabled_by = {
|
title: localize("ui.panel.config.devices.data_table.manufacturer"),
|
||||||
title: "",
|
sortable: true,
|
||||||
label: localize("ui.panel.config.devices.data_table.disabled_by"),
|
filterable: true,
|
||||||
hidden: true,
|
groupable: true,
|
||||||
template: (device) =>
|
width: "15%",
|
||||||
device.disabled_by
|
},
|
||||||
? this.hass.localize("ui.panel.config.devices.disabled")
|
model: {
|
||||||
: "",
|
title: localize("ui.panel.config.devices.data_table.model"),
|
||||||
};
|
sortable: true,
|
||||||
columns.labels = {
|
filterable: true,
|
||||||
title: "",
|
width: "15%",
|
||||||
hidden: true,
|
},
|
||||||
filterable: true,
|
area: {
|
||||||
template: (device) =>
|
title: localize("ui.panel.config.devices.data_table.area"),
|
||||||
device.label_entries.map((lbl) => lbl.name).join(" "),
|
sortable: true,
|
||||||
};
|
filterable: true,
|
||||||
|
groupable: true,
|
||||||
|
width: "15%",
|
||||||
|
},
|
||||||
|
integration: {
|
||||||
|
title: localize("ui.panel.config.devices.data_table.integration"),
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
groupable: true,
|
||||||
|
width: "15%",
|
||||||
|
},
|
||||||
|
battery_entity: {
|
||||||
|
title: localize("ui.panel.config.devices.data_table.battery"),
|
||||||
|
showNarrow: true,
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
type: "numeric",
|
||||||
|
width: narrow ? "105px" : "15%",
|
||||||
|
maxWidth: "105px",
|
||||||
|
valueColumn: "battery_level",
|
||||||
|
template: (device) => {
|
||||||
|
const batteryEntityPair = device.battery_entity;
|
||||||
|
const battery =
|
||||||
|
batteryEntityPair && batteryEntityPair[0]
|
||||||
|
? this.hass.states[batteryEntityPair[0]]
|
||||||
|
: undefined;
|
||||||
|
const batteryDomain = battery
|
||||||
|
? computeStateDomain(battery)
|
||||||
|
: undefined;
|
||||||
|
const batteryCharging =
|
||||||
|
batteryEntityPair && batteryEntityPair[1]
|
||||||
|
? this.hass.states[batteryEntityPair[1]]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return columns;
|
return battery &&
|
||||||
|
(batteryDomain === "binary_sensor" || !isNaN(battery.state as any))
|
||||||
|
? html`
|
||||||
|
${batteryDomain === "sensor"
|
||||||
|
? this.hass.formatEntityState(battery)
|
||||||
|
: nothing}
|
||||||
|
<ha-battery-icon
|
||||||
|
.hass=${this.hass}
|
||||||
|
.batteryStateObj=${battery}
|
||||||
|
.batteryChargingStateObj=${batteryCharging}
|
||||||
|
></ha-battery-icon>
|
||||||
|
`
|
||||||
|
: html`—`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disabled_by: {
|
||||||
|
title: "",
|
||||||
|
label: localize("ui.panel.config.devices.data_table.disabled_by"),
|
||||||
|
hidden: true,
|
||||||
|
template: (device) =>
|
||||||
|
device.disabled_by
|
||||||
|
? this.hass.localize("ui.panel.config.devices.disabled")
|
||||||
|
: "",
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
title: "",
|
||||||
|
hidden: true,
|
||||||
|
filterable: true,
|
||||||
|
template: (device) =>
|
||||||
|
device.label_entries.map((lbl) => lbl.name).join(" "),
|
||||||
|
},
|
||||||
|
} as DataTableColumnContainer<DeviceItem>;
|
||||||
});
|
});
|
||||||
|
|
||||||
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`
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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 });
|
||||||
|
@ -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,81 +137,93 @@ class DialogZWaveJSAddNode extends LitElement {
|
|||||||
indeterminate
|
indeterminate
|
||||||
></ha-circular-progress>
|
></ha-circular-progress>
|
||||||
</div>`
|
</div>`
|
||||||
: this._status === "choose_strategy"
|
: this._status === "waiting_for_device"
|
||||||
? html`<h3>Choose strategy</h3>
|
? html`<div class="flex-container">
|
||||||
<div class="flex-column">
|
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||||
<ha-formfield
|
<p>
|
||||||
.label=${html`<b>Secure if possible</b>
|
${this.hass.localize(
|
||||||
<div class="secondary">
|
"ui.panel.config.zwave_js.add_node.waiting_for_device"
|
||||||
Requires user interaction during inclusion. Fast and
|
)}
|
||||||
secure with S2 when supported. Fallback to legacy S0 or
|
</p>
|
||||||
no encryption when necessary.
|
</div>`
|
||||||
</div>`}
|
: this._status === "choose_strategy"
|
||||||
>
|
? html`<h3>Choose strategy</h3>
|
||||||
<ha-radio
|
<div class="flex-column">
|
||||||
name="strategy"
|
<ha-formfield
|
||||||
@change=${this._handleStrategyChange}
|
.label=${html`<b>Secure if possible</b>
|
||||||
.value=${InclusionStrategy.Default}
|
<div class="secondary">
|
||||||
.checked=${this._inclusionStrategy ===
|
Requires user interaction during inclusion. Fast and
|
||||||
InclusionStrategy.Default ||
|
secure with S2 when supported. Fallback to legacy S0
|
||||||
this._inclusionStrategy === undefined}
|
or no encryption when necessary.
|
||||||
|
</div>`}
|
||||||
>
|
>
|
||||||
</ha-radio>
|
<ha-radio
|
||||||
</ha-formfield>
|
name="strategy"
|
||||||
<ha-formfield
|
@change=${this._handleStrategyChange}
|
||||||
.label=${html`<b>Legacy Secure</b>
|
.value=${InclusionStrategy.Default}
|
||||||
<div class="secondary">
|
.checked=${this._inclusionStrategy ===
|
||||||
Uses the older S0 security that is secure, but slow due
|
InclusionStrategy.Default ||
|
||||||
to a lot of overhead. Allows securely including S2
|
this._inclusionStrategy === undefined}
|
||||||
capable devices which fail to be included with S2.
|
>
|
||||||
</div>`}
|
</ha-radio>
|
||||||
>
|
</ha-formfield>
|
||||||
<ha-radio
|
<ha-formfield
|
||||||
name="strategy"
|
.label=${html`<b>Legacy Secure</b>
|
||||||
@change=${this._handleStrategyChange}
|
<div class="secondary">
|
||||||
.value=${InclusionStrategy.Security_S0}
|
Uses the older S0 security that is secure, but slow
|
||||||
.checked=${this._inclusionStrategy ===
|
due to a lot of overhead. Allows securely including S2
|
||||||
InclusionStrategy.Security_S0}
|
capable devices which fail to be included with S2.
|
||||||
|
</div>`}
|
||||||
>
|
>
|
||||||
</ha-radio>
|
<ha-radio
|
||||||
</ha-formfield>
|
name="strategy"
|
||||||
<ha-formfield
|
@change=${this._handleStrategyChange}
|
||||||
.label=${html`<b>Insecure</b>
|
.value=${InclusionStrategy.Security_S0}
|
||||||
<div class="secondary">Do not use encryption.</div>`}
|
.checked=${this._inclusionStrategy ===
|
||||||
>
|
InclusionStrategy.Security_S0}
|
||||||
<ha-radio
|
>
|
||||||
name="strategy"
|
</ha-radio>
|
||||||
@change=${this._handleStrategyChange}
|
</ha-formfield>
|
||||||
.value=${InclusionStrategy.Insecure}
|
<ha-formfield
|
||||||
.checked=${this._inclusionStrategy ===
|
.label=${html`<b>Insecure</b>
|
||||||
InclusionStrategy.Insecure}
|
<div class="secondary">Do not use encryption.</div>`}
|
||||||
>
|
>
|
||||||
</ha-radio>
|
<ha-radio
|
||||||
</ha-formfield>
|
name="strategy"
|
||||||
</div>
|
@change=${this._handleStrategyChange}
|
||||||
<mwc-button
|
.value=${InclusionStrategy.Insecure}
|
||||||
slot="primaryAction"
|
.checked=${this._inclusionStrategy ===
|
||||||
@click=${this._startManualInclusion}
|
InclusionStrategy.Insecure}
|
||||||
>
|
>
|
||||||
Search device
|
</ha-radio>
|
||||||
</mwc-button>`
|
</ha-formfield>
|
||||||
: this._status === "qr_scan"
|
</div>
|
||||||
? html`${this._error
|
<mwc-button
|
||||||
? html`<ha-alert alert-type="error"
|
slot="primaryAction"
|
||||||
>${this._error}</ha-alert
|
@click=${this._startManualInclusion}
|
||||||
>`
|
>
|
||||||
: ""}
|
Search device
|
||||||
<ha-qr-scanner
|
|
||||||
.localize=${this.hass.localize}
|
|
||||||
@qr-code-scanned=${this._qrCodeScanned}
|
|
||||||
></ha-qr-scanner>
|
|
||||||
<mwc-button slot="secondaryAction" @click=${this._startOver}>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.zwave_js.common.back"
|
|
||||||
)}
|
|
||||||
</mwc-button>`
|
</mwc-button>`
|
||||||
: this._status === "validate_dsk_enter_pin"
|
: this._status === "qr_scan"
|
||||||
? html`
|
? html`${this._error
|
||||||
|
? html`<ha-alert alert-type="error"
|
||||||
|
>${this._error}</ha-alert
|
||||||
|
>`
|
||||||
|
: ""}
|
||||||
|
<ha-qr-scanner
|
||||||
|
.localize=${this.hass.localize}
|
||||||
|
@qr-code-scanned=${this._qrCodeScanned}
|
||||||
|
></ha-qr-scanner>
|
||||||
|
<mwc-button
|
||||||
|
slot="secondaryAction"
|
||||||
|
@click=${this._startOver}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.common.back"
|
||||||
|
)}
|
||||||
|
</mwc-button>`
|
||||||
|
: this._status === "validate_dsk_enter_pin"
|
||||||
|
? html`
|
||||||
<p>
|
<p>
|
||||||
Please enter the 5-digit PIN for your device and verify that
|
Please enter the 5-digit PIN for your device and verify that
|
||||||
the rest of the device-specific key matches the one that can
|
the rest of the device-specific key matches the one that can
|
||||||
@ -225,198 +252,160 @@ class DialogZWaveJSAddNode extends LitElement {
|
|||||||
</mwc-button>
|
</mwc-button>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: this._status === "grant_security_classes"
|
: this._status === "grant_security_classes"
|
||||||
? html`
|
|
||||||
<h3>
|
|
||||||
The device has requested the following security classes:
|
|
||||||
</h3>
|
|
||||||
${this._error
|
|
||||||
? html`<ha-alert alert-type="error"
|
|
||||||
>${this._error}</ha-alert
|
|
||||||
>`
|
|
||||||
: ""}
|
|
||||||
<div class="flex-column">
|
|
||||||
${this._requestedGrant?.securityClasses
|
|
||||||
.sort((a, b) => {
|
|
||||||
// Put highest security classes at the top, S0 at the bottom
|
|
||||||
if (a === SecurityClass.S0_Legacy) return 1;
|
|
||||||
if (b === SecurityClass.S0_Legacy) return -1;
|
|
||||||
return b - a;
|
|
||||||
})
|
|
||||||
.map(
|
|
||||||
(securityClass) =>
|
|
||||||
html`<ha-formfield
|
|
||||||
.label=${html`<b
|
|
||||||
>${this.hass.localize(
|
|
||||||
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.title`
|
|
||||||
)}</b
|
|
||||||
>
|
|
||||||
<div class="secondary">
|
|
||||||
${this.hass.localize(
|
|
||||||
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.description`
|
|
||||||
)}
|
|
||||||
</div>`}
|
|
||||||
>
|
|
||||||
<ha-checkbox
|
|
||||||
@change=${this._handleSecurityClassChange}
|
|
||||||
.value=${securityClass}
|
|
||||||
.checked=${this._securityClasses.includes(
|
|
||||||
securityClass
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
</ha-checkbox>
|
|
||||||
</ha-formfield>`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<mwc-button
|
|
||||||
slot="primaryAction"
|
|
||||||
.disabled=${!this._securityClasses.length}
|
|
||||||
@click=${this._grantSecurityClasses}
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</mwc-button>
|
|
||||||
`
|
|
||||||
: this._status === "timed_out"
|
|
||||||
? html`
|
? html`
|
||||||
<h3>Timed out!</h3>
|
<h3>
|
||||||
<p>
|
The device has requested the following security
|
||||||
We have not found any device in inclusion mode. Make
|
classes:
|
||||||
sure the device is active and in inclusion mode.
|
</h3>
|
||||||
</p>
|
${this._error
|
||||||
<mwc-button
|
? html`<ha-alert alert-type="error"
|
||||||
slot="primaryAction"
|
>${this._error}</ha-alert
|
||||||
@click=${this._startOver}
|
>`
|
||||||
>
|
: ""}
|
||||||
Retry
|
<div class="flex-column">
|
||||||
</mwc-button>
|
${this._requestedGrant?.securityClasses
|
||||||
`
|
.sort((a, b) => {
|
||||||
: this._status === "started_specific"
|
// Put highest security classes at the top, S0 at the bottom
|
||||||
? html`<h3>
|
if (a === SecurityClass.S0_Legacy) return 1;
|
||||||
${this.hass.localize(
|
if (b === SecurityClass.S0_Legacy) return -1;
|
||||||
"ui.panel.config.zwave_js.add_node.searching_device"
|
return b - a;
|
||||||
)}
|
})
|
||||||
</h3>
|
.map(
|
||||||
<ha-circular-progress
|
(securityClass) =>
|
||||||
indeterminate
|
html`<ha-formfield
|
||||||
></ha-circular-progress>
|
.label=${html`<b
|
||||||
<p>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
|
|
||||||
)}
|
|
||||||
</p>`
|
|
||||||
: this._status === "started"
|
|
||||||
? html`
|
|
||||||
<div class="select-inclusion">
|
|
||||||
<div class="outline">
|
|
||||||
<h2>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.zwave_js.add_node.searching_device"
|
|
||||||
)}
|
|
||||||
</h2>
|
|
||||||
<ha-circular-progress
|
|
||||||
indeterminate
|
|
||||||
></ha-circular-progress>
|
|
||||||
<p>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<button
|
|
||||||
class="link"
|
|
||||||
@click=${this._chooseInclusionStrategy}
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.zwave_js.add_node.choose_inclusion_strategy"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
${this._supportsSmartStart
|
|
||||||
? html` <div class="outline">
|
|
||||||
<h2>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.zwave_js.add_node.qr_code"
|
|
||||||
)}
|
|
||||||
</h2>
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${mdiQrcodeScan}
|
|
||||||
></ha-svg-icon>
|
|
||||||
<p>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.zwave_js.add_node.qr_code_paragraph"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<mwc-button @click=${this._scanQRCode}>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.zwave_js.add_node.scan_qr_code"
|
|
||||||
)}
|
|
||||||
</mwc-button>
|
|
||||||
</p>
|
|
||||||
</div>`
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
<mwc-button
|
|
||||||
slot="primaryAction"
|
|
||||||
@click=${this.closeDialog}
|
|
||||||
>
|
|
||||||
${this.hass.localize("ui.common.cancel")}
|
|
||||||
</mwc-button>
|
|
||||||
`
|
|
||||||
: this._status === "interviewing"
|
|
||||||
? html`
|
|
||||||
<div class="flex-container">
|
|
||||||
<ha-circular-progress
|
|
||||||
indeterminate
|
|
||||||
></ha-circular-progress>
|
|
||||||
<div class="status">
|
|
||||||
<p>
|
|
||||||
<b
|
|
||||||
>${this.hass.localize(
|
>${this.hass.localize(
|
||||||
"ui.panel.config.zwave_js.add_node.interview_started"
|
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.title`
|
||||||
)}</b
|
)}</b
|
||||||
>
|
>
|
||||||
|
<div class="secondary">
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.description`
|
||||||
|
)}
|
||||||
|
</div>`}
|
||||||
|
>
|
||||||
|
<ha-checkbox
|
||||||
|
@change=${this._handleSecurityClassChange}
|
||||||
|
.value=${securityClass}
|
||||||
|
.checked=${this._securityClasses.includes(
|
||||||
|
securityClass
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
</ha-checkbox>
|
||||||
|
</ha-formfield>`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<mwc-button
|
||||||
|
slot="primaryAction"
|
||||||
|
.disabled=${!this._securityClasses.length}
|
||||||
|
@click=${this._grantSecurityClasses}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</mwc-button>
|
||||||
|
`
|
||||||
|
: this._status === "timed_out"
|
||||||
|
? html`
|
||||||
|
<h3>Timed out!</h3>
|
||||||
|
<p>
|
||||||
|
We have not found any device in inclusion mode. Make
|
||||||
|
sure the device is active and in inclusion mode.
|
||||||
|
</p>
|
||||||
|
<mwc-button
|
||||||
|
slot="primaryAction"
|
||||||
|
@click=${this._startOver}
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</mwc-button>
|
||||||
|
`
|
||||||
|
: this._status === "started_specific"
|
||||||
|
? html`<h3>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.add_node.searching_device"
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
<ha-circular-progress
|
||||||
|
indeterminate
|
||||||
|
></ha-circular-progress>
|
||||||
|
<p>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
|
||||||
|
)}
|
||||||
|
</p>`
|
||||||
|
: this._status === "started"
|
||||||
|
? html`
|
||||||
|
<div class="select-inclusion">
|
||||||
|
<div class="outline">
|
||||||
|
<h2>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.add_node.searching_device"
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
<ha-circular-progress
|
||||||
|
indeterminate
|
||||||
|
></ha-circular-progress>
|
||||||
|
<p>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button
|
||||||
|
class="link"
|
||||||
|
@click=${this._chooseInclusionStrategy}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.add_node.choose_inclusion_strategy"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
${this._stages
|
|
||||||
? html` <div class="stages">
|
|
||||||
${this._stages.map(
|
|
||||||
(stage) => html`
|
|
||||||
<span class="stage">
|
|
||||||
<ha-svg-icon
|
|
||||||
.path=${mdiCheckCircle}
|
|
||||||
class="success"
|
|
||||||
></ha-svg-icon>
|
|
||||||
${stage}
|
|
||||||
</span>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>`
|
|
||||||
: ""}
|
|
||||||
</div>
|
</div>
|
||||||
|
${this._supportsSmartStart
|
||||||
|
? html` <div class="outline">
|
||||||
|
<h2>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.add_node.qr_code"
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${mdiQrcodeScan}
|
||||||
|
></ha-svg-icon>
|
||||||
|
<p>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.add_node.qr_code_paragraph"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<mwc-button @click=${this._scanQRCode}>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.add_node.scan_qr_code"
|
||||||
|
)}
|
||||||
|
</mwc-button>
|
||||||
|
</p>
|
||||||
|
</div>`
|
||||||
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
<mwc-button
|
<mwc-button
|
||||||
slot="primaryAction"
|
slot="primaryAction"
|
||||||
@click=${this.closeDialog}
|
@click=${this.closeDialog}
|
||||||
>
|
>
|
||||||
${this.hass.localize("ui.common.close")}
|
${this.hass.localize("ui.common.cancel")}
|
||||||
</mwc-button>
|
</mwc-button>
|
||||||
`
|
`
|
||||||
: this._status === "failed"
|
: this._status === "interviewing"
|
||||||
? html`
|
? html`
|
||||||
<div class="flex-container">
|
<div class="flex-container">
|
||||||
|
<ha-circular-progress
|
||||||
|
indeterminate
|
||||||
|
></ha-circular-progress>
|
||||||
<div class="status">
|
<div class="status">
|
||||||
<ha-alert
|
<p>
|
||||||
alert-type="error"
|
<b
|
||||||
.title=${this.hass.localize(
|
>${this.hass.localize(
|
||||||
"ui.panel.config.zwave_js.add_node.inclusion_failed"
|
"ui.panel.config.zwave_js.add_node.interview_started"
|
||||||
)}
|
)}</b
|
||||||
>
|
>
|
||||||
${this._error ||
|
</p>
|
||||||
this.hass.localize(
|
|
||||||
"ui.panel.config.zwave_js.add_node.check_logs"
|
|
||||||
)}
|
|
||||||
</ha-alert>
|
|
||||||
${this._stages
|
${this._stages
|
||||||
? html` <div class="stages">
|
? html` <div class="stages">
|
||||||
${this._stages.map(
|
${this._stages.map(
|
||||||
@ -441,45 +430,21 @@ class DialogZWaveJSAddNode extends LitElement {
|
|||||||
${this.hass.localize("ui.common.close")}
|
${this.hass.localize("ui.common.close")}
|
||||||
</mwc-button>
|
</mwc-button>
|
||||||
`
|
`
|
||||||
: this._status === "finished"
|
: this._status === "failed"
|
||||||
? html`
|
? html`
|
||||||
<div class="flex-container">
|
<div class="flex-container">
|
||||||
<ha-svg-icon
|
|
||||||
.path=${this._lowSecurity
|
|
||||||
? mdiAlertCircle
|
|
||||||
: mdiCheckCircle}
|
|
||||||
class=${this._lowSecurity
|
|
||||||
? "warning"
|
|
||||||
: "success"}
|
|
||||||
></ha-svg-icon>
|
|
||||||
<div class="status">
|
<div class="status">
|
||||||
<p>
|
<ha-alert
|
||||||
${this.hass.localize(
|
alert-type="error"
|
||||||
"ui.panel.config.zwave_js.add_node.inclusion_finished"
|
.title=${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.add_node.inclusion_failed"
|
||||||
)}
|
)}
|
||||||
</p>
|
|
||||||
${this._lowSecurity
|
|
||||||
? html`<ha-alert
|
|
||||||
alert-type="warning"
|
|
||||||
title="The device was added insecurely"
|
|
||||||
>
|
|
||||||
There was an error during secure
|
|
||||||
inclusion. You can try again by
|
|
||||||
excluding the device and adding it
|
|
||||||
again.
|
|
||||||
</ha-alert>`
|
|
||||||
: ""}
|
|
||||||
<a
|
|
||||||
href=${`/config/devices/device/${
|
|
||||||
this._device!.id
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<mwc-button>
|
${this._error ||
|
||||||
${this.hass.localize(
|
this.hass.localize(
|
||||||
"ui.panel.config.zwave_js.add_node.view_device"
|
"ui.panel.config.zwave_js.add_node.check_logs"
|
||||||
)}
|
)}
|
||||||
</mwc-button>
|
</ha-alert>
|
||||||
</a>
|
|
||||||
${this._stages
|
${this._stages
|
||||||
? html` <div class="stages">
|
? html` <div class="stages">
|
||||||
${this._stages.map(
|
${this._stages.map(
|
||||||
@ -504,18 +469,60 @@ class DialogZWaveJSAddNode extends LitElement {
|
|||||||
${this.hass.localize("ui.common.close")}
|
${this.hass.localize("ui.common.close")}
|
||||||
</mwc-button>
|
</mwc-button>
|
||||||
`
|
`
|
||||||
: this._status === "provisioned"
|
: this._status === "finished"
|
||||||
? html` <div class="flex-container">
|
? html`
|
||||||
|
<div class="flex-container">
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
.path=${mdiCheckCircle}
|
.path=${this._lowSecurity
|
||||||
class="success"
|
? mdiAlertCircle
|
||||||
|
: mdiCheckCircle}
|
||||||
|
class=${this._lowSecurity
|
||||||
|
? "warning"
|
||||||
|
: "success"}
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
<div class="status">
|
<div class="status">
|
||||||
<p>
|
<p>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.zwave_js.add_node.provisioning_finished"
|
"ui.panel.config.zwave_js.add_node.inclusion_finished"
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
${this._lowSecurity
|
||||||
|
? html`<ha-alert
|
||||||
|
alert-type="warning"
|
||||||
|
title="The device was added insecurely"
|
||||||
|
>
|
||||||
|
There was an error during secure
|
||||||
|
inclusion. You can try again by
|
||||||
|
excluding the device and adding it
|
||||||
|
again.
|
||||||
|
</ha-alert>`
|
||||||
|
: ""}
|
||||||
|
<a
|
||||||
|
href=${`/config/devices/device/${
|
||||||
|
this._device?.id
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<mwc-button>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.add_node.view_device"
|
||||||
|
)}
|
||||||
|
</mwc-button>
|
||||||
|
</a>
|
||||||
|
${this._stages
|
||||||
|
? html` <div class="stages">
|
||||||
|
${this._stages.map(
|
||||||
|
(stage) => html`
|
||||||
|
<span class="stage">
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${mdiCheckCircle}
|
||||||
|
class="success"
|
||||||
|
></ha-svg-icon>
|
||||||
|
${stage}
|
||||||
|
</span>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>`
|
||||||
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<mwc-button
|
<mwc-button
|
||||||
@ -523,12 +530,42 @@ class DialogZWaveJSAddNode extends LitElement {
|
|||||||
@click=${this.closeDialog}
|
@click=${this.closeDialog}
|
||||||
>
|
>
|
||||||
${this.hass.localize("ui.common.close")}
|
${this.hass.localize("ui.common.close")}
|
||||||
</mwc-button>`
|
</mwc-button>
|
||||||
: ""}
|
`
|
||||||
|
: this._status === "provisioned"
|
||||||
|
? html` <div class="flex-container">
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${mdiCheckCircle}
|
||||||
|
class="success"
|
||||||
|
></ha-svg-icon>
|
||||||
|
<div class="status">
|
||||||
|
<p>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.zwave_js.add_node.provisioning_finished"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<mwc-button
|
||||||
|
slot="primaryAction"
|
||||||
|
@click=${this.closeDialog}
|
||||||
|
>
|
||||||
|
${this.hass.localize("ui.common.close")}
|
||||||
|
</mwc-button>`
|
||||||
|
: ""}
|
||||||
</ha-dialog>
|
</ha-dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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,87 +144,75 @@ 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.title}
|
: (dashboard) => html`
|
||||||
${dashboard.default
|
${dashboard.title}
|
||||||
? html`
|
${dashboard.default
|
||||||
<ha-svg-icon
|
? html`
|
||||||
style="padding-left: 10px; padding-inline-start: 10px; direction: var(--direction);"
|
<ha-svg-icon
|
||||||
.path=${mdiCheckCircleOutline}
|
style="padding-left: 10px; padding-inline-start: 10px; direction: var(--direction);"
|
||||||
></ha-svg-icon>
|
.path=${mdiCheckCircleOutline}
|
||||||
<simple-tooltip animation-delay="0">
|
></ha-svg-icon>
|
||||||
${this.hass.localize(
|
<simple-tooltip animation-delay="0">
|
||||||
`ui.panel.config.lovelace.dashboards.default_dashboard`
|
${this.hass.localize(
|
||||||
)}
|
`ui.panel.config.lovelace.dashboards.default_dashboard`
|
||||||
</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(
|
||||||
|
"ui.panel.config.lovelace.dashboards.picker.headers.conf_mode"
|
||||||
|
),
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
width: "20%",
|
||||||
|
template: (dashboard) => html`
|
||||||
|
${this.hass.localize(
|
||||||
|
`ui.panel.config.lovelace.dashboards.conf_mode.${dashboard.mode}`
|
||||||
|
) || dashboard.mode}
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
if (dashboards.some((dashboard) => dashboard.filename)) {
|
||||||
|
columns.filename = {
|
||||||
title: localize(
|
title: localize(
|
||||||
"ui.panel.config.lovelace.dashboards.picker.headers.conf_mode"
|
"ui.panel.config.lovelace.dashboards.picker.headers.filename"
|
||||||
),
|
),
|
||||||
|
width: "15%",
|
||||||
sortable: true,
|
sortable: true,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
width: "20%",
|
|
||||||
template: (dashboard) => html`
|
|
||||||
${this.hass.localize(
|
|
||||||
`ui.panel.config.lovelace.dashboards.conf_mode.${dashboard.mode}`
|
|
||||||
) || dashboard.mode}
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
if (dashboards.some((dashboard) => dashboard.filename)) {
|
|
||||||
columns.filename = {
|
|
||||||
title: localize(
|
|
||||||
"ui.panel.config.lovelace.dashboards.picker.headers.filename"
|
|
||||||
),
|
|
||||||
width: "15%",
|
|
||||||
sortable: true,
|
|
||||||
filterable: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
columns.require_admin = {
|
|
||||||
title: localize(
|
|
||||||
"ui.panel.config.lovelace.dashboards.picker.headers.require_admin"
|
|
||||||
),
|
|
||||||
sortable: true,
|
|
||||||
type: "icon",
|
|
||||||
width: "100px",
|
|
||||||
template: (dashboard) =>
|
|
||||||
dashboard.require_admin
|
|
||||||
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
|
||||||
: html`—`,
|
|
||||||
};
|
|
||||||
columns.show_in_sidebar = {
|
|
||||||
title: localize(
|
|
||||||
"ui.panel.config.lovelace.dashboards.picker.headers.sidebar"
|
|
||||||
),
|
|
||||||
type: "icon",
|
|
||||||
width: "121px",
|
|
||||||
template: (dashboard) =>
|
|
||||||
dashboard.show_in_sidebar
|
|
||||||
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
|
||||||
: html`—`,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
columns.require_admin = {
|
||||||
|
title: localize(
|
||||||
|
"ui.panel.config.lovelace.dashboards.picker.headers.require_admin"
|
||||||
|
),
|
||||||
|
sortable: true,
|
||||||
|
type: "icon",
|
||||||
|
hidden: narrow,
|
||||||
|
width: "100px",
|
||||||
|
template: (dashboard) =>
|
||||||
|
dashboard.require_admin
|
||||||
|
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
||||||
|
: html`—`,
|
||||||
|
};
|
||||||
|
columns.show_in_sidebar = {
|
||||||
|
title: localize(
|
||||||
|
"ui.panel.config.lovelace.dashboards.picker.headers.sidebar"
|
||||||
|
),
|
||||||
|
type: "icon",
|
||||||
|
hidden: narrow,
|
||||||
|
width: "121px",
|
||||||
|
template: (dashboard) =>
|
||||||
|
dashboard.show_in_sidebar
|
||||||
|
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
|
||||||
|
: 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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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">
|
||||||
"ui.panel.config.person.detail.allow_login"
|
${this.hass!.localize(
|
||||||
)}
|
"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);
|
||||||
|
@ -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,
|
||||||
|
@ -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")
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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, {
|
||||||
|
@ -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();
|
? html`<ha-data-table-labels
|
||||||
const dayDifference = differenceInDays(now, date);
|
@label-clicked=${this._labelClicked}
|
||||||
return html`
|
.labels=${script.labels}
|
||||||
<div style="font-size: 14px;">${script.name}</div>
|
></ha-data-table-labels>`
|
||||||
${narrow
|
: nothing,
|
||||||
? 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
|
|
||||||
@label-clicked=${this._labelClicked}
|
|
||||||
.labels=${script.labels}
|
|
||||||
></ha-data-table-labels>`
|
|
||||||
: 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,
|
||||||
|
@ -66,93 +66,82 @@ 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: "",
|
|
||||||
label: localize("ui.panel.config.tag.headers.icon"),
|
|
||||||
type: "icon",
|
|
||||||
template: (tag) => html`<tag-image .tag=${tag}></tag-image>`,
|
|
||||||
},
|
|
||||||
display_name: {
|
|
||||||
title: localize("ui.panel.config.tag.headers.name"),
|
|
||||||
main: true,
|
|
||||||
sortable: true,
|
|
||||||
filterable: 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: {
|
|
||||||
title: localize("ui.panel.config.tag.headers.last_scanned"),
|
|
||||||
sortable: true,
|
|
||||||
hidden: narrow,
|
|
||||||
direction: "desc",
|
|
||||||
width: "20%",
|
|
||||||
template: (tag) => html`
|
|
||||||
${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")}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (this._canWriteTags) {
|
|
||||||
columns.write = {
|
|
||||||
title: "",
|
|
||||||
label: localize("ui.panel.config.tag.headers.write"),
|
|
||||||
type: "icon-button",
|
|
||||||
template: (tag) =>
|
|
||||||
html` <ha-icon-button
|
|
||||||
.tag=${tag}
|
|
||||||
@click=${this._handleWriteClick}
|
|
||||||
.label=${this.hass.localize("ui.panel.config.tag.write")}
|
|
||||||
.path=${mdiContentDuplicate}
|
|
||||||
></ha-icon-button>`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
columns.automation = {
|
|
||||||
title: "",
|
title: "",
|
||||||
|
moveable: false,
|
||||||
|
showNarrow: true,
|
||||||
|
label: localize("ui.panel.config.tag.headers.icon"),
|
||||||
|
type: "icon",
|
||||||
|
template: (tag) => html`<tag-image .tag=${tag}></tag-image>`,
|
||||||
|
},
|
||||||
|
display_name: {
|
||||||
|
title: localize("ui.panel.config.tag.headers.name"),
|
||||||
|
main: true,
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
grows: true,
|
||||||
|
},
|
||||||
|
last_scanned_datetime: {
|
||||||
|
title: localize("ui.panel.config.tag.headers.last_scanned"),
|
||||||
|
sortable: true,
|
||||||
|
direction: "desc",
|
||||||
|
width: "20%",
|
||||||
|
template: (tag) => html`
|
||||||
|
${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")}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (this._canWriteTags) {
|
||||||
|
columns.write = {
|
||||||
|
title: "",
|
||||||
|
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._handleAutomationClick}
|
@click=${this._handleWriteClick}
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize("ui.panel.config.tag.write")}
|
||||||
"ui.panel.config.tag.create_automation"
|
.path=${mdiContentDuplicate}
|
||||||
)}
|
|
||||||
.path=${mdiRobot}
|
|
||||||
></ha-icon-button>`,
|
></ha-icon-button>`,
|
||||||
};
|
};
|
||||||
columns.edit = {
|
|
||||||
title: "",
|
|
||||||
type: "icon-button",
|
|
||||||
template: (tag) =>
|
|
||||||
html` <ha-icon-button
|
|
||||||
.tag=${tag}
|
|
||||||
@click=${this._handleEditClick}
|
|
||||||
.label=${this.hass.localize("ui.panel.config.tag.edit")}
|
|
||||||
.path=${mdiCog}
|
|
||||||
></ha-icon-button>`,
|
|
||||||
};
|
|
||||||
return columns;
|
|
||||||
}
|
}
|
||||||
);
|
columns.automation = {
|
||||||
|
title: "",
|
||||||
|
type: "icon-button",
|
||||||
|
showNarrow: true,
|
||||||
|
template: (tag) =>
|
||||||
|
html`<ha-icon-button
|
||||||
|
.tag=${tag}
|
||||||
|
@click=${this._handleAutomationClick}
|
||||||
|
.label=${this.hass.localize("ui.panel.config.tag.create_automation")}
|
||||||
|
.path=${mdiRobot}
|
||||||
|
></ha-icon-button>`,
|
||||||
|
};
|
||||||
|
columns.edit = {
|
||||||
|
title: "",
|
||||||
|
type: "icon-button",
|
||||||
|
showNarrow: true,
|
||||||
|
hideable: false,
|
||||||
|
moveable: false,
|
||||||
|
template: (tag) =>
|
||||||
|
html`<ha-icon-button
|
||||||
|
.tag=${tag}
|
||||||
|
@click=${this._handleEditClick}
|
||||||
|
.label=${this.hass.localize("ui.panel.config.tag.edit")}
|
||||||
|
.path=${mdiCog}
|
||||||
|
></ha-icon-button>`,
|
||||||
|
};
|
||||||
|
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}
|
||||||
|
@ -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
|
||||||
|
.checked=${this._localOnly}
|
||||||
|
@change=${this._localOnlyChanged}
|
||||||
>
|
>
|
||||||
<ha-switch
|
</ha-switch>
|
||||||
.checked=${this._localOnly}
|
</ha-settings-row>
|
||||||
@change=${this._localOnlyChanged}
|
<ha-settings-row>
|
||||||
>
|
<span slot="heading">
|
||||||
</ha-switch>
|
${this.hass.localize("ui.panel.config.users.editor.admin")}
|
||||||
</ha-formfield>
|
</span>
|
||||||
</div>
|
<span slot="description">
|
||||||
<div class="row">
|
${this.hass.localize(
|
||||||
<ha-formfield
|
"ui.panel.config.users.editor.admin_description"
|
||||||
.label=${this.hass.localize("ui.panel.config.users.editor.admin")}
|
)}
|
||||||
>
|
</span>
|
||||||
<ha-switch
|
<ha-switch .checked=${this._isAdmin} @change=${this._adminChanged}>
|
||||||
.checked=${this._isAdmin}
|
</ha-switch>
|
||||||
@change=${this._adminChanged}
|
</ha-settings-row>
|
||||||
>
|
|
||||||
</ha-switch>
|
|
||||||
</ha-formfield>
|
|
||||||
</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;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
@ -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
|
||||||
|
@ -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">
|
||||||
<ha-textfield
|
${!user.system_generated
|
||||||
dialogInitialFocus
|
|
||||||
.value=${this._name}
|
|
||||||
.disabled=${user.system_generated}
|
|
||||||
@input=${this._nameChanged}
|
|
||||||
.label=${this.hass!.localize("ui.panel.config.users.editor.name")}
|
|
||||||
></ha-textfield>
|
|
||||||
<div class="row">
|
|
||||||
<ha-formfield
|
|
||||||
.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`
|
? html`
|
||||||
<br />
|
<ha-textfield
|
||||||
${this.hass.localize(
|
dialogInitialFocus
|
||||||
"ui.panel.config.users.users_privileges_note"
|
.value=${this._name}
|
||||||
)}
|
@input=${this._nameChanged}
|
||||||
|
.label=${this.hass!.localize(
|
||||||
|
"ui.panel.config.users.editor.name"
|
||||||
|
)}
|
||||||
|
></ha-textfield>
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.users.editor.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.users.editor.change_username"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
</ha-icon-button>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-settings-row>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
<div class="row">
|
${!user.system_generated && this.hass.user?.is_owner
|
||||||
<ha-formfield
|
? html`
|
||||||
.label=${this.hass.localize(
|
<ha-settings-row>
|
||||||
"ui.panel.config.users.editor.active"
|
<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
|
||||||
|
.disabled=${user.system_generated || user.is_owner}
|
||||||
|
.checked=${this._isActive}
|
||||||
|
@change=${this._activeChanged}
|
||||||
>
|
>
|
||||||
<ha-switch
|
</ha-switch>
|
||||||
.disabled=${user.system_generated || user.is_owner}
|
</ha-settings-row>
|
||||||
.checked=${this._isActive}
|
<ha-settings-row>
|
||||||
@change=${this._activeChanged}
|
<span slot="heading">
|
||||||
>
|
${this.hass.localize(
|
||||||
</ha-switch>
|
"ui.panel.config.users.editor.local_access_only"
|
||||||
</ha-formfield>
|
|
||||||
<ha-help-tooltip
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.users.editor.active_tooltip"
|
|
||||||
)}
|
)}
|
||||||
|
</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;
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
@ -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 {
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,10 +168,20 @@ export class VoiceAssistantsExpose extends LitElement {
|
|||||||
filterable: true,
|
filterable: true,
|
||||||
direction: "asc",
|
direction: "asc",
|
||||||
grows: true,
|
grows: true,
|
||||||
template: (entry) => html`
|
template: narrow
|
||||||
${entry.name}<br />
|
? undefined
|
||||||
<div class="secondary">${entry.entity_id}</div>
|
: (entry) => html`
|
||||||
`,
|
${entry.name}<br />
|
||||||
|
<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(
|
||||||
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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")) {
|
||||||
this._element.hass = this.hass;
|
try {
|
||||||
|
this._element.hass = this.hass;
|
||||||
|
} catch (e: any) {
|
||||||
|
this._buildElement(createErrorCardConfig(e.message, null));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (changedProperties.has("lovelace")) {
|
if (changedProps.has("preview")) {
|
||||||
this._element.editMode = this.lovelace?.editMode;
|
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("hass") || changedProperties.has("lovelace")) {
|
if (changedProps.has("isPanel")) {
|
||||||
this._updateElement();
|
|
||||||
}
|
|
||||||
if (changedProperties.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.toggleAttribute("hidden", !visible);
|
||||||
|
fireEvent(this, "card-visibility-changed", { value: visible });
|
||||||
|
}
|
||||||
|
|
||||||
this.style.setProperty("display", visible ? "" : "none");
|
|
||||||
this.toggleAttribute("hidden", !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) {
|
||||||
|
@ -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.config = cardConfig;
|
||||||
element.addEventListener(
|
|
||||||
"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
Loading…
x
Reference in New Issue
Block a user