mirror of
https://github.com/home-assistant/frontend.git
synced 2025-10-06 18:29:42 +00:00
Compare commits
6 Commits
triggers-d
...
sec_pypi_p
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f22f01e513 | ||
![]() |
e04a04632a | ||
![]() |
04bc5fba63 | ||
![]() |
3f86f144b5 | ||
![]() |
4efef5ed16 | ||
![]() |
cac7ae2a40 |
13
.github/workflows/release.yaml
vendored
13
.github/workflows/release.yaml
vendored
@@ -19,8 +19,11 @@ jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
environment: pypi
|
||||
permissions:
|
||||
contents: write # Required to upload release assets
|
||||
id-token: write # For "Trusted Publisher" to PyPi
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
@@ -46,14 +49,18 @@ jobs:
|
||||
run: ./script/translations_download
|
||||
env:
|
||||
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
|
||||
|
||||
- name: Build and release package
|
||||
run: |
|
||||
python3 -m pip install twine build
|
||||
export TWINE_USERNAME="__token__"
|
||||
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
|
||||
python3 -m pip install build
|
||||
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
|
||||
script/release
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||
with:
|
||||
skip-existing: true
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
|
||||
with:
|
||||
|
@@ -1,5 +1,4 @@
|
||||
#!/bin/sh
|
||||
# Pushes a new version to PyPi.
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
@@ -12,5 +11,4 @@ yarn install
|
||||
script/build_frontend
|
||||
|
||||
rm -rf dist home_assistant_frontend.egg-info
|
||||
python3 -m build
|
||||
python3 -m twine upload dist/*.whl --skip-existing
|
||||
python3 -m build -q
|
||||
|
141
src/common/controllers/undo-redo-controller.ts
Normal file
141
src/common/controllers/undo-redo-controller.ts
Normal 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;
|
||||
}
|
||||
}
|
@@ -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(
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
@@ -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`)
|
||||
);
|
||||
|
@@ -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
|
||||
|
@@ -73,8 +73,7 @@ export type TranslationCategory =
|
||||
| "application_credentials"
|
||||
| "issues"
|
||||
| "selector"
|
||||
| "services"
|
||||
| "triggers";
|
||||
| "services";
|
||||
|
||||
export const subscribeTranslationPreferences = (
|
||||
hass: HomeAssistant,
|
||||
|
@@ -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",
|
||||
});
|
||||
|
@@ -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;
|
||||
};
|
@@ -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");
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@@ -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 },
|
||||
|
@@ -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");
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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");
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@@ -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")
|
||||
|
@@ -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";
|
||||
|
||||
|
@@ -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}
|
||||
|
@@ -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>
|
||||
|
@@ -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(
|
||||
|
@@ -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}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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,
|
||||
|
@@ -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");
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@@ -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;
|
||||
},
|
||||
|
@@ -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": {
|
||||
|
Reference in New Issue
Block a user