Compare commits

..

1 Commits

Author SHA1 Message Date
Cursor Agent
605a77fa2c feat: Add support for multiple media selections
This change allows the media selector to handle multiple media selections. It also includes improvements to thumbnail handling and UI updates for multiple selections.

Co-authored-by: paulus.schoutsen <paulus.schoutsen@nabucasa.com>
2025-10-13 01:29:03 +00:00
26 changed files with 621 additions and 1141 deletions

View File

@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8 uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8 uses: github/codeql-action/autobuild@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8 uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6

View File

@@ -55,7 +55,7 @@ jobs:
script/release script/release
- name: Upload release assets - name: Upload release assets
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
with: with:
files: | files: |
dist/*.whl dist/*.whl
@@ -108,7 +108,7 @@ jobs:
- name: Tar folder - name: Tar folder
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist . run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
- name: Upload release asset - name: Upload release asset
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
with: with:
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
@@ -137,6 +137,6 @@ jobs:
- name: Tar folder - name: Tar folder
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build . run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
- name: Upload release asset - name: Upload release asset
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1 uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
with: with:
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz

View File

@@ -37,15 +37,15 @@
"@codemirror/view": "6.38.5", "@codemirror/view": "6.38.5",
"@date-fns/tz": "1.4.1", "@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17", "@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.18.2", "@formatjs/intl-datetimeformat": "6.18.1",
"@formatjs/intl-displaynames": "6.8.13", "@formatjs/intl-displaynames": "6.8.12",
"@formatjs/intl-durationformat": "0.7.6", "@formatjs/intl-durationformat": "0.7.5",
"@formatjs/intl-getcanonicallocales": "2.5.6", "@formatjs/intl-getcanonicallocales": "2.5.6",
"@formatjs/intl-listformat": "7.7.13", "@formatjs/intl-listformat": "7.7.12",
"@formatjs/intl-locale": "4.2.13", "@formatjs/intl-locale": "4.2.12",
"@formatjs/intl-numberformat": "8.15.6", "@formatjs/intl-numberformat": "8.15.5",
"@formatjs/intl-pluralrules": "5.4.6", "@formatjs/intl-pluralrules": "5.4.5",
"@formatjs/intl-relativetimeformat": "11.4.13", "@formatjs/intl-relativetimeformat": "11.4.12",
"@fullcalendar/core": "6.1.19", "@fullcalendar/core": "6.1.19",
"@fullcalendar/daygrid": "6.1.19", "@fullcalendar/daygrid": "6.1.19",
"@fullcalendar/interaction": "6.1.19", "@fullcalendar/interaction": "6.1.19",
@@ -99,7 +99,7 @@
"barcode-detector": "3.0.6", "barcode-detector": "3.0.6",
"color-name": "2.0.2", "color-name": "2.0.2",
"comlink": "4.4.2", "comlink": "4.4.2",
"core-js": "3.46.0", "core-js": "3.45.1",
"cropperjs": "1.6.2", "cropperjs": "1.6.2",
"culori": "4.0.2", "culori": "4.0.2",
"date-fns": "4.1.0", "date-fns": "4.1.0",
@@ -114,7 +114,7 @@
"hls.js": "1.6.13", "hls.js": "1.6.13",
"home-assistant-js-websocket": "9.5.0", "home-assistant-js-websocket": "9.5.0",
"idb-keyval": "6.2.2", "idb-keyval": "6.2.2",
"intl-messageformat": "10.7.18", "intl-messageformat": "10.7.17",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"leaflet": "1.9.4", "leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch", "leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@@ -135,7 +135,7 @@
"stacktrace-js": "2.0.2", "stacktrace-js": "2.0.2",
"superstruct": "2.0.2", "superstruct": "2.0.2",
"tinykeys": "3.0.0", "tinykeys": "3.0.0",
"ua-parser-js": "2.0.6", "ua-parser-js": "2.0.5",
"vue": "2.7.16", "vue": "2.7.16",
"vue2-daterange-picker": "0.6.8", "vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0", "weekstart": "2.0.0",

View File

@@ -1,4 +1,4 @@
import { mdiPlayBox, mdiPlus } from "@mdi/js"; import { mdiPlus } from "@mdi/js";
import type { PropertyValues } from "lit"; import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
@@ -7,10 +7,7 @@ import { fireEvent } from "../../common/dom/fire_event";
import { supportsFeature } from "../../common/entity/supports-feature"; import { supportsFeature } from "../../common/entity/supports-feature";
import { getSignedPath } from "../../data/auth"; import { getSignedPath } from "../../data/auth";
import type { MediaPickedEvent } from "../../data/media-player"; import type { MediaPickedEvent } from "../../data/media-player";
import { import { MediaPlayerEntityFeature } from "../../data/media-player";
MediaClassBrowserSettings,
MediaPlayerEntityFeature,
} from "../../data/media-player";
import type { MediaSelector, MediaSelectorValue } from "../../data/selector"; import type { MediaSelector, MediaSelectorValue } from "../../data/selector";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url"; import { brandsUrl, extractDomainFromBrandUrl } from "../../util/brands-url";
@@ -20,6 +17,8 @@ import type { SchemaUnion } from "../ha-form/types";
import { showMediaBrowserDialog } from "../media-player/show-media-browser-dialog"; import { showMediaBrowserDialog } from "../media-player/show-media-browser-dialog";
import { ensureArray } from "../../common/array/ensure-array"; import { ensureArray } from "../../common/array/ensure-array";
import "../ha-picture-upload"; import "../ha-picture-upload";
import "../chips/ha-chip-set";
import "../chips/ha-input-chip";
const MANUAL_SCHEMA = [ const MANUAL_SCHEMA = [
{ name: "media_content_id", required: false, selector: { text: {} } }, { name: "media_content_id", required: false, selector: { text: {} } },
@@ -36,7 +35,8 @@ export class HaMediaSelector extends LitElement {
@property({ attribute: false }) public selector!: MediaSelector; @property({ attribute: false }) public selector!: MediaSelector;
@property({ attribute: false }) public value?: MediaSelectorValue; @property({ attribute: false })
public value?: MediaSelectorValue | MediaSelectorValue[];
@property() public label?: string; @property() public label?: string;
@@ -52,6 +52,9 @@ export class HaMediaSelector extends LitElement {
@state() private _thumbnailUrl?: string | null; @state() private _thumbnailUrl?: string | null;
// For multiple selection mode, cache signed/rewritten URLs per thumbnail string
@state() private _thumbnailUrlMap: Record<string, string | null> = {};
private _contextEntities: string[] | undefined; private _contextEntities: string[] | undefined;
private get _hasAccept(): boolean { private get _hasAccept(): boolean {
@@ -59,6 +62,15 @@ export class HaMediaSelector extends LitElement {
} }
willUpdate(changedProps: PropertyValues<this>) { willUpdate(changedProps: PropertyValues<this>) {
if (changedProps.has("selector") && this.value !== undefined) {
if (this.selector.media?.multiple && !Array.isArray(this.value)) {
this.value = [this.value];
fireEvent(this, "value-changed", { value: this.value });
} else if (!this.selector.media?.multiple && Array.isArray(this.value)) {
this.value = this.value[0];
fireEvent(this, "value-changed", { value: this.value });
}
}
if (changedProps.has("context")) { if (changedProps.has("context")) {
if (!this._hasAccept) { if (!this._hasAccept) {
this._contextEntities = ensureArray(this.context?.filter_entity); this._contextEntities = ensureArray(this.context?.filter_entity);
@@ -66,32 +78,91 @@ export class HaMediaSelector extends LitElement {
} }
if (changedProps.has("value")) { if (changedProps.has("value")) {
const thumbnail = this.value?.metadata?.thumbnail; if (this.selector.media?.multiple) {
const oldThumbnail = (changedProps.get("value") as this["value"]) const values = Array.isArray(this.value)
?.metadata?.thumbnail; ? this.value
if (thumbnail === oldThumbnail) { : this.value
return; ? [this.value]
} : [];
if (thumbnail && thumbnail.startsWith("/")) { const seenThumbs = new Set<string>();
this._thumbnailUrl = undefined; values.forEach((val) => {
// Thumbnails served by local API require authentication const thumbnail = val.metadata?.thumbnail;
getSignedPath(this.hass, thumbnail).then((signedPath) => { if (!thumbnail) {
this._thumbnailUrl = signedPath.path; return;
}
seenThumbs.add(thumbnail);
// Only (re)compute if not cached yet
if (this._thumbnailUrlMap[thumbnail] !== undefined) {
return;
}
if (thumbnail.startsWith("/")) {
this._thumbnailUrlMap = {
...this._thumbnailUrlMap,
[thumbnail]: null,
};
getSignedPath(this.hass, thumbnail).then((signedPath) => {
// Avoid losing other keys
this._thumbnailUrlMap = {
...this._thumbnailUrlMap,
[thumbnail]: signedPath.path,
};
});
} else if (thumbnail.startsWith("https://brands.home-assistant.io")) {
this._thumbnailUrlMap = {
...this._thumbnailUrlMap,
[thumbnail]: brandsUrl({
domain: extractDomainFromBrandUrl(thumbnail),
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
}),
};
} else {
this._thumbnailUrlMap = {
...this._thumbnailUrlMap,
[thumbnail]: thumbnail,
};
}
}); });
} else if ( // Clean up thumbnails no longer present
thumbnail && const newMap: Record<string, string | null> = {};
thumbnail.startsWith("https://brands.home-assistant.io") Object.keys(this._thumbnailUrlMap).forEach((key) => {
) { if (seenThumbs.has(key)) {
// The backend is not aware of the theme used by the users, newMap[key] = this._thumbnailUrlMap[key];
// so we rewrite the URL to show a proper icon }
this._thumbnailUrl = brandsUrl({
domain: extractDomainFromBrandUrl(thumbnail),
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
}); });
this._thumbnailUrlMap = newMap;
} else { } else {
this._thumbnailUrl = thumbnail; const currVal = Array.isArray(this.value) ? this.value[0] : this.value;
const prevVal = Array.isArray(changedProps.get("value") as any)
? (changedProps.get("value") as MediaSelectorValue[])[0]
: (changedProps.get("value") as MediaSelectorValue);
const thumbnail = currVal?.metadata?.thumbnail;
const oldThumbnail = prevVal?.metadata?.thumbnail;
if (thumbnail === oldThumbnail) {
return;
}
if (thumbnail && thumbnail.startsWith("/")) {
this._thumbnailUrl = undefined;
// Thumbnails served by local API require authentication
getSignedPath(this.hass, thumbnail).then((signedPath) => {
this._thumbnailUrl = signedPath.path;
});
} else if (
thumbnail &&
thumbnail.startsWith("https://brands.home-assistant.io")
) {
// The backend is not aware of the theme used by the users,
// so we rewrite the URL to show a proper icon
this._thumbnailUrl = brandsUrl({
domain: extractDomainFromBrandUrl(thumbnail),
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
});
} else {
this._thumbnailUrl = thumbnail ?? undefined;
}
} }
} }
} }
@@ -106,7 +177,12 @@ export class HaMediaSelector extends LitElement {
(stateObj && (stateObj &&
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)); supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA));
if (this.selector.media?.image_upload && !this.value) { const isMultiple = this.selector.media?.multiple === true;
if (
this.selector.media?.image_upload &&
(!this.value || (Array.isArray(this.value) && this.value.length === 0))
) {
return html`<ha-picture-upload return html`<ha-picture-upload
.hass=${this.hass} .hass=${this.hass}
.value=${null} .value=${null}
@@ -148,19 +224,47 @@ export class HaMediaSelector extends LitElement {
</ha-alert> </ha-alert>
<ha-form <ha-form
.hass=${this.hass} .hass=${this.hass}
.data=${this.value || EMPTY_FORM} .data=${Array.isArray(this.value)
? this.value[0]
: this.value || EMPTY_FORM}
.schema=${MANUAL_SCHEMA} .schema=${MANUAL_SCHEMA}
.computeLabel=${this._computeLabelCallback} .computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback} .computeHelper=${this._computeHelperCallback}
></ha-form> ></ha-form>
` `
: html`<ha-card : html`
${isMultiple && Array.isArray(this.value) && this.value.length
? html`
<ha-chip-set>
${this.value.map(
(item, idx) => html`
<ha-input-chip
selected
.idx=${idx}
@remove=${this._removeItem}
>${item.metadata?.title ||
item.media_content_id}</ha-input-chip
>
`
)}
</ha-chip-set>
`
: nothing}
<ha-card
outlined outlined
tabindex="0" tabindex="0"
role="button" role="button"
aria-label=${!this.value?.media_content_id aria-label=${(() => {
? this.hass.localize("ui.components.selectors.media.pick_media") const currVal = Array.isArray(this.value)
: this.value.metadata?.title || this.value.media_content_id} ? this.value[this.value.length - 1]
: this.value;
return !currVal?.media_content_id
? this.hass.localize(
"ui.components.selectors.media.pick_media"
)
: currVal.metadata?.title || currVal.media_content_id;
})()}
@click=${this._pickMedia} @click=${this._pickMedia}
@keydown=${this._handleKeyDown} @keydown=${this._handleKeyDown}
class=${this.disabled || (!entityId && !this._hasAccept) class=${this.disabled || (!entityId && !this._hasAccept)
@@ -169,14 +273,22 @@ export class HaMediaSelector extends LitElement {
> >
<div class="content-container"> <div class="content-container">
<div class="thumbnail"> <div class="thumbnail">
${this.value?.metadata?.thumbnail ${!isMultiple &&
(Array.isArray(this.value) ? this.value[0] : this.value)
?.metadata?.thumbnail
? html` ? html`
<div <div
class="${classMap({ class="${classMap({
"centered-image": "centered-image":
!!this.value.metadata.media_class && !!(
Array.isArray(this.value)
? this.value[0]
: this.value
)!.metadata!.media_class &&
["app", "directory"].includes( ["app", "directory"].includes(
this.value.metadata.media_class (Array.isArray(this.value)
? this.value[0]
: this.value)!.metadata!.media_class!
), ),
})} })}
image" image"
@@ -189,32 +301,27 @@ export class HaMediaSelector extends LitElement {
<div class="icon-holder image"> <div class="icon-holder image">
<ha-svg-icon <ha-svg-icon
class="folder" class="folder"
.path=${!this.value?.media_content_id .path=${mdiPlus}
? mdiPlus
: this.value?.metadata?.media_class
? MediaClassBrowserSettings[
this.value.metadata.media_class ===
"directory"
? this.value.metadata
.children_media_class ||
this.value.metadata.media_class
: this.value.metadata.media_class
].icon
: mdiPlayBox}
></ha-svg-icon> ></ha-svg-icon>
</div> </div>
`} `}
</div> </div>
<div class="title"> <div class="title">
${!this.value?.media_content_id ${(() => {
? this.hass.localize( const currVal = Array.isArray(this.value)
"ui.components.selectors.media.pick_media" ? this.value[this.value.length - 1]
) : this.value;
: this.value.metadata?.title || this.value.media_content_id} return !currVal?.media_content_id
? this.hass.localize(
"ui.components.selectors.media.pick_media"
)
: currVal.metadata?.title || currVal.media_content_id;
})()}
</div> </div>
</div> </div>
</ha-card> </ha-card>
${this.selector.media?.clearable ${this.selector.media?.clearable &&
(Array.isArray(this.value) ? this.value.length : this.value)
? html`<div> ? html`<div>
<ha-button <ha-button
appearance="plain" appearance="plain"
@@ -227,7 +334,8 @@ export class HaMediaSelector extends LitElement {
)} )}
</ha-button> </ha-button>
</div>` </div>`
: nothing}`} : nothing}
`}
`; `;
} }
@@ -268,41 +376,61 @@ export class HaMediaSelector extends LitElement {
showMediaBrowserDialog(this, { showMediaBrowserDialog(this, {
action: "pick", action: "pick",
entityId: this._getActiveEntityId(), entityId: this._getActiveEntityId(),
navigateIds: this.value?.metadata?.navigateIds, navigateIds: (Array.isArray(this.value)
? this.value[this.value.length - 1]
: this.value
)?.metadata?.navigateIds,
accept: this.selector.media?.accept, accept: this.selector.media?.accept,
defaultId: this.value?.media_content_id, defaultId: Array.isArray(this.value)
defaultType: this.value?.media_content_type, ? this.value[this.value.length - 1]?.media_content_id
: this.value?.media_content_id,
defaultType: Array.isArray(this.value)
? this.value[this.value.length - 1]?.media_content_type
: this.value?.media_content_type,
hideContentType: this.selector.media?.hide_content_type, hideContentType: this.selector.media?.hide_content_type,
contentIdHelper: this.selector.media?.content_id_helper, contentIdHelper: this.selector.media?.content_id_helper,
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => { mediaPickedCallback: (pickedMedia: MediaPickedEvent) => {
fireEvent(this, "value-changed", { const newItem: MediaSelectorValue = {
value: { ...(Array.isArray(this.value) ? {} : (this.value as any)),
...this.value, media_content_id: pickedMedia.item.media_content_id,
media_content_id: pickedMedia.item.media_content_id, media_content_type: pickedMedia.item.media_content_type,
media_content_type: pickedMedia.item.media_content_type, metadata: {
metadata: { title: pickedMedia.item.title,
title: pickedMedia.item.title, thumbnail: pickedMedia.item.thumbnail,
thumbnail: pickedMedia.item.thumbnail, media_class: pickedMedia.item.media_class,
media_class: pickedMedia.item.media_class, children_media_class: pickedMedia.item.children_media_class,
children_media_class: pickedMedia.item.children_media_class, navigateIds: pickedMedia.navigateIds?.map((id) => ({
navigateIds: pickedMedia.navigateIds?.map((id) => ({ media_content_type: id.media_content_type,
media_content_type: id.media_content_type, media_content_id: id.media_content_id,
media_content_id: id.media_content_id, })),
})), ...(!this._hasAccept && this.context?.filter_entity
...(!this._hasAccept && this.context?.filter_entity ? { browse_entity_id: this._getActiveEntityId() }
? { browse_entity_id: this._getActiveEntityId() } : {}),
: {}),
},
}, },
}); };
if (this.selector.media?.multiple) {
const current = Array.isArray(this.value)
? this.value
: this.value
? [this.value]
: [];
fireEvent(this, "value-changed", {
value: [...current, newItem],
});
return;
}
fireEvent(this, "value-changed", { value: newItem });
}, },
}); });
} }
private _getActiveEntityId(): string | undefined { private _getActiveEntityId(): string | undefined {
const metaId = this.value?.metadata?.browse_entity_id; const val = Array.isArray(this.value)
? this.value[this.value.length - 1]
: this.value;
const metaId = val?.metadata?.browse_entity_id;
return ( return (
this.value?.entity_id || val?.entity_id ||
(metaId && this._contextEntities?.includes(metaId) && metaId) || (metaId && this._contextEntities?.includes(metaId) && metaId) ||
this._contextEntities?.[0] this._contextEntities?.[0]
); );
@@ -317,27 +445,47 @@ export class HaMediaSelector extends LitElement {
private _pictureUploadMediaPicked(ev) { private _pictureUploadMediaPicked(ev) {
const pickedMedia = ev.detail as MediaPickedEvent; const pickedMedia = ev.detail as MediaPickedEvent;
fireEvent(this, "value-changed", { const newItem: MediaSelectorValue = {
value: { ...(Array.isArray(this.value) ? {} : (this.value as any)),
...this.value, media_content_id: pickedMedia.item.media_content_id,
media_content_id: pickedMedia.item.media_content_id, media_content_type: pickedMedia.item.media_content_type,
media_content_type: pickedMedia.item.media_content_type, metadata: {
metadata: { title: pickedMedia.item.title,
title: pickedMedia.item.title, thumbnail: pickedMedia.item.thumbnail,
thumbnail: pickedMedia.item.thumbnail, media_class: pickedMedia.item.media_class,
media_class: pickedMedia.item.media_class, children_media_class: pickedMedia.item.children_media_class,
children_media_class: pickedMedia.item.children_media_class, navigateIds: pickedMedia.navigateIds?.map((id) => ({
navigateIds: pickedMedia.navigateIds?.map((id) => ({ media_content_type: id.media_content_type,
media_content_type: id.media_content_type, media_content_id: id.media_content_id,
media_content_id: id.media_content_id, })),
})),
},
}, },
}); };
if (this.selector.media?.multiple) {
const current = Array.isArray(this.value)
? this.value
: this.value
? [this.value]
: [];
fireEvent(this, "value-changed", { value: [...current, newItem] });
return;
}
fireEvent(this, "value-changed", { value: newItem });
} }
private _clearValue() { private _clearValue() {
fireEvent(this, "value-changed", { value: undefined }); fireEvent(this, "value-changed", {
value: this.selector.media?.multiple ? [] : undefined,
});
}
private _removeItem(ev: CustomEvent) {
ev.stopPropagation();
if (!Array.isArray(this.value)) return;
const idx = (ev.currentTarget as any).idx as number;
if (idx === undefined) return;
const newValue = this.value.slice();
newValue.splice(idx, 1);
fireEvent(this, "value-changed", { value: newValue });
} }
static styles = css` static styles = css`
@@ -349,6 +497,9 @@ export class HaMediaSelector extends LitElement {
display: block; display: block;
margin-bottom: 16px; margin-bottom: 16px;
} }
ha-chip-set {
padding-bottom: 8px;
}
ha-card { ha-card {
position: relative; position: relative;
width: 100%; width: 100%;

View File

@@ -94,6 +94,9 @@ export class HaServiceControl extends LitElement {
@property({ attribute: "hide-picker", type: Boolean, reflect: true }) @property({ attribute: "hide-picker", type: Boolean, reflect: true })
public hidePicker = false; public hidePicker = false;
@property({ attribute: "hide-description", type: Boolean })
public hideDescription = false;
@state() private _value!: this["value"]; @state() private _value!: this["value"];
@state() private _checkedKeys = new Set(); @state() private _checkedKeys = new Set();
@@ -469,135 +472,136 @@ export class HaServiceControl extends LitElement {
serviceData?.description; serviceData?.description;
return html`${this.hidePicker return html`${this.hidePicker
? nothing ? nothing
: html`<ha-service-picker : html`<ha-service-picker
.hass=${this.hass} .hass=${this.hass}
.value=${this._value?.action} .value=${this._value?.action}
.disabled=${this.disabled} .disabled=${this.disabled}
@value-changed=${this._serviceChanged} @value-changed=${this._serviceChanged}
.showServiceId=${this.showServiceId} .showServiceId=${this.showServiceId}
></ha-service-picker>`} ></ha-service-picker>`}
${this.hideDescription
<div class="description"> ? nothing
${description ? html`<p>${description}</p>` : ""} : html`
${this._manifest <div class="description">
? html` <a ${description ? html`<p>${description}</p>` : ""}
href=${this._manifest.is_built_in ${this._manifest
? documentationUrl( ? html` <a
this.hass, href=${this._manifest.is_built_in
`/integrations/${this._manifest.domain}` ? documentationUrl(
) this.hass,
: this._manifest.documentation} `/integrations/${this._manifest.domain}`
title=${this.hass.localize( )
"ui.components.service-control.integration_doc" : this._manifest.documentation}
)} title=${this.hass.localize(
target="_blank" "ui.components.service-control.integration_doc"
rel="noreferrer"
>
<ha-icon-button
.path=${mdiHelpCircle}
class="help-icon"
></ha-icon-button>
</a>`
: nothing}
</div>
${serviceData && "target" in serviceData
? html`<ha-settings-row .narrow=${this.narrow}>
${hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
: ""}
<span slot="heading"
>${this.hass.localize(
"ui.components.service-control.target"
)}</span
>
<span slot="description"
>${this.hass.localize(
"ui.components.service-control.target_secondary"
)}</span
><ha-selector
.hass=${this.hass}
.selector=${this._targetSelector(
serviceData.target as TargetSelector,
this._value?.target
)}
.disabled=${this.disabled}
@value-changed=${this._targetChanged}
.value=${this._value?.target}
></ha-selector
></ha-settings-row>`
: entityId
? html`<ha-entity-picker
.hass=${this.hass}
.disabled=${this.disabled}
.value=${this._value?.data?.entity_id}
.label=${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.entity_id.description`
) || entityId.description}
@value-changed=${this._entityPicked}
allow-custom-entity
></ha-entity-picker>`
: ""}
${shouldRenderServiceDataYaml
? html`<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.components.service-control.action_data"
)}
.name=${"data"}
.readOnly=${this.disabled}
.defaultValue=${this._value?.data}
@value-changed=${this._dataChanged}
></ha-yaml-editor>`
: serviceData?.fields.map((dataField) => {
if (!dataField.fields) {
return this._renderField(
dataField,
hasOptional,
domain,
serviceName,
targetEntities
);
}
const fields = Object.entries(dataField.fields).map(
([key, field]) => ({ key, ...field })
);
return fields.length &&
this._hasFilteredFields(fields, targetEntities)
? html`<ha-expansion-panel
left-chevron
.expanded=${!dataField.collapsed}
.header=${this.hass.localize(
`component.${domain}.services.${serviceName}.sections.${dataField.key}.name`
) ||
dataField.name ||
dataField.key}
.secondary=${this._getSectionDescription(
dataField,
domain,
serviceName
)} )}
target="_blank"
rel="noreferrer"
> >
<ha-service-section-icon <ha-icon-button
slot="icons" .path=${mdiHelpCircle}
.hass=${this.hass} class="help-icon"
.service=${this._value!.action} ></ha-icon-button>
.section=${dataField.key} </a>`
></ha-service-section-icon> : nothing}
${Object.entries(dataField.fields).map(([key, field]) => </div>
this._renderField( `}
{ key, ...field }, ${serviceData && "target" in serviceData
hasOptional, ? html`<ha-settings-row .narrow=${this.narrow}>
domain, ${hasOptional
serviceName, ? html`<div slot="prefix" class="checkbox-spacer"></div>`
targetEntities : ""}
) <span slot="heading"
)} >${this.hass.localize("ui.components.service-control.target")}</span
</ha-expansion-panel>` >
: nothing; <span slot="description"
})} `; >${this.hass.localize(
"ui.components.service-control.target_secondary"
)}</span
><ha-selector
.hass=${this.hass}
.selector=${this._targetSelector(
serviceData.target as TargetSelector,
this._value?.target
)}
.disabled=${this.disabled}
@value-changed=${this._targetChanged}
.value=${this._value?.target}
></ha-selector
></ha-settings-row>`
: entityId
? html`<ha-entity-picker
.hass=${this.hass}
.disabled=${this.disabled}
.value=${this._value?.data?.entity_id}
.label=${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.entity_id.description`
) || entityId.description}
@value-changed=${this._entityPicked}
allow-custom-entity
></ha-entity-picker>`
: ""}
${shouldRenderServiceDataYaml
? html`<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.components.service-control.action_data"
)}
.name=${"data"}
.readOnly=${this.disabled}
.defaultValue=${this._value?.data}
@value-changed=${this._dataChanged}
></ha-yaml-editor>`
: serviceData?.fields.map((dataField) => {
if (!dataField.fields) {
return this._renderField(
dataField,
hasOptional,
domain,
serviceName,
targetEntities
);
}
const fields = Object.entries(dataField.fields).map(
([key, field]) => ({ key, ...field })
);
return fields.length &&
this._hasFilteredFields(fields, targetEntities)
? html`<ha-expansion-panel
left-chevron
.expanded=${!dataField.collapsed}
.header=${this.hass.localize(
`component.${domain}.services.${serviceName}.sections.${dataField.key}.name`
) ||
dataField.name ||
dataField.key}
.secondary=${this._getSectionDescription(
dataField,
domain,
serviceName
)}
>
<ha-service-section-icon
slot="icons"
.hass=${this.hass}
.service=${this._value!.action}
.section=${dataField.key}
></ha-service-section-icon>
${Object.entries(dataField.fields).map(([key, field]) =>
this._renderField(
{ key, ...field },
hasOptional,
domain,
serviceName,
targetEntities
)
)}
</ha-expansion-panel>`
: nothing;
})} `;
} }
private _getSectionDescription( private _getSectionDescription(

View File

@@ -1,97 +0,0 @@
import {
mdiAvTimer,
mdiCalendar,
mdiClockOutline,
mdiCodeBraces,
mdiDevices,
mdiFormatListBulleted,
mdiGestureDoubleTap,
mdiHomeAssistant,
mdiMapMarker,
mdiMapMarkerRadius,
mdiMessageAlert,
mdiMicrophoneMessage,
mdiNfcVariant,
mdiNumeric,
mdiStateMachine,
mdiSwapHorizontal,
mdiWeatherSunny,
mdiWebhook,
} from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { computeDomain } from "../common/entity/compute_domain";
import { FALLBACK_DOMAIN_ICONS, triggerIcon } from "../data/icons";
import type { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";
export const TRIGGER_ICONS = {
calendar: mdiCalendar,
device: mdiDevices,
event: mdiGestureDoubleTap,
state: mdiStateMachine,
geo_location: mdiMapMarker,
homeassistant: mdiHomeAssistant,
mqtt: mdiSwapHorizontal,
numeric_state: mdiNumeric,
sun: mdiWeatherSunny,
conversation: mdiMicrophoneMessage,
tag: mdiNfcVariant,
template: mdiCodeBraces,
time: mdiClockOutline,
time_pattern: mdiAvTimer,
webhook: mdiWebhook,
persistent_notification: mdiMessageAlert,
zone: mdiMapMarkerRadius,
list: mdiFormatListBulleted,
};
@customElement("ha-trigger-icon")
export class HaTriggerIcon extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public trigger?: string;
@property() public icon?: string;
protected render() {
if (this.icon) {
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
}
if (!this.trigger) {
return nothing;
}
if (!this.hass) {
return this._renderFallback();
}
const icon = triggerIcon(this.hass, this.trigger).then((icn) => {
if (icn) {
return html`<ha-icon .icon=${icn}></ha-icon>`;
}
return this._renderFallback();
});
return html`${until(icon)}`;
}
private _renderFallback() {
const domain = computeDomain(this.trigger!);
return html`
<ha-svg-icon
.path=${TRIGGER_ICONS[this.trigger!] || FALLBACK_DOMAIN_ICONS[domain]}
></ha-svg-icon>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-trigger-icon": HaTriggerIcon;
}
}

View File

@@ -1,7 +1,6 @@
import type { import type {
HassEntityAttributeBase, HassEntityAttributeBase,
HassEntityBase, HassEntityBase,
HassServiceTarget,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { ensureArray } from "../common/array/ensure-array"; import { ensureArray } from "../common/array/ensure-array";
import { navigate } from "../common/navigate"; import { navigate } from "../common/navigate";
@@ -12,7 +11,6 @@ import { CONDITION_BUILDING_BLOCKS } from "./condition";
import type { DeviceCondition, DeviceTrigger } from "./device_automation"; import type { DeviceCondition, DeviceTrigger } from "./device_automation";
import type { Action, Field, MODES } from "./script"; import type { Action, Field, MODES } from "./script";
import { migrateAutomationAction } from "./script"; import { migrateAutomationAction } from "./script";
import type { TriggerDescription } from "./trigger";
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single"; export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
export const AUTOMATION_DEFAULT_MAX = 10; export const AUTOMATION_DEFAULT_MAX = 10;
@@ -86,11 +84,6 @@ export interface BaseTrigger {
id?: string; id?: string;
variables?: Record<string, unknown>; variables?: Record<string, unknown>;
enabled?: boolean; enabled?: boolean;
options?: Record<string, unknown>;
}
export interface PlatformTrigger extends BaseTrigger {
target?: HassServiceTarget;
} }
export interface StateTrigger extends BaseTrigger { export interface StateTrigger extends BaseTrigger {
@@ -577,7 +570,6 @@ export interface TriggerSidebarConfig extends BaseSidebarConfig {
insertAfter: (value: Trigger | Trigger[]) => boolean; insertAfter: (value: Trigger | Trigger[]) => boolean;
toggleYamlMode: () => void; toggleYamlMode: () => void;
config: Trigger; config: Trigger;
description?: TriggerDescription;
yamlMode: boolean; yamlMode: boolean;
uiSupported: boolean; uiSupported: boolean;
} }

View File

@@ -803,15 +803,9 @@ const tryDescribeTrigger = (
); );
} }
const triggerType = trigger.trigger;
const [domain, type] = triggerType.split(".", 2);
return ( return (
hass.localize( hass.localize(
`component.${domain}.triggers.${type || "_"}.description_configured` `ui.panel.config.automation.editor.triggers.type.${trigger.trigger}.label`
) ||
hass.localize(
`ui.panel.config.automation.editor.triggers.type.${triggerType}.label`
) || ) ||
hass.localize(`ui.panel.config.automation.editor.triggers.unknown_trigger`) hass.localize(`ui.panel.config.automation.editor.triggers.unknown_trigger`)
); );

View File

@@ -131,19 +131,14 @@ const resources: {
all?: Promise<Record<string, ServiceIcons>>; all?: Promise<Record<string, ServiceIcons>>;
domains: Record<string, ServiceIcons | Promise<ServiceIcons>>; domains: Record<string, ServiceIcons | Promise<ServiceIcons>>;
}; };
triggers: {
all?: Promise<Record<string, TriggerIcons>>;
domains: Record<string, TriggerIcons | Promise<TriggerIcons>>;
};
} = { } = {
entity: {}, entity: {},
entity_component: {}, entity_component: {},
services: { domains: {} }, services: { domains: {} },
triggers: { domains: {} },
}; };
interface IconResources< interface IconResources<
T extends ComponentIcons | PlatformIcons | ServiceIcons | TriggerIcons, T extends ComponentIcons | PlatformIcons | ServiceIcons,
> { > {
resources: Record<string, T>; resources: Record<string, T>;
} }
@@ -187,22 +182,12 @@ type ServiceIcons = Record<
{ service: string; sections?: Record<string, string> } { service: string; sections?: Record<string, string> }
>; >;
type TriggerIcons = Record< export type IconCategory = "entity" | "entity_component" | "services";
string,
{ trigger: string; sections?: Record<string, string> }
>;
export type IconCategory =
| "entity"
| "entity_component"
| "services"
| "triggers";
interface CategoryType { interface CategoryType {
entity: PlatformIcons; entity: PlatformIcons;
entity_component: ComponentIcons; entity_component: ComponentIcons;
services: ServiceIcons; services: ServiceIcons;
triggers: TriggerIcons;
} }
export const getHassIcons = async <T extends IconCategory>( export const getHassIcons = async <T extends IconCategory>(
@@ -280,10 +265,12 @@ export const getServiceIcons = async (
if (!force && resources.services.all) { if (!force && resources.services.all) {
return resources.services.all; return resources.services.all;
} }
resources.services.all = getHassIcons(hass, "services").then((res) => { resources.services.all = getHassIcons(hass, "services", domain).then(
resources.services.domains = res.resources; (res) => {
return res?.resources; resources.services.domains = res.resources;
}); return res?.resources;
}
);
return resources.services.all; return resources.services.all;
} }
if (!force && domain in resources.services.domains) { if (!force && domain in resources.services.domains) {
@@ -305,40 +292,6 @@ export const getServiceIcons = async (
return resources.services.domains[domain]; return resources.services.domains[domain];
}; };
export const getTriggerIcons = async (
hass: HomeAssistant,
domain?: string,
force = false
): Promise<TriggerIcons | Record<string, TriggerIcons> | undefined> => {
if (!domain) {
if (!force && resources.triggers.all) {
return resources.triggers.all;
}
resources.triggers.all = getHassIcons(hass, "triggers").then((res) => {
resources.triggers.domains = res.resources;
return res?.resources;
});
return resources.triggers.all;
}
if (!force && domain in resources.triggers.domains) {
return resources.triggers.domains[domain];
}
if (resources.triggers.all && !force) {
await resources.triggers.all;
if (domain in resources.triggers.domains) {
return resources.triggers.domains[domain];
}
}
if (!isComponentLoaded(hass, domain)) {
return undefined;
}
const result = getHassIcons(hass, "triggers", domain);
resources.triggers.domains[domain] = result.then(
(res) => res?.resources[domain]
);
return resources.triggers.domains[domain];
};
// Cache for sorted range keys // Cache for sorted range keys
const sortedRangeCache = new WeakMap<Record<string, string>, number[]>(); const sortedRangeCache = new WeakMap<Record<string, string>, number[]>();
@@ -518,26 +471,6 @@ export const attributeIcon = async (
return icon; return icon;
}; };
export const triggerIcon = async (
hass: HomeAssistant,
trigger: string
): Promise<string | undefined> => {
let icon: string | undefined;
const domain = trigger.includes(".") ? computeDomain(trigger) : trigger;
const triggerName = trigger.includes(".") ? computeObjectId(trigger) : "_";
const triggerIcons = await getTriggerIcons(hass, domain);
if (triggerIcons) {
const trgrIcon = triggerIcons[triggerName] as TriggerIcons[string];
icon = trgrIcon?.trigger;
}
if (!icon) {
icon = await domainIcon(hass, domain);
}
return icon;
};
export const serviceIcon = async ( export const serviceIcon = async (
hass: HomeAssistant, hass: HomeAssistant,
service: string service: string

View File

@@ -316,6 +316,7 @@ export interface MediaSelector {
clearable?: boolean; clearable?: boolean;
hide_content_type?: boolean; hide_content_type?: boolean;
content_id_helper?: string; content_id_helper?: string;
multiple?: boolean;
} | null; } | null;
} }

View File

@@ -73,8 +73,7 @@ export type TranslationCategory =
| "application_credentials" | "application_credentials"
| "issues" | "issues"
| "selector" | "selector"
| "services" | "services";
| "triggers";
export const subscribeTranslationPreferences = ( export const subscribeTranslationPreferences = (
hass: HomeAssistant, hass: HomeAssistant,

View File

@@ -1,12 +1,53 @@
import { mdiDotsHorizontal, mdiMapClock, mdiShape } from "@mdi/js"; import {
mdiAvTimer,
mdiCalendar,
mdiClockOutline,
mdiCodeBraces,
mdiDevices,
mdiDotsHorizontal,
mdiFormatListBulleted,
mdiGestureDoubleTap,
mdiMapClock,
mdiMapMarker,
mdiMapMarkerRadius,
mdiMessageAlert,
mdiMicrophoneMessage,
mdiNfcVariant,
mdiNumeric,
mdiShape,
mdiStateMachine,
mdiSwapHorizontal,
mdiWeatherSunny,
mdiWebhook,
} from "@mdi/js";
import type { HomeAssistant } from "../types"; import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
import type { import type {
AutomationElementGroup, AutomationElementGroup,
Trigger, Trigger,
TriggerList, TriggerList,
} from "./automation"; } from "./automation";
import type { Selector, TargetSelector } from "./selector";
export const TRIGGER_ICONS = {
calendar: mdiCalendar,
device: mdiDevices,
event: mdiGestureDoubleTap,
state: mdiStateMachine,
geo_location: mdiMapMarker,
homeassistant: mdiHomeAssistant,
mqtt: mdiSwapHorizontal,
numeric_state: mdiNumeric,
sun: mdiWeatherSunny,
conversation: mdiMicrophoneMessage,
tag: mdiNfcVariant,
template: mdiCodeBraces,
time: mdiClockOutline,
time_pattern: mdiAvTimer,
webhook: mdiWebhook,
persistent_notification: mdiMessageAlert,
zone: mdiMapMarkerRadius,
list: mdiFormatListBulleted,
};
export const TRIGGER_GROUPS: AutomationElementGroup = { export const TRIGGER_GROUPS: AutomationElementGroup = {
device: {}, device: {},
@@ -33,26 +74,3 @@ export const TRIGGER_GROUPS: AutomationElementGroup = {
export const isTriggerList = (trigger: Trigger): trigger is TriggerList => export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
"triggers" in trigger; "triggers" in trigger;
export interface TriggerDescription {
target?: TargetSelector["target"];
fields: Record<
string,
{
example?: string | boolean | number;
default?: unknown;
required?: boolean;
selector?: Selector;
}
>;
}
export type TriggerDescriptions = Record<string, TriggerDescription>;
export const subscribeTriggers = (
hass: HomeAssistant,
callback: (triggers: TriggerDescriptions) => void
) =>
hass.connection.subscribeMessage<TriggerDescriptions>(callback, {
type: "trigger_platforms/subscribe",
});

View File

@@ -15,6 +15,7 @@ import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache"; import { cache } from "lit/directives/cache";
import { join } from "lit/directives/join";
import { keyed } from "lit/directives/keyed"; import { keyed } from "lit/directives/keyed";
import { dynamicElement } from "../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
@@ -32,7 +33,6 @@ import {
} from "../../common/entity/context/get_entity_context"; } from "../../common/entity/context/get_entity_context";
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event"; import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
import { navigate } from "../../common/navigate"; import { navigate } from "../../common/navigate";
import { computeRTL } from "../../common/util/compute_rtl";
import "../../components/ha-button-menu"; import "../../components/ha-button-menu";
import "../../components/ha-dialog"; import "../../components/ha-dialog";
import "../../components/ha-dialog-header"; import "../../components/ha-dialog-header";
@@ -361,8 +361,6 @@ export class MoreInfoDialog extends LitElement {
); );
const title = this._childView?.viewTitle || breadcrumb.pop() || entityId; const title = this._childView?.viewTitle || breadcrumb.pop() || entityId;
const isRTL = computeRTL(this.hass);
return html` return html`
<ha-dialog <ha-dialog
open open
@@ -396,13 +394,17 @@ export class MoreInfoDialog extends LitElement {
${breadcrumb.length > 0 ${breadcrumb.length > 0
? !__DEMO__ && isAdmin ? !__DEMO__ && isAdmin
? html` ? html`
<button class="breadcrumb" @click=${this._breadcrumbClick}> <button
${breadcrumb.join(isRTL ? " ◂ " : " ▸ ")} class="breadcrumb"
@click=${this._breadcrumbClick}
aria-label=${breadcrumb.join(" > ")}
>
${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
</button> </button>
` `
: html` : html`
<p class="breadcrumb"> <p class="breadcrumb">
${breadcrumb.join(isRTL ? " ◂ " : " ▸ ")} ${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)}
</p> </p>
` `
: nothing} : nothing}

View File

@@ -44,7 +44,7 @@ import {
domainToName, domainToName,
fetchIntegrationManifests, fetchIntegrationManifests,
} from "../../../data/integration"; } from "../../../data/integration";
import { TRIGGER_GROUPS } from "../../../data/trigger"; import { TRIGGER_GROUPS, TRIGGER_ICONS } from "../../../data/trigger";
import type { HassDialog } from "../../../dialogs/make-dialog-manager"; import type { HassDialog } from "../../../dialogs/make-dialog-manager";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin"; import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { HaFuse } from "../../../resources/fuse"; import { HaFuse } from "../../../resources/fuse";
@@ -54,7 +54,6 @@ import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog"; import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
import { PASTE_VALUE } from "./show-add-automation-element-dialog"; import { PASTE_VALUE } from "./show-add-automation-element-dialog";
import { TRIGGER_ICONS } from "../../../components/ha-trigger-icon";
const TYPES = { const TYPES = {
trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS }, trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS },

View File

@@ -27,6 +27,7 @@ import type HaAutomationConditionEditor from "../action/ha-automation-action-edi
import { getAutomationActionType } from "../action/ha-automation-action-row"; import { getAutomationActionType } from "../action/ha-automation-action-row";
import { getRepeatType } from "../action/types/ha-automation-action-repeat"; import { getRepeatType } from "../action/types/ha-automation-action-repeat";
import { overflowStyles, sidebarEditorStyles } from "../styles"; import { overflowStyles, sidebarEditorStyles } from "../styles";
import "../trigger/ha-automation-trigger-editor";
import "./ha-automation-sidebar-card"; import "./ha-automation-sidebar-card";
@customElement("ha-automation-sidebar-action") @customElement("ha-automation-sidebar-action")

View File

@@ -17,6 +17,7 @@ import "../../../../components/ha-dialog-header";
import "../../../../components/ha-icon-button"; import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-button-menu"; import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-divider"; import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import "../ha-automation-editor-warning"; import "../ha-automation-editor-warning";

View File

@@ -6,9 +6,6 @@ import {
} from "@mdi/js"; } from "@mdi/js";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators"; import { customElement, property, query } from "lit/decorators";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-svg-icon";
import type { OptionSidebarConfig } from "../../../../data/automation"; import type { OptionSidebarConfig } from "../../../../data/automation";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac"; import { isMac } from "../../../../util/is_mac";

View File

@@ -22,8 +22,6 @@ import { overflowStyles, sidebarEditorStyles } from "../styles";
import "../trigger/ha-automation-trigger-editor"; import "../trigger/ha-automation-trigger-editor";
import type HaAutomationTriggerEditor from "../trigger/ha-automation-trigger-editor"; import type HaAutomationTriggerEditor from "../trigger/ha-automation-trigger-editor";
import "./ha-automation-sidebar-card"; import "./ha-automation-sidebar-card";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { computeObjectId } from "../../../../common/entity/compute_object_id";
@customElement("ha-automation-sidebar-trigger") @customElement("ha-automation-sidebar-trigger")
export default class HaAutomationSidebarTrigger extends LitElement { export default class HaAutomationSidebarTrigger extends LitElement {
@@ -70,22 +68,9 @@ export default class HaAutomationSidebarTrigger extends LitElement {
"ui.panel.config.automation.editor.triggers.trigger" "ui.panel.config.automation.editor.triggers.trigger"
); );
const domain = const title = this.hass.localize(
"trigger" in this.config.config && `ui.panel.config.automation.editor.triggers.type.${type}.label`
this.config.config.trigger.includes(".") );
? computeDomain(this.config.config.trigger)
: "trigger" in this.config.config && this.config.config.trigger;
const triggerName =
"trigger" in this.config.config &&
this.config.config.trigger.includes(".")
? computeObjectId(this.config.config.trigger)
: "_";
const title =
this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.${type}.label`
) ||
this.hass.localize(`component.${domain}.triggers.${triggerName}.name`);
return html` return html`
<ha-automation-sidebar-card <ha-automation-sidebar-card
@@ -261,7 +246,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
class="sidebar-editor" class="sidebar-editor"
.hass=${this.hass} .hass=${this.hass}
.trigger=${this.config.config} .trigger=${this.config.config}
.description=${this.config.description}
@value-changed=${this._valueChangedSidebar} @value-changed=${this._valueChangedSidebar}
@yaml-changed=${this._yamlChangedSidebar} @yaml-changed=${this._yamlChangedSidebar}
.uiSupported=${this.config.uiSupported} .uiSupported=${this.config.uiSupported}

View File

@@ -9,12 +9,10 @@ import "../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import type { Trigger } from "../../../../data/automation"; import type { Trigger } from "../../../../data/automation";
import { migrateAutomationTrigger } from "../../../../data/automation"; import { migrateAutomationTrigger } from "../../../../data/automation";
import type { TriggerDescription } from "../../../../data/trigger";
import { isTriggerList } from "../../../../data/trigger"; import { isTriggerList } from "../../../../data/trigger";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import "../ha-automation-editor-warning"; import "../ha-automation-editor-warning";
import "./types/ha-automation-trigger-platform";
@customElement("ha-automation-trigger-editor") @customElement("ha-automation-trigger-editor")
export default class HaAutomationTriggerEditor extends LitElement { export default class HaAutomationTriggerEditor extends LitElement {
@@ -31,8 +29,6 @@ export default class HaAutomationTriggerEditor extends LitElement {
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false; @property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
@property({ attribute: false }) public description?: TriggerDescription;
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor; @query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
protected render() { protected render() {
@@ -92,18 +88,11 @@ export default class HaAutomationTriggerEditor extends LitElement {
` `
: nothing} : nothing}
<div @value-changed=${this._onUiChanged}> <div @value-changed=${this._onUiChanged}>
${this.description ${dynamicElement(`ha-automation-trigger-${type}`, {
? html`<ha-automation-trigger-platform hass: this.hass,
.hass=${this.hass} trigger: this.trigger,
.trigger=${this.trigger} disabled: this.disabled,
.description=${this.description} })}
.disabled=${this.disabled}
></ha-automation-trigger-platform>`
: dynamicElement(`ha-automation-trigger-${type}`, {
hass: this.hass,
trigger: this.trigger,
disabled: this.disabled,
})}
</div> </div>
`} `}
</div> </div>

View File

@@ -40,11 +40,9 @@ import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-divider"; import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item"; import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-svg-icon"; import "../../../../components/ha-svg-icon";
import { TRIGGER_ICONS } from "../../../../components/ha-trigger-icon";
import type { import type {
AutomationClipboard, AutomationClipboard,
Trigger, Trigger,
TriggerList,
TriggerSidebarConfig, TriggerSidebarConfig,
} from "../../../../data/automation"; } from "../../../../data/automation";
import { isTrigger, subscribeTrigger } from "../../../../data/automation"; import { isTrigger, subscribeTrigger } from "../../../../data/automation";
@@ -52,8 +50,7 @@ import { describeTrigger } from "../../../../data/automation_i18n";
import { validateConfig } from "../../../../data/config"; import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context"; import { fullEntitiesContext } from "../../../../data/context";
import type { EntityRegistryEntry } from "../../../../data/entity_registry"; import type { EntityRegistryEntry } from "../../../../data/entity_registry";
import type { TriggerDescriptions } from "../../../../data/trigger"; import { TRIGGER_ICONS, isTriggerList } from "../../../../data/trigger";
import { isTriggerList } from "../../../../data/trigger";
import { import {
showAlertDialog, showAlertDialog,
showPromptDialog, showPromptDialog,
@@ -75,7 +72,6 @@ import "./types/ha-automation-trigger-list";
import "./types/ha-automation-trigger-mqtt"; import "./types/ha-automation-trigger-mqtt";
import "./types/ha-automation-trigger-numeric_state"; import "./types/ha-automation-trigger-numeric_state";
import "./types/ha-automation-trigger-persistent_notification"; import "./types/ha-automation-trigger-persistent_notification";
import "./types/ha-automation-trigger-platform";
import "./types/ha-automation-trigger-state"; import "./types/ha-automation-trigger-state";
import "./types/ha-automation-trigger-sun"; import "./types/ha-automation-trigger-sun";
import "./types/ha-automation-trigger-tag"; import "./types/ha-automation-trigger-tag";
@@ -141,9 +137,6 @@ export default class HaAutomationTriggerRow extends LitElement {
@state() private _warnings?: string[]; @state() private _warnings?: string[];
@property({ attribute: false })
public triggerDescriptions: TriggerDescriptions = {};
@property({ type: Boolean }) public narrow = false; @property({ type: Boolean }) public narrow = false;
@query("ha-automation-trigger-editor") @query("ha-automation-trigger-editor")
@@ -185,24 +178,18 @@ export default class HaAutomationTriggerRow extends LitElement {
} }
private _renderRow() { private _renderRow() {
const type = this._getType(this.trigger, this.triggerDescriptions); const type = this._getType(this.trigger);
const supported = this._uiSupported(type); const supported = this._uiSupported(type);
const yamlMode = this._yamlMode || !supported; const yamlMode = this._yamlMode || !supported;
return html` return html`
${type === "list" <ha-svg-icon
? html`<ha-svg-icon slot="leading-icon"
slot="leading-icon" class="trigger-icon"
class="trigger-icon" .path=${TRIGGER_ICONS[type]}
.path=${TRIGGER_ICONS[type]} ></ha-svg-icon>
></ha-svg-icon>`
: html`<ha-trigger-icon
slot="leading-icon"
.hass=${this.hass}
.trigger=${(this.trigger as Exclude<Trigger, TriggerList>).trigger}
></ha-trigger-icon>`}
<h3 slot="header"> <h3 slot="header">
${describeTrigger(this.trigger, this.hass, this._entityReg)} ${describeTrigger(this.trigger, this.hass, this._entityReg)}
</h3> </h3>
@@ -406,9 +393,6 @@ export default class HaAutomationTriggerRow extends LitElement {
<ha-automation-trigger-editor <ha-automation-trigger-editor
.hass=${this.hass} .hass=${this.hass}
.trigger=${this.trigger} .trigger=${this.trigger}
.description=${"trigger" in this.trigger
? this.triggerDescriptions[this.trigger.trigger]
: undefined}
.disabled=${this.disabled} .disabled=${this.disabled}
.yamlMode=${this._yamlMode} .yamlMode=${this._yamlMode}
.uiSupported=${supported} .uiSupported=${supported}
@@ -568,7 +552,6 @@ export default class HaAutomationTriggerRow extends LitElement {
} }
public openSidebar(trigger?: Trigger): void { public openSidebar(trigger?: Trigger): void {
trigger = trigger || this.trigger;
fireEvent(this, "open-sidebar", { fireEvent(this, "open-sidebar", {
save: (value) => { save: (value) => {
fireEvent(this, "value-changed", { value }); fireEvent(this, "value-changed", { value });
@@ -593,14 +576,8 @@ export default class HaAutomationTriggerRow extends LitElement {
duplicate: this._duplicateTrigger, duplicate: this._duplicateTrigger,
cut: this._cutTrigger, cut: this._cutTrigger,
insertAfter: this._insertAfter, insertAfter: this._insertAfter,
config: trigger, config: trigger || this.trigger,
uiSupported: this._uiSupported( uiSupported: this._uiSupported(this._getType(trigger || this.trigger)),
this._getType(trigger, this.triggerDescriptions)
),
description:
"trigger" in trigger
? this.triggerDescriptions[trigger.trigger]
: undefined,
yamlMode: this._yamlMode, yamlMode: this._yamlMode,
} satisfies TriggerSidebarConfig); } satisfies TriggerSidebarConfig);
this._selected = true; this._selected = true;
@@ -782,18 +759,8 @@ export default class HaAutomationTriggerRow extends LitElement {
}); });
} }
private _getType = memoizeOne( private _getType = memoizeOne((trigger: Trigger) =>
(trigger: Trigger, triggerDescriptions: TriggerDescriptions) => { isTriggerList(trigger) ? "list" : trigger.trigger
if (isTriggerList(trigger)) {
return "list";
}
if (trigger.trigger in triggerDescriptions) {
return "platform";
}
return trigger.trigger;
}
); );
private _uiSupported = memoizeOne( private _uiSupported = memoizeOne(

View File

@@ -4,7 +4,6 @@ import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat"; import { repeat } from "lit/directives/repeat";
import { ensureArray } from "../../../../common/array/ensure-array";
import { storage } from "../../../../common/decorators/storage"; import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../../common/dom/stop_propagation";
@@ -18,9 +17,7 @@ import type {
Trigger, Trigger,
TriggerList, TriggerList,
} from "../../../../data/automation"; } from "../../../../data/automation";
import type { TriggerDescriptions } from "../../../../data/trigger"; import { isTriggerList } from "../../../../data/trigger";
import { isTriggerList, subscribeTriggers } from "../../../../data/trigger";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { import {
PASTE_VALUE, PASTE_VALUE,
@@ -29,9 +26,10 @@ import {
import { automationRowsStyles } from "../styles"; import { automationRowsStyles } from "../styles";
import "./ha-automation-trigger-row"; import "./ha-automation-trigger-row";
import type HaAutomationTriggerRow from "./ha-automation-trigger-row"; import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
import { ensureArray } from "../../../../common/array/ensure-array";
@customElement("ha-automation-trigger") @customElement("ha-automation-trigger")
export default class HaAutomationTrigger extends SubscribeMixin(LitElement) { export default class HaAutomationTrigger extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public triggers!: Trigger[]; @property({ attribute: false }) public triggers!: Trigger[];
@@ -64,23 +62,6 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
private _triggerKeys = new WeakMap<Trigger, string>(); private _triggerKeys = new WeakMap<Trigger, string>();
@state() private _triggerDescriptions: TriggerDescriptions = {};
protected hassSubscribe() {
return [
subscribeTriggers(this.hass, (triggers) => this._addTriggers(triggers)),
];
}
private _addTriggers(triggers: TriggerDescriptions) {
this._triggerDescriptions = { ...this._triggerDescriptions, ...triggers };
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.hass.loadBackendTranslation("triggers");
}
protected render() { protected render() {
return html` return html`
<ha-sortable <ha-sortable
@@ -104,7 +85,6 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
.first=${idx === 0} .first=${idx === 0}
.last=${idx === this.triggers.length - 1} .last=${idx === this.triggers.length - 1}
.trigger=${trg} .trigger=${trg}
.triggerDescriptions=${this._triggerDescriptions}
@duplicate=${this._duplicateTrigger} @duplicate=${this._duplicateTrigger}
@insert-after=${this._insertAfter} @insert-after=${this._insertAfter}
@move-down=${this._moveDown} @move-down=${this._moveDown}

View File

@@ -1,406 +0,0 @@
import { mdiHelpCircle } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { computeDomain } from "../../../../../common/entity/compute_domain";
import { computeObjectId } from "../../../../../common/entity/compute_object_id";
import "../../../../../components/ha-checkbox";
import "../../../../../components/ha-selector/ha-selector";
import "../../../../../components/ha-settings-row";
import "../../../../../components/ha-textfield";
import "../../../../../components/ha-yaml-editor";
import "../../../../../components/user/ha-users-picker";
import type { PlatformTrigger } from "../../../../../data/automation";
import type { IntegrationManifest } from "../../../../../data/integration";
import { fetchIntegrationManifest } from "../../../../../data/integration";
import type { TargetSelector } from "../../../../../data/selector";
import type { TriggerDescription } from "../../../../../data/trigger";
import type { HomeAssistant } from "../../../../../types";
import { documentationUrl } from "../../../../../util/documentation-url";
const showOptionalToggle = (field) =>
field.selector &&
!field.required &&
!("boolean" in field.selector && field.default);
@customElement("ha-automation-trigger-platform")
export class HaPlatformTrigger extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trigger!: PlatformTrigger;
@property({ attribute: false }) public description?: TriggerDescription;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public narrow = true;
@state() private _checkedKeys = new Set();
@state() private _manifest?: IntegrationManifest;
public static get defaultConfig(): PlatformTrigger {
return { trigger: "" };
}
protected willUpdate(changedProperties: PropertyValues<this>) {
super.willUpdate(changedProperties);
if (!this.hasUpdated) {
this.hass.loadBackendTranslation("triggers");
this.hass.loadBackendTranslation("selector");
}
if (!changedProperties.has("trigger")) {
return;
}
const oldValue = changedProperties.get("trigger") as
| undefined
| this["trigger"];
// Fetch the manifest if we have a trigger selected and the trigger domain changed.
// If no trigger is selected, clear the manifest.
if (this.trigger?.trigger) {
const domain = this.trigger.trigger.includes(".")
? computeDomain(this.trigger.trigger)
: this.trigger.trigger;
const oldDomain = oldValue?.trigger.includes(".")
? computeDomain(oldValue.trigger)
: oldValue?.trigger;
if (domain !== oldDomain) {
this._fetchManifest(domain);
}
} else {
this._manifest = undefined;
}
}
protected render() {
const domain = this.trigger.trigger.includes(".")
? computeDomain(this.trigger.trigger)
: this.trigger.trigger;
const triggerName = this.trigger.trigger.includes(".")
? computeObjectId(this.trigger.trigger)
: "_";
const description = this.hass.localize(
`component.${domain}.triggers.${triggerName}.description`
);
const triggerDesc = this.description;
const shouldRenderDataYaml = !triggerDesc?.fields;
const hasOptional = Boolean(
triggerDesc?.fields &&
Object.values(triggerDesc.fields).some((field) =>
showOptionalToggle(field)
)
);
return html`
<div class="description">
${description ? html`<p>${description}</p>` : ""}
${this._manifest
? html` <a
href=${this._manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._manifest.domain}`
)
: this._manifest.documentation}
title=${this.hass.localize(
"ui.components.service-control.integration_doc"
)}
target="_blank"
rel="noreferrer"
>
<ha-icon-button
.path=${mdiHelpCircle}
class="help-icon"
></ha-icon-button>
</a>`
: nothing}
</div>
${triggerDesc && "target" in triggerDesc
? html`<ha-settings-row .narrow=${this.narrow}>
${hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
: ""}
<span slot="heading"
>${this.hass.localize(
"ui.components.service-control.target"
)}</span
>
<span slot="description"
>${this.hass.localize(
"ui.components.service-control.target_secondary"
)}</span
><ha-selector
.hass=${this.hass}
.selector=${this._targetSelector(triggerDesc.target)}
.disabled=${this.disabled}
@value-changed=${this._targetChanged}
.value=${this.trigger?.target}
></ha-selector
></ha-settings-row>`
: nothing}
${shouldRenderDataYaml
? html`<ha-yaml-editor
.hass=${this.hass}
.label=${this.hass.localize(
"ui.components.service-control.action_data"
)}
.name=${"data"}
.readOnly=${this.disabled}
.defaultValue=${this.trigger?.options}
@value-changed=${this._dataChanged}
></ha-yaml-editor>`
: Object.entries(triggerDesc.fields).map(([fieldName, dataField]) =>
this._renderField(
fieldName,
dataField,
hasOptional,
domain,
triggerName
)
)}
`;
}
private _targetSelector = memoizeOne(
(targetSelector: TargetSelector["target"] | null | undefined) =>
targetSelector ? { target: { ...targetSelector } } : { target: {} }
);
private _renderField = (
fieldName: string,
dataField: TriggerDescription["fields"][string],
hasOptional: boolean,
domain: string | undefined,
triggerName: string | undefined
) => {
const selector = dataField?.selector ?? { text: null };
const showOptional = showOptionalToggle(dataField);
return dataField.selector
? html`<ha-settings-row .narrow=${this.narrow}>
${!showOptional
? hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
: ""
: html`<ha-checkbox
.key=${fieldName}
.checked=${this._checkedKeys.has(fieldName) ||
(this.trigger?.options &&
this.trigger.options[fieldName] !== undefined)}
.disabled=${this.disabled}
@change=${this._checkboxChanged}
slot="prefix"
></ha-checkbox>`}
<span slot="heading"
>${this.hass.localize(
`component.${domain}.triggers.${triggerName}.fields.${fieldName}.name`
) || triggerName}</span
>
<span slot="description"
>${this.hass.localize(
`component.${domain}.triggers.${triggerName}.fields.${fieldName}.description`
)}</span
>
<ha-selector
.disabled=${this.disabled ||
(showOptional &&
!this._checkedKeys.has(fieldName) &&
(!this.trigger?.options ||
this.trigger.options[fieldName] === undefined))}
.hass=${this.hass}
.selector=${selector}
.key=${fieldName}
@value-changed=${this._dataChanged}
.value=${this.trigger?.options
? this.trigger.options[fieldName]
: undefined}
.placeholder=${dataField.default}
.localizeValue=${this._localizeValueCallback}
></ha-selector>
</ha-settings-row>`
: "";
};
private _dataChanged(ev: CustomEvent) {
ev.stopPropagation();
if (ev.detail.isValid === false) {
// Don't clear an object selector that returns invalid YAML
return;
}
const key = (ev.currentTarget as any).key;
const value = ev.detail.value;
if (
this.trigger?.options?.[key] === value ||
((!this.trigger?.options || !(key in this.trigger.options)) &&
(value === "" || value === undefined))
) {
return;
}
const options = { ...this.trigger?.options, [key]: value };
if (
value === "" ||
value === undefined ||
(typeof value === "object" && !Object.keys(value).length)
) {
delete options[key];
}
fireEvent(this, "value-changed", {
value: {
...this.trigger,
options,
},
});
}
private _targetChanged(ev: CustomEvent): void {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: {
...this.trigger,
target: ev.detail.value,
},
});
}
private _checkboxChanged(ev) {
const checked = ev.currentTarget.checked;
const key = ev.currentTarget.key;
let options;
if (checked) {
this._checkedKeys.add(key);
const field =
this.description &&
Object.entries(this.description).find(([k, _value]) => k === key)?.[1];
let defaultValue = field?.default;
if (
defaultValue == null &&
field?.selector &&
"constant" in field.selector
) {
defaultValue = field.selector.constant?.value;
}
if (
defaultValue == null &&
field?.selector &&
"boolean" in field.selector
) {
defaultValue = false;
}
if (defaultValue != null) {
options = {
...this.trigger?.options,
[key]: defaultValue,
};
}
} else {
this._checkedKeys.delete(key);
options = { ...this.trigger?.options };
delete options[key];
}
if (options) {
fireEvent(this, "value-changed", {
value: {
...this.trigger,
options,
},
});
}
this.requestUpdate("_checkedKeys");
}
private _localizeValueCallback = (key: string) => {
if (!this.trigger?.trigger) {
return "";
}
return this.hass.localize(
`component.${computeDomain(this.trigger.trigger)}.selector.${key}`
);
};
private async _fetchManifest(integration: string) {
this._manifest = undefined;
try {
this._manifest = await fetchIntegrationManifest(this.hass, integration);
} catch (_err: any) {
// Ignore if loading manifest fails. Probably bad JSON in manifest
}
}
static styles = css`
ha-settings-row {
padding: var(--service-control-padding, 0 16px);
}
ha-settings-row[narrow] {
padding-bottom: 8px;
}
ha-settings-row {
--settings-row-content-width: 100%;
--settings-row-prefix-display: contents;
border-top: var(
--service-control-items-border-top,
1px solid var(--divider-color)
);
}
ha-service-picker,
ha-entity-picker,
ha-yaml-editor {
display: block;
margin: var(--service-control-padding, 0 16px);
}
ha-yaml-editor {
padding: 16px 0;
}
p {
margin: var(--service-control-padding, 0 16px);
padding: 16px 0;
}
:host([hide-picker]) p {
padding-top: 0;
}
.checkbox-spacer {
width: 32px;
}
ha-checkbox {
margin-left: -16px;
margin-inline-start: -16px;
margin-inline-end: initial;
}
.help-icon {
color: var(--secondary-text-color);
}
.description {
justify-content: space-between;
display: flex;
align-items: center;
padding-right: 2px;
padding-inline-end: 2px;
padding-inline-start: initial;
}
.description p {
direction: ltr;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-platform": HaPlatformTrigger;
}
}

View File

@@ -128,10 +128,11 @@ class ZHAAddDevicesPage extends LitElement {
this.hass, this.hass,
"/integrations/zha#adding-devices" "/integrations/zha#adding-devices"
)} )}
>${this.hass.localize(
"ui.panel.config.zha.add_device_page.pairing_mode_link"
)}</a
> >
${this.hass.localize(
"ui.panel.config.zha.add_device_page.pairing_mode_link"
)}
</a>
`, `,
} }
)} )}

View File

@@ -4,7 +4,6 @@ import { isComponentLoaded } from "../../../../common/config/is_component_loaded
import { generateEntityFilter } from "../../../../common/entity/entity_filter"; import { generateEntityFilter } from "../../../../common/entity/entity_filter";
import type { AreaRegistryEntry } from "../../../../data/area_registry"; import type { AreaRegistryEntry } from "../../../../data/area_registry";
import { getEnergyPreferences } from "../../../../data/energy"; import { getEnergyPreferences } from "../../../../data/energy";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { import type {
LovelaceSectionConfig, LovelaceSectionConfig,
LovelaceSectionRawConfig, LovelaceSectionRawConfig,
@@ -16,11 +15,11 @@ import type {
AreaCardConfig, AreaCardConfig,
HomeSummaryCard, HomeSummaryCard,
MarkdownCardConfig, MarkdownCardConfig,
TileCardConfig,
WeatherForecastCardConfig, WeatherForecastCardConfig,
} from "../../cards/types"; } from "../../cards/types";
import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper"; import { getAreas } from "../areas/helpers/areas-strategy-helper";
import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy"; import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
import { getHomeStructure } from "./helpers/home-structure";
export interface HomeMainViewStrategyConfig { export interface HomeMainViewStrategyConfig {
type: "home-main"; type: "home-main";
@@ -60,67 +59,25 @@ export class HomeMainViewStrategy extends ReactiveElement {
hass: HomeAssistant hass: HomeAssistant
): Promise<LovelaceViewConfig> { ): Promise<LovelaceViewConfig> {
const areas = getAreas(hass.areas); const areas = getAreas(hass.areas);
const floors = getFloors(hass.floors);
const home = getHomeStructure(floors, areas); const areasSection: LovelaceSectionConfig = {
type: "grid",
const floorCount = home.floors.length + (home.areas.length ? 1 : 0); column_span: 2,
cards: [
{
type: "heading",
heading_style: "title",
heading: hass.localize("ui.panel.lovelace.strategy.home.areas"),
},
...areas.map<AreaCardConfig>((area) =>
computeAreaCard(area.area_id, hass)
),
],
};
// Allow between 2 and 3 columns (the max should be set to define the width of the header) // Allow between 2 and 3 columns (the max should be set to define the width of the header)
const maxColumns = 2; const maxColumns = 2;
const floorsSections: LovelaceSectionConfig[] = [];
for (const floorStructure of home.floors) {
const floorId = floorStructure.id;
const areaIds = floorStructure.areas;
const floor = hass.floors[floorId];
const cards: LovelaceCardConfig[] = [];
for (const areaId of areaIds) {
cards.push(computeAreaCard(areaId, hass));
}
if (cards.length) {
floorsSections.push({
type: "grid",
column_span: maxColumns,
cards: [
{
type: "heading",
heading:
floorCount > 1
? floor.name
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
heading_style: "title",
},
...cards,
],
});
}
}
if (home.areas.length) {
const cards: LovelaceCardConfig[] = [];
for (const areaId of home.areas) {
cards.push(computeAreaCard(areaId, hass));
}
floorsSections.push({
type: "grid",
column_span: maxColumns,
cards: [
{
type: "heading",
heading:
floorCount > 1
? hass.localize("ui.panel.lovelace.strategy.home.other_areas")
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
heading_style: "title",
},
...cards,
],
});
}
const favoriteSection: LovelaceSectionConfig = { const favoriteSection: LovelaceSectionConfig = {
type: "grid", type: "grid",
column_span: maxColumns, column_span: maxColumns,
@@ -130,14 +87,31 @@ export class HomeMainViewStrategy extends ReactiveElement {
const favoriteEntities = (config.favorite_entities || []).filter( const favoriteEntities = (config.favorite_entities || []).filter(
(entityId) => hass.states[entityId] !== undefined (entityId) => hass.states[entityId] !== undefined
); );
const maxCommonControls = Math.max(8, favoriteEntities.length);
if (favoriteEntities.length > 0) {
favoriteSection.cards!.push(
{
type: "heading",
heading: "",
heading_style: "subtitle",
},
...favoriteEntities.map(
(entityId) =>
({
type: "tile",
entity: entityId,
show_entity_picture: true,
}) as TileCardConfig
)
);
}
const commonControlsSection = { const commonControlsSection = {
strategy: { strategy: {
type: "common-controls", type: "common-controls",
title: hass.localize("ui.panel.lovelace.strategy.home.common_controls"), title: hass.localize("ui.panel.lovelace.strategy.home.common_controls"),
limit: maxCommonControls, limit: 4,
include_entities: favoriteEntities, exclude_entities: favoriteEntities,
hide_empty: true, hide_empty: true,
} satisfies CommonControlSectionStrategyConfig, } satisfies CommonControlSectionStrategyConfig,
column_span: maxColumns, column_span: maxColumns,
@@ -260,7 +234,7 @@ export class HomeMainViewStrategy extends ReactiveElement {
favoriteSection.cards && favoriteSection, favoriteSection.cards && favoriteSection,
commonControlsSection, commonControlsSection,
summarySection, summarySection,
...floorsSections, areasSection,
widgetSection.cards && widgetSection, widgetSection.cards && widgetSection,
] satisfies (LovelaceSectionRawConfig | undefined)[] ] satisfies (LovelaceSectionRawConfig | undefined)[]
).filter(Boolean) as LovelaceSectionRawConfig[]; ).filter(Boolean) as LovelaceSectionRawConfig[];

View File

@@ -14,7 +14,6 @@ export interface CommonControlSectionStrategyConfig {
icon?: string; icon?: string;
limit?: number; limit?: number;
exclude_entities?: string[]; exclude_entities?: string[];
include_entities?: string[];
hide_empty?: boolean; hide_empty?: boolean;
} }
@@ -53,23 +52,12 @@ export class CommonControlsSectionStrategy extends ReactiveElement {
(entity) => entity in hass.states (entity) => entity in hass.states
); );
if (config.exclude_entities?.length) { if (config.exclude_entities) {
predictedEntities = predictedEntities.filter( predictedEntities = predictedEntities.filter(
(entity) => !config.exclude_entities!.includes(entity) (entity) => !config.exclude_entities!.includes(entity)
); );
} }
if (config.include_entities?.length) {
// Remove included entities from predicted list to avoid duplicates
predictedEntities = predictedEntities.filter(
(entity) => !config.include_entities!.includes(entity)
);
// Add included entities to the start of the list
predictedEntities.unshift(
...config.include_entities!.filter((entity) => entity in hass.states)
);
}
const limit = config.limit ?? DEFAULT_LIMIT; const limit = config.limit ?? DEFAULT_LIMIT;
predictedEntities = predictedEntities.slice(0, limit); predictedEntities = predictedEntities.slice(0, limit);

180
yarn.lock
View File

@@ -1698,15 +1698,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/ecma402-abstract@npm:2.3.6": "@formatjs/ecma402-abstract@npm:2.3.5":
version: 2.3.6 version: 2.3.5
resolution: "@formatjs/ecma402-abstract@npm:2.3.6" resolution: "@formatjs/ecma402-abstract@npm:2.3.5"
dependencies: dependencies:
"@formatjs/fast-memoize": "npm:2.2.7" "@formatjs/fast-memoize": "npm:2.2.7"
"@formatjs/intl-localematcher": "npm:0.6.2" "@formatjs/intl-localematcher": "npm:0.6.2"
decimal.js: "npm:^10.4.3" decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0" tslib: "npm:^2.8.0"
checksum: 10/30b1b5cd6b62ba46245f934429936592df5500bc1b089dc92dd49c826757b873dd92c305dcfe370701e4df6b057bf007782113abb9b65db550d73be4961718bc checksum: 10/254651057170836237dc4f0fbb372157f97133c4dcee414007e0cdb5b589baf0546c2f6337d117b988ee0a4f0a4d8247780aaa9e96b410c568495f162c40dc50
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1719,68 +1719,68 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/icu-messageformat-parser@npm:2.11.4": "@formatjs/icu-messageformat-parser@npm:2.11.3":
version: 2.11.4 version: 2.11.3
resolution: "@formatjs/icu-messageformat-parser@npm:2.11.4" resolution: "@formatjs/icu-messageformat-parser@npm:2.11.3"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6" "@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/icu-skeleton-parser": "npm:1.8.16" "@formatjs/icu-skeleton-parser": "npm:1.8.15"
tslib: "npm:^2.8.0" tslib: "npm:^2.8.0"
checksum: 10/2acb100c06c2ade666d72787fb9f9795b1ace41e8e73bfadc2b1a7b8562e81f655e484f0f33d8c39473aa17bf0ad96fb2228871806a9b3dc4f5f876754a0de3a checksum: 10/339f5ff5ea7417e2db7f01bd41340f78fd5a8e56a66e723272d21ce7ab4b265dcb45748cdca76eac7137e2b5e6767986812b471e011b4602cf7afbc6da57fb98
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/icu-skeleton-parser@npm:1.8.16": "@formatjs/icu-skeleton-parser@npm:1.8.15":
version: 1.8.16 version: 1.8.15
resolution: "@formatjs/icu-skeleton-parser@npm:1.8.16" resolution: "@formatjs/icu-skeleton-parser@npm:1.8.15"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6" "@formatjs/ecma402-abstract": "npm:2.3.5"
tslib: "npm:^2.8.0" tslib: "npm:^2.8.0"
checksum: 10/428001e5bed81889b276a2356a1393157af91dc59220b765a1a132f6407ac5832b7ac6ae9737674ac38e44035295c0c1c310b2630f383f2b5779ea90bf2849e6 checksum: 10/19825abc1a5eef0288456c08420d06f3da8256fbe81db0b9ead48cacc94954d748c8068988e26d184d38fca2e50c191ecda5a10ff3935529c3134b8d80db0538
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-datetimeformat@npm:6.18.2": "@formatjs/intl-datetimeformat@npm:6.18.1":
version: 6.18.2 version: 6.18.1
resolution: "@formatjs/intl-datetimeformat@npm:6.18.2" resolution: "@formatjs/intl-datetimeformat@npm:6.18.1"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6" "@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/intl-localematcher": "npm:0.6.2" "@formatjs/intl-localematcher": "npm:0.6.2"
decimal.js: "npm:^10.4.3" decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0" tslib: "npm:^2.8.0"
checksum: 10/e6f80d0eb2049564502370839697a18858268a0dff8d199b1908137c4a229b1303131c12b8b8a8e8e259a1feba26dbc25b003b150adabea10d1c43f68086efbe checksum: 10/66938778ecf37472a7e2f1d9349b0ac249fcbd5d684ae5614dea07287876182429980ba2fe3671224f981065baf017ac955f4b3c1f3c924c89bf2ec82dd1acd8
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-displaynames@npm:6.8.13": "@formatjs/intl-displaynames@npm:6.8.12":
version: 6.8.13 version: 6.8.12
resolution: "@formatjs/intl-displaynames@npm:6.8.13" resolution: "@formatjs/intl-displaynames@npm:6.8.12"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6" "@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/intl-localematcher": "npm:0.6.2" "@formatjs/intl-localematcher": "npm:0.6.2"
tslib: "npm:^2.8.0" tslib: "npm:^2.8.0"
checksum: 10/adefd25fa42266c7bc33dd3cd50f3681bdce51d18b32a03c98f8ad7587dfd8b9291345e185a4b16f31f4eee10fc799fd1b6361bdfd3a2c9fe127744e1e0f3b07 checksum: 10/7de27ef7e8cde2febce84d5443f00b70062cbd0c3f1039ce8ed1caacb15c4c7a36da16295f26657d59aa4663141a04d7b1083bfd1eea6a4e8ad9dc6093a2c886
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-durationformat@npm:0.7.6": "@formatjs/intl-durationformat@npm:0.7.5":
version: 0.7.6 version: 0.7.5
resolution: "@formatjs/intl-durationformat@npm:0.7.6" resolution: "@formatjs/intl-durationformat@npm:0.7.5"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6" "@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/intl-localematcher": "npm:0.6.2" "@formatjs/intl-localematcher": "npm:0.6.2"
tslib: "npm:^2.8.0" tslib: "npm:^2.8.0"
checksum: 10/442236ba85bcd9cb7296c43a708271fa09f110b1ca9d5899066d00812fc2965eaeaec6b5240be421b80daba62860352131088449ba0fcd2061f671cec6240f0b checksum: 10/4dc81b112fed25dc8da0a16ddeff033b7c763bf9a1cfd7b1b25c1216f7f147eb67a47059a3cf95b4d4ade150c54a813542b84e69298905a4bc22548d74bf8567
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-enumerator@npm:1.8.12": "@formatjs/intl-enumerator@npm:1.8.11":
version: 1.8.12 version: 1.8.11
resolution: "@formatjs/intl-enumerator@npm:1.8.12" resolution: "@formatjs/intl-enumerator@npm:1.8.11"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6" "@formatjs/ecma402-abstract": "npm:2.3.5"
tslib: "npm:^2.8.0" tslib: "npm:^2.8.0"
checksum: 10/8dfd7ca5383b4dca530e1df5118a72f71347f4e0daa6131b82dbf7e860a8b96bec0fed43bfa6f6e650e55fa50fcd3e9e3a5253515131b578539d8eaa84630927 checksum: 10/8646a517cd4160c1ceff888ec8fdf652caa3d375fa41231e829c13bc7be0cd156c9642e339b75e9cfa8ef60ae8140c766f9055318c62f1c1d9345f25cdb7f426
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1793,26 +1793,26 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-listformat@npm:7.7.13": "@formatjs/intl-listformat@npm:7.7.12":
version: 7.7.13 version: 7.7.12
resolution: "@formatjs/intl-listformat@npm:7.7.13" resolution: "@formatjs/intl-listformat@npm:7.7.12"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6" "@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/intl-localematcher": "npm:0.6.2" "@formatjs/intl-localematcher": "npm:0.6.2"
tslib: "npm:^2.8.0" tslib: "npm:^2.8.0"
checksum: 10/476d7cffb64eb996a888b1865aa237f04088de60fa7c65b6d073bca8a3c0f4304040ef12f16eafaf6587895976b773607296951afa7f119447d8f9b2c40daa55 checksum: 10/eee910e83ad28b3b3c24ab6e155720187ae5b5ac936ffa2c8ec6cc8c392c194fd5c79a166290da1c6de8dc1857e3d9d11241029832ec88f7a85cce1821b7f067
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-locale@npm:4.2.13": "@formatjs/intl-locale@npm:4.2.12":
version: 4.2.13 version: 4.2.12
resolution: "@formatjs/intl-locale@npm:4.2.13" resolution: "@formatjs/intl-locale@npm:4.2.12"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6" "@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/intl-enumerator": "npm:1.8.12" "@formatjs/intl-enumerator": "npm:1.8.11"
"@formatjs/intl-getcanonicallocales": "npm:2.5.6" "@formatjs/intl-getcanonicallocales": "npm:2.5.6"
tslib: "npm:^2.8.0" tslib: "npm:^2.8.0"
checksum: 10/865615561b4bad8b8d7d93539cae7eb3ed2d46b6156486ef3ccb1b8f9f46f075c7cf2f6e5325aba1cf07150e19280858dff7dfd86d530fbf45fd31ea4fabf8d4 checksum: 10/42111a3002a5a2076b3eb012073230f69c62355dc03647bc17f4d0805f39c7e720e2281b359277d020fef623944a5bcc1ddc3dae9a3af74886d876147680147d
languageName: node languageName: node
linkType: hard linkType: hard
@@ -1825,38 +1825,38 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-numberformat@npm:8.15.6": "@formatjs/intl-numberformat@npm:8.15.5":
version: 8.15.6 version: 8.15.5
resolution: "@formatjs/intl-numberformat@npm:8.15.6" resolution: "@formatjs/intl-numberformat@npm:8.15.5"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6" "@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/intl-localematcher": "npm:0.6.2" "@formatjs/intl-localematcher": "npm:0.6.2"
decimal.js: "npm:^10.4.3" decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0" tslib: "npm:^2.8.0"
checksum: 10/674c5fefa0b14fcd7c58d0c0e592b4887dc2563fa5a11d80a0a82328ac12b2bb82b9a5367fa0a4d80060d61d15a1821bca7085e20cad09aa93b87edb3cff68ea checksum: 10/3440371a43c54cdd2aa3714cb518ad22e491dd19fbc0c046e712dde078d3f6ed709474376863d64d2bddb506957d1cf265d440f6723b88211044a7b56186e550
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-pluralrules@npm:5.4.6": "@formatjs/intl-pluralrules@npm:5.4.5":
version: 5.4.6 version: 5.4.5
resolution: "@formatjs/intl-pluralrules@npm:5.4.6" resolution: "@formatjs/intl-pluralrules@npm:5.4.5"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6" "@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/intl-localematcher": "npm:0.6.2" "@formatjs/intl-localematcher": "npm:0.6.2"
decimal.js: "npm:^10.4.3" decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0" tslib: "npm:^2.8.0"
checksum: 10/88aa244e69ccfdf459899f5fa3c64df345f451ef91ce1188eab35b7e37daa225d22120f64be633f2cd8b826ea705d19831915118f555f2d17611ee842a9a86dc checksum: 10/00f650891893b743d126dd2bf0d17c1b16a8c9e0e0dd94cd0895e66cb556246116263e9603204e1991924814d0ed3a3503765914aff08181d5e4435dfc5e547c
languageName: node languageName: node
linkType: hard linkType: hard
"@formatjs/intl-relativetimeformat@npm:11.4.13": "@formatjs/intl-relativetimeformat@npm:11.4.12":
version: 11.4.13 version: 11.4.12
resolution: "@formatjs/intl-relativetimeformat@npm:11.4.13" resolution: "@formatjs/intl-relativetimeformat@npm:11.4.12"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6" "@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/intl-localematcher": "npm:0.6.2" "@formatjs/intl-localematcher": "npm:0.6.2"
tslib: "npm:^2.8.0" tslib: "npm:^2.8.0"
checksum: 10/c2058d5f29a13aa216d317d309a6ffd7d203f0fe11696b7bd524e17ac3cc22ae50ad56a26dbf18125e4c115a3e75f01e6cf2134a83df6c7916ae6d3fb21a1e9b checksum: 10/f6adca59738cb7f58d2ea985558d8fc45e567406de6fb6e67894afe790e2a9fa1a19d34853afc36805fa4a3d638e29c62d6c6ba3ec2a85628c240081dcdfebc1
languageName: node languageName: node
linkType: hard linkType: hard
@@ -6925,10 +6925,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"core-js@npm:3.46.0": "core-js@npm:3.45.1":
version: 3.46.0 version: 3.45.1
resolution: "core-js@npm:3.46.0" resolution: "core-js@npm:3.45.1"
checksum: 10/82993ca487c6cbbf8bbf00e45eeb9705eb63dc2f9c90d7f35696733efbc3f4b52426e1f8dbef0f0b68ea16caa21e4f44cc5490e08120e1cad4a72b031ed8adaa checksum: 10/b9dca79b1af8bb4f0d4af0752ea98d694fe157abaf55513fd4084df32dfd4398f0fc57898b32cdb643c1cecb87b9231c2a2ce535797c80ae328eac6d6078ee61
languageName: node languageName: node
linkType: hard linkType: hard
@@ -9192,15 +9192,15 @@ __metadata:
"@codemirror/view": "npm:6.38.5" "@codemirror/view": "npm:6.38.5"
"@date-fns/tz": "npm:1.4.1" "@date-fns/tz": "npm:1.4.1"
"@egjs/hammerjs": "npm:2.0.17" "@egjs/hammerjs": "npm:2.0.17"
"@formatjs/intl-datetimeformat": "npm:6.18.2" "@formatjs/intl-datetimeformat": "npm:6.18.1"
"@formatjs/intl-displaynames": "npm:6.8.13" "@formatjs/intl-displaynames": "npm:6.8.12"
"@formatjs/intl-durationformat": "npm:0.7.6" "@formatjs/intl-durationformat": "npm:0.7.5"
"@formatjs/intl-getcanonicallocales": "npm:2.5.6" "@formatjs/intl-getcanonicallocales": "npm:2.5.6"
"@formatjs/intl-listformat": "npm:7.7.13" "@formatjs/intl-listformat": "npm:7.7.12"
"@formatjs/intl-locale": "npm:4.2.13" "@formatjs/intl-locale": "npm:4.2.12"
"@formatjs/intl-numberformat": "npm:8.15.6" "@formatjs/intl-numberformat": "npm:8.15.5"
"@formatjs/intl-pluralrules": "npm:5.4.6" "@formatjs/intl-pluralrules": "npm:5.4.5"
"@formatjs/intl-relativetimeformat": "npm:11.4.13" "@formatjs/intl-relativetimeformat": "npm:11.4.12"
"@fullcalendar/core": "npm:6.1.19" "@fullcalendar/core": "npm:6.1.19"
"@fullcalendar/daygrid": "npm:6.1.19" "@fullcalendar/daygrid": "npm:6.1.19"
"@fullcalendar/interaction": "npm:6.1.19" "@fullcalendar/interaction": "npm:6.1.19"
@@ -9283,7 +9283,7 @@ __metadata:
browserslist-useragent-regexp: "npm:4.1.3" browserslist-useragent-regexp: "npm:4.1.3"
color-name: "npm:2.0.2" color-name: "npm:2.0.2"
comlink: "npm:4.4.2" comlink: "npm:4.4.2"
core-js: "npm:3.46.0" core-js: "npm:3.45.1"
cropperjs: "npm:1.6.2" cropperjs: "npm:1.6.2"
culori: "npm:4.0.2" culori: "npm:4.0.2"
date-fns: "npm:4.1.0" date-fns: "npm:4.1.0"
@@ -9317,7 +9317,7 @@ __metadata:
html-minifier-terser: "npm:7.2.0" html-minifier-terser: "npm:7.2.0"
husky: "npm:9.1.7" husky: "npm:9.1.7"
idb-keyval: "npm:6.2.2" idb-keyval: "npm:6.2.2"
intl-messageformat: "npm:10.7.18" intl-messageformat: "npm:10.7.17"
js-yaml: "npm:4.1.0" js-yaml: "npm:4.1.0"
jsdom: "npm:27.0.0" jsdom: "npm:27.0.0"
jszip: "npm:3.10.1" jszip: "npm:3.10.1"
@@ -9355,7 +9355,7 @@ __metadata:
ts-lit-plugin: "npm:2.0.2" ts-lit-plugin: "npm:2.0.2"
typescript: "npm:5.9.3" typescript: "npm:5.9.3"
typescript-eslint: "npm:8.46.0" typescript-eslint: "npm:8.46.0"
ua-parser-js: "npm:2.0.6" ua-parser-js: "npm:2.0.5"
vite-tsconfig-paths: "npm:5.1.4" vite-tsconfig-paths: "npm:5.1.4"
vitest: "npm:3.2.4" vitest: "npm:3.2.4"
vue: "npm:2.7.16" vue: "npm:2.7.16"
@@ -9712,15 +9712,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"intl-messageformat@npm:10.7.18": "intl-messageformat@npm:10.7.17":
version: 10.7.18 version: 10.7.17
resolution: "intl-messageformat@npm:10.7.18" resolution: "intl-messageformat@npm:10.7.17"
dependencies: dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6" "@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/fast-memoize": "npm:2.2.7" "@formatjs/fast-memoize": "npm:2.2.7"
"@formatjs/icu-messageformat-parser": "npm:2.11.4" "@formatjs/icu-messageformat-parser": "npm:2.11.3"
tslib: "npm:^2.8.0" tslib: "npm:^2.8.0"
checksum: 10/96650d673912763d21bbfa14b50749b992d45f1901092a020e3155961e3c70f4644dd1731c3ecb1207a1eb94d84bedf4c34b1ac8127c29ad6b015b6a2a4045cb checksum: 10/4f8c30c998bfc14eb64894414b94a8923045ab31d7bbf0978dab6621c644d451ff5c533c04ce8128163b74dd6d59061ec1ef3acb1cbab3302d31cbdb21947620
languageName: node languageName: node
linkType: hard linkType: hard
@@ -14379,16 +14379,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ua-parser-js@npm:2.0.6": "ua-parser-js@npm:2.0.5":
version: 2.0.6 version: 2.0.5
resolution: "ua-parser-js@npm:2.0.6" resolution: "ua-parser-js@npm:2.0.5"
dependencies: dependencies:
detect-europe-js: "npm:^0.1.2" detect-europe-js: "npm:^0.1.2"
is-standalone-pwa: "npm:^0.1.1" is-standalone-pwa: "npm:^0.1.1"
ua-is-frozen: "npm:^0.1.2" ua-is-frozen: "npm:^0.1.2"
undici: "npm:^7.12.0"
bin: bin:
ua-parser-js: script/cli.js ua-parser-js: script/cli.js
checksum: 10/b0049d3b272979049c7df6af2ec2ce032e4351316b10c33699f6e3f0bec701336f67530cc3ccb363c554b1bb5047b75d2f46575699afacd6e541762ca3861f4d checksum: 10/e946cb1c85bfcd0f2d30c7d5e1b605e340bb458432e7e87fc4aa1b2f90117e4220521d4e0bc7dd8c2a5cadd0935dedb5ac434b70efdc0007221288c1d98b3cd5
languageName: node languageName: node
linkType: hard linkType: hard
@@ -14451,6 +14452,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"undici@npm:^7.12.0":
version: 7.16.0
resolution: "undici@npm:7.16.0"
checksum: 10/2bb71672b23d3dc0f56f1b7fb6c936e4487a350db46eaafc03f2f9107f99cdf8e51ecdd32e589e2381ef47a64b6369cfb31f328b2c3ea663023aa47bc5258b9e
languageName: node
linkType: hard
"unicode-canonical-property-names-ecmascript@npm:^2.0.0": "unicode-canonical-property-names-ecmascript@npm:^2.0.0":
version: 2.0.1 version: 2.0.1
resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.1" resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.1"