mirror of
https://github.com/home-assistant/frontend.git
synced 2025-12-29 11:27:21 +00:00
Compare commits
2 Commits
grid_secti
...
sequence_d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e642c80003 | ||
|
|
cc07d51613 |
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
||||
14
.github/workflows/ci.yaml
vendored
14
.github/workflows/ci.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
@@ -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.1.1
|
||||
uses: actions/cache@v4.1.0
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.4.0
|
||||
with:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
with:
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.4.0
|
||||
with:
|
||||
name: supervisor-bundle-stats
|
||||
path: build/stats/*.json
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
||||
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
||||
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
|
||||
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.4
|
||||
|
||||
6
.github/workflows/nightly.yaml
vendored
6
.github/workflows/nightly.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v5
|
||||
@@ -57,14 +57,14 @@ jobs:
|
||||
run: tar -czvf translations.tar.gz translations
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.4.0
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
uses: actions/upload-artifact@v4.4.0
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
|
||||
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.2.1
|
||||
uses: actions/checkout@v4.2.0
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
||||
34
package.json
34
package.json
@@ -28,21 +28,21 @@
|
||||
"@babel/runtime": "7.25.7",
|
||||
"@braintree/sanitize-url": "7.1.0",
|
||||
"@codemirror/autocomplete": "6.18.1",
|
||||
"@codemirror/commands": "6.7.0",
|
||||
"@codemirror/commands": "6.6.2",
|
||||
"@codemirror/language": "6.10.3",
|
||||
"@codemirror/legacy-modes": "6.4.1",
|
||||
"@codemirror/search": "6.5.6",
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@codemirror/view": "6.34.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.13.0",
|
||||
"@formatjs/intl-displaynames": "6.6.9",
|
||||
"@formatjs/intl-datetimeformat": "6.12.5",
|
||||
"@formatjs/intl-displaynames": "6.6.8",
|
||||
"@formatjs/intl-getcanonicallocales": "2.3.0",
|
||||
"@formatjs/intl-listformat": "7.5.8",
|
||||
"@formatjs/intl-locale": "4.0.1",
|
||||
"@formatjs/intl-numberformat": "8.11.0",
|
||||
"@formatjs/intl-pluralrules": "5.2.15",
|
||||
"@formatjs/intl-relativetimeformat": "11.2.15",
|
||||
"@formatjs/intl-listformat": "7.5.7",
|
||||
"@formatjs/intl-locale": "4.0.0",
|
||||
"@formatjs/intl-numberformat": "8.10.3",
|
||||
"@formatjs/intl-pluralrules": "5.2.14",
|
||||
"@formatjs/intl-relativetimeformat": "11.2.14",
|
||||
"@fullcalendar/core": "6.1.15",
|
||||
"@fullcalendar/daygrid": "6.1.15",
|
||||
"@fullcalendar/interaction": "6.1.15",
|
||||
@@ -89,8 +89,8 @@
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.4.11",
|
||||
"@vaadin/vaadin-themable-mixin": "24.4.11",
|
||||
"@vaadin/combo-box": "24.4.10",
|
||||
"@vaadin/vaadin-themable-mixin": "24.4.10",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@@ -114,7 +114,7 @@
|
||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||
"home-assistant-js-websocket": "9.4.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.6.0",
|
||||
"intl-messageformat": "10.5.14",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
@@ -151,11 +151,11 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.25.8",
|
||||
"@babel/core": "7.25.7",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.2",
|
||||
"@babel/plugin-proposal-decorators": "7.25.7",
|
||||
"@babel/plugin-transform-runtime": "7.25.7",
|
||||
"@babel/preset-env": "7.25.8",
|
||||
"@babel/preset-env": "7.25.7",
|
||||
"@babel/preset-typescript": "7.25.7",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.15.1",
|
||||
"@koa/cors": "5.0.0",
|
||||
@@ -195,7 +195,7 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.3",
|
||||
"chai": "5.1.1",
|
||||
"del": "8.0.0",
|
||||
"del": "7.1.0",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-airbnb-typescript": "18.0.0",
|
||||
@@ -205,7 +205,7 @@
|
||||
"eslint-plugin-lit": "1.15.0",
|
||||
"eslint-plugin-lit-a11y": "4.1.4",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"eslint-plugin-wc": "2.2.0",
|
||||
"eslint-plugin-wc": "2.1.1",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"glob": "11.0.0",
|
||||
@@ -222,7 +222,7 @@
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
"magic-string": "0.30.12",
|
||||
"magic-string": "0.30.11",
|
||||
"map-stream": "0.0.7",
|
||||
"mocha": "10.5.0",
|
||||
"object-hash": "3.0.0",
|
||||
@@ -240,7 +240,7 @@
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"transform-async-modules-webpack-plugin": "1.1.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.6.3",
|
||||
"typescript": "5.6.2",
|
||||
"webpack": "5.95.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.1.0",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[build-system]
|
||||
requires = ["setuptools~=75.1"]
|
||||
requires = ["setuptools~=68.0", "wheel~=0.40.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20241010.0"
|
||||
version = "20241002.2"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -18,9 +18,5 @@ if [[ -n "$DEVCONTAINER" ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! command -v yarn &> /dev/null; then
|
||||
echo "Error: yarn not found. Please install it following the official instructions: https://yarnpkg.com/getting-started/install" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Install node modules
|
||||
yarn install
|
||||
yarn install
|
||||
|
||||
@@ -20,15 +20,6 @@ function findNestedItem(
|
||||
}, obj);
|
||||
}
|
||||
|
||||
function updateNestedItem(obj: any, path: ItemPath): any {
|
||||
const lastKey = path.pop()!;
|
||||
const parent = findNestedItem(obj, path);
|
||||
parent[lastKey] = Array.isArray(parent[lastKey])
|
||||
? [...parent[lastKey]]
|
||||
: [parent[lastKey]];
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function nestedArrayMove<A>(
|
||||
obj: A,
|
||||
oldIndex: number,
|
||||
@@ -36,18 +27,14 @@ export function nestedArrayMove<A>(
|
||||
oldPath?: ItemPath,
|
||||
newPath?: ItemPath
|
||||
): A {
|
||||
let newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
|
||||
|
||||
if (oldPath) {
|
||||
newObj = updateNestedItem(newObj, [...oldPath]);
|
||||
}
|
||||
if (newPath) {
|
||||
newObj = updateNestedItem(newObj, [...newPath]);
|
||||
}
|
||||
|
||||
const newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
|
||||
const from = oldPath ? findNestedItem(newObj, oldPath) : newObj;
|
||||
const to = newPath ? findNestedItem(newObj, newPath, true) : newObj;
|
||||
|
||||
if (!Array.isArray(from) || !Array.isArray(to)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const item = from.splice(oldIndex, 1)[0];
|
||||
to.splice(newIndex, 0, item);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import "@material/mwc-list/mwc-list-item";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { fullEntitiesContext } from "../../data/context";
|
||||
import {
|
||||
DeviceAutomation,
|
||||
@@ -103,6 +104,7 @@ export abstract class HaDeviceAutomationPicker<
|
||||
.label=${this.label}
|
||||
.value=${value}
|
||||
@selected=${this._automationChanged}
|
||||
@closed=${stopPropagation}
|
||||
.disabled=${this._automations.length === 0}
|
||||
>
|
||||
${value === NO_AUTOMATION_KEY
|
||||
|
||||
@@ -174,6 +174,7 @@ export class HaServiceControl extends LitElement {
|
||||
if (this._value && serviceData) {
|
||||
const loadDefaults = this.value && !("data" in this.value);
|
||||
// Set mandatory bools without a default value to false
|
||||
this._value = { ...this._value };
|
||||
if (!this._value.data) {
|
||||
this._value.data = {};
|
||||
}
|
||||
@@ -499,23 +500,8 @@ export class HaServiceControl extends LitElement {
|
||||
.defaultValue=${this._value?.data}
|
||||
@value-changed=${this._dataChanged}
|
||||
></ha-yaml-editor>`
|
||||
: serviceData?.fields.map((dataField) => {
|
||||
if (!dataField.fields) {
|
||||
return this._renderField(
|
||||
dataField,
|
||||
hasOptional,
|
||||
domain,
|
||||
serviceName,
|
||||
targetEntities
|
||||
);
|
||||
}
|
||||
|
||||
const fields = Object.entries(dataField.fields).map(
|
||||
([key, field]) => ({ key, ...field })
|
||||
);
|
||||
|
||||
return fields.length &&
|
||||
this._hasFilteredFields(fields, targetEntities)
|
||||
: serviceData?.fields.map((dataField) =>
|
||||
dataField.fields
|
||||
? html`<ha-expansion-panel
|
||||
leftChevron
|
||||
.expanded=${!dataField.collapsed}
|
||||
@@ -546,8 +532,14 @@ export class HaServiceControl extends LitElement {
|
||||
)
|
||||
)}
|
||||
</ha-expansion-panel>`
|
||||
: nothing;
|
||||
})} `;
|
||||
: this._renderField(
|
||||
dataField,
|
||||
hasOptional,
|
||||
domain,
|
||||
serviceName,
|
||||
targetEntities
|
||||
)
|
||||
)} `;
|
||||
}
|
||||
|
||||
private _getSectionDescription(
|
||||
@@ -560,16 +552,6 @@ export class HaServiceControl extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _hasFilteredFields(
|
||||
dataFields: ExtHassService["fields"],
|
||||
targetEntities: string[]
|
||||
) {
|
||||
return dataFields.some(
|
||||
(dataField) =>
|
||||
!dataField.filter || this._filterField(dataField.filter, targetEntities)
|
||||
);
|
||||
}
|
||||
|
||||
private _renderField = (
|
||||
dataField: ExtHassService["fields"][number],
|
||||
hasOptional: boolean,
|
||||
|
||||
@@ -167,7 +167,7 @@ export interface TagTrigger extends BaseTrigger {
|
||||
|
||||
export interface TimeTrigger extends BaseTrigger {
|
||||
trigger: "time";
|
||||
at: string | { entity_id: string; offset?: string };
|
||||
at: string;
|
||||
}
|
||||
|
||||
export interface TemplateTrigger extends BaseTrigger {
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
import secondsToDuration from "../common/datetime/seconds_to_duration";
|
||||
import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { isValidEntityId } from "../common/entity/valid_entity_id";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { Condition, ForDict, Trigger } from "./automation";
|
||||
import {
|
||||
@@ -372,22 +371,13 @@ const tryDescribeTrigger = (
|
||||
|
||||
// Time Trigger
|
||||
if (trigger.trigger === "time" && trigger.at) {
|
||||
const result = ensureArray(trigger.at).map((at) => {
|
||||
if (typeof at === "string") {
|
||||
if (isValidEntityId(at)) {
|
||||
return `entity ${hass.states[at] ? computeStateName(hass.states[at]) : at}`;
|
||||
}
|
||||
return localizeTimeString(at, hass.locale, hass.config);
|
||||
}
|
||||
const entityStr = `entity ${hass.states[at.entity_id] ? computeStateName(hass.states[at.entity_id]) : at.entity_id}`;
|
||||
const offsetStr = at.offset
|
||||
? " " +
|
||||
hass.localize(`${triggerTranslationBaseKey}.time.offset_by`, {
|
||||
offset: describeDuration(hass.locale, at.offset),
|
||||
})
|
||||
: "";
|
||||
return `${entityStr}${offsetStr}`;
|
||||
});
|
||||
const result = ensureArray(trigger.at).map((at) =>
|
||||
typeof at !== "string"
|
||||
? at
|
||||
: at.includes(".")
|
||||
? `entity ${hass.states[at] ? computeStateName(hass.states[at]) : at}`
|
||||
: localizeTimeString(at, hass.locale, hass.config)
|
||||
);
|
||||
|
||||
return hass.localize(`${triggerTranslationBaseKey}.time.description.full`, {
|
||||
time: formatListWithOrs(hass.locale, result),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { Action } from "../../script";
|
||||
|
||||
export interface ToggleActionConfig extends BaseActionConfig {
|
||||
action: "toggle";
|
||||
@@ -31,6 +32,11 @@ export interface MoreInfoActionConfig extends BaseActionConfig {
|
||||
entity_id?: string;
|
||||
}
|
||||
|
||||
export interface SequenceActionConfig extends BaseActionConfig {
|
||||
action: "sequence";
|
||||
actions?: Action[];
|
||||
}
|
||||
|
||||
export interface AssistActionConfig extends BaseActionConfig {
|
||||
action: "assist";
|
||||
pipeline_id?: string;
|
||||
@@ -67,4 +73,5 @@ export type ActionConfig =
|
||||
| MoreInfoActionConfig
|
||||
| AssistActionConfig
|
||||
| NoActionConfig
|
||||
| CustomActionConfig;
|
||||
| CustomActionConfig
|
||||
| SequenceActionConfig;
|
||||
|
||||
@@ -17,10 +17,6 @@ export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
|
||||
cards?: LovelaceCardConfig[];
|
||||
}
|
||||
|
||||
export interface LovelaceGridSectionConfig extends LovelaceSectionConfig {
|
||||
grid_base?: number;
|
||||
}
|
||||
|
||||
export interface LovelaceStrategySectionConfig
|
||||
extends LovelaceBaseSectionConfig {
|
||||
strategy: LovelaceStrategyConfig;
|
||||
|
||||
@@ -51,7 +51,6 @@ export class HuiPersistentNotificationItem extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.time {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 6px;
|
||||
|
||||
@@ -208,7 +208,6 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
const options: IFuseOptions<ListItem> = {
|
||||
keys: ["key", "name", "description"],
|
||||
isCaseSensitive: false,
|
||||
ignoreLocation: true,
|
||||
minMatchCharLength: Math.min(filter.length, 2),
|
||||
threshold: 0.2,
|
||||
getFn: getStripDiacriticsFn,
|
||||
|
||||
@@ -9,9 +9,6 @@ import type { TimeTrigger } from "../../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { TriggerElement } from "../ha-automation-trigger-row";
|
||||
|
||||
const MODE_TIME = "time";
|
||||
const MODE_ENTITY = "entity";
|
||||
|
||||
@customElement("ha-automation-trigger-time")
|
||||
export class HaTimeTrigger extends LitElement implements TriggerElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -20,60 +17,48 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _inputMode:
|
||||
| undefined
|
||||
| typeof MODE_TIME
|
||||
| typeof MODE_ENTITY;
|
||||
@state() private _inputMode?: boolean;
|
||||
|
||||
public static get defaultConfig(): TimeTrigger {
|
||||
return { trigger: "time", at: "" };
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
inputMode: typeof MODE_TIME | typeof MODE_ENTITY,
|
||||
showOffset: boolean
|
||||
) =>
|
||||
[
|
||||
(localize: LocalizeFunc, inputMode?: boolean) => {
|
||||
const atSelector = inputMode
|
||||
? {
|
||||
entity: {
|
||||
filter: [
|
||||
{ domain: "input_datetime" },
|
||||
{ domain: "sensor", device_class: "timestamp" },
|
||||
],
|
||||
},
|
||||
}
|
||||
: { time: {} };
|
||||
|
||||
return [
|
||||
{
|
||||
name: "mode",
|
||||
type: "select",
|
||||
required: true,
|
||||
options: [
|
||||
[
|
||||
MODE_TIME,
|
||||
"value",
|
||||
localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.time.type_value"
|
||||
),
|
||||
],
|
||||
[
|
||||
MODE_ENTITY,
|
||||
"input",
|
||||
localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.time.type_input"
|
||||
),
|
||||
],
|
||||
],
|
||||
},
|
||||
...(inputMode === MODE_TIME
|
||||
? ([{ name: "time", selector: { time: {} } }] as const)
|
||||
: ([
|
||||
{
|
||||
name: "entity",
|
||||
selector: {
|
||||
entity: {
|
||||
filter: [
|
||||
{ domain: "input_datetime" },
|
||||
{ domain: "sensor", device_class: "timestamp" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const)),
|
||||
...(showOffset
|
||||
? ([{ name: "offset", selector: { text: {} } }] as const)
|
||||
: ([] as const)),
|
||||
] as const
|
||||
{ name: "at", selector: atSelector },
|
||||
] as const;
|
||||
}
|
||||
);
|
||||
|
||||
public willUpdate(changedProperties: PropertyValues) {
|
||||
@@ -90,46 +75,23 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _data = memoizeOne(
|
||||
(
|
||||
inputMode: undefined | typeof MODE_ENTITY | typeof MODE_TIME,
|
||||
at:
|
||||
| string
|
||||
| { entity_id: string | undefined; offset?: string | undefined }
|
||||
): {
|
||||
mode: typeof MODE_TIME | typeof MODE_ENTITY;
|
||||
entity: string | undefined;
|
||||
time: string | undefined;
|
||||
offset: string | undefined;
|
||||
} => {
|
||||
const entity =
|
||||
typeof at === "object"
|
||||
? at.entity_id
|
||||
: at?.startsWith("input_datetime.") || at?.startsWith("sensor.")
|
||||
? at
|
||||
: undefined;
|
||||
const time = entity ? undefined : (at as string | undefined);
|
||||
const offset = typeof at === "object" ? at.offset : undefined;
|
||||
const mode = inputMode ?? (entity ? MODE_ENTITY : MODE_TIME);
|
||||
return {
|
||||
mode,
|
||||
entity,
|
||||
time,
|
||||
offset,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const at = this.trigger.at;
|
||||
|
||||
if (Array.isArray(at)) {
|
||||
return nothing;
|
||||
}
|
||||
const data = this._data(this._inputMode, at);
|
||||
const showOffset =
|
||||
data.mode === MODE_ENTITY && data.entity?.startsWith("sensor.");
|
||||
const schema = this._schema(this.hass.localize, data.mode, !!showOffset);
|
||||
|
||||
const inputMode =
|
||||
this._inputMode ??
|
||||
(at?.startsWith("input_datetime.") || at?.startsWith("sensor."));
|
||||
|
||||
const schema = this._schema(this.hass.localize, inputMode);
|
||||
|
||||
const data = {
|
||||
mode: inputMode ? "input" : "value",
|
||||
...this.trigger,
|
||||
};
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
@@ -145,43 +107,26 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const newValue = { ...ev.detail.value };
|
||||
this._inputMode = newValue.mode;
|
||||
if (newValue.mode === MODE_TIME) {
|
||||
delete newValue.entity;
|
||||
delete newValue.offset;
|
||||
} else {
|
||||
delete newValue.time;
|
||||
if (!newValue.entity?.startsWith("sensor.")) {
|
||||
delete newValue.offset;
|
||||
}
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.trigger,
|
||||
at: newValue.offset
|
||||
? {
|
||||
entity_id: newValue.entity,
|
||||
offset: newValue.offset,
|
||||
}
|
||||
: newValue.entity || newValue.time,
|
||||
},
|
||||
});
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
this._inputMode = newValue.mode === "input";
|
||||
delete newValue.mode;
|
||||
|
||||
Object.keys(newValue).forEach((key) =>
|
||||
newValue[key] === undefined || newValue[key] === ""
|
||||
? delete newValue[key]
|
||||
: {}
|
||||
);
|
||||
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string => {
|
||||
switch (schema.name) {
|
||||
case "time":
|
||||
return this.hass.localize(
|
||||
`ui.panel.config.automation.editor.triggers.type.time.at`
|
||||
);
|
||||
}
|
||||
return this.hass.localize(
|
||||
): string =>
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.triggers.type.time.${schema.name}`
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -83,15 +83,9 @@ export const getZHADeviceActions = async (
|
||||
classes: "warning",
|
||||
action: async () => {
|
||||
const confirmed = await showConfirmationDialog(el, {
|
||||
title: hass.localize(
|
||||
"ui.dialogs.zha_device_info.confirmations.remove_title"
|
||||
),
|
||||
text: hass.localize(
|
||||
"ui.dialogs.zha_device_info.confirmations.remove_text"
|
||||
"ui.dialogs.zha_device_info.confirmations.remove"
|
||||
),
|
||||
confirmText: hass.localize("ui.common.remove"),
|
||||
dismissText: hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
|
||||
@@ -482,9 +482,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
const network = (ev.currentTarget as any).network as ThreadNetwork;
|
||||
const router = (ev.currentTarget as any).router as ThreadRouter;
|
||||
const otbr = (ev.currentTarget as any).otbr as OTBRInfo;
|
||||
const index = network.dataset
|
||||
? Number(ev.detail.index)
|
||||
: Number(ev.detail.index) + 1;
|
||||
const index = Number(ev.detail.index);
|
||||
switch (index) {
|
||||
case 0:
|
||||
this._setPreferredBorderAgent(network.dataset!, router);
|
||||
|
||||
@@ -83,6 +83,8 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
@state() private _config?: ScriptConfig;
|
||||
|
||||
@state() private _idError = false;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@state() private _errors?: string;
|
||||
@@ -412,18 +414,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
this._loadConfig();
|
||||
}
|
||||
|
||||
if (
|
||||
(changedProps.has("scriptId") || changedProps.has("entityRegistry")) &&
|
||||
this.scriptId &&
|
||||
this.entityRegistry
|
||||
) {
|
||||
// find entity for when script entity id changed
|
||||
const entity = this.entityRegistry.find(
|
||||
(ent) => ent.platform === "script" && ent.unique_id === this.scriptId
|
||||
);
|
||||
this._entityId = entity?.entity_id;
|
||||
}
|
||||
|
||||
if (changedProps.has("scriptId") && !this.scriptId && this.hass) {
|
||||
const initData = getScriptEditorInitData();
|
||||
this._dirty = !!initData;
|
||||
@@ -458,6 +448,15 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _setEntityId(id?: string) {
|
||||
this._entityId = id;
|
||||
if (this.hass.states[`script.${this._entityId}`]) {
|
||||
this._idError = true;
|
||||
} else {
|
||||
this._idError = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async _checkValidation() {
|
||||
this._validationErrors = undefined;
|
||||
if (!this._entityId || !this._config) {
|
||||
@@ -767,12 +766,28 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _saveScript(): Promise<void> {
|
||||
if (this._idError) {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.script.editor.id_already_exists_save_error"
|
||||
),
|
||||
dismissable: false,
|
||||
duration: -1,
|
||||
action: {
|
||||
action: () => {},
|
||||
text: this.hass.localize("ui.dialogs.generic.ok"),
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.scriptId) {
|
||||
const saved = await this._promptScriptAlias();
|
||||
if (!saved) {
|
||||
return;
|
||||
}
|
||||
this._entityId = this._computeEntityIdFromAlias(this._config!.alias);
|
||||
const entityId = this._computeEntityIdFromAlias(this._config!.alias);
|
||||
this._setEntityId(entityId);
|
||||
}
|
||||
const id = this.scriptId || this._entityId || Date.now();
|
||||
|
||||
|
||||
@@ -68,16 +68,6 @@ class HaPanelDevAction extends LitElement {
|
||||
|
||||
@query("#yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||
|
||||
protected willUpdate() {
|
||||
if (
|
||||
!this.hasUpdated &&
|
||||
this._serviceData?.action &&
|
||||
typeof this._serviceData.action !== "string"
|
||||
) {
|
||||
this._serviceData.action = "";
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(params) {
|
||||
super.firstUpdated(params);
|
||||
this.hass.loadBackendTranslation("services");
|
||||
|
||||
@@ -187,6 +187,7 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
.content p {
|
||||
margin: 0;
|
||||
font-family: Roboto;
|
||||
font-style: normal;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -275,7 +275,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
"ui.panel.lovelace.cards.todo-list.no_unchecked_items"
|
||||
)}
|
||||
</p>`}
|
||||
${!this._config.hide_completed && checkedItems.length
|
||||
${checkedItems.length
|
||||
? html`
|
||||
<div role="separator">
|
||||
<div class="divider"></div>
|
||||
|
||||
@@ -453,7 +453,6 @@ export interface TodoListCardConfig extends LovelaceCardConfig {
|
||||
title?: string;
|
||||
theme?: string;
|
||||
entity?: string;
|
||||
hide_completed?: boolean;
|
||||
}
|
||||
|
||||
export interface StackCardConfig extends LovelaceCardConfig {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { navigate } from "../../../common/navigate";
|
||||
import { forwardHaptic } from "../../../data/haptics";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import { ActionConfig } from "../../../data/lovelace/config/action";
|
||||
import { callExecuteScript } from "../../../data/service";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showVoiceCommandDialog } from "../../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
@@ -177,6 +178,13 @@ export const handleAction = async (
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "sequence": {
|
||||
if (!actionConfig.actions) {
|
||||
return;
|
||||
}
|
||||
callExecuteScript(hass, actionConfig.actions);
|
||||
break;
|
||||
}
|
||||
case "fire-dom-event": {
|
||||
fireEvent(node, "ll-custom", actionConfig);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ContextProvider } from "@lit-labs/context";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@@ -14,7 +16,10 @@ import "../../../components/ha-assist-pipeline-picker";
|
||||
import { HaFormSchema, SchemaUnion } from "../../../components/ha-form/types";
|
||||
import "../../../components/ha-help-tooltip";
|
||||
import "../../../components/ha-navigation-picker";
|
||||
import { HaSelect } from "../../../components/ha-select";
|
||||
import "../../../components/ha-service-control";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import { subscribeEntityRegistry } from "../../../data/entity_registry";
|
||||
import {
|
||||
ActionConfig,
|
||||
CallServiceActionConfig,
|
||||
@@ -22,9 +27,9 @@ import {
|
||||
UrlActionConfig,
|
||||
} from "../../../data/lovelace/config/action";
|
||||
import { ServiceAction } from "../../../data/script";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { EditorTarget } from "../editor/types";
|
||||
import { HaSelect } from "../../../components/ha-select";
|
||||
|
||||
export type UiAction = Exclude<ActionConfig["action"], "fire-dom-event">;
|
||||
|
||||
@@ -34,6 +39,7 @@ const DEFAULT_ACTIONS: UiAction[] = [
|
||||
"navigate",
|
||||
"url",
|
||||
"perform-action",
|
||||
"sequence",
|
||||
"assist",
|
||||
"none",
|
||||
];
|
||||
@@ -70,8 +76,17 @@ const ASSIST_SCHEMA = [
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[];
|
||||
|
||||
const SEQUENCE_SCHEMA = [
|
||||
{
|
||||
name: "actions",
|
||||
selector: {
|
||||
action: {},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[];
|
||||
|
||||
@customElement("hui-action-editor")
|
||||
export class HuiActionEditor extends LitElement {
|
||||
export class HuiActionEditor extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public config?: ActionConfig;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -86,6 +101,19 @@ export class HuiActionEditor extends LitElement {
|
||||
|
||||
@query("ha-select") private _select!: HaSelect;
|
||||
|
||||
private _entitiesContext = new ContextProvider(this, {
|
||||
context: fullEntitiesContext,
|
||||
initialValue: [],
|
||||
});
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass!.connection!, (entities) => {
|
||||
this._entitiesContext.setValue(entities);
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
get _navigation_path(): string {
|
||||
const config = this.config as NavigateActionConfig | undefined;
|
||||
return config?.navigation_path || "";
|
||||
@@ -120,6 +148,11 @@ export class HuiActionEditor extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||
this.hass!.loadFragmentTranslation("config");
|
||||
this.hass!.loadBackendTranslation("device_automation");
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
@@ -218,6 +251,17 @@ export class HuiActionEditor extends LitElement {
|
||||
</ha-form>
|
||||
`
|
||||
: nothing}
|
||||
${this.config?.action === "sequence"
|
||||
? html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.schema=${SEQUENCE_SCHEMA}
|
||||
.data=${this.config}
|
||||
.computeLabel=${this._computeFormLabel}
|
||||
@value-changed=${this._formValueChanged}
|
||||
></ha-form>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -289,7 +333,15 @@ export class HuiActionEditor extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _computeFormLabel(schema: SchemaUnion<typeof ASSIST_SCHEMA>) {
|
||||
private _computeFormLabel(
|
||||
schema:
|
||||
| SchemaUnion<typeof ASSIST_SCHEMA>
|
||||
| SchemaUnion<typeof NAVIGATE_SCHEMA>
|
||||
| SchemaUnion<typeof SEQUENCE_SCHEMA>
|
||||
) {
|
||||
if (schema.name === "actions") {
|
||||
return "";
|
||||
}
|
||||
return this.hass?.localize(
|
||||
`ui.panel.lovelace.editor.action-editor.${schema.name}`
|
||||
);
|
||||
|
||||
@@ -3,15 +3,12 @@ import "@material/mwc-tab/mwc-tab";
|
||||
import { CSSResultGroup, TemplateResult, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import {
|
||||
LovelaceGridSectionConfig,
|
||||
LovelaceSectionConfig,
|
||||
} from "../../../../data/lovelace/config/section";
|
||||
import { getCardElementClass } from "../../create-element/create-card-element";
|
||||
import type { LovelaceCardEditor, LovelaceConfigForm } from "../../types";
|
||||
import { HuiTypedElementEditor } from "../hui-typed-element-editor";
|
||||
import "./hui-card-layout-editor";
|
||||
import "./hui-card-visibility-editor";
|
||||
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
|
||||
const tabs = ["config", "visibility", "layout"] as const;
|
||||
|
||||
@@ -62,7 +59,7 @@ export class HuiCardElementEditor extends HuiTypedElementEditor<LovelaceCardConf
|
||||
protected renderConfigElement(): TemplateResult {
|
||||
const displayedTabs: string[] = ["config"];
|
||||
if (this.showVisibilityTab) displayedTabs.push("visibility");
|
||||
if (this.sectionConfig?.type === "grid") displayedTabs.push("layout");
|
||||
if (this._showLayoutTab) displayedTabs.push("layout");
|
||||
|
||||
if (displayedTabs.length === 1) return super.renderConfigElement();
|
||||
|
||||
@@ -86,8 +83,8 @@ export class HuiCardElementEditor extends HuiTypedElementEditor<LovelaceCardConf
|
||||
<hui-card-layout-editor
|
||||
.hass=${this.hass}
|
||||
.config=${this.value}
|
||||
.sectionConfig=${this.sectionConfig!}
|
||||
@value-changed=${this._configChanged}
|
||||
.sectionConfig=${this.sectionConfig as LovelaceGridSectionConfig}
|
||||
>
|
||||
</hui-card-layout-editor>
|
||||
`;
|
||||
|
||||
@@ -19,10 +19,7 @@ import "../../../../components/ha-switch";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import {
|
||||
LovelaceGridSectionConfig,
|
||||
LovelaceSectionConfig,
|
||||
} from "../../../../data/lovelace/config/section";
|
||||
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { HuiCard } from "../../cards/hui-card";
|
||||
@@ -30,7 +27,6 @@ import {
|
||||
CardGridSize,
|
||||
computeCardGridSize,
|
||||
} from "../../common/compute-card-grid-size";
|
||||
import { DEFAULT_GRID_BASE } from "../../sections/hui-grid-section";
|
||||
import { LovelaceLayoutOptions } from "../../types";
|
||||
|
||||
@customElement("hui-card-layout-editor")
|
||||
@@ -76,10 +72,7 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
|
||||
const value = this._computeCardGridSize(options);
|
||||
|
||||
const totalColumns =
|
||||
(this.sectionConfig.column_span ?? 1) *
|
||||
((this.sectionConfig as LovelaceGridSectionConfig).grid_base ||
|
||||
DEFAULT_GRID_BASE);
|
||||
const totalColumns = (this.sectionConfig.column_span ?? 1) * 4;
|
||||
|
||||
return html`
|
||||
<div class="header">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { assert, assign, boolean, object, optional, string } from "superstruct";
|
||||
import { assert, assign, object, optional, string } from "superstruct";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-alert";
|
||||
@@ -18,7 +18,6 @@ const cardConfigStruct = assign(
|
||||
title: optional(string()),
|
||||
theme: optional(string()),
|
||||
entity: optional(string()),
|
||||
hide_completed: optional(boolean()),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -31,7 +30,6 @@ const SCHEMA = [
|
||||
},
|
||||
},
|
||||
{ name: "theme", selector: { theme: {} } },
|
||||
{ name: "hide_completed", selector: { boolean: {} } },
|
||||
] as const;
|
||||
|
||||
@customElement("hui-todo-list-card-editor")
|
||||
@@ -89,10 +87,6 @@ export class HuiTodoListEditor
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
case "hide_completed":
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.todo-list.hide_completed"
|
||||
);
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
@@ -6,21 +6,12 @@ import {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import {
|
||||
isStrategySection,
|
||||
LovelaceGridSectionConfig,
|
||||
LovelaceSectionRawConfig,
|
||||
} from "../../../../data/lovelace/config/section";
|
||||
import { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import { DEFAULT_GRID_BASE } from "../../sections/hui-grid-section";
|
||||
|
||||
type GridDensity = "default" | "dense" | "custom";
|
||||
|
||||
type SettingsData = {
|
||||
column_span?: number;
|
||||
grid_density?: GridDensity;
|
||||
};
|
||||
|
||||
@customElement("hui-section-settings-editor")
|
||||
@@ -32,89 +23,27 @@ export class HuiDialogEditSection extends LitElement {
|
||||
@property({ attribute: false }) public viewConfig!: LovelaceViewConfig;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(
|
||||
maxColumns: number,
|
||||
localize: LocalizeFunc,
|
||||
type?: string | undefined,
|
||||
columnDensity?: GridDensity,
|
||||
columnBase?: number
|
||||
) =>
|
||||
(maxColumns: number) =>
|
||||
[
|
||||
{
|
||||
name: "title",
|
||||
selector: { text: {} },
|
||||
name: "column_span",
|
||||
selector: {
|
||||
number: {
|
||||
min: 1,
|
||||
max: maxColumns,
|
||||
slider_ticks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
...(type === "grid"
|
||||
? ([
|
||||
{
|
||||
name: "grid_density",
|
||||
default: "default",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "list",
|
||||
options: [
|
||||
{
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.edit_section.settings.grid_density_options.default`,
|
||||
{ count: 4 }
|
||||
),
|
||||
value: "default",
|
||||
},
|
||||
{
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.edit_section.settings.grid_density_options.dense`,
|
||||
{ count: 6 }
|
||||
),
|
||||
value: "dense",
|
||||
},
|
||||
...(columnDensity === "custom" && columnBase
|
||||
? [
|
||||
{
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.edit_section.settings.grid_density_options.custom`,
|
||||
{ count: columnBase }
|
||||
),
|
||||
value: "custom",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
] as const satisfies HaFormSchema[]
|
||||
);
|
||||
|
||||
private _isGridSectionConfig(
|
||||
config: LovelaceSectionRawConfig
|
||||
): config is LovelaceGridSectionConfig {
|
||||
return !isStrategySection(config) && config.type === "grid";
|
||||
}
|
||||
|
||||
render() {
|
||||
const gridBase = this._isGridSectionConfig(this.config)
|
||||
? this.config.grid_base || DEFAULT_GRID_BASE
|
||||
: undefined;
|
||||
|
||||
const columnDensity =
|
||||
gridBase === 6 ? "dense" : gridBase === 4 ? "default" : "custom";
|
||||
|
||||
const data: SettingsData = {
|
||||
column_span: this.config.column_span || 1,
|
||||
grid_density: columnDensity,
|
||||
};
|
||||
|
||||
const type = "type" in this.config ? this.config.type : undefined;
|
||||
|
||||
const schema = this._schema(
|
||||
this.viewConfig.max_columns || 4,
|
||||
this.hass.localize,
|
||||
type,
|
||||
columnDensity,
|
||||
gridBase
|
||||
);
|
||||
const schema = this._schema(this.viewConfig.max_columns || 4);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
@@ -146,26 +75,11 @@ export class HuiDialogEditSection extends LitElement {
|
||||
ev.stopPropagation();
|
||||
const newData = ev.detail.value as SettingsData;
|
||||
|
||||
const { column_span, grid_density } = newData;
|
||||
|
||||
const newConfig: LovelaceSectionRawConfig = {
|
||||
...this.config,
|
||||
column_span: column_span,
|
||||
column_span: newData.column_span,
|
||||
};
|
||||
|
||||
if (this._isGridSectionConfig(newConfig)) {
|
||||
const gridBase =
|
||||
grid_density === "default"
|
||||
? 4
|
||||
: grid_density === "dense"
|
||||
? 6
|
||||
: undefined;
|
||||
|
||||
if (gridBase) {
|
||||
(newConfig as LovelaceGridSectionConfig).grid_base = gridBase;
|
||||
}
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value: newConfig });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,12 @@ const actionConfigStructService = object({
|
||||
confirmation: optional(actionConfigStructConfirmation),
|
||||
});
|
||||
|
||||
const actionConfigStructSequence = object({
|
||||
action: literal("sequence"),
|
||||
actions: optional(array(object())),
|
||||
confirmation: optional(actionConfigStructConfirmation),
|
||||
});
|
||||
|
||||
const actionConfigStructNavigate = object({
|
||||
action: literal("navigate"),
|
||||
navigation_path: string(),
|
||||
@@ -101,6 +107,9 @@ export const actionConfigStruct = dynamic<any>((value) => {
|
||||
case "more-info": {
|
||||
return actionConfigStructMoreInfo;
|
||||
}
|
||||
case "sequence": {
|
||||
return actionConfigStructSequence;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { HaSortableOptions } from "../../../components/ha-sortable";
|
||||
import { LovelaceSectionElement } from "../../../data/lovelace";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type { LovelaceGridSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { HuiCard } from "../cards/hui-card";
|
||||
@@ -24,8 +24,6 @@ const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
|
||||
invertedSwapThreshold: 0.7,
|
||||
} as HaSortableOptions;
|
||||
|
||||
export const DEFAULT_GRID_BASE = 4;
|
||||
|
||||
export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@@ -39,11 +37,11 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
|
||||
@property({ attribute: false }) public cards: HuiCard[] = [];
|
||||
|
||||
@state() _config?: LovelaceGridSectionConfig;
|
||||
@state() _config?: LovelaceSectionConfig;
|
||||
|
||||
@state() _dragging = false;
|
||||
|
||||
public setConfig(config: LovelaceGridSectionConfig): void {
|
||||
public setConfig(config: LovelaceSectionConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
@@ -66,8 +64,6 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
|
||||
const editMode = Boolean(this.lovelace?.editMode && !this.isStrategy);
|
||||
|
||||
const columnCount = this._config.grid_base ?? DEFAULT_GRID_BASE;
|
||||
|
||||
return html`
|
||||
<ha-sortable
|
||||
.disabled=${!editMode}
|
||||
@@ -81,10 +77,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
.options=${CARD_SORTABLE_OPTIONS}
|
||||
invert-swap
|
||||
>
|
||||
<div
|
||||
class="container ${classMap({ "edit-mode": editMode })}"
|
||||
style=${styleMap({ "--column-count": columnCount })}
|
||||
>
|
||||
<div class="container ${classMap({ "edit-mode": editMode })}">
|
||||
${repeat(
|
||||
cardsConfig,
|
||||
(cardConfig) => this._getKey(cardConfig),
|
||||
@@ -172,6 +165,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
--base-column-count: 4;
|
||||
--row-gap: var(--ha-section-grid-row-gap, 8px);
|
||||
--column-gap: var(--ha-section-grid-column-gap, 8px);
|
||||
--row-height: var(--ha-section-grid-row-height, 56px);
|
||||
@@ -181,7 +175,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
}
|
||||
.container {
|
||||
--grid-column-count: calc(
|
||||
var(--column-count, 4) * var(--column-span, 1)
|
||||
var(--base-column-count) * var(--column-span, 1)
|
||||
);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(
|
||||
|
||||
@@ -1635,8 +1635,7 @@
|
||||
"zigbee_information": "View the Zigbee information for the device."
|
||||
},
|
||||
"confirmations": {
|
||||
"remove_title": "Remove device",
|
||||
"remove_text": "This device will be permanently removed from the Zigbee network."
|
||||
"remove": "Are you sure that you want to remove the device?"
|
||||
},
|
||||
"quirk": "Quirk",
|
||||
"last_seen": "Last seen",
|
||||
@@ -2811,7 +2810,7 @@
|
||||
"migrate": "Migrate",
|
||||
"duplicate": "[%key:ui::common::duplicate%]",
|
||||
"take_control": "Take control",
|
||||
"confirm_take_control": "You are viewing a preview of the automation config, do you want to take control?",
|
||||
"confirm_take_control": "Your are viewing a preview of the automation config, do you want to take control?",
|
||||
"run": "[%key:ui::panel::config::automation::editor::actions::run%]",
|
||||
"rename": "[%key:ui::panel::config::automation::editor::triggers::rename%]",
|
||||
"show_trace": "Traces",
|
||||
@@ -3054,9 +3053,6 @@
|
||||
"type_input": "Value of a date/time helper or timestamp-class sensor",
|
||||
"label": "Time",
|
||||
"at": "At time",
|
||||
"offset": "[%key:ui::panel::config::automation::editor::triggers::type::sun::offset%]",
|
||||
"entity": "Entity with timestamp",
|
||||
"offset_by": "offset by {offset}",
|
||||
"mode": "Mode",
|
||||
"description": {
|
||||
"picker": "At a specific time, or on a specific date.",
|
||||
@@ -3688,13 +3684,16 @@
|
||||
"editor": {
|
||||
"alias": "Name",
|
||||
"icon": "Icon",
|
||||
"id": "Entity ID",
|
||||
"id_already_exists_save_error": "You can't save this script because the ID is not unique, pick another ID or leave it blank to automatically generate one.",
|
||||
"id_already_exists": "This ID already exists",
|
||||
"introduction": "Use scripts to run a sequence of actions.",
|
||||
"show_trace": "[%key:ui::panel::config::automation::editor::show_trace%]",
|
||||
"show_info": "[%key:ui::panel::config::automation::editor::show_info%]",
|
||||
"rename": "[%key:ui::panel::config::automation::editor::triggers::rename%]",
|
||||
"change_mode": "[%key:ui::panel::config::automation::editor::change_mode%]",
|
||||
"take_control": "[%key:ui::panel::config::automation::editor::take_control%]",
|
||||
"confirm_take_control": "You are viewing a preview of the script config, do you want to take control?",
|
||||
"confirm_take_control": "Your are viewing a preview of the script config, do you want to take control?",
|
||||
"read_only": "This script cannot be edited from the UI, because it is not stored in the ''scripts.yaml'' file.",
|
||||
"unavailable": "Script is unavailable",
|
||||
"migrate": "Migrate",
|
||||
@@ -5714,13 +5713,7 @@
|
||||
"title": "Title",
|
||||
"title_helper": "The title will appear at the top of section. Leave empty to hide the title.",
|
||||
"column_span": "Width",
|
||||
"column_span_helper": "Larger sections will be made smaller to fit the display. (e.g. on mobile devices)",
|
||||
"grid_density": "Grid density",
|
||||
"grid_density_options": {
|
||||
"default": "Default ({count} columns)",
|
||||
"dense": "Dense ({count} columns)",
|
||||
"custom": "Custom ({count} {count, plural,\n one {column}\n other {columns}\n})"
|
||||
}
|
||||
"column_span_helper": "Larger sections will be made smaller to fit the display. (e.g. on mobile devices)"
|
||||
},
|
||||
"visibility": {
|
||||
"explanation": "The section will be shown when ALL conditions below are fulfilled. If no conditions are set, the section will always be shown."
|
||||
@@ -5759,10 +5752,12 @@
|
||||
"more-info": "More info",
|
||||
"toggle": "Toggle",
|
||||
"navigate": "Navigate",
|
||||
"sequence": "Sequence",
|
||||
"assist": "Assist",
|
||||
"url": "URL",
|
||||
"none": "Nothing"
|
||||
}
|
||||
},
|
||||
"sequence_actions": "Actions"
|
||||
},
|
||||
"condition-editor": {
|
||||
"explanation": "The card will be shown when ALL conditions below are fulfilled.",
|
||||
@@ -6138,8 +6133,7 @@
|
||||
"todo-list": {
|
||||
"name": "To-do list",
|
||||
"description": "The to-do list card allows you to add, edit, check-off, and clear items from your to-do list.",
|
||||
"integration_not_loaded": "This card requires the `todo` integration to be set up.",
|
||||
"hide_completed": "Hide completed items"
|
||||
"integration_not_loaded": "This card requires the `todo` integration to be set up."
|
||||
},
|
||||
"thermostat": {
|
||||
"name": "Thermostat",
|
||||
|
||||
Reference in New Issue
Block a user