mirror of
https://github.com/home-assistant/frontend.git
synced 2025-10-11 12:49:41 +00:00
Compare commits
31 Commits
sec_pypi_p
...
add-automa
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f831f876de | ||
![]() |
6653333c38 | ||
![]() |
8c19e080be | ||
![]() |
c649b1015a | ||
![]() |
1b6c33efd4 | ||
![]() |
5cfc34b020 | ||
![]() |
1e7647b214 | ||
![]() |
cef3a7ef99 | ||
![]() |
14d0028426 | ||
![]() |
28032d9d0d | ||
![]() |
6c1995ba1b | ||
![]() |
b68464c5d5 | ||
![]() |
31ccf114a6 | ||
![]() |
1b932ae4a2 | ||
![]() |
0df6019b95 | ||
![]() |
94fb03d2e2 | ||
![]() |
6dc165ebf8 | ||
![]() |
f2c5b91def | ||
![]() |
b312cca050 | ||
![]() |
ac14733bff | ||
![]() |
a2d4165511 | ||
![]() |
b87ffbd4f7 | ||
![]() |
a8f8d197f8 | ||
![]() |
4fcac79047 | ||
![]() |
42ddacd41a | ||
![]() |
ebc9981289 | ||
![]() |
23deab253b | ||
![]() |
ab172abe02 | ||
![]() |
10d5d8b15d | ||
![]() |
c9e472dab7 | ||
![]() |
1e13b2b812 |
13
.github/workflows/release.yaml
vendored
13
.github/workflows/release.yaml
vendored
@@ -19,11 +19,8 @@ jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
environment: pypi
|
||||
permissions:
|
||||
contents: write # Required to upload release assets
|
||||
id-token: write # For "Trusted Publisher" to PyPi
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
@@ -49,18 +46,14 @@ jobs:
|
||||
run: ./script/translations_download
|
||||
env:
|
||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||
|
||||
- name: Build and release package
|
||||
run: |
|
||||
python3 -m pip install build
|
||||
python3 -m pip install twine build
|
||||
export TWINE_USERNAME="__token__"
|
||||
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
||||
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
|
||||
script/release
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||
with:
|
||||
skip-existing: true
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
|
||||
with:
|
||||
|
3
gallery/src/pages/components/ha-wa-dialog.markdown
Normal file
3
gallery/src/pages/components/ha-wa-dialog.markdown
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Dialog (ha-wa-dialog)
|
||||
---
|
523
gallery/src/pages/components/ha-wa-dialog.ts
Normal file
523
gallery/src/pages/components/ha-wa-dialog.ts
Normal file
@@ -0,0 +1,523 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { mdiCog, mdiHelp } from "@mdi/js";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-dialog-footer";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import "../../../../src/components/ha-icon-button";
|
||||
import "../../../../src/components/ha-wa-dialog";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
|
||||
const SCHEMA: HaFormSchema[] = [
|
||||
{ type: "string", name: "Name", default: "", autofocus: true },
|
||||
{ type: "string", name: "Email", default: "" },
|
||||
];
|
||||
|
||||
type DialogType =
|
||||
| false
|
||||
| "basic"
|
||||
| "basic-subtitle-below"
|
||||
| "basic-subtitle-above"
|
||||
| "form"
|
||||
| "actions";
|
||||
|
||||
@customElement("demo-components-ha-wa-dialog")
|
||||
export class DemoHaWaDialog extends LitElement {
|
||||
@state() private _openDialog: DialogType = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="content">
|
||||
<h1>Dialog <code><ha-wa-dialog></code></h1>
|
||||
|
||||
<p class="subtitle">Dialog component built with WebAwesome.</p>
|
||||
|
||||
<h2>Demos</h2>
|
||||
|
||||
<div class="buttons">
|
||||
<ha-button @click=${this._handleOpenDialog("basic")}
|
||||
>Basic dialog</ha-button
|
||||
>
|
||||
<ha-button @click=${this._handleOpenDialog("basic-subtitle-below")}
|
||||
>Basic dialog with subtitle below</ha-button
|
||||
>
|
||||
<ha-button @click=${this._handleOpenDialog("basic-subtitle-above")}
|
||||
>Basic dialog with subtitle above</ha-button
|
||||
>
|
||||
<ha-button @click=${this._handleOpenDialog("form")}
|
||||
>Dialog with form</ha-button
|
||||
>
|
||||
<ha-button @click=${this._handleOpenDialog("actions")}
|
||||
>Dialog with actions</ha-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<ha-wa-dialog
|
||||
.open=${this._openDialog === "basic"}
|
||||
header-title="Basic dialog"
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div>Dialog content</div>
|
||||
</ha-wa-dialog>
|
||||
|
||||
<ha-wa-dialog
|
||||
.open=${this._openDialog === "basic-subtitle-below"}
|
||||
header-title="Basic dialog with subtitle"
|
||||
header-subtitle="This is a basic dialog with a subtitle below"
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div>Dialog content</div>
|
||||
</ha-wa-dialog>
|
||||
|
||||
<ha-wa-dialog
|
||||
.open=${this._openDialog === "basic-subtitle-above"}
|
||||
header-title="Dialog with subtitle above"
|
||||
header-subtitle="This is a basic dialog with a subtitle above"
|
||||
header-subtitle-position="above"
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div>Dialog content</div>
|
||||
</ha-wa-dialog>
|
||||
|
||||
<ha-wa-dialog
|
||||
.open=${this._openDialog === "form"}
|
||||
header-title="Dialog with form"
|
||||
header-subtitle="This is a dialog with a form and a footer"
|
||||
prevent-scrim-close
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<ha-form autofocus .schema=${SCHEMA}></ha-form>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
data-dialog="close"
|
||||
slot="secondaryAction"
|
||||
variant="plain"
|
||||
>Cancel</ha-button
|
||||
>
|
||||
<ha-button data-dialog="close" slot="primaryAction" variant="accent"
|
||||
>Submit</ha-button
|
||||
>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
|
||||
<ha-wa-dialog
|
||||
.open=${this._openDialog === "actions"}
|
||||
header-title="Dialog with actions"
|
||||
header-subtitle="This is a dialog with header actions"
|
||||
@closed=${this._handleClosed}
|
||||
>
|
||||
<div slot="headerActionItems">
|
||||
<ha-icon-button label="Settings" path=${mdiCog}></ha-icon-button>
|
||||
<ha-icon-button label="Help" path=${mdiHelp}></ha-icon-button>
|
||||
</div>
|
||||
|
||||
<div>Dialog content</div>
|
||||
</ha-wa-dialog>
|
||||
|
||||
<h2>Design</h2>
|
||||
|
||||
<h3>Width</h3>
|
||||
|
||||
<p>There are multiple widths available for the dialog.</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>small</code></td>
|
||||
<td><code>min(320px, var(--full-width))</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>medium</code></td>
|
||||
<td><code>min(580px, var(--full-width))</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>large</code></td>
|
||||
<td><code>min(720px, var(--full-width))</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>full</code></td>
|
||||
<td><code>var(--full-width)</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
<code>--full-width</code> is calculated based on the available width
|
||||
of the screen. 95vw is the maximum width of the dialog on a large
|
||||
screen, while on a small screen it is 100vw minus the safe area
|
||||
insets.
|
||||
</p>
|
||||
|
||||
<p>Dialogs have a default width of <code>medium</code>.</p>
|
||||
|
||||
<h3>Prevent scrim close</h3>
|
||||
|
||||
<p>
|
||||
You can prevent the dialog from being closed by clicking the
|
||||
scrim/overlay. This is allowed by default.
|
||||
</p>
|
||||
|
||||
<h3>Header</h3>
|
||||
|
||||
<p>The header contains a title, a subtitle and action items.</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Slot</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>header</code></td>
|
||||
<td>The entire header area.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>headerTitle</code></td>
|
||||
<td>The header title text.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>headerSubtitle</code></td>
|
||||
<td>The header subtitle text.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>headerActionItems</code></td>
|
||||
<td>The header action items.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>Header title</h4>
|
||||
|
||||
<p>The header title is a text string.</p>
|
||||
|
||||
<h4>Header subtitle</h4>
|
||||
|
||||
<p>The header subtitle is a text string.</p>
|
||||
|
||||
<h4>Header action items</h4>
|
||||
|
||||
<p>
|
||||
The header action items usually containing icon buttons and/or menu
|
||||
buttons.
|
||||
</p>
|
||||
|
||||
<h3>Body</h3>
|
||||
|
||||
<p>The body is the content of the dialog.</p>
|
||||
|
||||
<h3>Footer</h3>
|
||||
|
||||
<p>The footer is the footer of the dialog.</p>
|
||||
|
||||
<p>
|
||||
It is recommended to use the <code>ha-dialog-footer</code> component
|
||||
for the footer and to style the buttons inside the footer as so:
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Slot</th>
|
||||
<th>Description</th>
|
||||
<th>Variant to use</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>secondaryAction</code></td>
|
||||
<td>The secondary action button(s).</td>
|
||||
<td><code>plain</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>primaryAction</code></td>
|
||||
<td>The primary action button(s).</td>
|
||||
<td><code>accent</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Implementation</h2>
|
||||
|
||||
<h3>Example Usage</h3>
|
||||
|
||||
<pre><code><ha-wa-dialog
|
||||
open
|
||||
header-title="Dialog title"
|
||||
header-subtitle="Dialog subtitle"
|
||||
prevent-scrim-close
|
||||
>
|
||||
<div slot="headerActionItems">
|
||||
<ha-icon-button label="Settings" path="mdiCog"></ha-icon-button>
|
||||
<ha-icon-button label="Help" path="mdiHelp"></ha-icon-button>
|
||||
</div>
|
||||
<div>Dialog content</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button data-dialog="close" slot="secondaryAction" variant="plain"
|
||||
>Cancel</ha-button
|
||||
>
|
||||
<ha-button slot="primaryAction" variant="accent">Submit</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog></code></pre>
|
||||
|
||||
<h3>API</h3>
|
||||
|
||||
<p>
|
||||
This component is based on the webawesome dialog component. Check the
|
||||
<a
|
||||
href="https://webawesome.com/docs/components/dialog/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>webawesome documentation</a
|
||||
>
|
||||
for more details.
|
||||
</p>
|
||||
|
||||
<h4>Attributes</h4>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Attribute</th>
|
||||
<th>Description</th>
|
||||
<th>Default</th>
|
||||
<th>Options</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>open</code></td>
|
||||
<td>Controls the dialog open state.</td>
|
||||
<td><code>false</code></td>
|
||||
<td><code>false</code>, <code>true</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>width</code></td>
|
||||
<td>Preferred dialog width preset.</td>
|
||||
<td><code>medium</code></td>
|
||||
<td>
|
||||
<code>small</code>, <code>medium</code>, <code>large</code>,
|
||||
<code>full</code>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>prevent-scrim-close</code></td>
|
||||
<td>
|
||||
Prevents closing the dialog by clicking the scrim/overlay.
|
||||
</td>
|
||||
<td><code>false</code></td>
|
||||
<td><code>true</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>header-title</code></td>
|
||||
<td>Header title text when no custom title slot is provided.</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>header-subtitle</code></td>
|
||||
<td>
|
||||
Header subtitle text when no custom subtitle slot is provided.
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>header-subtitle-position</code></td>
|
||||
<td>Position of the subtitle relative to the title.</td>
|
||||
<td><code>below</code></td>
|
||||
<td><code>above</code>, <code>below</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>flexcontent</code></td>
|
||||
<td>
|
||||
Makes the dialog body a flex container for flexible layouts.
|
||||
</td>
|
||||
<td><code>false</code></td>
|
||||
<td><code>false</code>, <code>true</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>CSS Custom Properties</h4>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>CSS Property</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>--dialog-content-padding</code></td>
|
||||
<td>Padding for dialog content sections.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--ha-dialog-show-duration</code></td>
|
||||
<td>Show animation duration.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--ha-dialog-hide-duration</code></td>
|
||||
<td>Hide animation duration.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--ha-dialog-surface-background</code></td>
|
||||
<td>Dialog background color.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--ha-dialog-border-radius</code></td>
|
||||
<td>Border radius of the dialog surface.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--dialog-z-index</code></td>
|
||||
<td>Z-index for the dialog.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--dialog-surface-position</code></td>
|
||||
<td>CSS position of the dialog surface.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>--dialog-surface-margin-top</code></td>
|
||||
<td>Top margin for the dialog surface.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>Events</h4>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Event</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>opened</code></td>
|
||||
<td>Fired when the dialog is shown.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>closed</code></td>
|
||||
<td>Fired after the dialog is hidden.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleOpenDialog = (dialog: DialogType) => () => {
|
||||
this._openDialog = dialog;
|
||||
};
|
||||
|
||||
private _handleClosed = () => {
|
||||
this._openDialog = false;
|
||||
};
|
||||
|
||||
static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: var(--ha-space-6);
|
||||
margin-bottom: var(--ha-space-3);
|
||||
}
|
||||
|
||||
h3,
|
||||
h4 {
|
||||
margin-top: var(--ha-space-4);
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: var(--ha-space-2) 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 1.1em;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: var(--ha-space-3) 0;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
text-align: left;
|
||||
padding: var(--ha-space-2);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: var(--secondary-background-color);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: var(--secondary-background-color);
|
||||
padding: var(--ha-space-3);
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: var(--ha-space-3) 0;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-2);
|
||||
margin: var(--ha-space-4) 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-wa-dialog": DemoHaWaDialog;
|
||||
}
|
||||
}
|
26
package.json
26
package.json
@@ -37,22 +37,22 @@
|
||||
"@codemirror/view": "6.38.4",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.18.0",
|
||||
"@formatjs/intl-displaynames": "6.8.11",
|
||||
"@formatjs/intl-durationformat": "0.7.4",
|
||||
"@formatjs/intl-getcanonicallocales": "2.5.5",
|
||||
"@formatjs/intl-listformat": "7.7.11",
|
||||
"@formatjs/intl-locale": "4.2.11",
|
||||
"@formatjs/intl-numberformat": "8.15.4",
|
||||
"@formatjs/intl-pluralrules": "5.4.4",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.11",
|
||||
"@formatjs/intl-datetimeformat": "6.18.1",
|
||||
"@formatjs/intl-displaynames": "6.8.12",
|
||||
"@formatjs/intl-durationformat": "0.7.5",
|
||||
"@formatjs/intl-getcanonicallocales": "2.5.6",
|
||||
"@formatjs/intl-listformat": "7.7.12",
|
||||
"@formatjs/intl-locale": "4.2.12",
|
||||
"@formatjs/intl-numberformat": "8.15.5",
|
||||
"@formatjs/intl-pluralrules": "5.4.5",
|
||||
"@formatjs/intl-relativetimeformat": "11.4.12",
|
||||
"@fullcalendar/core": "6.1.19",
|
||||
"@fullcalendar/daygrid": "6.1.19",
|
||||
"@fullcalendar/interaction": "6.1.19",
|
||||
"@fullcalendar/list": "6.1.19",
|
||||
"@fullcalendar/luxon3": "6.1.19",
|
||||
"@fullcalendar/timegrid": "6.1.19",
|
||||
"@home-assistant/webawesome": "3.0.0-beta.6.ha.1",
|
||||
"@home-assistant/webawesome": "3.0.0-beta.6.ha.4",
|
||||
"@lezer/highlight": "1.2.1",
|
||||
"@lit-labs/motion": "1.0.9",
|
||||
"@lit-labs/observers": "2.0.6",
|
||||
@@ -114,7 +114,7 @@
|
||||
"hls.js": "1.6.13",
|
||||
"home-assistant-js-websocket": "9.5.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "10.7.16",
|
||||
"intl-messageformat": "10.7.17",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
@@ -183,7 +183,7 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"del": "8.0.1",
|
||||
"eslint": "9.36.0",
|
||||
"eslint": "9.37.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.10",
|
||||
@@ -217,7 +217,7 @@
|
||||
"terser-webpack-plugin": "5.3.14",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.45.0",
|
||||
"typescript-eslint": "8.46.0",
|
||||
"vite-tsconfig-paths": "5.1.4",
|
||||
"vitest": "3.2.4",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
|
@@ -9,7 +9,7 @@
|
||||
":semanticCommitsDisabled",
|
||||
"group:monorepos",
|
||||
"group:recommended",
|
||||
"npm:unpublishSafe"
|
||||
"security:minimumReleaseAgeNpm"
|
||||
],
|
||||
"enabledManagers": ["npm", "nvm"],
|
||||
"postUpdateOptions": ["yarnDedupeHighest"],
|
||||
|
@@ -1,4 +1,5 @@
|
||||
#!/bin/sh
|
||||
# Pushes a new version to PyPi.
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
@@ -11,4 +12,5 @@ yarn install
|
||||
script/build_frontend
|
||||
|
||||
rm -rf dist home_assistant_frontend.egg-info
|
||||
python3 -m build -q
|
||||
python3 -m build
|
||||
python3 -m twine upload dist/*.whl --skip-existing
|
||||
|
@@ -61,3 +61,9 @@ export const computeEntityEntryName = (
|
||||
|
||||
return name;
|
||||
};
|
||||
|
||||
export const entityUseDeviceName = (
|
||||
stateObj: HassEntity,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): boolean => !computeEntityName(stateObj, entities, devices);
|
||||
|
104
src/common/entity/compute_entity_name_display.ts
Normal file
104
src/common/entity/compute_entity_name_display.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { ensureArray } from "../array/ensure-array";
|
||||
import { computeAreaName } from "./compute_area_name";
|
||||
import { computeDeviceName } from "./compute_device_name";
|
||||
import { computeEntityName, entityUseDeviceName } from "./compute_entity_name";
|
||||
import { computeFloorName } from "./compute_floor_name";
|
||||
import { getEntityContext } from "./context/get_entity_context";
|
||||
|
||||
const DEFAULT_SEPARATOR = " ";
|
||||
|
||||
export type EntityNameItem =
|
||||
| {
|
||||
type: "entity" | "device" | "area" | "floor";
|
||||
}
|
||||
| {
|
||||
type: "text";
|
||||
text: string;
|
||||
};
|
||||
|
||||
export interface EntityNameOptions {
|
||||
separator?: string;
|
||||
}
|
||||
|
||||
export const computeEntityNameDisplay = (
|
||||
stateObj: HassEntity,
|
||||
name: EntityNameItem | EntityNameItem[],
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"],
|
||||
options?: EntityNameOptions
|
||||
) => {
|
||||
let items = ensureArray(name);
|
||||
|
||||
const separator = options?.separator ?? DEFAULT_SEPARATOR;
|
||||
|
||||
// If all items are text, just join them
|
||||
if (items.every((n) => n.type === "text")) {
|
||||
return items.map((item) => item.text).join(separator);
|
||||
}
|
||||
|
||||
const useDeviceName = entityUseDeviceName(stateObj, entities, devices);
|
||||
|
||||
// If entity uses device name, and device is not already included, replace it with device name
|
||||
if (useDeviceName) {
|
||||
const hasDevice = items.some((n) => n.type === "device");
|
||||
if (!hasDevice) {
|
||||
items = items.map((n) => (n.type === "entity" ? { type: "device" } : n));
|
||||
}
|
||||
}
|
||||
|
||||
const names = computeEntityNameList(
|
||||
stateObj,
|
||||
items,
|
||||
entities,
|
||||
devices,
|
||||
areas,
|
||||
floors
|
||||
);
|
||||
|
||||
// If after processing there is only one name, return that
|
||||
if (names.length === 1) {
|
||||
return names[0] || "";
|
||||
}
|
||||
|
||||
return names.filter((n) => n).join(separator);
|
||||
};
|
||||
|
||||
export const computeEntityNameList = (
|
||||
stateObj: HassEntity,
|
||||
name: EntityNameItem[],
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"]
|
||||
): (string | undefined)[] => {
|
||||
const { device, area, floor } = getEntityContext(
|
||||
stateObj,
|
||||
entities,
|
||||
devices,
|
||||
areas,
|
||||
floors
|
||||
);
|
||||
|
||||
const names = name.map((item) => {
|
||||
switch (item.type) {
|
||||
case "entity":
|
||||
return computeEntityName(stateObj, entities, devices);
|
||||
case "device":
|
||||
return device ? computeDeviceName(device) : undefined;
|
||||
case "area":
|
||||
return area ? computeAreaName(area) : undefined;
|
||||
case "floor":
|
||||
return floor ? computeFloorName(floor) : undefined;
|
||||
case "text":
|
||||
return item.text;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
return names;
|
||||
};
|
@@ -1,13 +1,12 @@
|
||||
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
computeEntityNameDisplay,
|
||||
type EntityNameItem,
|
||||
type EntityNameOptions,
|
||||
} from "../entity/compute_entity_name_display";
|
||||
import type { LocalizeFunc } from "./localize";
|
||||
import { computeEntityName } from "../entity/compute_entity_name";
|
||||
import { computeDeviceName } from "../entity/compute_device_name";
|
||||
import { getEntityContext } from "../entity/context/get_entity_context";
|
||||
import { computeAreaName } from "../entity/compute_area_name";
|
||||
import { computeFloorName } from "../entity/compute_floor_name";
|
||||
import { ensureArray } from "../array/ensure-array";
|
||||
|
||||
export type FormatEntityStateFunc = (
|
||||
stateObj: HassEntity,
|
||||
@@ -27,8 +26,8 @@ export type EntityNameType = "entity" | "device" | "area" | "floor";
|
||||
|
||||
export type FormatEntityNameFunc = (
|
||||
stateObj: HassEntity,
|
||||
type: EntityNameType | EntityNameType[],
|
||||
separator?: string
|
||||
name: EntityNameItem | EntityNameItem[],
|
||||
options?: EntityNameOptions
|
||||
) => string;
|
||||
|
||||
export const computeFormatFunctions = async (
|
||||
@@ -75,45 +74,15 @@ export const computeFormatFunctions = async (
|
||||
),
|
||||
formatEntityAttributeName: (stateObj, attribute) =>
|
||||
computeAttributeNameDisplay(localize, stateObj, entities, attribute),
|
||||
formatEntityName: (stateObj, type, separator = " ") => {
|
||||
const types = ensureArray(type);
|
||||
const namesList: (string | undefined)[] = [];
|
||||
|
||||
const { device, area, floor } = getEntityContext(
|
||||
formatEntityName: (stateObj, name, options) =>
|
||||
computeEntityNameDisplay(
|
||||
stateObj,
|
||||
name,
|
||||
entities,
|
||||
devices,
|
||||
areas,
|
||||
floors
|
||||
);
|
||||
|
||||
for (const t of types) {
|
||||
switch (t) {
|
||||
case "entity": {
|
||||
namesList.push(computeEntityName(stateObj, entities, devices));
|
||||
break;
|
||||
}
|
||||
case "device": {
|
||||
if (device) {
|
||||
namesList.push(computeDeviceName(device));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "area": {
|
||||
if (area) {
|
||||
namesList.push(computeAreaName(area));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "floor": {
|
||||
if (floor) {
|
||||
namesList.push(computeFloorName(floor));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return namesList.filter((name) => name !== undefined).join(separator);
|
||||
},
|
||||
floors,
|
||||
options
|
||||
),
|
||||
};
|
||||
};
|
||||
|
8
src/common/util/xss.ts
Normal file
8
src/common/util/xss.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import xss from "xss";
|
||||
|
||||
export const filterXSS = (html: string) =>
|
||||
xss(html, {
|
||||
whiteList: {},
|
||||
stripIgnoreTag: true,
|
||||
stripIgnoreTagBody: true,
|
||||
});
|
@@ -27,6 +27,7 @@ import type { HomeAssistant } from "../../types";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../ha-icon-button";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import { formatTimeLabel } from "./axis-label";
|
||||
import { downSampleLineData } from "./down-sample";
|
||||
|
||||
@@ -811,7 +812,8 @@ export class HaChartBase extends LitElement {
|
||||
};
|
||||
}
|
||||
}
|
||||
return { ...s, data };
|
||||
const name = filterXSS(String(s.name ?? s.id ?? ""));
|
||||
return { ...s, name, data };
|
||||
});
|
||||
return series as ECOption["series"];
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ECOption } from "../../resources/echarts";
|
||||
import { measureTextWidth } from "../../util/text";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import "./ha-chart-base";
|
||||
import { NODE_SIZE } from "../trace/hat-graph-const";
|
||||
import "../ha-alert";
|
||||
@@ -92,12 +93,12 @@ export class HaSankeyChart extends LitElement {
|
||||
: data.value;
|
||||
if (data.id) {
|
||||
const node = this.data.nodes.find((n) => n.id === data.id);
|
||||
return `${params.marker} ${node?.label ?? data.id}<br>${value}`;
|
||||
return `${params.marker} ${filterXSS(node?.label ?? data.id)}<br>${value}`;
|
||||
}
|
||||
if (data.source && data.target) {
|
||||
const source = this.data.nodes.find((n) => n.id === data.source);
|
||||
const target = this.data.nodes.find((n) => n.id === data.target);
|
||||
return `${source?.label ?? data.source} → ${target?.label ?? data.target}<br>${value}`;
|
||||
return `${filterXSS(source?.label ?? data.source)} → ${filterXSS(target?.label ?? data.target)}<br>${value}`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
@@ -1,6 +1,9 @@
|
||||
import { expose } from "comlink";
|
||||
import { stringCompare, ipCompare } from "../../common/string/compare";
|
||||
import Fuse from "fuse.js";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ipCompare, stringCompare } from "../../common/string/compare";
|
||||
import { stripDiacritics } from "../../common/string/strip-diacritics";
|
||||
import { HaFuse } from "../../resources/fuse";
|
||||
import type {
|
||||
ClonedDataTableColumnData,
|
||||
DataTableRowData,
|
||||
@@ -8,29 +11,48 @@ import type {
|
||||
SortingDirection,
|
||||
} from "./ha-data-table";
|
||||
|
||||
const fuseIndex = memoizeOne(
|
||||
(data: DataTableRowData[], columns: SortableColumnContainer) => {
|
||||
const searchKeys = new Set<string>();
|
||||
Object.entries(columns).forEach(([key, column]) => {
|
||||
if (column.filterable) {
|
||||
searchKeys.add(
|
||||
column.filterKey
|
||||
? `${column.valueColumn || key}.${column.filterKey}`
|
||||
: key
|
||||
);
|
||||
}
|
||||
});
|
||||
return Fuse.createIndex([...searchKeys], data);
|
||||
}
|
||||
);
|
||||
|
||||
const filterData = (
|
||||
data: DataTableRowData[],
|
||||
columns: SortableColumnContainer,
|
||||
filter: string
|
||||
) => {
|
||||
filter = stripDiacritics(filter.toLowerCase());
|
||||
return data.filter((row) =>
|
||||
Object.entries(columns).some((columnEntry) => {
|
||||
const [key, column] = columnEntry;
|
||||
if (column.filterable) {
|
||||
const value = String(
|
||||
column.filterKey
|
||||
? row[column.valueColumn || key][column.filterKey]
|
||||
: row[column.valueColumn || key]
|
||||
);
|
||||
|
||||
if (stripDiacritics(value).toLowerCase().includes(filter)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})
|
||||
if (filter === "") {
|
||||
return data;
|
||||
}
|
||||
|
||||
const index = fuseIndex(data, columns);
|
||||
|
||||
const fuse = new HaFuse(
|
||||
data,
|
||||
{ shouldSort: false, minMatchCharLength: 1 },
|
||||
index
|
||||
);
|
||||
|
||||
const searchResults = fuse.multiTermsSearch(filter);
|
||||
|
||||
if (searchResults) {
|
||||
return searchResults.map((result) => result.item);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const sortData = (
|
||||
|
493
src/components/entity/ha-entity-name-picker.ts
Normal file
493
src/components/entity/ha-entity-name-picker.ts
Normal file
@@ -0,0 +1,493 @@
|
||||
import "@material/mwc-menu/mwc-menu-surface";
|
||||
import { mdiDrag, mdiPlus } from "@mdi/js";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import type { IFuseOptions } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import type { EntityNameItem } from "../../common/entity/compute_entity_name_display";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import type { EntityNameType } from "../../common/translations/entity-state";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
import "../ha-sortable";
|
||||
|
||||
interface EntityNameOption {
|
||||
primary: string;
|
||||
secondary?: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<EntityNameOption> = (item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
const KNOWN_TYPES = new Set(["entity", "device", "area", "floor"]);
|
||||
|
||||
const UNIQUE_TYPES = new Set(["entity", "device", "area", "floor"]);
|
||||
|
||||
@customElement("ha-entity-name-picker")
|
||||
export class HaEntityNamePicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entityId?: string;
|
||||
|
||||
@property({ attribute: false }) public value?:
|
||||
| string
|
||||
| EntityNameItem
|
||||
| EntityNameItem[];
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@query(".container", true) private _container?: HTMLDivElement;
|
||||
|
||||
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
private _editIndex?: number;
|
||||
|
||||
private _validOptions = memoizeOne((entityId?: string) => {
|
||||
const options = new Set<string>();
|
||||
if (!entityId) {
|
||||
return options;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[entityId];
|
||||
|
||||
if (!stateObj) {
|
||||
return options;
|
||||
}
|
||||
|
||||
options.add("entity");
|
||||
|
||||
const context = getEntityContext(
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
if (context.device) options.add("device");
|
||||
if (context.area) options.add("area");
|
||||
if (context.floor) options.add("floor");
|
||||
return options;
|
||||
});
|
||||
|
||||
private _getOptions = memoizeOne((entityId?: string) => {
|
||||
if (!entityId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const options = this._validOptions(entityId);
|
||||
|
||||
const items = (
|
||||
["entity", "device", "area", "floor"] as const
|
||||
).map<EntityNameOption>((name) => {
|
||||
const stateObj = this.hass.states[entityId];
|
||||
const isValid = options.has(name);
|
||||
const primary = this.hass.localize(
|
||||
`ui.components.entity.entity-name-picker.types.${name}`
|
||||
);
|
||||
const secondary =
|
||||
stateObj && isValid
|
||||
? this.hass.formatEntityName(stateObj, { type: name })
|
||||
: this.hass.localize(
|
||||
`ui.components.entity.entity-name-picker.types.${name}_missing` as LocalizeKeys
|
||||
) || "-";
|
||||
|
||||
return {
|
||||
primary,
|
||||
secondary,
|
||||
value: name,
|
||||
};
|
||||
});
|
||||
|
||||
return items;
|
||||
});
|
||||
|
||||
private _formatItem = (item: EntityNameItem) => {
|
||||
if (item.type === "text") {
|
||||
return `"${item.text}"`;
|
||||
}
|
||||
if (KNOWN_TYPES.has(item.type)) {
|
||||
return this.hass.localize(
|
||||
`ui.components.entity.entity-name-picker.types.${item.type as EntityNameType}`
|
||||
);
|
||||
}
|
||||
return item.type;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
const value = this._value;
|
||||
const options = this._getOptions(this.entityId);
|
||||
const validOptions = this._validOptions(this.entityId);
|
||||
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<div class="container">
|
||||
<ha-sortable
|
||||
no-style
|
||||
@item-moved=${this._moveItem}
|
||||
.disabled=${this.disabled}
|
||||
handle-selector="button.primary.action"
|
||||
filter=".add"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
this._value,
|
||||
(item) => item,
|
||||
(item: EntityNameItem, idx) => {
|
||||
const label = this._formatItem(item);
|
||||
const isValid =
|
||||
item.type === "text" || validOptions.has(item.type);
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
@click=${this._editItem}
|
||||
.label=${label}
|
||||
.selected=${!this.disabled}
|
||||
.disabled=${this.disabled}
|
||||
class=${!isValid ? "invalid" : ""}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiDrag}></ha-svg-icon>
|
||||
<span>${label}</span>
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
${this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.add"
|
||||
)}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
|
||||
<mwc-menu-surface
|
||||
.open=${this._opened}
|
||||
@closed=${this._onClosed}
|
||||
@opened=${this._onOpened}
|
||||
@input=${stopPropagation}
|
||||
.anchor=${this._container}
|
||||
>
|
||||
<ha-combo-box
|
||||
.hass=${this.hass}
|
||||
.value=${""}
|
||||
.autofocus=${this.autofocus}
|
||||
.disabled=${this.disabled || !this.entityId}
|
||||
.required=${this.required && !value.length}
|
||||
.helper=${this.helper}
|
||||
.items=${options}
|
||||
allow-custom-value
|
||||
item-id-path="value"
|
||||
item-value-path="value"
|
||||
item-label-path="primary"
|
||||
.renderer=${rowRenderer}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._comboBoxValueChanged}
|
||||
@filter-changed=${this._filterChanged}
|
||||
>
|
||||
</ha-combo-box>
|
||||
</mwc-menu-surface>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _onClosed(ev) {
|
||||
ev.stopPropagation();
|
||||
this._opened = false;
|
||||
this._editIndex = undefined;
|
||||
}
|
||||
|
||||
private async _onOpened(ev) {
|
||||
if (!this._opened) {
|
||||
return;
|
||||
}
|
||||
ev.stopPropagation();
|
||||
this._opened = true;
|
||||
await this._comboBox?.focus();
|
||||
await this._comboBox?.open();
|
||||
}
|
||||
|
||||
private async _addItem(ev) {
|
||||
ev.stopPropagation();
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
private async _editItem(ev) {
|
||||
ev.stopPropagation();
|
||||
const idx = parseInt(ev.currentTarget.dataset.idx, 10);
|
||||
this._editIndex = idx;
|
||||
this._opened = true;
|
||||
}
|
||||
|
||||
private get _value(): EntityNameItem[] {
|
||||
return this._toItems(this.value);
|
||||
}
|
||||
|
||||
private _toItems = memoizeOne((value?: typeof this.value) => {
|
||||
if (typeof value === "string") {
|
||||
return [{ type: "text", text: value } as const];
|
||||
}
|
||||
return value ? ensureArray(value) : [];
|
||||
});
|
||||
|
||||
private _toValue = memoizeOne(
|
||||
(items: EntityNameItem[]): typeof this.value => {
|
||||
if (items.length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (items.length === 1) {
|
||||
const item = items[0];
|
||||
return item.type === "text" ? item.text : item;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||
const open = ev.detail.value;
|
||||
if (open) {
|
||||
const options = this._comboBox.items || [];
|
||||
|
||||
const initialItem =
|
||||
this._editIndex != null ? this._value[this._editIndex] : undefined;
|
||||
|
||||
const initialValue = initialItem
|
||||
? initialItem.type === "text"
|
||||
? initialItem.text
|
||||
: initialItem.type
|
||||
: "";
|
||||
|
||||
const filteredItems = this._filterSelectedOptions(options, initialValue);
|
||||
|
||||
this._comboBox.filteredItems = filteredItems;
|
||||
this._comboBox.setInputValue(initialValue);
|
||||
} else {
|
||||
this._opened = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _filterSelectedOptions = (
|
||||
options: EntityNameOption[],
|
||||
current?: string
|
||||
) => {
|
||||
const value = this._value;
|
||||
|
||||
const types = value.map((item) => item.type) as string[];
|
||||
|
||||
const filteredOptions = options.filter(
|
||||
(option) =>
|
||||
!UNIQUE_TYPES.has(option.value) ||
|
||||
!types.includes(option.value) ||
|
||||
option.value === current
|
||||
);
|
||||
return filteredOptions;
|
||||
};
|
||||
|
||||
private _filterChanged(ev: ValueChangedEvent<string>) {
|
||||
const input = ev.detail.value;
|
||||
const filter = input?.toLowerCase() || "";
|
||||
const options = this._comboBox.items || [];
|
||||
|
||||
const currentItem =
|
||||
this._editIndex != null ? this._value[this._editIndex] : undefined;
|
||||
|
||||
const currentValue = currentItem
|
||||
? currentItem.type === "text"
|
||||
? currentItem.text
|
||||
: currentItem.type
|
||||
: "";
|
||||
|
||||
this._comboBox.filteredItems = this._filterSelectedOptions(
|
||||
options,
|
||||
currentValue
|
||||
);
|
||||
|
||||
if (!filter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fuseOptions: IFuseOptions<EntityNameOption> = {
|
||||
keys: ["primary", "secondary", "value"],
|
||||
isCaseSensitive: false,
|
||||
minMatchCharLength: Math.min(filter.length, 2),
|
||||
threshold: 0.2,
|
||||
ignoreDiacritics: true,
|
||||
};
|
||||
|
||||
const fuse = new Fuse(this._comboBox.filteredItems, fuseOptions);
|
||||
const filteredItems = fuse.search(filter).map((result) => result.item);
|
||||
|
||||
this._comboBox.filteredItems = filteredItems;
|
||||
}
|
||||
|
||||
private async _moveItem(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const value = this._value;
|
||||
const newValue = value.concat();
|
||||
const element = newValue.splice(oldIndex, 1)[0];
|
||||
newValue.splice(newIndex, 0, element);
|
||||
this._setValue(newValue);
|
||||
await this.updateComplete;
|
||||
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
|
||||
}
|
||||
|
||||
private async _removeItem(ev) {
|
||||
ev.stopPropagation();
|
||||
const value = [...this._value];
|
||||
const idx = parseInt(ev.target.dataset.idx, 10);
|
||||
value.splice(idx, 1);
|
||||
this._setValue(value);
|
||||
await this.updateComplete;
|
||||
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
|
||||
}
|
||||
|
||||
private _comboBoxValueChanged(ev: ValueChangedEvent<string>): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (this.disabled || value === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
const item: EntityNameItem = KNOWN_TYPES.has(value as any)
|
||||
? { type: value as EntityNameType }
|
||||
: { type: "text", text: value };
|
||||
|
||||
const newValue = [...this._value];
|
||||
|
||||
if (this._editIndex != null) {
|
||||
newValue[this._editIndex] = item;
|
||||
} else {
|
||||
newValue.push(item);
|
||||
}
|
||||
|
||||
this._setValue(newValue);
|
||||
}
|
||||
|
||||
private _setValue(value: EntityNameItem[]) {
|
||||
const newValue = this._toValue(value);
|
||||
this.value = newValue;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
border-end-end-radius: var(--ha-border-radius-square);
|
||||
border-end-start-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
.container:after {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background-color: var(
|
||||
--mdc-text-field-idle-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
transform:
|
||||
height 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
}
|
||||
:host([disabled]) .container:after {
|
||||
background-color: var(
|
||||
--mdc-text-field-disabled-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
}
|
||||
.container:focus-within:after {
|
||||
height: 2px;
|
||||
background-color: var(--mdc-theme-primary);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin: 0 0 var(--ha-space-2);
|
||||
}
|
||||
|
||||
.add {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
mwc-menu-surface {
|
||||
--mdc-menu-min-width: 100%;
|
||||
}
|
||||
|
||||
ha-chip-set {
|
||||
padding: var(--ha-space-2) var(--ha-space-2);
|
||||
}
|
||||
|
||||
.invalid {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.sortable-fallback {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.sortable-drag {
|
||||
cursor: grabbing;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-entity-name-picker": HaEntityNamePicker;
|
||||
}
|
||||
}
|
@@ -6,6 +6,7 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
@@ -144,9 +145,14 @@ export class HaEntityPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
const entityName = this.hass.formatEntityName(stateObj, "entity");
|
||||
const deviceName = this.hass.formatEntityName(stateObj, "device");
|
||||
const areaName = this.hass.formatEntityName(stateObj, "area");
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
@@ -300,21 +306,24 @@ export class HaEntityPicker extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isRTL = computeRTL(hass);
|
||||
|
||||
items = entityIds.map<EntityComboBoxItem>((entityId) => {
|
||||
const stateObj = hass!.states[entityId];
|
||||
const stateObj = hass.states[entityId];
|
||||
|
||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||
const entityName = this.hass.formatEntityName(stateObj, "entity");
|
||||
const deviceName = this.hass.formatEntityName(stateObj, "device");
|
||||
const areaName = this.hass.formatEntityName(stateObj, "area");
|
||||
|
||||
const domainName = domainToName(
|
||||
this.hass.localize,
|
||||
computeDomain(entityId)
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
const domainName = domainToName(hass.localize, computeDomain(entityId));
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
|
@@ -6,6 +6,7 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { domainToName } from "../../data/integration";
|
||||
@@ -199,7 +200,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isRTL = computeRTL(hass);
|
||||
|
||||
const output: StatisticComboBoxItem[] = [];
|
||||
|
||||
@@ -256,9 +257,15 @@ export class HaStatisticPicker extends LitElement {
|
||||
const id = meta.statistic_id;
|
||||
|
||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||
const entityName = hass.formatEntityName(stateObj, "entity");
|
||||
const deviceName = hass.formatEntityName(stateObj, "device");
|
||||
const areaName = hass.formatEntityName(stateObj, "area");
|
||||
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
const primary = entityName || deviceName || id;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
@@ -331,9 +338,14 @@ export class HaStatisticPicker extends LitElement {
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
|
||||
if (stateObj) {
|
||||
const entityName = this.hass.formatEntityName(stateObj, "entity");
|
||||
const deviceName = this.hass.formatEntityName(stateObj, "device");
|
||||
const areaName = this.hass.formatEntityName(stateObj, "area");
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
|
@@ -41,8 +41,7 @@ export class HaButton extends Button {
|
||||
return [
|
||||
Button.styles,
|
||||
css`
|
||||
.button {
|
||||
/* set theme vars */
|
||||
:host {
|
||||
--wa-form-control-padding-inline: 16px;
|
||||
--wa-font-weight-action: var(--ha-font-weight-medium);
|
||||
--wa-form-control-border-radius: var(
|
||||
@@ -54,7 +53,8 @@ export class HaButton extends Button {
|
||||
--ha-button-height,
|
||||
var(--button-height, 40px)
|
||||
);
|
||||
|
||||
}
|
||||
.button {
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: 1;
|
||||
|
||||
@@ -223,6 +223,12 @@ export class HaButton extends Button {
|
||||
.button.has-end {
|
||||
padding-inline-end: 8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: var(--ha-space-1) 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
52
src/components/ha-dialog-footer.ts
Normal file
52
src/components/ha-dialog-footer.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
/**
|
||||
* Home Assistant dialog footer component
|
||||
*
|
||||
* @element ha-dialog-footer
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* A simple footer container for dialog actions,
|
||||
* typically used as the `footer` slot in `ha-wa-dialog`.
|
||||
*
|
||||
* @slot primaryAction - Primary action button(s), aligned to the end.
|
||||
* @slot secondaryAction - Secondary action button(s), placed before the primary action.
|
||||
*
|
||||
* @remarks
|
||||
* **Button Styling Guidance:**
|
||||
* - `primaryAction` slot: Use `variant="accent"`
|
||||
* - `secondaryAction` slot: Use `variant="plain"`
|
||||
*/
|
||||
@customElement("ha-dialog-footer")
|
||||
export class HaDialogFooter extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<footer>
|
||||
<slot name="secondaryAction"></slot>
|
||||
<slot name="primaryAction"></slot>
|
||||
</footer>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
css`
|
||||
footer {
|
||||
display: flex;
|
||||
gap: var(--ha-space-3);
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-dialog-footer": HaDialogFooter;
|
||||
}
|
||||
}
|
@@ -1,9 +1,20 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-dialog-header")
|
||||
export class HaDialogHeader extends LitElement {
|
||||
@property({ type: String, attribute: "subtitle-position" })
|
||||
public subtitlePosition: "above" | "below" = "below";
|
||||
|
||||
protected render() {
|
||||
const titleSlot = html`<div class="header-title">
|
||||
<slot name="title"></slot>
|
||||
</div>`;
|
||||
|
||||
const subtitleSlot = html`<div class="header-subtitle">
|
||||
<slot name="subtitle"></slot>
|
||||
</div>`;
|
||||
|
||||
return html`
|
||||
<header class="header">
|
||||
<div class="header-bar">
|
||||
@@ -11,12 +22,9 @@ export class HaDialogHeader extends LitElement {
|
||||
<slot name="navigationIcon"></slot>
|
||||
</section>
|
||||
<section class="header-content">
|
||||
<div class="header-title">
|
||||
<slot name="title"></slot>
|
||||
</div>
|
||||
<div class="header-subtitle">
|
||||
<slot name="subtitle"></slot>
|
||||
</div>
|
||||
${this.subtitlePosition === "above"
|
||||
? html`${subtitleSlot}${titleSlot}`
|
||||
: html`${titleSlot}${subtitleSlot}`}
|
||||
</section>
|
||||
<section class="header-action-items">
|
||||
<slot name="actionItems"></slot>
|
||||
@@ -40,7 +48,7 @@ export class HaDialogHeader extends LitElement {
|
||||
.header-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -53,13 +61,17 @@ export class HaDialogHeader extends LitElement {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.header-title {
|
||||
height: var(
|
||||
--ha-dialog-header-title-height,
|
||||
calc(var(--ha-font-size-xl) + 4px)
|
||||
);
|
||||
font-size: var(--ha-font-size-xl);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
}
|
||||
.header-subtitle {
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: 20px;
|
||||
line-height: var(--ha-line-height-normal);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
@media all and (min-width: 450px) and (min-height: 500px) {
|
||||
|
@@ -121,7 +121,7 @@ export class HaDialog extends DialogBase {
|
||||
position: var(--dialog-surface-position, relative);
|
||||
top: var(--dialog-surface-top);
|
||||
margin-top: var(--dialog-surface-margin-top);
|
||||
min-width: var(--mdc-dialog-min-width, 100vw);
|
||||
min-width: var(--mdc-dialog-min-width, auto);
|
||||
min-height: var(--mdc-dialog-min-height, auto);
|
||||
border-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
@@ -133,25 +133,13 @@ export class HaDialog extends DialogBase {
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
padding: var(--dialog-surface-padding);
|
||||
}
|
||||
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.mdc-dialog .mdc-dialog__surface {
|
||||
min-height: 100vh;
|
||||
min-height: 100svh;
|
||||
max-height: 100vh;
|
||||
max-height: 100svh;
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
}
|
||||
|
||||
.header_title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@@ -61,6 +61,7 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
.required=${this.schema.required}
|
||||
.autoValidate=${this.schema.required}
|
||||
.name=${this.schema.name}
|
||||
.autofocus=${this.schema.autofocus}
|
||||
.autocomplete=${this.schema.autocomplete}
|
||||
.suffix=${this.isPassword
|
||||
? // reserve some space for the icon.
|
||||
|
@@ -105,6 +105,11 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
}
|
||||
}
|
||||
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="root" part="root">
|
||||
|
@@ -39,6 +39,15 @@ export class HaPictureUpload extends LitElement {
|
||||
@property({ type: Boolean, attribute: "select-media" }) public selectMedia =
|
||||
false;
|
||||
|
||||
// This property is set when this component is used inside a media selector.
|
||||
// When set, it returns selected media or uploaded files as MediaSelectorValue
|
||||
// When unset, it only allows selecting images from image-upload, and returns
|
||||
// selected or uploaded images as a string starting with /api/...
|
||||
@property({ type: Boolean, attribute: "full-media" }) public fullMedia =
|
||||
false;
|
||||
|
||||
@property({ attribute: false }) public contentIdHelper?: string;
|
||||
|
||||
@property({ attribute: false }) public cropOptions?: CropOptions;
|
||||
|
||||
@property({ type: Boolean }) public original = false;
|
||||
@@ -164,12 +173,33 @@ export class HaPictureUpload extends LitElement {
|
||||
this._uploading = true;
|
||||
try {
|
||||
const media = await createImage(this.hass, file);
|
||||
this.value = generateImageThumbnailUrl(
|
||||
media.id,
|
||||
this.size,
|
||||
this.original
|
||||
);
|
||||
fireEvent(this, "change");
|
||||
if (this.fullMedia) {
|
||||
const item = {
|
||||
media_content_id: `${MEDIA_PREFIX}/${media.id}`,
|
||||
media_content_type: media.content_type,
|
||||
title: media.name,
|
||||
media_class: "image" as const,
|
||||
can_play: true,
|
||||
can_expand: false,
|
||||
can_search: false,
|
||||
thumbnail: generateImageThumbnailUrl(media.id, 256),
|
||||
} as const;
|
||||
const navigateIds = [
|
||||
{},
|
||||
{ media_content_type: "app", media_content_id: MEDIA_PREFIX },
|
||||
];
|
||||
fireEvent(this, "media-picked", {
|
||||
item,
|
||||
navigateIds,
|
||||
});
|
||||
} else {
|
||||
this.value = generateImageThumbnailUrl(
|
||||
media.id,
|
||||
this.size,
|
||||
this.original
|
||||
);
|
||||
fireEvent(this, "change");
|
||||
}
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
text: err.toString(),
|
||||
@@ -183,15 +213,24 @@ export class HaPictureUpload extends LitElement {
|
||||
showMediaBrowserDialog(this, {
|
||||
action: "pick",
|
||||
entityId: "browser",
|
||||
navigateIds: [
|
||||
{ media_content_id: undefined, media_content_type: undefined },
|
||||
{
|
||||
media_content_id: MEDIA_PREFIX,
|
||||
media_content_type: "app",
|
||||
},
|
||||
],
|
||||
minimumNavigateLevel: 2,
|
||||
accept: ["image/*"],
|
||||
navigateIds: this.fullMedia
|
||||
? undefined
|
||||
: [
|
||||
{ media_content_id: undefined, media_content_type: undefined },
|
||||
{
|
||||
media_content_id: MEDIA_PREFIX,
|
||||
media_content_type: "app",
|
||||
},
|
||||
],
|
||||
minimumNavigateLevel: this.fullMedia ? undefined : 2,
|
||||
hideContentType: true,
|
||||
contentIdHelper: this.contentIdHelper,
|
||||
mediaPickedCallback: async (pickedMedia: MediaPickedEvent) => {
|
||||
if (this.fullMedia) {
|
||||
fireEvent(this, "media-picked", pickedMedia);
|
||||
return;
|
||||
}
|
||||
const mediaId = getIdFromUrl(pickedMedia.item.media_content_id);
|
||||
if (mediaId) {
|
||||
if (this.crop) {
|
||||
|
50
src/components/ha-selector/ha-selector-entity-name.ts
Normal file
50
src/components/ha-selector/ha-selector-entity-name.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { EntityNameSelector } from "../../data/selector";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entity-name-picker";
|
||||
|
||||
@customElement("ha-selector-entity_name")
|
||||
export class HaSelectorEntityName extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: EntityNameSelector;
|
||||
|
||||
@property() public value?: string | string[];
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@property({ attribute: false }) public context?: {
|
||||
entity?: string;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
const value = this.value ?? this.selector.entity_name?.default_name;
|
||||
|
||||
return html`
|
||||
<ha-entity-name-picker
|
||||
.hass=${this.hass}
|
||||
.entityId=${this.selector.entity_name?.entity_id ||
|
||||
this.context?.entity}
|
||||
.value=${value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-entity-name-picker>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-entity_name": HaSelectorEntityName;
|
||||
}
|
||||
}
|
@@ -19,6 +19,7 @@ import "../ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../ha-form/types";
|
||||
import { showMediaBrowserDialog } from "../media-player/show-media-browser-dialog";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import "../ha-picture-upload";
|
||||
|
||||
const MANUAL_SCHEMA = [
|
||||
{ name: "media_content_id", required: false, selector: { text: {} } },
|
||||
@@ -105,6 +106,17 @@ export class HaMediaSelector extends LitElement {
|
||||
(stateObj &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA));
|
||||
|
||||
if (this.selector.media?.image_upload && !this.value) {
|
||||
return html`<ha-picture-upload
|
||||
.hass=${this.hass}
|
||||
.value=${null}
|
||||
.contentIdHelper=${this.selector.media?.content_id_helper}
|
||||
select-media
|
||||
full-media
|
||||
@media-picked=${this._pictureUploadMediaPicked}
|
||||
></ha-picture-upload>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this._hasAccept ||
|
||||
(this._contextEntities && this._contextEntities.length <= 1)
|
||||
@@ -142,8 +154,7 @@ export class HaMediaSelector extends LitElement {
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
></ha-form>
|
||||
`
|
||||
: html`
|
||||
<ha-card
|
||||
: html`<ha-card
|
||||
outlined
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@@ -203,7 +214,20 @@ export class HaMediaSelector extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`}
|
||||
${this.selector.media?.clearable
|
||||
? html`<div>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
variant="danger"
|
||||
@click=${this._clearValue}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.components.picture-upload.clear_picture"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>`
|
||||
: nothing}`}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -248,6 +272,8 @@ export class HaMediaSelector extends LitElement {
|
||||
accept: this.selector.media?.accept,
|
||||
defaultId: this.value?.media_content_id,
|
||||
defaultType: this.value?.media_content_type,
|
||||
hideContentType: this.selector.media?.hide_content_type,
|
||||
contentIdHelper: this.selector.media?.content_id_helper,
|
||||
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
@@ -289,6 +315,31 @@ export class HaMediaSelector extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _pictureUploadMediaPicked(ev) {
|
||||
const pickedMedia = ev.detail as MediaPickedEvent;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.value,
|
||||
media_content_id: pickedMedia.item.media_content_id,
|
||||
media_content_type: pickedMedia.item.media_content_type,
|
||||
metadata: {
|
||||
title: pickedMedia.item.title,
|
||||
thumbnail: pickedMedia.item.thumbnail,
|
||||
media_class: pickedMedia.item.media_class,
|
||||
children_media_class: pickedMedia.item.children_media_class,
|
||||
navigateIds: pickedMedia.navigateIds?.map((id) => ({
|
||||
media_content_type: id.media_content_type,
|
||||
media_content_id: id.media_content_id,
|
||||
})),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _clearValue() {
|
||||
fireEvent(this, "value-changed", { value: undefined });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-entity-picker {
|
||||
display: block;
|
||||
|
@@ -29,6 +29,7 @@ const LOAD_ELEMENTS = {
|
||||
device: () => import("./ha-selector-device"),
|
||||
duration: () => import("./ha-selector-duration"),
|
||||
entity: () => import("./ha-selector-entity"),
|
||||
entity_name: () => import("./ha-selector-entity-name"),
|
||||
statistic: () => import("./ha-selector-statistic"),
|
||||
file: () => import("./ha-selector-file"),
|
||||
floor: () => import("./ha-selector-floor"),
|
||||
|
@@ -857,7 +857,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 8px;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
line-height: normal;
|
||||
background-color: var(--accent-color);
|
||||
|
@@ -17,7 +17,7 @@ export class HaTooltip extends Tooltip {
|
||||
css`
|
||||
:host {
|
||||
--wa-tooltip-background-color: var(--secondary-background-color);
|
||||
--wa-tooltip-color: var(--primary-text-color);
|
||||
--wa-tooltip-content-color: var(--primary-text-color);
|
||||
--wa-tooltip-font-family: var(
|
||||
--ha-tooltip-font-family,
|
||||
var(--ha-font-family-body)
|
||||
|
328
src/components/ha-wa-dialog.ts
Normal file
328
src/components/ha-wa-dialog.ts
Normal file
@@ -0,0 +1,328 @@
|
||||
import "@home-assistant/webawesome/dist/components/dialog/dialog";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
|
||||
export type DialogWidth = "small" | "medium" | "large" | "full";
|
||||
|
||||
/**
|
||||
* Home Assistant dialog component
|
||||
*
|
||||
* @element ha-wa-dialog
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* A stylable dialog built using the `wa-dialog` component, providing a standardized header (ha-dialog-header),
|
||||
* body, and footer (preferably using `ha-dialog-footer`) with slots
|
||||
*
|
||||
* You can open and close the dialog declaratively by using the `data-dialog="close"` attribute.
|
||||
* @see https://webawesome.com/docs/components/dialog/#opening-and-closing-dialogs-declaratively
|
||||
*
|
||||
* @slot header - Replace the entire header area.
|
||||
* @slot headerNavigationIcon - Leading header action (e.g. close/back button).
|
||||
* @slot headerActionItems - Trailing header actions (e.g. buttons, menus).
|
||||
* @slot - Dialog content body.
|
||||
* @slot footer - Dialog footer content.
|
||||
*
|
||||
* @csspart dialog - The dialog surface.
|
||||
* @csspart header - The header container.
|
||||
* @csspart body - The scrollable body container.
|
||||
* @csspart footer - The footer container.
|
||||
*
|
||||
* @cssprop --dialog-content-padding - Padding for the dialog content sections.
|
||||
* @cssprop --ha-dialog-show-duration - Show animation duration.
|
||||
* @cssprop --ha-dialog-hide-duration - Hide animation duration.
|
||||
* @cssprop --ha-dialog-surface-background - Dialog background color.
|
||||
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface.
|
||||
* @cssprop --dialog-z-index - Z-index for the dialog.
|
||||
* @cssprop --dialog-surface-position - CSS position of the dialog surface.
|
||||
* @cssprop --dialog-surface-margin-top - Top margin for the dialog surface.
|
||||
*
|
||||
* @attr {boolean} open - Controls the dialog open state.
|
||||
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset. Defaults to "medium".
|
||||
* @attr {boolean} prevent-scrim-close - Prevents closing the dialog by clicking the scrim/overlay. Defaults to false.
|
||||
* @attr {string} header-title - Header title text when no custom title slot is provided.
|
||||
* @attr {string} header-subtitle - Header subtitle text when no custom subtitle slot is provided.
|
||||
* @attr {("above"|"below")} header-subtitle-position - Position of the subtitle relative to the title. Defaults to "below".
|
||||
* @attr {boolean} flexcontent - Makes the dialog body a flex container for flexible layouts.
|
||||
*
|
||||
* @event opened - Fired when the dialog is shown.
|
||||
* @event closed - Fired after the dialog is hidden.
|
||||
*
|
||||
* @remarks
|
||||
* **Focus Management:**
|
||||
* To automatically focus an element when the dialog opens, add the `autofocus` attribute to it.
|
||||
* Components with `delegatesFocus: true` (like `ha-form`) will forward focus to their first focusable child.
|
||||
* Example: `<ha-form autofocus .schema=${schema}></ha-form>`
|
||||
*
|
||||
* @see https://github.com/home-assistant/frontend/issues/27143
|
||||
*/
|
||||
@customElement("ha-wa-dialog")
|
||||
export class HaWaDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public open = false;
|
||||
|
||||
@property({ type: String, reflect: true, attribute: "width" })
|
||||
public width: DialogWidth = "medium";
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "prevent-scrim-close" })
|
||||
public preventScrimClose = false;
|
||||
|
||||
@property({ type: String, attribute: "header-title" })
|
||||
public headerTitle = "";
|
||||
|
||||
@property({ type: String, attribute: "header-subtitle" })
|
||||
public headerSubtitle = "";
|
||||
|
||||
@property({ type: String, attribute: "header-subtitle-position" })
|
||||
public headerSubtitlePosition: "above" | "below" = "below";
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "flexcontent" })
|
||||
public flexContent = false;
|
||||
|
||||
@state()
|
||||
private _open = false;
|
||||
|
||||
@query(".body") public bodyContainer!: HTMLDivElement;
|
||||
|
||||
protected updated(
|
||||
changedProperties: Map<string | number | symbol, unknown>
|
||||
): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
if (changedProperties.has("open")) {
|
||||
this._open = this.open;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<wa-dialog
|
||||
.open=${this._open}
|
||||
.lightDismiss=${!this.preventScrimClose}
|
||||
without-header
|
||||
@wa-show=${this._handleShow}
|
||||
@wa-after-show=${this._handleAfterShow}
|
||||
@wa-after-hide=${this._handleAfterHide}
|
||||
>
|
||||
<slot name="header">
|
||||
<ha-dialog-header .subtitlePosition=${this.headerSubtitlePosition}>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-dialog="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle
|
||||
? html`<span slot="title" class="title">
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: nothing}
|
||||
${this.headerSubtitle
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: nothing}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
</slot>
|
||||
<div class="body ha-scrollbar">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
</wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleShow = async () => {
|
||||
this._open = true;
|
||||
fireEvent(this, "opened");
|
||||
|
||||
await this.updateComplete;
|
||||
|
||||
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
|
||||
};
|
||||
|
||||
private _handleAfterShow = () => {
|
||||
fireEvent(this, "after-show");
|
||||
};
|
||||
|
||||
private _handleAfterHide = () => {
|
||||
this._open = false;
|
||||
fireEvent(this, "closed");
|
||||
};
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
wa-dialog {
|
||||
--full-width: var(
|
||||
--ha-dialog-width-full,
|
||||
min(
|
||||
95vw,
|
||||
calc(
|
||||
100vw - var(--safe-area-inset-left, var(--ha-space-0)) - var(
|
||||
--safe-area-inset-right,
|
||||
var(--ha-space-0)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
--width: min(var(--ha-dialog-width-md, 580px), var(--full-width));
|
||||
--spacing: var(--dialog-content-padding, var(--ha-space-6));
|
||||
--show-duration: var(--ha-dialog-show-duration, 200ms);
|
||||
--hide-duration: var(--ha-dialog-hide-duration, 200ms);
|
||||
--ha-dialog-surface-background: var(
|
||||
--card-background-color,
|
||||
var(--ha-color-surface-default)
|
||||
);
|
||||
--wa-color-surface-raised: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--card-background-color, var(--ha-color-surface-default))
|
||||
);
|
||||
--wa-panel-border-radius: var(
|
||||
--ha-dialog-border-radius,
|
||||
var(--ha-border-radius-3xl)
|
||||
);
|
||||
max-width: var(--ha-dialog-max-width, 100vw);
|
||||
max-width: var(--ha-dialog-max-width, 100svw);
|
||||
}
|
||||
|
||||
:host([width="small"]) wa-dialog {
|
||||
--width: min(var(--ha-dialog-width-sm, 320px), var(--full-width));
|
||||
}
|
||||
|
||||
:host([width="large"]) wa-dialog {
|
||||
--width: min(var(--ha-dialog-width-lg, 720px), var(--full-width));
|
||||
}
|
||||
|
||||
:host([width="full"]) wa-dialog {
|
||||
--width: var(--full-width);
|
||||
}
|
||||
|
||||
wa-dialog::part(dialog) {
|
||||
min-width: var(--width, var(--full-width));
|
||||
max-width: var(--width, var(--full-width));
|
||||
max-height: var(
|
||||
--ha-dialog-max-height,
|
||||
calc(100% - var(--ha-space-20))
|
||||
);
|
||||
position: var(--dialog-surface-position, relative);
|
||||
margin-top: var(--dialog-surface-margin-top, auto);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
:host {
|
||||
--ha-dialog-border-radius: var(--ha-space-0);
|
||||
}
|
||||
|
||||
wa-dialog {
|
||||
--full-width: var(--ha-dialog-width-full, 100vw);
|
||||
}
|
||||
|
||||
wa-dialog::part(dialog) {
|
||||
min-height: var(--ha-dialog-min-height, 100vh);
|
||||
min-height: var(--ha-dialog-min-height, 100svh);
|
||||
max-height: var(--ha-dialog-max-height, 100vh);
|
||||
max-height: var(--ha-dialog-max-height, 100svh);
|
||||
padding-top: var(--safe-area-inset-top, var(--ha-space-0));
|
||||
padding-bottom: var(--safe-area-inset-bottom, var(--ha-space-0));
|
||||
padding-left: var(--safe-area-inset-left, var(--ha-space-0));
|
||||
padding-right: var(--safe-area-inset-right, var(--ha-space-0));
|
||||
}
|
||||
}
|
||||
|
||||
.header-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
margin: 0;
|
||||
margin-bottom: 0;
|
||||
color: var(
|
||||
--ha-dialog-header-title-color,
|
||||
var(--ha-color-on-surface-default, var(--primary-text-color))
|
||||
);
|
||||
font-size: var(
|
||||
--ha-dialog-header-title-font-size,
|
||||
var(--ha-font-size-2xl)
|
||||
);
|
||||
line-height: var(
|
||||
--ha-dialog-header-title-line-height,
|
||||
var(--ha-line-height-condensed)
|
||||
);
|
||||
font-weight: var(
|
||||
--ha-dialog-header-title-font-weight,
|
||||
var(--ha-font-weight-normal)
|
||||
);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: var(--ha-space-3);
|
||||
}
|
||||
|
||||
wa-dialog::part(body) {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.body {
|
||||
position: var(--dialog-content-position, relative);
|
||||
padding: 0 var(--dialog-content-padding, var(--ha-space-6))
|
||||
var(--dialog-content-padding, var(--ha-space-6))
|
||||
var(--dialog-content-padding, var(--ha-space-6));
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
:host([flexcontent]) .body {
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
wa-dialog::part(footer) {
|
||||
padding: var(--ha-space-0);
|
||||
}
|
||||
|
||||
::slotted([slot="footer"]) {
|
||||
display: flex;
|
||||
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4)
|
||||
var(--ha-space-4);
|
||||
gap: var(--ha-space-3);
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-wa-dialog": HaWaDialog;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
opened: undefined;
|
||||
"after-show": undefined;
|
||||
closed: undefined;
|
||||
}
|
||||
}
|
@@ -167,6 +167,8 @@ class DialogMediaPlayerBrowse extends LitElement {
|
||||
.accept=${this._params.accept}
|
||||
.defaultId=${this._params.defaultId}
|
||||
.defaultType=${this._params.defaultType}
|
||||
.hideContentType=${this._params.hideContentType}
|
||||
.contentIdHelper=${this._params.contentIdHelper}
|
||||
@close-dialog=${this.closeDialog}
|
||||
@media-picked=${this._mediaPicked}
|
||||
@media-browsed=${this._mediaBrowsed}
|
||||
|
@@ -19,8 +19,12 @@ class BrowseMediaManual extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public item!: MediaPlayerItemId;
|
||||
|
||||
@property({ attribute: false }) public hideContentType = false;
|
||||
|
||||
@property({ attribute: false }) public contentIdHelper?: string;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
() =>
|
||||
(hideContentType: boolean) =>
|
||||
[
|
||||
{
|
||||
name: "media_content_id",
|
||||
@@ -29,13 +33,17 @@ class BrowseMediaManual extends LitElement {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "media_content_type",
|
||||
required: false,
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
...(hideContentType
|
||||
? []
|
||||
: [
|
||||
{
|
||||
name: "media_content_type",
|
||||
required: false,
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
]),
|
||||
] as const
|
||||
);
|
||||
|
||||
@@ -45,7 +53,7 @@ class BrowseMediaManual extends LitElement {
|
||||
<div class="card-content">
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.schema=${this._schema()}
|
||||
.schema=${this._schema(this.hideContentType)}
|
||||
.data=${this.item}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@@ -69,13 +77,35 @@ class BrowseMediaManual extends LitElement {
|
||||
|
||||
private _computeLabel = (
|
||||
entry: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string =>
|
||||
this.hass.localize(`ui.components.selectors.media.${entry.name}`);
|
||||
): string => {
|
||||
switch (entry.name) {
|
||||
case "media_content_id":
|
||||
case "media_content_type":
|
||||
return this.hass.localize(
|
||||
`ui.components.selectors.media.${entry.name}`
|
||||
);
|
||||
}
|
||||
return entry.name;
|
||||
};
|
||||
|
||||
private _computeHelper = (
|
||||
entry: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string =>
|
||||
this.hass.localize(`ui.components.selectors.media.${entry.name}_detail`);
|
||||
): string => {
|
||||
switch (entry.name) {
|
||||
case "media_content_id":
|
||||
return (
|
||||
this.contentIdHelper ||
|
||||
this.hass.localize(
|
||||
`ui.components.selectors.media.${entry.name}_detail`
|
||||
)
|
||||
);
|
||||
case "media_content_type":
|
||||
return this.hass.localize(
|
||||
`ui.components.selectors.media.${entry.name}_detail`
|
||||
);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
private _mediaPicked() {
|
||||
fireEvent(this, "manual-media-picked", {
|
||||
|
@@ -76,8 +76,8 @@ declare global {
|
||||
}
|
||||
|
||||
export interface MediaPlayerItemId {
|
||||
media_content_id: string | undefined;
|
||||
media_content_type: string | undefined;
|
||||
media_content_id?: string | undefined;
|
||||
media_content_type?: string | undefined;
|
||||
}
|
||||
|
||||
const MANUAL_ITEM: MediaPlayerItem = {
|
||||
@@ -113,6 +113,10 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public defaultType?: string;
|
||||
|
||||
@property({ attribute: false }) public hideContentType = false;
|
||||
|
||||
@property({ attribute: false }) public contentIdHelper?: string;
|
||||
|
||||
// @todo Consider reworking to eliminate need for attribute since it is manipulated internally
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@@ -521,6 +525,8 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
media_content_type: this.defaultType || "",
|
||||
}}
|
||||
.hass=${this.hass}
|
||||
.hideContentType=${this.hideContentType}
|
||||
.contentIdHelper=${this.contentIdHelper}
|
||||
@manual-media-picked=${this._manualPicked}
|
||||
></ha-browse-media-manual>`
|
||||
: isTTSMediaSource(currentItem.media_content_id)
|
||||
|
@@ -14,6 +14,8 @@ export interface MediaPlayerBrowseDialogParams {
|
||||
accept?: string[];
|
||||
defaultId?: string;
|
||||
defaultType?: string;
|
||||
hideContentType?: boolean;
|
||||
contentIdHelper?: string;
|
||||
}
|
||||
|
||||
export const showMediaBrowserDialog = (
|
||||
|
@@ -39,8 +39,6 @@ import type { HomeAssistant, TranslationDict } from "../types";
|
||||
import { isUnavailableState } from "./entity";
|
||||
import { isTTSMediaSource } from "./tts";
|
||||
|
||||
import { generateEntityFilter } from "../common/entity/entity_filter";
|
||||
|
||||
interface MediaPlayerEntityAttributes extends HassEntityAttributeBase {
|
||||
media_content_id?: string;
|
||||
media_content_type?: string;
|
||||
@@ -524,33 +522,3 @@ export const mediaPlayerJoin = (
|
||||
|
||||
export const mediaPlayerUnjoin = (hass: HomeAssistant, entity_id: string) =>
|
||||
hass.callService("media_player", "unjoin", {}, { entity_id });
|
||||
|
||||
/**
|
||||
* Compute active media player states in a specific area.
|
||||
* @param hass Home Assistant object
|
||||
* @param areaId Area ID to filter media players by
|
||||
* @returns Array of playing media player entities
|
||||
*/
|
||||
export const computeActiveAreaMediaStates = (
|
||||
hass: HomeAssistant,
|
||||
areaId: string
|
||||
): MediaPlayerEntity[] => {
|
||||
const area = hass.areas[areaId];
|
||||
if (!area) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Get all media_player entities in this area
|
||||
const mediaFilter = generateEntityFilter(hass, {
|
||||
area: areaId,
|
||||
domain: "media_player",
|
||||
});
|
||||
|
||||
const mediaEntities = Object.keys(hass.entities).filter(mediaFilter);
|
||||
|
||||
return mediaEntities
|
||||
.map((entityId) => hass.states[entityId] as MediaPlayerEntity | undefined)
|
||||
.filter(
|
||||
(stateObj): stateObj is MediaPlayerEntity => stateObj?.state === "playing"
|
||||
);
|
||||
};
|
||||
|
@@ -95,7 +95,9 @@ export interface StatisticsValidationResultUnitsChanged {
|
||||
data: {
|
||||
statistic_id: string;
|
||||
state_unit: string;
|
||||
state_unit_class: string | null;
|
||||
metadata_unit: string;
|
||||
metadata_unit_class: string | null;
|
||||
supported_unit: string;
|
||||
};
|
||||
}
|
||||
@@ -231,12 +233,14 @@ export const validateStatistics = (hass: HomeAssistant) =>
|
||||
export const updateStatisticsMetadata = (
|
||||
hass: HomeAssistant,
|
||||
statistic_id: string,
|
||||
unit_of_measurement: string | null
|
||||
unit_of_measurement: string | null,
|
||||
unit_class: string | null
|
||||
) =>
|
||||
hass.callWS<undefined>({
|
||||
type: "recorder/update_statistics_metadata",
|
||||
statistic_id,
|
||||
unit_of_measurement,
|
||||
unit_class,
|
||||
});
|
||||
|
||||
export const clearStatistics = (hass: HomeAssistant, statistic_ids: string[]) =>
|
||||
|
@@ -18,6 +18,7 @@ import type {
|
||||
EntityRegistryEntry,
|
||||
} from "./entity_registry";
|
||||
import type { EntitySources } from "./entity_sources";
|
||||
import type { EntityNameItem } from "../common/entity/compute_entity_name_display";
|
||||
|
||||
export type Selector =
|
||||
| ActionSelector
|
||||
@@ -41,6 +42,7 @@ export type Selector =
|
||||
| LegacyDeviceSelector
|
||||
| DurationSelector
|
||||
| EntitySelector
|
||||
| EntityNameSelector
|
||||
| LegacyEntitySelector
|
||||
| FileSelector
|
||||
| IconSelector
|
||||
@@ -310,6 +312,10 @@ export interface LocationSelectorValue {
|
||||
export interface MediaSelector {
|
||||
media: {
|
||||
accept?: string[];
|
||||
image_upload?: boolean;
|
||||
clearable?: boolean;
|
||||
hide_content_type?: boolean;
|
||||
content_id_helper?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
@@ -499,6 +505,13 @@ export interface UiStateContentSelector {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface EntityNameSelector {
|
||||
entity_name: {
|
||||
entity_id?: string;
|
||||
default_name?: EntityNameItem | EntityNameItem[] | string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export const expandLabelTarget = (
|
||||
hass: HomeAssistant,
|
||||
labelId: string,
|
||||
|
@@ -76,7 +76,10 @@ export const formatSelectorValue = (
|
||||
if (!stateObj) {
|
||||
return entityId;
|
||||
}
|
||||
const name = hass.formatEntityName(stateObj, ["device", "entity"], " ");
|
||||
const name = hass.formatEntityName(stateObj, [
|
||||
{ type: "device" },
|
||||
{ type: "entity" },
|
||||
]);
|
||||
return name || entityId;
|
||||
})
|
||||
.join(", ");
|
||||
|
@@ -43,7 +43,7 @@ export type ModernForecastType = "hourly" | "daily" | "twice_daily";
|
||||
|
||||
export type ForecastType = ModernForecastType | "legacy";
|
||||
|
||||
interface ForecastAttribute {
|
||||
export interface ForecastAttribute {
|
||||
temperature: number;
|
||||
datetime: string;
|
||||
templow?: number;
|
||||
|
@@ -77,80 +77,84 @@ class MoreInfoMediaPlayer extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!stateActive(this.stateObj)) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const supportsMute = supportsFeature(
|
||||
this.stateObj,
|
||||
MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
);
|
||||
const supportsSliding = supportsFeature(
|
||||
const supportsSet = supportsFeature(
|
||||
this.stateObj,
|
||||
MediaPlayerEntityFeature.VOLUME_SET
|
||||
);
|
||||
|
||||
return html`${(supportsFeature(
|
||||
this.stateObj!,
|
||||
MediaPlayerEntityFeature.VOLUME_SET
|
||||
) ||
|
||||
supportsFeature(this.stateObj!, MediaPlayerEntityFeature.VOLUME_STEP)) &&
|
||||
stateActive(this.stateObj!)
|
||||
? html`
|
||||
<div class="volume">
|
||||
${supportsMute
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${this.stateObj.attributes.is_volume_muted
|
||||
? mdiVolumeOff
|
||||
: mdiVolumeHigh}
|
||||
.label=${this.hass.localize(
|
||||
`ui.card.media_player.${
|
||||
this.stateObj.attributes.is_volume_muted
|
||||
? "media_volume_unmute"
|
||||
: "media_volume_mute"
|
||||
}`
|
||||
)}
|
||||
@click=${this._toggleMute}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
${supportsFeature(
|
||||
this.stateObj,
|
||||
MediaPlayerEntityFeature.VOLUME_STEP
|
||||
) && !supportsSliding
|
||||
? html`
|
||||
<ha-icon-button
|
||||
action="volume_down"
|
||||
.path=${mdiVolumeMinus}
|
||||
.label=${this.hass.localize(
|
||||
"ui.card.media_player.media_volume_down"
|
||||
)}
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
action="volume_up"
|
||||
.path=${mdiVolumePlus}
|
||||
.label=${this.hass.localize(
|
||||
"ui.card.media_player.media_volume_up"
|
||||
)}
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
${supportsSliding
|
||||
? html`
|
||||
${!supportsMute
|
||||
? html`<ha-svg-icon .path=${mdiVolumeHigh}></ha-svg-icon>`
|
||||
: nothing}
|
||||
<ha-slider
|
||||
labeled
|
||||
id="input"
|
||||
.value=${Number(this.stateObj.attributes.volume_level) *
|
||||
100}
|
||||
@change=${this._selectedValueChanged}
|
||||
></ha-slider>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing}`;
|
||||
const supportsStep = supportsFeature(
|
||||
this.stateObj,
|
||||
MediaPlayerEntityFeature.VOLUME_STEP
|
||||
);
|
||||
|
||||
if (!supportsMute && !supportsSet && !supportsStep) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="volume">
|
||||
${supportsMute
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${this.stateObj.attributes.is_volume_muted
|
||||
? mdiVolumeOff
|
||||
: mdiVolumeHigh}
|
||||
.label=${this.hass.localize(
|
||||
`ui.card.media_player.${
|
||||
this.stateObj.attributes.is_volume_muted
|
||||
? "media_volume_unmute"
|
||||
: "media_volume_mute"
|
||||
}`
|
||||
)}
|
||||
@click=${this._toggleMute}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
${supportsStep
|
||||
? html` <ha-icon-button
|
||||
action="volume_down"
|
||||
.path=${mdiVolumeMinus}
|
||||
.label=${this.hass.localize(
|
||||
"ui.card.media_player.media_volume_down"
|
||||
)}
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
${supportsSet
|
||||
? html`
|
||||
${!supportsMute && !supportsStep
|
||||
? html`<ha-svg-icon .path=${mdiVolumeHigh}></ha-svg-icon>`
|
||||
: nothing}
|
||||
<ha-slider
|
||||
labeled
|
||||
id="input"
|
||||
.value=${Number(this.stateObj.attributes.volume_level) * 100}
|
||||
@change=${this._selectedValueChanged}
|
||||
></ha-slider>
|
||||
`
|
||||
: nothing}
|
||||
${supportsStep
|
||||
? html`
|
||||
<ha-icon-button
|
||||
action="volume_up"
|
||||
.path=${mdiVolumePlus}
|
||||
.label=${this.hass.localize(
|
||||
"ui.card.media_player.media_volume_up"
|
||||
)}
|
||||
@click=${this._handleClick}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected _renderSourceControl() {
|
||||
|
@@ -16,6 +16,7 @@ import "../../../components/ha-tab-group";
|
||||
import "../../../components/ha-tab-group-tab";
|
||||
import "../../../components/ha-tooltip";
|
||||
import type {
|
||||
ForecastAttribute,
|
||||
ForecastEvent,
|
||||
ModernForecastType,
|
||||
WeatherEntity,
|
||||
@@ -131,6 +132,24 @@ class MoreInfoWeather extends LitElement {
|
||||
getSupportedForecastTypes(stateObj)
|
||||
);
|
||||
|
||||
private _groupForecastByDay = memoizeOne((forecast: ForecastAttribute[]) => {
|
||||
if (!forecast) return [];
|
||||
|
||||
const grouped = new Map<string, NonNullable<typeof forecast>>();
|
||||
|
||||
forecast.forEach((item) => {
|
||||
const date = new Date(item.datetime);
|
||||
const dateKey = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
||||
|
||||
if (!grouped.has(dateKey)) {
|
||||
grouped.set(dateKey, []);
|
||||
}
|
||||
grouped.get(dateKey)!.push(item);
|
||||
});
|
||||
|
||||
return Array.from(grouped.values());
|
||||
});
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return nothing;
|
||||
@@ -314,78 +333,90 @@ class MoreInfoWeather extends LitElement {
|
||||
: nothing}
|
||||
<div class="forecast">
|
||||
${forecast?.length
|
||||
? forecast.map((item) =>
|
||||
this._showValue(item.templow) || this._showValue(item.temperature)
|
||||
? html`
|
||||
<div>
|
||||
<div>
|
||||
${dayNight
|
||||
? html`
|
||||
${formatDateWeekdayShort(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
<div class="daynight">
|
||||
${item.is_daytime !== false
|
||||
? this.hass!.localize("ui.card.weather.day")
|
||||
: this.hass!.localize(
|
||||
"ui.card.weather.night"
|
||||
)}<br />
|
||||
</div>
|
||||
`
|
||||
: hourly
|
||||
? html`
|
||||
${formatTime(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
`
|
||||
: html`
|
||||
${formatDateWeekdayShort(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
`}
|
||||
</div>
|
||||
${this._showValue(item.condition)
|
||||
? this._groupForecastByDay(forecast).map((dayForecast) => {
|
||||
const showDayHeader = hourly || dayNight;
|
||||
return html`
|
||||
<div class="forecast-day">
|
||||
${showDayHeader
|
||||
? html`<div class="forecast-day-header">
|
||||
${formatDateWeekdayShort(
|
||||
new Date(dayForecast[0].datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="forecast-day-content">
|
||||
${dayForecast.map((item) =>
|
||||
this._showValue(item.templow) ||
|
||||
this._showValue(item.temperature)
|
||||
? html`
|
||||
<div class="forecast-image-icon">
|
||||
${getWeatherStateIcon(
|
||||
item.condition!,
|
||||
this,
|
||||
!(
|
||||
item.is_daytime ||
|
||||
item.is_daytime === undefined
|
||||
)
|
||||
)}
|
||||
<div class="forecast-item">
|
||||
<div
|
||||
class="forecast-item-label ${showDayHeader
|
||||
? ""
|
||||
: "no-header"}"
|
||||
>
|
||||
${hourly
|
||||
? formatTime(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)
|
||||
: dayNight
|
||||
? html`<div class="daynight">
|
||||
${item.is_daytime !== false
|
||||
? this.hass!.localize(
|
||||
"ui.card.weather.day"
|
||||
)
|
||||
: this.hass!.localize(
|
||||
"ui.card.weather.night"
|
||||
)}
|
||||
</div>`
|
||||
: formatDateWeekdayShort(
|
||||
new Date(item.datetime),
|
||||
this.hass!.locale,
|
||||
this.hass!.config
|
||||
)}
|
||||
</div>
|
||||
${this._showValue(item.condition)
|
||||
? html`
|
||||
<div class="forecast-image-icon">
|
||||
${getWeatherStateIcon(
|
||||
item.condition!,
|
||||
this,
|
||||
!(
|
||||
item.is_daytime ||
|
||||
item.is_daytime === undefined
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="temp">
|
||||
${this._showValue(item.temperature)
|
||||
? html`${formatNumber(
|
||||
item.temperature,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: "—"}
|
||||
</div>
|
||||
<div class="templow">
|
||||
${this._showValue(item.templow)
|
||||
? html`${formatNumber(
|
||||
item.templow!,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="temp">
|
||||
${this._showValue(item.temperature)
|
||||
? html`${formatNumber(
|
||||
item.temperature,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: "—"}
|
||||
</div>
|
||||
<div class="templow">
|
||||
${this._showValue(item.templow)
|
||||
? html`${formatNumber(
|
||||
item.templow!,
|
||||
this.hass!.locale
|
||||
)}°`
|
||||
: hourly
|
||||
? nothing
|
||||
: "—"}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
)
|
||||
: nothing
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
: html`<ha-spinner size="medium"></ha-spinner>`}
|
||||
</div>
|
||||
|
||||
@@ -556,14 +587,46 @@ class MoreInfoWeather extends LitElement {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.forecast > div {
|
||||
.forecast-day {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.forecast-day-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
color: var(--primary-text-color);
|
||||
z-index: 1;
|
||||
padding: 0 var(--ha-space-3) var(--ha-space-1) var(--ha-space-3);
|
||||
width: fit-content;
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
padding: 0 10px;
|
||||
font-weight: var(--ha-font-weight-semi-bold);
|
||||
}
|
||||
|
||||
.forecast-day-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.forecast-item {
|
||||
text-align: center;
|
||||
padding: 0 var(--ha-space-3);
|
||||
}
|
||||
|
||||
.forecast-item-label {
|
||||
font-size: var(--ha-font-size-m);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.forecast-item-label.no-header {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.forecast .icon,
|
||||
.forecast .temp {
|
||||
margin: 4px 0;
|
||||
margin: var(--ha-space-1) 0;
|
||||
}
|
||||
|
||||
.forecast .temp {
|
||||
|
@@ -23,8 +23,14 @@ import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityEntryName } from "../../common/entity/compute_entity_name";
|
||||
import { getEntityEntryContext } from "../../common/entity/context/get_entity_context";
|
||||
import {
|
||||
computeEntityEntryName,
|
||||
computeEntityName,
|
||||
} from "../../common/entity/compute_entity_name";
|
||||
import {
|
||||
getEntityContext,
|
||||
getEntityEntryContext,
|
||||
} from "../../common/entity/context/get_entity_context";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import "../../components/ha-button-menu";
|
||||
@@ -321,28 +327,34 @@ export class MoreInfoDialog extends LitElement {
|
||||
(isDefaultView && this._parentEntityIds.length === 0) ||
|
||||
isSpecificInitialView;
|
||||
|
||||
let entityName: string | undefined;
|
||||
let deviceName: string | undefined;
|
||||
let areaName: string | undefined;
|
||||
const context = stateObj
|
||||
? getEntityContext(
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: this._entry
|
||||
? getEntityEntryContext(
|
||||
this._entry,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (stateObj) {
|
||||
entityName = this.hass.formatEntityName(stateObj, "entity");
|
||||
deviceName = this.hass.formatEntityName(stateObj, "device");
|
||||
areaName = this.hass.formatEntityName(stateObj, "area");
|
||||
} else if (this._entry) {
|
||||
const { device, area } = getEntityEntryContext(
|
||||
this._entry,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
entityName = computeEntityEntryName(this._entry, this.hass.devices);
|
||||
deviceName = device ? computeDeviceName(device) : undefined;
|
||||
areaName = area ? computeAreaName(area) : undefined;
|
||||
} else {
|
||||
entityName = entityId;
|
||||
}
|
||||
const entityName = stateObj
|
||||
? computeEntityName(stateObj, this.hass.entities, this.hass.devices)
|
||||
: this._entry
|
||||
? computeEntityEntryName(this._entry, this.hass.devices)
|
||||
: entityId;
|
||||
|
||||
const deviceName = context?.device
|
||||
? computeDeviceName(context.device)
|
||||
: undefined;
|
||||
const areaName = context?.area ? computeAreaName(context.area) : undefined;
|
||||
|
||||
const breadcrumb = [areaName, deviceName, entityName].filter(
|
||||
(v): v is string => Boolean(v)
|
||||
|
@@ -23,6 +23,7 @@ import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { entityUseDeviceName } from "../../common/entity/compute_entity_name";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { getDeviceContext } from "../../common/entity/context/get_device_context";
|
||||
import { navigate } from "../../common/navigate";
|
||||
@@ -30,9 +31,9 @@ import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-label";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-list";
|
||||
import "../../components/ha-md-list-item";
|
||||
import "../../components/ha-spinner";
|
||||
@@ -631,14 +632,29 @@ export class QuickBar extends LitElement {
|
||||
const stateObj = this.hass.states[entityId];
|
||||
|
||||
const friendlyName = computeStateName(stateObj); // Keep this for search
|
||||
const entityName = this.hass.formatEntityName(stateObj, "entity");
|
||||
const deviceName = this.hass.formatEntityName(stateObj, "device");
|
||||
const areaName = this.hass.formatEntityName(stateObj, "area");
|
||||
|
||||
const primary = entityName || deviceName || entityId;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
const useDeviceName = entityUseDeviceName(
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices
|
||||
);
|
||||
|
||||
const name = this.hass.formatEntityName(
|
||||
stateObj,
|
||||
useDeviceName ? { type: "device" } : { type: "entity" }
|
||||
);
|
||||
|
||||
const primary = name || entityId;
|
||||
|
||||
const secondary = this.hass.formatEntityName(
|
||||
stateObj,
|
||||
useDeviceName
|
||||
? [{ type: "area" }]
|
||||
: [{ type: "area" }, { type: "device" }],
|
||||
{
|
||||
separator: isRTL ? " ◂ " : " ▸ ",
|
||||
}
|
||||
);
|
||||
|
||||
const translatedDomain = domainToName(
|
||||
this.hass.localize,
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import {
|
||||
mdiAutoFix,
|
||||
mdiClose,
|
||||
mdiLifebuoy,
|
||||
mdiPower,
|
||||
mdiPowerCycle,
|
||||
@@ -9,16 +8,14 @@ import {
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-expansion-panel";
|
||||
import "../../components/ha-fade-in";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-icon-next";
|
||||
import "../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../components/ha-md-dialog";
|
||||
import "../../components/ha-wa-dialog";
|
||||
import "../../components/ha-md-list";
|
||||
import "../../components/ha-md-list-item";
|
||||
import "../../components/ha-spinner";
|
||||
@@ -58,12 +55,14 @@ class DialogRestart extends LitElement {
|
||||
@state()
|
||||
private _hostInfo?: HassioHostInfo;
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
@state()
|
||||
private _dialogOpen = false;
|
||||
|
||||
public async showDialog(): Promise<void> {
|
||||
const isHassioLoaded = isComponentLoaded(this.hass, "hassio");
|
||||
|
||||
this._open = true;
|
||||
this._dialogOpen = true;
|
||||
|
||||
if (isHassioLoaded && !this._hostInfo) {
|
||||
this._loadHostInfo();
|
||||
@@ -92,16 +91,13 @@ class DialogRestart extends LitElement {
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._dialogOpen = false;
|
||||
this._open = false;
|
||||
this._loadingHostInfo = false;
|
||||
this._loadingBackupInfo = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._dialog?.close();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._open) {
|
||||
return nothing;
|
||||
@@ -113,17 +109,13 @@ class DialogRestart extends LitElement {
|
||||
const dialogTitle = this.hass.localize("ui.dialogs.restart.heading");
|
||||
|
||||
return html`
|
||||
<ha-md-dialog open @closed=${this._dialogClosed}>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
></ha-icon-button>
|
||||
<span slot="title" .title=${dialogTitle}> ${dialogTitle} </span>
|
||||
</ha-dialog-header>
|
||||
<div slot="content" class="content">
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._dialogOpen}
|
||||
header-title=${dialogTitle}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div class="content">
|
||||
<div class="action-loader">
|
||||
${this._loadingBackupInfo
|
||||
? html`<ha-fade-in .delay=${250}>
|
||||
@@ -265,12 +257,12 @@ class DialogRestart extends LitElement {
|
||||
</ha-expansion-panel>
|
||||
`}
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _reload() {
|
||||
this.closeDialog();
|
||||
this._dialogOpen = false;
|
||||
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.dialogs.restart.reload.reloading"),
|
||||
@@ -374,7 +366,7 @@ class DialogRestart extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeDialog();
|
||||
this._dialogOpen = false;
|
||||
|
||||
let actionFunc;
|
||||
|
||||
@@ -413,15 +405,9 @@ class DialogRestart extends LitElement {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-md-dialog {
|
||||
ha-wa-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
@media all and (min-width: 550px) {
|
||||
ha-md-dialog {
|
||||
min-width: 500px;
|
||||
max-width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
ha-expansion-panel {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
|
@@ -17,8 +17,6 @@ import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
import "../../../components/ha-dialog";
|
||||
import type { HaDialog } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-header";
|
||||
import "../../../components/ha-domain-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
@@ -26,8 +24,11 @@ import "../../../components/ha-icon-button-prev";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-list";
|
||||
import type { HaMdList } from "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-service-icon";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import type { HaWaDialog } from "../../../components/ha-wa-dialog";
|
||||
import "../../../components/search-input";
|
||||
import {
|
||||
ACTION_GROUPS,
|
||||
@@ -111,12 +112,12 @@ class DialogAddAutomationElement
|
||||
|
||||
@state() private _domains?: Set<string>;
|
||||
|
||||
@query("ha-dialog") private _dialog?: HaDialog;
|
||||
@query("#content") private _contentElement?: HaMdList;
|
||||
|
||||
@query("ha-wa-dialog") private _dialogElement?: HaWaDialog;
|
||||
|
||||
private _fullScreen = false;
|
||||
|
||||
@state() private _width?: number;
|
||||
|
||||
@state() private _height?: number;
|
||||
|
||||
@state() private _narrow = false;
|
||||
@@ -146,7 +147,6 @@ class DialogAddAutomationElement
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
this._height = undefined;
|
||||
this._width = undefined;
|
||||
this._params = undefined;
|
||||
this._group = undefined;
|
||||
this._prev = undefined;
|
||||
@@ -461,10 +461,8 @@ class DialogAddAutomationElement
|
||||
}
|
||||
|
||||
protected _opened(): void {
|
||||
// Store the width and height so that when we search, box doesn't jump
|
||||
const boundingRect =
|
||||
this.shadowRoot!.querySelector("ha-md-list")?.getBoundingClientRect();
|
||||
this._width = boundingRect?.width;
|
||||
// Store the height so that when we search, box doesn't jump
|
||||
const boundingRect = this._contentElement?.getBoundingClientRect();
|
||||
this._height = boundingRect?.height;
|
||||
}
|
||||
|
||||
@@ -502,6 +500,15 @@ class DialogAddAutomationElement
|
||||
this._manifests
|
||||
);
|
||||
|
||||
const groupItems = this._getGroupItems(
|
||||
this._params.type,
|
||||
undefined,
|
||||
undefined,
|
||||
this.hass.localize,
|
||||
this.hass.services,
|
||||
this._manifests
|
||||
);
|
||||
|
||||
const groupName = isService(this._group)
|
||||
? domainToName(
|
||||
this.hass.localize,
|
||||
@@ -514,14 +521,13 @@ class DialogAddAutomationElement
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
<ha-wa-dialog
|
||||
open
|
||||
hideActions
|
||||
@opened=${this._opened}
|
||||
persist-initial-height
|
||||
@after-show=${this._opened}
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${true}
|
||||
>
|
||||
<div slot="heading">
|
||||
<div slot="header">
|
||||
<ha-dialog-header>
|
||||
<span slot="title"
|
||||
>${this._group
|
||||
@@ -538,7 +544,7 @@ class DialogAddAutomationElement
|
||||
: html`<ha-icon-button
|
||||
.path=${mdiClose}
|
||||
slot="navigationIcon"
|
||||
dialogAction="cancel"
|
||||
data-dialog="close"
|
||||
></ha-icon-button>`}
|
||||
</ha-dialog-header>
|
||||
<search-input
|
||||
@@ -556,100 +562,206 @@ class DialogAddAutomationElement
|
||||
)}
|
||||
></search-input>
|
||||
</div>
|
||||
<ha-md-list
|
||||
dialogInitialFocus=${ifDefined(this._fullScreen ? "" : undefined)}
|
||||
<div
|
||||
id="content"
|
||||
style=${styleMap({
|
||||
width: this._width ? `${this._width}px` : "auto",
|
||||
height: this._height ? `${Math.min(468, this._height)}px` : "auto",
|
||||
height: this._height ? `${Math.min(468, this._height)}px` : "100vh",
|
||||
})}
|
||||
>
|
||||
${this._params.clipboardItem &&
|
||||
!this._filter &&
|
||||
(!this._group ||
|
||||
items.find((item) => item.key === this._params!.clipboardItem))
|
||||
? html`<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
class="paste"
|
||||
.value=${PASTE_VALUE}
|
||||
@click=${this._selected}
|
||||
>
|
||||
<div class="shortcut-label">
|
||||
<div class="label">
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${this._params.type}s.paste`
|
||||
)}
|
||||
</div>
|
||||
<div class="supporting-text">
|
||||
${this.hass.localize(
|
||||
// @ts-ignore
|
||||
`ui.panel.config.automation.editor.${this._params.type}s.type.${this._params.clipboardItem}.label`
|
||||
)}
|
||||
<ha-md-list
|
||||
class="groups"
|
||||
dialogInitialFocus=${ifDefined(this._fullScreen ? "" : undefined)}
|
||||
>
|
||||
${this._params.clipboardItem &&
|
||||
!this._filter &&
|
||||
(!this._group ||
|
||||
items.find((item) => item.key === this._params!.clipboardItem))
|
||||
? html`<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
class="paste"
|
||||
.value=${PASTE_VALUE}
|
||||
@click=${this._selected}
|
||||
>
|
||||
<div class="shortcut-label">
|
||||
<div class="label">
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${this._params.type}s.paste`
|
||||
)}
|
||||
</div>
|
||||
<div class="supporting-text">
|
||||
${this.hass.localize(
|
||||
// @ts-ignore
|
||||
`ui.panel.config.automation.editor.${this._params.type}s.type.${this._params.clipboardItem}.label`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
${!this._narrow
|
||||
? html`<span class="shortcut">
|
||||
<span
|
||||
>${isMac
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.ctrl"
|
||||
)}</span
|
||||
>
|
||||
<span>+</span>
|
||||
<span>V</span>
|
||||
</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
${!this._narrow
|
||||
? html`<span class="shortcut">
|
||||
<span
|
||||
>${isMac
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.ctrl"
|
||||
)}</span
|
||||
>
|
||||
<span>+</span>
|
||||
<span>V</span>
|
||||
</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiContentPaste}
|
||||
></ha-svg-icon
|
||||
><ha-svg-icon slot="end" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-md-list-item>
|
||||
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
|
||||
: nothing}
|
||||
${repeat(
|
||||
items,
|
||||
(item) => item.key,
|
||||
(item) => html`
|
||||
<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
.value=${item.key}
|
||||
.group=${item.group}
|
||||
@click=${this._selected}
|
||||
>
|
||||
<div slot="headline">${item.name}</div>
|
||||
<div slot="supporting-text">${item.description}</div>
|
||||
${item.icon
|
||||
? html`<span slot="start">${item.icon}</span>`
|
||||
: item.iconPath
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${item.iconPath}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
${item.group
|
||||
? html`<ha-icon-next slot="end"></ha-icon-next>`
|
||||
: html`<ha-svg-icon
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiContentPaste}
|
||||
></ha-svg-icon
|
||||
><ha-svg-icon
|
||||
class="plus"
|
||||
slot="end"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>`}
|
||||
</ha-md-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-md-list>
|
||||
</ha-dialog>
|
||||
></ha-svg-icon>
|
||||
</ha-md-list-item>
|
||||
<ha-md-divider
|
||||
role="separator"
|
||||
tabindex="-1"
|
||||
></ha-md-divider>`
|
||||
: nothing}
|
||||
${repeat(
|
||||
groupItems,
|
||||
(item) => item.key,
|
||||
(item) => html`
|
||||
<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
.value=${item.key}
|
||||
.group=${item.group}
|
||||
@click=${this._selected}
|
||||
class=${item.key === this._group ? "selected" : ""}
|
||||
>
|
||||
<div slot="headline">${item.name}</div>
|
||||
${item.icon
|
||||
? html`<span slot="start">${item.icon}</span>`
|
||||
: item.iconPath
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${item.iconPath}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-md-list>
|
||||
<div class="items ${!this._group ? "blank" : ""}">
|
||||
${!this._group
|
||||
? "Select a group"
|
||||
: html`<h3>${this._params.type}</h3>
|
||||
<ha-md-list
|
||||
dialogInitialFocus=${ifDefined(
|
||||
this._fullScreen ? "" : undefined
|
||||
)}
|
||||
>
|
||||
${this._params.clipboardItem &&
|
||||
!this._filter &&
|
||||
(!this._group ||
|
||||
items.find(
|
||||
(item) => item.key === this._params!.clipboardItem
|
||||
))
|
||||
? html`<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
class="paste"
|
||||
.value=${PASTE_VALUE}
|
||||
@click=${this._selected}
|
||||
>
|
||||
<div class="shortcut-label">
|
||||
<div class="label">
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${this._params.type}s.paste`
|
||||
)}
|
||||
</div>
|
||||
<div class="supporting-text">
|
||||
${this.hass.localize(
|
||||
// @ts-ignore
|
||||
`ui.panel.config.automation.editor.${this._params.type}s.type.${this._params.clipboardItem}.label`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
${!this._narrow
|
||||
? html`<span class="shortcut">
|
||||
<span
|
||||
>${isMac
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.ctrl"
|
||||
)}</span
|
||||
>
|
||||
<span>+</span>
|
||||
<span>V</span>
|
||||
</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiContentPaste}
|
||||
></ha-svg-icon
|
||||
><ha-svg-icon
|
||||
class="plus"
|
||||
slot="end"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
</ha-md-list-item>
|
||||
<ha-md-divider
|
||||
role="separator"
|
||||
tabindex="-1"
|
||||
></ha-md-divider>`
|
||||
: nothing}
|
||||
${repeat(
|
||||
items,
|
||||
(item) => item.key,
|
||||
(item) => html`
|
||||
<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
.value=${item.key}
|
||||
.group=${item.group}
|
||||
@click=${this._selected}
|
||||
>
|
||||
<div slot="headline">${item.name}</div>
|
||||
<div slot="supporting-text">${item.description}</div>
|
||||
${item.icon
|
||||
? html`<span slot="start">${item.icon}</span>`
|
||||
: item.iconPath
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${item.iconPath}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
${item.group
|
||||
? html`<ha-icon-next slot="end"></ha-icon-next>`
|
||||
: html`<ha-svg-icon
|
||||
slot="end"
|
||||
class="plus"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>`}
|
||||
</ha-md-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-md-list>`}
|
||||
</div>
|
||||
</div>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _back() {
|
||||
this._dialog!.scrollToPos(0, 0);
|
||||
this._dialogElement?.bodyContainer.scrollTo(0, 0);
|
||||
if (this._filter) {
|
||||
this._filter = "";
|
||||
return;
|
||||
@@ -663,7 +775,7 @@ class DialogAddAutomationElement
|
||||
}
|
||||
|
||||
private _selected(ev) {
|
||||
this._dialog!.scrollToPos(0, 0);
|
||||
this._dialogElement?.bodyContainer.scrollTo(0, 0);
|
||||
const item = ev.currentTarget;
|
||||
if (item.group) {
|
||||
this._prev = this._group;
|
||||
@@ -707,29 +819,92 @@ class DialogAddAutomationElement
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
ha-wa-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
--ha-dialog-width-md: 888px;
|
||||
--mdc-dialog-max-height: 60vh;
|
||||
--mdc-dialog-max-height: 60dvh;
|
||||
}
|
||||
@media all and (min-width: 550px) {
|
||||
ha-dialog {
|
||||
ha-wa-dialog {
|
||||
--mdc-dialog-min-width: 500px;
|
||||
}
|
||||
}
|
||||
ha-icon-next {
|
||||
width: 24px;
|
||||
}
|
||||
ha-md-list {
|
||||
|
||||
#content {
|
||||
max-height: 468px;
|
||||
max-width: 100vw;
|
||||
--md-list-item-leading-space: 24px;
|
||||
--md-list-item-trailing-space: 24px;
|
||||
--md-list-item-supporting-text-font: var(--ha-font-size-s);
|
||||
display: flex;
|
||||
gap: var(--ha-space-3);
|
||||
padding: var(--ha-space-3) var(--ha-space-4);
|
||||
}
|
||||
|
||||
ha-md-list.groups {
|
||||
overflow: auto;
|
||||
flex: 3;
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
border: 1px solid var(--ha-color-border-neutral-quiet);
|
||||
--md-list-item-leading-space: var(--ha-space-3);
|
||||
--md-list-item-trailing-space: var(--md-list-item-leading-space);
|
||||
--md-list-item-bottom-space: var(--ha-space-1);
|
||||
--md-list-item-top-space: var(--md-list-item-bottom-space);
|
||||
--md-list-item-supporting-text-font: var(--ha-font-size-s);
|
||||
--md-list-item-one-line-container-height: var(--ha-space-8);
|
||||
}
|
||||
|
||||
ha-md-list.groups ha-md-list-item.selected {
|
||||
background-color: var(--ha-color-fill-primary-normal-active);
|
||||
--md-list-item-label-text-color: var(--primary-color);
|
||||
--icon-primary-color: var(--primary-color);
|
||||
}
|
||||
ha-md-list.groups ha-md-list-item.selected ha-svg-icon {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
#content .items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
flex: 7;
|
||||
}
|
||||
|
||||
#content .items.blank {
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
background-color: var(--ha-color-surface-default);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
#content .items ha-md-list {
|
||||
--md-list-item-two-line-container-height: var(--ha-space-12);
|
||||
--md-list-item-leading-space: var(--ha-space-3);
|
||||
--md-list-item-trailing-space: var(--md-list-item-leading-space);
|
||||
--md-list-item-bottom-space: var(--ha-space-2);
|
||||
--md-list-item-top-space: var(--md-list-item-bottom-space);
|
||||
--md-list-item-supporting-text-font: var(--ha-font-size-s);
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
#content .items ha-md-list ha-md-list-item {
|
||||
border-radius: var(--ha-border-radius-lg);
|
||||
border: 1px solid var(--ha-color-border-neutral-quiet);
|
||||
}
|
||||
|
||||
ha-md-list-item img {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
ha-md-list-item.paste {
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-quiet);
|
||||
}
|
||||
|
||||
ha-svg-icon.plus {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
search-input {
|
||||
display: block;
|
||||
margin: 0 16px;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
@@ -9,9 +9,9 @@ import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-icon-button-prev";
|
||||
import "../../../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
|
||||
import "../../../../components/ha-wa-dialog";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-md-select";
|
||||
@@ -73,12 +73,13 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
|
||||
@state() private _formData?: FormData;
|
||||
|
||||
@query("ha-md-dialog") private _dialog?: HaMdDialog;
|
||||
@state() private _open = false;
|
||||
|
||||
public showDialog(_params: GenerateBackupDialogParams): void {
|
||||
this._step = STEPS[0];
|
||||
this._formData = INITIAL_DATA;
|
||||
this._params = _params;
|
||||
this._open = true;
|
||||
|
||||
this._fetchAgents();
|
||||
this._fetchBackupConfig();
|
||||
@@ -88,6 +89,7 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
if (this._params!.cancel) {
|
||||
this._params!.cancel();
|
||||
}
|
||||
this._open = false;
|
||||
this._step = undefined;
|
||||
this._formData = undefined;
|
||||
this._agents = [];
|
||||
@@ -114,7 +116,7 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
this._dialog?.close();
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -179,15 +181,19 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
const selectedAgents = this._formData.agent_ids;
|
||||
|
||||
return html`
|
||||
<ha-md-dialog open disable-cancel-action @closed=${this._dialogClosed}>
|
||||
<ha-dialog-header slot="headline">
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-dialog-header slot="header">
|
||||
${isFirstStep
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
data-dialog="close"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
@click=${this.closeDialog}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: html`
|
||||
@@ -198,13 +204,17 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
`}
|
||||
<span slot="title" .title=${dialogTitle}> ${dialogTitle} </span>
|
||||
</ha-dialog-header>
|
||||
<div slot="content" class="content">
|
||||
<div class="content">
|
||||
${this._step === "data" ? this._renderData() : this._renderSync()}
|
||||
</div>
|
||||
<div slot="actions">
|
||||
<ha-dialog-footer slot="footer">
|
||||
${isFirstStep
|
||||
? html`
|
||||
<ha-button @click=${this.closeDialog} appearance="plain">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
`
|
||||
@@ -212,6 +222,7 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
${isLastStep
|
||||
? html`
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._submit}
|
||||
.disabled=${this._formData.agents_mode === "custom" &&
|
||||
!selectedAgents.length}
|
||||
@@ -223,14 +234,15 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
`
|
||||
: html`
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._nextStep}
|
||||
.disabled=${this._step === "data" && this._noDataSelected}
|
||||
>
|
||||
${this.hass.localize("ui.common.next")}
|
||||
</ha-button>
|
||||
`}
|
||||
</div>
|
||||
</ha-md-dialog>
|
||||
</ha-dialog-footer>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -436,9 +448,8 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-md-dialog {
|
||||
ha-wa-dialog {
|
||||
--dialog-content-padding: 24px;
|
||||
max-height: calc(100vh - 48px);
|
||||
}
|
||||
ha-md-list {
|
||||
background: none;
|
||||
|
@@ -71,6 +71,9 @@ import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup"
|
||||
import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup";
|
||||
import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup";
|
||||
import { downloadBackup } from "./helper/download_backup";
|
||||
import type { HaMdMenu } from "../../../components/ha-md-menu";
|
||||
import "../../../components/ha-md-menu";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
|
||||
interface BackupRow extends DataTableRowData, BackupContent {
|
||||
formatted_type: string;
|
||||
@@ -120,6 +123,10 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
@query("hass-tabs-subpage-data-table", true)
|
||||
private _dataTable!: HaTabsSubpageDataTable;
|
||||
|
||||
@query("#overflow-menu") private _overflowMenu?: HaMdMenu;
|
||||
|
||||
private _overflowBackup?: BackupContent;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("location-changed", this._locationChanged);
|
||||
@@ -254,24 +261,12 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
hideable: false,
|
||||
type: "overflow-menu",
|
||||
template: (backup) => html`
|
||||
<ha-icon-overflow-menu
|
||||
.hass=${this.hass}
|
||||
narrow
|
||||
.items=${[
|
||||
{
|
||||
label: this.hass.localize("ui.common.download"),
|
||||
path: mdiDownload,
|
||||
action: () => this._downloadBackup(backup),
|
||||
},
|
||||
{
|
||||
label: this.hass.localize("ui.common.delete"),
|
||||
path: mdiDelete,
|
||||
action: () => this._deleteBackup(backup),
|
||||
warning: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
</ha-icon-overflow-menu>
|
||||
<ha-icon-button
|
||||
.selected=${backup}
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
@click=${this._toggleOverflowMenu}
|
||||
></ha-icon-button>
|
||||
`,
|
||||
},
|
||||
})
|
||||
@@ -290,6 +285,20 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
: undefined
|
||||
);
|
||||
|
||||
private _toggleOverflowMenu = (ev) => {
|
||||
if (!this._overflowMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._overflowMenu.open) {
|
||||
this._overflowMenu.close();
|
||||
return;
|
||||
}
|
||||
this._overflowBackup = ev.target.selected;
|
||||
this._overflowMenu.anchorElement = ev.target;
|
||||
this._overflowMenu.show();
|
||||
};
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
this._activeGrouping = ev.detail.value;
|
||||
}
|
||||
@@ -366,14 +375,16 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
clickable
|
||||
id="backup_id"
|
||||
has-filters
|
||||
.filters=${Object.values(this._filters).filter((filter) =>
|
||||
Array.isArray(filter)
|
||||
? filter.length
|
||||
: filter &&
|
||||
Object.values(filter).some((val) =>
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
.filters=${
|
||||
Object.values(this._filters).filter((filter) =>
|
||||
Array.isArray(filter)
|
||||
? filter.length
|
||||
: filter &&
|
||||
Object.values(filter).some((val) =>
|
||||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length
|
||||
}
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
@@ -415,28 +426,30 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
</div>
|
||||
|
||||
<div slot="selection-bar">
|
||||
${!this.narrow
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._deleteSelected}
|
||||
variant="danger"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.backups.delete_selected"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.backup.backups.delete_selected"
|
||||
)}
|
||||
.path=${mdiDelete}
|
||||
class="warning"
|
||||
@click=${this._deleteSelected}
|
||||
></ha-icon-button>
|
||||
`}
|
||||
${
|
||||
!this.narrow
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._deleteSelected}
|
||||
variant="danger"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.backups.delete_selected"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.backup.backups.delete_selected"
|
||||
)}
|
||||
.path=${mdiDelete}
|
||||
class="warning"
|
||||
@click=${this._deleteSelected}
|
||||
></ha-icon-button>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
|
||||
<ha-filter-states
|
||||
@@ -449,29 +462,43 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
expanded
|
||||
.narrow=${this.narrow}
|
||||
></ha-filter-states>
|
||||
${!this._needsOnboarding
|
||||
? html`
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
?disabled=${backupInProgress}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.backup.backups.new_backup"
|
||||
)}
|
||||
extended
|
||||
@click=${this._newBackup}
|
||||
>
|
||||
${backupInProgress
|
||||
? html`<div slot="icon" class="loading">
|
||||
<ha-spinner .size=${"small"}></ha-spinner>
|
||||
</div>`
|
||||
: html`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>`}
|
||||
</ha-fab>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
!this._needsOnboarding
|
||||
? html`
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
?disabled=${backupInProgress}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.backup.backups.new_backup"
|
||||
)}
|
||||
extended
|
||||
@click=${this._newBackup}
|
||||
>
|
||||
${backupInProgress
|
||||
? html`<div slot="icon" class="loading">
|
||||
<ha-spinner .size=${"small"}></ha-spinner>
|
||||
</div>`
|
||||
: html`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>`}
|
||||
</ha-fab>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-md-menu id="overflow-menu" positioning="fixed">
|
||||
<ha-md-menu-item .clickAction=${this._downloadBackup}>
|
||||
<ha-svg-icon slot="start" .path=${mdiDownload}></ha-svg-icon>
|
||||
${this.hass.localize("ui.common.download")}
|
||||
</ha-md-menu-item>
|
||||
<ha-md-menu-item class="warning" .clickAction=${this._deleteBackup}>
|
||||
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
|
||||
${this.hass.localize("ui.common.delete")}
|
||||
</ha-md-menu-item>
|
||||
</ha-md-menu>
|
||||
>
|
||||
</ha-icon-overflow-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -545,11 +572,18 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
navigate(`/config/backup/details/${id}`);
|
||||
}
|
||||
|
||||
private async _downloadBackup(backup: BackupContent): Promise<void> {
|
||||
downloadBackup(this.hass, this, backup, this.config);
|
||||
private async _downloadBackup(): Promise<void> {
|
||||
if (!this._overflowBackup) {
|
||||
return;
|
||||
}
|
||||
downloadBackup(this.hass, this, this._overflowBackup, this.config);
|
||||
}
|
||||
|
||||
private async _deleteBackup(backup: BackupContent): Promise<void> {
|
||||
private async _deleteBackup(): Promise<void> {
|
||||
if (!this._overflowBackup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.backup.dialogs.delete.title"),
|
||||
text: this.hass.localize("ui.panel.config.backup.dialogs.delete.text"),
|
||||
@@ -562,9 +596,11 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteBackup(this.hass, backup.backup_id);
|
||||
if (this._selected.includes(backup.backup_id)) {
|
||||
this._selected = this._selected.filter((id) => id !== backup.backup_id);
|
||||
await deleteBackup(this.hass, this._overflowBackup.backup_id);
|
||||
if (this._selected.includes(this._overflowBackup.backup_id)) {
|
||||
this._selected = this._selected.filter(
|
||||
(id) => id !== this._overflowBackup!.backup_id
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
|
@@ -770,43 +770,39 @@ export class HaConfigDevicePage extends LitElement {
|
||||
${firstDeviceAction || actions.length
|
||||
? html`
|
||||
<div class="card-actions" slot="actions">
|
||||
<div>
|
||||
<ha-button
|
||||
href=${ifDefined(firstDeviceAction!.href)}
|
||||
rel=${ifDefined(
|
||||
firstDeviceAction!.target ? "noreferrer" : undefined
|
||||
)}
|
||||
appearance="plain"
|
||||
target=${ifDefined(firstDeviceAction!.target)}
|
||||
class=${ifDefined(firstDeviceAction!.classes)}
|
||||
.variant=${firstDeviceAction!.classes?.includes(
|
||||
"warning"
|
||||
)
|
||||
? "danger"
|
||||
: "brand"}
|
||||
.action=${firstDeviceAction!.action}
|
||||
@click=${this._deviceActionClicked}
|
||||
>
|
||||
${firstDeviceAction!.label}
|
||||
${firstDeviceAction!.icon
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class=${ifDefined(firstDeviceAction!.classes)}
|
||||
.path=${firstDeviceAction!.icon}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
${firstDeviceAction!.trailingIcon
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${firstDeviceAction!.trailingIcon}
|
||||
slot="end"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
</ha-button>
|
||||
</div>
|
||||
<ha-button
|
||||
href=${ifDefined(firstDeviceAction!.href)}
|
||||
rel=${ifDefined(
|
||||
firstDeviceAction!.target ? "noreferrer" : undefined
|
||||
)}
|
||||
appearance="plain"
|
||||
target=${ifDefined(firstDeviceAction!.target)}
|
||||
class=${ifDefined(firstDeviceAction!.classes)}
|
||||
.variant=${firstDeviceAction!.classes?.includes("warning")
|
||||
? "danger"
|
||||
: "brand"}
|
||||
.action=${firstDeviceAction!.action}
|
||||
@click=${this._deviceActionClicked}
|
||||
>
|
||||
${firstDeviceAction!.label}
|
||||
${firstDeviceAction!.icon
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class=${ifDefined(firstDeviceAction!.classes)}
|
||||
.path=${firstDeviceAction!.icon}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
${firstDeviceAction!.trailingIcon
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${firstDeviceAction!.trailingIcon}
|
||||
slot="end"
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
</ha-button>
|
||||
|
||||
${actions.length
|
||||
? html`
|
||||
|
@@ -110,7 +110,7 @@ class MoveDatadiskDialog extends LitElement {
|
||||
>
|
||||
${this._moving
|
||||
? html`
|
||||
<ha-spinner aria-label="Moving" size="large"> </ha-spinner>
|
||||
<ha-spinner aria-label="Moving" size="large"></ha-spinner>
|
||||
<p class="progress-text">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.storage.datadisk.moving_desc"
|
||||
@@ -206,8 +206,7 @@ class MoveDatadiskDialog extends LitElement {
|
||||
}
|
||||
ha-spinner {
|
||||
display: block;
|
||||
margin: 32px;
|
||||
text-align: center;
|
||||
margin: 32px auto;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
|
@@ -139,7 +139,8 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
|
||||
await updateStatisticsMetadata(
|
||||
this.hass,
|
||||
this._params!.issue.data.statistic_id,
|
||||
this._params!.issue.data.state_unit
|
||||
this._params!.issue.data.state_unit,
|
||||
this._params!.issue.data.state_unit_class
|
||||
);
|
||||
}
|
||||
this._params?.fixedCallback!();
|
||||
|
@@ -27,6 +27,7 @@ import {
|
||||
} from "../../../../../common/datetime/format_date";
|
||||
import { formatTime } from "../../../../../common/datetime/format_time";
|
||||
import type { ECOption } from "../../../../../resources/echarts";
|
||||
import { filterXSS } from "../../../../../common/util/xss";
|
||||
|
||||
export function getSuggestedMax(dayDifference: number, end: Date): number {
|
||||
let suggestedMax = new Date(end);
|
||||
@@ -201,7 +202,7 @@ function formatTooltip(
|
||||
countNegative++;
|
||||
}
|
||||
}
|
||||
return `${param.marker} ${param.seriesName}: ${value} ${unit}`;
|
||||
return `${param.marker} ${filterXSS(param.seriesName!)}: ${value} ${unit}`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
let footer = "";
|
||||
|
@@ -6,6 +6,7 @@ import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { BarSeriesOption } from "echarts/charts";
|
||||
import type { ECElementEvent } from "echarts/types/dist/shared";
|
||||
import { filterXSS } from "../../../../common/util/xss";
|
||||
import { getGraphColorByIndex } from "../../../../common/color/colors";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
@@ -96,9 +97,8 @@ export class HuiEnergyDevicesGraphCard
|
||||
}
|
||||
|
||||
private _renderTooltip(params: any) {
|
||||
const title = `<h4 style="text-align: center; margin: 0;">${this._getDeviceName(
|
||||
params.value[1]
|
||||
)}</h4>`;
|
||||
const deviceName = filterXSS(this._getDeviceName(params.value[1]));
|
||||
const title = `<h4 style="text-align: center; margin: 0;">${deviceName}</h4>`;
|
||||
const value = `${formatNumber(
|
||||
params.value[0] as number,
|
||||
this.hass.locale,
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { mdiPlay, mdiTextureBox } from "@mdi/js";
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
@@ -13,10 +13,6 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
computeActiveAreaMediaStates,
|
||||
type MediaPlayerEntity,
|
||||
} from "../../../data/media-player";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { BINARY_STATE_ON } from "../../../common/const";
|
||||
import { computeAreaName } from "../../../common/entity/compute_area_name";
|
||||
@@ -289,19 +285,15 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
);
|
||||
}
|
||||
|
||||
private _computeActiveAreaMediaStates(): MediaPlayerEntity[] {
|
||||
return computeActiveAreaMediaStates(this.hass, this._config?.area || "");
|
||||
}
|
||||
private _renderAlertSensorBadge(): TemplateResult<1> | typeof nothing {
|
||||
const states = this._computeActiveAlertStates();
|
||||
|
||||
private _renderAlertSensorBadge(
|
||||
alertStates: HassEntity[]
|
||||
): TemplateResult<1> | typeof nothing {
|
||||
if (alertStates.length === 0) {
|
||||
if (states.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
// Only render the first one when using a badge
|
||||
const stateObj = alertStates[0] as HassEntity | undefined;
|
||||
const stateObj = states[0] as HassEntity | undefined;
|
||||
|
||||
return html`
|
||||
<ha-tile-badge class="alert-badge">
|
||||
@@ -310,30 +302,6 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderMediaBadge(): TemplateResult<1> | typeof nothing {
|
||||
const states = this._computeActiveAreaMediaStates();
|
||||
|
||||
if (states.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-tile-badge
|
||||
class="media-badge"
|
||||
.label=${this.hass.localize("ui.card.area.media_playing")}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlay}></ha-svg-icon>
|
||||
</ha-tile-badge>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderCompactBadge(): TemplateResult<1> | typeof nothing {
|
||||
const alertStates = this._computeActiveAlertStates();
|
||||
return alertStates.length > 0
|
||||
? this._renderAlertSensorBadge(alertStates)
|
||||
: this._renderMediaBadge();
|
||||
}
|
||||
|
||||
private _renderAlertSensors(): TemplateResult<1> | typeof nothing {
|
||||
const states = this._computeActiveAlertStates();
|
||||
|
||||
@@ -595,7 +563,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
<div class="content ${classMap(contentClasses)}">
|
||||
<ha-tile-icon>
|
||||
${displayType === "compact"
|
||||
? this._renderCompactBadge()
|
||||
? this._renderAlertSensorBadge()
|
||||
: nothing}
|
||||
${icon
|
||||
? html`<ha-icon slot="icon" .icon=${icon}></ha-icon>`
|
||||
@@ -773,9 +741,6 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
|
||||
.alert-badge {
|
||||
--tile-badge-background-color: var(--orange-color);
|
||||
}
|
||||
.media-badge {
|
||||
--tile-badge-background-color: var(--light-blue-color);
|
||||
}
|
||||
.alerts {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@@ -49,6 +49,8 @@ import type {
|
||||
LovelaceGridOptions,
|
||||
} from "../types";
|
||||
import type { ButtonCardConfig } from "./types";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
|
||||
export const getEntityDefaultButtonAction = (entityId?: string) =>
|
||||
entityId && DOMAINS_TOGGLE.has(computeDomain(entityId))
|
||||
@@ -122,11 +124,6 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
})
|
||||
_entity?: EntityRegistryDisplayEntry;
|
||||
|
||||
private _getStateColor(stateObj: HassEntity, config: ButtonCardConfig) {
|
||||
const domain = stateObj ? computeStateDomain(stateObj) : undefined;
|
||||
return config && (config.state_color ?? domain === "light");
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return (
|
||||
(this._config?.show_icon ? 4 : 0) + (this._config?.show_name ? 1 : 0)
|
||||
@@ -166,7 +163,8 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
double_tap_action: { action: "none" },
|
||||
show_icon: true,
|
||||
show_name: true,
|
||||
state_color: true,
|
||||
color:
|
||||
config.color ?? (config.state_color === false ? "none" : undefined),
|
||||
...config,
|
||||
};
|
||||
}
|
||||
@@ -189,8 +187,6 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
? this._config.name || (stateObj ? computeStateName(stateObj) : "")
|
||||
: "";
|
||||
|
||||
const colored = stateObj && this._getStateColor(stateObj, this._config);
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
@action=${this._handleAction}
|
||||
@@ -205,7 +201,10 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
hasAction(this._config.tap_action) ? "0" : undefined
|
||||
)}
|
||||
style=${styleMap({
|
||||
"--state-color": colored ? this._computeColor(stateObj) : undefined,
|
||||
"--state-color":
|
||||
this._config.color !== "none"
|
||||
? this._computeColor(stateObj, this._config)
|
||||
: undefined,
|
||||
})}
|
||||
>
|
||||
<ha-ripple></ha-ripple>
|
||||
@@ -221,7 +220,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
style=${styleMap({
|
||||
filter: colored ? stateColorBrightness(stateObj) : undefined,
|
||||
filter: stateObj ? stateColorBrightness(stateObj) : undefined,
|
||||
height: this._config.icon_height
|
||||
? this._config.icon_height
|
||||
: undefined,
|
||||
@@ -334,7 +333,20 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
];
|
||||
}
|
||||
|
||||
private _computeColor(stateObj: HassEntity): string | undefined {
|
||||
private _computeColor(
|
||||
stateObj: HassEntity | undefined,
|
||||
config: ButtonCardConfig
|
||||
): string | undefined {
|
||||
if (config.color) {
|
||||
return !stateObj || stateActive(stateObj)
|
||||
? computeCssColor(config.color)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
if (!stateObj) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (stateObj.attributes.rgb_color) {
|
||||
return `rgb(${stateObj.attributes.rgb_color.join(",")})`;
|
||||
}
|
||||
|
@@ -83,6 +83,21 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
|
||||
private _footerElement?: LovelaceHeaderFooter;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("row-visibility-changed", (ev) =>
|
||||
this._updateRowVisibility(ev)
|
||||
);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener(
|
||||
"row-visibility-changed",
|
||||
this._updateRowVisibility
|
||||
);
|
||||
}
|
||||
|
||||
set hass(hass: HomeAssistant) {
|
||||
this._hass = hass;
|
||||
this.shadowRoot
|
||||
@@ -251,18 +266,9 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
|
||||
#states {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#states > * {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
#states > *:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#states > *:last-child {
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--entities-card-row-gap, var(--card-row-gap, 8px));
|
||||
}
|
||||
|
||||
#states > div > * {
|
||||
@@ -320,8 +326,16 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
|
||||
element.hass = this._hass;
|
||||
}
|
||||
|
||||
return html`<div>${element}</div>`;
|
||||
return html`<div ?hidden=${element.hidden}>${element}</div>`;
|
||||
}
|
||||
|
||||
private _updateRowVisibility = (ev) => {
|
||||
if (ev.detail?.value === false) {
|
||||
ev.detail?.row?.parentElement!.style.setProperty("display", "none");
|
||||
} else {
|
||||
ev.detail?.row?.parentElement!.style.setProperty("display", "");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -93,17 +93,21 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
|
||||
changedProps.has("_config") &&
|
||||
changedProps.get("_config")?.image !== this._config?.image;
|
||||
|
||||
const image =
|
||||
(typeof this._config?.image === "object" &&
|
||||
this._config.image.media_content_id) ||
|
||||
(this._config.image as string | undefined);
|
||||
if (
|
||||
(firstHass || imageChanged) &&
|
||||
typeof this._config?.image === "string" &&
|
||||
isMediaSourceContentId(this._config.image)
|
||||
typeof image === "string" &&
|
||||
isMediaSourceContentId(image)
|
||||
) {
|
||||
this._resolvedImage = undefined;
|
||||
resolveMediaSource(this.hass, this._config?.image).then((result) => {
|
||||
resolveMediaSource(this.hass, image).then((result) => {
|
||||
this._resolvedImage = result.url;
|
||||
});
|
||||
} else if (imageChanged) {
|
||||
this._resolvedImage = this._config?.image;
|
||||
this._resolvedImage = image;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
@@ -9,7 +9,7 @@ import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
|
||||
import { DOMAINS_TOGGLE } from "../../../common/const";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import type { EntityNameItem } from "../../../common/entity/compute_entity_name_display";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import "../../../components/ha-card";
|
||||
@@ -47,6 +47,11 @@ export const getEntityDefaultTileIconAction = (entityId: string) => {
|
||||
return supportsIconAction ? "toggle" : "none";
|
||||
};
|
||||
|
||||
export const DEFAULT_NAME = [
|
||||
{ type: "device" },
|
||||
{ type: "entity" },
|
||||
] satisfies EntityNameItem[];
|
||||
|
||||
@customElement("hui-tile-card")
|
||||
export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
@@ -255,7 +260,13 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
|
||||
const contentClasses = { vertical: Boolean(this._config.vertical) };
|
||||
|
||||
const name = this._config.name || computeStateName(stateObj);
|
||||
const nameConfig = this._config.name;
|
||||
|
||||
const nameDisplay =
|
||||
typeof nameConfig === "string"
|
||||
? nameConfig
|
||||
: this.hass.formatEntityName(stateObj, nameConfig || DEFAULT_NAME);
|
||||
|
||||
const active = stateActive(stateObj);
|
||||
const color = this._computeStateColor(stateObj, this._config.color);
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
@@ -267,7 +278,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
.content=${this._config.state_content}
|
||||
.name=${this._config.name}
|
||||
.name=${nameDisplay}
|
||||
>
|
||||
</state-display>
|
||||
`;
|
||||
@@ -326,7 +337,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
${renderTileBadge(stateObj, this.hass)}
|
||||
</ha-tile-icon>
|
||||
<ha-tile-info id="info">
|
||||
<span slot="primary" class="primary">${name}</span>
|
||||
<span slot="primary" class="primary">${nameDisplay}</span>
|
||||
${stateDisplay
|
||||
? html`<span slot="secondary">${stateDisplay}</span>`
|
||||
: nothing}
|
||||
|
@@ -331,14 +331,16 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
: nothing}
|
||||
${!this._reordering && uncheckedItems.length
|
||||
? html`
|
||||
<div class="header" role="separator">
|
||||
<h2>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.unchecked_items"
|
||||
)}
|
||||
</h2>
|
||||
${this._renderMenu(this._config, unavailable)}
|
||||
</div>
|
||||
${!this._config.hide_section_headers
|
||||
? html`<div class="header">
|
||||
<h2>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.unchecked_items"
|
||||
)}
|
||||
</h2>
|
||||
${this._renderMenu(this._config, unavailable)}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this._renderItems(uncheckedItems, unavailable)}
|
||||
`
|
||||
: nothing}
|
||||
@@ -366,39 +368,41 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
? html`
|
||||
<div>
|
||||
<div class="divider" role="separator"></div>
|
||||
<div class="header">
|
||||
<h2>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.checked_items"
|
||||
)}
|
||||
</h2>
|
||||
${this._todoListSupportsFeature(
|
||||
TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
)
|
||||
? html`<ha-button-menu
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
@action=${this._handleCompletedMenuAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon" class="warning">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.clear_items"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="graphic"
|
||||
.path=${mdiDeleteSweep}
|
||||
.disabled=${unavailable}
|
||||
${!this._config.hide_section_headers
|
||||
? html`<div class="header">
|
||||
<h2>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.checked_items"
|
||||
)}
|
||||
</h2>
|
||||
${this._todoListSupportsFeature(
|
||||
TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
)
|
||||
? html`<ha-button-menu
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
@action=${this._handleCompletedMenuAction}
|
||||
>
|
||||
</ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon" class="warning">
|
||||
${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.todo-list.clear_items"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
class="warning"
|
||||
slot="graphic"
|
||||
.path=${mdiDeleteSweep}
|
||||
.disabled=${unavailable}
|
||||
>
|
||||
</ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>`
|
||||
: nothing}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._renderItems(checkedItems, unavailable)}
|
||||
`
|
||||
|
@@ -1,8 +1,11 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import type { EntityNameItem } from "../../../common/entity/compute_entity_name_display";
|
||||
import type { HaDurationData } from "../../../components/ha-duration-input";
|
||||
import type { EnergySourceByType } from "../../../data/energy";
|
||||
import type { ActionConfig } from "../../../data/lovelace/config/action";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { Statistic, StatisticType } from "../../../data/recorder";
|
||||
import type { TimeFormat } from "../../../data/translation";
|
||||
import type { ForecastType } from "../../../data/weather";
|
||||
import type {
|
||||
FullCalendarView,
|
||||
@@ -25,9 +28,8 @@ import type {
|
||||
} from "../entity-rows/types";
|
||||
import type { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||
import type { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
|
||||
import type { TimeFormat } from "../../../data/translation";
|
||||
import type { HomeSummary } from "../strategies/home/helpers/home-summaries";
|
||||
import type { EnergySourceByType } from "../../../data/energy";
|
||||
import type { MediaSelectorValue } from "../../../data/selector";
|
||||
|
||||
export type AlarmPanelCardConfigState =
|
||||
| "arm_away"
|
||||
@@ -135,8 +137,10 @@ export interface ButtonCardConfig extends LovelaceCardConfig {
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
/** @deprecated use `color` instead */
|
||||
state_color?: boolean;
|
||||
show_state?: boolean;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface EnergyCardBaseConfig extends LovelaceCardConfig {
|
||||
@@ -438,7 +442,7 @@ export interface StatisticCardConfig extends LovelaceCardConfig {
|
||||
}
|
||||
|
||||
export interface PictureCardConfig extends LovelaceCardConfig {
|
||||
image?: string;
|
||||
image?: string | MediaSelectorValue;
|
||||
image_entity?: string;
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
@@ -531,6 +535,7 @@ export interface TodoListCardConfig extends LovelaceCardConfig {
|
||||
entity?: string;
|
||||
hide_completed?: boolean;
|
||||
hide_create?: boolean;
|
||||
hide_section_headers?: boolean;
|
||||
sort?: string;
|
||||
}
|
||||
|
||||
@@ -568,7 +573,7 @@ export interface WeatherForecastCardConfig extends LovelaceCardConfig {
|
||||
|
||||
export interface TileCardConfig extends LovelaceCardConfig {
|
||||
entity: string;
|
||||
name?: string;
|
||||
name?: string | EntityNameItem | EntityNameItem[];
|
||||
hide_state?: boolean;
|
||||
state_content?: string | string[];
|
||||
icon?: string;
|
||||
|
@@ -3,6 +3,8 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { entityUseDeviceName } from "../../../common/entity/compute_entity_name";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/entity/ha-entity-picker";
|
||||
import type {
|
||||
HaEntityPicker,
|
||||
@@ -12,11 +14,10 @@ import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-sortable";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { EntityConfig } from "../entity-rows/types";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
|
||||
@customElement("hui-entity-editor")
|
||||
export class HuiEntityEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entities?: EntityConfig[];
|
||||
|
||||
@@ -38,20 +39,32 @@ export class HuiEntityEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _renderItem(item: EntityConfig, index: number) {
|
||||
const stateObj = this.hass!.states[item.entity];
|
||||
const stateObj = this.hass.states[item.entity];
|
||||
|
||||
const entityName =
|
||||
stateObj && this.hass!.formatEntityName(stateObj, "entity");
|
||||
const deviceName =
|
||||
stateObj && this.hass!.formatEntityName(stateObj, "device");
|
||||
const areaName = stateObj && this.hass!.formatEntityName(stateObj, "area");
|
||||
const useDeviceName = entityUseDeviceName(
|
||||
stateObj,
|
||||
this.hass.entities,
|
||||
this.hass.devices
|
||||
);
|
||||
|
||||
const isRTL = computeRTL(this.hass!);
|
||||
const name = this.hass.formatEntityName(
|
||||
stateObj,
|
||||
useDeviceName ? { type: "device" } : { type: "entity" }
|
||||
);
|
||||
|
||||
const primary = item.name || entityName || deviceName || item.entity;
|
||||
const secondary = [areaName, entityName ? deviceName : undefined]
|
||||
.filter(Boolean)
|
||||
.join(isRTL ? " ◂ " : " ▸ ");
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
const primary = item.name || name || item.entity;
|
||||
|
||||
const secondary = this.hass.formatEntityName(
|
||||
stateObj,
|
||||
useDeviceName
|
||||
? [{ type: "area" }]
|
||||
: [{ type: "area" }, { type: "device" }],
|
||||
{
|
||||
separator: isRTL ? " ◂ " : " ▸ ",
|
||||
}
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-md-list-item class="item">
|
||||
@@ -67,14 +80,14 @@ export class HuiEntityEditor extends LitElement {
|
||||
slot="end"
|
||||
.item=${item}
|
||||
.index=${index}
|
||||
.label=${this.hass!.localize("ui.common.edit")}
|
||||
.label=${this.hass.localize("ui.common.edit")}
|
||||
.path=${mdiPencil}
|
||||
@click=${this._editItem}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
slot="end"
|
||||
.index=${index}
|
||||
.label=${this.hass!.localize("ui.common.delete")}
|
||||
.label=${this.hass.localize("ui.common.delete")}
|
||||
.path=${mdiClose}
|
||||
@click=${this._deleteItem}
|
||||
></ha-icon-button>
|
||||
@@ -109,9 +122,9 @@ export class HuiEntityEditor extends LitElement {
|
||||
return html`
|
||||
<h3>
|
||||
${this.label ||
|
||||
this.hass!.localize("ui.panel.lovelace.editor.card.generic.entities") +
|
||||
this.hass.localize("ui.panel.lovelace.editor.card.generic.entities") +
|
||||
" (" +
|
||||
this.hass!.localize("ui.panel.lovelace.editor.card.config.required") +
|
||||
this.hass.localize("ui.panel.lovelace.editor.card.config.required") +
|
||||
")"}
|
||||
</h3>
|
||||
${this.canEdit
|
||||
|
@@ -6,6 +6,7 @@ import memoizeOne from "memoize-one";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { computeEntityNameList } from "../../../../common/entity/compute_entity_name_display";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import { computeRTL } from "../../../../common/util/compute_rtl";
|
||||
import "../../../../components/data-table/ha-data-table";
|
||||
@@ -62,9 +63,14 @@ export class HuiEntityPickerTable extends LitElement {
|
||||
(entity) => {
|
||||
const stateObj = this.hass.states[entity];
|
||||
|
||||
const entityName = this.hass.formatEntityName(stateObj, "entity");
|
||||
const deviceName = this.hass.formatEntityName(stateObj, "device");
|
||||
const areaName = this.hass.formatEntityName(stateObj, "area");
|
||||
const [entityName, deviceName, areaName] = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }, { type: "area" }],
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
this.hass.floors
|
||||
);
|
||||
const name = [deviceName, entityName].filter(Boolean).join(" ");
|
||||
const domain = computeDomain(entity);
|
||||
|
||||
|
@@ -32,6 +32,8 @@ const cardConfigStruct = assign(
|
||||
double_tap_action: optional(actionConfigStruct),
|
||||
theme: optional(string()),
|
||||
show_state: optional(boolean()),
|
||||
state_color: optional(boolean()),
|
||||
color: optional(string()),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -46,6 +48,19 @@ export class HuiButtonCardEditor
|
||||
|
||||
public setConfig(config: ButtonCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
|
||||
// Migrate state_color to color
|
||||
if (config.state_color !== undefined) {
|
||||
config = {
|
||||
...config,
|
||||
color: config.state_color ? undefined : "none",
|
||||
};
|
||||
delete config.state_color;
|
||||
|
||||
fireEvent(this, "config-changed", { config: config });
|
||||
return;
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
@@ -53,11 +68,11 @@ export class HuiButtonCardEditor
|
||||
(entityId: string | undefined) =>
|
||||
[
|
||||
{ name: "entity", selector: { entity: {} } },
|
||||
{ name: "name", selector: { text: {} } },
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{ name: "name", selector: { text: {} } },
|
||||
{
|
||||
name: "icon",
|
||||
selector: {
|
||||
@@ -67,6 +82,18 @@ export class HuiButtonCardEditor
|
||||
icon_entity: "entity",
|
||||
},
|
||||
},
|
||||
{ name: "icon_height", selector: { text: { suffix: "px" } } },
|
||||
{
|
||||
name: "color",
|
||||
selector: {
|
||||
ui_color: {
|
||||
default_color: "state",
|
||||
include_state: true,
|
||||
include_none: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: "theme", selector: { theme: {} } },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -79,14 +106,6 @@ export class HuiButtonCardEditor
|
||||
{ name: "show_icon", selector: { boolean: {} } },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{ name: "icon_height", selector: { text: { suffix: "px" } } },
|
||||
{ name: "theme", selector: { theme: {} } },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "interactions",
|
||||
type: "expandable",
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { mdiGestureTap } from "@mdi/js";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { assert, assign, object, optional, string } from "superstruct";
|
||||
import { assert, assign, object, optional, string, union } from "superstruct";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-theme-picker";
|
||||
@@ -11,11 +12,12 @@ import "../../components/hui-action-editor";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
image: optional(string()),
|
||||
image: optional(union([string(), object()])),
|
||||
image_entity: optional(string()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
hold_action: optional(actionConfigStruct),
|
||||
@@ -25,47 +27,6 @@ const cardConfigStruct = assign(
|
||||
})
|
||||
);
|
||||
|
||||
const SCHEMA = [
|
||||
{ name: "image", selector: { image: {} } },
|
||||
{
|
||||
name: "image_entity",
|
||||
selector: { entity: { domain: ["image", "person"] } },
|
||||
},
|
||||
{ name: "alt_text", selector: { text: {} } },
|
||||
{ name: "theme", selector: { theme: {} } },
|
||||
{
|
||||
name: "interactions",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiGestureTap,
|
||||
schema: [
|
||||
{
|
||||
name: "tap_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "more-info",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "optional_actions",
|
||||
flatten: true,
|
||||
schema: (["hold_action", "double_tap_action"] as const).map(
|
||||
(action) => ({
|
||||
name: action,
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none" as const,
|
||||
},
|
||||
},
|
||||
})
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
|
||||
@customElement("hui-picture-card-editor")
|
||||
export class HuiPictureCardEditor
|
||||
extends LitElement
|
||||
@@ -75,6 +36,63 @@ export class HuiPictureCardEditor
|
||||
|
||||
@state() private _config?: PictureCardConfig;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
{
|
||||
name: "image",
|
||||
selector: {
|
||||
media: {
|
||||
accept: ["image/*"] as string[],
|
||||
clearable: true,
|
||||
image_upload: true,
|
||||
hide_content_type: true,
|
||||
content_id_helper: localize(
|
||||
"ui.panel.lovelace.editor.card.picture.content_id_helper"
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "image_entity",
|
||||
selector: { entity: { domain: ["image", "person"] } },
|
||||
},
|
||||
{ name: "alt_text", selector: { text: {} } },
|
||||
{ name: "theme", selector: { theme: {} } },
|
||||
{
|
||||
name: "interactions",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiGestureTap,
|
||||
schema: [
|
||||
{
|
||||
name: "tap_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "more-info",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "optional_actions",
|
||||
flatten: true,
|
||||
schema: (["hold_action", "double_tap_action"] as const).map(
|
||||
(action) => ({
|
||||
name: action,
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none" as const,
|
||||
},
|
||||
},
|
||||
})
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const
|
||||
);
|
||||
|
||||
public setConfig(config: PictureCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
@@ -88,19 +106,28 @@ export class HuiPictureCardEditor
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${SCHEMA}
|
||||
.data=${this._processData(this._config)}
|
||||
.schema=${this._schema(this.hass.localize)}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _processData = memoizeOne((config: PictureCardConfig) => ({
|
||||
...config,
|
||||
...(typeof config.image === "string"
|
||||
? { image: { media_content_id: config.image } }
|
||||
: {}),
|
||||
}));
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "theme":
|
||||
return `${this.hass!.localize(
|
||||
|
@@ -30,11 +30,15 @@ import type {
|
||||
LovelaceCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "../../card-features/types";
|
||||
import { getEntityDefaultTileIconAction } from "../../cards/hui-tile-card";
|
||||
import {
|
||||
DEFAULT_NAME,
|
||||
getEntityDefaultTileIconAction,
|
||||
} from "../../cards/hui-tile-card";
|
||||
import type { TileCardConfig } from "../../cards/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { entityNameStruct } from "../structs/entity-name-struct";
|
||||
import type { EditDetailElementEvent, EditSubElementEvent } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import { getSupportedFeaturesType } from "./hui-card-features-editor";
|
||||
@@ -43,7 +47,7 @@ const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
entity: optional(string()),
|
||||
name: optional(string()),
|
||||
name: optional(entityNameStruct),
|
||||
icon: optional(string()),
|
||||
color: optional(string()),
|
||||
show_entity_picture: optional(boolean()),
|
||||
@@ -97,11 +101,19 @@ export class HuiTileCardEditor
|
||||
type: "expandable",
|
||||
iconPath: mdiTextShort,
|
||||
schema: [
|
||||
{
|
||||
name: "name",
|
||||
selector: {
|
||||
entity_name: {
|
||||
default_name: DEFAULT_NAME,
|
||||
},
|
||||
},
|
||||
context: { entity: "entity" },
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{ name: "name", selector: { text: {} } },
|
||||
{
|
||||
name: "icon",
|
||||
selector: {
|
||||
|
@@ -32,6 +32,7 @@ const cardConfigStruct = assign(
|
||||
entity: optional(string()),
|
||||
hide_completed: optional(boolean()),
|
||||
hide_create: optional(boolean()),
|
||||
hide_section_headers: optional(boolean()),
|
||||
display_order: optional(string()),
|
||||
item_tap_action: optional(string()),
|
||||
})
|
||||
@@ -59,6 +60,7 @@ export class HuiTodoListEditor
|
||||
{ name: "theme", selector: { theme: {} } },
|
||||
{ name: "hide_completed", selector: { boolean: {} } },
|
||||
{ name: "hide_create", selector: { boolean: {} } },
|
||||
{ name: "hide_section_headers", selector: { boolean: {} } },
|
||||
{
|
||||
name: "display_order",
|
||||
selector: {
|
||||
@@ -131,6 +133,7 @@ export class HuiTodoListEditor
|
||||
.data=${this._data(this._config)}
|
||||
.schema=${this._schema(this.hass.localize, this._todoListSupportsFeature(TodoListEntityFeature.MOVE_TODO_ITEM))}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
.computeHelper=${this._computeHelperCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
</div>
|
||||
@@ -164,6 +167,7 @@ export class HuiTodoListEditor
|
||||
)})`;
|
||||
case "hide_completed":
|
||||
case "hide_create":
|
||||
case "hide_section_headers":
|
||||
case "display_order":
|
||||
case "item_tap_action":
|
||||
return this.hass!.localize(
|
||||
@@ -176,6 +180,19 @@ export class HuiTodoListEditor
|
||||
}
|
||||
};
|
||||
|
||||
private _computeHelperCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "hide_section_headers":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.todo-list.${schema.name}_helper`
|
||||
);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return configElementStyle;
|
||||
}
|
||||
|
@@ -4,8 +4,8 @@ import { property, query, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { handleStructError } from "../../../common/structs/handle-errors";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-spinner";
|
||||
@@ -57,8 +57,6 @@ export abstract class HuiElementEditor<
|
||||
|
||||
@property({ attribute: false }) public context?: C;
|
||||
|
||||
@property({ attribute: false }) public schema?;
|
||||
|
||||
@state() private _config?: T;
|
||||
|
||||
@state() private _configElement?: LovelaceGenericElementEditor;
|
||||
@@ -314,9 +312,6 @@ export abstract class HuiElementEditor<
|
||||
if (this._configElement && changedProperties.has("context")) {
|
||||
this._configElement.context = this.context;
|
||||
}
|
||||
if (this._configElement && changedProperties.has("schema")) {
|
||||
this._configElement.schema = this.schema;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleUIConfigChanged(ev: UIConfigChangedEvent<T>) {
|
||||
@@ -404,7 +399,6 @@ export abstract class HuiElementEditor<
|
||||
configElement.lovelace = this.lovelace;
|
||||
}
|
||||
configElement.context = this.context;
|
||||
configElement.schema = this.schema;
|
||||
configElement.addEventListener("config-changed", (ev) =>
|
||||
this._handleUIConfigChanged(ev as UIConfigChangedEvent<T>)
|
||||
);
|
||||
|
19
src/panels/lovelace/editor/hui-form-element-editor.ts
Normal file
19
src/panels/lovelace/editor/hui-form-element-editor.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HaFormSchema } from "../../../components/ha-form/types";
|
||||
import type { LovelaceConfigForm } from "../types";
|
||||
import { HuiElementEditor } from "./hui-element-editor";
|
||||
|
||||
@customElement("hui-form-element-editor")
|
||||
export class HuiFormElementEditor extends HuiElementEditor {
|
||||
@property({ attribute: false }) public schema!: HaFormSchema[];
|
||||
|
||||
protected async getConfigForm(): Promise<LovelaceConfigForm | undefined> {
|
||||
return { schema: this.schema };
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-form-element-editor": HuiFormElementEditor;
|
||||
}
|
||||
}
|
@@ -12,6 +12,7 @@ import "./feature-editor/hui-card-feature-element-editor";
|
||||
import "./header-footer-editor/hui-header-footer-element-editor";
|
||||
import "./heading-badge-editor/hui-heading-badge-element-editor";
|
||||
import type { HuiElementEditor } from "./hui-element-editor";
|
||||
import "./hui-form-element-editor";
|
||||
import "./picture-element-editor/hui-picture-element-element-editor";
|
||||
import type { GUIModeChangedEvent, SubElementEditorConfig } from "./types";
|
||||
|
||||
@@ -83,6 +84,18 @@ export class HuiSubElementEditor extends LitElement {
|
||||
private _renderEditor() {
|
||||
const type = this.config.type;
|
||||
|
||||
if (this.schema) {
|
||||
return html`
|
||||
<hui-form-element-editor
|
||||
class="editor"
|
||||
.hass=${this.hass}
|
||||
.value=${this.config.elementConfig}
|
||||
.schema=${this.schema}
|
||||
.context=${this.config.context}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
></hui-form-element-editor>
|
||||
`;
|
||||
}
|
||||
switch (type) {
|
||||
case "row":
|
||||
return html`
|
||||
@@ -91,7 +104,6 @@ export class HuiSubElementEditor extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.value=${this.config.elementConfig}
|
||||
.context=${this.config.context}
|
||||
.schema=${this.schema}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-row-element-editor>
|
||||
|
22
src/panels/lovelace/editor/structs/entity-name-struct.ts
Normal file
22
src/panels/lovelace/editor/structs/entity-name-struct.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { array, literal, object, string, union } from "superstruct";
|
||||
|
||||
const entityNameItemStruct = union([
|
||||
object({
|
||||
type: literal("text"),
|
||||
text: string(),
|
||||
}),
|
||||
object({
|
||||
type: union([
|
||||
literal("entity"),
|
||||
literal("device"),
|
||||
literal("area"),
|
||||
literal("floor"),
|
||||
]),
|
||||
}),
|
||||
string(),
|
||||
]);
|
||||
|
||||
export const entityNameStruct = union([
|
||||
entityNameItemStruct,
|
||||
array(entityNameItemStruct),
|
||||
]);
|
@@ -318,7 +318,7 @@ class HUIRoot extends LitElement {
|
||||
menu-corner="END"
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${label}
|
||||
.id="button-${index}"
|
||||
.path=${item.icon}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
@@ -340,6 +340,9 @@ class HUIRoot extends LitElement {
|
||||
`
|
||||
)}
|
||||
</ha-button-menu>
|
||||
<ha-tooltip placement="bottom" .for="button-${index}">
|
||||
${label}
|
||||
</ha-tooltip>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button
|
||||
|
@@ -7,7 +7,13 @@ import type {
|
||||
EntityConfig,
|
||||
LovelaceRow,
|
||||
} from "../entity-rows/types";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"row-visibility-changed": { row: LovelaceRow; value: boolean };
|
||||
}
|
||||
}
|
||||
@customElement("hui-conditional-row")
|
||||
class HuiConditionalRow extends HuiConditionalBase implements LovelaceRow {
|
||||
public setConfig(config: ConditionalRowConfig): void {
|
||||
@@ -26,6 +32,15 @@ class HuiConditionalRow extends HuiConditionalBase implements LovelaceRow {
|
||||
: config.row
|
||||
) as LovelaceRow;
|
||||
}
|
||||
|
||||
protected setVisibility(conditionMet: boolean): void {
|
||||
const visible = this.preview || conditionMet;
|
||||
const previouslyHidden = this.hidden;
|
||||
super.setVisibility(conditionMet);
|
||||
if (previouslyHidden !== this.hidden) {
|
||||
fireEvent(this, "row-visibility-changed", { row: this, value: visible });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -270,15 +270,12 @@ export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
})),
|
||||
],
|
||||
} satisfies HeadingCardConfig,
|
||||
...entities.map((e) => {
|
||||
const stateObj = hass.states[e];
|
||||
return {
|
||||
...computeTileCard(e),
|
||||
name:
|
||||
hass.formatEntityName(stateObj, "entity") ||
|
||||
hass.formatEntityName(stateObj, "device"),
|
||||
};
|
||||
}),
|
||||
...entities.map((e) => ({
|
||||
...computeTileCard(e),
|
||||
name: {
|
||||
type: "entity",
|
||||
},
|
||||
})),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
@@ -161,6 +161,9 @@ export const haStyleDialog = css`
|
||||
--mdc-dialog-min-height: 100svh;
|
||||
--mdc-dialog-max-height: 100vh;
|
||||
--mdc-dialog-max-height: 100svh;
|
||||
--dialog-surface-padding: var(--safe-area-inset-top)
|
||||
var(--safe-area-inset-right) var(--safe-area-inset-bottom)
|
||||
var(--safe-area-inset-left);
|
||||
--vertical-align-dialog: flex-end;
|
||||
--ha-dialog-border-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
|
@@ -152,6 +152,10 @@ export const semanticColorStyles = css`
|
||||
--ha-color-on-success-quiet: var(--ha-color-green-50);
|
||||
--ha-color-on-success-normal: var(--ha-color-green-40);
|
||||
--ha-color-on-success-loud: var(--white-color);
|
||||
|
||||
/* Surfaces */
|
||||
--ha-color-surface-default: var(--ha-color-neutral-95);
|
||||
--ha-color-on-surface-default: var(--ha-color-neutral-05);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -280,5 +284,9 @@ export const darkSemanticColorStyles = css`
|
||||
--ha-color-on-success-quiet: var(--ha-color-green-70);
|
||||
--ha-color-on-success-normal: var(--ha-color-green-60);
|
||||
--ha-color-on-success-loud: var(--white-color);
|
||||
|
||||
/* Surfaces */
|
||||
--ha-color-surface-default: var(--ha-color-neutral-10);
|
||||
--ha-color-on-surface-default: var(--ha-color-neutral-95);
|
||||
}
|
||||
`;
|
||||
|
@@ -106,8 +106,7 @@
|
||||
}
|
||||
},
|
||||
"area": {
|
||||
"area_not_found": "Area not found.",
|
||||
"media_playing": "Media playing"
|
||||
"area_not_found": "Area not found."
|
||||
},
|
||||
"automation": {
|
||||
"last_triggered": "Last triggered",
|
||||
@@ -657,6 +656,18 @@
|
||||
"placeholder": "Select an entity",
|
||||
"create_helper": "Create a new {domain, select, \n undefined {} \n other {{domain} }\n } helper."
|
||||
},
|
||||
"entity-name-picker": {
|
||||
"types": {
|
||||
"floor": "Floor",
|
||||
"area": "Area",
|
||||
"device": "Device",
|
||||
"entity": "Entity",
|
||||
"area_missing": "No area assigned",
|
||||
"floor_missing": "No floor assigned",
|
||||
"device_missing": "No related device"
|
||||
},
|
||||
"add": "Add"
|
||||
},
|
||||
"entity-attribute-picker": {
|
||||
"attribute": "Attribute",
|
||||
"show_attributes": "Show attributes"
|
||||
@@ -5830,8 +5841,8 @@
|
||||
"change_channel_initiated_text": "The channel change has been initiated and will complete in {delay} {delay, plural,\n one {minute}\n other {minutes}\n}.",
|
||||
"change_channel_invalid": "Invalid channel",
|
||||
"change_channel_label": "Channel",
|
||||
"change_channel_multiprotocol_enabled_title": "The Thread radio has multiprotocol enabled",
|
||||
"change_channel_multiprotocol_enabled_text": "To change channel when the Thread radio has multiprotocol enabled, please use the hardware settings menu.",
|
||||
"change_channel_multiprotocol_enabled_title": "The Thread adapter has multiprotocol enabled",
|
||||
"change_channel_multiprotocol_enabled_text": "To change channel when the Thread adapter has multiprotocol enabled, please use the hardware settings menu.",
|
||||
"change_channel_range": "Channel must be in the range 11 to 26",
|
||||
"change_channel_text": "Initiating a channel change for your Home Assistant Thread network should be performed with caution. Some Thread devices may not migrate to the new channel automatically and, if the new channel is congested, your Thread devices may become intermittently unavailable. Some devices may need to be manually re-joined to your Thread network before they show in Home Assistant again. This action cannot be reversed (without performing another channel change).",
|
||||
"thread_network_info": "Thread network information",
|
||||
@@ -5876,12 +5887,12 @@
|
||||
"devices_offline": "{count} offline",
|
||||
"update_button": "Update configuration",
|
||||
"download_backup": "Download backup",
|
||||
"migrate_radio": "Migrate radio",
|
||||
"migrate_radio": "Migrate adapter",
|
||||
"network_settings_title": "Network settings",
|
||||
"change_channel": "Change channel",
|
||||
"channel_dialog": {
|
||||
"title": "Multiprotocol add-on in use",
|
||||
"text": "Zigbee and Thread share the same radio and must use the same channel. Change the channel of both networks by reconfiguring multiprotocol from the hardware menu."
|
||||
"text": "Zigbee and Thread share the same adapter and must use the same channel. Change the channel of both networks by reconfiguring multiprotocol from the hardware menu."
|
||||
}
|
||||
},
|
||||
"add_device_page": {
|
||||
@@ -7869,7 +7880,8 @@
|
||||
},
|
||||
"picture": {
|
||||
"name": "Picture",
|
||||
"description": "The Picture card allows you to set an image to use for navigation to various paths in your interface or to perform an action."
|
||||
"description": "The Picture card allows you to set an image to use for navigation to various paths in your interface or to perform an action.",
|
||||
"content_id_helper": "Enter a media_source id or a URL for the image to be displayed."
|
||||
},
|
||||
"picture-elements": {
|
||||
"name": "Picture elements",
|
||||
@@ -7923,6 +7935,8 @@
|
||||
"integration_not_loaded": "This card requires the `todo` integration to be set up.",
|
||||
"hide_completed": "Hide completed items",
|
||||
"hide_create": "Hide 'Add item' field",
|
||||
"hide_section_headers": "Hide section headers",
|
||||
"hide_section_headers_helper": "Removes the 'Active' and 'Completed' sections with the overflow menus.",
|
||||
"display_order": "Display order",
|
||||
"item_tap_action": "Item tap behavior",
|
||||
"actions": {
|
||||
|
@@ -9,7 +9,10 @@ import type {
|
||||
HassServiceTarget,
|
||||
MessageBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import type { EntityNameType } from "./common/translations/entity-state";
|
||||
import type {
|
||||
EntityNameItem,
|
||||
EntityNameOptions,
|
||||
} from "./common/entity/compute_entity_name_display";
|
||||
import type { LocalizeFunc } from "./common/translations/localize";
|
||||
import type { AreaRegistryEntry } from "./data/area_registry";
|
||||
import type { DeviceRegistryEntry } from "./data/device_registry";
|
||||
@@ -288,8 +291,8 @@ export interface HomeAssistant {
|
||||
formatEntityAttributeName(stateObj: HassEntity, attribute: string): string;
|
||||
formatEntityName(
|
||||
stateObj: HassEntity,
|
||||
type: EntityNameType | EntityNameType[],
|
||||
separator?: string
|
||||
type: EntityNameItem | EntityNameItem[],
|
||||
separator?: EntityNameOptions
|
||||
): string;
|
||||
}
|
||||
|
||||
|
@@ -1,196 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
computeActiveAreaMediaStates,
|
||||
type MediaPlayerEntity,
|
||||
} from "../../../src/data/media-player";
|
||||
|
||||
describe("computeActiveAreaMediaStates", () => {
|
||||
it("returns playing media entities in the area", () => {
|
||||
const hass = {
|
||||
areas: { living_room: { area_id: "living_room" } },
|
||||
entities: {
|
||||
"media_player.tv": {
|
||||
entity_id: "media_player.tv",
|
||||
area_id: "living_room",
|
||||
},
|
||||
"media_player.speaker": {
|
||||
entity_id: "media_player.speaker",
|
||||
area_id: "living_room",
|
||||
},
|
||||
},
|
||||
states: {
|
||||
"media_player.tv": {
|
||||
entity_id: "media_player.tv",
|
||||
state: "playing",
|
||||
} as MediaPlayerEntity,
|
||||
"media_player.speaker": {
|
||||
entity_id: "media_player.speaker",
|
||||
state: "idle",
|
||||
} as MediaPlayerEntity,
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = computeActiveAreaMediaStates(hass, "living_room");
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].entity_id).toBe("media_player.tv");
|
||||
expect(result[0].state).toBe("playing");
|
||||
});
|
||||
|
||||
it("returns empty array when no area is configured", () => {
|
||||
const hass = {
|
||||
areas: {},
|
||||
entities: {},
|
||||
states: {},
|
||||
} as any;
|
||||
|
||||
const result = computeActiveAreaMediaStates(hass, "living_room");
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("returns empty array when media player is not assigned to area", () => {
|
||||
const hass = {
|
||||
areas: { living_room: { area_id: "living_room" } },
|
||||
entities: {
|
||||
"media_player.bedroom": { entity_id: "media_player.bedroom" },
|
||||
},
|
||||
states: {
|
||||
"media_player.bedroom": {
|
||||
entity_id: "media_player.bedroom",
|
||||
state: "playing",
|
||||
} as MediaPlayerEntity,
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = computeActiveAreaMediaStates(hass, "living_room");
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("returns playing speaker when speaker is playing", () => {
|
||||
const hass = {
|
||||
areas: { living_room: { area_id: "living_room" } },
|
||||
entities: {
|
||||
"media_player.tv": {
|
||||
entity_id: "media_player.tv",
|
||||
area_id: "living_room",
|
||||
},
|
||||
"media_player.speaker": {
|
||||
entity_id: "media_player.speaker",
|
||||
area_id: "living_room",
|
||||
},
|
||||
},
|
||||
states: {
|
||||
"media_player.tv": {
|
||||
entity_id: "media_player.tv",
|
||||
state: "idle",
|
||||
} as MediaPlayerEntity,
|
||||
"media_player.speaker": {
|
||||
entity_id: "media_player.speaker",
|
||||
state: "playing",
|
||||
} as MediaPlayerEntity,
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = computeActiveAreaMediaStates(hass, "living_room");
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].entity_id).toBe("media_player.speaker");
|
||||
expect(result[0].state).toBe("playing");
|
||||
});
|
||||
|
||||
it("returns media entities that inherit area from device", () => {
|
||||
const hass = {
|
||||
areas: { living_room: { area_id: "living_room" } },
|
||||
devices: {
|
||||
device_tv: {
|
||||
id: "device_tv",
|
||||
area_id: "living_room",
|
||||
},
|
||||
},
|
||||
entities: {
|
||||
"media_player.tv": {
|
||||
entity_id: "media_player.tv",
|
||||
device_id: "device_tv", // Entity belongs to device
|
||||
// No direct area_id - inherits from device
|
||||
},
|
||||
},
|
||||
states: {
|
||||
"media_player.tv": {
|
||||
entity_id: "media_player.tv",
|
||||
state: "playing",
|
||||
} as MediaPlayerEntity,
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = computeActiveAreaMediaStates(hass, "living_room");
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].entity_id).toBe("media_player.tv");
|
||||
expect(result[0].state).toBe("playing");
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeActiveAreaMediaStates badge priority", () => {
|
||||
it("prioritizes alert badge over media badge", () => {
|
||||
const hass = {
|
||||
areas: { living_room: { area_id: "living_room" } },
|
||||
entities: {
|
||||
"binary_sensor.door": {
|
||||
entity_id: "binary_sensor.door",
|
||||
area_id: "living_room",
|
||||
},
|
||||
"media_player.tv": {
|
||||
entity_id: "media_player.tv",
|
||||
area_id: "living_room",
|
||||
},
|
||||
},
|
||||
states: {
|
||||
"binary_sensor.door": {
|
||||
entity_id: "binary_sensor.door",
|
||||
state: "on",
|
||||
} as MediaPlayerEntity,
|
||||
"media_player.tv": {
|
||||
entity_id: "media_player.tv",
|
||||
state: "playing",
|
||||
} as MediaPlayerEntity,
|
||||
},
|
||||
} as any;
|
||||
|
||||
const alertStates = hass.states["binary_sensor.door"]
|
||||
? [hass.states["binary_sensor.door"]]
|
||||
: [];
|
||||
const mediaStates = computeActiveAreaMediaStates(hass, "living_room");
|
||||
|
||||
// Alert badge should take priority
|
||||
expect(alertStates.length > 0).toBe(true);
|
||||
expect(mediaStates.length > 0).toBe(true);
|
||||
expect(alertStates.length > 0 ? "alert" : "media").toBe("alert");
|
||||
});
|
||||
|
||||
it("shows media badge when no alerts", () => {
|
||||
const hass = {
|
||||
areas: { living_room: { area_id: "living_room" } },
|
||||
entities: {
|
||||
"media_player.tv": {
|
||||
entity_id: "media_player.tv",
|
||||
area_id: "living_room",
|
||||
},
|
||||
},
|
||||
states: {
|
||||
"media_player.tv": {
|
||||
entity_id: "media_player.tv",
|
||||
state: "playing",
|
||||
} as MediaPlayerEntity,
|
||||
},
|
||||
} as any;
|
||||
|
||||
const alertStates: MediaPlayerEntity[] = [];
|
||||
const mediaStates = computeActiveAreaMediaStates(hass, "living_room");
|
||||
|
||||
expect(alertStates.length).toBe(0);
|
||||
expect(mediaStates.length).toBe(1);
|
||||
expect(alertStates.length > 0 ? "alert" : "media").toBe("media");
|
||||
});
|
||||
});
|
408
test/common/entity/compute_entity_name_display.test.ts
Normal file
408
test/common/entity/compute_entity_name_display.test.ts
Normal file
@@ -0,0 +1,408 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
computeEntityNameDisplay,
|
||||
computeEntityNameList,
|
||||
} from "../../../src/common/entity/compute_entity_name_display";
|
||||
import type { HomeAssistant } from "../../../src/types";
|
||||
import {
|
||||
mockArea,
|
||||
mockDevice,
|
||||
mockEntity,
|
||||
mockFloor,
|
||||
mockStateObj,
|
||||
} from "./context/context-mock";
|
||||
|
||||
describe("computeEntityNameDisplay", () => {
|
||||
it("returns text when all items are text", () => {
|
||||
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
|
||||
const hass = {
|
||||
entities: {},
|
||||
devices: {},
|
||||
areas: {},
|
||||
floors: {},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const result = computeEntityNameDisplay(
|
||||
stateObj,
|
||||
[
|
||||
{ type: "text", text: "Hello" },
|
||||
{ type: "text", text: "World" },
|
||||
],
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
expect(result).toBe("Hello World");
|
||||
});
|
||||
|
||||
it("uses custom separator for text items", () => {
|
||||
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
|
||||
const hass = {
|
||||
entities: {},
|
||||
devices: {},
|
||||
areas: {},
|
||||
floors: {},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const result = computeEntityNameDisplay(
|
||||
stateObj,
|
||||
[
|
||||
{ type: "text", text: "Hello" },
|
||||
{ type: "text", text: "World" },
|
||||
],
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors,
|
||||
{ separator: " - " }
|
||||
);
|
||||
|
||||
expect(result).toBe("Hello - World");
|
||||
});
|
||||
|
||||
it("returns entity name", () => {
|
||||
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
|
||||
const hass = {
|
||||
entities: {
|
||||
"light.kitchen": mockEntity({
|
||||
entity_id: "light.kitchen",
|
||||
name: "Kitchen Light",
|
||||
}),
|
||||
},
|
||||
devices: {},
|
||||
areas: {},
|
||||
floors: {},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const result = computeEntityNameDisplay(
|
||||
stateObj,
|
||||
{ type: "entity" },
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
expect(result).toBe("Kitchen Light");
|
||||
});
|
||||
|
||||
it("replaces entity with device name when entity uses device name", () => {
|
||||
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
|
||||
const hass = {
|
||||
entities: {
|
||||
"light.kitchen": mockEntity({
|
||||
entity_id: "light.kitchen",
|
||||
name: "Kitchen Device",
|
||||
device_id: "dev1",
|
||||
}),
|
||||
},
|
||||
devices: {
|
||||
dev1: mockDevice({
|
||||
id: "dev1",
|
||||
name: "Kitchen Device",
|
||||
}),
|
||||
},
|
||||
areas: {},
|
||||
floors: {},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const result = computeEntityNameDisplay(
|
||||
stateObj,
|
||||
{ type: "entity" },
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
expect(result).toBe("Kitchen Device");
|
||||
});
|
||||
|
||||
it("does not replace entity with device when device is already included", () => {
|
||||
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
|
||||
const hass = {
|
||||
entities: {
|
||||
"light.kitchen": mockEntity({
|
||||
entity_id: "light.kitchen",
|
||||
name: "Kitchen Device",
|
||||
device_id: "dev1",
|
||||
}),
|
||||
},
|
||||
devices: {
|
||||
dev1: mockDevice({
|
||||
id: "dev1",
|
||||
name: "Kitchen Device",
|
||||
}),
|
||||
},
|
||||
areas: {},
|
||||
floors: {},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const result = computeEntityNameDisplay(
|
||||
stateObj,
|
||||
[{ type: "entity" }, { type: "device" }],
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
// Since entity name equals device name, entity returns undefined
|
||||
// So we only get the device name
|
||||
expect(result).toBe("Kitchen Device");
|
||||
});
|
||||
|
||||
it("returns combined entity and area names", () => {
|
||||
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
|
||||
const hass = {
|
||||
entities: {
|
||||
"light.kitchen": mockEntity({
|
||||
entity_id: "light.kitchen",
|
||||
name: "Ceiling Light",
|
||||
area_id: "kitchen",
|
||||
}),
|
||||
},
|
||||
devices: {},
|
||||
areas: {
|
||||
kitchen: mockArea({
|
||||
area_id: "kitchen",
|
||||
name: "Kitchen",
|
||||
}),
|
||||
},
|
||||
floors: {},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const result = computeEntityNameDisplay(
|
||||
stateObj,
|
||||
[{ type: "area" }, { type: "entity" }],
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
expect(result).toBe("Kitchen Ceiling Light");
|
||||
});
|
||||
|
||||
it("returns combined device and area names", () => {
|
||||
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
|
||||
const hass = {
|
||||
entities: {
|
||||
"light.kitchen": mockEntity({
|
||||
entity_id: "light.kitchen",
|
||||
name: "Light",
|
||||
device_id: "dev1",
|
||||
}),
|
||||
},
|
||||
devices: {
|
||||
dev1: mockDevice({
|
||||
id: "dev1",
|
||||
name: "Smart Light",
|
||||
area_id: "kitchen",
|
||||
}),
|
||||
},
|
||||
areas: {
|
||||
kitchen: mockArea({
|
||||
area_id: "kitchen",
|
||||
name: "Kitchen",
|
||||
}),
|
||||
},
|
||||
floors: {},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const result = computeEntityNameDisplay(
|
||||
stateObj,
|
||||
[{ type: "area" }, { type: "device" }],
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
expect(result).toBe("Kitchen Smart Light");
|
||||
});
|
||||
|
||||
it("returns floor name", () => {
|
||||
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
|
||||
const hass = {
|
||||
entities: {
|
||||
"light.kitchen": mockEntity({
|
||||
entity_id: "light.kitchen",
|
||||
name: "Light",
|
||||
area_id: "kitchen",
|
||||
}),
|
||||
},
|
||||
devices: {},
|
||||
areas: {
|
||||
kitchen: mockArea({
|
||||
area_id: "kitchen",
|
||||
name: "Kitchen",
|
||||
floor_id: "first",
|
||||
}),
|
||||
},
|
||||
floors: {
|
||||
first: mockFloor({
|
||||
floor_id: "first",
|
||||
name: "First Floor",
|
||||
}),
|
||||
},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const result = computeEntityNameDisplay(
|
||||
stateObj,
|
||||
{ type: "floor" },
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
expect(result).toBe("First Floor");
|
||||
});
|
||||
|
||||
it("filters out undefined names when combining", () => {
|
||||
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
|
||||
const hass = {
|
||||
entities: {
|
||||
"light.kitchen": mockEntity({
|
||||
entity_id: "light.kitchen",
|
||||
name: "Light",
|
||||
}),
|
||||
},
|
||||
devices: {},
|
||||
areas: {},
|
||||
floors: {},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const result = computeEntityNameDisplay(
|
||||
stateObj,
|
||||
[{ type: "area" }, { type: "entity" }, { type: "floor" }],
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
// Area and floor don't exist, so only entity name is included
|
||||
expect(result).toBe("Light");
|
||||
});
|
||||
|
||||
it("mixes text with entity types", () => {
|
||||
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
|
||||
const hass = {
|
||||
entities: {
|
||||
"light.kitchen": mockEntity({
|
||||
entity_id: "light.kitchen",
|
||||
name: "Light",
|
||||
area_id: "kitchen",
|
||||
}),
|
||||
},
|
||||
devices: {},
|
||||
areas: {
|
||||
kitchen: mockArea({
|
||||
area_id: "kitchen",
|
||||
name: "Kitchen",
|
||||
}),
|
||||
},
|
||||
floors: {},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const result = computeEntityNameDisplay(
|
||||
stateObj,
|
||||
[{ type: "area" }, { type: "text", text: "-" }, { type: "entity" }],
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
expect(result).toBe("Kitchen - Light");
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeEntityNameList", () => {
|
||||
it("returns list of names for each item type", () => {
|
||||
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
|
||||
const hass = {
|
||||
entities: {
|
||||
"light.kitchen": mockEntity({
|
||||
entity_id: "light.kitchen",
|
||||
name: "Light",
|
||||
device_id: "dev1",
|
||||
area_id: "kitchen",
|
||||
}),
|
||||
},
|
||||
devices: {
|
||||
dev1: mockDevice({
|
||||
id: "dev1",
|
||||
name: "Smart Device",
|
||||
area_id: "kitchen",
|
||||
}),
|
||||
},
|
||||
areas: {
|
||||
kitchen: mockArea({
|
||||
area_id: "kitchen",
|
||||
name: "Kitchen",
|
||||
floor_id: "first",
|
||||
}),
|
||||
},
|
||||
floors: {
|
||||
first: mockFloor({
|
||||
floor_id: "first",
|
||||
name: "First Floor",
|
||||
}),
|
||||
},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const result = computeEntityNameList(
|
||||
stateObj,
|
||||
[
|
||||
{ type: "floor" },
|
||||
{ type: "area" },
|
||||
{ type: "device" },
|
||||
{ type: "entity" },
|
||||
{ type: "text", text: "Custom" },
|
||||
],
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
"First Floor",
|
||||
"Kitchen",
|
||||
"Smart Device",
|
||||
"Light",
|
||||
"Custom",
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns undefined for missing context items", () => {
|
||||
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
|
||||
const hass = {
|
||||
entities: {
|
||||
"light.kitchen": mockEntity({
|
||||
entity_id: "light.kitchen",
|
||||
name: "Light",
|
||||
}),
|
||||
},
|
||||
devices: {},
|
||||
areas: {},
|
||||
floors: {},
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const result = computeEntityNameList(
|
||||
stateObj,
|
||||
[{ type: "device" }, { type: "area" }, { type: "floor" }],
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
|
||||
expect(result).toEqual([undefined, undefined, undefined]);
|
||||
});
|
||||
});
|
398
yarn.lock
398
yarn.lock
@@ -1613,19 +1613,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/config-helpers@npm:^0.3.1":
|
||||
version: 0.3.1
|
||||
resolution: "@eslint/config-helpers@npm:0.3.1"
|
||||
checksum: 10/fc1a90ef6180aa4b5187cee04cfc566abb2a32b77ca3e7eeb4312c7388f6898221adaf8451d9ddb22e0b8860d900fefb1eb1435e4f32f8d8732de87f14605f8f
|
||||
"@eslint/config-helpers@npm:^0.4.0":
|
||||
version: 0.4.0
|
||||
resolution: "@eslint/config-helpers@npm:0.4.0"
|
||||
dependencies:
|
||||
"@eslint/core": "npm:^0.16.0"
|
||||
checksum: 10/d5fdbf927a77b98d2462f025f8b1a5b610609201f8d1dd47032a2937842f02bf3bdf9cb672025c83a00f3255dfd218172f989caa724853c4a8f434124a6d79ff
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/core@npm:^0.15.2":
|
||||
version: 0.15.2
|
||||
resolution: "@eslint/core@npm:0.15.2"
|
||||
"@eslint/core@npm:^0.16.0":
|
||||
version: 0.16.0
|
||||
resolution: "@eslint/core@npm:0.16.0"
|
||||
dependencies:
|
||||
"@types/json-schema": "npm:^7.0.15"
|
||||
checksum: 10/41d6273bbc6897cca34a2ca4e80a24bf6f1d43519456ebaa3c38f187da2d9e06f442c64f6e2a2813f055dce35e5cea33a21d0ac3b5b0830b7165641c640faf5d
|
||||
checksum: 10/3cea45971b2d0114267b6101b673270b5d8047448cc7a8cbfdca0b0245e9d5e081cb25f13551dc7d55a090f98c13b33f0c4999f8ee8ab058537e6037629a0f71
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1646,10 +1648,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/js@npm:9.36.0":
|
||||
version: 9.36.0
|
||||
resolution: "@eslint/js@npm:9.36.0"
|
||||
checksum: 10/a0542f529f87b9ad69ef85c47b0c070b763591a61773b131a9d1d53934a587f0708c05a1a8f48a6805486004a4922c91d696c1e4835ff61f8750ffbded2f0c30
|
||||
"@eslint/js@npm:9.37.0":
|
||||
version: 9.37.0
|
||||
resolution: "@eslint/js@npm:9.37.0"
|
||||
checksum: 10/2ead426ed47af0b914c7d7064eb59fede858483cf9511f78ded840708aca578138f2a6c375916d520f4f2ecf25945f4bd47b8a84e42106b4eb46f7708a36db1d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1660,13 +1662,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint/plugin-kit@npm:^0.3.5":
|
||||
version: 0.3.5
|
||||
resolution: "@eslint/plugin-kit@npm:0.3.5"
|
||||
"@eslint/plugin-kit@npm:^0.4.0":
|
||||
version: 0.4.0
|
||||
resolution: "@eslint/plugin-kit@npm:0.4.0"
|
||||
dependencies:
|
||||
"@eslint/core": "npm:^0.15.2"
|
||||
"@eslint/core": "npm:^0.16.0"
|
||||
levn: "npm:^0.4.1"
|
||||
checksum: 10/b8552d79c3091446b07d8b87a9a8ccb8cdee4d933c0ed46b8f61029c3382246fec8d04ea7d1e61656d9275263205ccaa40019fd7581bbce897eca3eda42d5dad
|
||||
checksum: 10/2c37ca00e352447215aeadcaff5765faead39695f1cb91cd3079a43261b234887caf38edc462811bb3401acf8c156c04882f87740df936838290c705351483be
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1696,15 +1698,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/ecma402-abstract@npm:2.3.4":
|
||||
version: 2.3.4
|
||||
resolution: "@formatjs/ecma402-abstract@npm:2.3.4"
|
||||
"@formatjs/ecma402-abstract@npm:2.3.5":
|
||||
version: 2.3.5
|
||||
resolution: "@formatjs/ecma402-abstract@npm:2.3.5"
|
||||
dependencies:
|
||||
"@formatjs/fast-memoize": "npm:2.2.7"
|
||||
"@formatjs/intl-localematcher": "npm:0.6.1"
|
||||
"@formatjs/intl-localematcher": "npm:0.6.2"
|
||||
decimal.js: "npm:^10.4.3"
|
||||
tslib: "npm:^2.8.0"
|
||||
checksum: 10/573971ffc291096a4b9fcc80b4708124e89bf2e3ac50e0f78b41eb797e9aa1b842f4dc3665e4467a853c738386821769d9e40408a1d25bc73323a1f057a16cf2
|
||||
checksum: 10/254651057170836237dc4f0fbb372157f97133c4dcee414007e0cdb5b589baf0546c2f6337d117b988ee0a4f0a4d8247780aaa9e96b410c568495f162c40dc50
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1717,144 +1719,144 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/icu-messageformat-parser@npm:2.11.2":
|
||||
version: 2.11.2
|
||||
resolution: "@formatjs/icu-messageformat-parser@npm:2.11.2"
|
||||
"@formatjs/icu-messageformat-parser@npm:2.11.3":
|
||||
version: 2.11.3
|
||||
resolution: "@formatjs/icu-messageformat-parser@npm:2.11.3"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.4"
|
||||
"@formatjs/icu-skeleton-parser": "npm:1.8.14"
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.5"
|
||||
"@formatjs/icu-skeleton-parser": "npm:1.8.15"
|
||||
tslib: "npm:^2.8.0"
|
||||
checksum: 10/e919eb2a132ac1d54fb1a7e3a3254007649b55196d3818090df92a4268dcddf20cbdf863c06039fbbe7a35a8a3f17bdc172dade99d1f17c1d8a95dcec444c3e3
|
||||
checksum: 10/339f5ff5ea7417e2db7f01bd41340f78fd5a8e56a66e723272d21ce7ab4b265dcb45748cdca76eac7137e2b5e6767986812b471e011b4602cf7afbc6da57fb98
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/icu-skeleton-parser@npm:1.8.14":
|
||||
version: 1.8.14
|
||||
resolution: "@formatjs/icu-skeleton-parser@npm:1.8.14"
|
||||
"@formatjs/icu-skeleton-parser@npm:1.8.15":
|
||||
version: 1.8.15
|
||||
resolution: "@formatjs/icu-skeleton-parser@npm:1.8.15"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.4"
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.5"
|
||||
tslib: "npm:^2.8.0"
|
||||
checksum: 10/2fbe3155c310358820b118d8c9844f314eff3500a82f1c65402434a3095823e1afeaab8d1762b4a59cc5679d82dc4c8c134683565d7cdae4daace23251f46a47
|
||||
checksum: 10/19825abc1a5eef0288456c08420d06f3da8256fbe81db0b9ead48cacc94954d748c8068988e26d184d38fca2e50c191ecda5a10ff3935529c3134b8d80db0538
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-datetimeformat@npm:6.18.0":
|
||||
version: 6.18.0
|
||||
resolution: "@formatjs/intl-datetimeformat@npm:6.18.0"
|
||||
"@formatjs/intl-datetimeformat@npm:6.18.1":
|
||||
version: 6.18.1
|
||||
resolution: "@formatjs/intl-datetimeformat@npm:6.18.1"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.4"
|
||||
"@formatjs/intl-localematcher": "npm:0.6.1"
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.5"
|
||||
"@formatjs/intl-localematcher": "npm:0.6.2"
|
||||
decimal.js: "npm:^10.4.3"
|
||||
tslib: "npm:^2.8.0"
|
||||
checksum: 10/b70edaa4cfa150f0a6cbeeb1488e6acdea21349abdefc4e37b923de68592c6f330a966456bf6000f233d0f715cf3b8cfce23d5a4ed574fa8ea35ccb5bea80886
|
||||
checksum: 10/66938778ecf37472a7e2f1d9349b0ac249fcbd5d684ae5614dea07287876182429980ba2fe3671224f981065baf017ac955f4b3c1f3c924c89bf2ec82dd1acd8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-displaynames@npm:6.8.11":
|
||||
version: 6.8.11
|
||||
resolution: "@formatjs/intl-displaynames@npm:6.8.11"
|
||||
"@formatjs/intl-displaynames@npm:6.8.12":
|
||||
version: 6.8.12
|
||||
resolution: "@formatjs/intl-displaynames@npm:6.8.12"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.4"
|
||||
"@formatjs/intl-localematcher": "npm:0.6.1"
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.5"
|
||||
"@formatjs/intl-localematcher": "npm:0.6.2"
|
||||
tslib: "npm:^2.8.0"
|
||||
checksum: 10/05c785d9e767cc1e4d1bd40d6989c3318b6a98cb43dd6808f501f5e5538bb3a1fb8fa80f8d2282d598501d3d193a406f0127acce6b14cb7c595ab6d981437e6f
|
||||
checksum: 10/7de27ef7e8cde2febce84d5443f00b70062cbd0c3f1039ce8ed1caacb15c4c7a36da16295f26657d59aa4663141a04d7b1083bfd1eea6a4e8ad9dc6093a2c886
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-durationformat@npm:0.7.4":
|
||||
version: 0.7.4
|
||||
resolution: "@formatjs/intl-durationformat@npm:0.7.4"
|
||||
"@formatjs/intl-durationformat@npm:0.7.5":
|
||||
version: 0.7.5
|
||||
resolution: "@formatjs/intl-durationformat@npm:0.7.5"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.4"
|
||||
"@formatjs/intl-localematcher": "npm:0.6.1"
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.5"
|
||||
"@formatjs/intl-localematcher": "npm:0.6.2"
|
||||
tslib: "npm:^2.8.0"
|
||||
checksum: 10/d62273ecd635475ca91e9b501301f3f396403fa91b584c550734b19b2d194ba1316b27303fed985c1d42ae933d54eb220da6540edfdf376b0d9371ecfd0d4e15
|
||||
checksum: 10/4dc81b112fed25dc8da0a16ddeff033b7c763bf9a1cfd7b1b25c1216f7f147eb67a47059a3cf95b4d4ade150c54a813542b84e69298905a4bc22548d74bf8567
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-enumerator@npm:1.8.10":
|
||||
version: 1.8.10
|
||||
resolution: "@formatjs/intl-enumerator@npm:1.8.10"
|
||||
"@formatjs/intl-enumerator@npm:1.8.11":
|
||||
version: 1.8.11
|
||||
resolution: "@formatjs/intl-enumerator@npm:1.8.11"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.4"
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.5"
|
||||
tslib: "npm:^2.8.0"
|
||||
checksum: 10/9e0e762143248bf91e174d3abc15261b47ac7294632d26797cf5b001707aa68ca2deeb05c95f7308aa2cffa46d61b0fac46306dea722ab210dfa012990743798
|
||||
checksum: 10/8646a517cd4160c1ceff888ec8fdf652caa3d375fa41231e829c13bc7be0cd156c9642e339b75e9cfa8ef60ae8140c766f9055318c62f1c1d9345f25cdb7f426
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-getcanonicallocales@npm:2.5.5":
|
||||
version: 2.5.5
|
||||
resolution: "@formatjs/intl-getcanonicallocales@npm:2.5.5"
|
||||
"@formatjs/intl-getcanonicallocales@npm:2.5.6":
|
||||
version: 2.5.6
|
||||
resolution: "@formatjs/intl-getcanonicallocales@npm:2.5.6"
|
||||
dependencies:
|
||||
tslib: "npm:^2.8.0"
|
||||
checksum: 10/2a32202765c9a4f16fc36f4e4afca7fd5f4f35885ad2ca671352a7bba1a19d5ec81933d52ab1855c8570e73247213739d9d2d95d2438bd9f02a1f0db7cb9b8a9
|
||||
checksum: 10/1d3d13fa1758a9bb7854f3afd844ecb70a4333a7cfbb6822b99e3b8ab6269e525a0ca23a8a47c3944e5376bc19e9e423b5cc3043db1c6de64909986c5cec6fc0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-listformat@npm:7.7.11":
|
||||
version: 7.7.11
|
||||
resolution: "@formatjs/intl-listformat@npm:7.7.11"
|
||||
"@formatjs/intl-listformat@npm:7.7.12":
|
||||
version: 7.7.12
|
||||
resolution: "@formatjs/intl-listformat@npm:7.7.12"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.4"
|
||||
"@formatjs/intl-localematcher": "npm:0.6.1"
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.5"
|
||||
"@formatjs/intl-localematcher": "npm:0.6.2"
|
||||
tslib: "npm:^2.8.0"
|
||||
checksum: 10/e7de54dcbcfdd8718870501623fb1be55dbac11e2582b7961d4668fb5e1f0d1f6da0388ed49084a4527e500dbea548670659efccb690f3b4398f0f8bcd5221dd
|
||||
checksum: 10/eee910e83ad28b3b3c24ab6e155720187ae5b5ac936ffa2c8ec6cc8c392c194fd5c79a166290da1c6de8dc1857e3d9d11241029832ec88f7a85cce1821b7f067
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-locale@npm:4.2.11":
|
||||
version: 4.2.11
|
||||
resolution: "@formatjs/intl-locale@npm:4.2.11"
|
||||
"@formatjs/intl-locale@npm:4.2.12":
|
||||
version: 4.2.12
|
||||
resolution: "@formatjs/intl-locale@npm:4.2.12"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.4"
|
||||
"@formatjs/intl-enumerator": "npm:1.8.10"
|
||||
"@formatjs/intl-getcanonicallocales": "npm:2.5.5"
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.5"
|
||||
"@formatjs/intl-enumerator": "npm:1.8.11"
|
||||
"@formatjs/intl-getcanonicallocales": "npm:2.5.6"
|
||||
tslib: "npm:^2.8.0"
|
||||
checksum: 10/8746af66ebd5284f189c83e0d59a4d781490ce3eadaab284bf96c4240eaf8b9422130a94a842a1ab12fa14bb2cdf02e9f78ac3b9cf955156fafeffab9d73d7a2
|
||||
checksum: 10/42111a3002a5a2076b3eb012073230f69c62355dc03647bc17f4d0805f39c7e720e2281b359277d020fef623944a5bcc1ddc3dae9a3af74886d876147680147d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-localematcher@npm:0.6.1":
|
||||
version: 0.6.1
|
||||
resolution: "@formatjs/intl-localematcher@npm:0.6.1"
|
||||
"@formatjs/intl-localematcher@npm:0.6.2":
|
||||
version: 0.6.2
|
||||
resolution: "@formatjs/intl-localematcher@npm:0.6.2"
|
||||
dependencies:
|
||||
tslib: "npm:^2.8.0"
|
||||
checksum: 10/c7b3bc8395d18670677f207b2fd107561fff5d6394a9b4273c29e0bea920300ec3a2eefead600ebb7761c04a770cada28f78ac059f84d00520bfb57a9db36998
|
||||
checksum: 10/eb12a7f5367bbecdfafc20d7f005559ce840f420e970f425c5213d35e94e86dfe75bde03464971a26494bf8427d4961269db22ecad2834f2a19d888b5d9cc064
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-numberformat@npm:8.15.4":
|
||||
version: 8.15.4
|
||||
resolution: "@formatjs/intl-numberformat@npm:8.15.4"
|
||||
"@formatjs/intl-numberformat@npm:8.15.5":
|
||||
version: 8.15.5
|
||||
resolution: "@formatjs/intl-numberformat@npm:8.15.5"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.4"
|
||||
"@formatjs/intl-localematcher": "npm:0.6.1"
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.5"
|
||||
"@formatjs/intl-localematcher": "npm:0.6.2"
|
||||
decimal.js: "npm:^10.4.3"
|
||||
tslib: "npm:^2.8.0"
|
||||
checksum: 10/232740eb4992f1bf4f829f05a755f427089a70b56a8a715fa9ac8604f701691701e989247ef1537a1d7c90e315b4153b82cf2e67e7f9d5b78d471c1cf59abace
|
||||
checksum: 10/3440371a43c54cdd2aa3714cb518ad22e491dd19fbc0c046e712dde078d3f6ed709474376863d64d2bddb506957d1cf265d440f6723b88211044a7b56186e550
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-pluralrules@npm:5.4.4":
|
||||
version: 5.4.4
|
||||
resolution: "@formatjs/intl-pluralrules@npm:5.4.4"
|
||||
"@formatjs/intl-pluralrules@npm:5.4.5":
|
||||
version: 5.4.5
|
||||
resolution: "@formatjs/intl-pluralrules@npm:5.4.5"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.4"
|
||||
"@formatjs/intl-localematcher": "npm:0.6.1"
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.5"
|
||||
"@formatjs/intl-localematcher": "npm:0.6.2"
|
||||
decimal.js: "npm:^10.4.3"
|
||||
tslib: "npm:^2.8.0"
|
||||
checksum: 10/919f80e144283b5849014bc245626916224adc0d693e8be5531168f1c7af54bb4c8cbd77a12ceba1d13ad49171680d346d9176464fae5013e13f79d9c7baa02a
|
||||
checksum: 10/00f650891893b743d126dd2bf0d17c1b16a8c9e0e0dd94cd0895e66cb556246116263e9603204e1991924814d0ed3a3503765914aff08181d5e4435dfc5e547c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-relativetimeformat@npm:11.4.11":
|
||||
version: 11.4.11
|
||||
resolution: "@formatjs/intl-relativetimeformat@npm:11.4.11"
|
||||
"@formatjs/intl-relativetimeformat@npm:11.4.12":
|
||||
version: 11.4.12
|
||||
resolution: "@formatjs/intl-relativetimeformat@npm:11.4.12"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.4"
|
||||
"@formatjs/intl-localematcher": "npm:0.6.1"
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.5"
|
||||
"@formatjs/intl-localematcher": "npm:0.6.2"
|
||||
tslib: "npm:^2.8.0"
|
||||
checksum: 10/fda4da27c0245869316c9199ed4e0521988be8b41b3e685f4abcb486f01d5b4c72f2ecf1b19b07091c15360c7691a4dd87199f81943d1ad6bda084c746fc8ec3
|
||||
checksum: 10/f6adca59738cb7f58d2ea985558d8fc45e567406de6fb6e67894afe790e2a9fa1a19d34853afc36805fa4a3d638e29c62d6c6ba3ec2a85628c240081dcdfebc1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1940,9 +1942,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@home-assistant/webawesome@npm:3.0.0-beta.6.ha.1":
|
||||
version: 3.0.0-beta.6.ha.1
|
||||
resolution: "@home-assistant/webawesome@npm:3.0.0-beta.6.ha.1"
|
||||
"@home-assistant/webawesome@npm:3.0.0-beta.6.ha.4":
|
||||
version: 3.0.0-beta.6.ha.4
|
||||
resolution: "@home-assistant/webawesome@npm:3.0.0-beta.6.ha.4"
|
||||
dependencies:
|
||||
"@ctrl/tinycolor": "npm:4.1.0"
|
||||
"@floating-ui/dom": "npm:^1.6.13"
|
||||
@@ -1953,7 +1955,7 @@ __metadata:
|
||||
lit: "npm:^3.2.1"
|
||||
nanoid: "npm:^5.1.5"
|
||||
qr-creator: "npm:^1.0.0"
|
||||
checksum: 10/c9510e0c65b682c3868b5cbbf046f62aea30e3c5d969128d9032e0d89a8943faa4c9d78c3500446ec04cffeb0ab1939b870b60d454db657faed2aa0ac6026a3e
|
||||
checksum: 10/d9072b321126ef458468ed2cf040e0b04cb2aff73336c6e742c0cfb25d9fb674b7672e7c9abcf5bcb0aa0b2fe953c20186f0910f485024c827bfe4cf399f10a4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -4943,106 +4945,106 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/eslint-plugin@npm:8.45.0":
|
||||
version: 8.45.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.45.0"
|
||||
"@typescript-eslint/eslint-plugin@npm:8.46.0":
|
||||
version: 8.46.0
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.46.0"
|
||||
dependencies:
|
||||
"@eslint-community/regexpp": "npm:^4.10.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.45.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.45.0"
|
||||
"@typescript-eslint/utils": "npm:8.45.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.45.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.46.0"
|
||||
"@typescript-eslint/type-utils": "npm:8.46.0"
|
||||
"@typescript-eslint/utils": "npm:8.46.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.46.0"
|
||||
graphemer: "npm:^1.4.0"
|
||||
ignore: "npm:^7.0.0"
|
||||
natural-compare: "npm:^1.4.0"
|
||||
ts-api-utils: "npm:^2.1.0"
|
||||
peerDependencies:
|
||||
"@typescript-eslint/parser": ^8.45.0
|
||||
"@typescript-eslint/parser": ^8.46.0
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10/6d31dbd3354028b4a010af0ea2614a171b11616e6f20d36d74529b8888681ae8d15e1269122b8a8d5fae117bdd66dac4a38cfc99dc2a0ee33bd22c10075f63e4
|
||||
checksum: 10/415afd894a5fec9cfe2c327c8b26377045979cc6bdf720aeecb32af335b9e6865c70fa6a355dd16f52a36dc38f50755df3eb1466d5822c53c80465ff824c9881
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/parser@npm:8.45.0":
|
||||
version: 8.45.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.45.0"
|
||||
"@typescript-eslint/parser@npm:8.46.0":
|
||||
version: 8.46.0
|
||||
resolution: "@typescript-eslint/parser@npm:8.46.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager": "npm:8.45.0"
|
||||
"@typescript-eslint/types": "npm:8.45.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.45.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.45.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.46.0"
|
||||
"@typescript-eslint/types": "npm:8.46.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.46.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.46.0"
|
||||
debug: "npm:^4.3.4"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10/4f8b7c73ae3b53c2adc4e981ac2ca90839a118947635481b45d29423d39b7b73cde2b185ad1084c9e19c3239444bf1be81f40b861176eec4540cb46848731991
|
||||
checksum: 10/6838fde776fd2b2932b259a20cc89b517e0c94a2cfa363a5e8531095c23fb35d8f803196f6594026d0510bf2a8ec003c67181bb2c407904685a64c97602da65f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/project-service@npm:8.45.0":
|
||||
version: 8.45.0
|
||||
resolution: "@typescript-eslint/project-service@npm:8.45.0"
|
||||
"@typescript-eslint/project-service@npm:8.46.0":
|
||||
version: 8.46.0
|
||||
resolution: "@typescript-eslint/project-service@npm:8.46.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.45.0"
|
||||
"@typescript-eslint/types": "npm:^8.45.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:^8.46.0"
|
||||
"@typescript-eslint/types": "npm:^8.46.0"
|
||||
debug: "npm:^4.3.4"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10/919c8260dae79eaec79de84a5ae66fbb09c2ab7aca8c3b7785cb011582a2864c8091e64c84013b05bce812e522fbc4a5ae1c68f86404e078fc84da0fe80247ce
|
||||
checksum: 10/de11af23ae6b82769b667e8d6e81d47ce039c7817465b99c1e29c8fbcac58af898bebe70368a274cd7b3c7232354134d53ceba0415b8d7e18317037bc4a4a2f7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/scope-manager@npm:8.45.0":
|
||||
version: 8.45.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.45.0"
|
||||
"@typescript-eslint/scope-manager@npm:8.46.0":
|
||||
version: 8.46.0
|
||||
resolution: "@typescript-eslint/scope-manager@npm:8.46.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.45.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.45.0"
|
||||
checksum: 10/e45d63a0109eca00f6b431d87e73eacfa03b1795905f123e9144bcacb5abb83888167d1849317c6f90ba1f3553196b2eab13e5e7cdd1050d7a84eaadb65ba801
|
||||
"@typescript-eslint/types": "npm:8.46.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.46.0"
|
||||
checksum: 10/ed85abd08c0edf088b1b11757c658acf593cf84051bddde651304a609d3a6cd9e331149e88653676606a565c3f92c191d4af049f540f6e3bb692a4f38305fd71
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/tsconfig-utils@npm:8.45.0, @typescript-eslint/tsconfig-utils@npm:^8.45.0":
|
||||
version: 8.45.0
|
||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.45.0"
|
||||
"@typescript-eslint/tsconfig-utils@npm:8.46.0, @typescript-eslint/tsconfig-utils@npm:^8.46.0":
|
||||
version: 8.46.0
|
||||
resolution: "@typescript-eslint/tsconfig-utils@npm:8.46.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10/91696bbc34758749d3647236986bf418bacdc0de0e27c2d39cd7c2408c404c35ed18c47c2a55aea0bb9525cc7eb656586359c4e651144603f3438ce93fe80081
|
||||
checksum: 10/e78a66a854322423aca835070c5ee9489975c4d80d2f8ffe9cf4d6e3f67a1646ddc05b086f7156599c90ad521670ca572a4315f2b49a5922c33d6e49723558e4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/type-utils@npm:8.45.0":
|
||||
version: 8.45.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.45.0"
|
||||
"@typescript-eslint/type-utils@npm:8.46.0":
|
||||
version: 8.46.0
|
||||
resolution: "@typescript-eslint/type-utils@npm:8.46.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.45.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.45.0"
|
||||
"@typescript-eslint/utils": "npm:8.45.0"
|
||||
"@typescript-eslint/types": "npm:8.46.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.46.0"
|
||||
"@typescript-eslint/utils": "npm:8.46.0"
|
||||
debug: "npm:^4.3.4"
|
||||
ts-api-utils: "npm:^2.1.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10/81017b3f4780a65a4e4268ab208f1cb8891c1ced9ade23d8eb4575b18aeb99fe59a0d0ddbb4eea9c086567a1b4515d3466e850d4c81ec0d2d88658c43877a6cf
|
||||
checksum: 10/5405b71b91d02ed4eac1028fc156c053953403b9f48393d92340b15a8b05bee5bf1281324c6283ac31a0e03cc1a19baf94768cb3fd70b4621f8c07a4243837db
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/types@npm:8.45.0, @typescript-eslint/types@npm:^8.45.0":
|
||||
version: 8.45.0
|
||||
resolution: "@typescript-eslint/types@npm:8.45.0"
|
||||
checksum: 10/889ded2b9bf376c876611b2a37f89051fdc8ec501314a4b97832caefa4305bffc4b752548941ce2e7f9659a81336d096d439d4c2ed236c99fefdf60b715593dd
|
||||
"@typescript-eslint/types@npm:8.46.0, @typescript-eslint/types@npm:^8.46.0":
|
||||
version: 8.46.0
|
||||
resolution: "@typescript-eslint/types@npm:8.46.0"
|
||||
checksum: 10/0118b0dd592bf4beaf41e8c6be812980dd0adea44d48c90d8b0272777b58d4cfd6326b8bc363efa3c640be476a6bf3632aee2d97052d5e34071e6576b9c28264
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/typescript-estree@npm:8.45.0":
|
||||
version: 8.45.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.45.0"
|
||||
"@typescript-eslint/typescript-estree@npm:8.46.0":
|
||||
version: 8.46.0
|
||||
resolution: "@typescript-eslint/typescript-estree@npm:8.46.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/project-service": "npm:8.45.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.45.0"
|
||||
"@typescript-eslint/types": "npm:8.45.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.45.0"
|
||||
"@typescript-eslint/project-service": "npm:8.46.0"
|
||||
"@typescript-eslint/tsconfig-utils": "npm:8.46.0"
|
||||
"@typescript-eslint/types": "npm:8.46.0"
|
||||
"@typescript-eslint/visitor-keys": "npm:8.46.0"
|
||||
debug: "npm:^4.3.4"
|
||||
fast-glob: "npm:^3.3.2"
|
||||
is-glob: "npm:^4.0.3"
|
||||
@@ -5051,32 +5053,32 @@ __metadata:
|
||||
ts-api-utils: "npm:^2.1.0"
|
||||
peerDependencies:
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10/2fb4e63ad6128afbada8eabaabfe7d5a8f1a1f387bb13d7d3209103493ba974b518bf47b17e9a853beba10ec81efd5582ebf628c2eb77a924cf67d4d85466e5e
|
||||
checksum: 10/61053bd0c35a1fe5c82aef00cb70dbe0878ab28e55550cc1e2d6e7d4a0520c81947eb7505227c85a742a93db905d7e7376aed7d958dc257507b9bdda1daf0b00
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/utils@npm:8.45.0":
|
||||
version: 8.45.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.45.0"
|
||||
"@typescript-eslint/utils@npm:8.46.0":
|
||||
version: 8.46.0
|
||||
resolution: "@typescript-eslint/utils@npm:8.46.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.7.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.45.0"
|
||||
"@typescript-eslint/types": "npm:8.45.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.45.0"
|
||||
"@typescript-eslint/scope-manager": "npm:8.46.0"
|
||||
"@typescript-eslint/types": "npm:8.46.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.46.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10/9e675a0da4434bd434901f9ba3e1e91d4d7ad542d7fcf8c23534a67f2f9039a569da20929e67a6562e3a263be226ad424cd0c1ac80f7828f4285f7f34e361926
|
||||
checksum: 10/4e0da60de389799afdd36249fd4bcf9e085a4d6f119e241e436a701b45cdf10becc3f1e3cdef29ebbf147a81f40d9a4800d428cb4a66799d3e4aa80b879c9ee2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/visitor-keys@npm:8.45.0":
|
||||
version: 8.45.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.45.0"
|
||||
"@typescript-eslint/visitor-keys@npm:8.46.0":
|
||||
version: 8.46.0
|
||||
resolution: "@typescript-eslint/visitor-keys@npm:8.46.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/types": "npm:8.45.0"
|
||||
"@typescript-eslint/types": "npm:8.46.0"
|
||||
eslint-visitor-keys: "npm:^4.2.1"
|
||||
checksum: 10/8ae7e19c69c1f67fa8f952c18a09ad42a8cba492545d6e1dca6750e760893773f69ec6b1a96d0997e833c82aecc5ff7fb9546c5abd6c4427d91206670cf8ff37
|
||||
checksum: 10/37e6145b6a5e960c59777d7fc86f722ff696e76c627106ac4577b945ca35744a5f96525d77bde50fe8c328503e9392e21e3adb7cf9899ae0efc054d63f4c3916
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8030,18 +8032,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"eslint@npm:9.36.0":
|
||||
version: 9.36.0
|
||||
resolution: "eslint@npm:9.36.0"
|
||||
"eslint@npm:9.37.0":
|
||||
version: 9.37.0
|
||||
resolution: "eslint@npm:9.37.0"
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils": "npm:^4.8.0"
|
||||
"@eslint-community/regexpp": "npm:^4.12.1"
|
||||
"@eslint/config-array": "npm:^0.21.0"
|
||||
"@eslint/config-helpers": "npm:^0.3.1"
|
||||
"@eslint/core": "npm:^0.15.2"
|
||||
"@eslint/config-helpers": "npm:^0.4.0"
|
||||
"@eslint/core": "npm:^0.16.0"
|
||||
"@eslint/eslintrc": "npm:^3.3.1"
|
||||
"@eslint/js": "npm:9.36.0"
|
||||
"@eslint/plugin-kit": "npm:^0.3.5"
|
||||
"@eslint/js": "npm:9.37.0"
|
||||
"@eslint/plugin-kit": "npm:^0.4.0"
|
||||
"@humanfs/node": "npm:^0.16.6"
|
||||
"@humanwhocodes/module-importer": "npm:^1.0.1"
|
||||
"@humanwhocodes/retry": "npm:^0.4.2"
|
||||
@@ -8076,7 +8078,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
eslint: bin/eslint.js
|
||||
checksum: 10/6e512a82e680e6cdc554e97c7e232b83171f24a52fb46f89c2df74bcb80fe59b6e0a989485c9fe7e9ca81556b1953dd8604ace4ed37f651eded9a37816c06b7c
|
||||
checksum: 10/c7530470c9cafe9a7f768477f7894d9b9d28e92995186223e99fbd9edeb391119e2a70678a2e98e213ae37cbb41de89403b510f5f33df2340aa65dd6f2a3c0bb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9190,22 +9192,22 @@ __metadata:
|
||||
"@codemirror/view": "npm:6.38.4"
|
||||
"@date-fns/tz": "npm:1.4.1"
|
||||
"@egjs/hammerjs": "npm:2.0.17"
|
||||
"@formatjs/intl-datetimeformat": "npm:6.18.0"
|
||||
"@formatjs/intl-displaynames": "npm:6.8.11"
|
||||
"@formatjs/intl-durationformat": "npm:0.7.4"
|
||||
"@formatjs/intl-getcanonicallocales": "npm:2.5.5"
|
||||
"@formatjs/intl-listformat": "npm:7.7.11"
|
||||
"@formatjs/intl-locale": "npm:4.2.11"
|
||||
"@formatjs/intl-numberformat": "npm:8.15.4"
|
||||
"@formatjs/intl-pluralrules": "npm:5.4.4"
|
||||
"@formatjs/intl-relativetimeformat": "npm:11.4.11"
|
||||
"@formatjs/intl-datetimeformat": "npm:6.18.1"
|
||||
"@formatjs/intl-displaynames": "npm:6.8.12"
|
||||
"@formatjs/intl-durationformat": "npm:0.7.5"
|
||||
"@formatjs/intl-getcanonicallocales": "npm:2.5.6"
|
||||
"@formatjs/intl-listformat": "npm:7.7.12"
|
||||
"@formatjs/intl-locale": "npm:4.2.12"
|
||||
"@formatjs/intl-numberformat": "npm:8.15.5"
|
||||
"@formatjs/intl-pluralrules": "npm:5.4.5"
|
||||
"@formatjs/intl-relativetimeformat": "npm:11.4.12"
|
||||
"@fullcalendar/core": "npm:6.1.19"
|
||||
"@fullcalendar/daygrid": "npm:6.1.19"
|
||||
"@fullcalendar/interaction": "npm:6.1.19"
|
||||
"@fullcalendar/list": "npm:6.1.19"
|
||||
"@fullcalendar/luxon3": "npm:6.1.19"
|
||||
"@fullcalendar/timegrid": "npm:6.1.19"
|
||||
"@home-assistant/webawesome": "npm:3.0.0-beta.6.ha.1"
|
||||
"@home-assistant/webawesome": "npm:3.0.0-beta.6.ha.4"
|
||||
"@lezer/highlight": "npm:1.2.1"
|
||||
"@lit-labs/motion": "npm:1.0.9"
|
||||
"@lit-labs/observers": "npm:2.0.6"
|
||||
@@ -9291,7 +9293,7 @@ __metadata:
|
||||
dialog-polyfill: "npm:0.5.6"
|
||||
echarts: "npm:6.0.0"
|
||||
element-internals-polyfill: "npm:3.0.2"
|
||||
eslint: "npm:9.36.0"
|
||||
eslint: "npm:9.37.0"
|
||||
eslint-config-airbnb-base: "npm:15.0.0"
|
||||
eslint-config-prettier: "npm:10.1.8"
|
||||
eslint-import-resolver-webpack: "npm:0.13.10"
|
||||
@@ -9315,7 +9317,7 @@ __metadata:
|
||||
html-minifier-terser: "npm:7.2.0"
|
||||
husky: "npm:9.1.7"
|
||||
idb-keyval: "npm:6.2.2"
|
||||
intl-messageformat: "npm:10.7.16"
|
||||
intl-messageformat: "npm:10.7.17"
|
||||
js-yaml: "npm:4.1.0"
|
||||
jsdom: "npm:27.0.0"
|
||||
jszip: "npm:3.10.1"
|
||||
@@ -9352,7 +9354,7 @@ __metadata:
|
||||
tinykeys: "npm:3.0.0"
|
||||
ts-lit-plugin: "npm:2.0.2"
|
||||
typescript: "npm:5.9.3"
|
||||
typescript-eslint: "npm:8.45.0"
|
||||
typescript-eslint: "npm:8.46.0"
|
||||
ua-parser-js: "npm:2.0.5"
|
||||
vite-tsconfig-paths: "npm:5.1.4"
|
||||
vitest: "npm:3.2.4"
|
||||
@@ -9710,15 +9712,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"intl-messageformat@npm:10.7.16":
|
||||
version: 10.7.16
|
||||
resolution: "intl-messageformat@npm:10.7.16"
|
||||
"intl-messageformat@npm:10.7.17":
|
||||
version: 10.7.17
|
||||
resolution: "intl-messageformat@npm:10.7.17"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.4"
|
||||
"@formatjs/ecma402-abstract": "npm:2.3.5"
|
||||
"@formatjs/fast-memoize": "npm:2.2.7"
|
||||
"@formatjs/icu-messageformat-parser": "npm:2.11.2"
|
||||
"@formatjs/icu-messageformat-parser": "npm:2.11.3"
|
||||
tslib: "npm:^2.8.0"
|
||||
checksum: 10/c19b77c5e495ce8b0d1aa0d95444bf3a4f73886805f1e08d7159b364abcf2f63686b2ccf202eaafb0e39a0e9fde61848b8dd2db1679efd4f6ec8f6a3d0e77928
|
||||
checksum: 10/4f8c30c998bfc14eb64894414b94a8923045ab31d7bbf0978dab6621c644d451ff5c533c04ce8128163b74dd6d59061ec1ef3acb1cbab3302d31cbdb21947620
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -14315,18 +14317,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typescript-eslint@npm:8.45.0":
|
||||
version: 8.45.0
|
||||
resolution: "typescript-eslint@npm:8.45.0"
|
||||
"typescript-eslint@npm:8.46.0":
|
||||
version: 8.46.0
|
||||
resolution: "typescript-eslint@npm:8.46.0"
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin": "npm:8.45.0"
|
||||
"@typescript-eslint/parser": "npm:8.45.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.45.0"
|
||||
"@typescript-eslint/utils": "npm:8.45.0"
|
||||
"@typescript-eslint/eslint-plugin": "npm:8.46.0"
|
||||
"@typescript-eslint/parser": "npm:8.46.0"
|
||||
"@typescript-eslint/typescript-estree": "npm:8.46.0"
|
||||
"@typescript-eslint/utils": "npm:8.46.0"
|
||||
peerDependencies:
|
||||
eslint: ^8.57.0 || ^9.0.0
|
||||
typescript: ">=4.8.4 <6.0.0"
|
||||
checksum: 10/1c17ebb5bcbea418c8f372d71b5c2df8c9b8c6897d1bda8196ea17bac8fabeffe1814bc4f7a28d40f404fb811c97fcda0d69c4375b4f010d9bf44d19d8401706
|
||||
checksum: 10/fd74aab1d21d661299a64107236b5c3515d6d955eb1764b56c5c9505b8cef5f2600e8290d251f1379138333573df94a1fe1fd7fef23952b5ab9f12ff2b774f92
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
Reference in New Issue
Block a user