Compare commits

..

11 Commits

Author SHA1 Message Date
Paul Bottein
a8f8d197f8 Add tooltip instead of title for 'add' button (#27399) 2025-10-07 17:48:49 +02:00
Paul Bottein
4fcac79047 Use right variable for content color in tooltip (#27400) 2025-10-07 15:46:40 +00:00
Paul Bottein
42ddacd41a Add plus and minus button for media player more info (#27398)
* Add plus and minus button for media player even if it support volume slider

* Update src/dialogs/more-info/controls/more-info-media_player.ts

Co-authored-by: Jan-Philipp Benecke <jan-philipp@bnck.me>

* Remove hardcoded support

---------

Co-authored-by: Jan-Philipp Benecke <jan-philipp@bnck.me>
2025-10-07 17:43:17 +02:00
dcapslock
ebc9981289 Fix hui-conditional-row causing varying row margins. (#26355)
* Fix hui-conditional-row causing varying row margins.

* Update row gap CSS var

* Apply suggestion from @bramkragten

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Apply suggestion from @bramkragten

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Apply suggestion from @bramkragten

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

* Use row-visibility-change method fired in hui-conditional-row

* Update to pass row in row-visibility-changed event

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-10-07 16:11:55 +03:00
Wendelin
23deab253b Add ellipsis to ha-button label (#27391) 2025-10-07 16:03:54 +03:00
Jan-Philipp Benecke
ab172abe02 Refactor overflow menu in backups data table to have a single instance (#27383)
* Refactor overflow menu in backups data table to have a single instance

* Fix
2025-10-07 08:39:11 +03:00
renovate[bot]
10d5d8b15d Update dependency eslint to v9.37.0 (#27390)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 06:58:32 +02:00
renovate[bot]
c9e472dab7 Update formatjs monorepo (#27389)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 06:58:18 +02:00
Wendelin
1e13b2b812 Fix android tap highlight border radius (#27382) 2025-10-06 20:32:42 +02:00
Jan-Philipp Benecke
e04a04632a Replace confirm dialogs with toast for delete actions in automation/script editor (#27324)
* Replace confirm dialogs with toast for delete actions in automation/script editor

* Migrate confirm dialog to toast in option row
2025-10-06 20:21:54 +02:00
Jan-Philipp Benecke
04bc5fba63 Refactor undo/redo to be a controller instead (#27279) 2025-10-06 16:04:42 +02:00
36 changed files with 887 additions and 1388 deletions

View File

@@ -37,15 +37,15 @@
"@codemirror/view": "6.38.4",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.18.0",
"@formatjs/intl-displaynames": "6.8.11",
"@formatjs/intl-durationformat": "0.7.4",
"@formatjs/intl-getcanonicallocales": "2.5.5",
"@formatjs/intl-listformat": "7.7.11",
"@formatjs/intl-locale": "4.2.11",
"@formatjs/intl-numberformat": "8.15.4",
"@formatjs/intl-pluralrules": "5.4.4",
"@formatjs/intl-relativetimeformat": "11.4.11",
"@formatjs/intl-datetimeformat": "6.18.1",
"@formatjs/intl-displaynames": "6.8.12",
"@formatjs/intl-durationformat": "0.7.5",
"@formatjs/intl-getcanonicallocales": "2.5.6",
"@formatjs/intl-listformat": "7.7.12",
"@formatjs/intl-locale": "4.2.12",
"@formatjs/intl-numberformat": "8.15.5",
"@formatjs/intl-pluralrules": "5.4.5",
"@formatjs/intl-relativetimeformat": "11.4.12",
"@fullcalendar/core": "6.1.19",
"@fullcalendar/daygrid": "6.1.19",
"@fullcalendar/interaction": "6.1.19",
@@ -114,7 +114,7 @@
"hls.js": "1.6.13",
"home-assistant-js-websocket": "9.5.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "10.7.16",
"intl-messageformat": "10.7.17",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@@ -183,7 +183,7 @@
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.1",
"eslint": "9.36.0",
"eslint": "9.37.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-webpack": "0.13.10",

View File

@@ -0,0 +1,141 @@
import type {
ReactiveController,
ReactiveControllerHost,
} from "@lit/reactive-element/reactive-controller";
const UNDO_REDO_STACK_LIMIT = 75;
/**
* Configuration options for the UndoRedoController.
*
* @template ConfigType The type of configuration to manage.
*/
export interface UndoRedoControllerConfig<ConfigType> {
stackLimit?: number;
currentConfig: () => ConfigType;
apply: (config: ConfigType) => void;
}
/**
* A controller to manage undo and redo operations for a given configuration type.
*
* @template ConfigType The type of configuration to manage.
*/
export class UndoRedoController<ConfigType> implements ReactiveController {
private _host: ReactiveControllerHost;
private _undoStack: ConfigType[] = [];
private _redoStack: ConfigType[] = [];
private readonly _stackLimit: number = UNDO_REDO_STACK_LIMIT;
private readonly _apply: (config: ConfigType) => void = () => {
throw new Error("No apply function provided");
};
private readonly _currentConfig: () => ConfigType = () => {
throw new Error("No currentConfig function provided");
};
constructor(
host: ReactiveControllerHost,
options: UndoRedoControllerConfig<ConfigType>
) {
if (options.stackLimit !== undefined) {
this._stackLimit = options.stackLimit;
}
this._apply = options.apply;
this._currentConfig = options.currentConfig;
this._host = host;
host.addController(this);
}
hostConnected() {
window.addEventListener("undo-change", this._onUndoChange);
}
hostDisconnected() {
window.removeEventListener("undo-change", this._onUndoChange);
}
private _onUndoChange = (ev: Event) => {
ev.stopPropagation();
this.undo();
this._host.requestUpdate();
};
/**
* Indicates whether there are actions available to undo.
*
* @returns `true` if there are actions to undo, `false` otherwise.
*/
public get canUndo(): boolean {
return this._undoStack.length > 0;
}
/**
* Indicates whether there are actions available to redo.
*
* @returns `true` if there are actions to redo, `false` otherwise.
*/
public get canRedo(): boolean {
return this._redoStack.length > 0;
}
/**
* Commits the current configuration to the undo stack and clears the redo stack.
*
* @param config The current configuration to commit.
*/
public commit(config: ConfigType) {
if (this._undoStack.length >= this._stackLimit) {
this._undoStack.shift();
}
this._undoStack.push({ ...config });
this._redoStack = [];
}
/**
* Undoes the last action and applies the previous configuration
* while saving the current configuration to the redo stack.
*/
public undo() {
if (this._undoStack.length === 0) {
return;
}
this._redoStack.push({ ...this._currentConfig() });
const config = this._undoStack.pop()!;
this._apply(config);
this._host.requestUpdate();
}
/**
* Redoes the last undone action and reapplies the configuration
* while saving the current configuration to the undo stack.
*/
public redo() {
if (this._redoStack.length === 0) {
return;
}
this._undoStack.push({ ...this._currentConfig() });
const config = this._redoStack.pop()!;
this._apply(config);
this._host.requestUpdate();
}
/**
* Resets the undo and redo stacks, clearing all history.
*/
public reset() {
this._undoStack = [];
this._redoStack = [];
}
}
declare global {
interface HASSDomEvents {
"undo-change": undefined;
}
}

View File

@@ -41,8 +41,7 @@ export class HaButton extends Button {
return [
Button.styles,
css`
.button {
/* set theme vars */
:host {
--wa-form-control-padding-inline: 16px;
--wa-font-weight-action: var(--ha-font-weight-medium);
--wa-form-control-border-radius: var(
@@ -54,7 +53,8 @@ export class HaButton extends Button {
--ha-button-height,
var(--button-height, 40px)
);
}
.button {
font-size: var(--ha-font-size-m);
line-height: 1;
@@ -223,6 +223,12 @@ export class HaButton extends Button {
.button.has-end {
padding-inline-end: 8px;
}
.label {
overflow: hidden;
text-overflow: ellipsis;
padding: var(--ha-space-1) 0;
}
`,
];
}

View File

@@ -94,6 +94,9 @@ export class HaServiceControl extends LitElement {
@property({ attribute: "hide-picker", type: Boolean, reflect: true })
public hidePicker = false;
@property({ attribute: "hide-description", type: Boolean })
public hideDescription = false;
@state() private _value!: this["value"];
@state() private _checkedKeys = new Set();
@@ -469,135 +472,136 @@ export class HaServiceControl extends LitElement {
serviceData?.description;
return html`${this.hidePicker
? nothing
: html`<ha-service-picker
.hass=${this.hass}
.value=${this._value?.action}
.disabled=${this.disabled}
@value-changed=${this._serviceChanged}
.showServiceId=${this.showServiceId}
></ha-service-picker>`}
<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>
${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
? nothing
: html`<ha-service-picker
.hass=${this.hass}
.value=${this._value?.action}
.disabled=${this.disabled}
@value-changed=${this._serviceChanged}
.showServiceId=${this.showServiceId}
></ha-service-picker>`}
${this.hideDescription
? nothing
: 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-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;
})} `;
<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
)}
>
<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(

View File

@@ -17,7 +17,7 @@ export class HaTooltip extends Tooltip {
css`
:host {
--wa-tooltip-background-color: var(--secondary-background-color);
--wa-tooltip-color: var(--primary-text-color);
--wa-tooltip-content-color: var(--primary-text-color);
--wa-tooltip-font-family: var(
--ha-tooltip-font-family,
var(--ha-font-family-body)

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

View File

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

View File

@@ -131,19 +131,14 @@ const resources: {
all?: Promise<Record<string, ServiceIcons>>;
domains: Record<string, ServiceIcons | Promise<ServiceIcons>>;
};
triggers: {
all?: Promise<Record<string, TriggerIcons>>;
domains: Record<string, TriggerIcons | Promise<TriggerIcons>>;
};
} = {
entity: {},
entity_component: {},
services: { domains: {} },
triggers: { domains: {} },
};
interface IconResources<
T extends ComponentIcons | PlatformIcons | ServiceIcons | TriggerIcons,
T extends ComponentIcons | PlatformIcons | ServiceIcons,
> {
resources: Record<string, T>;
}
@@ -187,22 +182,12 @@ type ServiceIcons = Record<
{ service: string; sections?: Record<string, string> }
>;
type TriggerIcons = Record<
string,
{ trigger: string; sections?: Record<string, string> }
>;
export type IconCategory =
| "entity"
| "entity_component"
| "services"
| "triggers";
export type IconCategory = "entity" | "entity_component" | "services";
interface CategoryType {
entity: PlatformIcons;
entity_component: ComponentIcons;
services: ServiceIcons;
triggers: TriggerIcons;
}
export const getHassIcons = async <T extends IconCategory>(
@@ -280,10 +265,12 @@ export const getServiceIcons = async (
if (!force && resources.services.all) {
return resources.services.all;
}
resources.services.all = getHassIcons(hass, "services").then((res) => {
resources.services.domains = res.resources;
return res?.resources;
});
resources.services.all = getHassIcons(hass, "services", domain).then(
(res) => {
resources.services.domains = res.resources;
return res?.resources;
}
);
return resources.services.all;
}
if (!force && domain in resources.services.domains) {
@@ -305,40 +292,6 @@ export const getServiceIcons = async (
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
const sortedRangeCache = new WeakMap<Record<string, string>, number[]>();
@@ -518,26 +471,6 @@ export const attributeIcon = async (
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 (
hass: HomeAssistant,
service: string

View File

@@ -73,8 +73,7 @@ export type TranslationCategory =
| "application_credentials"
| "issues"
| "selector"
| "services"
| "triggers";
| "services";
export const subscribeTranslationPreferences = (
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 {
AutomationElementGroup,
Trigger,
TriggerList,
} 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 = {
device: {},
@@ -33,26 +74,3 @@ export const TRIGGER_GROUPS: AutomationElementGroup = {
export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
"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

@@ -77,80 +77,84 @@ class MoreInfoMediaPlayer extends LitElement {
return nothing;
}
if (!stateActive(this.stateObj)) {
return nothing;
}
const supportsMute = supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.VOLUME_MUTE
);
const supportsSliding = supportsFeature(
const supportsSet = supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.VOLUME_SET
);
return html`${(supportsFeature(
this.stateObj!,
MediaPlayerEntityFeature.VOLUME_SET
) ||
supportsFeature(this.stateObj!, MediaPlayerEntityFeature.VOLUME_STEP)) &&
stateActive(this.stateObj!)
? html`
<div class="volume">
${supportsMute
? html`
<ha-icon-button
.path=${this.stateObj.attributes.is_volume_muted
? mdiVolumeOff
: mdiVolumeHigh}
.label=${this.hass.localize(
`ui.card.media_player.${
this.stateObj.attributes.is_volume_muted
? "media_volume_unmute"
: "media_volume_mute"
}`
)}
@click=${this._toggleMute}
></ha-icon-button>
`
: ""}
${supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.VOLUME_STEP
) && !supportsSliding
? html`
<ha-icon-button
action="volume_down"
.path=${mdiVolumeMinus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_down"
)}
@click=${this._handleClick}
></ha-icon-button>
<ha-icon-button
action="volume_up"
.path=${mdiVolumePlus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_up"
)}
@click=${this._handleClick}
></ha-icon-button>
`
: nothing}
${supportsSliding
? html`
${!supportsMute
? html`<ha-svg-icon .path=${mdiVolumeHigh}></ha-svg-icon>`
: nothing}
<ha-slider
labeled
id="input"
.value=${Number(this.stateObj.attributes.volume_level) *
100}
@change=${this._selectedValueChanged}
></ha-slider>
`
: nothing}
</div>
`
: nothing}`;
const supportsStep = supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.VOLUME_STEP
);
if (!supportsMute && !supportsSet && !supportsStep) {
return nothing;
}
return html`
<div class="volume">
${supportsMute
? html`
<ha-icon-button
.path=${this.stateObj.attributes.is_volume_muted
? mdiVolumeOff
: mdiVolumeHigh}
.label=${this.hass.localize(
`ui.card.media_player.${
this.stateObj.attributes.is_volume_muted
? "media_volume_unmute"
: "media_volume_mute"
}`
)}
@click=${this._toggleMute}
></ha-icon-button>
`
: nothing}
${supportsStep
? html` <ha-icon-button
action="volume_down"
.path=${mdiVolumeMinus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_down"
)}
@click=${this._handleClick}
></ha-icon-button>`
: nothing}
${supportsSet
? html`
${!supportsMute && !supportsStep
? html`<ha-svg-icon .path=${mdiVolumeHigh}></ha-svg-icon>`
: nothing}
<ha-slider
labeled
id="input"
.value=${Number(this.stateObj.attributes.volume_level) * 100}
@change=${this._selectedValueChanged}
></ha-slider>
`
: nothing}
${supportsStep
? html`
<ha-icon-button
action="volume_up"
.path=${mdiVolumePlus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_up"
)}
@click=${this._handleClick}
></ha-icon-button>
`
: nothing}
</div>
`;
}
protected _renderSourceControl() {

View File

@@ -1,60 +0,0 @@
import type { LitElement } from "lit";
import type { Constructor } from "../types";
export const UndoRedoMixin = <T extends Constructor<LitElement>, ConfigType>(
superClass: T
) => {
class UndoRedoClass extends superClass {
private _undoStack: ConfigType[] = [];
private _redoStack: ConfigType[] = [];
protected _undoStackLimit = 75;
protected pushToUndo(config: ConfigType) {
if (this._undoStack.length >= this._undoStackLimit) {
this._undoStack.shift();
}
this._undoStack.push({ ...config });
this._redoStack = [];
}
public undo() {
const currentConfig = this.currentConfig;
if (this._undoStack.length === 0 || !currentConfig) {
return;
}
this._redoStack.push({ ...currentConfig });
const config = this._undoStack.pop()!;
this.applyUndoRedo(config);
}
public redo() {
const currentConfig = this.currentConfig;
if (this._redoStack.length === 0 || !currentConfig) {
return;
}
this._undoStack.push({ ...currentConfig });
const config = this._redoStack.pop()!;
this.applyUndoRedo(config);
}
public get canUndo(): boolean {
return this._undoStack.length > 0;
}
public get canRedo(): boolean {
return this._redoStack.length > 0;
}
protected get currentConfig(): ConfigType | undefined {
return undefined;
}
protected applyUndoRedo(_: ConfigType) {
throw new Error("applyUndoRedo not implemented");
}
}
return UndoRedoClass;
};

View File

@@ -70,7 +70,6 @@ import { describeAction } from "../../../../data/script_i18n";
import { callExecuteScript } from "../../../../data/service";
import {
showAlertDialog,
showConfirmationDialog,
showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../types";
@@ -650,21 +649,19 @@ export default class HaAutomationActionRow extends LitElement {
};
private _onDelete = () => {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.actions.delete_confirm_title"
),
text: this.hass.localize(
"ui.panel.config.automation.editor.actions.delete_confirm_text"
),
dismissText: this.hass.localize("ui.common.cancel"),
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: () => {
fireEvent(this, "value-changed", { value: null });
if (this._selected) {
fireEvent(this, "close-sidebar");
}
fireEvent(this, "value-changed", { value: null });
if (this._selected) {
fireEvent(this, "close-sidebar");
}
showToast(this, {
message: this.hass.localize("ui.common.successfully_deleted"),
duration: 4000,
action: {
text: this.hass.localize("ui.common.undo"),
action: () => {
fireEvent(window, "undo-change");
},
},
});
};

View File

@@ -44,7 +44,7 @@ import {
domainToName,
fetchIntegrationManifests,
} 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 { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { HaFuse } from "../../../resources/fuse";
@@ -54,7 +54,6 @@ import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import type { AddAutomationElementDialogParams } 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 = {
trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS },

View File

@@ -53,7 +53,6 @@ import { fullEntitiesContext } from "../../../../data/context";
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
import {
showAlertDialog,
showConfirmationDialog,
showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../types";
@@ -532,21 +531,19 @@ export default class HaAutomationConditionRow extends LitElement {
};
private _onDelete = () => {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.conditions.delete_confirm_title"
),
text: this.hass.localize(
"ui.panel.config.automation.editor.conditions.delete_confirm_text"
),
dismissText: this.hass.localize("ui.common.cancel"),
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: () => {
fireEvent(this, "value-changed", { value: null });
if (this._selected) {
fireEvent(this, "close-sidebar");
}
fireEvent(this, "value-changed", { value: null });
if (this._selected) {
fireEvent(this, "close-sidebar");
}
showToast(this, {
message: this.hass.localize("ui.common.successfully_deleted"),
duration: 4000,
action: {
text: this.hass.localize("ui.common.undo"),
action: () => {
fireEvent(window, "undo-change");
},
},
});
};

View File

@@ -74,7 +74,7 @@ import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info
import "../../../layouts/hass-subpage";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
import { UndoRedoMixin } from "../../../mixins/undo-redo-mixin";
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
import { haStyle } from "../../../resources/styles";
import type { Entries, HomeAssistant, Route } from "../../../types";
import { isMac } from "../../../util/is_mac";
@@ -111,12 +111,9 @@ declare global {
}
}
const baseEditorMixins = PreventUnsavedMixin(KeyboardShortcutMixin(LitElement));
export class HaAutomationEditor extends UndoRedoMixin<
typeof baseEditorMixins,
AutomationConfig
>(baseEditorMixins) {
export class HaAutomationEditor extends PreventUnsavedMixin(
KeyboardShortcutMixin(LitElement)
) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public automationId: string | null = null;
@@ -183,6 +180,11 @@ export class HaAutomationEditor extends UndoRedoMixin<
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
) => void;
private _undoRedoController = new UndoRedoController<AutomationConfig>(this, {
apply: (config) => this._applyUndoRedo(config),
currentConfig: () => this._config!,
});
protected willUpdate(changedProps) {
super.willUpdate(changedProps);
@@ -235,8 +237,8 @@ export class HaAutomationEditor extends UndoRedoMixin<
slot="toolbar-icon"
.label=${this.hass.localize("ui.common.undo")}
.path=${mdiUndo}
@click=${this.undo}
.disabled=${!this.canUndo}
@click=${this._undo}
.disabled=${!this._undoRedoController.canUndo}
id="button-undo"
>
</ha-icon-button>
@@ -253,8 +255,8 @@ export class HaAutomationEditor extends UndoRedoMixin<
slot="toolbar-icon"
.label=${this.hass.localize("ui.common.redo")}
.path=${mdiRedo}
@click=${this.redo}
.disabled=${!this.canRedo}
@click=${this._redo}
.disabled=${!this._undoRedoController.canRedo}
id="button-redo"
>
</ha-icon-button>
@@ -298,16 +300,16 @@ export class HaAutomationEditor extends UndoRedoMixin<
${this._mode === "gui" && this.narrow
? html`<ha-list-item
graphic="icon"
@click=${this.undo}
.disabled=${!this.canUndo}
@click=${this._undo}
.disabled=${!this._undoRedoController.canUndo}
>
${this.hass.localize("ui.common.undo")}
<ha-svg-icon slot="graphic" .path=${mdiUndo}></ha-svg-icon>
</ha-list-item>
<ha-list-item
graphic="icon"
@click=${this.redo}
.disabled=${!this.canRedo}
@click=${this._redo}
.disabled=${!this._undoRedoController.canRedo}
>
${this.hass.localize("ui.common.redo")}
<ha-svg-icon slot="graphic" .path=${mdiRedo}></ha-svg-icon>
@@ -518,7 +520,6 @@ export class HaAutomationEditor extends UndoRedoMixin<
@value-changed=${this._valueChanged}
@save-automation=${this._handleSaveAutomation}
@editor-save=${this._handleSaveAutomation}
@undo-paste=${this.undo}
>
<div class="alert-wrapper" slot="alerts">
${this._errors || stateObj?.state === UNAVAILABLE
@@ -791,7 +792,7 @@ export class HaAutomationEditor extends UndoRedoMixin<
ev.stopPropagation();
if (this._config) {
this.pushToUndo(this._config);
this._undoRedoController.commit(this._config);
}
this._config = ev.detail.value;
@@ -1202,9 +1203,9 @@ export class HaAutomationEditor extends UndoRedoMixin<
x: () => this._cutSelectedRow(),
Delete: () => this._deleteSelectedRow(),
Backspace: () => this._deleteSelectedRow(),
z: () => this.undo(),
Z: () => this.redo(),
y: () => this.redo(),
z: () => this._undo(),
Z: () => this._redo(),
y: () => this._redo(),
};
}
@@ -1238,16 +1239,20 @@ export class HaAutomationEditor extends UndoRedoMixin<
this._manualEditor?.deleteSelectedRow();
}
protected get currentConfig() {
return this._config;
}
protected applyUndoRedo(config: AutomationConfig) {
private _applyUndoRedo(config: AutomationConfig) {
this._manualEditor?.triggerCloseSidebar();
this._config = config;
this._dirty = true;
}
private _undo() {
this._undoRedoController.undo();
}
private _redo() {
this._undoRedoController.redo();
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@@ -616,7 +616,7 @@ export class HaManualAutomationEditor extends LitElement {
action: {
text: this.hass.localize("ui.common.undo"),
action: () => {
fireEvent(this, "undo-paste");
fireEvent(this, "undo-change");
this._pastedConfig = undefined;
},
@@ -742,6 +742,5 @@ declare global {
"open-sidebar": SidebarConfig;
"request-close-sidebar": undefined;
"close-sidebar": undefined;
"undo-paste": undefined;
}
}

View File

@@ -33,10 +33,7 @@ import { describeCondition } from "../../../../data/automation_i18n";
import { fullEntitiesContext } from "../../../../data/context";
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
import type { Action, Option } from "../../../../data/script";
import {
showConfirmationDialog,
showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import { showPromptDialog } from "../../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import "../action/ha-automation-action";
@@ -49,6 +46,7 @@ import {
overflowStyles,
rowStyles,
} from "../styles";
import { showToast } from "../../../../util/toast";
@customElement("ha-automation-option-row")
export default class HaAutomationOptionRow extends LitElement {
@@ -365,23 +363,21 @@ export default class HaAutomationOptionRow extends LitElement {
private _removeOption = () => {
if (this.option) {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.delete_confirm_title"
),
text: this.hass.localize(
"ui.panel.config.automation.editor.actions.delete_confirm_text"
),
dismissText: this.hass.localize("ui.common.cancel"),
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: () => {
fireEvent(this, "value-changed", {
value: null,
});
if (this._selected) {
fireEvent(this, "close-sidebar");
}
fireEvent(this, "value-changed", {
value: null,
});
if (this._selected) {
fireEvent(this, "close-sidebar");
}
showToast(this, {
message: this.hass.localize("ui.common.successfully_deleted"),
duration: 4000,
action: {
text: this.hass.localize("ui.common.undo"),
action: () => {
fireEvent(window, "undo-change");
},
},
});
}

View File

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

View File

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

View File

@@ -22,8 +22,6 @@ import { overflowStyles, sidebarEditorStyles } from "../styles";
import "../trigger/ha-automation-trigger-editor";
import type HaAutomationTriggerEditor from "../trigger/ha-automation-trigger-editor";
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")
export default class HaAutomationSidebarTrigger extends LitElement {
@@ -70,22 +68,9 @@ export default class HaAutomationSidebarTrigger extends LitElement {
"ui.panel.config.automation.editor.triggers.trigger"
);
const domain =
"trigger" in this.config.config &&
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`);
const title = this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.${type}.label`
);
return html`
<ha-automation-sidebar-card
@@ -261,7 +246,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
class="sidebar-editor"
.hass=${this.hass}
.trigger=${this.config.config}
.description=${this.config.description}
@value-changed=${this._valueChangedSidebar}
@yaml-changed=${this._yamlChangedSidebar}
.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 { Trigger } from "../../../../data/automation";
import { migrateAutomationTrigger } from "../../../../data/automation";
import type { TriggerDescription } from "../../../../data/trigger";
import { isTriggerList } from "../../../../data/trigger";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "../ha-automation-editor-warning";
import "./types/ha-automation-trigger-platform";
@customElement("ha-automation-trigger-editor")
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({ attribute: false }) public description?: TriggerDescription;
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
protected render() {
@@ -92,18 +88,11 @@ export default class HaAutomationTriggerEditor extends LitElement {
`
: nothing}
<div @value-changed=${this._onUiChanged}>
${this.description
? html`<ha-automation-trigger-platform
.hass=${this.hass}
.trigger=${this.trigger}
.description=${this.description}
.disabled=${this.disabled}
></ha-automation-trigger-platform>`
: dynamicElement(`ha-automation-trigger-${type}`, {
hass: this.hass,
trigger: this.trigger,
disabled: this.disabled,
})}
${dynamicElement(`ha-automation-trigger-${type}`, {
hass: this.hass,
trigger: this.trigger,
disabled: this.disabled,
})}
</div>
`}
</div>

View File

@@ -40,11 +40,9 @@ import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-svg-icon";
import { TRIGGER_ICONS } from "../../../../components/ha-trigger-icon";
import type {
AutomationClipboard,
Trigger,
TriggerList,
TriggerSidebarConfig,
} from "../../../../data/automation";
import { isTrigger, subscribeTrigger } from "../../../../data/automation";
@@ -52,11 +50,9 @@ import { describeTrigger } from "../../../../data/automation_i18n";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
import type { TriggerDescriptions } from "../../../../data/trigger";
import { isTriggerList } from "../../../../data/trigger";
import { TRIGGER_ICONS, isTriggerList } from "../../../../data/trigger";
import {
showAlertDialog,
showConfirmationDialog,
showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../types";
@@ -76,7 +72,6 @@ import "./types/ha-automation-trigger-list";
import "./types/ha-automation-trigger-mqtt";
import "./types/ha-automation-trigger-numeric_state";
import "./types/ha-automation-trigger-persistent_notification";
import "./types/ha-automation-trigger-platform";
import "./types/ha-automation-trigger-state";
import "./types/ha-automation-trigger-sun";
import "./types/ha-automation-trigger-tag";
@@ -142,9 +137,6 @@ export default class HaAutomationTriggerRow extends LitElement {
@state() private _warnings?: string[];
@property({ attribute: false })
public triggerDescriptions: TriggerDescriptions = {};
@property({ type: Boolean }) public narrow = false;
@query("ha-automation-trigger-editor")
@@ -186,24 +178,18 @@ export default class HaAutomationTriggerRow extends LitElement {
}
private _renderRow() {
const type = this._getType(this.trigger, this.triggerDescriptions);
const type = this._getType(this.trigger);
const supported = this._uiSupported(type);
const yamlMode = this._yamlMode || !supported;
return html`
${type === "list"
? html`<ha-svg-icon
slot="leading-icon"
class="trigger-icon"
.path=${TRIGGER_ICONS[type]}
></ha-svg-icon>`
: html`<ha-trigger-icon
slot="leading-icon"
.hass=${this.hass}
.trigger=${(this.trigger as Exclude<Trigger, TriggerList>).trigger}
></ha-trigger-icon>`}
<ha-svg-icon
slot="leading-icon"
class="trigger-icon"
.path=${TRIGGER_ICONS[type]}
></ha-svg-icon>
<h3 slot="header">
${describeTrigger(this.trigger, this.hass, this._entityReg)}
</h3>
@@ -407,9 +393,6 @@ export default class HaAutomationTriggerRow extends LitElement {
<ha-automation-trigger-editor
.hass=${this.hass}
.trigger=${this.trigger}
.description=${"trigger" in this.trigger
? this.triggerDescriptions[this.trigger.trigger]
: undefined}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
.uiSupported=${supported}
@@ -569,7 +552,6 @@ export default class HaAutomationTriggerRow extends LitElement {
}
public openSidebar(trigger?: Trigger): void {
trigger = trigger || this.trigger;
fireEvent(this, "open-sidebar", {
save: (value) => {
fireEvent(this, "value-changed", { value });
@@ -594,14 +576,8 @@ export default class HaAutomationTriggerRow extends LitElement {
duplicate: this._duplicateTrigger,
cut: this._cutTrigger,
insertAfter: this._insertAfter,
config: trigger,
uiSupported: this._uiSupported(
this._getType(trigger, this.triggerDescriptions)
),
description:
"trigger" in trigger
? this.triggerDescriptions[trigger.trigger]
: undefined,
config: trigger || this.trigger,
uiSupported: this._uiSupported(this._getType(trigger || this.trigger)),
yamlMode: this._yamlMode,
} satisfies TriggerSidebarConfig);
this._selected = true;
@@ -626,22 +602,20 @@ export default class HaAutomationTriggerRow extends LitElement {
}
private _onDelete = () => {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.triggers.delete_confirm_title"
),
text: this.hass.localize(
"ui.panel.config.automation.editor.triggers.delete_confirm_text"
),
dismissText: this.hass.localize("ui.common.cancel"),
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: () => {
fireEvent(this, "value-changed", { value: null });
fireEvent(this, "value-changed", { value: null });
if (this._selected) {
fireEvent(this, "close-sidebar");
}
if (this._selected) {
fireEvent(this, "close-sidebar");
}
showToast(this, {
message: this.hass.localize("ui.common.successfully_deleted"),
duration: 4000,
action: {
text: this.hass.localize("ui.common.undo"),
action: () => {
fireEvent(window, "undo-change");
},
},
});
};
@@ -785,18 +759,8 @@ export default class HaAutomationTriggerRow extends LitElement {
});
}
private _getType = memoizeOne(
(trigger: Trigger, triggerDescriptions: TriggerDescriptions) => {
if (isTriggerList(trigger)) {
return "list";
}
if (trigger.trigger in triggerDescriptions) {
return "platform";
}
return trigger.trigger;
}
private _getType = memoizeOne((trigger: Trigger) =>
isTriggerList(trigger) ? "list" : trigger.trigger
);
private _uiSupported = memoizeOne(

View File

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

@@ -71,6 +71,9 @@ import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup"
import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup";
import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup";
import { downloadBackup } from "./helper/download_backup";
import type { HaMdMenu } from "../../../components/ha-md-menu";
import "../../../components/ha-md-menu";
import "../../../components/ha-md-menu-item";
interface BackupRow extends DataTableRowData, BackupContent {
formatted_type: string;
@@ -120,6 +123,10 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
@query("#overflow-menu") private _overflowMenu?: HaMdMenu;
private _overflowBackup?: BackupContent;
public connectedCallback() {
super.connectedCallback();
window.addEventListener("location-changed", this._locationChanged);
@@ -254,24 +261,12 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
hideable: false,
type: "overflow-menu",
template: (backup) => html`
<ha-icon-overflow-menu
.hass=${this.hass}
narrow
.items=${[
{
label: this.hass.localize("ui.common.download"),
path: mdiDownload,
action: () => this._downloadBackup(backup),
},
{
label: this.hass.localize("ui.common.delete"),
path: mdiDelete,
action: () => this._deleteBackup(backup),
warning: true,
},
]}
>
</ha-icon-overflow-menu>
<ha-icon-button
.selected=${backup}
.label=${this.hass.localize("ui.common.overflow_menu")}
.path=${mdiDotsVertical}
@click=${this._toggleOverflowMenu}
></ha-icon-button>
`,
},
})
@@ -290,6 +285,20 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
: undefined
);
private _toggleOverflowMenu = (ev) => {
if (!this._overflowMenu) {
return;
}
if (this._overflowMenu.open) {
this._overflowMenu.close();
return;
}
this._overflowBackup = ev.target.selected;
this._overflowMenu.anchorElement = ev.target;
this._overflowMenu.show();
};
private _handleGroupingChanged(ev: CustomEvent) {
this._activeGrouping = ev.detail.value;
}
@@ -366,14 +375,16 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
clickable
id="backup_id"
has-filters
.filters=${Object.values(this._filters).filter((filter) =>
Array.isArray(filter)
? filter.length
: filter &&
Object.values(filter).some((val) =>
Array.isArray(val) ? val.length : val
)
).length}
.filters=${
Object.values(this._filters).filter((filter) =>
Array.isArray(filter)
? filter.length
: filter &&
Object.values(filter).some((val) =>
Array.isArray(val) ? val.length : val
)
).length
}
selectable
.selected=${this._selected.length}
.initialGroupColumn=${this._activeGrouping}
@@ -415,28 +426,30 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
</div>
<div slot="selection-bar">
${!this.narrow
? html`
<ha-button
appearance="plain"
@click=${this._deleteSelected}
variant="danger"
>
${this.hass.localize(
"ui.panel.config.backup.backups.delete_selected"
)}
</ha-button>
`
: html`
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.backup.backups.delete_selected"
)}
.path=${mdiDelete}
class="warning"
@click=${this._deleteSelected}
></ha-icon-button>
`}
${
!this.narrow
? html`
<ha-button
appearance="plain"
@click=${this._deleteSelected}
variant="danger"
>
${this.hass.localize(
"ui.panel.config.backup.backups.delete_selected"
)}
</ha-button>
`
: html`
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.backup.backups.delete_selected"
)}
.path=${mdiDelete}
class="warning"
@click=${this._deleteSelected}
></ha-icon-button>
`
}
</div>
<ha-filter-states
@@ -449,29 +462,43 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
expanded
.narrow=${this.narrow}
></ha-filter-states>
${!this._needsOnboarding
? html`
<ha-fab
slot="fab"
?disabled=${backupInProgress}
.label=${this.hass.localize(
"ui.panel.config.backup.backups.new_backup"
)}
extended
@click=${this._newBackup}
>
${backupInProgress
? html`<div slot="icon" class="loading">
<ha-spinner .size=${"small"}></ha-spinner>
</div>`
: html`<ha-svg-icon
slot="icon"
.path=${mdiPlus}
></ha-svg-icon>`}
</ha-fab>
`
: nothing}
${
!this._needsOnboarding
? html`
<ha-fab
slot="fab"
?disabled=${backupInProgress}
.label=${this.hass.localize(
"ui.panel.config.backup.backups.new_backup"
)}
extended
@click=${this._newBackup}
>
${backupInProgress
? html`<div slot="icon" class="loading">
<ha-spinner .size=${"small"}></ha-spinner>
</div>`
: html`<ha-svg-icon
slot="icon"
.path=${mdiPlus}
></ha-svg-icon>`}
</ha-fab>
`
: nothing
}
</hass-tabs-subpage-data-table>
<ha-md-menu id="overflow-menu" positioning="fixed">
<ha-md-menu-item .clickAction=${this._downloadBackup}>
<ha-svg-icon slot="start" .path=${mdiDownload}></ha-svg-icon>
${this.hass.localize("ui.common.download")}
</ha-md-menu-item>
<ha-md-menu-item class="warning" .clickAction=${this._deleteBackup}>
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
${this.hass.localize("ui.common.delete")}
</ha-md-menu-item>
</ha-md-menu>
>
</ha-icon-overflow-menu>
`;
}
@@ -545,11 +572,18 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
navigate(`/config/backup/details/${id}`);
}
private async _downloadBackup(backup: BackupContent): Promise<void> {
downloadBackup(this.hass, this, backup, this.config);
private async _downloadBackup(): Promise<void> {
if (!this._overflowBackup) {
return;
}
downloadBackup(this.hass, this, this._overflowBackup, this.config);
}
private async _deleteBackup(backup: BackupContent): Promise<void> {
private async _deleteBackup(): Promise<void> {
if (!this._overflowBackup) {
return;
}
const confirm = await showConfirmationDialog(this, {
title: this.hass.localize("ui.panel.config.backup.dialogs.delete.title"),
text: this.hass.localize("ui.panel.config.backup.dialogs.delete.text"),
@@ -562,9 +596,11 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
}
try {
await deleteBackup(this.hass, backup.backup_id);
if (this._selected.includes(backup.backup_id)) {
this._selected = this._selected.filter((id) => id !== backup.backup_id);
await deleteBackup(this.hass, this._overflowBackup.backup_id);
if (this._selected.includes(this._overflowBackup.backup_id)) {
this._selected = this._selected.filter(
(id) => id !== this._overflowBackup!.backup_id
);
}
} catch (err: any) {
showAlertDialog(this, {

View File

@@ -770,43 +770,39 @@ export class HaConfigDevicePage extends LitElement {
${firstDeviceAction || actions.length
? html`
<div class="card-actions" slot="actions">
<div>
<ha-button
href=${ifDefined(firstDeviceAction!.href)}
rel=${ifDefined(
firstDeviceAction!.target ? "noreferrer" : undefined
)}
appearance="plain"
target=${ifDefined(firstDeviceAction!.target)}
class=${ifDefined(firstDeviceAction!.classes)}
.variant=${firstDeviceAction!.classes?.includes(
"warning"
)
? "danger"
: "brand"}
.action=${firstDeviceAction!.action}
@click=${this._deviceActionClicked}
>
${firstDeviceAction!.label}
${firstDeviceAction!.icon
? html`
<ha-svg-icon
class=${ifDefined(firstDeviceAction!.classes)}
.path=${firstDeviceAction!.icon}
slot="start"
></ha-svg-icon>
`
: nothing}
${firstDeviceAction!.trailingIcon
? html`
<ha-svg-icon
.path=${firstDeviceAction!.trailingIcon}
slot="end"
></ha-svg-icon>
`
: nothing}
</ha-button>
</div>
<ha-button
href=${ifDefined(firstDeviceAction!.href)}
rel=${ifDefined(
firstDeviceAction!.target ? "noreferrer" : undefined
)}
appearance="plain"
target=${ifDefined(firstDeviceAction!.target)}
class=${ifDefined(firstDeviceAction!.classes)}
.variant=${firstDeviceAction!.classes?.includes("warning")
? "danger"
: "brand"}
.action=${firstDeviceAction!.action}
@click=${this._deviceActionClicked}
>
${firstDeviceAction!.label}
${firstDeviceAction!.icon
? html`
<ha-svg-icon
class=${ifDefined(firstDeviceAction!.classes)}
.path=${firstDeviceAction!.icon}
slot="start"
></ha-svg-icon>
`
: nothing}
${firstDeviceAction!.trailingIcon
? html`
<ha-svg-icon
.path=${firstDeviceAction!.trailingIcon}
slot="end"
></ha-svg-icon>
`
: nothing}
</ha-button>
${actions.length
? html`

View File

@@ -65,7 +65,7 @@ import "../../../layouts/hass-subpage";
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { UndoRedoMixin } from "../../../mixins/undo-redo-mixin";
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
import { haStyle } from "../../../resources/styles";
import type { Entries, HomeAssistant, Route } from "../../../types";
import { isMac } from "../../../util/is_mac";
@@ -78,14 +78,9 @@ import "./blueprint-script-editor";
import "./manual-script-editor";
import type { HaManualScriptEditor } from "./manual-script-editor";
const baseEditorMixins = SubscribeMixin(
export class HaScriptEditor extends SubscribeMixin(
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
);
export class HaScriptEditor extends UndoRedoMixin<
typeof baseEditorMixins,
ScriptConfig
>(baseEditorMixins) {
) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public scriptId: string | null = null;
@@ -141,6 +136,11 @@ export class HaScriptEditor extends UndoRedoMixin<
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
) => void;
private _undoRedoController = new UndoRedoController<ScriptConfig>(this, {
apply: (config) => this._applyUndoRedo(config),
currentConfig: () => this._config!,
});
protected willUpdate(changedProps) {
super.willUpdate(changedProps);
@@ -188,8 +188,8 @@ export class HaScriptEditor extends UndoRedoMixin<
slot="toolbar-icon"
.label=${this.hass.localize("ui.common.undo")}
.path=${mdiUndo}
@click=${this.undo}
.disabled=${!this.canUndo}
@click=${this._undo}
.disabled=${!this._undoRedoController.canUndo}
id="button-undo"
>
</ha-icon-button>
@@ -205,8 +205,8 @@ export class HaScriptEditor extends UndoRedoMixin<
slot="toolbar-icon"
.label=${this.hass.localize("ui.common.redo")}
.path=${mdiRedo}
@click=${this.redo}
.disabled=${!this.canRedo}
@click=${this._redo}
.disabled=${!this._undoRedoController.canRedo}
id="button-redo"
>
</ha-icon-button>
@@ -249,16 +249,16 @@ export class HaScriptEditor extends UndoRedoMixin<
${this._mode === "gui" && this.narrow
? html`<ha-list-item
graphic="icon"
@click=${this.undo}
.disabled=${!this.canUndo}
@click=${this._undo}
.disabled=${!this._undoRedoController.canUndo}
>
${this.hass.localize("ui.common.undo")}
<ha-svg-icon slot="graphic" .path=${mdiUndo}></ha-svg-icon>
</ha-list-item>
<ha-list-item
graphic="icon"
@click=${this.redo}
.disabled=${!this.canRedo}
@click=${this._redo}
.disabled=${!this._undoRedoController.canRedo}
>
${this.hass.localize("ui.common.redo")}
<ha-svg-icon slot="graphic" .path=${mdiRedo}></ha-svg-icon>
@@ -463,7 +463,6 @@ export class HaScriptEditor extends UndoRedoMixin<
@value-changed=${this._valueChanged}
@editor-save=${this._handleSaveScript}
@save-script=${this._handleSaveScript}
@undo-paste=${this.undo}
>
<div class="alert-wrapper" slot="alerts">
${this._errors || stateObj?.state === UNAVAILABLE
@@ -679,7 +678,7 @@ export class HaScriptEditor extends UndoRedoMixin<
private _valueChanged(ev) {
if (this._config) {
this.pushToUndo(this._config);
this._undoRedoController.commit(this._config);
}
this._config = ev.detail.value;
@@ -776,7 +775,7 @@ export class HaScriptEditor extends UndoRedoMixin<
}
if (this._config) {
this.pushToUndo(this._config);
this._undoRedoController.commit(this._config);
}
this._manualEditor?.addFields();
@@ -1110,9 +1109,9 @@ export class HaScriptEditor extends UndoRedoMixin<
x: () => this._cutSelectedRow(),
Delete: () => this._deleteSelectedRow(),
Backspace: () => this._deleteSelectedRow(),
z: () => this.undo(),
Z: () => this.redo(),
y: () => this.redo(),
z: () => this._undo(),
Z: () => this._redo(),
y: () => this._redo(),
};
}
@@ -1146,16 +1145,20 @@ export class HaScriptEditor extends UndoRedoMixin<
this._manualEditor?.deleteSelectedRow();
}
protected get currentConfig() {
return this._config;
}
protected applyUndoRedo(config: ScriptConfig) {
private _applyUndoRedo(config: ScriptConfig) {
this._manualEditor?.triggerCloseSidebar();
this._config = config;
this._dirty = true;
}
private _undo() {
this._undoRedoController.undo();
}
private _redo() {
this._undoRedoController.redo();
}
static get styles(): CSSResultGroup {
return [
haStyle,

View File

@@ -20,13 +20,13 @@ import "../../../components/ha-md-menu-item";
import type { ScriptFieldSidebarConfig } from "../../../data/automation";
import type { Field } from "../../../data/script";
import { SELECTOR_SELECTOR_BUILDING_BLOCKS } from "../../../data/selector/selector_selector";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { indentStyle, overflowStyles } from "../automation/styles";
import "./ha-script-field-selector-editor";
import type HaScriptFieldSelectorEditor from "./ha-script-field-selector-editor";
import { showToast } from "../../../util/toast";
@customElement("ha-script-field-row")
export default class HaScriptFieldRow extends LitElement {
@@ -386,21 +386,19 @@ export default class HaScriptFieldRow extends LitElement {
};
private _onDelete = () => {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.script.editor.field_delete_confirm_title"
),
text: this.hass.localize(
"ui.panel.config.script.editor.field_delete_confirm_text"
),
dismissText: this.hass.localize("ui.common.cancel"),
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: () => {
fireEvent(this, "value-changed", { value: null });
if (this._selected || this._selectorRowSelected) {
fireEvent(this, "close-sidebar");
}
fireEvent(this, "value-changed", { value: null });
if (this._selected || this._selectorRowSelected) {
fireEvent(this, "close-sidebar");
}
showToast(this, {
message: this.hass.localize("ui.common.successfully_deleted"),
duration: 4000,
action: {
text: this.hass.localize("ui.common.undo"),
action: () => {
fireEvent(window, "undo-change");
},
},
});
};

View File

@@ -491,7 +491,7 @@ export class HaManualScriptEditor extends LitElement {
action: {
text: this.hass.localize("ui.common.undo"),
action: () => {
fireEvent(this, "undo-paste");
fireEvent(this, "undo-change");
this._pastedConfig = undefined;
},

View File

@@ -83,6 +83,21 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
private _footerElement?: LovelaceHeaderFooter;
connectedCallback(): void {
super.connectedCallback();
this.addEventListener("row-visibility-changed", (ev) =>
this._updateRowVisibility(ev)
);
}
disconnectedCallback(): void {
super.disconnectedCallback();
this.removeEventListener(
"row-visibility-changed",
this._updateRowVisibility
);
}
set hass(hass: HomeAssistant) {
this._hass = hass;
this.shadowRoot
@@ -251,18 +266,9 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
#states {
flex: 1;
}
#states > * {
margin: 8px 0;
}
#states > *:first-child {
margin-top: 0;
}
#states > *:last-child {
margin-bottom: 0;
display: flex;
flex-direction: column;
gap: var(--entities-card-row-gap, var(--card-row-gap, 8px));
}
#states > div > * {
@@ -320,8 +326,16 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
element.hass = this._hass;
}
return html`<div>${element}</div>`;
return html`<div ?hidden=${element.hidden}>${element}</div>`;
}
private _updateRowVisibility = (ev) => {
if (ev.detail?.value === false) {
ev.detail?.row?.parentElement!.style.setProperty("display", "none");
} else {
ev.detail?.row?.parentElement!.style.setProperty("display", "");
}
};
}
declare global {

View File

@@ -318,7 +318,7 @@ class HUIRoot extends LitElement {
menu-corner="END"
>
<ha-icon-button
.label=${label}
.id="button-${index}"
.path=${item.icon}
slot="trigger"
></ha-icon-button>
@@ -340,6 +340,9 @@ class HUIRoot extends LitElement {
`
)}
</ha-button-menu>
<ha-tooltip placement="bottom" .for="button-${index}">
${label}
</ha-tooltip>
`
: html`
<ha-icon-button

View File

@@ -7,7 +7,13 @@ import type {
EntityConfig,
LovelaceRow,
} from "../entity-rows/types";
import { fireEvent } from "../../../common/dom/fire_event";
declare global {
interface HASSDomEvents {
"row-visibility-changed": { row: LovelaceRow; value: boolean };
}
}
@customElement("hui-conditional-row")
class HuiConditionalRow extends HuiConditionalBase implements LovelaceRow {
public setConfig(config: ConditionalRowConfig): void {
@@ -26,6 +32,15 @@ class HuiConditionalRow extends HuiConditionalBase implements LovelaceRow {
: config.row
) as LovelaceRow;
}
protected setVisibility(conditionMet: boolean): void {
const visible = this.preview || conditionMet;
const previouslyHidden = this.hidden;
super.setVisibility(conditionMet);
if (previouslyHidden !== this.hidden) {
fireEvent(this, "row-visibility-changed", { row: this, value: visible });
}
}
}
declare global {

View File

@@ -3907,8 +3907,6 @@
"change_alias": "Rename trigger",
"alias": "Trigger name",
"delete": "[%key:ui::common::delete%]",
"delete_confirm_title": "Delete trigger?",
"delete_confirm_text": "It will be permanently deleted.",
"unsupported_platform": "No visual editor support for platform: {platform}",
"type_select": "Trigger type",
"unknown_trigger": "[%key:ui::panel::config::devices::automation::triggers::unknown_trigger%]",
@@ -4172,8 +4170,6 @@
"change_alias": "Rename condition",
"alias": "Condition name",
"delete": "[%key:ui::common::delete%]",
"delete_confirm_title": "Delete condition?",
"delete_confirm_text": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm_text%]",
"unsupported_condition": "No visual editor support for condition: {condition}",
"type_select": "Condition type",
"unknown_condition": "[%key:ui::panel::config::devices::automation::conditions::unknown_condition%]",
@@ -4343,8 +4339,6 @@
"disable": "Disable",
"disabled": "Disabled",
"delete": "[%key:ui::common::delete%]",
"delete_confirm_title": "Delete action?",
"delete_confirm_text": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm_text%]",
"unsupported_action": "No visual editor support for this action",
"type_select": "Action type",
"continue_on_error": "Continue on error",
@@ -4806,8 +4800,6 @@
"label": "Field",
"field_selector": "Field selector"
},
"field_delete_confirm_title": "Delete field?",
"field_delete_confirm_text": "[%key:ui::panel::config::automation::editor::triggers::delete_confirm_text%]",
"header": "Script: {name}",
"default_name": "New script",
"modes": {

242
yarn.lock
View File

@@ -1613,19 +1613,21 @@ __metadata:
languageName: node
linkType: hard
"@eslint/config-helpers@npm:^0.3.1":
version: 0.3.1
resolution: "@eslint/config-helpers@npm:0.3.1"
checksum: 10/fc1a90ef6180aa4b5187cee04cfc566abb2a32b77ca3e7eeb4312c7388f6898221adaf8451d9ddb22e0b8860d900fefb1eb1435e4f32f8d8732de87f14605f8f
"@eslint/config-helpers@npm:^0.4.0":
version: 0.4.0
resolution: "@eslint/config-helpers@npm:0.4.0"
dependencies:
"@eslint/core": "npm:^0.16.0"
checksum: 10/d5fdbf927a77b98d2462f025f8b1a5b610609201f8d1dd47032a2937842f02bf3bdf9cb672025c83a00f3255dfd218172f989caa724853c4a8f434124a6d79ff
languageName: node
linkType: hard
"@eslint/core@npm:^0.15.2":
version: 0.15.2
resolution: "@eslint/core@npm:0.15.2"
"@eslint/core@npm:^0.16.0":
version: 0.16.0
resolution: "@eslint/core@npm:0.16.0"
dependencies:
"@types/json-schema": "npm:^7.0.15"
checksum: 10/41d6273bbc6897cca34a2ca4e80a24bf6f1d43519456ebaa3c38f187da2d9e06f442c64f6e2a2813f055dce35e5cea33a21d0ac3b5b0830b7165641c640faf5d
checksum: 10/3cea45971b2d0114267b6101b673270b5d8047448cc7a8cbfdca0b0245e9d5e081cb25f13551dc7d55a090f98c13b33f0c4999f8ee8ab058537e6037629a0f71
languageName: node
linkType: hard
@@ -1646,10 +1648,10 @@ __metadata:
languageName: node
linkType: hard
"@eslint/js@npm:9.36.0":
version: 9.36.0
resolution: "@eslint/js@npm:9.36.0"
checksum: 10/a0542f529f87b9ad69ef85c47b0c070b763591a61773b131a9d1d53934a587f0708c05a1a8f48a6805486004a4922c91d696c1e4835ff61f8750ffbded2f0c30
"@eslint/js@npm:9.37.0":
version: 9.37.0
resolution: "@eslint/js@npm:9.37.0"
checksum: 10/2ead426ed47af0b914c7d7064eb59fede858483cf9511f78ded840708aca578138f2a6c375916d520f4f2ecf25945f4bd47b8a84e42106b4eb46f7708a36db1d
languageName: node
linkType: hard
@@ -1660,13 +1662,13 @@ __metadata:
languageName: node
linkType: hard
"@eslint/plugin-kit@npm:^0.3.5":
version: 0.3.5
resolution: "@eslint/plugin-kit@npm:0.3.5"
"@eslint/plugin-kit@npm:^0.4.0":
version: 0.4.0
resolution: "@eslint/plugin-kit@npm:0.4.0"
dependencies:
"@eslint/core": "npm:^0.15.2"
"@eslint/core": "npm:^0.16.0"
levn: "npm:^0.4.1"
checksum: 10/b8552d79c3091446b07d8b87a9a8ccb8cdee4d933c0ed46b8f61029c3382246fec8d04ea7d1e61656d9275263205ccaa40019fd7581bbce897eca3eda42d5dad
checksum: 10/2c37ca00e352447215aeadcaff5765faead39695f1cb91cd3079a43261b234887caf38edc462811bb3401acf8c156c04882f87740df936838290c705351483be
languageName: node
linkType: hard
@@ -1696,15 +1698,15 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/ecma402-abstract@npm:2.3.4":
version: 2.3.4
resolution: "@formatjs/ecma402-abstract@npm:2.3.4"
"@formatjs/ecma402-abstract@npm:2.3.5":
version: 2.3.5
resolution: "@formatjs/ecma402-abstract@npm:2.3.5"
dependencies:
"@formatjs/fast-memoize": "npm:2.2.7"
"@formatjs/intl-localematcher": "npm:0.6.1"
"@formatjs/intl-localematcher": "npm:0.6.2"
decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0"
checksum: 10/573971ffc291096a4b9fcc80b4708124e89bf2e3ac50e0f78b41eb797e9aa1b842f4dc3665e4467a853c738386821769d9e40408a1d25bc73323a1f057a16cf2
checksum: 10/254651057170836237dc4f0fbb372157f97133c4dcee414007e0cdb5b589baf0546c2f6337d117b988ee0a4f0a4d8247780aaa9e96b410c568495f162c40dc50
languageName: node
linkType: hard
@@ -1717,144 +1719,144 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/icu-messageformat-parser@npm:2.11.2":
version: 2.11.2
resolution: "@formatjs/icu-messageformat-parser@npm:2.11.2"
"@formatjs/icu-messageformat-parser@npm:2.11.3":
version: 2.11.3
resolution: "@formatjs/icu-messageformat-parser@npm:2.11.3"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/icu-skeleton-parser": "npm:1.8.14"
"@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/icu-skeleton-parser": "npm:1.8.15"
tslib: "npm:^2.8.0"
checksum: 10/e919eb2a132ac1d54fb1a7e3a3254007649b55196d3818090df92a4268dcddf20cbdf863c06039fbbe7a35a8a3f17bdc172dade99d1f17c1d8a95dcec444c3e3
checksum: 10/339f5ff5ea7417e2db7f01bd41340f78fd5a8e56a66e723272d21ce7ab4b265dcb45748cdca76eac7137e2b5e6767986812b471e011b4602cf7afbc6da57fb98
languageName: node
linkType: hard
"@formatjs/icu-skeleton-parser@npm:1.8.14":
version: 1.8.14
resolution: "@formatjs/icu-skeleton-parser@npm:1.8.14"
"@formatjs/icu-skeleton-parser@npm:1.8.15":
version: 1.8.15
resolution: "@formatjs/icu-skeleton-parser@npm:1.8.15"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/ecma402-abstract": "npm:2.3.5"
tslib: "npm:^2.8.0"
checksum: 10/2fbe3155c310358820b118d8c9844f314eff3500a82f1c65402434a3095823e1afeaab8d1762b4a59cc5679d82dc4c8c134683565d7cdae4daace23251f46a47
checksum: 10/19825abc1a5eef0288456c08420d06f3da8256fbe81db0b9ead48cacc94954d748c8068988e26d184d38fca2e50c191ecda5a10ff3935529c3134b8d80db0538
languageName: node
linkType: hard
"@formatjs/intl-datetimeformat@npm:6.18.0":
version: 6.18.0
resolution: "@formatjs/intl-datetimeformat@npm:6.18.0"
"@formatjs/intl-datetimeformat@npm:6.18.1":
version: 6.18.1
resolution: "@formatjs/intl-datetimeformat@npm:6.18.1"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/intl-localematcher": "npm:0.6.1"
"@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/intl-localematcher": "npm:0.6.2"
decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0"
checksum: 10/b70edaa4cfa150f0a6cbeeb1488e6acdea21349abdefc4e37b923de68592c6f330a966456bf6000f233d0f715cf3b8cfce23d5a4ed574fa8ea35ccb5bea80886
checksum: 10/66938778ecf37472a7e2f1d9349b0ac249fcbd5d684ae5614dea07287876182429980ba2fe3671224f981065baf017ac955f4b3c1f3c924c89bf2ec82dd1acd8
languageName: node
linkType: hard
"@formatjs/intl-displaynames@npm:6.8.11":
version: 6.8.11
resolution: "@formatjs/intl-displaynames@npm:6.8.11"
"@formatjs/intl-displaynames@npm:6.8.12":
version: 6.8.12
resolution: "@formatjs/intl-displaynames@npm:6.8.12"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/intl-localematcher": "npm:0.6.1"
"@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/intl-localematcher": "npm:0.6.2"
tslib: "npm:^2.8.0"
checksum: 10/05c785d9e767cc1e4d1bd40d6989c3318b6a98cb43dd6808f501f5e5538bb3a1fb8fa80f8d2282d598501d3d193a406f0127acce6b14cb7c595ab6d981437e6f
checksum: 10/7de27ef7e8cde2febce84d5443f00b70062cbd0c3f1039ce8ed1caacb15c4c7a36da16295f26657d59aa4663141a04d7b1083bfd1eea6a4e8ad9dc6093a2c886
languageName: node
linkType: hard
"@formatjs/intl-durationformat@npm:0.7.4":
version: 0.7.4
resolution: "@formatjs/intl-durationformat@npm:0.7.4"
"@formatjs/intl-durationformat@npm:0.7.5":
version: 0.7.5
resolution: "@formatjs/intl-durationformat@npm:0.7.5"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/intl-localematcher": "npm:0.6.1"
"@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/intl-localematcher": "npm:0.6.2"
tslib: "npm:^2.8.0"
checksum: 10/d62273ecd635475ca91e9b501301f3f396403fa91b584c550734b19b2d194ba1316b27303fed985c1d42ae933d54eb220da6540edfdf376b0d9371ecfd0d4e15
checksum: 10/4dc81b112fed25dc8da0a16ddeff033b7c763bf9a1cfd7b1b25c1216f7f147eb67a47059a3cf95b4d4ade150c54a813542b84e69298905a4bc22548d74bf8567
languageName: node
linkType: hard
"@formatjs/intl-enumerator@npm:1.8.10":
version: 1.8.10
resolution: "@formatjs/intl-enumerator@npm:1.8.10"
"@formatjs/intl-enumerator@npm:1.8.11":
version: 1.8.11
resolution: "@formatjs/intl-enumerator@npm:1.8.11"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/ecma402-abstract": "npm:2.3.5"
tslib: "npm:^2.8.0"
checksum: 10/9e0e762143248bf91e174d3abc15261b47ac7294632d26797cf5b001707aa68ca2deeb05c95f7308aa2cffa46d61b0fac46306dea722ab210dfa012990743798
checksum: 10/8646a517cd4160c1ceff888ec8fdf652caa3d375fa41231e829c13bc7be0cd156c9642e339b75e9cfa8ef60ae8140c766f9055318c62f1c1d9345f25cdb7f426
languageName: node
linkType: hard
"@formatjs/intl-getcanonicallocales@npm:2.5.5":
version: 2.5.5
resolution: "@formatjs/intl-getcanonicallocales@npm:2.5.5"
"@formatjs/intl-getcanonicallocales@npm:2.5.6":
version: 2.5.6
resolution: "@formatjs/intl-getcanonicallocales@npm:2.5.6"
dependencies:
tslib: "npm:^2.8.0"
checksum: 10/2a32202765c9a4f16fc36f4e4afca7fd5f4f35885ad2ca671352a7bba1a19d5ec81933d52ab1855c8570e73247213739d9d2d95d2438bd9f02a1f0db7cb9b8a9
checksum: 10/1d3d13fa1758a9bb7854f3afd844ecb70a4333a7cfbb6822b99e3b8ab6269e525a0ca23a8a47c3944e5376bc19e9e423b5cc3043db1c6de64909986c5cec6fc0
languageName: node
linkType: hard
"@formatjs/intl-listformat@npm:7.7.11":
version: 7.7.11
resolution: "@formatjs/intl-listformat@npm:7.7.11"
"@formatjs/intl-listformat@npm:7.7.12":
version: 7.7.12
resolution: "@formatjs/intl-listformat@npm:7.7.12"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/intl-localematcher": "npm:0.6.1"
"@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/intl-localematcher": "npm:0.6.2"
tslib: "npm:^2.8.0"
checksum: 10/e7de54dcbcfdd8718870501623fb1be55dbac11e2582b7961d4668fb5e1f0d1f6da0388ed49084a4527e500dbea548670659efccb690f3b4398f0f8bcd5221dd
checksum: 10/eee910e83ad28b3b3c24ab6e155720187ae5b5ac936ffa2c8ec6cc8c392c194fd5c79a166290da1c6de8dc1857e3d9d11241029832ec88f7a85cce1821b7f067
languageName: node
linkType: hard
"@formatjs/intl-locale@npm:4.2.11":
version: 4.2.11
resolution: "@formatjs/intl-locale@npm:4.2.11"
"@formatjs/intl-locale@npm:4.2.12":
version: 4.2.12
resolution: "@formatjs/intl-locale@npm:4.2.12"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/intl-enumerator": "npm:1.8.10"
"@formatjs/intl-getcanonicallocales": "npm:2.5.5"
"@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/intl-enumerator": "npm:1.8.11"
"@formatjs/intl-getcanonicallocales": "npm:2.5.6"
tslib: "npm:^2.8.0"
checksum: 10/8746af66ebd5284f189c83e0d59a4d781490ce3eadaab284bf96c4240eaf8b9422130a94a842a1ab12fa14bb2cdf02e9f78ac3b9cf955156fafeffab9d73d7a2
checksum: 10/42111a3002a5a2076b3eb012073230f69c62355dc03647bc17f4d0805f39c7e720e2281b359277d020fef623944a5bcc1ddc3dae9a3af74886d876147680147d
languageName: node
linkType: hard
"@formatjs/intl-localematcher@npm:0.6.1":
version: 0.6.1
resolution: "@formatjs/intl-localematcher@npm:0.6.1"
"@formatjs/intl-localematcher@npm:0.6.2":
version: 0.6.2
resolution: "@formatjs/intl-localematcher@npm:0.6.2"
dependencies:
tslib: "npm:^2.8.0"
checksum: 10/c7b3bc8395d18670677f207b2fd107561fff5d6394a9b4273c29e0bea920300ec3a2eefead600ebb7761c04a770cada28f78ac059f84d00520bfb57a9db36998
checksum: 10/eb12a7f5367bbecdfafc20d7f005559ce840f420e970f425c5213d35e94e86dfe75bde03464971a26494bf8427d4961269db22ecad2834f2a19d888b5d9cc064
languageName: node
linkType: hard
"@formatjs/intl-numberformat@npm:8.15.4":
version: 8.15.4
resolution: "@formatjs/intl-numberformat@npm:8.15.4"
"@formatjs/intl-numberformat@npm:8.15.5":
version: 8.15.5
resolution: "@formatjs/intl-numberformat@npm:8.15.5"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/intl-localematcher": "npm:0.6.1"
"@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/intl-localematcher": "npm:0.6.2"
decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0"
checksum: 10/232740eb4992f1bf4f829f05a755f427089a70b56a8a715fa9ac8604f701691701e989247ef1537a1d7c90e315b4153b82cf2e67e7f9d5b78d471c1cf59abace
checksum: 10/3440371a43c54cdd2aa3714cb518ad22e491dd19fbc0c046e712dde078d3f6ed709474376863d64d2bddb506957d1cf265d440f6723b88211044a7b56186e550
languageName: node
linkType: hard
"@formatjs/intl-pluralrules@npm:5.4.4":
version: 5.4.4
resolution: "@formatjs/intl-pluralrules@npm:5.4.4"
"@formatjs/intl-pluralrules@npm:5.4.5":
version: 5.4.5
resolution: "@formatjs/intl-pluralrules@npm:5.4.5"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/intl-localematcher": "npm:0.6.1"
"@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/intl-localematcher": "npm:0.6.2"
decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0"
checksum: 10/919f80e144283b5849014bc245626916224adc0d693e8be5531168f1c7af54bb4c8cbd77a12ceba1d13ad49171680d346d9176464fae5013e13f79d9c7baa02a
checksum: 10/00f650891893b743d126dd2bf0d17c1b16a8c9e0e0dd94cd0895e66cb556246116263e9603204e1991924814d0ed3a3503765914aff08181d5e4435dfc5e547c
languageName: node
linkType: hard
"@formatjs/intl-relativetimeformat@npm:11.4.11":
version: 11.4.11
resolution: "@formatjs/intl-relativetimeformat@npm:11.4.11"
"@formatjs/intl-relativetimeformat@npm:11.4.12":
version: 11.4.12
resolution: "@formatjs/intl-relativetimeformat@npm:11.4.12"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/intl-localematcher": "npm:0.6.1"
"@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/intl-localematcher": "npm:0.6.2"
tslib: "npm:^2.8.0"
checksum: 10/fda4da27c0245869316c9199ed4e0521988be8b41b3e685f4abcb486f01d5b4c72f2ecf1b19b07091c15360c7691a4dd87199f81943d1ad6bda084c746fc8ec3
checksum: 10/f6adca59738cb7f58d2ea985558d8fc45e567406de6fb6e67894afe790e2a9fa1a19d34853afc36805fa4a3d638e29c62d6c6ba3ec2a85628c240081dcdfebc1
languageName: node
linkType: hard
@@ -8030,18 +8032,18 @@ __metadata:
languageName: node
linkType: hard
"eslint@npm:9.36.0":
version: 9.36.0
resolution: "eslint@npm:9.36.0"
"eslint@npm:9.37.0":
version: 9.37.0
resolution: "eslint@npm:9.37.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.8.0"
"@eslint-community/regexpp": "npm:^4.12.1"
"@eslint/config-array": "npm:^0.21.0"
"@eslint/config-helpers": "npm:^0.3.1"
"@eslint/core": "npm:^0.15.2"
"@eslint/config-helpers": "npm:^0.4.0"
"@eslint/core": "npm:^0.16.0"
"@eslint/eslintrc": "npm:^3.3.1"
"@eslint/js": "npm:9.36.0"
"@eslint/plugin-kit": "npm:^0.3.5"
"@eslint/js": "npm:9.37.0"
"@eslint/plugin-kit": "npm:^0.4.0"
"@humanfs/node": "npm:^0.16.6"
"@humanwhocodes/module-importer": "npm:^1.0.1"
"@humanwhocodes/retry": "npm:^0.4.2"
@@ -8076,7 +8078,7 @@ __metadata:
optional: true
bin:
eslint: bin/eslint.js
checksum: 10/6e512a82e680e6cdc554e97c7e232b83171f24a52fb46f89c2df74bcb80fe59b6e0a989485c9fe7e9ca81556b1953dd8604ace4ed37f651eded9a37816c06b7c
checksum: 10/c7530470c9cafe9a7f768477f7894d9b9d28e92995186223e99fbd9edeb391119e2a70678a2e98e213ae37cbb41de89403b510f5f33df2340aa65dd6f2a3c0bb
languageName: node
linkType: hard
@@ -9190,15 +9192,15 @@ __metadata:
"@codemirror/view": "npm:6.38.4"
"@date-fns/tz": "npm:1.4.1"
"@egjs/hammerjs": "npm:2.0.17"
"@formatjs/intl-datetimeformat": "npm:6.18.0"
"@formatjs/intl-displaynames": "npm:6.8.11"
"@formatjs/intl-durationformat": "npm:0.7.4"
"@formatjs/intl-getcanonicallocales": "npm:2.5.5"
"@formatjs/intl-listformat": "npm:7.7.11"
"@formatjs/intl-locale": "npm:4.2.11"
"@formatjs/intl-numberformat": "npm:8.15.4"
"@formatjs/intl-pluralrules": "npm:5.4.4"
"@formatjs/intl-relativetimeformat": "npm:11.4.11"
"@formatjs/intl-datetimeformat": "npm:6.18.1"
"@formatjs/intl-displaynames": "npm:6.8.12"
"@formatjs/intl-durationformat": "npm:0.7.5"
"@formatjs/intl-getcanonicallocales": "npm:2.5.6"
"@formatjs/intl-listformat": "npm:7.7.12"
"@formatjs/intl-locale": "npm:4.2.12"
"@formatjs/intl-numberformat": "npm:8.15.5"
"@formatjs/intl-pluralrules": "npm:5.4.5"
"@formatjs/intl-relativetimeformat": "npm:11.4.12"
"@fullcalendar/core": "npm:6.1.19"
"@fullcalendar/daygrid": "npm:6.1.19"
"@fullcalendar/interaction": "npm:6.1.19"
@@ -9291,7 +9293,7 @@ __metadata:
dialog-polyfill: "npm:0.5.6"
echarts: "npm:6.0.0"
element-internals-polyfill: "npm:3.0.2"
eslint: "npm:9.36.0"
eslint: "npm:9.37.0"
eslint-config-airbnb-base: "npm:15.0.0"
eslint-config-prettier: "npm:10.1.8"
eslint-import-resolver-webpack: "npm:0.13.10"
@@ -9315,7 +9317,7 @@ __metadata:
html-minifier-terser: "npm:7.2.0"
husky: "npm:9.1.7"
idb-keyval: "npm:6.2.2"
intl-messageformat: "npm:10.7.16"
intl-messageformat: "npm:10.7.17"
js-yaml: "npm:4.1.0"
jsdom: "npm:27.0.0"
jszip: "npm:3.10.1"
@@ -9710,15 +9712,15 @@ __metadata:
languageName: node
linkType: hard
"intl-messageformat@npm:10.7.16":
version: 10.7.16
resolution: "intl-messageformat@npm:10.7.16"
"intl-messageformat@npm:10.7.17":
version: 10.7.17
resolution: "intl-messageformat@npm:10.7.17"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/ecma402-abstract": "npm:2.3.5"
"@formatjs/fast-memoize": "npm:2.2.7"
"@formatjs/icu-messageformat-parser": "npm:2.11.2"
"@formatjs/icu-messageformat-parser": "npm:2.11.3"
tslib: "npm:^2.8.0"
checksum: 10/c19b77c5e495ce8b0d1aa0d95444bf3a4f73886805f1e08d7159b364abcf2f63686b2ccf202eaafb0e39a0e9fde61848b8dd2db1679efd4f6ec8f6a3d0e77928
checksum: 10/4f8c30c998bfc14eb64894414b94a8923045ab31d7bbf0978dab6621c644d451ff5c533c04ce8128163b74dd6d59061ec1ef3acb1cbab3302d31cbdb21947620
languageName: node
linkType: hard