Compare commits

..

8 Commits

Author SHA1 Message Date
Bram Kragten
b01ab9234b Bumped version to 20250731.0 2025-07-31 16:54:24 +02:00
Wendelin
ad39228dea Fix line-height, fix script editor buttons (#26337)
* Fix line-height

* Fix script root buttons
2025-07-31 16:54:03 +02:00
Wendelin
8cc48cdecb Use tilecard button feature editor (#26335)
Use button feature editor
2025-07-31 16:54:02 +02:00
Wendelin
524e89acf0 Revert "Use query params instead of path for media browser navigate ids" (#26333) 2025-07-31 16:54:01 +02:00
Wendelin
48f6b34882 Fix ha-button with missing label and links (#26332) 2025-07-31 16:54:00 +02:00
Bram Kragten
44d9185574 Fix area picker text alignment in voice wizard (#26330) 2025-07-31 16:53:59 +02:00
Joost Lekkerkerker
51ff6c6564 Use underscores in AI task name (#26327) 2025-07-31 16:53:58 +02:00
Franck Nijhof
b49b8e3db8 Add weekdays to time trigger (#25908)
* Add weekdays to time trigger

* Update src/translations/en.json

Co-authored-by: Norbert Rittel <norbert@rittel.de>

* Localization changes

---------

Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2025-07-31 16:53:57 +02:00
151 changed files with 3219 additions and 6560 deletions

View File

@@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@v4.2.4
uses: actions/cache@v4.2.3
with:
path: |
node_modules/.cache/prettier

View File

@@ -74,7 +74,7 @@ jobs:
echo "home-assistant-frontend==$version" > ./requirements.txt
- name: Build wheels
uses: home-assistant/wheels@2025.07.0
uses: home-assistant/wheels@2025.03.0
with:
abi: cp313
tag: musllinux_1_2

View File

@@ -1,8 +0,0 @@
# People marked here will be automatically requested for a review
# when the code that they own is touched.
# https://github.com/blog/2392-introducing-code-owners
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# Part of the frontend that mobile developper should review
src/external_app/ @bgoncal @TimoPtr
test/external_app/ @bgoncal @TimoPtr

View File

@@ -68,7 +68,7 @@
}
#ha-launch-screen .ha-launch-screen-spacer-top {
flex: 1;
margin-top: calc( 2 * max(var(--safe-area-inset-bottom, 0px), 48px) + 46px );
margin-top: calc( 2 * max(var(--safe-area-inset-bottom), 48px) + 46px );
padding-top: 48px;
}
#ha-launch-screen .ha-launch-screen-spacer-bottom {
@@ -76,7 +76,7 @@
padding-top: 48px;
}
.ohf-logo {
margin: max(var(--safe-area-inset-bottom, 0px), 48px) 0;
margin: max(var(--safe-area-inset-bottom), 48px) 0;
display: flex;
flex-direction: column;
align-items: center;

View File

@@ -64,4 +64,4 @@ Check the [webawesome documentation](https://webawesome.com/docs/components/butt
**CSS Custom Properties**
- `--ha-button-height` - Height of the button.
- `--ha-button-border-radius` - Border radius of the button. Defaults to `var(--ha-border-radius-pill)`.
- `--ha-button-radius` - Border radius of the button. Defaults to `var(--wa-border-radius-pill)`.

View File

@@ -11,7 +11,6 @@ import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
import { ClimateEntityFeature } from "../../../../src/data/climate";
import { FanEntityFeature } from "../../../../src/data/fan";
const ENTITIES = [
getEntity("switch", "tv_outlet", "on", {
@@ -101,12 +100,6 @@ const ENTITIES = [
ClimateEntityFeature.FAN_MODE +
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
}),
getEntity("fan", "fan_direction", "on", {
friendly_name: "Ceiling fan",
device_class: "fan",
direction: "reverse",
supported_features: [FanEntityFeature.DIRECTION],
}),
];
const CONFIGS = [
@@ -268,15 +261,6 @@ const CONFIGS = [
- type: target-temperature
`,
},
{
heading: "Fan direction feature",
config: `
- type: tile
entity: fan.fan_direction
features:
- type: fan-direction
`,
},
];
@customElement("demo-lovelace-tile-card")

View File

@@ -21,8 +21,8 @@ import type { HomeAssistant } from "../../../../src/types";
import type { HassioDatatiskDialogParams } from "./show-dialog-hassio-datadisk";
const calculateMoveTime = memoizeOne((supervisor: Supervisor): number => {
// Assume a speed of 30 MB/s.
const moveTime = (supervisor.host.disk_used * 1000) / 60 / 30;
const speed = supervisor.host.disk_life_time !== "" ? 30 : 10;
const moveTime = (supervisor.host.disk_used * 1000) / 60 / speed;
const rebootTime = (supervisor.host.startup_time * 4) / 60;
return Math.ceil((moveTime + rebootTime) / 10) * 10;
});

View File

@@ -2,13 +2,13 @@ import { mdiDelete, mdiPlus } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-button";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../src/components/ha-form/types";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-settings-row";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
addHassioDockerRegistry,

View File

@@ -7,14 +7,10 @@ import { fireEvent } from "../../../../src/common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-tooltip";
import "../../../../src/components/ha-svg-icon";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-md-list";
import "../../../../src/components/ha-md-list-item";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-textfield";
import type { HaTextField } from "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-tooltip";
import type {
HassioAddonInfo,
HassioAddonRepository,
@@ -28,6 +24,10 @@ import {
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import type { HassioRepositoryDialogParams } from "./show-dialog-repositories";
import type { HaTextField } from "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-md-list";
import "../../../../src/components/ha-md-list-item";
@customElement("dialog-hassio-repositories")
class HassioRepositoriesDialog extends LitElement {

View File

@@ -143,12 +143,16 @@ class HassioHostInfo extends LitElement {
: ""}
</div>
<div>
${this.supervisor.host.disk_life_time !== null
${this.supervisor.host.disk_life_time !== "" &&
this.supervisor.host.disk_life_time >= 10
? html` <ha-settings-row>
<span slot="heading">
${this.supervisor.localize("system.host.lifetime_used")}
${this.supervisor.localize(
"system.host.emmc_lifetime_used"
)}
</span>
<span slot="description">
${this.supervisor.host.disk_life_time - 10} % -
${this.supervisor.host.disk_life_time} %
</span>
</ha-settings-row>`

View File

@@ -3,26 +3,26 @@ import { mdiArrowCollapseDown, mdiDownload } from "@mdi/js";
// eslint-disable-next-line import/extensions
import { IntersectionController } from "@lit-labs/observers/intersection-controller.js";
import { LitElement, type PropertyValues, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { customElement, property, query, state } from "lit/decorators";
import type {
LandingPageKeys,
LocalizeFunc,
} from "../../../src/common/translations/localize";
import { waitForSeconds } from "../../../src/common/util/wait";
import "../../../src/components/ha-alert";
import "../../../src/components/ha-ansi-to-html";
import type { HaAnsiToHtml } from "../../../src/components/ha-ansi-to-html";
import "../../../src/components/ha-button";
import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-svg-icon";
import { fileDownload } from "../../../src/util/file_download";
import "../../../src/components/ha-ansi-to-html";
import "../../../src/components/ha-alert";
import type { HaAnsiToHtml } from "../../../src/components/ha-ansi-to-html";
import {
getObserverLogs,
downloadUrl as observerLogsDownloadUrl,
} from "../data/observer";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { fileDownload } from "../../../src/util/file_download";
import { getSupervisorLogs, getSupervisorLogsFollow } from "../data/supervisor";
import { waitForSeconds } from "../../../src/common/util/wait";
import { ASSUME_CORE_START_SECONDS } from "../ha-landing-page";
const ERROR_CHECK = /^[\d\s-:]+(ERROR|CRITICAL)(.*)/gm;
@@ -108,8 +108,6 @@ class LandingPageLogs extends LitElement {
!this._scrolledToBottomController.value) ||
false,
})}"
size="small"
appearance="filled"
@click=${this._scrollToBottom}
>
<ha-svg-icon .path=${mdiArrowCollapseDown} slot="start"></ha-svg-icon>
@@ -311,14 +309,21 @@ class LandingPageLogs extends LitElement {
}
.new-logs-indicator {
--mdc-theme-primary: var(--text-primary-color);
overflow: hidden;
position: absolute;
bottom: 4px;
left: 4px;
bottom: 0;
left: 0;
right: 0;
height: 0;
background-color: var(--primary-color);
border-radius: 8px;
transition: height 0.4s ease-out;
display: flex;
justify-content: space-between;
align-items: center;
}
.new-logs-indicator.visible {

View File

@@ -26,7 +26,7 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@awesome.me/webawesome": "3.0.0-beta.4",
"@awesome.me/webawesome": "3.0.0-beta.3",
"@babel/runtime": "7.28.2",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.18.6",
@@ -46,12 +46,12 @@
"@formatjs/intl-numberformat": "8.15.4",
"@formatjs/intl-pluralrules": "5.4.4",
"@formatjs/intl-relativetimeformat": "11.4.11",
"@fullcalendar/core": "6.1.19",
"@fullcalendar/daygrid": "6.1.19",
"@fullcalendar/interaction": "6.1.19",
"@fullcalendar/list": "6.1.19",
"@fullcalendar/luxon3": "6.1.19",
"@fullcalendar/timegrid": "6.1.19",
"@fullcalendar/core": "6.1.18",
"@fullcalendar/daygrid": "6.1.18",
"@fullcalendar/interaction": "6.1.18",
"@fullcalendar/list": "6.1.18",
"@fullcalendar/luxon3": "6.1.18",
"@fullcalendar/timegrid": "6.1.18",
"@lezer/highlight": "1.2.1",
"@lit-labs/motion": "1.0.9",
"@lit-labs/observers": "2.0.6",
@@ -88,7 +88,7 @@
"@shoelace-style/shoelace": "2.20.1",
"@swc/helpers": "0.5.17",
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.9.1",
"@tsparticles/engine": "3.8.1",
"@tsparticles/preset-links": "3.2.0",
"@vaadin/combo-box": "24.7.9",
"@vaadin/vaadin-themable-mixin": "24.7.9",
@@ -100,7 +100,7 @@
"barcode-detector": "3.0.5",
"color-name": "2.0.0",
"comlink": "4.4.2",
"core-js": "3.45.0",
"core-js": "3.44.0",
"cropperjs": "1.6.2",
"culori": "4.0.2",
"date-fns": "4.1.0",
@@ -108,12 +108,12 @@
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
"echarts": "6.0.0",
"echarts": "5.6.0",
"element-internals-polyfill": "3.0.2",
"fuse.js": "7.1.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "6.0.2",
"hls.js": "1.6.9",
"hls.js": "1.6.7",
"home-assistant-js-websocket": "9.5.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "10.7.16",
@@ -124,7 +124,7 @@
"lit": "3.3.1",
"lit-html": "3.3.1",
"luxon": "3.7.1",
"marked": "16.1.2",
"marked": "16.1.1",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.3",
"object-hash": "3.0.0",
@@ -154,14 +154,14 @@
"@babel/helper-define-polyfill-provider": "0.6.5",
"@babel/plugin-transform-runtime": "7.28.0",
"@babel/preset-env": "7.28.0",
"@bundle-stats/plugin-webpack-filter": "4.21.2",
"@bundle-stats/plugin-webpack-filter": "4.21.1",
"@lokalise/node-api": "15.0.0",
"@octokit/auth-oauth-device": "8.0.1",
"@octokit/plugin-retry": "8.0.1",
"@octokit/rest": "22.0.0",
"@rsdoctor/rspack-plugin": "1.2.1",
"@rspack/cli": "1.4.11",
"@rspack/core": "1.4.11",
"@rsdoctor/rspack-plugin": "1.1.10",
"@rspack/cli": "1.4.10",
"@rspack/core": "1.4.10",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.22",
"@types/chromecast-caf-sender": "1.0.11",
@@ -173,7 +173,7 @@
"@types/leaflet-draw": "1.0.12",
"@types/leaflet.markercluster": "1.5.5",
"@types/lodash.merge": "4.6.9",
"@types/luxon": "3.7.1",
"@types/luxon": "3.6.2",
"@types/mocha": "10.0.10",
"@types/qrcode": "1.5.5",
"@types/sortablejs": "1.15.8",
@@ -185,7 +185,7 @@
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.0",
"eslint": "9.33.0",
"eslint": "9.32.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-webpack": "0.13.10",
@@ -195,7 +195,7 @@
"eslint-plugin-unused-imports": "4.1.4",
"eslint-plugin-wc": "3.0.1",
"fancy-log": "2.0.0",
"fs-extra": "11.3.1",
"fs-extra": "11.3.0",
"glob": "11.0.3",
"gulp": "5.0.1",
"gulp-brotli": "3.0.0",
@@ -205,7 +205,7 @@
"husky": "9.1.7",
"jsdom": "26.1.0",
"jszip": "3.10.1",
"lint-staged": "16.1.5",
"lint-staged": "16.1.2",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
@@ -218,8 +218,8 @@
"tar": "7.4.3",
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.2",
"typescript-eslint": "8.39.0",
"typescript": "5.8.3",
"typescript-eslint": "8.38.0",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.2.4",
"webpack-stats-plugin": "1.1.3",
@@ -232,7 +232,7 @@
"lit-html": "3.3.1",
"clean-css": "5.3.3",
"@lit/reactive-element": "2.1.1",
"@fullcalendar/daygrid": "6.1.19",
"@fullcalendar/daygrid": "6.1.18",
"globals": "16.3.0",
"tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20250730.0"
version = "20250731.0"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"

View File

@@ -237,11 +237,10 @@ export class HaAuthFlow extends LitElement {
@value-changed=${this._stepDataChanged}
></ha-auth-form>`
)}
<div class="space-between">
${this.clientId === genClientId() &&
!["select_mfa_module", "mfa"].includes(step.step_id)
? html`
${this.clientId === genClientId() &&
!["select_mfa_module", "mfa"].includes(step.step_id)
? html`
<div class="space-between">
<ha-formfield
class="store-token"
.label=${this.localize(
@@ -253,16 +252,18 @@ export class HaAuthFlow extends LitElement {
@change=${this._storeTokenChanged}
></ha-checkbox>
</ha-formfield>
`
: ""}
<a
class="forgot-password"
href="https://www.home-assistant.io/docs/locked_out/#forgot-password"
target="_blank"
rel="noreferrer noopener"
>${this.localize("ui.panel.page-authorize.forgot_password")}</a
>
</div>
<a
class="forgot-password"
href="https://www.home-assistant.io/docs/locked_out/#forgot-password"
target="_blank"
rel="noreferrer noopener"
>${this.localize(
"ui.panel.page-authorize.forgot_password"
)}</a
>
</div>
`
: ""}
`;
default:
return nothing;

View File

@@ -132,13 +132,15 @@ export const shiftDateRange = (
end = calcDate(endDate, addDays, locale, config, difference);
} else {
const difference =
(calcDateDifferenceProperty(
((calcDateDifferenceProperty(
endDate,
startDate,
differenceInMilliseconds,
locale,
config
) as number) * (forward ? 1 : -1);
) as number) +
1) *
(forward ? 1 : -1);
start = calcDate(startDate, addMilliseconds, locale, config, difference);
end = calcDate(endDate, addMilliseconds, locale, config, difference);
}

View File

@@ -1,4 +0,0 @@
export const preventDefaultStopPropagation = (ev) => {
ev.preventDefault();
ev.stopPropagation();
};

View File

@@ -22,8 +22,8 @@ export type LocalizeKeys =
| `ui.dialogs.more_info_control.lawn_mower.${string}`
| `ui.dialogs.more_info_control.vacuum.${string}`
| `ui.dialogs.quick-bar.commands.${string}`
| `ui.dialogs.unhealthy.reasons.${string}`
| `ui.dialogs.unsupported.reasons.${string}`
| `ui.dialogs.unhealthy.reason.${string}`
| `ui.dialogs.unsupported.reason.${string}`
| `ui.panel.config.${string}.${"caption" | "description"}`
| `ui.panel.config.dashboard.${string}`
| `ui.panel.config.zha.${string}`

View File

@@ -117,7 +117,7 @@ export class HaProgressButton extends LitElement {
}
ha-svg-icon {
color: var(--white-color);
color: var(--white);
}
`;
}

View File

@@ -29,6 +29,7 @@ import { formatTimeLabel } from "./axis-label";
import { ensureArray } from "../../common/array/ensure-array";
import "../chips/ha-assist-chip";
import { downSampleLineData } from "./down-sample";
import { colorVariables } from "../../resources/theme/color/color.globals";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
const LEGEND_OVERFLOW_LIMIT = 10;
@@ -167,16 +168,14 @@ export class HaChartBase extends LitElement {
}
protected firstUpdated() {
if (this.isConnected) {
this._setupChart();
}
this._setupChart();
}
public willUpdate(changedProps: PropertyValues): void {
if (!this.chart) {
return;
}
if (changedProps.has("_themes") && this.hasUpdated) {
if (changedProps.has("_themes")) {
this._setupChart();
return;
}
@@ -343,8 +342,7 @@ export class HaChartBase extends LitElement {
echarts.use(this.extraComponents);
}
const style = getComputedStyle(this);
echarts.registerTheme("custom", this._createTheme(style));
echarts.registerTheme("custom", this._createTheme());
this.chart = echarts.init(container, "custom");
this.chart.on("datazoom", (e: any) => {
@@ -387,25 +385,24 @@ export class HaChartBase extends LitElement {
lastTipX = e.x;
lastTipY = e.y;
this.chart?.setOption({
xAxis: ensureArray(
(this.chart?.getOption().xAxis as any) ?? []
).map((axis: XAXisOption) =>
axis.show
? {
...axis,
axisPointer: {
...axis.axisPointer,
status: "show",
handle: {
color: style.getPropertyValue("--primary-color"),
margin: 0,
size: 20,
...axis.axisPointer?.handle,
show: true,
xAxis: ensureArray(this.chart?.getOption().xAxis as any).map(
(axis: XAXisOption) =>
axis.show
? {
...axis,
axisPointer: {
...axis.axisPointer,
status: "show",
handle: {
color: colorVariables["primary-color"],
margin: 0,
size: 20,
...axis.axisPointer?.handle,
show: true,
},
},
},
}
: axis
}
: axis
),
});
});
@@ -418,22 +415,21 @@ export class HaChartBase extends LitElement {
return;
}
this.chart?.setOption({
xAxis: ensureArray(
(this.chart?.getOption().xAxis as any) ?? []
).map((axis: XAXisOption) =>
axis.show
? {
...axis,
axisPointer: {
...axis.axisPointer,
handle: {
...axis.axisPointer?.handle,
show: false,
xAxis: ensureArray(this.chart?.getOption().xAxis as any).map(
(axis: XAXisOption) =>
axis.show
? {
...axis,
axisPointer: {
...axis.axisPointer,
handle: {
...axis.axisPointer?.handle,
show: false,
},
status: "hide",
},
status: "hide",
},
}
: axis
}
: axis
),
});
this.chart?.dispatchAction({
@@ -572,7 +568,8 @@ export class HaChartBase extends LitElement {
return options;
}
private _createTheme(style: CSSStyleDeclaration) {
private _createTheme() {
const style = getComputedStyle(this);
return {
color: getAllGraphColors(style),
backgroundColor: "transparent",
@@ -600,13 +597,6 @@ export class HaChartBase extends LitElement {
textBorderWidth: 2,
},
},
sankey: {
label: {
color: style.getPropertyValue("--primary-text-color"),
textBorderColor: style.getPropertyValue("--primary-background-color"),
textBorderWidth: 2,
},
},
categoryAxis: {
axisLine: { show: false },
axisTick: { show: false },
@@ -812,7 +802,6 @@ export class HaChartBase extends LitElement {
};
}
}
const replaceMerge = options.series ? ["series"] : [];
this.chart.setOption(options, { replaceMerge });
}

View File

@@ -105,14 +105,12 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
}
protected render() {
if (!GraphChart || !this.data.nodes?.length) {
if (!GraphChart) {
return nothing;
}
const isMobile = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
return html`<ha-chart-base
.hass=${this.hass}
.data=${this._getSeries(
@@ -245,7 +243,6 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
) {
const containerWidth = this.clientWidth;
const containerHeight = this.clientHeight;
const positionedNodes: NetworkNode[] = nodes.map((node) => ({ ...node }));
positionedNodes.forEach((node) => {
if (nodePositions[node.id]) {

View File

@@ -186,22 +186,23 @@ export class HaSankeyChart extends LitElement {
""
);
const wordWidth = measureTextWidth(longestWord, FONT_SIZE);
const availableWidth = params.rect.width + 6;
const fontSize = Math.min(
FONT_SIZE,
(availableWidth / wordWidth) * FONT_SIZE
(params.rect.width / wordWidth) * FONT_SIZE
);
return {
fontSize: fontSize > 1 ? fontSize : 0,
width: availableWidth,
width: params.rect.width,
align: "center",
dy: -2, // shift up or the lowest row labels may be cut off
};
}
const availableHeight = params.rect.height + 8; // account for the margin
// estimate the number of lines after the label is wrapped
// this is a very rough estimate, but it works for now
const lineCount = Math.ceil(params.labelRect.width / labelSpace);
// `overflow: "break"` allows the label to overflow outside its height, so we need to account for that
const fontSize = Math.min(
(availableHeight / params.labelRect.height) * FONT_SIZE,
(params.rect.height / lineCount) * FONT_SIZE,
FONT_SIZE
);
return {

View File

@@ -10,8 +10,8 @@ import {
} from "../../data/device_automation";
import type { EntityRegistryEntry } from "../../data/entity_registry";
import type { HomeAssistant } from "../../types";
import "../ha-md-select-option";
import "../ha-md-select";
import "../ha-list-item";
import "../ha-select";
import { stopPropagation } from "../../common/dom/stop_propagation";
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
@@ -100,35 +100,35 @@ export abstract class HaDeviceAutomationPicker<
}
const value = this._value;
return html`
<ha-md-select
<ha-select
.label=${this.label}
.value=${value}
@change=${this._automationChanged}
@selected=${this._automationChanged}
@closed=${stopPropagation}
.disabled=${this._automations.length === 0}
>
${value === NO_AUTOMATION_KEY
? html`<ha-md-select-option .value=${NO_AUTOMATION_KEY}>
? html`<ha-list-item .value=${NO_AUTOMATION_KEY}>
${this.NO_AUTOMATION_TEXT}
</ha-md-select-option>`
: nothing}
</ha-list-item>`
: ""}
${value === UNKNOWN_AUTOMATION_KEY
? html`<ha-md-select-option .value=${UNKNOWN_AUTOMATION_KEY}>
? html`<ha-list-item .value=${UNKNOWN_AUTOMATION_KEY}>
${this.UNKNOWN_AUTOMATION_TEXT}
</ha-md-select-option>`
: nothing}
</ha-list-item>`
: ""}
${this._automations.map(
(automation, idx) => html`
<ha-md-select-option .value=${`${automation.device_id}_${idx}`}>
<ha-list-item .value=${`${automation.device_id}_${idx}`}>
${this._localizeDeviceAutomation(
this.hass,
this._entityReg,
automation
)}
</ha-md-select-option>
</ha-list-item>
`
)}
</ha-md-select>
</ha-select>
`;
}

View File

@@ -63,10 +63,10 @@ class HaEntityStatePicker extends LitElement {
const entityIds = this.entityId ? ensureArray(this.entityId) : [];
const entitiesOptions = entityIds.map<StateOption[]>((entityId) => {
const stateObj = this.hass.states[entityId] || {
entity_id: entityId,
attributes: {},
};
const stateObj = this.hass.states[entityId];
if (!stateObj) {
return [];
}
const states = getStates(this.hass, stateObj, this.attribute).filter(
(s) => !this.hideStates?.includes(s)

View File

@@ -1,149 +0,0 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../common/dom/fire_event";
import { ensureArray } from "../../common/array/ensure-array";
import type { HomeAssistant } from "../../types";
import "./ha-entity-state-picker";
@customElement("ha-entity-states-picker")
export class HaEntityStatesPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId?: string;
@property() public attribute?: string;
@property({ attribute: false }) public extraOptions?: any[];
@property({ type: Boolean, attribute: "allow-custom-value" })
public allowCustomValue;
@property() public label?: string;
@property({ type: Array }) public value?: string[];
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ attribute: false })
public hideStates?: string[];
private _keys: string[] = [];
private _getKey(index: number) {
if (!this._keys[index]) {
this._keys[index] = Math.random().toString();
}
return this._keys[index];
}
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("value")) {
this.value = ensureArray(this.value);
}
}
protected render() {
if (!this.hass) {
return nothing;
}
const value = this.value || [];
const hide = [...(this.hideStates || []), ...value];
return html`
${repeat(
value,
(_state, index) => this._getKey(index),
(state, index) => html`
<div>
<ha-entity-state-picker
.index=${index}
.hass=${this.hass}
.entityId=${this.entityId}
.attribute=${this.attribute}
.extraOptions=${this.extraOptions}
.hideStates=${hide.filter((v) => v !== state)}
.allowCustomValue=${this.allowCustomValue}
.label=${this.label}
.value=${state}
.disabled=${this.disabled}
.helper=${this.disabled && index === value.length - 1
? this.helper
: undefined}
@value-changed=${this._valueChanged}
></ha-entity-state-picker>
</div>
`
)}
<div>
${this.disabled && value.length
? nothing
: keyed(
value.length,
html`<ha-entity-state-picker
.hass=${this.hass}
.entityId=${this.entityId}
.attribute=${this.attribute}
.extraOptions=${this.extraOptions}
.hideStates=${hide}
.allowCustomValue=${this.allowCustomValue}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required && !value.length}
@value-changed=${this._addValue}
></ha-entity-state-picker>`
)}
</div>
`;
}
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
const newState = ev.detail.value;
const newValue = [...this.value!];
const index = (ev.currentTarget as any)?.index;
if (index == null) {
return;
}
if (newState === undefined) {
newValue.splice(index, 1);
this._keys.splice(index, 1);
fireEvent(this, "value-changed", {
value: newValue,
});
return;
}
newValue[index] = newState;
fireEvent(this, "value-changed", {
value: newValue,
});
}
private _addValue(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: [...(this.value || []), ev.detail.value],
});
}
static override styles = css`
div {
margin-top: 8px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-entity-states-picker": HaEntityStatesPicker;
}
}

View File

@@ -1,148 +0,0 @@
import { mdiChevronUp } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-icon-button";
@customElement("ha-automation-row")
export class HaAutomationRow extends LitElement {
@property({ attribute: "left-chevron", type: Boolean })
public leftChevron = false;
@property({ type: Boolean, reflect: true })
public collapsed = false;
@property({ type: Boolean, reflect: true })
public selected = false;
@property({ type: Boolean, reflect: true })
public disabled = false;
@property({ type: Boolean, reflect: true, attribute: "building-block" })
public buildingBlock = false;
protected render(): TemplateResult {
return html`
<div
class="row"
tabindex="0"
role="button"
@keydown=${this._handleKeydown}
>
${this.leftChevron
? html`
<ha-icon-button
class="expand-button"
.path=${mdiChevronUp}
@click=${this._handleExpand}
@keydown=${this._handleExpand}
></ha-icon-button>
`
: nothing}
<div class="leading-icon-wrapper">
<slot name="leading-icon"></slot>
</div>
<slot class="header" name="header"></slot>
<slot name="icons"></slot>
</div>
`;
}
private async _handleExpand(ev) {
if (ev.defaultPrevented) {
return;
}
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
return;
}
ev.stopPropagation();
ev.preventDefault();
fireEvent(this, "toggle-collapsed");
}
private async _handleKeydown(ev: KeyboardEvent): Promise<void> {
if (ev.defaultPrevented) {
return;
}
if (ev.key !== "Enter" && ev.key !== " ") {
return;
}
ev.preventDefault();
ev.stopPropagation();
this.click();
}
static styles = css`
:host {
display: block;
}
.row {
display: flex;
padding: 0 8px;
min-height: 48px;
align-items: center;
cursor: pointer;
overflow: hidden;
font-weight: var(--ha-font-weight-medium);
outline: none;
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
}
.row:focus {
outline: var(--wa-focus-ring);
outline-offset: -2px;
}
.expand-button {
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
color: var(--ha-color-on-neutral-quiet);
}
:host([building-block]) .leading-icon-wrapper {
background-color: var(--ha-color-fill-neutral-loud-resting);
border-radius: var(--ha-border-radius-md);
padding: 4px;
display: flex;
justify-content: center;
align-items: center;
transform: rotate(45deg);
}
::slotted([slot="leading-icon"]) {
color: var(--ha-color-on-neutral-quiet);
}
:host([building-block]) ::slotted([slot="leading-icon"]) {
--mdc-icon-size: 20px;
color: var(--white-color);
transform: rotate(-45deg);
}
:host([collapsed]) .expand-button {
transform: rotate(180deg);
}
:host([selected]) .row,
:host([selected]) .row:focus {
outline: solid;
outline-color: var(--primary-color);
outline-offset: -2px;
outline-width: 2px;
}
:host([disabled]) .row {
border-top-right-radius: var(--ha-border-radius-square);
border-top-left-radius: var(--ha-border-radius-square);
}
::slotted([slot="header"]) {
flex: 1;
overflow-wrap: anywhere;
margin: 0 12px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-row": HaAutomationRow;
}
interface HASSDomEvents {
"toggle-collapsed": undefined;
}
}

View File

@@ -27,7 +27,7 @@ export type Appearance = "accent" | "filled" | "outlined" | "plain";
* @csspart spinner - The spinner that shows when the button is in the loading state.
*
* @cssprop --ha-button-height - The height of the button.
* @cssprop --ha-button-border-radius - The border radius of the button. defaults to `var(--ha-border-radius-pill)`.
* @cssprop --ha-button-radius - The border radius of the button. defaults to `var(--wa-border-radius-pill)`.
*
* @attr {("small"|"medium")} size - Sets the button size.
* @attr {("brand"|"neutral"|"danger"|"warning"|"success")} variant - Sets the button color variant. "primary" is default.
@@ -55,9 +55,10 @@ export class HaButton extends Button {
/* set theme vars */
--wa-form-control-padding-inline: 16px;
--wa-font-weight-action: var(--ha-font-weight-medium);
--wa-border-radius-pill: 9999px;
--wa-form-control-border-radius: var(
--ha-button-border-radius,
var(--ha-border-radius-pill)
--ha-button-radius,
var(--wa-border-radius-pill)
);
--wa-form-control-height: var(
@@ -75,106 +76,64 @@ export class HaButton extends Button {
var(--button-height, 32px)
);
font-size: var(--wa-font-size-s, var(--ha-font-size-m));
--wa-form-control-padding-inline: 12px;
}
:host([variant="brand"]) {
--button-color-fill-normal-active: var(
--ha-color-fill-primary-normal-active
);
--button-color-fill-normal-hover: var(
--ha-color-fill-primary-normal-hover
);
--button-color-fill-loud-active: var(
--ha-color-fill-primary-loud-active
);
--button-color-fill-loud-hover: var(
--ha-color-fill-primary-loud-hover
);
--color-fill-normal-active: var(--color-fill-primary-normal-active);
--color-fill-normal-hover: var(--color-fill-primary-normal-hover);
--color-fill-loud-active: var(--color-fill-primary-loud-active);
--color-fill-loud-hover: var(--color-fill-primary-loud-hover);
}
:host([variant="neutral"]) {
--button-color-fill-normal-active: var(
--ha-color-fill-neutral-normal-active
);
--button-color-fill-normal-hover: var(
--ha-color-fill-neutral-normal-hover
);
--button-color-fill-loud-active: var(
--ha-color-fill-neutral-loud-active
);
--button-color-fill-loud-hover: var(
--ha-color-fill-neutral-loud-hover
);
--color-fill-normal-active: var(--color-fill-neutral-normal-active);
--color-fill-normal-hover: var(--color-fill-neutral-normal-hover);
--color-fill-loud-active: var(--color-fill-neutral-loud-active);
--color-fill-loud-hover: var(--color-fill-neutral-loud-hover);
}
:host([variant="success"]) {
--button-color-fill-normal-active: var(
--ha-color-fill-success-normal-active
);
--button-color-fill-normal-hover: var(
--ha-color-fill-success-normal-hover
);
--button-color-fill-loud-active: var(
--ha-color-fill-success-loud-active
);
--button-color-fill-loud-hover: var(
--ha-color-fill-success-loud-hover
);
--color-fill-normal-active: var(--color-fill-success-normal-active);
--color-fill-normal-hover: var(--color-fill-success-normal-hover);
--color-fill-loud-active: var(--color-fill-success-loud-active);
--color-fill-loud-hover: var(--color-fill-success-loud-hover);
}
:host([variant="warning"]) {
--button-color-fill-normal-active: var(
--ha-color-fill-warning-normal-active
);
--button-color-fill-normal-hover: var(
--ha-color-fill-warning-normal-hover
);
--button-color-fill-loud-active: var(
--ha-color-fill-warning-loud-active
);
--button-color-fill-loud-hover: var(
--ha-color-fill-warning-loud-hover
);
--color-fill-normal-active: var(--color-fill-warning-normal-active);
--color-fill-normal-hover: var(--color-fill-warning-normal-hover);
--color-fill-loud-active: var(--color-fill-warning-loud-active);
--color-fill-loud-hover: var(--color-fill-warning-loud-hover);
}
:host([variant="danger"]) {
--button-color-fill-normal-active: var(
--ha-color-fill-danger-normal-active
);
--button-color-fill-normal-hover: var(
--ha-color-fill-danger-normal-hover
);
--button-color-fill-loud-active: var(
--ha-color-fill-danger-loud-active
);
--button-color-fill-loud-hover: var(
--ha-color-fill-danger-loud-hover
);
--color-fill-normal-active: var(--color-fill-danger-normal-active);
--color-fill-normal-hover: var(--color-fill-danger-normal-hover);
--color-fill-loud-active: var(--color-fill-danger-loud-active);
--color-fill-loud-hover: var(--color-fill-danger-loud-hover);
}
:host([appearance~="plain"]) .button {
color: var(--wa-color-on-normal);
background-color: transparent;
}
:host([appearance~="plain"]) .button.disabled {
background-color: transparent;
color: var(--ha-color-on-disabled-quiet);
background-color: var(--transparent-none);
color: var(--color-on-disabled-quiet);
}
:host([appearance~="outlined"]) .button.disabled {
background-color: transparent;
color: var(--ha-color-on-disabled-quiet);
background-color: var(--transparent-none);
color: var(--color-on-disabled-quiet);
}
@media (hover: hover) {
:host([appearance~="filled"])
.button:not(.disabled):not(.loading):hover {
background-color: var(--button-color-fill-normal-hover);
background-color: var(--color-fill-normal-hover);
}
:host([appearance~="accent"])
.button:not(.disabled):not(.loading):hover {
background-color: var(--button-color-fill-loud-hover);
background-color: var(--color-fill-loud-hover);
}
:host([appearance~="plain"])
.button:not(.disabled):not(.loading):hover {
@@ -183,11 +142,11 @@ export class HaButton extends Button {
}
:host([appearance~="filled"])
.button:not(.disabled):not(.loading):active {
background-color: var(--button-color-fill-normal-active);
background-color: var(--color-fill-normal-active);
}
:host([appearance~="filled"]) .button.disabled {
background-color: var(--ha-color-fill-disabled-normal-resting);
color: var(--ha-color-on-disabled-normal);
background-color: var(--color-fill-disabled-normal-resting);
color: var(--color-on-disabled-normal);
}
:host([appearance~="accent"]) .button {
@@ -198,11 +157,11 @@ export class HaButton extends Button {
}
:host([appearance~="accent"])
.button:not(.disabled):not(.loading):active {
background-color: var(--button-color-fill-loud-active);
background-color: var(--color-fill-loud-active);
}
:host([appearance~="accent"]) .button.disabled {
background-color: var(--ha-color-fill-disabled-loud-resting);
color: var(--ha-color-on-disabled-loud);
background-color: var(--color-fill-disabled-loud-resting);
color: var(--color-on-disabled-loud);
}
:host([loading]) {
@@ -212,20 +171,6 @@ export class HaButton extends Button {
.button.disabled {
opacity: 1;
}
slot[name="start"]::slotted(*) {
margin-inline-end: 4px;
}
slot[name="end"]::slotted(*) {
margin-inline-start: 4px;
}
.button.has-start {
padding-left: 8px;
}
.button.has-end {
padding-right: 8px;
}
`,
];
}

View File

@@ -1,58 +0,0 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
export interface CompletionItem {
label: string;
value: string;
subValue?: string;
}
@customElement("ha-code-editor-completion-items")
export class HaCodeEditorCompletionItems extends LitElement {
@property({ attribute: false }) public items: CompletionItem[] = [];
render() {
return this.items.map(
(item) => html`
<span><strong>${item.label}</strong>:</span>
<span
>${item.value}${item.subValue && item.subValue.length > 0
? // prettier-ignore
html` (<pre>${item.subValue}</pre>)`
: nothing}</span
>
`
);
}
static styles = css`
:host {
display: grid;
grid-template-columns: auto 1fr;
gap: 6px;
white-space: pre-wrap;
flex-wrap: nowrap;
}
span {
display: flex;
align-items: center;
flex-flow: wrap;
word-wrap: break-word;
}
pre {
margin: 0 3px;
padding: 3px;
background-color: var(--markdown-code-background-color, none);
border-radius: var(--ha-border-radius-sm, 4px);
line-height: var(--ha-line-height-condensed);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-code-editor-completion-items": HaCodeEditorCompletionItems;
}
}

View File

@@ -1,7 +1,6 @@
import type {
Completion,
CompletionContext,
CompletionInfo,
CompletionResult,
CompletionSource,
} from "@codemirror/autocomplete";
@@ -10,17 +9,14 @@ import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
import { mdiArrowExpand, mdiArrowCollapse } from "@mdi/js";
import type { HassEntities } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, ReactiveElement, html, render } from "lit";
import { css, ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { getEntityContext } from "../common/entity/context/get_entity_context";
import type { HomeAssistant } from "../types";
import type { CompletionItem } from "./ha-code-editor-completion-items";
import "./ha-icon";
import "./ha-icon-button";
import "./ha-code-editor-completion-items";
declare global {
interface HASSDomEvents {
@@ -328,72 +324,15 @@ export class HaCodeEditor extends ReactiveElement {
}
};
private _renderInfo = (completion: Completion): CompletionInfo => {
const key = completion.label;
const context = getEntityContext(this.hass!.states[key], this.hass!);
const completionInfo = document.createElement("div");
completionInfo.classList.add("completion-info");
const formattedState = this.hass!.formatEntityState(this.hass!.states[key]);
const completionItems: CompletionItem[] = [
{
label: this.hass!.localize(
"ui.components.entity.entity-state-picker.state"
),
value: formattedState,
subValue:
// If the state exactly matches the formatted state, don't show the raw state
this.hass!.states[key].state === formattedState
? undefined
: this.hass!.states[key].state,
},
];
if (context.device && context.device.name) {
completionItems.push({
label: this.hass!.localize("ui.components.device-picker.device"),
value: context.device.name,
});
}
if (context.area && context.area.name) {
completionItems.push({
label: this.hass!.localize("ui.components.area-picker.area"),
value: context.area.name,
});
}
if (context.floor && context.floor.name) {
completionItems.push({
label: this.hass!.localize("ui.components.floor-picker.floor"),
value: context.floor.name,
});
}
render(
html`
<ha-code-editor-completion-items
.items=${completionItems}
></ha-code-editor-completion-items>
`,
completionInfo
);
return completionInfo;
};
private _getStates = memoizeOne((states: HassEntities): Completion[] => {
if (!states) {
return [];
}
const options = Object.keys(states).map((key) => ({
type: "variable",
label: key,
detail: states[key].attributes.friendly_name,
info: this._renderInfo,
info: `State: ${states[key].state}`,
}));
return options;
@@ -676,20 +615,6 @@ export class HaCodeEditor extends ReactiveElement {
top: calc(var(--safe-area-inset-top, 0px) + 8px);
right: calc(var(--safe-area-inset-right, 0px) + 8px);
}
.completion-info {
display: grid;
gap: 3px;
padding: 8px;
}
/* Hide completion info on narrow screens */
@media (max-width: 600px) {
.cm-completionInfo,
.completion-info {
display: none;
}
}
`;
}

View File

@@ -188,7 +188,7 @@ export class HaComboBox extends LitElement {
class="input"
autocapitalize="none"
autocomplete="off"
.autocorrect=${false}
autocorrect="off"
input-spellcheck="false"
.suffix=${html`<div
style="width: 28px;"
@@ -207,7 +207,6 @@ export class HaComboBox extends LitElement {
aria-label=${ifDefined(this.hass?.localize("ui.common.clear"))}
class=${`clear-button ${this.label ? "" : "no-label"}`}
.path=${mdiClose}
?disabled=${this.disabled}
@click=${this._clearValue}
></ha-svg-icon>`
: ""}
@@ -394,8 +393,7 @@ export class HaComboBox extends LitElement {
:host([opened]) .toggle-button {
color: var(--primary-color);
}
.toggle-button[disabled],
.clear-button[disabled] {
.toggle-button[disabled] {
color: var(--disabled-text-color);
pointer-events: none;
}

View File

@@ -506,7 +506,7 @@ export class HaControlSlider extends LitElement {
width: 100%;
}
.slider .slider-track-bar {
--ha-border-radius: var(--control-slider-border-radius);
--border-radius: var(--control-slider-border-radius);
--slider-size: 100%;
position: absolute;
height: 100%;

View File

@@ -5,8 +5,8 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { debounce } from "../common/util/debounce";
import type { ConfigEntry, SubEntry } from "../data/config_entries";
import { getConfigEntry, getSubEntries } from "../data/config_entries";
import type { ConfigEntry } from "../data/config_entries";
import { getConfigEntry } from "../data/config_entries";
import type { Agent } from "../data/conversation";
import { listAgents } from "../data/conversation";
import { fetchIntegrationManifest } from "../data/integration";
@@ -16,7 +16,6 @@ import "./ha-list-item";
import "./ha-select";
import type { HaSelect } from "./ha-select";
import { getExtendedEntityRegistryEntry } from "../data/entity_registry";
import { showSubConfigFlowDialog } from "../dialogs/config-flow/show-dialog-sub-config-flow";
const NONE = "__NONE_OPTION__";
@@ -38,8 +37,6 @@ export class HaConversationAgentPicker extends LitElement {
@state() private _configEntry?: ConfigEntry;
@state() private _subConfigEntry?: SubEntry;
protected render() {
if (!this._agents) {
return nothing;
@@ -104,11 +101,7 @@ export class HaConversationAgentPicker extends LitElement {
${agent.name}
</ha-list-item>`
)}</ha-select
>${(this._subConfigEntry &&
this._configEntry?.supported_subentry_types[
this._subConfigEntry.subentry_type
]?.supports_reconfigure) ||
this._configEntry?.supports_options
>${this._configEntry?.supports_options
? html`<ha-icon-button
.path=${mdiCog}
@click=${this._openOptionsFlow}
@@ -149,17 +142,8 @@ export class HaConversationAgentPicker extends LitElement {
this._configEntry = (
await getConfigEntry(this.hass, regEntry.config_entry_id)
).config_entry;
if (!regEntry.config_subentry_id) {
this._subConfigEntry = undefined;
} else {
this._subConfigEntry = (
await getSubEntries(this.hass, regEntry.config_entry_id)
).find((entry) => entry.subentry_id === regEntry.config_subentry_id);
}
} catch (_err) {
this._configEntry = undefined;
this._subConfigEntry = undefined;
}
}
@@ -198,25 +182,6 @@ export class HaConversationAgentPicker extends LitElement {
if (!this._configEntry) {
return;
}
if (
this._subConfigEntry &&
this._configEntry.supported_subentry_types[
this._subConfigEntry.subentry_type
]?.supports_reconfigure
) {
showSubConfigFlowDialog(
this,
this._configEntry,
this._subConfigEntry.subentry_type,
{
startFlowHandler: this._configEntry.entry_id,
subEntryId: this._subConfigEntry.subentry_id,
}
);
return;
}
showOptionsFlowDialog(this, this._configEntry, {
manifest: await fetchIntegrationManifest(
this.hass,

View File

@@ -254,37 +254,21 @@ export class HaDateRangePicker extends LitElement {
}
private _applyDateRange() {
let start = new Date(this._dateRangePicker.start);
let end = new Date(this._dateRangePicker.end);
if (this.timePicker) {
start.setSeconds(0);
start.setMilliseconds(0);
end.setSeconds(0);
end.setMilliseconds(0);
if (
end.getHours() === 0 &&
end.getMinutes() === 0 &&
start.getFullYear() === end.getFullYear() &&
start.getMonth() === end.getMonth() &&
start.getDate() === end.getDate()
) {
end.setDate(end.getDate() + 1);
}
}
if (this.hass.locale.time_zone === TimeZone.server) {
start = fromZonedTime(start, this.hass.config.time_zone);
end = fromZonedTime(end, this.hass.config.time_zone);
const dateRangePicker = this._dateRangePicker;
const startDate = fromZonedTime(
dateRangePicker.start,
this.hass.config.time_zone
);
const endDate = fromZonedTime(
dateRangePicker.end,
this.hass.config.time_zone
);
dateRangePicker.clickRange([startDate, endDate]);
}
if (
start.getTime() !== this._dateRangePicker.start.getTime() ||
end.getTime() !== this._dateRangePicker.end.getTime()
) {
this._dateRangePicker.clickRange([start, end]);
}
this._dateRangePicker.clickedApply();
}

View File

@@ -20,18 +20,6 @@ export class HaFab extends FabBase {
--mdc-typography-button-font-family: var(--ha-font-family-body);
--mdc-typography-button-font-weight: var(--ha-font-weight-medium);
}
:host .mdc-fab--extended {
border-radius: var(
--ha-button-border-radius,
var(--ha-border-radius-pill)
);
}
:host .mdc-fab.mdc-fab--extended .ripple {
border-radius: var(
--ha-button-border-radius,
var(--ha-border-radius-pill)
);
}
:host .mdc-fab--extended .mdc-fab__icon {
margin-inline-start: -8px;
margin-inline-end: 12px;

View File

@@ -106,8 +106,6 @@ export const computeInitialHaFormData = (
data[field.name] = [];
} else if ("media" in selector || "target" in selector) {
data[field.name] = {};
} else if ("state" in selector) {
data[field.name] = selector.state?.multiple ? [] : "";
} else {
throw new Error(
`Selector ${Object.keys(selector)[0]} not supported in initial form data`

View File

@@ -7,8 +7,8 @@ import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-button";
import "./ha-icon-button";
import "./ha-input-helper-text";
import "./ha-textfield";
import "./ha-input-helper-text";
import type { HaTextField } from "./ha-textfield";
@customElement("ha-multi-textfield")
@@ -79,7 +79,6 @@ class HaMultiTextField extends LitElement {
@click=${this._addItem}
.disabled=${this.disabled}
>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
${this.addLabel ??
(this.label
? this.hass?.localize("ui.components.multi-textfield.add_item", {
@@ -87,6 +86,7 @@ class HaMultiTextField extends LitElement {
})
: this.hass?.localize("ui.common.add")) ??
"Add"}
<ha-svg-icon slot="end" .path=${mdiPlus}></ha-svg-icon>
</ha-button>
</div>
${this.helper

View File

@@ -28,7 +28,7 @@ export class HaPasswordField extends LitElement {
@property() public autocomplete?: string;
@property({ type: Boolean }) public autocorrect = true;
@property() public autocorrect?: string;
@property({ attribute: "input-spellcheck" })
public inputSpellcheck?: string;

View File

@@ -93,7 +93,7 @@ class HaPushNotificationsToggle extends LitElement {
return;
}
let applicationServerKey: Uint8Array<ArrayBuffer> | null;
let applicationServerKey: Uint8Array | null;
try {
applicationServerKey = await getAppKey(this.hass);
} catch (_err) {

View File

@@ -1,19 +1,19 @@
import { consume, ContextProvider } from "@lit/context";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { ContextProvider, consume } from "@lit/context";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { fullEntitiesContext } from "../../data/context";
import type { Action } from "../../data/script";
import { migrateAutomationAction } from "../../data/script";
import type { ActionSelector } from "../../data/selector";
import "../../panels/config/automation/action/ha-automation-action";
import type { HomeAssistant } from "../../types";
import {
subscribeEntityRegistry,
type EntityRegistryEntry,
} from "../../data/entity_registry";
import type { Action } from "../../data/script";
import { migrateAutomationAction } from "../../data/script";
import type { ActionSelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import "../../panels/config/automation/action/ha-automation-action";
import type { HomeAssistant } from "../../types";
@customElement("ha-selector-action")
export class HaActionSelector extends SubscribeMixin(LitElement) {
@@ -69,7 +69,6 @@ export class HaActionSelector extends SubscribeMixin(LitElement) {
.actions=${this._actions(this.value)}
.hass=${this.hass}
.narrow=${this.narrow}
.optionsInSidebar=${!!this.selector.action?.optionsInSidebar}
></ha-automation-action>
`;
}

View File

@@ -27,7 +27,6 @@ export class HaConditionSelector extends LitElement {
.conditions=${this.value || []}
.hass=${this.hass}
.narrow=${this.narrow}
.optionsInSidebar=${!!this.selector.condition?.optionsInSidebar}
></ha-automation-condition>
`;
}

View File

@@ -121,10 +121,6 @@ const SELECTOR_SCHEMAS = {
name: "entity_id",
selector: { entity: {} },
},
{
name: "multiple",
selector: { boolean: {} },
},
] as const,
target: [] as const,
template: [] as const,

View File

@@ -4,7 +4,6 @@ import type { StateSelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../types";
import "../entity/ha-entity-state-picker";
import "../entity/ha-entity-states-picker";
@customElement("ha-selector-state")
export class HaSelectorState extends SubscribeMixin(LitElement) {
@@ -28,25 +27,6 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
};
protected render() {
if (this.selector.state?.multiple) {
return html`
<ha-entity-states-picker
.hass=${this.hass}
.entityId=${this.selector.state?.entity_id ||
this.context?.filter_entity}
.attribute=${this.selector.state?.attribute ||
this.context?.filter_attribute}
.extraOptions=${this.selector.state?.extra_options}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-value
.hideStates=${this.selector.state?.hide_states}
></ha-entity-states-picker>
`;
}
return html`
<ha-entity-state-picker
.hass=${this.hass}

View File

@@ -1,10 +1,9 @@
import Spinner from "@awesome.me/webawesome/dist/components/spinner/spinner";
import type { CSSResultGroup, PropertyValues } from "lit";
import Spinner from "@shoelace-style/shoelace/dist/components/spinner/spinner.component";
import spinnerStyles from "@shoelace-style/shoelace/dist/components/spinner/spinner.styles";
import type { PropertyValues } from "lit";
import { css } from "lit";
import { customElement, property } from "lit/decorators";
import { StateSet } from "../resources/polyfills/stateset";
@customElement("ha-spinner")
export class HaSpinner extends Spinner {
@property() public size?: "tiny" | "small" | "medium" | "large";
@@ -33,31 +32,21 @@ export class HaSpinner extends Spinner {
}
}
attachInternals() {
const internals = super.attachInternals();
Object.defineProperty(internals, "states", {
value: new StateSet(this, internals.states),
});
return internals;
}
static get styles(): CSSResultGroup {
return [
Spinner.styles,
css`
:host {
--indicator-color: var(
--ha-spinner-indicator-color,
var(--primary-color)
);
--track-color: var(--ha-spinner-divider-color, var(--divider-color));
--track-width: 4px;
--speed: 3.5s;
font-size: var(--ha-spinner-size, 48px);
}
`,
];
}
static override styles = [
spinnerStyles,
css`
:host {
--indicator-color: var(
--ha-spinner-indicator-color,
var(--primary-color)
);
--track-color: var(--ha-spinner-divider-color, var(--divider-color));
--track-width: 4px;
--speed: 3.5s;
font-size: var(--ha-spinner-size, 48px);
}
`,
];
}
declare global {

View File

@@ -20,7 +20,7 @@ export class HaTextField extends TextFieldBase {
@property() public autocomplete?: string;
@property({ type: Boolean }) public autocorrect = true;
@property() public autocorrect?: string;
@property({ attribute: "input-spellcheck" })
public inputSpellcheck?: string;
@@ -57,8 +57,8 @@ export class HaTextField extends TextFieldBase {
}
}
if (changedProperties.has("autocorrect")) {
if (this.autocorrect === false) {
this.formElement.setAttribute("autocorrect", "off");
if (this.autocorrect) {
this.formElement.setAttribute("autocorrect", this.autocorrect);
} else {
this.formElement.removeAttribute("autocorrect");
}

View File

@@ -211,7 +211,6 @@ export class HaYamlEditor extends LitElement {
}
ha-code-editor {
flex-grow: 1;
min-height: 0;
}
`,
];

View File

@@ -116,7 +116,7 @@ class DialogMediaManage extends LitElement {
`
: html`
<ha-button
variant="danger"
class="danger"
slot="navigationIcon"
.disabled=${this._deleting}
@click=${this._handleDelete}
@@ -327,6 +327,10 @@ class DialogMediaManage extends LitElement {
display: block;
}
.danger {
--mdc-theme-primary: var(--error-color);
}
ha-tip {
margin: 16px;
}

View File

@@ -18,9 +18,9 @@ import { fireEvent } from "../../common/dom/fire_event";
import { debounce } from "../../common/util/debounce";
import { isUnavailableState } from "../../data/entity";
import type {
MediaPlayerItem,
MediaPickedEvent,
MediaPlayerBrowseAction,
MediaPlayerItem,
MediaPlayerLayoutType,
} from "../../data/media-player";
import {
@@ -32,7 +32,6 @@ import { browseLocalMediaPlayer } from "../../data/media_source";
import { isTTSMediaSource } from "../../data/tts";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
import {
brandsUrl,
@@ -45,15 +44,16 @@ import "../ha-alert";
import "../ha-button";
import "../ha-button-menu";
import "../ha-card";
import "../ha-spinner";
import "../ha-fab";
import "../ha-icon-button";
import "../ha-list";
import "../ha-list-item";
import "../ha-spinner";
import "../ha-svg-icon";
import "../ha-tooltip";
import "../ha-list";
import "../ha-list-item";
import "./ha-browse-media-tts";
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
import { loadVirtualizer } from "../../resources/virtualizer";
declare global {
interface HASSDomEvents {

View File

@@ -30,10 +30,6 @@ export const ACTION_ICONS = {
wait_template: mdiCodeBraces,
wait_for_trigger: mdiTrafficLight,
repeat: mdiRefresh,
repeat_count: mdiRefresh,
repeat_while: mdiRefresh,
repeat_until: mdiRefresh,
repeat_for_each: mdiRefresh,
choose: mdiArrowDecision,
if: mdiCallSplit,
device_id: mdiDevices,
@@ -61,10 +57,7 @@ export const ACTION_GROUPS: AutomationElementGroup = {
delay: {},
wait_template: {},
wait_for_trigger: {},
repeat_count: {},
repeat_while: {},
repeat_until: {},
repeat_for_each: {},
repeat: {},
choose: {},
if: {},
stop: {},
@@ -90,19 +83,3 @@ export const isService = (key: string | undefined): boolean | undefined =>
export const getService = (key: string): string =>
key.substring(SERVICE_PREFIX.length);
export const ACTION_BUILDING_BLOCKS = [
"choose",
"if",
"parallel",
"sequence",
"repeat_while",
"repeat_until",
];
// Building blocks that have options in the sidebar
export const ACTION_COMBINED_BLOCKS = [
"repeat_count", // virtual repeat variant
"repeat_for_each", // virtual repeat variant
"wait_for_trigger",
];

View File

@@ -2,15 +2,14 @@ import type {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { ensureArray } from "../common/array/ensure-array";
import { navigate } from "../common/navigate";
import { createSearchParam } from "../common/url/search-params";
import { ensureArray } from "../common/array/ensure-array";
import type { Context, HomeAssistant } from "../types";
import type { BlueprintInput } from "./blueprint";
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
import type { Action, MODES } from "./script";
import { migrateAutomationAction } from "./script";
import { CONDITION_BUILDING_BLOCKS } from "./condition";
import { createSearchParam } from "../common/url/search-params";
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
export const AUTOMATION_DEFAULT_MAX = 10;
@@ -326,7 +325,7 @@ export const expandConditionWithShorthand = (
};
}
for (const condition of CONDITION_BUILDING_BLOCKS) {
for (const condition of ["and", "or", "not"]) {
if (condition in cond) {
return {
condition,

View File

@@ -50,5 +50,3 @@ export const CONDITION_GROUPS: AutomationElementGroup = {
},
},
} as const;
export const CONDITION_BUILDING_BLOCKS = ["and", "or", "not"];

View File

@@ -34,8 +34,6 @@ export interface FanEntity extends HassEntityBase {
attributes: FanEntityAttributes;
}
export type FanDirection = "forward" | "reverse";
export type FanSpeed = "off" | "low" | "medium" | "high" | "on";
export const FAN_SPEEDS: Partial<Record<number, FanSpeed[]>> = {

View File

@@ -8,7 +8,7 @@ export interface HassioHostInfo {
chassis: string;
cpe: string;
deployment: string;
disk_life_time: number | null;
disk_life_time: number | "";
disk_free: number;
disk_total: number;
disk_used: number;

View File

@@ -1,6 +1,6 @@
import type { HomeAssistant } from "../types";
function urlBase64ToUint8Array(base64String: string) {
function urlBase64ToUint8Array(base64String) {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");

View File

@@ -1,10 +1,7 @@
import type {
HassEntity,
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import type { HomeAssistant } from "../types";
export interface BasePerson {
@@ -70,28 +67,3 @@ export const deletePerson = (hass: HomeAssistant, personId: string) =>
type: "person/delete",
person_id: personId,
});
const cachedUserPerson: Record<string, string> = {};
export const getUserPerson = (hass: HomeAssistant): undefined | HassEntity => {
if (!hass.user?.id) {
return undefined;
}
const cachedPersonEntityId = cachedUserPerson[hass.user.id];
if (cachedPersonEntityId) {
const stateObj = hass.states[cachedPersonEntityId];
if (stateObj && stateObj.attributes.user_id === hass.user.id) {
return stateObj;
}
}
const result = Object.values(hass.states).find(
(state) =>
state.attributes.user_id === hass.user!.id &&
computeStateDomain(state) === "person"
);
if (result) {
cachedUserPerson[hass.user.id] = result.entity_id;
}
return result;
};

View File

@@ -74,9 +74,7 @@ export type Selector =
| BackupLocationSelector;
export interface ActionSelector {
action: {
optionsInSidebar?: boolean;
} | null;
action: {} | null;
}
export interface AddonSelector {
@@ -132,9 +130,7 @@ export interface ColorTempSelector {
}
export interface ConditionSelector {
condition: {
optionsInSidebar?: boolean;
} | null;
condition: {} | null;
}
export interface ConversationAgentSelector {
@@ -401,7 +397,6 @@ export interface StateSelector {
entity_id?: string | string[];
attribute?: string;
hide_states?: string[];
multiple?: boolean;
} | null;
}

View File

@@ -4,7 +4,7 @@ import type {
HassEntityBase,
HassEvent,
} from "home-assistant-js-websocket";
import { BINARY_STATE_ON, BINARY_STATE_OFF } from "../common/const";
import { BINARY_STATE_ON } from "../common/const";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { supportsFeature } from "../common/entity/supports-feature";
@@ -52,15 +52,6 @@ export const updateCanInstall = (
(showSkipped && Boolean(entity.attributes.skipped_version))) &&
supportsFeature(entity, UpdateEntityFeature.INSTALL);
export const latestVersionIsSkipped = (entity: UpdateEntity): boolean =>
!!(
entity.attributes.latest_version &&
entity.attributes.skipped_version === entity.attributes.latest_version
);
export const updateButtonIsDisabled = (entity: UpdateEntity): boolean =>
entity.state === BINARY_STATE_OFF && !latestVersionIsSkipped(entity);
export const updateIsInstalling = (entity: UpdateEntity): boolean =>
!!entity.attributes.in_progress;

View File

@@ -7,7 +7,6 @@ import { relativeTime } from "../../../common/datetime/relative_time";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/buttons/ha-progress-button";
import "../../../components/ha-checkbox";
import "../../../components/ha-faded";
import "../../../components/ha-markdown";
@@ -27,8 +26,6 @@ import {
UpdateEntityFeature,
updateIsInstalling,
updateReleaseNotes,
latestVersionIsSkipped,
updateButtonIsDisabled,
} from "../../../data/update";
import type { HomeAssistant } from "../../../types";
import { showAlertDialog } from "../../generic/show-dialog-box";
@@ -183,6 +180,11 @@ class MoreInfoUpdate extends LitElement {
return nothing;
}
const skippedVersion =
this.stateObj.attributes.latest_version &&
this.stateObj.attributes.skipped_version ===
this.stateObj.attributes.latest_version;
const createBackupTexts = this._computeCreateBackupTexts();
return html`
@@ -249,17 +251,15 @@ class MoreInfoUpdate extends LitElement {
<hr />
${this._markdownLoading ? this._renderLoader() : nothing}
`
: this._releaseNotes
? html`
<hr />
<ha-markdown
@content-resize=${this._markdownLoaded}
.content=${this._releaseNotes}
class=${this._markdownLoading ? "hidden" : ""}
></ha-markdown>
${this._markdownLoading ? this._renderLoader() : nothing}
`
: nothing
: html`
<hr />
<ha-markdown
@content-resize=${this._markdownLoaded}
.content=${this._releaseNotes}
class=${this._markdownLoading ? "hidden" : ""}
></ha-markdown>
${this._markdownLoading ? this._renderLoader() : nothing}
`
: this.stateObj.attributes.release_summary
? html`
<hr />
@@ -312,7 +312,7 @@ class MoreInfoUpdate extends LitElement {
<ha-button
appearance="plain"
@click=${this._handleSkip}
.disabled=${latestVersionIsSkipped(this.stateObj) ||
.disabled=${skippedVersion ||
this.stateObj.state === BINARY_STATE_OFF ||
updateIsInstalling(this.stateObj)}
>
@@ -325,8 +325,9 @@ class MoreInfoUpdate extends LitElement {
? html`
<ha-button
@click=${this._handleInstall}
.loading=${updateIsInstalling(this.stateObj)}
.disabled=${updateButtonIsDisabled(this.stateObj)}
.disabled=${(this.stateObj.state === BINARY_STATE_OFF &&
!skippedVersion) ||
updateIsInstalling(this.stateObj)}
>
${this.hass.localize(
"ui.dialogs.more_info_control.update.update"

View File

@@ -144,12 +144,10 @@ export class MoreInfoDialog extends LitElement {
public closeDialog() {
this._entityId = undefined;
this._parentEntityIds = [];
this._entry = undefined;
this._childView = undefined;
this._infoEditMode = false;
this._initialView = DEFAULT_VIEW;
this._currView = DEFAULT_VIEW;
this._childView = undefined;
this._isEscapeEnabled = true;
window.removeEventListener("dialog-closed", this._enableEscapeKeyClose);
window.removeEventListener("show-dialog", this._disableEscapeKeyClose);
@@ -529,69 +527,67 @@ export class MoreInfoDialog extends LitElement {
`
: nothing}
</ha-dialog-header>
${keyed(
this._entityId,
html`
<div
class="content"
tabindex="-1"
dialogInitialFocus
@show-child-view=${this._showChildView}
@entity-entry-updated=${this._entryUpdated}
@toggle-edit-mode=${this._handleToggleInfoEditModeEvent}
@hass-more-info=${this._handleMoreInfoEvent}
>
${cache(
this._childView
<div
class="content"
tabindex="-1"
dialogInitialFocus
@show-child-view=${this._showChildView}
@entity-entry-updated=${this._entryUpdated}
@toggle-edit-mode=${this._handleToggleInfoEditModeEvent}
@hass-more-info=${this._handleMoreInfoEvent}
>
${keyed(
this._entityId,
cache(
this._childView
? html`
<div class="child-view">
${dynamicElement(this._childView.viewTag, {
hass: this.hass,
entry: this._entry,
params: this._childView.viewParams,
})}
</div>
`
: this._currView === "info"
? html`
<div class="child-view">
${dynamicElement(this._childView.viewTag, {
hass: this.hass,
entry: this._entry,
params: this._childView.viewParams,
})}
</div>
<ha-more-info-info
dialogInitialFocus
.hass=${this.hass}
.entityId=${this._entityId}
.entry=${this._entry}
.editMode=${this._infoEditMode}
></ha-more-info-info>
`
: this._currView === "info"
: this._currView === "history"
? html`
<ha-more-info-info
dialogInitialFocus
<ha-more-info-history-and-logbook
.hass=${this.hass}
.entityId=${this._entityId}
.entry=${this._entry}
.editMode=${this._infoEditMode}
></ha-more-info-info>
></ha-more-info-history-and-logbook>
`
: this._currView === "history"
: this._currView === "settings"
? html`
<ha-more-info-history-and-logbook
<ha-more-info-settings
.hass=${this.hass}
.entityId=${this._entityId}
></ha-more-info-history-and-logbook>
.entry=${this._entry}
></ha-more-info-settings>
`
: this._currView === "settings"
: this._currView === "related"
? html`
<ha-more-info-settings
<ha-related-items
.hass=${this.hass}
.entityId=${this._entityId}
.entry=${this._entry}
></ha-more-info-settings>
.itemId=${entityId}
.itemType=${SearchableDomains.has(domain)
? (domain as ItemType)
: "entity"}
></ha-related-items>
`
: this._currView === "related"
? html`
<ha-related-items
.hass=${this.hass}
.itemId=${entityId}
.itemType=${SearchableDomains.has(domain)
? (domain as ItemType)
: "entity"}
></ha-related-items>
`
: nothing
)}
</div>
`
)}
: nothing
)
)}
</div>
</ha-dialog>
`;
}

View File

@@ -72,40 +72,31 @@ class MoreInfoContent extends LitElement {
return (
stateObj.attributes &&
stateObj.attributes.entity_id &&
Array.isArray(stateObj.attributes.entity_id) &&
stateObj.attributes.entity_id.some(
(entityId: string) => !this.hass!.entities[entityId]?.hidden
)
Array.isArray(stateObj.attributes.entity_id)
);
}
private _entitiesSectionConfig = memoizeOne((entityIds: string[]) => {
const cards = entityIds
.map((entityId) => {
const entity = this.hass!.entities[entityId];
if (entity?.hidden) {
return null;
}
const features: LovelaceCardFeatureConfig[] = [];
const context = { entity_id: entityId };
if (supportsCoverPositionCardFeature(this.hass!, context)) {
features.push({
type: "cover-position",
});
} else if (supportsLightBrightnessCardFeature(this.hass!, context)) {
features.push({
type: "light-brightness",
});
}
return {
type: "tile",
entity: entityId,
features_position: "inline",
features,
grid_options: { columns: 12 },
} as TileCardConfig;
})
.filter(Boolean);
const cards = entityIds.map((entityId) => {
const features: LovelaceCardFeatureConfig[] = [];
const context = { entity_id: entityId };
if (supportsCoverPositionCardFeature(this.hass!, context)) {
features.push({
type: "cover-position",
});
} else if (supportsLightBrightnessCardFeature(this.hass!, context)) {
features.push({
type: "light-brightness",
});
}
return {
type: "tile",
entity: entityId,
features_position: "inline",
features,
grid_options: { columns: 12 },
} as TileCardConfig;
});
return {
type: "grid",
cards,

View File

@@ -44,7 +44,7 @@
}
#ha-launch-screen .ha-launch-screen-spacer-top {
flex: 1;
margin-top: calc( 2 * max(var(--safe-area-inset-bottom, 0px), 48px) + 46px );
margin-top: calc( 2 * max(var(--safe-area-inset-bottom), 48px) + 46px );
padding-top: 48px;
}
#ha-launch-screen .ha-launch-screen-spacer-bottom {
@@ -52,7 +52,7 @@
padding-top: 48px;
}
.ohf-logo {
margin: max(var(--safe-area-inset-bottom, 0px), 48px) 0;
margin: max(var(--safe-area-inset-bottom), 48px) 0;
display: flex;
flex-direction: column;
align-items: center;

View File

@@ -69,7 +69,6 @@ class HassTabsSubpage extends LitElement {
activeTab: PageNavigation | undefined,
_components,
_language,
_userData,
_narrow,
localizeFunc
) => {
@@ -124,7 +123,6 @@ class HassTabsSubpage extends LitElement {
this._activeTab,
this.hass.config.components,
this.hass.language,
this.hass.userData,
this.narrow,
this.localizeFunc || this.hass.localize
);

View File

@@ -1,19 +1,16 @@
import { mdiClose } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { property, query, state } from "lit/decorators";
import type { LocalizeKeys } from "../common/translations/localize";
import "../components/ha-button";
import "../components/ha-icon-button";
import "../components/ha-toast";
import "../components/ha-icon-button";
import type { HaToast } from "../components/ha-toast";
import type { HomeAssistant } from "../types";
export interface ShowToastParams {
// Unique ID for the toast. If a new toast is shown with the same ID as the previous toast, it will be replaced to avoid flickering.
id?: string;
message:
| string
| { translationKey: LocalizeKeys; args?: Record<string, string> };
message: string;
action?: ToastActionParams;
duration?: number;
dismissable?: boolean;
@@ -21,9 +18,7 @@ export interface ShowToastParams {
export interface ToastActionParams {
action: () => void;
text:
| string
| { translationKey: LocalizeKeys; args?: Record<string, string> };
text: string;
}
class NotificationManager extends LitElement {
@@ -67,12 +62,7 @@ class NotificationManager extends LitElement {
return html`
<ha-toast
leading
.labelText=${typeof this._parameters.message !== "string"
? this.hass.localize(
this._parameters.message.translationKey,
this._parameters.message.args
)
: this._parameters.message}
.labelText=${this._parameters.message}
.timeoutMs=${this._parameters.duration!}
@MDCSnackbar:closed=${this._toastClosed}
>
@@ -84,12 +74,7 @@ class NotificationManager extends LitElement {
slot="action"
@click=${this._buttonClicked}
>
${typeof this._parameters?.action.text !== "string"
? this.hass.localize(
this._parameters?.action.text.translationKey,
this._parameters?.action.text.args
)
: this._parameters?.action.text}
${this._parameters?.action.text}
</ha-button>
`
: nothing}

View File

@@ -1,34 +0,0 @@
import type { LitElement } from "lit";
import { state } from "lit/decorators";
import type { Constructor } from "../types";
import { isMobileClient } from "../util/is_mobile";
import { listenMediaQuery } from "../common/dom/media_query";
export const MobileAwareMixin = <T extends Constructor<LitElement>>(
superClass: T
) => {
class MobileAwareClass extends superClass {
@state() protected _isMobileSize = false;
protected _isMobileClient = isMobileClient;
private _unsubMql?: () => void;
public connectedCallback() {
super.connectedCallback();
this._unsubMql = listenMediaQuery(
"all and (max-width: 450px), all and (max-height: 500px)",
(matches) => {
this._isMobileSize = matches;
}
);
}
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubMql?.();
this._unsubMql = undefined;
}
}
return MobileAwareClass;
};

View File

@@ -1,3 +1,4 @@
import { formatInTimeZone, toDate } from "date-fns-tz";
import {
addDays,
addHours,
@@ -5,7 +6,6 @@ import {
differenceInMilliseconds,
startOfHour,
} from "date-fns";
import { formatInTimeZone, toDate } from "date-fns-tz";
import type { HassEntity } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
@@ -18,11 +18,11 @@ import { supportsFeature } from "../../common/entity/supports-feature";
import { isDate } from "../../common/string/is_date";
import "../../components/entity/ha-entity-picker";
import "../../components/ha-alert";
import "../../components/ha-button";
import "../../components/ha-date-input";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-formfield";
import "../../components/ha-switch";
import "../../components/ha-button";
import "../../components/ha-textarea";
import "../../components/ha-textfield";
import "../../components/ha-time-input";
@@ -282,7 +282,6 @@ class DialogCalendarEventEditor extends LitElement {
? html`
<ha-button
slot="secondaryAction"
appearance="plain"
variant="danger"
@click=${this._deleteEvent}
.disabled=${this._submitting}

View File

@@ -7,7 +7,6 @@ import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-combo-box";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-fade-in";
import "../../../components/ha-markdown";
import "../../../components/ha-password-field";
import "../../../components/ha-spinner";
@@ -83,7 +82,7 @@ export class DialogAddApplicationCredential extends LitElement {
}
protected render() {
if (!this._params) {
if (!this._params || !this._domains) {
return nothing;
}
const selectedDomainName = this._params.selectedDomain
@@ -102,159 +101,144 @@ export class DialogAddApplicationCredential extends LitElement {
)
)}
>
${!this._config
? html`<ha-fade-in .delay=${500}>
<ha-spinner size="large"></ha-spinner>
</ha-fade-in>`
: html`<div>
${this._error
? html`<ha-alert alert-type="error"
>${this._error}</ha-alert
> `
: nothing}
${this._params.selectedDomain && !this._description
? html`<p>
<div>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert> `
: ""}
${this._params.selectedDomain && !this._description
? html`<p>
${this.hass.localize(
"ui.panel.config.application_credentials.editor.missing_credentials",
{
integration: selectedDomainName,
}
)}
${this._manifest?.is_built_in || this._manifest?.documentation
? html`<a
href=${this._manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._domain}`
)
: this._manifest.documentation}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.application_credentials.editor.missing_credentials",
"ui.panel.config.application_credentials.editor.missing_credentials_domain_link",
{
integration: selectedDomainName,
}
)}
${this._manifest?.is_built_in ||
this._manifest?.documentation
? html`<a
href=${this._manifest.is_built_in
? documentationUrl(
this.hass,
`/integrations/${this._domain}`
)
: this._manifest.documentation}
target="_blank"
rel="noreferrer"
>
${this.hass.localize(
"ui.panel.config.application_credentials.editor.missing_credentials_domain_link",
{
integration: selectedDomainName,
}
)}
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
</a>`
: nothing}
</p>`
: nothing}
${!this._params.selectedDomain || !this._description
? html`<p>
${this.hass.localize(
"ui.panel.config.application_credentials.editor.description"
)}
<a
href=${documentationUrl(
this.hass!,
"/integrations/application_credentials"
)}
target="_blank"
rel="noreferrer"
>
${this.hass!.localize(
"ui.panel.config.application_credentials.editor.view_documentation"
)}
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
</a>
</p>`
: nothing}
${this._params.selectedDomain
? nothing
: html`<ha-combo-box
name="domain"
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.domain"
)}
.value=${this._domain}
.items=${this._domains}
item-id-path="id"
item-value-path="id"
item-label-path="name"
required
@value-changed=${this._handleDomainPicked}
></ha-combo-box>`}
${this._description
? html`<ha-markdown
breaks
.content=${this._description}
></ha-markdown>`
: nothing}
<ha-textfield
class="name"
name="name"
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.name"
)}
.value=${this._name}
required
@input=${this._handleValueChanged}
.validationMessage=${this.hass.localize(
"ui.common.error_required"
)}
dialogInitialFocus
></ha-textfield>
<ha-textfield
class="clientId"
name="clientId"
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_id"
)}
.value=${this._clientId}
required
@input=${this._handleValueChanged}
.validationMessage=${this.hass.localize(
"ui.common.error_required"
)}
dialogInitialFocus
.helper=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_id_helper"
)}
helperPersistent
></ha-textfield>
<ha-password-field
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_secret"
)}
name="clientSecret"
.value=${this._clientSecret}
required
@input=${this._handleValueChanged}
.validationMessage=${this.hass.localize(
"ui.common.error_required"
)}
.helper=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_secret_helper"
)}
helperPersistent
></ha-password-field>
</div>
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this._abortDialog}
.disabled=${this._loading}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
.disabled=${!this._domain ||
!this._clientId ||
!this._clientSecret}
@click=${this._addApplicationCredential}
.loading=${this._loading}
>
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
</a>`
: ""}
</p>`
: ""}
${!this._params.selectedDomain || !this._description
? html`<p>
${this.hass.localize(
"ui.panel.config.application_credentials.editor.add"
"ui.panel.config.application_credentials.editor.description"
)}
</ha-button>`}
<a
href=${documentationUrl(
this.hass!,
"/integrations/application_credentials"
)}
target="_blank"
rel="noreferrer"
>
${this.hass!.localize(
"ui.panel.config.application_credentials.editor.view_documentation"
)}
<ha-svg-icon .path=${mdiOpenInNew}></ha-svg-icon>
</a>
</p>`
: ""}
${this._params.selectedDomain
? ""
: html`<ha-combo-box
name="domain"
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.domain"
)}
.value=${this._domain}
.items=${this._domains}
item-id-path="id"
item-value-path="id"
item-label-path="name"
required
@value-changed=${this._handleDomainPicked}
></ha-combo-box>`}
${this._description
? html`<ha-markdown
breaks
.content=${this._description}
></ha-markdown>`
: ""}
<ha-textfield
class="name"
name="name"
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.name"
)}
.value=${this._name}
required
@input=${this._handleValueChanged}
.validationMessage=${this.hass.localize("ui.common.error_required")}
dialogInitialFocus
></ha-textfield>
<ha-textfield
class="clientId"
name="clientId"
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_id"
)}
.value=${this._clientId}
required
@input=${this._handleValueChanged}
.validationMessage=${this.hass.localize("ui.common.error_required")}
dialogInitialFocus
.helper=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_id_helper"
)}
helperPersistent
></ha-textfield>
<ha-password-field
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_secret"
)}
name="clientSecret"
.value=${this._clientSecret}
required
@input=${this._handleValueChanged}
.validationMessage=${this.hass.localize("ui.common.error_required")}
.helper=${this.hass.localize(
"ui.panel.config.application_credentials.editor.client_secret_helper"
)}
helperPersistent
></ha-password-field>
</div>
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this._abortDialog}
.disabled=${this._loading}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
.disabled=${!this._domain || !this._clientId || !this._clientSecret}
@click=${this._addApplicationCredential}
.loading=${this._loading}
>
${this.hass.localize(
"ui.panel.config.application_credentials.editor.add"
)}
</ha-button>
</ha-dialog>
`;
}
@@ -357,11 +341,6 @@ export class DialogAddApplicationCredential extends LitElement {
ha-markdown {
margin-bottom: 16px;
}
ha-fade-in {
display: flex;
width: 100%;
justify-content: center;
}
`,
];
}

View File

@@ -1,113 +0,0 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { migrateAutomationAction, type Action } from "../../../../data/script";
import type { HomeAssistant } from "../../../../types";
import "../ha-automation-editor-warning";
import { editorStyles } from "../styles";
import { getAutomationActionType } from "./ha-automation-action-row";
@customElement("ha-automation-action-editor")
export default class HaAutomationActionEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) action!: Action;
@property({ type: Boolean }) public disabled = false;
@property({ attribute: false }) public yamlMode = false;
@property({ type: Boolean }) public indent = false;
@property({ type: Boolean, reflect: true }) public selected = false;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
@property({ type: Boolean, attribute: "supported" }) public uiSupported =
false;
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
protected render() {
const yamlMode = this.yamlMode || !this.uiSupported;
const type = getAutomationActionType(this.action);
return html`
<div
class=${classMap({
"card-content": true,
disabled:
this.disabled || (this.action.enabled === false && !this.yamlMode),
yaml: yamlMode,
indent: this.indent,
})}
>
${yamlMode
? html`
${!this.uiSupported
? html`
<ha-automation-editor-warning
.alertTitle=${this.hass.localize(
"ui.panel.config.automation.editor.actions.unsupported_action"
)}
.localize=${this.hass.localize}
></ha-automation-editor-warning>
`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.action}
@value-changed=${this._onYamlChange}
.readOnly=${this.disabled}
></ha-yaml-editor>
`
: html`
<div @value-changed=${this._onUiChanged}>
${dynamicElement(`ha-automation-action-${type}`, {
hass: this.hass,
action: this.action,
disabled: this.disabled,
narrow: this.narrow,
optionsInSidebar: this.indent,
indent: this.indent,
inSidebar: this.inSidebar,
})}
</div>
`}
</div>
`;
}
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
if (!ev.detail.isValid) {
return;
}
fireEvent(this, "value-changed", {
value: migrateAutomationAction(ev.detail.value),
});
}
private _onUiChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = {
...(this.action.alias ? { alias: this.action.alias } : {}),
...ev.detail.value,
};
fireEvent(this, "value-changed", { value });
}
static styles = editorStyles;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-action-editor": HaAutomationActionEditor;
}
}

View File

@@ -15,35 +15,28 @@ import {
mdiStopCircleOutline,
} from "@mdi/js";
import deepClone from "deep-clone-simple";
import type { PropertyValues } from "lit";
import { LitElement, html, nothing } from "lit";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { classMap } from "lit/directives/class-map";
import { storage } from "../../../../common/decorators/storage";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-automation-row";
import "../../../../components/ha-alert";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-service-icon";
import "../../../../components/ha-tooltip";
import {
ACTION_BUILDING_BLOCKS,
ACTION_COMBINED_BLOCKS,
ACTION_ICONS,
YAML_ONLY_ACTION_TYPES,
} from "../../../../data/action";
import type {
AutomationClipboard,
Condition,
} from "../../../../data/automation";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import { ACTION_ICONS, YAML_ONLY_ACTION_TYPES } from "../../../../data/action";
import type { AutomationClipboard } from "../../../../data/automation";
import { validateConfig } from "../../../../data/config";
import {
floorsContext,
@@ -53,12 +46,11 @@ import {
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
import type { FloorRegistryEntry } from "../../../../data/floor_registry";
import type { LabelRegistryEntry } from "../../../../data/label_registry";
import type {
Action,
NonConditionAction,
RepeatAction,
import type { Action, NonConditionAction } from "../../../../data/script";
import {
getActionType,
migrateAutomationAction,
} from "../../../../data/script";
import { getActionType } from "../../../../data/script";
import { describeAction } from "../../../../data/script_i18n";
import { callExecuteScript } from "../../../../data/service";
import {
@@ -66,12 +58,9 @@ import {
showConfirmationDialog,
showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast";
import "../ha-automation-editor-warning";
import { rowStyles } from "../styles";
import "./ha-automation-action-editor";
import type HaAutomationActionEditor from "./ha-automation-action-editor";
import "./types/ha-automation-action-choose";
import "./types/ha-automation-action-condition";
import "./types/ha-automation-action-delay";
@@ -80,31 +69,28 @@ import "./types/ha-automation-action-event";
import "./types/ha-automation-action-if";
import "./types/ha-automation-action-parallel";
import "./types/ha-automation-action-play_media";
import { getRepeatType } from "./types/ha-automation-action-repeat";
import "./types/ha-automation-action-repeat";
import "./types/ha-automation-action-sequence";
import "./types/ha-automation-action-service";
import "./types/ha-automation-action-set_conversation_response";
import "./types/ha-automation-action-stop";
import "./types/ha-automation-action-wait_for_trigger";
import "./types/ha-automation-action-wait_template";
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
export const getAutomationActionType = memoizeOne(
(action: Action | undefined) => {
if (!action) {
return undefined;
}
if ("action" in action) {
return getActionType(action) as "action" | "play_media";
}
if (CONDITION_BUILDING_BLOCKS.some((key) => key in action)) {
return "condition" as const;
}
return Object.keys(ACTION_ICONS).find(
(option) => option in action
) as keyof typeof ACTION_ICONS;
export const getType = (action: Action | undefined) => {
if (!action) {
return undefined;
}
);
if ("action" in action) {
return getActionType(action) as "action" | "play_media";
}
if (["and", "or", "not"].some((key) => key in action)) {
return "condition" as const;
}
return Object.keys(ACTION_ICONS).find(
(option) => option in action
) as keyof typeof ACTION_ICONS;
};
export interface ActionElement extends LitElement {
action: Action;
@@ -132,6 +118,8 @@ export const handleChangeEvent = (element: ActionElement, ev: CustomEvent) => {
fireEvent(element, "value-changed", { value: newAction });
};
const preventDefault = (ev) => ev.preventDefault();
@customElement("ha-automation-action-row")
export default class HaAutomationActionRow extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -146,9 +134,6 @@ export default class HaAutomationActionRow extends LitElement {
@property({ type: Boolean }) public last?: boolean;
@property({ type: Boolean, attribute: "sidebar" })
public optionsInSidebar = false;
@storage({
key: "automationClipboard",
state: false,
@@ -169,27 +154,19 @@ export default class HaAutomationActionRow extends LitElement {
@consume({ context: floorsContext, subscribe: true })
_floorReg!: Record<string, FloorRegistryEntry>;
@state() private _warnings?: string[];
@state() private _uiModeAvailable = true;
@state() private _yamlMode = false;
@state() private _selected = false;
@state() private _collapsed = false;
@state() private _warnings?: string[];
@query("ha-automation-action-editor")
private actionEditor?: HaAutomationActionEditor;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
protected willUpdate(changedProperties: PropertyValues) {
if (changedProperties.has("yamlMode")) {
this._warnings = undefined;
}
if (!changedProperties.has("action")) {
return;
}
const type = getAutomationActionType(this.action);
const type = getType(this.action);
this._uiModeAvailable =
type !== undefined && !YAML_ONLY_ACTION_TYPES.has(type as any);
if (!this._uiModeAvailable && !this._yamlMode) {
@@ -197,207 +174,23 @@ export default class HaAutomationActionRow extends LitElement {
}
}
private _renderRow() {
const type = getAutomationActionType(this.action);
return html`
${type === "service" && "action" in this.action && this.action.action
? html`
<ha-service-icon
slot="leading-icon"
class="action-icon"
.hass=${this.hass}
.service=${this.action.action}
></ha-service-icon>
`
: html`
<ha-svg-icon
slot="leading-icon"
class="action-icon"
.path=${ACTION_ICONS[type!]}
></ha-svg-icon>
`}
<h3 slot="header">
${capitalizeFirstLetter(
describeAction(
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
this.action
)
)}
</h3>
<slot name="icons" slot="icons"></slot>
${type !== "condition" &&
(this.action as NonConditionAction).continue_on_error === true
? html`<ha-tooltip
slot="icons"
.content=${this.hass.localize(
"ui.panel.config.automation.editor.actions.continue_on_error"
)}
>
<ha-svg-icon .path=${mdiAlertCircleCheck}></ha-svg-icon>
</ha-tooltip>`
: nothing}
<ha-md-button-menu
slot="icons"
@click=${preventDefaultStopPropagation}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item .clickAction=${this._runAction}>
${this.hass.localize("ui.panel.config.automation.editor.actions.run")}
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
</ha-md-menu-item>
${!this.optionsInSidebar
? html` <ha-md-menu-item
.clickAction=${this._renameAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>`
: nothing}
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._duplicateAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon slot="start" .path=${mdiContentDuplicate}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._copyAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._cutAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveUp}
.disabled=${this.disabled || !!this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveDown}
.disabled=${this.disabled || !!this.last}
>
${this.hass.localize("ui.panel.config.automation.editor.move_down")}
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
${!this.optionsInSidebar
? html`<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!this._uiModeAvailable || !!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>`
: nothing}
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
${this.action.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${this.action.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
class="warning"
.clickAction=${this._onDelete}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
${!this.optionsInSidebar
? html`${this._warnings
? html`<ha-automation-editor-warning
.localize=${this.hass.localize}
.warnings=${this._warnings}
>
</ha-automation-editor-warning>`
: nothing}
<ha-automation-action-editor
.hass=${this.hass}
.action=${this.action}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
.narrow=${this.narrow}
.uiSupported=${this._uiSupported(type!)}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-action-editor>`
: nothing}
`;
protected updated(changedProperties: PropertyValues) {
if (!changedProperties.has("action")) {
return;
}
if (this._yamlMode) {
const yamlEditor = this._yamlEditor;
if (yamlEditor && yamlEditor.value !== this.action) {
yamlEditor.setValue(this.action);
}
}
}
protected render() {
if (!this.action) return nothing;
const type = getAutomationActionType(this.action);
const blockType =
type === "repeat"
? `repeat_${getRepeatType((this.action as RepeatAction).repeat)}`
: type;
const type = getType(this.action);
const yamlMode = this._yamlMode;
return html`
<ha-card outlined>
@@ -410,57 +203,245 @@ export default class HaAutomationActionRow extends LitElement {
</div>
`
: nothing}
${this.optionsInSidebar
? html`<ha-automation-row
.disabled=${this.action.enabled === false}
@click=${this._toggleSidebar}
.leftChevron=${[
...ACTION_BUILDING_BLOCKS,
...ACTION_COMBINED_BLOCKS,
].includes(blockType!)}
.collapsed=${this._collapsed}
.selected=${this._selected}
@toggle-collapsed=${this._toggleCollapse}
.buildingBlock=${[
...ACTION_BUILDING_BLOCKS,
...ACTION_COMBINED_BLOCKS,
].includes(blockType!)}
>${this._renderRow()}</ha-automation-row
>`
: html`
<ha-expansion-panel left-chevron>
${this._renderRow()}
</ha-expansion-panel>
`}
</ha-card>
<ha-expansion-panel left-chevron>
${type === "service" && "action" in this.action && this.action.action
? html`
<ha-service-icon
slot="leading-icon"
class="action-icon"
.hass=${this.hass}
.service=${this.action.action}
></ha-service-icon>
`
: html`
<ha-svg-icon
slot="leading-icon"
class="action-icon"
.path=${ACTION_ICONS[type!]}
></ha-svg-icon>
`}
<h3 slot="header">
${capitalizeFirstLetter(
describeAction(
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
this.action
)
)}
</h3>
${this.optionsInSidebar &&
([...ACTION_BUILDING_BLOCKS, ...ACTION_COMBINED_BLOCKS].includes(
blockType!
) ||
(blockType === "condition" &&
CONDITION_BUILDING_BLOCKS.includes(
(this.action as Condition).condition
))) &&
!this._collapsed
? html`<ha-automation-action-editor
.hass=${this.hass}
.action=${this.action}
.narrow=${this.narrow}
.disabled=${this.disabled}
.uiSupported=${this._uiSupported(type!)}
indent
.selected=${this._selected}
@value-changed=${this._onValueChange}
></ha-automation-action-editor>`
: nothing}
<slot name="icons" slot="icons"></slot>
${type !== "condition" &&
(this.action as NonConditionAction).continue_on_error === true
? html`<ha-tooltip
slot="icons"
.content=${this.hass.localize(
"ui.panel.config.automation.editor.actions.continue_on_error"
)}
>
<ha-svg-icon .path=${mdiAlertCircleCheck}></ha-svg-icon>
</ha-tooltip>`
: nothing}
<ha-md-button-menu
slot="icons"
@click=${preventDefault}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item .clickAction=${this._runAction}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.run"
)}
<ha-svg-icon slot="start" .path=${mdiPlay}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._renameAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._duplicateAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="start"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._copyAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._cutAction}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveUp}
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveDown}
.disabled=${this.disabled || this.last}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!this._uiModeAvailable}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
${this.action.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${this.action.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
class="warning"
.clickAction=${this._onDelete}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
<div
class=${classMap({
"card-content": true,
disabled: this.action.enabled === false,
})}
>
${this._warnings
? html`<ha-alert
alert-type="warning"
.title=${this.hass.localize(
"ui.errors.config.editor_not_supported"
)}
>
${this._warnings!.length > 0 &&
this._warnings![0] !== undefined
? html` <ul>
${this._warnings!.map(
(warning) => html`<li>${warning}</li>`
)}
</ul>`
: ""}
${this.hass.localize(
"ui.errors.config.edit_in_yaml_supported"
)}
</ha-alert>`
: ""}
${yamlMode
? html`
${type === undefined
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.actions.unsupported_action"
)}
`
: ""}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.action}
.readOnly=${this.disabled}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
`
: html`
<div
@ui-mode-not-available=${this._handleUiModeNotAvailable}
@value-changed=${this._onUiChanged}
>
${dynamicElement(`ha-automation-action-${type}`, {
hass: this.hass,
action: this.action,
narrow: this.narrow,
disabled: this.disabled,
})}
</div>
`}
</div>
</ha-expansion-panel>
</ha-card>
`;
}
private _onValueChange(event: CustomEvent) {
// reload sidebar if sort, deleted,... happend
if (this._selected && this.optionsInSidebar) {
this.openSidebar(event.detail.value);
private _handleUiModeNotAvailable(ev: CustomEvent) {
// Prevent possible parent action-row from switching to yamlMode
ev.stopPropagation();
this._warnings = handleStructError(this.hass, ev.detail).warnings;
if (!this._yamlMode) {
this._yamlMode = true;
}
}
@@ -475,10 +456,8 @@ export default class HaAutomationActionRow extends LitElement {
const enabled = !(this.action.enabled ?? true);
const value = { ...this.action, enabled };
fireEvent(this, "value-changed", { value });
this.openSidebar(value); // refresh sidebar
if (this._yamlMode && !this.optionsInSidebar) {
this.actionEditor?.yamlEditor?.setValue(value);
if (this._yamlMode) {
this._yamlEditor?.setValue(value);
}
};
@@ -529,18 +508,36 @@ export default class HaAutomationActionRow extends LitElement {
destructive: true,
confirm: () => {
fireEvent(this, "value-changed", { value: null });
if (this._selected) {
fireEvent(this, "close-sidebar");
}
},
});
};
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
if (!ev.detail.isValid) {
return;
}
fireEvent(this, "value-changed", {
value: migrateAutomationAction(ev.detail.value),
});
}
private _onUiChanged(ev: CustomEvent) {
ev.stopPropagation();
const value = {
...(this.action.alias ? { alias: this.action.alias } : {}),
...ev.detail.value,
};
fireEvent(this, "value-changed", { value });
}
private _switchUiMode() {
this._warnings = undefined;
this._yamlMode = false;
}
private _switchYamlMode() {
this._warnings = undefined;
this._yamlMode = true;
}
@@ -577,11 +574,8 @@ export default class HaAutomationActionRow extends LitElement {
fireEvent(this, "value-changed", {
value,
});
if (this._selected && this.optionsInSidebar) {
this.openSidebar(value); // refresh sidebar
} else if (this._yamlMode) {
this.actionEditor?.yamlEditor?.setValue(value);
if (this._yamlMode) {
this._yamlEditor?.setValue(value);
}
}
};
@@ -597,9 +591,6 @@ export default class HaAutomationActionRow extends LitElement {
private _cutAction = () => {
this._setClipboard();
fireEvent(this, "value-changed", { value: null });
if (this._selected) {
fireEvent(this, "close-sidebar");
}
};
private _moveUp = () => {
@@ -616,78 +607,82 @@ export default class HaAutomationActionRow extends LitElement {
} else {
this._switchYamlMode();
}
if (!this.optionsInSidebar) {
this.expand();
}
this.expand();
};
private _handleUiModeNotAvailable(ev: CustomEvent) {
this._warnings = handleStructError(this.hass, ev.detail).warnings;
if (!this._yamlMode) {
this._yamlMode = true;
}
}
private _toggleSidebar(ev: Event) {
ev?.stopPropagation();
if (this._selected) {
this._selected = false;
fireEvent(this, "close-sidebar");
return;
}
this.openSidebar();
}
public openSidebar(action?: Action): void {
if (this.narrow) {
this.scrollIntoView();
}
const sidebarAction = action ?? this.action;
const actionType = getAutomationActionType(sidebarAction);
fireEvent(this, "open-sidebar", {
save: (value) => {
fireEvent(this, "value-changed", { value });
},
close: () => {
this._selected = false;
fireEvent(this, "close-sidebar");
},
rename: () => {
this._renameAction();
},
toggleYamlMode: () => {
this._toggleYamlMode();
return this._yamlMode;
},
disable: this._onDisable,
delete: this._onDelete,
config: sidebarAction,
type: "action",
uiSupported: actionType ? this._uiSupported(actionType) : false,
yamlMode: this._yamlMode,
});
this._selected = true;
}
public expand() {
this.updateComplete.then(() => {
this.shadowRoot!.querySelector("ha-expansion-panel")!.expanded = true;
});
}
private _uiSupported = memoizeOne(
(type: string) =>
customElements.get(`ha-automation-action-${type}`) !== undefined
);
private _toggleCollapse() {
this._collapsed = !this._collapsed;
static get styles(): CSSResultGroup {
return [
haStyle,
css`
ha-icon-button {
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.disabled {
opacity: 0.5;
pointer-events: none;
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0;
}
h3 {
margin: 0;
font-size: inherit;
font-weight: inherit;
}
.action-icon {
display: none;
}
@media (min-width: 870px) {
.action-icon {
display: inline-block;
color: var(--secondary-text-color);
opacity: 0.9;
}
}
.card-content {
padding: 16px;
}
.disabled-bar {
background: var(--divider-color, #e0e0e0);
text-align: center;
border-top-right-radius: calc(
var(--ha-card-border-radius, 12px) - var(
--ha-card-border-width,
1px
)
);
border-top-left-radius: calc(
var(--ha-card-border-radius, 12px) - var(
--ha-card-border-width,
1px
)
);
}
.warning ul {
margin: 4px 0;
}
ha-md-menu-item > ha-svg-icon {
--mdc-icon-size: 24px;
}
ha-tooltip {
cursor: default;
}
:host([highlight]) ha-card {
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
border-color: var(--state-inactive-color);
box-shadow: var(--shadow-default), var(--shadow-focus);
}
`,
];
}
static styles = rowStyles;
}
declare global {

View File

@@ -11,21 +11,16 @@ import { nextRender } from "../../../../common/util/render-status";
import "../../../../components/ha-button";
import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon";
import {
ACTION_BUILDING_BLOCKS,
getService,
isService,
} from "../../../../data/action";
import { getService, isService } from "../../../../data/action";
import type { AutomationClipboard } from "../../../../data/automation";
import type { Action } from "../../../../data/script";
import type { HomeAssistant } from "../../../../types";
import {
PASTE_VALUE,
VIRTUAL_ACTIONS,
showAddAutomationElementDialog,
} from "../show-add-automation-element-dialog";
import type HaAutomationActionRow from "./ha-automation-action-row";
import { getAutomationActionType } from "./ha-automation-action-row";
import { getType } from "./ha-automation-action-row";
@customElement("ha-automation-action")
export default class HaAutomationAction extends LitElement {
@@ -41,9 +36,6 @@ export default class HaAutomationAction extends LitElement {
@property({ attribute: false }) public highlightedActions?: Action[];
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
false;
@state() private _showReorder = false;
@state()
@@ -105,7 +97,6 @@ export default class HaAutomationAction extends LitElement {
@value-changed=${this._actionChanged}
.hass=${this.hass}
?highlight=${this.highlightedActions?.includes(action)}
.optionsInSidebar=${this.optionsInSidebar}
>
${this._showReorder && !this.disabled
? html`
@@ -156,17 +147,7 @@ export default class HaAutomationAction extends LitElement {
"ha-automation-action-row:last-of-type"
)!;
row.updateComplete.then(() => {
// on new condition open the settings in the sidebar, except for building blocks
const type = getAutomationActionType(row.action);
if (
type &&
this.optionsInSidebar &&
!ACTION_BUILDING_BLOCKS.includes(type)
) {
row.openSidebar();
} else if (!this.optionsInSidebar) {
row.expand();
}
row.expand();
row.scrollIntoView();
row.focus();
});
@@ -186,7 +167,7 @@ export default class HaAutomationAction extends LitElement {
showAddAutomationElementDialog(this, {
type: "action",
add: this._addAction,
clipboardItem: getAutomationActionType(this._clipboard?.action),
clipboardItem: getType(this._clipboard?.action),
});
}
@@ -194,7 +175,7 @@ export default class HaAutomationAction extends LitElement {
showAddAutomationElementDialog(this, {
type: "action",
add: this._addAction,
clipboardItem: getAutomationActionType(this._clipboard?.action),
clipboardItem: getType(this._clipboard?.action),
group: "building_blocks",
});
}
@@ -203,8 +184,6 @@ export default class HaAutomationAction extends LitElement {
let actions: Action[];
if (action === PASTE_VALUE) {
actions = this.actions.concat(deepClone(this._clipboard!.action));
} else if (action in VIRTUAL_ACTIONS) {
actions = this.actions.concat(VIRTUAL_ACTIONS[action]);
} else if (isService(action)) {
actions = this.actions.concat({
action: getService(action),
@@ -290,7 +269,6 @@ export default class HaAutomationAction extends LitElement {
// Ensure action is removed even after update
const actions = this.actions.filter((a) => a !== action);
fireEvent(this, "value-changed", { value: actions });
fireEvent(this, "close-sidebar");
}
private _actionChanged(ev: CustomEvent) {
@@ -322,18 +300,15 @@ export default class HaAutomationAction extends LitElement {
static styles = css`
.actions {
padding: 16px 0 16px 16px;
padding: 16px;
margin: -16px;
display: flex;
flex-direction: column;
gap: 16px;
}
:host([root]) .actions {
padding-right: 8px;
}
.sortable-ghost {
background: none;
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
border-radius: var(--ha-card-border-radius, 12px);
}
.sortable-drag {
background: none;
@@ -342,6 +317,9 @@ export default class HaAutomationAction extends LitElement {
display: block;
scroll-margin-top: 48px;
}
ha-svg-icon {
height: 20px;
}
.handle {
padding: 12px;
cursor: move; /* fallback if grab cursor is unsupported */

View File

@@ -7,8 +7,8 @@ import type { Action, ChooseAction, Option } from "../../../../../data/script";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import "../../option/ha-automation-option";
import "../ha-automation-action";
import type { ActionElement } from "../ha-automation-action-row";
import "../ha-automation-action";
@customElement("ha-automation-action-choose")
export class HaChooseAction extends LitElement implements ActionElement {
@@ -20,8 +20,6 @@ export class HaChooseAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean }) public indent = false;
@state() private _showDefault = false;
public static get defaultConfig(): ChooseAction {
@@ -40,7 +38,6 @@ export class HaChooseAction extends LitElement implements ActionElement {
@value-changed=${this._optionsChanged}
.hass=${this.hass}
.narrow=${this.narrow}
.optionsInSidebar=${this.indent}
></ha-automation-option>
${this._showDefault || action.default
@@ -56,7 +53,6 @@ export class HaChooseAction extends LitElement implements ActionElement {
@value-changed=${this._defaultChanged}
.hass=${this.hass}
.narrow=${this.narrow}
.optionsInSidebar=${this.indent}
></ha-automation-action>
`
: html`

View File

@@ -1,4 +1,4 @@
import { css, html, LitElement, nothing } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
@@ -8,24 +8,10 @@ import "../../../../../components/ha-list-item";
import "../../../../../components/ha-select";
import type { HaSelect } from "../../../../../components/ha-select";
import type { Condition } from "../../../../../data/automation";
import {
CONDITION_BUILDING_BLOCKS,
CONDITION_ICONS,
} from "../../../../../data/condition";
import { CONDITION_ICONS } from "../../../../../data/condition";
import type { Entries, HomeAssistant } from "../../../../../types";
import "../../condition/ha-automation-condition-editor";
import type { ActionElement } from "../ha-automation-action-row";
import "../../condition/types/ha-automation-condition-and";
import "../../condition/types/ha-automation-condition-device";
import "../../condition/types/ha-automation-condition-not";
import "../../condition/types/ha-automation-condition-numeric_state";
import "../../condition/types/ha-automation-condition-or";
import "../../condition/types/ha-automation-condition-state";
import "../../condition/types/ha-automation-condition-sun";
import "../../condition/types/ha-automation-condition-template";
import "../../condition/types/ha-automation-condition-time";
import "../../condition/types/ha-automation-condition-trigger";
import "../../condition/types/ha-automation-condition-zone";
@customElement("ha-automation-action-condition")
export class HaConditionAction extends LitElement implements ActionElement {
@@ -35,63 +21,36 @@ export class HaConditionAction extends LitElement implements ActionElement {
@property({ attribute: false }) public action!: Condition;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
@property({ type: Boolean, attribute: "indent" }) public indent = false;
public static get defaultConfig(): Omit<Condition, "state" | "entity_id"> {
return { condition: "state" };
}
protected render() {
const buildingBlock = CONDITION_BUILDING_BLOCKS.includes(
this.action.condition
);
return html`
${this.inSidebar || (!this.inSidebar && !this.indent)
? html`
<ha-select
fixedMenuPosition
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type_select"
)}
.disabled=${this.disabled}
.value=${this.action.condition}
naturalMenuWidth
@selected=${this._typeChanged}
>
${this._processedTypes(this.hass.localize).map(
([opt, label, icon]) => html`
<ha-list-item .value=${opt} graphic="icon">
${label}<ha-svg-icon
slot="graphic"
.path=${icon}
></ha-svg-icon
></ha-list-item>
`
)}
</ha-select>
<ha-select
fixedMenuPosition
.label=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type_select"
)}
.disabled=${this.disabled}
.value=${this.action.condition}
naturalMenuWidth
@selected=${this._typeChanged}
>
${this._processedTypes(this.hass.localize).map(
([opt, label, icon]) => html`
<ha-list-item .value=${opt} graphic="icon">
${label}<ha-svg-icon slot="graphic" .path=${icon}></ha-svg-icon
></ha-list-item>
`
: nothing}
${(this.indent && buildingBlock) ||
(this.inSidebar && !buildingBlock) ||
(!this.indent && !this.inSidebar)
? html`
<ha-automation-condition-editor
.condition=${this.action}
.disabled=${this.disabled}
.hass=${this.hass}
@value-changed=${this._conditionChanged}
.narrow=${this.narrow}
.uiSupported=${this._uiSupported(this.action.condition)}
.indent=${this.indent}
action
></ha-automation-condition-editor>
`
: nothing}
)}
</ha-select>
<ha-automation-condition-editor
.condition=${this.action}
.disabled=${this.disabled}
.hass=${this.hass}
@value-changed=${this._conditionChanged}
></ha-automation-condition-editor>
`;
}
@@ -141,11 +100,6 @@ export class HaConditionAction extends LitElement implements ActionElement {
}
}
private _uiSupported = memoizeOne(
(type: string) =>
customElements.get(`ha-automation-condition-${type}`) !== undefined
);
static styles = css`
ha-select {
margin-bottom: 24px;

View File

@@ -20,8 +20,6 @@ export class HaIfAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean }) public indent = false;
@state() private _showElse = false;
public static get defaultConfig(): IfAction {
@@ -41,12 +39,11 @@ export class HaIfAction extends LitElement implements ActionElement {
)}*:
</h3>
<ha-automation-condition
.conditions=${action.if ?? []}
.conditions=${action.if}
.disabled=${this.disabled}
@value-changed=${this._ifChanged}
.hass=${this.hass}
.narrow=${this.narrow}
.optionsInSidebar=${this.indent}
></ha-automation-condition>
<h3>
@@ -55,12 +52,11 @@ export class HaIfAction extends LitElement implements ActionElement {
)}*:
</h3>
<ha-automation-action
.actions=${action.then ?? []}
.actions=${action.then}
.disabled=${this.disabled}
@value-changed=${this._thenChanged}
.hass=${this.hass}
.narrow=${this.narrow}
.optionsInSidebar=${this.indent}
></ha-automation-action>
${this._showElse || action.else
? html`
@@ -75,10 +71,9 @@ export class HaIfAction extends LitElement implements ActionElement {
@value-changed=${this._elseChanged}
.hass=${this.hass}
.narrow=${this.narrow}
.optionsInSidebar=${this.indent}
></ha-automation-action>
`
: html`<div class="link-button-row">
: html` <div class="link-button-row">
<button
class="link"
@click=${this._addElse}

View File

@@ -15,12 +15,8 @@ export class HaParallelAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public action!: ParallelAction;
@property({ type: Boolean }) public indent = false;
public static get defaultConfig(): ParallelAction {
return {
parallel: [],
@@ -33,11 +29,9 @@ export class HaParallelAction extends LitElement implements ActionElement {
return html`
<ha-automation-action
.actions=${action.parallel}
.narrow=${this.narrow}
.disabled=${this.disabled}
@value-changed=${this._actionsChanged}
.hass=${this.hass}
.optionsInSidebar=${this.indent}
></ha-automation-action>
`;
}

View File

@@ -11,6 +11,7 @@ import "../ha-automation-action";
import type { ActionElement } from "../ha-automation-action-row";
import { isTemplate } from "../../../../../common/string/has-template";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
@@ -18,10 +19,8 @@ import type {
} from "../../../../../components/ha-form/types";
const OPTIONS = ["count", "while", "until", "for_each"] as const;
type RepeatType = (typeof OPTIONS)[number];
export const getRepeatType = (action: RepeatAction["repeat"]) =>
OPTIONS.find((option) => option in action);
const getType = (action) => OPTIONS.find((option) => option in action);
@customElement("ha-automation-action-repeat")
export class HaRepeatAction extends LitElement implements ActionElement {
@@ -29,27 +28,30 @@ export class HaRepeatAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public action!: RepeatAction;
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
@property({ type: Boolean, attribute: "indent" }) public indent = false;
public static get defaultConfig(): RepeatAction {
return { repeat: { count: 2, sequence: [] } };
}
private _schema = memoizeOne(
(
type: RepeatType,
template: boolean,
inSidebar: boolean,
indent: boolean
) =>
(localize: LocalizeFunc, type: string, template: boolean) =>
[
...(type === "count" && (inSidebar || (!inSidebar && !indent))
{
name: "type",
selector: {
select: {
mode: "dropdown",
options: OPTIONS.map((opt) => ({
value: opt,
label: localize(
`ui.panel.config.automation.editor.actions.type.repeat.type.${opt}.label`
),
})),
},
},
},
...(type === "count"
? ([
{
name: "count",
@@ -60,20 +62,17 @@ export class HaRepeatAction extends LitElement implements ActionElement {
},
] as const satisfies readonly HaFormSchema[])
: []),
...((type === "until" || type === "while") &&
(indent || (!inSidebar && !indent))
...(type === "until" || type === "while"
? ([
{
name: type,
selector: {
condition: {
optionsInSidebar: indent,
},
condition: {},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
...(type === "for_each" && (inSidebar || (!inSidebar && !indent))
...(type === "for_each"
? ([
{
name: "for_each",
@@ -82,31 +81,24 @@ export class HaRepeatAction extends LitElement implements ActionElement {
},
] as const satisfies readonly HaFormSchema[])
: []),
...(indent || (!inSidebar && !indent)
? ([
{
name: "sequence",
selector: {
action: {
optionsInSidebar: indent,
},
},
},
] as const satisfies readonly HaFormSchema[])
: []),
{
name: "sequence",
selector: {
action: {},
},
},
] as const satisfies readonly HaFormSchema[]
);
protected render() {
const action = this.action.repeat;
const type = getRepeatType(action);
const type = getType(action);
const schema = this._schema(
this.hass.localize,
type ?? "count",
"count" in action && typeof action.count === "string"
? isTemplate(action.count)
: false,
this.inSidebar,
this.indent
: false
);
const data = { ...action, type };
@@ -117,7 +109,6 @@ export class HaRepeatAction extends LitElement implements ActionElement {
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
.computeLabel=${this._computeLabelCallback}
.narrow=${this.narrow}
></ha-form>`;
}
@@ -127,7 +118,7 @@ export class HaRepeatAction extends LitElement implements ActionElement {
const newType = newVal.type;
delete newVal.type;
const oldType = getRepeatType(this.action.repeat);
const oldType = getType(this.action.repeat);
if (newType !== oldType) {
if (newType === "count") {
@@ -179,6 +170,10 @@ export class HaRepeatAction extends LitElement implements ActionElement {
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string => {
switch (schema.name) {
case "type":
return this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.type_select"
);
case "count":
return this.hass.localize(
"ui.panel.config.automation.editor.actions.type.repeat.type.count.label"

View File

@@ -15,12 +15,8 @@ export class HaSequenceAction extends LitElement implements ActionElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public action!: SequenceAction;
@property({ type: Boolean }) public indent = false;
public static get defaultConfig(): SequenceAction {
return {
sequence: [],
@@ -33,11 +29,9 @@ export class HaSequenceAction extends LitElement implements ActionElement {
return html`
<ha-automation-action
.actions=${action.sequence}
.narrow=${this.narrow}
.disabled=${this.disabled}
@value-changed=${this._actionsChanged}
.hass=${this.hass}
.optionsInSidebar=${this.indent}
></ha-automation-action>
`;
}

View File

@@ -1,4 +1,4 @@
import { css, html, LitElement, nothing } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { ensureArray } from "../../../../../common/array/ensure-array";
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
@@ -24,12 +24,6 @@ export class HaWaitForTriggerAction
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
@property({ type: Boolean, attribute: "indent" }) public indent = false;
public static get defaultConfig(): WaitForTriggerAction {
return { wait_for_trigger: [] };
}
@@ -38,43 +32,34 @@ export class HaWaitForTriggerAction
const timeData = createDurationData(this.action.timeout);
return html`
${this.inSidebar || (!this.inSidebar && !this.indent)
? html`
<ha-duration-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_for_trigger.timeout"
)}
.data=${timeData}
.disabled=${this.disabled}
enable-millisecond
@value-changed=${this._timeoutChanged}
></ha-duration-input>
<ha-formfield
.disabled=${this.disabled}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_for_trigger.continue_timeout"
)}
>
<ha-switch
.checked=${this.action.continue_on_timeout ?? true}
.disabled=${this.disabled}
@change=${this._continueChanged}
></ha-switch>
</ha-formfield>
`
: nothing}
${this.indent || (!this.inSidebar && !this.indent)
? html`<ha-automation-trigger
class=${!this.inSidebar && !this.indent ? "expansion-panel" : ""}
.triggers=${ensureArray(this.action.wait_for_trigger)}
.hass=${this.hass}
.disabled=${this.disabled}
.name=${"wait_for_trigger"}
@value-changed=${this._valueChanged}
.optionsInSidebar=${this.indent}
.narrow=${this.narrow}
></ha-automation-trigger>`
: nothing}
<ha-duration-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_for_trigger.timeout"
)}
.data=${timeData}
.disabled=${this.disabled}
enable-millisecond
@value-changed=${this._timeoutChanged}
></ha-duration-input>
<ha-formfield
.disabled=${this.disabled}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.wait_for_trigger.continue_timeout"
)}
>
<ha-switch
.checked=${this.action.continue_on_timeout ?? true}
.disabled=${this.disabled}
@change=${this._continueChanged}
></ha-switch>
</ha-formfield>
<ha-automation-trigger
.triggers=${ensureArray(this.action.wait_for_trigger)}
.hass=${this.hass}
.disabled=${this.disabled}
.name=${"wait_for_trigger"}
@value-changed=${this._valueChanged}
></ha-automation-trigger>
`;
}
@@ -101,7 +86,7 @@ export class HaWaitForTriggerAction
display: block;
margin-bottom: 24px;
}
ha-automation-trigger.expansion-panel {
ha-automation-trigger {
display: block;
margin-top: 24px;
}

View File

@@ -652,7 +652,6 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
ha-dialog {
--dialog-content-padding: 0;
--mdc-dialog-max-height: 60vh;
--mdc-dialog-max-height: 60dvh;
}
@media all and (min-width: 550px) {
ha-dialog {

View File

@@ -260,14 +260,12 @@ class DialogAutomationSave extends LitElement implements HassDialog {
.path=${mdiClose}
></ha-icon-button>
<span slot="title">${this._params.title || title}</span>
${this._params.hideInputs
? nothing
: html` <ha-suggest-with-ai-button
slot="actionItems"
.hass=${this.hass}
.generateTask=${this._generateTask}
@suggestion=${this._handleSuggestion}
></ha-suggest-with-ai-button>`}
<ha-suggest-with-ai-button
slot="actionItems"
.hass=${this.hass}
.generateTask=${this._generateTask}
@suggestion=${this._handleSuggestion}
></ha-suggest-with-ai-button>
</ha-dialog-header>
${this._error
? html`<ha-alert alert-type="error"

View File

@@ -1,15 +1,12 @@
import { mdiContentSave } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, nothing, type CSSResultGroup } from "lit";
import { html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-markdown";
import type { BlueprintAutomationConfig } from "../../../data/automation";
import { fetchBlueprints } from "../../../data/blueprint";
import { HaBlueprintGenericEditor } from "../blueprint/blueprint-generic-editor";
import { saveFabStyles } from "./styles";
@customElement("blueprint-automation-editor")
export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
@@ -17,10 +14,6 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
@property({ attribute: false }) public stateObj?: HassEntity;
@property({ type: Boolean }) public saving = false;
@property({ type: Boolean }) public dirty = false;
protected get _config(): BlueprintAutomationConfig {
return this.config;
}
@@ -54,24 +47,9 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
></ha-markdown>`
: nothing}
${this.renderCard()}
<ha-fab
slot="fab"
class=${this.dirty ? "dirty" : ""}
.label=${this.hass.localize("ui.panel.config.automation.editor.save")}
.disabled=${this.saving}
extended
@click=${this._saveAutomation}
>
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</ha-fab>
`;
}
private _saveAutomation() {
fireEvent(this, "save-automation");
}
protected async _getBlueprints() {
this._blueprints = await fetchBlueprints(this.hass, "automation");
}
@@ -84,24 +62,6 @@ export class HaBlueprintAutomationEditor extends HaBlueprintGenericEditor {
entity_id: this.stateObj.entity_id,
});
}
static get styles(): CSSResultGroup {
return [
HaBlueprintGenericEditor.styles,
saveFabStyles,
css`
:host {
position: relative;
height: 100%;
min-height: calc(100vh - 85px);
min-height: calc(100dvh - 85px);
}
ha-fab {
position: fixed;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {

View File

@@ -1,16 +1,24 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import type { Condition } from "../../../../data/automation";
import { expandConditionWithShorthand } from "../../../../data/automation";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "../ha-automation-editor-warning";
import { editorStyles } from "../styles";
import "./types/ha-automation-condition-and";
import "./types/ha-automation-condition-device";
import "./types/ha-automation-condition-not";
import "./types/ha-automation-condition-numeric_state";
import "./types/ha-automation-condition-or";
import "./types/ha-automation-condition-state";
import "./types/ha-automation-condition-sun";
import "./types/ha-automation-condition-template";
import "./types/ha-automation-condition-time";
import "./types/ha-automation-condition-trigger";
import "./types/ha-automation-condition-zone";
@customElement("ha-automation-condition-editor")
export default class HaAutomationConditionEditor extends LitElement {
@@ -22,71 +30,46 @@ export default class HaAutomationConditionEditor extends LitElement {
@property({ attribute: false }) public yamlMode = false;
@property({ type: Boolean }) public indent = false;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, reflect: true }) public selected = false;
@property({ type: Boolean, attribute: "supported" }) public uiSupported =
false;
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
private _processedCondition = memoizeOne((condition) =>
expandConditionWithShorthand(condition)
);
protected render() {
const condition = this._processedCondition(this.condition);
const yamlMode = this.yamlMode || !this.uiSupported;
const supported =
customElements.get(`ha-automation-condition-${condition.condition}`) !==
undefined;
const yamlMode = this.yamlMode || !supported;
return html`
<div
class=${classMap({
"card-content": true,
disabled:
this.disabled ||
(this.condition.enabled === false && !this.yamlMode),
yaml: yamlMode,
indent: this.indent,
})}
>
${yamlMode
? html`
${!this.uiSupported
? html`
<ha-automation-editor-warning
.alertTitle=${this.hass.localize(
"ui.panel.config.automation.editor.conditions.unsupported_condition",
{ condition: condition.condition }
)}
.localize=${this.hass.localize}
></ha-automation-editor-warning>
`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.condition}
@value-changed=${this._onYamlChange}
.readOnly=${this.disabled}
></ha-yaml-editor>
`
: html`
<div @value-changed=${this._onUiChanged}>
${dynamicElement(
`ha-automation-condition-${condition.condition}`,
{
hass: this.hass,
condition: condition,
disabled: this.disabled,
optionsInSidebar: this.indent,
narrow: this.narrow,
}
)}
</div>
`}
</div>
${yamlMode
? html`
${!supported
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.unsupported_condition",
{ condition: condition.condition }
)}
`
: ""}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.condition}
@value-changed=${this._onYamlChange}
.readOnly=${this.disabled}
></ha-yaml-editor>
`
: html`
<div @value-changed=${this._onUiChanged}>
${dynamicElement(
`ha-automation-condition-${condition.condition}`,
{
hass: this.hass,
condition: condition,
disabled: this.disabled,
}
)}
</div>
`}
`;
}
@@ -108,20 +91,7 @@ export default class HaAutomationConditionEditor extends LitElement {
fireEvent(this, "value-changed", { value });
}
static styles = [
editorStyles,
css`
:host([action]) .card-content {
padding: 0;
}
:host([action]) .card-content.indent {
margin-left: 0;
margin-right: 0;
padding: 0;
border-left: none;
}
`,
];
static styles = haStyle;
}
declare global {

View File

@@ -16,32 +16,26 @@ import {
import deepClone from "deep-clone-simple";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { storage } from "../../../../common/decorators/storage";
import { fireEvent } from "../../../../common/dom/fire_event";
import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-automation-row";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import type {
AutomationClipboard,
Condition,
} from "../../../../data/automation";
import { testCondition } from "../../../../data/automation";
import { describeCondition } from "../../../../data/automation_i18n";
import {
CONDITION_BUILDING_BLOCKS,
CONDITION_ICONS,
} from "../../../../data/condition";
import { CONDITION_ICONS } from "../../../../data/condition";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
@@ -50,27 +44,16 @@ import {
showConfirmationDialog,
showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "../ha-automation-editor-warning";
import { rowStyles } from "../styles";
import "./ha-automation-condition-editor";
import type HaAutomationConditionEditor from "./ha-automation-condition-editor";
import "./types/ha-automation-condition-and";
import "./types/ha-automation-condition-device";
import "./types/ha-automation-condition-not";
import "./types/ha-automation-condition-numeric_state";
import "./types/ha-automation-condition-or";
import "./types/ha-automation-condition-state";
import "./types/ha-automation-condition-sun";
import "./types/ha-automation-condition-template";
import "./types/ha-automation-condition-time";
import "./types/ha-automation-condition-trigger";
import "./types/ha-automation-condition-zone";
export interface ConditionElement extends LitElement {
condition: Condition;
}
const preventDefault = (ev) => ev.preventDefault();
export const handleChangeEvent = (
element: ConditionElement,
ev: CustomEvent
@@ -108,15 +91,6 @@ export default class HaAutomationConditionRow extends LitElement {
@property({ type: Boolean }) public last?: boolean;
@property({ type: Boolean }) public narrow = false;
@state() private _collapsed = false;
@state() private _warnings?: string[];
@property({ type: Boolean, attribute: "sidebar" })
public optionsInSidebar = false;
@storage({
key: "automationClipboard",
state: false,
@@ -127,202 +101,23 @@ export default class HaAutomationConditionRow extends LitElement {
@state() private _yamlMode = false;
@state() private _warnings?: string[];
@state() private _testing = false;
@state() private _testingResult?: boolean;
@state() private _selected = false;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@query("ha-automation-condition-editor")
public conditionEditor?: HaAutomationConditionEditor;
private _renderRow() {
return html`
<ha-svg-icon
slot="leading-icon"
class="condition-icon"
.path=${CONDITION_ICONS[this.condition.condition]}
></ha-svg-icon>
<h3 slot="header">
${capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg)
)}
</h3>
<slot name="icons" slot="icons"></slot>
<ha-md-button-menu
slot="icons"
@click=${preventDefaultStopPropagation}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
>
</ha-icon-button>
<ha-md-menu-item .clickAction=${this._testCondition}>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.test"
)}
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon>
</ha-md-menu-item>
${!this.optionsInSidebar
? html`
<ha-md-menu-item
.clickAction=${this._renameCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
`
: nothing}
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._duplicateCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon slot="start" .path=${mdiContentDuplicate}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._copyCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._cutCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveUp}
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveDown}
.disabled=${this.disabled || this.last}
>
${this.hass.localize("ui.panel.config.automation.editor.move_down")}
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
${!this.optionsInSidebar
? html`<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${this._uiSupported(this.condition.condition) ||
!!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>`
: nothing}
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
${this.condition.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${this.condition.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
class="warning"
.clickAction=${this._onDelete}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
${!this.optionsInSidebar
? html`${this._warnings
? html`<ha-automation-editor-warning
.localize=${this.hass.localize}
.warnings=${this._warnings}
>
</ha-automation-editor-warning>`
: nothing}
<ha-automation-condition-editor
.hass=${this.hass}
.condition=${this.condition}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
.uiSupported=${this._uiSupported(this.condition.condition)}
.narrow=${this.narrow}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-condition-editor>`
: nothing}
`;
}
protected render() {
if (!this.condition) {
return nothing;
}
return html`
<ha-card
outlined
class=${classMap({
selected: this._selected,
"building-block":
this.optionsInSidebar &&
CONDITION_BUILDING_BLOCKS.includes(this.condition.condition) &&
!this._collapsed,
})}
>
<ha-card outlined>
${this.condition.enabled === false
? html`
<div class="disabled-bar">
@@ -331,27 +126,187 @@ export default class HaAutomationConditionRow extends LitElement {
)}
</div>
`
: nothing}
${this.optionsInSidebar
? html`<ha-automation-row
.disabled=${this.condition.enabled === false}
.leftChevron=${CONDITION_BUILDING_BLOCKS.includes(
this.condition.condition
: ""}
<ha-expansion-panel left-chevron>
<ha-svg-icon
slot="leading-icon"
class="condition-icon"
.path=${CONDITION_ICONS[this.condition.condition]}
></ha-svg-icon>
<h3 slot="header">
${capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg)
)}
</h3>
<slot name="icons" slot="icons"></slot>
<ha-md-button-menu
slot="icons"
@click=${preventDefault}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
>
</ha-icon-button>
<ha-md-menu-item .clickAction=${this._testCondition}>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.test"
)}
.collapsed=${this._collapsed}
.selected=${this._selected}
@click=${this._toggleSidebar}
@toggle-collapsed=${this._toggleCollapse}
.buildingBlock=${CONDITION_BUILDING_BLOCKS.includes(
this.condition.condition
<ha-svg-icon slot="start" .path=${mdiFlask}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._renameCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.rename"
)}
>${this._renderRow()}</ha-automation-row
>`
: html`
<ha-expansion-panel left-chevron>
${this._renderRow()}
</ha-expansion-panel>
`}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._duplicateCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="start"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._copyCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._cutCondition}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveUp}
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveDown}
.disabled=${this.disabled || this.last}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled}
>
${this.condition.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${this.condition.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
class="warning"
.clickAction=${this._onDelete}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
<div
class=${classMap({
"card-content": true,
disabled: this.condition.enabled === false,
})}
>
${this._warnings
? html`<ha-alert
alert-type="warning"
.title=${this.hass.localize(
"ui.errors.config.editor_not_supported"
)}
>
${this._warnings!.length > 0 &&
this._warnings![0] !== undefined
? html` <ul>
${this._warnings!.map(
(warning) => html`<li>${warning}</li>`
)}
</ul>`
: ""}
${this.hass.localize(
"ui.errors.config.edit_in_yaml_supported"
)}
</ha-alert>`
: ""}
<ha-automation-condition-editor
@ui-mode-not-available=${this._handleUiModeNotAvailable}
@value-changed=${this._handleChangeEvent}
.yamlMode=${this._yamlMode}
.disabled=${this.disabled}
.hass=${this.hass}
.condition=${this.condition}
></ha-automation-condition-editor>
</div>
</ha-expansion-panel>
<div
class="testing ${classMap({
active: this._testing,
@@ -368,35 +323,21 @@ export default class HaAutomationConditionRow extends LitElement {
)}
</div>
</ha-card>
${this.optionsInSidebar &&
CONDITION_BUILDING_BLOCKS.includes(this.condition.condition) &&
!this._collapsed
? html`<ha-automation-condition-editor
.hass=${this.hass}
.condition=${this.condition}
.disabled=${this.disabled}
.uiSupported=${this._uiSupported(this.condition.condition)}
indent
.selected=${this._selected}
.narrow=${this.narrow}
@value-changed=${this._onValueChange}
></ha-automation-condition-editor>`
: nothing}
`;
}
protected willUpdate(changedProperties) {
// on yaml toggle --> clear warnings
if (changedProperties.has("yamlMode")) {
this._warnings = undefined;
private _handleUiModeNotAvailable(ev: CustomEvent) {
// Prevent possible parent action-row from switching to yamlMode
ev.stopPropagation();
this._warnings = handleStructError(this.hass, ev.detail).warnings;
if (!this._yamlMode) {
this._yamlMode = true;
}
}
private _onValueChange(event: CustomEvent) {
// reload sidebar if sort, deleted,... happend
if (this._selected && this.optionsInSidebar) {
this.openSidebar(event.detail.value);
private _handleChangeEvent(ev: CustomEvent) {
if (ev.detail.yaml) {
this._warnings = undefined;
}
}
@@ -411,11 +352,6 @@ export default class HaAutomationConditionRow extends LitElement {
const enabled = !(this.condition.enabled ?? true);
const value = { ...this.condition, enabled };
fireEvent(this, "value-changed", { value });
this.openSidebar(value); // refresh sidebar
if (this._yamlMode && !this.optionsInSidebar) {
this.conditionEditor?.yamlEditor?.setValue(value);
}
};
private _onDelete = () => {
@@ -431,18 +367,17 @@ export default class HaAutomationConditionRow extends LitElement {
destructive: true,
confirm: () => {
fireEvent(this, "value-changed", { value: null });
if (this._selected) {
fireEvent(this, "close-sidebar");
}
},
});
};
private _switchUiMode() {
this._warnings = undefined;
this._yamlMode = false;
}
private _switchYamlMode() {
this._warnings = undefined;
this._yamlMode = true;
}
@@ -528,12 +463,6 @@ export default class HaAutomationConditionRow extends LitElement {
fireEvent(this, "value-changed", {
value,
});
if (this._selected && this.optionsInSidebar) {
this.openSidebar(value); // refresh sidebar
} else if (this._yamlMode) {
this.conditionEditor?.yamlEditor?.setValue(value);
}
}
};
@@ -548,9 +477,6 @@ export default class HaAutomationConditionRow extends LitElement {
private _cutCondition = () => {
this._setClipboard();
fireEvent(this, "value-changed", { value: null });
if (this._selected) {
fireEvent(this, "close-sidebar");
}
};
private _moveUp = () => {
@@ -567,10 +493,7 @@ export default class HaAutomationConditionRow extends LitElement {
} else {
this._switchYamlMode();
}
if (!this.optionsInSidebar) {
this.expand();
}
this.expand();
};
public expand() {
@@ -579,68 +502,52 @@ export default class HaAutomationConditionRow extends LitElement {
});
}
private _handleUiModeNotAvailable(ev: CustomEvent) {
this._warnings = handleStructError(this.hass, ev.detail).warnings;
if (!this._yamlMode) {
this._yamlMode = true;
}
}
private _toggleSidebar(ev: Event) {
ev?.stopPropagation();
if (this._selected) {
this._selected = false;
fireEvent(this, "close-sidebar");
return;
}
this.openSidebar();
}
public openSidebar(condition?: Condition): void {
if (this.narrow) {
this.scrollIntoView();
}
const sidebarCondition = condition || this.condition;
fireEvent(this, "open-sidebar", {
save: (value) => {
fireEvent(this, "value-changed", { value });
},
close: () => {
this._selected = false;
fireEvent(this, "close-sidebar");
},
rename: () => {
this._renameCondition();
},
toggleYamlMode: () => {
this._toggleYamlMode();
return this._yamlMode;
},
disable: this._onDisable,
delete: this._onDelete,
config: sidebarCondition,
type: "condition",
uiSupported: this._uiSupported(sidebarCondition.condition),
yamlMode: this._yamlMode,
});
this._selected = true;
}
private _uiSupported = memoizeOne(
(type: string) =>
customElements.get(`ha-automation-condition-${type}`) !== undefined
);
private _toggleCollapse() {
this._collapsed = !this._collapsed;
}
static get styles(): CSSResultGroup {
return [
rowStyles,
haStyle,
css`
.disabled {
opacity: 0.5;
pointer-events: none;
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0;
}
h3 {
margin: 0;
font-size: inherit;
font-weight: inherit;
}
.condition-icon {
display: none;
}
@media (min-width: 870px) {
.condition-icon {
display: inline-block;
color: var(--secondary-text-color);
opacity: 0.9;
}
}
.card-content {
padding: 16px;
}
.disabled-bar {
background: var(--divider-color, #e0e0e0);
text-align: center;
border-top-right-radius: calc(
var(--ha-card-border-radius, 12px) - var(
--ha-card-border-width,
1px
)
);
border-top-left-radius: calc(
var(--ha-card-border-radius, 12px) - var(
--ha-card-border-width,
1px
)
);
}
.testing {
position: absolute;
top: 0px;
@@ -655,8 +562,17 @@ export default class HaAutomationConditionRow extends LitElement {
overflow: hidden;
transition: max-height 0.3s;
text-align: center;
border-top-right-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
border-top-left-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
border-top-right-radius: calc(
var(--ha-card-border-radius, 12px) - var(
--ha-card-border-width,
1px
)
);
border-top-left-radius: calc(
var(--ha-card-border-radius, 12px) - var(
--ha-card-border-width,
1px
)
);
}
.testing.active {
@@ -668,6 +584,15 @@ export default class HaAutomationConditionRow extends LitElement {
.testing.pass {
background-color: var(--success-color);
}
ha-md-menu-item > ha-svg-icon {
--mdc-icon-size: 24px;
}
:host([highlight]) ha-card {
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
border-color: var(--state-inactive-color);
box-shadow: var(--shadow-default), var(--shadow-focus);
}
`,
];
}

View File

@@ -23,7 +23,6 @@ import {
} from "../show-add-automation-element-dialog";
import "./ha-automation-condition-row";
import type HaAutomationConditionRow from "./ha-automation-condition-row";
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
@customElement("ha-automation-condition")
export default class HaAutomationCondition extends LitElement {
@@ -35,13 +34,8 @@ export default class HaAutomationCondition extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean }) public root = false;
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
false;
@state() private _showReorder = false;
@state()
@@ -102,15 +96,7 @@ export default class HaAutomationCondition extends LitElement {
"ha-automation-condition-row:last-of-type"
)!;
row.updateComplete.then(() => {
// on new condition open the settings in the sidebar, except for building blocks
if (
this.optionsInSidebar &&
!CONDITION_BUILDING_BLOCKS.includes(row.condition.condition)
) {
row.openSidebar();
} else if (!this.optionsInSidebar) {
row.expand();
}
row.expand();
row.scrollIntoView();
row.focus();
});
@@ -154,14 +140,12 @@ export default class HaAutomationCondition extends LitElement {
.totalConditions=${this.conditions.length}
.condition=${cond}
.disabled=${this.disabled}
.narrow=${this.narrow}
@duplicate=${this._duplicateCondition}
@move-down=${this._moveDown}
@move-up=${this._moveUp}
@value-changed=${this._conditionChanged}
.hass=${this.hass}
?highlight=${this.highlightedConditions?.includes(cond)}
.optionsInSidebar=${this.optionsInSidebar}
>
${this._showReorder && !this.disabled
? html`
@@ -308,7 +292,6 @@ export default class HaAutomationCondition extends LitElement {
// Ensure condition is removed even after update
const conditions = this.conditions.filter((c) => c !== condition);
fireEvent(this, "value-changed", { value: conditions });
fireEvent(this, "close-sidebar");
}
private _conditionChanged(ev: CustomEvent) {
@@ -342,18 +325,15 @@ export default class HaAutomationCondition extends LitElement {
static styles = css`
.conditions {
padding: 16px 0 16px 16px;
padding: 16px;
margin: -16px;
display: flex;
flex-direction: column;
gap: 16px;
}
:host([root]) .conditions {
padding-right: 8px;
}
.sortable-ghost {
background: none;
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
border-radius: var(--ha-card-border-radius, 12px);
}
.sortable-drag {
background: none;
@@ -362,6 +342,12 @@ export default class HaAutomationCondition extends LitElement {
display: block;
scroll-margin-top: 48px;
}
.buttons {
order: 1;
}
ha-svg-icon {
height: 20px;
}
.handle {
padding: 12px;
cursor: move; /* fallback if grab cursor is unsupported */
@@ -375,7 +361,6 @@ export default class HaAutomationCondition extends LitElement {
display: flex;
flex-wrap: wrap;
gap: 8px;
order: 1;
}
`;
}

View File

@@ -17,11 +17,6 @@ export abstract class HaLogicalCondition
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
false;
protected render() {
return html`
<ha-automation-condition
@@ -29,8 +24,6 @@ export abstract class HaLogicalCondition
@value-changed=${this._valueChanged}
.hass=${this.hass}
.disabled=${this.disabled}
.optionsInSidebar=${this.optionsInSidebar}
.narrow=${this.narrow}
></ha-automation-condition>
`;
}

View File

@@ -232,7 +232,6 @@ class DialogNewAutomation extends LitElement implements HassDialog {
ha-dialog {
--dialog-content-padding: 0;
--mdc-dialog-max-height: 60vh;
--mdc-dialog-max-height: 60dvh;
}
@media all and (min-width: 550px) {
ha-dialog {

View File

@@ -1,36 +0,0 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import type { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-alert";
@customElement("ha-automation-editor-warning")
export class HaAutomationEditorWarning extends LitElement {
@property({ attribute: false }) public localize!: LocalizeFunc;
@property({ attribute: "alert-title" }) public alertTitle?: string;
@property({ attribute: false }) public warnings: string[] = [];
protected render() {
return html`
<ha-alert
alert-type="warning"
.title=${this.alertTitle ||
this.localize("ui.errors.config.editor_not_supported")}
>
${this.warnings.length && this.warnings[0] !== undefined
? html`<ul>
${this.warnings.map((warning) => html`<li>${warning}</li>`)}
</ul>`
: nothing}
${this.localize("ui.errors.config.edit_in_yaml_supported")}
</ha-alert>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-editor-warning": HaAutomationEditorWarning;
}
}

View File

@@ -28,9 +28,9 @@ import { navigate } from "../../../common/navigate";
import { computeRTL } from "../../../common/util/compute_rtl";
import { promiseTimeout } from "../../../common/util/promise-timeout";
import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-button-menu";
import "../../../components/ha-fab";
import "../../../components/ha-button";
import "../../../components/ha-fade-in";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
@@ -97,7 +97,6 @@ declare global {
"move-down": undefined;
"move-up": undefined;
duplicate: undefined;
"save-automation": undefined;
}
}
@@ -404,65 +403,61 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
</ha-list-item>
</ha-button-menu>
<div
class=${this._mode === "yaml" ? "yaml-mode" : ""}
class="content ${classMap({
"yaml-mode": this._mode === "yaml",
})}"
@subscribe-automation-config=${this._subscribeAutomationConfig}
>
<div class="error-wrapper">
${this._errors || stateObj?.state === UNAVAILABLE
? html`<ha-alert
alert-type="error"
.title=${stateObj?.state === UNAVAILABLE
? this.hass.localize(
"ui.panel.config.automation.editor.unavailable"
)
: undefined}
>
${this._errors || this._validationErrors}
${stateObj?.state === UNAVAILABLE
? html`<ha-svg-icon
slot="icon"
.path=${mdiRobotConfused}
></ha-svg-icon>`
: nothing}
</ha-alert>`
: ""}
${this._blueprintConfig
? html`<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.automation.editor.confirm_take_control"
${this._errors || stateObj?.state === UNAVAILABLE
? html`<ha-alert
alert-type="error"
.title=${stateObj?.state === UNAVAILABLE
? this.hass.localize(
"ui.panel.config.automation.editor.unavailable"
)
: undefined}
>
${this._errors || this._validationErrors}
${stateObj?.state === UNAVAILABLE
? html`<ha-svg-icon
slot="icon"
.path=${mdiRobotConfused}
></ha-svg-icon>`
: nothing}
</ha-alert>`
: ""}
${this._blueprintConfig
? html`<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.automation.editor.confirm_take_control"
)}
<div slot="action" style="display: flex;">
<ha-button appearance="plain" @click=${this._takeControlSave}
>${this.hass.localize("ui.common.yes")}</ha-button
>
<ha-button appearance="plain" @click=${this._revertBlueprint}
>${this.hass.localize("ui.common.no")}</ha-button
>
</div>
</ha-alert>`
: this._readOnly
? html`<ha-alert alert-type="warning" dismissable
>${this.hass.localize(
"ui.panel.config.automation.editor.read_only"
)}
<div slot="action" style="display: flex;">
<ha-button
appearance="plain"
@click=${this._takeControlSave}
>${this.hass.localize("ui.common.yes")}</ha-button
>
<ha-button
appearance="plain"
@click=${this._revertBlueprint}
>${this.hass.localize("ui.common.no")}</ha-button
>
</div>
</ha-alert>`
: this._readOnly
? html`<ha-alert alert-type="warning" dismissable
>${this.hass.localize(
"ui.panel.config.automation.editor.read_only"
<ha-button
appearance="filled"
size="small"
variant="warning"
slot="action"
@click=${this._duplicate}
>
${this.hass.localize(
"ui.panel.config.automation.editor.migrate"
)}
<ha-button
appearance="filled"
size="small"
variant="warning"
slot="action"
@click=${this._duplicate}
>
${this.hass.localize(
"ui.panel.config.automation.editor.migrate"
)}
</ha-button>
</ha-alert>`
: nothing}
</div>
</ha-button>
</ha-alert>`
: nothing}
${this._mode === "gui"
? html`
<div
@@ -479,10 +474,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.stateObj=${stateObj}
.config=${this._config}
.disabled=${Boolean(this._readOnly)}
.saving=${this._saving}
.dirty=${this._dirty}
@value-changed=${this._valueChanged}
@save-automation=${this._handleSaveAutomation}
></blueprint-automation-editor>
`
: html`
@@ -494,10 +486,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.config=${this._config}
.disabled=${Boolean(this._readOnly)}
.dirty=${this._dirty}
.saving=${this._saving}
@value-changed=${this._valueChanged}
@save-automation=${this._handleSaveAutomation}
@editor-save=${this._handleSaveAutomation}
></manual-automation-editor>
`}
</div>
@@ -528,27 +517,23 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
.defaultValue=${this._preprocessYaml()}
.readOnly=${this._readOnly}
@value-changed=${this._yamlChanged}
@editor-save=${this._handleSaveAutomation}
.showErrors=${false}
disable-fullscreen
></ha-yaml-editor>
<ha-fab
slot="fab"
class=${this._dirty ? "dirty" : ""}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.save"
)}
.disabled=${this._saving}
extended
@click=${this._saveAutomation}
>
<ha-svg-icon
slot="icon"
.path=${mdiContentSave}
></ha-svg-icon>
</ha-fab>`
></ha-yaml-editor>`
: nothing}
</div>
<ha-fab
slot="fab"
class=${classMap({
dirty: !this._readOnly && this._dirty,
})}
.label=${this.hass.localize("ui.panel.config.automation.editor.save")}
.disabled=${this._saving}
extended
@click=${this._handleSaveAutomation}
>
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</ha-fab>
</hass-subpage>
`;
}
@@ -1115,6 +1100,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
align-items: center;
height: 100%;
}
.content {
padding-bottom: 20px;
}
.yaml-mode {
height: 100%;
display: flex;
@@ -1122,34 +1110,13 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
padding-bottom: 0;
}
manual-automation-editor,
blueprint-automation-editor {
blueprint-automation-editor,
:not(.yaml-mode) > ha-alert {
margin: 0 auto;
max-width: 1040px;
padding: 28px 20px 0;
display: block;
}
:not(.yaml-mode) > .error-wrapper {
position: absolute;
top: 4px;
z-index: 3;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
:not(.yaml-mode) > .error-wrapper ha-alert {
background-color: var(--card-background-color);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
border-radius: var(--ha-border-radius-sm);
}
manual-automation-editor {
max-width: 1540px;
padding: 0 12px;
}
ha-yaml-editor {
flex-grow: 1;
--actions-border-radius: 0;
@@ -1166,6 +1133,14 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
margin-inline-end: 8px;
margin-inline-start: initial;
}
ha-fab {
position: relative;
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-fab.dirty {
bottom: 0;
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
}
@@ -1183,15 +1158,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
max-width: 1040px;
padding: 28px 20px 0;
}
ha-fab {
position: fixed;
right: 16px;
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-fab.dirty {
bottom: 16px;
}
`,
];
}

View File

@@ -1,412 +0,0 @@
import {
mdiClose,
mdiDelete,
mdiDotsVertical,
mdiIdentifier,
mdiPlayCircleOutline,
mdiPlaylistEdit,
mdiRenameBox,
mdiStopCircleOutline,
} from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { handleStructError } from "../../../common/structs/handle-errors";
import type { LocalizeKeys } from "../../../common/translations/localize";
import "../../../components/ha-card";
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 { Condition, Trigger } from "../../../data/automation";
import type { Action, RepeatAction } from "../../../data/script";
import { isTriggerList } from "../../../data/trigger";
import type { HomeAssistant } from "../../../types";
import "./action/ha-automation-action-editor";
import { getAutomationActionType } from "./action/ha-automation-action-row";
import { getRepeatType } from "./action/types/ha-automation-action-repeat";
import "./condition/ha-automation-condition-editor";
import type HaAutomationConditionEditor from "./condition/ha-automation-condition-editor";
import "./ha-automation-editor-warning";
import "./trigger/ha-automation-trigger-editor";
import type HaAutomationTriggerEditor from "./trigger/ha-automation-trigger-editor";
import { ACTION_BUILDING_BLOCKS } from "../../../data/action";
import { CONDITION_BUILDING_BLOCKS } from "../../../data/condition";
export interface OpenSidebarConfig {
save: (config: Trigger | Condition | Action) => void;
close: () => void;
rename: () => void;
toggleYamlMode: () => boolean;
disable: () => void;
delete: () => void;
config: Trigger | Condition | Action;
type: "trigger" | "condition" | "action" | "option";
uiSupported: boolean;
yamlMode: boolean;
}
@customElement("ha-automation-sidebar")
export default class HaAutomationSidebar extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public config?: OpenSidebarConfig;
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
@property({ type: Boolean }) public disabled = false;
@state() private _yamlMode = false;
@state() private _requestShowId = false;
@state() private _warnings?: string[];
@query(".sidebar-editor")
public editor?: HaAutomationTriggerEditor | HaAutomationConditionEditor;
protected willUpdate(changedProperties) {
if (changedProperties.has("config")) {
this._requestShowId = false;
this._warnings = undefined;
if (this.config) {
this._yamlMode = this.config.yamlMode;
if (this._yamlMode) {
this.editor?.yamlEditor?.setValue(this.config.config);
}
}
}
}
protected render() {
if (!this.config) {
return nothing;
}
const disabled =
this.disabled ||
("enabled" in this.config.config && this.config.config.enabled === false);
let type = isTriggerList(this.config.config as Trigger)
? "list"
: this.config.type === "action"
? getAutomationActionType(this.config.config as Action)
: this.config.config[this.config.type];
if (this.config.type === "action" && type === "repeat") {
type = `repeat_${getRepeatType((this.config.config as RepeatAction).repeat)}`;
}
const isBuildingBlock = [
...CONDITION_BUILDING_BLOCKS,
...ACTION_BUILDING_BLOCKS,
].includes(type);
const subtitle = this.hass.localize(
(this.config.type === "option"
? "ui.panel.config.automation.editor.actions.type.choose.label"
: `ui.panel.config.automation.editor.${this.config.type}s.${this.config.type}`) as LocalizeKeys
);
const title =
this.hass.localize(
(this.config.type === "option"
? "ui.panel.config.automation.editor.actions.type.choose.option_label"
: `ui.panel.config.automation.editor.${this.config.type}s.type.${type}.label`) as LocalizeKeys
) || type;
const description =
isBuildingBlock || this.config.type === "option"
? this.hass.localize(
(this.config.type === "option"
? "ui.panel.config.automation.editor.actions.type.choose.option_description"
: `ui.panel.config.automation.editor.${this.config.type}s.type.${type}.description.picker`) as LocalizeKeys
)
: "";
return html`
<ha-card
outlined
class=${classMap({
mobile: !this.isWide,
yaml: this._yamlMode,
})}
>
<ha-dialog-header>
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
@click=${this._closeSidebar}
></ha-icon-button>
<span slot="title">${title}</span>
<span slot="subtitle">${subtitle}</span>
<ha-md-button-menu
slot="actionItems"
@click=${this._openOverflowMenu}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item
.clickAction=${this.config.rename}
.disabled=${disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
${this.config.type === "trigger" &&
!this._yamlMode &&
!("id" in this.config.config) &&
!this._requestShowId
? html`<ha-md-menu-item
.clickAction=${this._showTriggerId}
.disabled=${disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
<ha-svg-icon
slot="start"
.path=${mdiIdentifier}
></ha-svg-icon>
</ha-md-menu-item>`
: nothing}
${this.config.type !== "option"
? html`
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!this.config.uiSupported || !!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!this._yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon
slot="start"
.path=${mdiPlaylistEdit}
></ha-svg-icon>
</ha-md-menu-item>
`
: nothing}
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
${this.config.type !== "option"
? html`
<ha-md-menu-item
.clickAction=${this.config.disable}
.disabled=${this.disabled || type === "list"}
>
${disabled
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${disabled
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
`
: nothing}
<ha-md-menu-item
.clickAction=${this.config.delete}
class="warning"
.disabled=${this.disabled}
>
${this.hass.localize(
`ui.panel.config.automation.editor.actions.${this.config.type !== "option" ? "delete" : "type.choose.remove_option"}`
)}
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
</ha-dialog-header>
${this._warnings
? html`<ha-automation-editor-warning
.localize=${this.hass.localize}
.warnings=${this._warnings}
>
</ha-automation-editor-warning>`
: nothing}
<div class="card-content">
${this.config.type === "trigger"
? html`<ha-automation-trigger-editor
class="sidebar-editor"
.hass=${this.hass}
.trigger=${this.config.config as Trigger}
@value-changed=${this._valueChangedSidebar}
.uiSupported=${this.config.uiSupported}
.showId=${this._requestShowId}
.yamlMode=${this._yamlMode}
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-trigger-editor>`
: this.config.type === "condition" &&
(this._yamlMode || !CONDITION_BUILDING_BLOCKS.includes(type))
? html`
<ha-automation-condition-editor
class="sidebar-editor"
.hass=${this.hass}
.condition=${this.config.config as Condition}
.yamlMode=${this._yamlMode}
.uiSupported=${this.config.uiSupported}
@value-changed=${this._valueChangedSidebar}
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-condition-editor>
`
: this.config.type === "action" &&
(this._yamlMode || !ACTION_BUILDING_BLOCKS.includes(type))
? html`
<ha-automation-action-editor
class="sidebar-editor"
.hass=${this.hass}
.action=${this.config.config as Action}
.yamlMode=${this._yamlMode}
.uiSupported=${this.config.uiSupported}
@value-changed=${this._valueChangedSidebar}
sidebar
narrow
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-action-editor>
`
: description || nothing}
</div>
</ha-card>
`;
}
private _handleUiModeNotAvailable(ev: CustomEvent) {
this._warnings = handleStructError(this.hass, ev.detail).warnings;
if (!this._yamlMode) {
this._yamlMode = true;
}
}
private _valueChangedSidebar(ev: CustomEvent) {
ev.stopPropagation();
this.config?.save(ev.detail.value);
if (this.config) {
fireEvent(this, "value-changed", {
value: {
...this.config,
config: ev.detail.value,
},
});
}
}
private _closeSidebar() {
this.config?.close();
}
private _openOverflowMenu(ev: MouseEvent) {
ev.stopPropagation();
ev.preventDefault();
}
private _toggleYamlMode = () => {
this._yamlMode = this.config!.toggleYamlMode();
fireEvent(this, "value-changed", {
value: {
...this.config,
yamlMode: this._yamlMode,
},
});
};
private _showTriggerId = () => {
this._requestShowId = true;
};
static styles = css`
:host {
height: 100%;
--ha-card-border-radius: var(
--ha-dialog-border-radius,
var(--ha-border-radius-2xl)
);
border-radius: var(--ha-card-border-radius);
}
ha-card {
height: 100%;
width: 100%;
border-color: var(--primary-color);
border-width: 2px;
display: block;
}
ha-card.mobile {
border-bottom-right-radius: var(--ha-border-radius-square);
border-bottom-left-radius: var(--ha-border-radius-square);
}
@media all and (max-width: 870px) {
ha-card.mobile {
max-height: 70vh;
max-height: 70dvh;
border-width: 2px 2px 0;
}
ha-card.mobile.yaml {
height: 70vh;
height: 70dvh;
}
}
ha-dialog-header {
border-radius: var(--ha-card-border-radius);
}
.sidebar-editor {
padding-top: 64px;
}
.card-content {
max-height: calc(100% - 80px);
overflow: auto;
}
@media (min-width: 450px) and (min-height: 500px) {
.card-content {
max-height: calc(100% - 104px);
overflow: auto;
}
}
@media all and (max-width: 870px) {
ha-card.mobile .card-content {
max-height: calc(
70vh - 88px - max(var(--safe-area-inset-bottom), 16px)
);
max-height: calc(
70dvh - 88px - max(var(--safe-area-inset-bottom), 16px)
);
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-sidebar": HaAutomationSidebar;
}
}

View File

@@ -1,10 +1,9 @@
import { mdiContentSave, mdiHelpCircle } from "@mdi/js";
import { mdiHelpCircle } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { load } from "js-yaml";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import {
any,
array,
@@ -24,7 +23,7 @@ import {
removeSearchParam,
} from "../../../common/url/search-params";
import "../../../components/ha-button";
import "../../../components/ha-fab";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-markdown";
import type {
@@ -39,6 +38,7 @@ import {
normalizeAutomationConfig,
} from "../../../data/automation";
import { getActionType, type Action } from "../../../data/script";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast";
@@ -46,10 +46,7 @@ import "./action/ha-automation-action";
import type HaAutomationAction from "./action/ha-automation-action";
import "./condition/ha-automation-condition";
import type HaAutomationCondition from "./condition/ha-automation-condition";
import "./ha-automation-sidebar";
import type { OpenSidebarConfig } from "./ha-automation-sidebar";
import { showPasteReplaceDialog } from "./paste-replace-dialog/show-dialog-paste-replace";
import { saveFabStyles } from "./styles";
import "./trigger/ha-automation-trigger";
import type HaAutomationTrigger from "./trigger/ha-automation-trigger";
@@ -80,8 +77,6 @@ export class HaManualAutomationEditor extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public saving = false;
@property({ attribute: false }) public config!: ManualAutomationConfig;
@property({ attribute: false }) public stateObj?: HassEntity;
@@ -90,8 +85,6 @@ export class HaManualAutomationEditor extends LitElement {
@state() private _pastedConfig?: ManualAutomationConfig;
@state() private _sidebarConfig?: OpenSidebarConfig;
private _previousConfig?: ManualAutomationConfig;
public connectedCallback() {
@@ -129,7 +122,7 @@ export class HaManualAutomationEditor extends LitElement {
);
}
private _renderContent() {
protected render() {
return html`
${this.stateObj?.state === "off"
? html`
@@ -137,7 +130,12 @@ export class HaManualAutomationEditor extends LitElement {
${this.hass.localize(
"ui.panel.config.automation.editor.disabled"
)}
<ha-button size="small" slot="action" @click=${this._enable}>
<ha-button
size="small"
appearance="filled"
slot="action"
@click=${this._enable}
>
${this.hass.localize(
"ui.panel.config.automation.editor.enable"
)}
@@ -184,14 +182,10 @@ export class HaManualAutomationEditor extends LitElement {
aria-labelledby="triggers-heading"
.triggers=${this.config.triggers || []}
.highlightedTriggers=${this._pastedConfig?.triggers || []}
.path=${["triggers"]}
@value-changed=${this._triggerChanged}
.hass=${this.hass}
.disabled=${this.disabled || this.saving}
.narrow=${this.narrow}
@open-sidebar=${this._openSidebar}
@close-sidebar=${this._handleCloseSidebar}
root
sidebar
.disabled=${this.disabled}
></ha-automation-trigger>
<div class="header">
@@ -230,14 +224,11 @@ export class HaManualAutomationEditor extends LitElement {
aria-labelledby="conditions-heading"
.conditions=${this.config.conditions || []}
.highlightedConditions=${this._pastedConfig?.conditions || []}
.path=${["conditions"]}
@value-changed=${this._conditionChanged}
.hass=${this.hass}
.disabled=${this.disabled || this.saving}
.narrow=${this.narrow}
@open-sidebar=${this._openSidebar}
@close-sidebar=${this._handleCloseSidebar}
.disabled=${this.disabled}
root
sidebar
></ha-automation-condition>
<div class="header">
@@ -274,82 +265,16 @@ export class HaManualAutomationEditor extends LitElement {
aria-labelledby="actions-heading"
.actions=${this.config.actions || []}
.highlightedActions=${this._pastedConfig?.actions || []}
.path=${["actions"]}
@value-changed=${this._actionChanged}
@open-sidebar=${this._openSidebar}
@close-sidebar=${this._handleCloseSidebar}
.hass=${this.hass}
.narrow=${this.narrow}
.disabled=${this.disabled || this.saving}
.disabled=${this.disabled}
root
sidebar
></ha-automation-action>
`;
}
protected render() {
return html`
<div class="split-view">
<div class="content-wrapper">
<div class="content">${this._renderContent()}</div>
<ha-fab
slot="fab"
class=${this.dirty ? "dirty" : ""}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.save"
)}
.disabled=${this.saving}
extended
@click=${this._saveAutomation}
>
<ha-svg-icon slot="icon" .path=${mdiContentSave}></ha-svg-icon>
</ha-fab>
</div>
<ha-automation-sidebar
class=${classMap({
sidebar: true,
hidden: !this._sidebarConfig,
overlay: !this.isWide,
})}
.isWide=${this.isWide}
.hass=${this.hass}
.config=${this._sidebarConfig}
@value-changed=${this._sidebarConfigChanged}
.disabled=${this.disabled}
></ha-automation-sidebar>
</div>
`;
}
private _openSidebar(ev: CustomEvent<OpenSidebarConfig>) {
// deselect previous selected row
this._sidebarConfig?.close?.();
this._sidebarConfig = ev.detail;
}
private _sidebarConfigChanged(ev: CustomEvent<{ value: OpenSidebarConfig }>) {
ev.stopPropagation();
if (!this._sidebarConfig) {
return;
}
this._sidebarConfig = {
...this._sidebarConfig,
...ev.detail.value,
};
}
private _closeSidebar() {
if (this._sidebarConfig) {
const closeRow = this._sidebarConfig?.close;
this._sidebarConfig = undefined;
closeRow?.();
}
}
private _handleCloseSidebar() {
this._sidebarConfig = undefined;
}
private _triggerChanged(ev: CustomEvent): void {
ev.stopPropagation();
this.resetPastedConfig();
@@ -386,11 +311,6 @@ export class HaManualAutomationEditor extends LitElement {
});
}
private _saveAutomation() {
this._closeSidebar();
fireEvent(this, "save-automation");
}
private _handlePaste = async (ev: ClipboardEvent) => {
if (!canOverrideAlphanumericInput(ev.composedPath())) {
return;
@@ -603,77 +523,14 @@ export class HaManualAutomationEditor extends LitElement {
static get styles(): CSSResultGroup {
return [
saveFabStyles,
haStyle,
css`
:host {
display: block;
}
.split-view {
display: flex;
flex-direction: row;
height: 100%;
position: relative;
gap: 16px;
}
.content-wrapper {
position: relative;
flex: 6;
}
.content {
padding: 32px 16px 64px 0;
height: calc(100vh - 153px);
height: calc(100dvh - 153px);
overflow-y: auto;
overflow-x: hidden;
}
.sidebar {
padding: 12px 0;
flex: 4;
height: calc(100vh - 81px);
height: calc(100dvh - 81px);
width: 40%;
}
.sidebar.hidden {
border-color: transparent;
border-width: 0;
ha-card {
overflow: hidden;
flex: 0;
visibility: hidden;
}
.sidebar.overlay {
position: fixed;
bottom: 0;
right: 0;
height: calc(100% - 64px);
padding: 0;
z-index: 5;
}
@media all and (max-width: 870px) {
.sidebar.overlay {
max-height: 70vh;
max-height: 70dvh;
height: auto;
width: 100%;
box-shadow: 0px -8px 16px rgba(0, 0, 0, 0.2);
}
}
@media all and (max-width: 870px) {
.sidebar.overlay.hidden {
height: 0;
}
}
.sidebar.overlay.hidden {
width: 0;
}
.description {
margin: 0;
}
@@ -702,11 +559,6 @@ export class HaManualAutomationEditor extends LitElement {
font-weight: var(--ha-font-weight-normal);
line-height: 0;
}
ha-alert {
display: block;
margin-bottom: 16px;
}
`,
];
}
@@ -716,9 +568,4 @@ declare global {
interface HTMLElementTagNameMap {
"manual-automation-editor": HaManualAutomationEditor;
}
interface HASSDomEvents {
"open-sidebar": OpenSidebarConfig;
"close-sidebar": undefined;
}
}

View File

@@ -1,4 +1,6 @@
import { consume } from "@lit/context";
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import {
mdiArrowDown,
mdiArrowUp,
@@ -10,19 +12,16 @@ import {
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ensureArray } from "../../../../common/array/ensure-array";
import { fireEvent } from "../../../../common/dom/fire_event";
import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation";
import { preventDefault } from "../../../../common/dom/prevent_default";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import "../../../../components/ha-automation-row";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-list-item";
import type { Condition } from "../../../../data/automation";
import { describeCondition } from "../../../../data/automation_i18n";
import { fullEntitiesContext } from "../../../../data/context";
@@ -32,10 +31,10 @@ import {
showConfirmationDialog,
showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "../action/ha-automation-action";
import "../condition/ha-automation-condition";
import { editorStyles, rowStyles } from "../styles";
@customElement("ha-automation-option-row")
export default class HaAutomationOptionRow extends LitElement {
@@ -53,15 +52,8 @@ export default class HaAutomationOptionRow extends LitElement {
@property({ type: Boolean }) public last = false;
@property({ type: Boolean, attribute: "sidebar" })
public optionsInSidebar = false;
@state() private _expanded = false;
@state() private _selected = false;
@state() private _collapsed = false;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@@ -95,175 +87,144 @@ export default class HaAutomationOptionRow extends LitElement {
return str;
}
private _renderRow() {
return html`
<h3 slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.option",
{ number: this.index + 1 }
)}:
${this.option.alias || (this._expanded ? "" : this._getDescription())}
</h3>
<slot name="icons" slot="icons"></slot>
<ha-md-button-menu
slot="icons"
@click=${preventDefaultStopPropagation}
@closed=${stopPropagation}
@keydown=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
${!this.optionsInSidebar
? html`
<ha-md-menu-item
@click=${this._renameOption}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.rename"
)}
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
`
: nothing}
<ha-md-menu-item
@click=${this._duplicateOption}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
@click=${this._moveUp}
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
@click=${this._moveDown}
.disabled=${this.disabled || this.last}
>
${this.hass.localize("ui.panel.config.automation.editor.move_down")}
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
@click=${this._removeOption}
class="warning"
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
)}
<ha-svg-icon
class="warning"
slot="graphic"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
${!this.optionsInSidebar ? this._renderContent() : nothing}
`;
}
private _renderContent() {
return html`<div
class=${classMap({
"card-content": true,
indent: this.optionsInSidebar,
selected: this._selected,
})}
>
<h4>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.conditions"
)}:
</h4>
<ha-automation-condition
.conditions=${ensureArray<string | Condition>(this.option.conditions)}
.disabled=${this.disabled}
.hass=${this.hass}
.narrow=${this.narrow}
@value-changed=${this._conditionChanged}
.optionsInSidebar=${this.optionsInSidebar}
></ha-automation-condition>
<h4>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.sequence"
)}:
</h4>
<ha-automation-action
.actions=${ensureArray(this.option.sequence) || []}
.disabled=${this.disabled}
.hass=${this.hass}
.narrow=${this.narrow}
@value-changed=${this._actionChanged}
.optionsInSidebar=${this.optionsInSidebar}
></ha-automation-action>
</div>`;
}
protected render() {
if (!this.option) return nothing;
return html`
<ha-card outlined class=${this._selected ? "selected" : ""}>
${this.optionsInSidebar
? html`<ha-automation-row
left-chevron
.collapsed=${this._collapsed}
.selected=${this._selected}
@click=${this._toggleSidebar}
@toggle-collapsed=${this._toggleCollapse}
>${this._renderRow()}</ha-automation-row
>`
: html`
<ha-expansion-panel
left-chevron
@expanded-changed=${this._expandedChanged}
id="option"
>
${this._renderRow()}
</ha-expansion-panel>
`}
</ha-card>
<ha-card outlined>
<ha-expansion-panel
left-chevron
@expanded-changed=${this._expandedChanged}
id="option"
>
<h3 slot="header">
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.option",
{ number: this.index + 1 }
)}:
${this.option.alias ||
(this._expanded ? "" : this._getDescription())}
</h3>
${this.optionsInSidebar && !this._collapsed
? this._renderContent()
: nothing}
<slot name="icons" slot="icons"></slot>
<ha-button-menu
slot="icons"
@action=${this._handleAction}
@click=${preventDefault}
@closed=${stopPropagation}
fixed
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.rename"
)}
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
</ha-list-item>
<ha-list-item graphic="icon" .disabled=${this.disabled}>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.duplicate"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-list-item>
<ha-list-item
graphic="icon"
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="graphic" .path=${mdiArrowUp}></ha-svg-icon>
</ha-list-item>
<ha-list-item
graphic="icon"
.disabled=${this.disabled || this.last}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon slot="graphic" .path=${mdiArrowDown}></ha-svg-icon>
</ha-list-item>
<ha-list-item
class="warning"
graphic="icon"
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.remove_option"
)}
<ha-svg-icon
class="warning"
slot="graphic"
.path=${mdiDelete}
></ha-svg-icon>
</ha-list-item>
</ha-button-menu>
<div class="card-content">
<h4>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.conditions"
)}:
</h4>
<ha-automation-condition
.conditions=${ensureArray<string | Condition>(
this.option.conditions
)}
.disabled=${this.disabled}
.hass=${this.hass}
.narrow=${this.narrow}
@value-changed=${this._conditionChanged}
></ha-automation-condition>
<h4>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.sequence"
)}:
</h4>
<ha-automation-action
.actions=${ensureArray(this.option.sequence) || []}
.disabled=${this.disabled}
.hass=${this.hass}
.narrow=${this.narrow}
@value-changed=${this._actionChanged}
></ha-automation-action>
</div>
</ha-expansion-panel>
</ha-card>
`;
}
private _duplicateOption() {
fireEvent(this, "duplicate");
private async _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
await this._renameOption();
break;
case 1:
fireEvent(this, "duplicate");
break;
case 2:
fireEvent(this, "move-up");
break;
case 3:
fireEvent(this, "move-down");
break;
case 4:
this._removeOption();
break;
}
}
private _moveUp() {
fireEvent(this, "move-up");
}
private _moveDown() {
fireEvent(this, "move-down");
}
private _removeOption = () => {
private _removeOption() {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.delete_confirm_title"
@@ -274,18 +235,14 @@ export default class HaAutomationOptionRow extends LitElement {
dismissText: this.hass.localize("ui.common.cancel"),
confirmText: this.hass.localize("ui.common.delete"),
destructive: true,
confirm: () => {
confirm: () =>
fireEvent(this, "value-changed", {
value: null,
});
if (this._selected) {
fireEvent(this, "close-sidebar");
}
},
}),
});
};
}
private _renameOption = async () => {
private async _renameOption(): Promise<void> {
const alias = await showPromptDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.change_alias"
@@ -309,7 +266,7 @@ export default class HaAutomationOptionRow extends LitElement {
value,
});
}
};
}
private _conditionChanged(ev: CustomEvent) {
ev.stopPropagation();
@@ -329,61 +286,46 @@ export default class HaAutomationOptionRow extends LitElement {
});
}
private _toggleSidebar(ev: Event) {
ev?.stopPropagation();
if (this._selected) {
this._selected = false;
fireEvent(this, "close-sidebar");
return;
}
this.openSidebar();
}
public openSidebar(): void {
if (this.narrow) {
this.scrollIntoView();
}
fireEvent(this, "open-sidebar", {
save: () => {
// nothing to save for an option in the sidebar
},
close: () => {
this._selected = false;
fireEvent(this, "close-sidebar");
},
rename: () => {
this._renameOption();
},
toggleYamlMode: () => false, // no yaml mode for options
disable: () => {
// option cannot be disabled
},
delete: this._removeOption,
config: {},
type: "option",
uiSupported: true,
yamlMode: false,
});
this._selected = true;
}
public expand() {
this.updateComplete.then(() => {
this.shadowRoot!.querySelector("ha-expansion-panel")!.expanded = true;
});
}
private _toggleCollapse() {
this._collapsed = !this._collapsed;
}
static get styles(): CSSResultGroup {
return [
rowStyles,
editorStyles,
haStyle,
css`
ha-button-menu,
ha-icon-button {
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
.disabled {
opacity: 0.5;
pointer-events: none;
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0;
}
h3 {
margin: 0;
font-size: inherit;
font-weight: inherit;
}
.card-content {
padding: 16px;
}
ha-list-item[disabled] {
--mdc-theme-text-primary-on-background: var(--disabled-text-color);
}
ha-list-item.hidden {
display: none;
}
.warning ul {
margin: 4px 0;
}
li[role="separator"] {
border-bottom-color: var(--divider-color);
}

View File

@@ -27,9 +27,6 @@ export default class HaAutomationOption extends LitElement {
@property({ attribute: false }) public options!: Option[];
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
false;
@state() private _showReorder = false;
@state()
@@ -90,7 +87,6 @@ export default class HaAutomationOption extends LitElement {
@move-up=${this._moveUp}
@value-changed=${this._optionChanged}
.hass=${this.hass}
.optionsInSidebar=${this.optionsInSidebar}
>
${this._showReorder && !this.disabled
? html`
@@ -105,7 +101,6 @@ export default class HaAutomationOption extends LitElement {
<div class="buttons">
<ha-button
appearance="filled"
size="small"
.disabled=${this.disabled}
@click=${this._addOption}
>
@@ -130,9 +125,7 @@ export default class HaAutomationOption extends LitElement {
"ha-automation-option-row:last-of-type"
)!;
row.updateComplete.then(() => {
if (!this.optionsInSidebar) {
row.expand();
}
row.expand();
row.scrollIntoView();
row.focus();
});
@@ -245,7 +238,7 @@ export default class HaAutomationOption extends LitElement {
static styles = css`
.options {
padding: 16px 0 16px 16px;
padding: 16px;
margin: -16px;
display: flex;
flex-direction: column;
@@ -253,7 +246,7 @@ export default class HaAutomationOption extends LitElement {
}
.sortable-ghost {
background: none;
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
border-radius: var(--ha-card-border-radius, 12px);
}
.sortable-drag {
background: none;
@@ -262,6 +255,9 @@ export default class HaAutomationOption extends LitElement {
display: block;
scroll-margin-top: 48px;
}
ha-svg-icon {
height: 20px;
}
.handle {
padding: 12px;
cursor: move; /* fallback if grab cursor is unsupported */

View File

@@ -1,40 +1,7 @@
import { fireEvent } from "../../../common/dom/fire_event";
import type { ACTION_GROUPS } from "../../../data/action";
import type { ActionType } from "../../../data/script";
export const PASTE_VALUE = "__paste__";
// These will be replaced with the correct action
export const VIRTUAL_ACTIONS: Record<
keyof (typeof ACTION_GROUPS)["building_blocks"]["members"],
ActionType
> = {
repeat_count: {
repeat: {
count: 2,
sequence: [],
},
},
repeat_while: {
repeat: {
while: [],
sequence: [],
},
},
repeat_until: {
repeat: {
until: [],
sequence: [],
},
},
repeat_for_each: {
repeat: {
for_each: {},
sequence: [],
},
},
} as const;
export interface AddAutomationElementDialogParams {
type: "trigger" | "condition" | "action";
add: (key: string) => void;

View File

@@ -1,90 +0,0 @@
import { css } from "lit";
export const rowStyles = css`
ha-icon-button {
--mdc-theme-text-primary-on-background: var(--primary-text-color);
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0;
}
h3 {
font-size: inherit;
font-weight: inherit;
}
ha-card {
transition: outline 0.2s;
}
.disabled-bar {
background: var(--divider-color, #e0e0e0);
text-align: center;
border-top-right-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
border-top-left-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
}
.warning ul {
margin: 4px 0;
}
ha-md-menu-item > ha-svg-icon {
--mdc-icon-size: 24px;
}
ha-tooltip {
cursor: default;
}
:host([highlight]) ha-card {
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
border-color: var(--state-inactive-color);
box-shadow: var(--shadow-default), var(--shadow-focus);
}
`;
export const editorStyles = css`
.disabled {
opacity: 0.5;
pointer-events: none;
}
.card-content {
padding: 16px;
}
.card-content.yaml {
padding: 0 1px;
border-top: 1px solid var(--divider-color);
border-bottom: 1px solid var(--divider-color);
}
.card-content.indent {
margin-left: 12px;
margin-right: -4px;
padding: 12px 24px 16px 16px;
border-left: 2px solid var(--ha-color-border-neutral-quiet);
}
.card-content.indent.selected,
:host([selected]) .card-content.indent {
border-color: var(--primary-color);
background-color: var(--ha-color-fill-primary-quiet-resting);
border-top-right-radius: var(--ha-border-radius-xl);
border-bottom-right-radius: var(--ha-border-radius-xl);
}
`;
export const saveFabStyles = css`
:host {
overflow: hidden;
}
ha-fab {
position: absolute;
right: 16px;
bottom: calc(-80px - var(--safe-area-inset-bottom));
transition: bottom 0.3s;
}
ha-fab.dirty {
bottom: 16px;
}
`;

View File

@@ -1,166 +0,0 @@
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-textfield";
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 { isTriggerList } from "../../../../data/trigger";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "../ha-automation-editor-warning";
@customElement("ha-automation-trigger-editor")
export default class HaAutomationTriggerEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trigger!: Trigger;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean, attribute: "yaml" }) public yamlMode = false;
@property({ type: Boolean, attribute: "supported" }) public uiSupported =
false;
@property({ type: Boolean, attribute: "show-id" }) public showId = false;
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
protected render() {
const type = isTriggerList(this.trigger) ? "list" : this.trigger.trigger;
const yamlMode = this.yamlMode || !this.uiSupported;
const showId = "id" in this.trigger || this.showId;
return html`
<div
class=${classMap({
"card-content": true,
disabled:
this.disabled ||
("enabled" in this.trigger &&
this.trigger.enabled === false &&
!this.yamlMode),
yaml: yamlMode,
})}
>
${yamlMode
? html`
${!this.uiSupported
? html`
<ha-automation-editor-warning
.alertTitle=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.unsupported_platform",
{ platform: type }
)}
.localize=${this.hass.localize}
></ha-automation-editor-warning>
`
: nothing}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.trigger}
.readOnly=${this.disabled}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
`
: html`
${showId && !isTriggerList(this.trigger)
? html`
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.id"
)}
.value=${this.trigger.id || ""}
.disabled=${this.disabled}
@change=${this._idChanged}
>
</ha-textfield>
`
: ""}
<div @value-changed=${this._onUiChanged}>
${dynamicElement(`ha-automation-trigger-${type}`, {
hass: this.hass,
trigger: this.trigger,
disabled: this.disabled,
})}
</div>
`}
</div>
`;
}
private _idChanged(ev: CustomEvent) {
if (isTriggerList(this.trigger)) return;
const newId = (ev.target as any).value;
if (newId === (this.trigger.id ?? "")) {
return;
}
const value = { ...this.trigger };
if (!newId) {
delete value.id;
} else {
value.id = newId;
}
fireEvent(this, "value-changed", {
value,
});
}
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
if (!ev.detail.isValid) {
return;
}
fireEvent(this, "value-changed", {
value: migrateAutomationTrigger(ev.detail.value),
});
}
private _onUiChanged(ev: CustomEvent) {
if (isTriggerList(this.trigger)) return;
ev.stopPropagation();
const value = {
...(this.trigger.alias ? { alias: this.trigger.alias } : {}),
...ev.detail.value,
};
fireEvent(this, "value-changed", { value });
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.disabled {
opacity: 0.5;
pointer-events: none;
}
.card-content {
padding: 16px;
}
.card-content.yaml {
padding: 0 1px;
border-top: 1px solid var(--divider-color);
border-bottom: 1px solid var(--divider-color);
}
ha-textfield {
display: block;
margin-bottom: 24px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-editor": HaAutomationTriggerEditor;
}
}

View File

@@ -18,24 +18,28 @@ import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { storage } from "../../../../common/decorators/storage";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation";
import { preventDefault } from "../../../../common/dom/prevent_default";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors";
import { debounce } from "../../../../common/util/debounce";
import "../../../../components/ha-alert";
import "../../../../components/ha-automation-row";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-card";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-md-button-menu";
import "../../../../components/ha-md-divider";
import "../../../../components/ha-md-menu-item";
import "../../../../components/ha-textfield";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import type { AutomationClipboard, Trigger } from "../../../../data/automation";
import { subscribeTrigger } from "../../../../data/automation";
import {
migrateAutomationTrigger,
subscribeTrigger,
} from "../../../../data/automation";
import { describeTrigger } from "../../../../data/automation_i18n";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
@@ -46,11 +50,8 @@ import {
showConfirmationDialog,
showPromptDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import "../ha-automation-editor-warning";
import { rowStyles } from "../styles";
import "./ha-automation-trigger-editor";
import type HaAutomationTriggerEditor from "./ha-automation-trigger-editor";
import "./types/ha-automation-trigger-calendar";
import "./types/ha-automation-trigger-conversation";
import "./types/ha-automation-trigger-device";
@@ -108,25 +109,17 @@ export default class HaAutomationTriggerRow extends LitElement {
@property({ type: Boolean }) public last?: boolean;
@property({ type: Boolean, attribute: "sidebar" })
public optionsInSidebar = false;
@state() private _warnings?: string[];
@state() private _yamlMode = false;
@state() private _requestShowId = false;
@state() private _triggered?: Record<string, unknown>;
@state() private _triggerColor = false;
@state() private _selected = false;
@state() private _requestShowId = false;
@state() private _warnings?: string[];
@property({ type: Boolean }) public narrow = false;
@query("ha-automation-trigger-editor")
public triggerEditor?: HaAutomationTriggerEditor;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
@storage({
key: "automationClipboard",
@@ -142,186 +135,19 @@ export default class HaAutomationTriggerRow extends LitElement {
private _triggerUnsub?: Promise<UnsubscribeFunc>;
private _renderRow() {
const type = this._getType(this.trigger);
const supported = this._uiSupported(type);
const yamlMode = this._yamlMode || !supported;
return html`
<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>
<slot name="icons" slot="icons"></slot>
<ha-md-button-menu
slot="icons"
@click=${preventDefaultStopPropagation}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
${!this.optionsInSidebar
? html` <ha-md-menu-item
.clickAction=${this._renameTrigger}
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._showTriggerId}
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
: nothing}
<ha-md-menu-item
.clickAction=${this._duplicateTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.duplicate"
)}
<ha-svg-icon slot="start" .path=${mdiContentDuplicate}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._copyTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._cutTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveUp}
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveDown}
.disabled=${this.disabled || this.last}
>
${this.hass.localize("ui.panel.config.automation.editor.move_down")}
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
${!this.optionsInSidebar
? html`
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!supported || !!this._warnings}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon
slot="start"
.path=${mdiPlaylistEdit}
></ha-svg-icon>
</ha-md-menu-item>
`
: nothing}
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled || type === "list"}
>
${"enabled" in this.trigger && this.trigger.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${"enabled" in this.trigger && this.trigger.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._onDelete}
class="warning"
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
${!this.optionsInSidebar
? html`${this._warnings
? html`<ha-automation-editor-warning
.localize=${this.hass.localize}
.warnings=${this._warnings}
>
</ha-automation-editor-warning>`
: nothing}
<ha-automation-trigger-editor
.hass=${this.hass}
.trigger=${this.trigger}
.disabled=${this.disabled}
.yamlMode=${this._yamlMode}
.showId=${this._requestShowId}
.uiSupported=${supported}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-trigger-editor>`
: nothing}
`;
}
protected render() {
if (!this.trigger) return nothing;
const type = isTriggerList(this.trigger) ? "list" : this.trigger.trigger;
const supported =
customElements.get(`ha-automation-trigger-${type}`) !== undefined;
const yamlMode = this._yamlMode || !supported;
const showId = "id" in this.trigger || this._requestShowId;
return html`
<ha-card outlined class=${this._selected ? "selected" : ""}>
<ha-card outlined>
${"enabled" in this.trigger && this.trigger.enabled === false
? html`
<div class="disabled-bar">
@@ -331,21 +157,223 @@ export default class HaAutomationTriggerRow extends LitElement {
</div>
`
: nothing}
${this.optionsInSidebar
? html`<ha-automation-row
.disabled=${"enabled" in this.trigger &&
this.trigger.enabled === false}
@click=${this._toggleSidebar}
.selected=${this._selected}
>${this._selected
? "selected"
: nothing}${this._renderRow()}</ha-automation-row
>`
: html`
<ha-expansion-panel left-chevron>
${this._renderRow()}
</ha-expansion-panel>
`}
<ha-expansion-panel left-chevron>
<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>
<slot name="icons" slot="icons"></slot>
<ha-md-button-menu
slot="icons"
@click=${preventDefault}
@keydown=${stopPropagation}
@closed=${stopPropagation}
positioning="fixed"
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item
.clickAction=${this._renameTrigger}
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.rename"
)}
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._showTriggerId}
.disabled=${this.disabled || type === "list"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.edit_id"
)}
<ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._duplicateTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.duplicate"
)}
<ha-svg-icon
slot="start"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._copyTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.copy"
)}
<ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._cutTrigger}
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.cut"
)}
<ha-svg-icon slot="start" .path=${mdiContentCut}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveUp}
.disabled=${this.disabled || this.first}
>
${this.hass.localize("ui.panel.config.automation.editor.move_up")}
<ha-svg-icon slot="start" .path=${mdiArrowUp}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._moveDown}
.disabled=${this.disabled || this.last}
>
${this.hass.localize(
"ui.panel.config.automation.editor.move_down"
)}
<ha-svg-icon slot="start" .path=${mdiArrowDown}></ha-svg-icon
></ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._toggleYamlMode}
.disabled=${!supported}
>
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${!yamlMode ? "yaml" : "ui"}`
)}
<ha-svg-icon slot="start" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._onDisable}
.disabled=${this.disabled || type === "list"}
>
${"enabled" in this.trigger && this.trigger.enabled === false
? this.hass.localize(
"ui.panel.config.automation.editor.actions.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.actions.disable"
)}
<ha-svg-icon
slot="start"
.path=${"enabled" in this.trigger &&
this.trigger.enabled === false
? mdiPlayCircleOutline
: mdiStopCircleOutline}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._onDelete}
class="warning"
.disabled=${this.disabled}
>
${this.hass.localize(
"ui.panel.config.automation.editor.actions.delete"
)}
<ha-svg-icon
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
</ha-md-menu-item>
</ha-md-button-menu>
<div
class=${classMap({
"card-content": true,
disabled:
"enabled" in this.trigger && this.trigger.enabled === false,
})}
>
${this._warnings
? html`<ha-alert
alert-type="warning"
.title=${this.hass.localize(
"ui.errors.config.editor_not_supported"
)}
>
${this._warnings.length && this._warnings[0] !== undefined
? html` <ul>
${this._warnings.map(
(warning) => html`<li>${warning}</li>`
)}
</ul>`
: ""}
${this.hass.localize(
"ui.errors.config.edit_in_yaml_supported"
)}
</ha-alert>`
: ""}
${yamlMode
? html`
${!supported
? html`
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.unsupported_platform",
{ platform: type }
)}
`
: ""}
<ha-yaml-editor
.hass=${this.hass}
.defaultValue=${this.trigger}
.readOnly=${this.disabled}
@value-changed=${this._onYamlChange}
></ha-yaml-editor>
`
: html`
${showId && !isTriggerList(this.trigger)
? html`
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.id"
)}
.value=${this.trigger.id || ""}
.disabled=${this.disabled}
@change=${this._idChanged}
>
</ha-textfield>
`
: ""}
<div
@ui-mode-not-available=${this._handleUiModeNotAvailable}
@value-changed=${this._onUiChanged}
>
${dynamicElement(`ha-automation-trigger-${type}`, {
hass: this.hass,
trigger: this.trigger,
disabled: this.disabled,
})}
</div>
`}
</div>
</ha-expansion-panel>
<div
class="triggered ${classMap({
active: this._triggered !== undefined,
@@ -361,13 +389,6 @@ export default class HaAutomationTriggerRow extends LitElement {
`;
}
protected willUpdate(changedProperties) {
// on yaml toggle --> clear warnings
if (changedProperties.has("yamlMode")) {
this._warnings = undefined;
}
}
protected override updated(changedProps: PropertyValues<this>): void {
super.updated(changedProps);
if (changedProps.has("trigger")) {
@@ -453,46 +474,6 @@ export default class HaAutomationTriggerRow extends LitElement {
}
}
private _toggleSidebar(ev: Event) {
ev?.stopPropagation();
if (this._selected) {
this._selected = false;
fireEvent(this, "close-sidebar");
return;
}
this.openSidebar();
}
public openSidebar(trigger?: Trigger): void {
if (this.narrow) {
this.scrollIntoView();
}
fireEvent(this, "open-sidebar", {
save: (value) => {
fireEvent(this, "value-changed", { value });
},
close: () => {
this._selected = false;
fireEvent(this, "close-sidebar");
},
rename: () => {
this._renameTrigger();
},
toggleYamlMode: () => {
this._toggleYamlMode();
return this._yamlMode;
},
disable: this._onDisable,
delete: this._onDelete,
config: trigger || this.trigger,
type: "trigger",
uiSupported: this._uiSupported(this._getType(trigger || this.trigger)),
yamlMode: this._yamlMode,
});
this._selected = true;
}
private _setClipboard() {
this._clipboard = {
...this._clipboard,
@@ -513,10 +494,6 @@ export default class HaAutomationTriggerRow extends LitElement {
destructive: true,
confirm: () => {
fireEvent(this, "value-changed", { value: null });
if (this._selected) {
fireEvent(this, "close-sidebar");
}
},
});
};
@@ -526,18 +503,58 @@ export default class HaAutomationTriggerRow extends LitElement {
const enabled = !(this.trigger.enabled ?? true);
const value = { ...this.trigger, enabled };
fireEvent(this, "value-changed", { value });
this.openSidebar(value); // refresh sidebar
if (this._yamlMode && !this.optionsInSidebar) {
this.triggerEditor?.yamlEditor?.setValue(value);
if (this._yamlMode) {
this._yamlEditor?.setValue(value);
}
};
private _idChanged(ev: CustomEvent) {
if (isTriggerList(this.trigger)) return;
const newId = (ev.target as any).value;
if (newId === (this.trigger.id ?? "")) {
return;
}
this._requestShowId = true;
const value = { ...this.trigger };
if (!newId) {
delete value.id;
} else {
value.id = newId;
}
fireEvent(this, "value-changed", {
value,
});
}
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
if (!ev.detail.isValid) {
return;
}
this._warnings = undefined;
fireEvent(this, "value-changed", {
value: migrateAutomationTrigger(ev.detail.value),
});
}
private _onUiChanged(ev: CustomEvent) {
if (isTriggerList(this.trigger)) return;
ev.stopPropagation();
const value = {
...(this.trigger.alias ? { alias: this.trigger.alias } : {}),
...ev.detail.value,
};
fireEvent(this, "value-changed", { value });
}
private _switchUiMode() {
this._warnings = undefined;
this._yamlMode = false;
}
private _switchYamlMode() {
this._warnings = undefined;
this._yamlMode = true;
}
@@ -584,21 +601,15 @@ export default class HaAutomationTriggerRow extends LitElement {
fireEvent(this, "value-changed", {
value,
});
if (this._selected && this.optionsInSidebar) {
this.openSidebar(value); // refresh sidebar
} else if (this._yamlMode) {
this.triggerEditor?.yamlEditor?.setValue(value);
if (this._yamlMode) {
this._yamlEditor?.setValue(value);
}
}
};
private _showTriggerId = () => {
this._requestShowId = true;
if (!this.optionsInSidebar) {
this.expand();
}
this.expand();
};
private _duplicateTrigger = () => {
@@ -612,9 +623,6 @@ export default class HaAutomationTriggerRow extends LitElement {
private _cutTrigger = () => {
this._setClipboard();
fireEvent(this, "value-changed", { value: null });
if (this._selected) {
fireEvent(this, "close-sidebar");
}
};
private _moveUp = () => {
@@ -631,10 +639,7 @@ export default class HaAutomationTriggerRow extends LitElement {
} else {
this._switchYamlMode();
}
if (!this.optionsInSidebar) {
this.expand();
}
this.expand();
};
public expand() {
@@ -643,19 +648,52 @@ export default class HaAutomationTriggerRow extends LitElement {
});
}
private _getType = memoizeOne((trigger: Trigger) =>
isTriggerList(trigger) ? "list" : trigger.trigger
);
private _uiSupported = memoizeOne(
(type: string) =>
customElements.get(`ha-automation-trigger-${type}`) !== undefined
);
static get styles(): CSSResultGroup {
return [
rowStyles,
haStyle,
css`
.disabled {
opacity: 0.5;
pointer-events: none;
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 0 0 8px;
--expansion-panel-content-padding: 0;
}
h3 {
margin: 0;
font-size: inherit;
font-weight: inherit;
}
.trigger-icon {
display: none;
}
@media (min-width: 870px) {
.trigger-icon {
display: inline-block;
color: var(--secondary-text-color);
opacity: 0.9;
}
}
.card-content {
padding: 16px;
}
.disabled-bar {
background: var(--divider-color, #e0e0e0);
text-align: center;
border-top-right-radius: calc(
var(--ha-card-border-radius, 12px) - var(
--ha-card-border-width,
1px
)
);
border-top-left-radius: calc(
var(--ha-card-border-radius, 12px) - var(
--ha-card-border-width,
1px
)
);
}
.triggered {
cursor: pointer;
position: absolute;
@@ -671,13 +709,17 @@ export default class HaAutomationTriggerRow extends LitElement {
overflow: hidden;
transition: max-height 0.3s;
text-align: center;
border-top-right-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
border-top-right-radius: calc(
var(--ha-card-border-radius, 12px) - var(
--ha-card-border-width,
1px
)
);
border-top-left-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
border-top-left-radius: calc(
var(--ha-card-border-radius, 12px) - var(
--ha-card-border-width,
1px
)
);
}
.triggered.active {
@@ -690,6 +732,19 @@ export default class HaAutomationTriggerRow extends LitElement {
background-color: var(--accent-color);
color: var(--text-accent-color, var(--text-primary-color));
}
ha-textfield {
display: block;
margin-bottom: 24px;
}
ha-md-menu-item > ha-svg-icon {
--mdc-icon-size: 24px;
}
:host([highlight]) ha-card {
--shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
--shadow-focus: 0 0 0 1px var(--state-inactive-color);
border-color: var(--state-inactive-color);
box-shadow: var(--shadow-default), var(--shadow-focus);
}
`,
];
}

View File

@@ -36,13 +36,6 @@ export default class HaAutomationTrigger extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
false;
@property({ type: Boolean }) public root = false;
@state() private _showReorder = false;
@state()
@@ -102,9 +95,7 @@ export default class HaAutomationTrigger extends LitElement {
@value-changed=${this._triggerChanged}
.hass=${this.hass}
.disabled=${this.disabled}
.narrow=${this.narrow}
?highlight=${this.highlightedTriggers?.includes(trg)}
.optionsInSidebar=${this.optionsInSidebar}
>
${this._showReorder && !this.disabled
? html`
@@ -120,8 +111,6 @@ export default class HaAutomationTrigger extends LitElement {
<ha-button
.disabled=${this.disabled}
@click=${this._addTriggerDialog}
.appearance=${this.root ? "accent" : "filled"}
.size=${this.root ? "medium" : "small"}
>
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.add"
@@ -175,11 +164,7 @@ export default class HaAutomationTrigger extends LitElement {
"ha-automation-trigger-row:last-of-type"
)!;
row.updateComplete.then(() => {
if (this.optionsInSidebar) {
row.openSidebar();
} else {
row.expand();
}
row.expand();
row.scrollIntoView();
row.focus();
});
@@ -294,18 +279,15 @@ export default class HaAutomationTrigger extends LitElement {
static styles = css`
.triggers {
padding: 16px 0 16px 16px;
padding: 16px;
margin: -16px;
display: flex;
flex-direction: column;
gap: 16px;
}
:host([root]) .triggers {
padding-right: 8px;
}
.sortable-ghost {
background: none;
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
border-radius: var(--ha-card-border-radius, 12px);
}
.sortable-drag {
background: none;
@@ -314,6 +296,9 @@ export default class HaAutomationTrigger extends LitElement {
display: block;
scroll-margin-top: 48px;
}
ha-svg-icon {
height: 20px;
}
.handle {
padding: 12px;
cursor: move; /* fallback if grab cursor is unsupported */

View File

@@ -2,10 +2,10 @@ import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import { createCloseHeading } from "../../../../components/ha-dialog";
import "../../../../components/ha-form/ha-form";
import "../../../../components/ha-alert";
import type {
HaFormSchema,
SchemaUnion,
@@ -91,7 +91,6 @@ class LocalBackupLocationDialog extends LitElement {
</ha-alert>
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
dialogInitialFocus
>

View File

@@ -2,20 +2,19 @@ import "@material/mwc-button";
import { mdiHelpCircle, mdiStarFourPoints } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import type { HaProgressButton } from "../../../components/buttons/ha-progress-button";
import "../../../components/entity/ha-entity-picker";
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
import "../../../components/ha-card";
import "../../../components/ha-settings-row";
import "../../../components/entity/ha-entity-picker";
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import {
fetchAITaskPreferences,
saveAITaskPreferences,
type AITaskPreferences,
} from "../../../data/ai_task";
import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { documentationUrl } from "../../../util/documentation-url";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
@customElement("ai-task-pref")
export class AITaskPref extends LitElement {
@@ -25,8 +24,6 @@ export class AITaskPref extends LitElement {
@state() private _prefs?: AITaskPreferences;
private _gen_data_entity_id?: string | null;
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (!this.hass || !isComponentLoaded(this.hass, "ai_task")) {
@@ -89,51 +86,30 @@ export class AITaskPref extends LitElement {
.hass=${this.hass}
.disabled=${this._prefs === undefined &&
isComponentLoaded(this.hass, "ai_task")}
.value=${this._gen_data_entity_id ||
this._prefs?.gen_data_entity_id}
.value=${this._prefs?.gen_data_entity_id}
.includeDomains=${["ai_task"]}
@value-changed=${this._handlePrefChange}
></ha-entity-picker>
</ha-settings-row>
</div>
<div class="card-actions">
<ha-progress-button @click=${this._update}>
${this.hass!.localize("ui.common.save")}
</ha-progress-button>
</div>
</ha-card>
`;
}
private _handlePrefChange(ev: CustomEvent<{ value: string | undefined }>) {
private async _handlePrefChange(
ev: CustomEvent<{ value: string | undefined }>
) {
const input = ev.target as HaEntityPicker;
const key = input.dataset.name as keyof AITaskPreferences;
const value = ev.detail.value || null;
this[`_${key}`] = value;
}
private async _update(ev) {
const button = ev.target as HaProgressButton;
if (button.progress) {
return;
}
button.progress = true;
const key = input.getAttribute("data-name") as keyof AITaskPreferences;
const entityId = ev.detail.value || null;
const oldPrefs = this._prefs;
const update: Partial<AITaskPreferences> = {
gen_data_entity_id: this._gen_data_entity_id,
};
this._prefs = { ...this._prefs!, ...update };
this._prefs = { ...this._prefs!, [key]: entityId };
try {
this._prefs = await saveAITaskPreferences(this.hass, {
...update,
[key]: entityId,
});
button.actionSuccess();
} catch (_err: any) {
button.actionError();
this._prefs = oldPrefs;
} finally {
button.progress = false;
}
}
@@ -169,9 +145,6 @@ export class AITaskPref extends LitElement {
direction: var(--direction);
color: var(--secondary-text-color);
}
.card-actions {
text-align: right;
}
ha-entity-picker {
flex: 1;
margin-left: 16px;

View File

@@ -1442,11 +1442,11 @@ export class HaConfigDevicePage extends LitElement {
}
private async _signUrl(ev) {
const a = ev.currentTarget.getAttribute("href")
? ev.currentTarget
: ev.currentTarget.closest("a");
const signedUrl = await getSignedPath(this.hass, a.getAttribute("href"));
const anchor = ev.currentTarget.closest("a");
const signedUrl = await getSignedPath(
this.hass,
anchor.getAttribute("href")
);
fileDownload(signedUrl.path);
}

View File

@@ -755,7 +755,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
iconTrailing
autocapitalize="none"
autocomplete="off"
.autocorrect=${false}
autocorrect="off"
input-spellcheck="false"
>
<div class="layout horizontal" slot="trailingIcon">

View File

@@ -324,7 +324,7 @@ class HaConfigInfo extends LitElement {
.ohf {
text-align: center;
padding-bottom: 5px;
padding-bottom: 0;
}
.ohf img {

View File

@@ -476,13 +476,7 @@ class HaConfigEntryRow extends LitElement {
private async _fetchSubEntries() {
this._subEntries = this.entry.num_subentries
? (await getSubEntries(this.hass, this.entry.entry_id)).sort((a, b) =>
caseInsensitiveStringCompare(
a.title,
b.title,
this.hass.locale.language
)
)
? await getSubEntries(this.hass, this.entry.entry_id)
: undefined;
}

View File

@@ -17,10 +17,6 @@ import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import {
PROTOCOL_INTEGRATIONS,
protocolIntegrationPicked,
} from "../../../common/integrations/protocolIntegrationPicked";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import { nextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
@@ -68,6 +64,10 @@ import "./ha-config-entry-row";
import type { DataEntryFlowProgressExtended } from "./ha-config-integrations";
import { showAddIntegrationDialog } from "./show-add-integration-dialog";
import { showPickConfigEntryDialog } from "./show-pick-config-entry-dialog";
import {
PROTOCOL_INTEGRATIONS,
protocolIntegrationPicked,
} from "../../../common/integrations/protocolIntegrationPicked";
export const renderConfigEntryError = (
hass: HomeAssistant,

Some files were not shown because too many files have changed in this diff Show More