mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-07 19:34:04 +00:00
Compare commits
37 Commits
repeat-ha-
...
grid_secti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b065799bf | ||
|
|
ca94267c44 | ||
|
|
df1f26cee7 | ||
|
|
24a4e075e6 | ||
|
|
43fcc6238e | ||
|
|
b40b96248b | ||
|
|
c7ac4c7490 | ||
|
|
6cd8471b91 | ||
|
|
940eaa26e0 | ||
|
|
79e68ce125 | ||
|
|
e581d35432 | ||
|
|
d3d578e0f4 | ||
|
|
79c71cbe48 | ||
|
|
82b50a1c5d | ||
|
|
6cfda78aa1 | ||
|
|
f9ff938775 | ||
|
|
778fcab90d | ||
|
|
07e5aa30c6 | ||
|
|
3f0ec03a14 | ||
|
|
1bb871b9ac | ||
|
|
0e8783fb01 | ||
|
|
1d88c4465b | ||
|
|
af2d575bf0 | ||
|
|
92165d776a | ||
|
|
a8bbd8ab90 | ||
|
|
43ac9dbea7 | ||
|
|
bba9eca4e9 | ||
|
|
40f65b1980 | ||
|
|
23a33b10a1 | ||
|
|
67a93013c7 | ||
|
|
1f838d7529 | ||
|
|
ffc0435144 | ||
|
|
5877d69c87 | ||
|
|
99035cea8f | ||
|
|
1b441a7eec | ||
|
|
ad49e9f7b0 | ||
|
|
e32b15ede2 |
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.0
|
||||
uses: actions/checkout@v4.2.1
|
||||
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.0
|
||||
uses: actions/checkout@v4.2.1
|
||||
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.0
|
||||
uses: actions/checkout@v4.2.1
|
||||
- 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.0
|
||||
uses: actions/cache@v4.1.1
|
||||
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.0
|
||||
uses: actions/checkout@v4.2.1
|
||||
- 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.0
|
||||
uses: actions/checkout@v4.2.1
|
||||
- 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.0
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
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.0
|
||||
uses: actions/checkout@v4.2.1
|
||||
- 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.0
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
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.0
|
||||
uses: actions/checkout@v4.2.1
|
||||
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.0
|
||||
uses: actions/checkout@v4.2.1
|
||||
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.0
|
||||
uses: actions/checkout@v4.2.1
|
||||
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.0
|
||||
uses: actions/checkout@v4.2.1
|
||||
|
||||
- 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.0
|
||||
uses: actions/checkout@v4.2.1
|
||||
|
||||
- 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.0
|
||||
uses: actions/checkout@v4.2.1
|
||||
|
||||
- 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.0
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@v4.4.0
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
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.0
|
||||
uses: actions/checkout@v4.2.1
|
||||
|
||||
- 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.0
|
||||
uses: actions/checkout@v4.2.1
|
||||
|
||||
- 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.6.2",
|
||||
"@codemirror/commands": "6.7.0",
|
||||
"@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.12.5",
|
||||
"@formatjs/intl-displaynames": "6.6.8",
|
||||
"@formatjs/intl-datetimeformat": "6.13.0",
|
||||
"@formatjs/intl-displaynames": "6.6.9",
|
||||
"@formatjs/intl-getcanonicallocales": "2.3.0",
|
||||
"@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",
|
||||
"@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",
|
||||
"@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.10",
|
||||
"@vaadin/vaadin-themable-mixin": "24.4.10",
|
||||
"@vaadin/combo-box": "24.4.11",
|
||||
"@vaadin/vaadin-themable-mixin": "24.4.11",
|
||||
"@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.5.14",
|
||||
"intl-messageformat": "10.6.0",
|
||||
"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.7",
|
||||
"@babel/core": "7.25.8",
|
||||
"@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.7",
|
||||
"@babel/preset-env": "7.25.8",
|
||||
"@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": "7.1.0",
|
||||
"del": "8.0.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.1.1",
|
||||
"eslint-plugin-wc": "2.2.0",
|
||||
"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.11",
|
||||
"magic-string": "0.30.12",
|
||||
"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.2",
|
||||
"typescript": "5.6.3",
|
||||
"webpack": "5.95.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.1.0",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[build-system]
|
||||
requires = ["setuptools~=68.0", "wheel~=0.40.0"]
|
||||
requires = ["setuptools~=75.1"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20241002.2"
|
||||
version = "20241010.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -18,5 +18,9 @@ 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,6 +20,15 @@ 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,
|
||||
@@ -27,14 +36,18 @@ export function nestedArrayMove<A>(
|
||||
oldPath?: ItemPath,
|
||||
newPath?: ItemPath
|
||||
): A {
|
||||
const newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
|
||||
let newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
|
||||
|
||||
if (oldPath) {
|
||||
newObj = updateNestedItem(newObj, [...oldPath]);
|
||||
}
|
||||
if (newPath) {
|
||||
newObj = updateNestedItem(newObj, [...newPath]);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../../types";
|
||||
@@ -111,16 +110,6 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _schemaKeys = new WeakMap<HaFormSchema, string>();
|
||||
|
||||
private _getSchemaKey(schema: HaFormSchema) {
|
||||
if (!this._schemaKeys.has(schema)) {
|
||||
this._schemaKeys.set(schema, Math.random().toString());
|
||||
}
|
||||
|
||||
return this._schemaKeys.get(schema)!;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="root" part="root">
|
||||
@@ -131,58 +120,55 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
</ha-alert>
|
||||
`
|
||||
: ""}
|
||||
${repeat(
|
||||
this.schema,
|
||||
(item) => this._getSchemaKey(item),
|
||||
(item) => {
|
||||
const error = getError(this.error, item);
|
||||
const warning = getWarning(this.warning, item);
|
||||
return html`
|
||||
${error
|
||||
${this.schema.map((item) => {
|
||||
const error = getError(this.error, item);
|
||||
const warning = getWarning(this.warning, item);
|
||||
|
||||
return html`
|
||||
${error
|
||||
? html`
|
||||
<ha-alert own-margin alert-type="error">
|
||||
${this._computeError(error, item)}
|
||||
</ha-alert>
|
||||
`
|
||||
: warning
|
||||
? html`
|
||||
<ha-alert own-margin alert-type="error">
|
||||
${this._computeError(error, item)}
|
||||
<ha-alert own-margin alert-type="warning">
|
||||
${this._computeWarning(warning, item)}
|
||||
</ha-alert>
|
||||
`
|
||||
: warning
|
||||
? html`
|
||||
<ha-alert own-margin alert-type="warning">
|
||||
${this._computeWarning(warning, item)}
|
||||
</ha-alert>
|
||||
`
|
||||
: ""}
|
||||
${"selector" in item
|
||||
? html`<ha-selector
|
||||
.schema=${item}
|
||||
.hass=${this.hass}
|
||||
.name=${item.name}
|
||||
.selector=${item.selector}
|
||||
.value=${getValue(this.data, item)}
|
||||
.label=${this._computeLabel(item, this.data)}
|
||||
.disabled=${item.disabled || this.disabled || false}
|
||||
.placeholder=${item.required ? "" : item.default}
|
||||
.helper=${this._computeHelper(item)}
|
||||
.localizeValue=${this.localizeValue}
|
||||
.required=${item.required || false}
|
||||
.context=${this._generateContext(item)}
|
||||
></ha-selector>`
|
||||
: dynamicElement(this.fieldElementName(item.type), {
|
||||
schema: item,
|
||||
data: getValue(this.data, item),
|
||||
label: this._computeLabel(item, this.data),
|
||||
helper: this._computeHelper(item),
|
||||
disabled: this.disabled || item.disabled || false,
|
||||
hass: this.hass,
|
||||
localize: this.hass?.localize,
|
||||
computeLabel: this.computeLabel,
|
||||
computeHelper: this.computeHelper,
|
||||
localizeValue: this.localizeValue,
|
||||
context: this._generateContext(item),
|
||||
...this.getFormProperties(),
|
||||
})}
|
||||
`;
|
||||
}
|
||||
)}
|
||||
: ""}
|
||||
${"selector" in item
|
||||
? html`<ha-selector
|
||||
.schema=${item}
|
||||
.hass=${this.hass}
|
||||
.name=${item.name}
|
||||
.selector=${item.selector}
|
||||
.value=${getValue(this.data, item)}
|
||||
.label=${this._computeLabel(item, this.data)}
|
||||
.disabled=${item.disabled || this.disabled || false}
|
||||
.placeholder=${item.required ? "" : item.default}
|
||||
.helper=${this._computeHelper(item)}
|
||||
.localizeValue=${this.localizeValue}
|
||||
.required=${item.required || false}
|
||||
.context=${this._generateContext(item)}
|
||||
></ha-selector>`
|
||||
: dynamicElement(this.fieldElementName(item.type), {
|
||||
schema: item,
|
||||
data: getValue(this.data, item),
|
||||
label: this._computeLabel(item, this.data),
|
||||
helper: this._computeHelper(item),
|
||||
disabled: this.disabled || item.disabled || false,
|
||||
hass: this.hass,
|
||||
localize: this.hass?.localize,
|
||||
computeLabel: this.computeLabel,
|
||||
computeHelper: this.computeHelper,
|
||||
localizeValue: this.localizeValue,
|
||||
context: this._generateContext(item),
|
||||
...this.getFormProperties(),
|
||||
})}
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -499,8 +499,23 @@ export class HaServiceControl extends LitElement {
|
||||
.defaultValue=${this._value?.data}
|
||||
@value-changed=${this._dataChanged}
|
||||
></ha-yaml-editor>`
|
||||
: serviceData?.fields.map((dataField) =>
|
||||
dataField.fields
|
||||
: serviceData?.fields.map((dataField) => {
|
||||
if (!dataField.fields) {
|
||||
return this._renderField(
|
||||
dataField,
|
||||
hasOptional,
|
||||
domain,
|
||||
serviceName,
|
||||
targetEntities
|
||||
);
|
||||
}
|
||||
|
||||
const fields = Object.entries(dataField.fields).map(
|
||||
([key, field]) => ({ key, ...field })
|
||||
);
|
||||
|
||||
return fields.length &&
|
||||
this._hasFilteredFields(fields, targetEntities)
|
||||
? html`<ha-expansion-panel
|
||||
leftChevron
|
||||
.expanded=${!dataField.collapsed}
|
||||
@@ -531,14 +546,8 @@ export class HaServiceControl extends LitElement {
|
||||
)
|
||||
)}
|
||||
</ha-expansion-panel>`
|
||||
: this._renderField(
|
||||
dataField,
|
||||
hasOptional,
|
||||
domain,
|
||||
serviceName,
|
||||
targetEntities
|
||||
)
|
||||
)} `;
|
||||
: nothing;
|
||||
})} `;
|
||||
}
|
||||
|
||||
private _getSectionDescription(
|
||||
@@ -551,6 +560,16 @@ 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;
|
||||
at: string | { entity_id: string; offset?: string };
|
||||
}
|
||||
|
||||
export interface TemplateTrigger extends BaseTrigger {
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 {
|
||||
@@ -371,13 +372,22 @@ const tryDescribeTrigger = (
|
||||
|
||||
// Time Trigger
|
||||
if (trigger.trigger === "time" && trigger.at) {
|
||||
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)
|
||||
);
|
||||
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}`;
|
||||
});
|
||||
|
||||
return hass.localize(`${triggerTranslationBaseKey}.time.description.full`, {
|
||||
time: formatListWithOrs(hass.locale, result),
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface UrlActionConfig extends BaseActionConfig {
|
||||
|
||||
export interface MoreInfoActionConfig extends BaseActionConfig {
|
||||
action: "more-info";
|
||||
entity_id?: string;
|
||||
}
|
||||
|
||||
export interface AssistActionConfig extends BaseActionConfig {
|
||||
|
||||
@@ -17,6 +17,10 @@ export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
|
||||
cards?: LovelaceCardConfig[];
|
||||
}
|
||||
|
||||
export interface LovelaceGridSectionConfig extends LovelaceSectionConfig {
|
||||
grid_base?: number;
|
||||
}
|
||||
|
||||
export interface LovelaceStrategySectionConfig
|
||||
extends LovelaceBaseSectionConfig {
|
||||
strategy: LovelaceStrategyConfig;
|
||||
|
||||
@@ -51,6 +51,7 @@ export class HuiPersistentNotificationItem extends LitElement {
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.time {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 6px;
|
||||
|
||||
@@ -52,7 +52,7 @@ export class HaPlayMediaAction extends LitElement implements ActionElement {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.action,
|
||||
service: "media_player.play_media",
|
||||
action: "media_player.play_media",
|
||||
target: { entity_id: ev.detail.value.entity_id },
|
||||
data: {
|
||||
media_content_id: ev.detail.value.media_content_id,
|
||||
|
||||
@@ -117,6 +117,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
||||
.value=${this._action}
|
||||
.disabled=${this.disabled}
|
||||
.showAdvanced=${this.hass.userData?.showAdvanced}
|
||||
.hidePicker=${!!this._action.metadata}
|
||||
@value-changed=${this._actionChanged}
|
||||
></ha-service-control>
|
||||
${domain && service && this.hass.services[domain]?.[service]?.response
|
||||
|
||||
@@ -208,6 +208,7 @@ 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,6 +9,9 @@ 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;
|
||||
@@ -17,48 +20,60 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _inputMode?: boolean;
|
||||
@state() private _inputMode:
|
||||
| undefined
|
||||
| typeof MODE_TIME
|
||||
| typeof MODE_ENTITY;
|
||||
|
||||
public static get defaultConfig(): TimeTrigger {
|
||||
return { trigger: "time", at: "" };
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, inputMode?: boolean) => {
|
||||
const atSelector = inputMode
|
||||
? {
|
||||
entity: {
|
||||
filter: [
|
||||
{ domain: "input_datetime" },
|
||||
{ domain: "sensor", device_class: "timestamp" },
|
||||
],
|
||||
},
|
||||
}
|
||||
: { time: {} };
|
||||
|
||||
return [
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
inputMode: typeof MODE_TIME | typeof MODE_ENTITY,
|
||||
showOffset: boolean
|
||||
) =>
|
||||
[
|
||||
{
|
||||
name: "mode",
|
||||
type: "select",
|
||||
required: true,
|
||||
options: [
|
||||
[
|
||||
"value",
|
||||
MODE_TIME,
|
||||
localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.time.type_value"
|
||||
),
|
||||
],
|
||||
[
|
||||
"input",
|
||||
MODE_ENTITY,
|
||||
localize(
|
||||
"ui.panel.config.automation.editor.triggers.type.time.type_input"
|
||||
),
|
||||
],
|
||||
],
|
||||
},
|
||||
{ name: "at", selector: atSelector },
|
||||
] as const;
|
||||
}
|
||||
...(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
|
||||
);
|
||||
|
||||
public willUpdate(changedProperties: PropertyValues) {
|
||||
@@ -75,23 +90,46 @@ 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 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,
|
||||
};
|
||||
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);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
@@ -107,26 +145,43 @@ export class HaTimeTrigger extends LitElement implements TriggerElement {
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
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 });
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string =>
|
||||
this.hass.localize(
|
||||
): string => {
|
||||
switch (schema.name) {
|
||||
case "time":
|
||||
return this.hass.localize(
|
||||
`ui.panel.config.automation.editor.triggers.type.time.at`
|
||||
);
|
||||
}
|
||||
return this.hass.localize(
|
||||
`ui.panel.config.automation.editor.triggers.type.time.${schema.name}`
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -83,9 +83,15 @@ export const getZHADeviceActions = async (
|
||||
classes: "warning",
|
||||
action: async () => {
|
||||
const confirmed = await showConfirmationDialog(el, {
|
||||
text: hass.localize(
|
||||
"ui.dialogs.zha_device_info.confirmations.remove"
|
||||
title: hass.localize(
|
||||
"ui.dialogs.zha_device_info.confirmations.remove_title"
|
||||
),
|
||||
text: hass.localize(
|
||||
"ui.dialogs.zha_device_info.confirmations.remove_text"
|
||||
),
|
||||
confirmText: hass.localize("ui.common.remove"),
|
||||
dismissText: hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
|
||||
@@ -482,7 +482,9 @@ 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 = Number(ev.detail.index);
|
||||
const index = network.dataset
|
||||
? Number(ev.detail.index)
|
||||
: Number(ev.detail.index) + 1;
|
||||
switch (index) {
|
||||
case 0:
|
||||
this._setPreferredBorderAgent(network.dataset!, router);
|
||||
|
||||
@@ -24,6 +24,7 @@ import { haStyle } from "../../../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../../../types";
|
||||
import { formatAsPaddedHex, sortZHAGroups } from "./functions";
|
||||
import { zhaTabs } from "./zha-config-dashboard";
|
||||
import { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
|
||||
export interface GroupRowData extends ZHAGroup {
|
||||
group?: GroupRowData;
|
||||
@@ -71,38 +72,35 @@ export class ZHAGroupsDashboard extends LitElement {
|
||||
});
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean): DataTableColumnContainer<GroupRowData> =>
|
||||
narrow
|
||||
? {
|
||||
name: {
|
||||
title: "Group",
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
flex: 2,
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: {
|
||||
title: this.hass.localize("ui.panel.config.zha.groups.groups"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
direction: "asc",
|
||||
flex: 2,
|
||||
},
|
||||
group_id: {
|
||||
title: this.hass.localize("ui.panel.config.zha.groups.group_id"),
|
||||
type: "numeric",
|
||||
template: (group) => html` ${formatAsPaddedHex(group.group_id)} `,
|
||||
sortable: true,
|
||||
},
|
||||
members: {
|
||||
title: this.hass.localize("ui.panel.config.zha.groups.members"),
|
||||
type: "numeric",
|
||||
template: (group) => html` ${group.members.length} `,
|
||||
sortable: true,
|
||||
},
|
||||
}
|
||||
(localize: LocalizeFunc): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer<GroupRowData> = {
|
||||
name: {
|
||||
title: localize("ui.panel.config.zha.groups.groups"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
showNarrow: true,
|
||||
main: true,
|
||||
hideable: false,
|
||||
moveable: false,
|
||||
direction: "asc",
|
||||
flex: 2,
|
||||
},
|
||||
group_id: {
|
||||
title: localize("ui.panel.config.zha.groups.group_id"),
|
||||
type: "numeric",
|
||||
template: (group) => html` ${formatAsPaddedHex(group.group_id)} `,
|
||||
sortable: true,
|
||||
},
|
||||
members: {
|
||||
title: localize("ui.panel.config.zha.groups.members"),
|
||||
type: "numeric",
|
||||
template: (group) => html` ${group.members.length} `,
|
||||
sortable: true,
|
||||
},
|
||||
};
|
||||
|
||||
return columns;
|
||||
}
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -112,7 +110,7 @@ export class ZHAGroupsDashboard extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.columns=${this._columns(this.narrow)}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${this._formattedGroups(this._groups)}
|
||||
@row-click=${this._handleRowClicked}
|
||||
clickable
|
||||
|
||||
@@ -83,8 +83,6 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
@state() private _config?: ScriptConfig;
|
||||
|
||||
@state() private _idError = false;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@state() private _errors?: string;
|
||||
@@ -414,6 +412,18 @@ 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;
|
||||
@@ -448,15 +458,6 @@ 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) {
|
||||
@@ -766,28 +767,12 @@ 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;
|
||||
}
|
||||
const entityId = this._computeEntityIdFromAlias(this._config!.alias);
|
||||
this._setEntityId(entityId);
|
||||
this._entityId = this._computeEntityIdFromAlias(this._config!.alias);
|
||||
}
|
||||
const id = this.scriptId || this._entityId || Date.now();
|
||||
|
||||
|
||||
@@ -68,6 +68,16 @@ 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");
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
StatisticsValidationResult,
|
||||
clearStatistics,
|
||||
getStatisticIds,
|
||||
updateStatisticsIssues,
|
||||
validateStatistics,
|
||||
} from "../../../data/recorder";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
@@ -636,6 +637,8 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
validateStatistics(this.hass),
|
||||
]);
|
||||
|
||||
updateStatisticsIssues(this.hass);
|
||||
|
||||
const statsIds = new Set();
|
||||
|
||||
this._data = statisticIds
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-sortable";
|
||||
@@ -124,7 +125,7 @@ export class HuiViewBadges extends LitElement {
|
||||
.options=${BADGE_SORTABLE_OPTIONS}
|
||||
invert-swap
|
||||
>
|
||||
<div class="badges">
|
||||
<div class="badges ${classMap({ "edit-mode": editMode })}">
|
||||
${repeat(
|
||||
badges,
|
||||
(badge) => this._getBadgeKey(badge),
|
||||
@@ -185,6 +186,8 @@ export class HuiViewBadges extends LitElement {
|
||||
hui-badge-edit-mode {
|
||||
display: block;
|
||||
position: relative;
|
||||
min-width: 36px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.add {
|
||||
|
||||
@@ -187,7 +187,6 @@ 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>`}
|
||||
${checkedItems.length
|
||||
${!this._config.hide_completed && checkedItems.length
|
||||
? html`
|
||||
<div role="separator">
|
||||
<div class="divider"></div>
|
||||
|
||||
@@ -453,6 +453,7 @@ export interface TodoListCardConfig extends LovelaceCardConfig {
|
||||
title?: string;
|
||||
theme?: string;
|
||||
entity?: string;
|
||||
hide_completed?: boolean;
|
||||
}
|
||||
|
||||
export interface StackCardConfig extends LovelaceCardConfig {
|
||||
|
||||
@@ -94,12 +94,13 @@ export const handleAction = async (
|
||||
|
||||
switch (actionConfig.action) {
|
||||
case "more-info": {
|
||||
if (config.entity || config.camera_image || config.image_entity) {
|
||||
fireEvent(node, "hass-more-info", {
|
||||
entityId: (config.entity ||
|
||||
config.camera_image ||
|
||||
config.image_entity)!,
|
||||
});
|
||||
const entityId =
|
||||
actionConfig.entity_id ||
|
||||
config.entity ||
|
||||
config.camera_image ||
|
||||
config.image_entity;
|
||||
if (entityId) {
|
||||
fireEvent(node, "hass-more-info", { entityId });
|
||||
} else {
|
||||
showToast(node, {
|
||||
message: hass.localize(
|
||||
|
||||
@@ -3,12 +3,15 @@ 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;
|
||||
|
||||
@@ -59,7 +62,7 @@ export class HuiCardElementEditor extends HuiTypedElementEditor<LovelaceCardConf
|
||||
protected renderConfigElement(): TemplateResult {
|
||||
const displayedTabs: string[] = ["config"];
|
||||
if (this.showVisibilityTab) displayedTabs.push("visibility");
|
||||
if (this._showLayoutTab) displayedTabs.push("layout");
|
||||
if (this.sectionConfig?.type === "grid") displayedTabs.push("layout");
|
||||
|
||||
if (displayedTabs.length === 1) return super.renderConfigElement();
|
||||
|
||||
@@ -83,8 +86,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,7 +19,10 @@ 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 { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import {
|
||||
LovelaceGridSectionConfig,
|
||||
LovelaceSectionConfig,
|
||||
} from "../../../../data/lovelace/config/section";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { HuiCard } from "../../cards/hui-card";
|
||||
@@ -27,6 +30,7 @@ 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")
|
||||
@@ -72,7 +76,10 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
|
||||
const value = this._computeCardGridSize(options);
|
||||
|
||||
const totalColumns = (this.sectionConfig.column_span ?? 1) * 4;
|
||||
const totalColumns =
|
||||
(this.sectionConfig.column_span ?? 1) *
|
||||
((this.sectionConfig as LovelaceGridSectionConfig).grid_base ||
|
||||
DEFAULT_GRID_BASE);
|
||||
|
||||
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, object, optional, string } from "superstruct";
|
||||
import { assert, assign, boolean, 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,6 +18,7 @@ const cardConfigStruct = assign(
|
||||
title: optional(string()),
|
||||
theme: optional(string()),
|
||||
entity: optional(string()),
|
||||
hide_completed: optional(boolean()),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -30,6 +31,7 @@ const SCHEMA = [
|
||||
},
|
||||
},
|
||||
{ name: "theme", selector: { theme: {} } },
|
||||
{ name: "hide_completed", selector: { boolean: {} } },
|
||||
] as const;
|
||||
|
||||
@customElement("hui-todo-list-card-editor")
|
||||
@@ -87,6 +89,10 @@ 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 { LitElement, html } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
@@ -6,12 +6,21 @@ import {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import {
|
||||
isStrategySection,
|
||||
LovelaceGridSectionConfig,
|
||||
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")
|
||||
@@ -23,27 +32,89 @@ export class HuiDialogEditSection extends LitElement {
|
||||
@property({ attribute: false }) public viewConfig!: LovelaceViewConfig;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(maxColumns: number) =>
|
||||
(
|
||||
maxColumns: number,
|
||||
localize: LocalizeFunc,
|
||||
type?: string | undefined,
|
||||
columnDensity?: GridDensity,
|
||||
columnBase?: number
|
||||
) =>
|
||||
[
|
||||
{
|
||||
name: "column_span",
|
||||
selector: {
|
||||
number: {
|
||||
min: 1,
|
||||
max: maxColumns,
|
||||
slider_ticks: true,
|
||||
},
|
||||
},
|
||||
name: "title",
|
||||
selector: { text: {} },
|
||||
},
|
||||
...(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 schema = this._schema(this.viewConfig.max_columns || 4);
|
||||
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
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
@@ -75,11 +146,26 @@ 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: newData.column_span,
|
||||
column_span: 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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,11 @@ const actionConfigStructAssist = type({
|
||||
start_listening: optional(boolean()),
|
||||
});
|
||||
|
||||
const actionConfigStructMoreInfo = type({
|
||||
action: literal("more-info"),
|
||||
entity_id: optional(string()),
|
||||
});
|
||||
|
||||
export const actionConfigStructType = object({
|
||||
action: enums([
|
||||
"none",
|
||||
@@ -93,6 +98,9 @@ export const actionConfigStruct = dynamic<any>((value) => {
|
||||
case "assist": {
|
||||
return actionConfigStructAssist;
|
||||
}
|
||||
case "more-info": {
|
||||
return actionConfigStructMoreInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceGridSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { HuiCard } from "../cards/hui-card";
|
||||
@@ -24,6 +24,8 @@ 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;
|
||||
|
||||
@@ -37,11 +39,11 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
|
||||
@property({ attribute: false }) public cards: HuiCard[] = [];
|
||||
|
||||
@state() _config?: LovelaceSectionConfig;
|
||||
@state() _config?: LovelaceGridSectionConfig;
|
||||
|
||||
@state() _dragging = false;
|
||||
|
||||
public setConfig(config: LovelaceSectionConfig): void {
|
||||
public setConfig(config: LovelaceGridSectionConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
@@ -64,6 +66,8 @@ 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}
|
||||
@@ -77,7 +81,10 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
.options=${CARD_SORTABLE_OPTIONS}
|
||||
invert-swap
|
||||
>
|
||||
<div class="container ${classMap({ "edit-mode": editMode })}">
|
||||
<div
|
||||
class="container ${classMap({ "edit-mode": editMode })}"
|
||||
style=${styleMap({ "--column-count": columnCount })}
|
||||
>
|
||||
${repeat(
|
||||
cardsConfig,
|
||||
(cardConfig) => this._getKey(cardConfig),
|
||||
@@ -165,7 +172,6 @@ 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);
|
||||
@@ -175,7 +181,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
}
|
||||
.container {
|
||||
--grid-column-count: calc(
|
||||
var(--base-column-count) * var(--column-span, 1)
|
||||
var(--column-count, 4) * var(--column-span, 1)
|
||||
);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(
|
||||
@@ -204,6 +210,10 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
grid-column: span min(var(--column-size, 1), var(--grid-column-count));
|
||||
}
|
||||
|
||||
.container.edit-mode .card {
|
||||
min-height: calc((var(--row-height) - var(--row-gap)) / 2);
|
||||
}
|
||||
|
||||
.card.fit-rows {
|
||||
height: calc(
|
||||
(var(--row-size, 1) * (var(--row-height) + var(--row-gap))) - var(
|
||||
|
||||
@@ -99,31 +99,38 @@ class StateDisplay extends LitElement {
|
||||
if (content === "name") {
|
||||
return html`${this.name || stateObj.attributes.friendly_name}`;
|
||||
}
|
||||
|
||||
let relativeDateTime: string | undefined;
|
||||
|
||||
// Check last-changed for backwards compatibility
|
||||
if (content === "last_changed" || content === "last-changed") {
|
||||
return html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`;
|
||||
relativeDateTime = stateObj.last_changed;
|
||||
}
|
||||
// Check last_updated for backwards compatibility
|
||||
if (content === "last_updated" || content === "last-updated") {
|
||||
return html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.last_updated}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`;
|
||||
relativeDateTime = stateObj.last_updated;
|
||||
}
|
||||
if (content === "last_triggered") {
|
||||
|
||||
if (
|
||||
content === "last_triggered" ||
|
||||
(domain === "calendar" &&
|
||||
(content === "start_time" || content === "end_time")) ||
|
||||
(domain === "sun" &&
|
||||
(content === "next_dawn" ||
|
||||
content === "next_dusk" ||
|
||||
content === "next_midnight" ||
|
||||
content === "next_noon" ||
|
||||
content === "next_rising" ||
|
||||
content === "next_setting"))
|
||||
) {
|
||||
relativeDateTime = stateObj.attributes[content];
|
||||
}
|
||||
|
||||
if (relativeDateTime) {
|
||||
return html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${stateObj.attributes.last_triggered}
|
||||
.datetime=${relativeDateTime}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
`;
|
||||
|
||||
@@ -1635,7 +1635,8 @@
|
||||
"zigbee_information": "View the Zigbee information for the device."
|
||||
},
|
||||
"confirmations": {
|
||||
"remove": "Are you sure that you want to remove the device?"
|
||||
"remove_title": "Remove device",
|
||||
"remove_text": "This device will be permanently removed from the Zigbee network."
|
||||
},
|
||||
"quirk": "Quirk",
|
||||
"last_seen": "Last seen",
|
||||
@@ -2810,7 +2811,7 @@
|
||||
"migrate": "Migrate",
|
||||
"duplicate": "[%key:ui::common::duplicate%]",
|
||||
"take_control": "Take control",
|
||||
"confirm_take_control": "Your are viewing a preview of the automation config, do you want to take control?",
|
||||
"confirm_take_control": "You 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",
|
||||
@@ -3053,6 +3054,9 @@
|
||||
"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.",
|
||||
@@ -3684,16 +3688,13 @@
|
||||
"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": "Your are viewing a preview of the script config, do you want to take control?",
|
||||
"confirm_take_control": "You 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",
|
||||
@@ -5713,7 +5714,13 @@
|
||||
"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)"
|
||||
"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})"
|
||||
}
|
||||
},
|
||||
"visibility": {
|
||||
"explanation": "The section will be shown when ALL conditions below are fulfilled. If no conditions are set, the section will always be shown."
|
||||
@@ -6131,7 +6138,8 @@
|
||||
"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."
|
||||
"integration_not_loaded": "This card requires the `todo` integration to be set up.",
|
||||
"hide_completed": "Hide completed items"
|
||||
},
|
||||
"thermostat": {
|
||||
"name": "Thermostat",
|
||||
|
||||
Reference in New Issue
Block a user