Merge branch 'home-assistant:dev' into raulcodes/import-calendar-events
This commit is contained in:
commit
c3efca0709
|
@ -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.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
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.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
|
@ -58,7 +58,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
|
@ -76,7 +76,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
|
@ -89,7 +89,7 @@ jobs:
|
|||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
uses: actions/upload-artifact@v4.3.2
|
||||
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.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
|
@ -113,7 +113,7 @@ jobs:
|
|||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
uses: actions/upload-artifact@v4.3.2
|
||||
with:
|
||||
name: supervisor-bundle-stats
|
||||
path: build/stats/*.json
|
||||
|
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
|
|
@ -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.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
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.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
|
|
@ -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.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
|
|
|
@ -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.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
|
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
|
||||
- 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.3.1
|
||||
uses: actions/upload-artifact@v4.3.2
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@v4.3.1
|
||||
uses: actions/upload-artifact@v4.3.2
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
|
|
@ -23,7 +23,7 @@ jobs:
|
|||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
|
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.2
|
||||
uses: actions/checkout@v4.1.3
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
|
|
@ -10,6 +10,7 @@ const WebpackBar = require("webpackbar");
|
|||
const {
|
||||
TransformAsyncModulesPlugin,
|
||||
} = require("transform-async-modules-webpack-plugin");
|
||||
const { dependencies } = require("../package.json");
|
||||
const paths = require("./paths.cjs");
|
||||
const bundle = require("./bundle.cjs");
|
||||
|
||||
|
@ -156,7 +157,10 @@ const createWebpackConfig = ({
|
|||
transform: (stats) => JSON.stringify(filterStats(stats)),
|
||||
}),
|
||||
!latestBuild &&
|
||||
new TransformAsyncModulesPlugin({ browserslistEnv: "legacy" }),
|
||||
new TransformAsyncModulesPlugin({
|
||||
browserslistEnv: "legacy",
|
||||
runtime: { version: dependencies["@babel/runtime"] },
|
||||
}),
|
||||
].filter(Boolean),
|
||||
resolve: {
|
||||
extensions: [".ts", ".js", ".json"],
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
import { HomeAssistantAppEl } from "../../src/layouts/home-assistant";
|
||||
import { HomeAssistant } from "../../src/types";
|
||||
import { selectedDemoConfig } from "./configs/demo-configs";
|
||||
import { mockAreaRegistry } from "./stubs/area_registry";
|
||||
import { mockAuth } from "./stubs/auth";
|
||||
import { mockConfigEntries } from "./stubs/config_entries";
|
||||
import { mockEnergy } from "./stubs/energy";
|
||||
|
@ -23,10 +24,10 @@ import { mockLovelace } from "./stubs/lovelace";
|
|||
import { mockMediaPlayer } from "./stubs/media_player";
|
||||
import { mockPersistentNotification } from "./stubs/persistent_notification";
|
||||
import { mockRecorder } from "./stubs/recorder";
|
||||
import { mockTodo } from "./stubs/todo";
|
||||
import { mockSensor } from "./stubs/sensor";
|
||||
import { mockSystemLog } from "./stubs/system_log";
|
||||
import { mockTemplate } from "./stubs/template";
|
||||
import { mockTodo } from "./stubs/todo";
|
||||
import { mockTranslations } from "./stubs/translations";
|
||||
|
||||
@customElement("ha-demo")
|
||||
|
@ -62,6 +63,7 @@ export class HaDemo extends HomeAssistantAppEl {
|
|||
mockEnergy(hass);
|
||||
mockPersistentNotification(hass);
|
||||
mockConfigEntries(hass);
|
||||
mockAreaRegistry(hass);
|
||||
mockEntityRegistry(hass, [
|
||||
{
|
||||
config_entry_id: "co2signal",
|
||||
|
|
|
@ -2,6 +2,7 @@ import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
|||
import { customElement, query } from "lit/decorators";
|
||||
import { CoverEntityFeature } from "../../../../src/data/cover";
|
||||
import { LightColorMode } from "../../../../src/data/light";
|
||||
import { LockEntityFeature } from "../../../../src/data/lock";
|
||||
import { VacuumEntityFeature } from "../../../../src/data/vacuum";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
|
@ -20,6 +21,11 @@ const ENTITIES = [
|
|||
getEntity("light", "unavailable", "unavailable", {
|
||||
friendly_name: "Unavailable entity",
|
||||
}),
|
||||
getEntity("lock", "front_door", "locked", {
|
||||
friendly_name: "Front Door Lock",
|
||||
device_class: "lock",
|
||||
supported_features: LockEntityFeature.OPEN,
|
||||
}),
|
||||
getEntity("climate", "thermostat", "heat", {
|
||||
current_temperature: 73,
|
||||
min_temp: 45,
|
||||
|
@ -138,6 +144,24 @@ const CONFIGS = [
|
|||
- type: "color-temp"
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Lock commands feature",
|
||||
config: `
|
||||
- type: tile
|
||||
entity: lock.front_door
|
||||
features:
|
||||
- type: "lock-commands"
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Lock open door feature",
|
||||
config: `
|
||||
- type: tile
|
||||
entity: lock.front_door
|
||||
features:
|
||||
- type: "lock-open-door"
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Vacuum commands feature",
|
||||
config: `
|
||||
|
|
|
@ -36,6 +36,8 @@ const createConfigEntry = (
|
|||
pref_disable_new_entities: false,
|
||||
pref_disable_polling: false,
|
||||
reason: null,
|
||||
error_reason_translation_key: null,
|
||||
error_reason_translation_placeholders: null,
|
||||
...override,
|
||||
});
|
||||
|
||||
|
|
54
package.json
54
package.json
|
@ -28,7 +28,7 @@
|
|||
"@babel/runtime": "7.24.4",
|
||||
"@braintree/sanitize-url": "7.0.1",
|
||||
"@codemirror/autocomplete": "6.16.0",
|
||||
"@codemirror/commands": "6.3.3",
|
||||
"@codemirror/commands": "6.5.0",
|
||||
"@codemirror/language": "6.10.1",
|
||||
"@codemirror/legacy-modes": "6.4.0",
|
||||
"@codemirror/search": "6.5.6",
|
||||
|
@ -81,7 +81,7 @@
|
|||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/web": "1.4.0",
|
||||
"@material/web": "1.4.1",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@polymer/paper-item": "3.0.1",
|
||||
|
@ -101,17 +101,17 @@
|
|||
"chart.js": "4.4.2",
|
||||
"color-name": "2.0.0",
|
||||
"comlink": "4.4.1",
|
||||
"core-js": "3.36.1",
|
||||
"cropperjs": "1.6.1",
|
||||
"core-js": "3.37.0",
|
||||
"cropperjs": "1.6.2",
|
||||
"date-fns": "3.6.0",
|
||||
"date-fns-tz": "3.0.1",
|
||||
"date-fns-tz": "3.1.3",
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"element-internals-polyfill": "1.3.11",
|
||||
"fuse.js": "7.0.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||
"home-assistant-js-websocket": "9.2.1",
|
||||
"home-assistant-js-websocket": "9.3.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.5.11",
|
||||
"js-yaml": "4.1.0",
|
||||
|
@ -119,7 +119,7 @@
|
|||
"leaflet-draw": "1.0.4",
|
||||
"lit": "2.8.0",
|
||||
"luxon": "3.4.4",
|
||||
"marked": "12.0.1",
|
||||
"marked": "12.0.2",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "0.3.2",
|
||||
|
@ -141,26 +141,26 @@
|
|||
"vue": "2.7.16",
|
||||
"vue2-daterange-picker": "0.6.8",
|
||||
"weekstart": "2.0.0",
|
||||
"workbox-cacheable-response": "7.0.0",
|
||||
"workbox-core": "7.0.0",
|
||||
"workbox-expiration": "7.0.0",
|
||||
"workbox-precaching": "7.0.0",
|
||||
"workbox-routing": "7.0.0",
|
||||
"workbox-strategies": "7.0.0",
|
||||
"workbox-cacheable-response": "7.1.0",
|
||||
"workbox-core": "7.1.0",
|
||||
"workbox-expiration": "7.1.0",
|
||||
"workbox-precaching": "7.1.0",
|
||||
"workbox-routing": "7.1.0",
|
||||
"workbox-strategies": "7.1.0",
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.24.4",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.1",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.2",
|
||||
"@babel/plugin-proposal-decorators": "7.24.1",
|
||||
"@babel/plugin-transform-runtime": "7.24.3",
|
||||
"@babel/preset-env": "7.24.4",
|
||||
"@babel/preset-typescript": "7.24.1",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.12.2",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@lokalise/node-api": "12.4.0",
|
||||
"@octokit/auth-oauth-device": "7.1.0",
|
||||
"@octokit/plugin-retry": "7.1.0",
|
||||
"@lokalise/node-api": "12.4.1",
|
||||
"@octokit/auth-oauth-device": "7.1.1",
|
||||
"@octokit/plugin-retry": "7.1.1",
|
||||
"@octokit/rest": "20.1.0",
|
||||
"@open-wc/dev-server-hmr": "0.1.4",
|
||||
"@rollup/plugin-babel": "6.0.4",
|
||||
|
@ -169,24 +169,24 @@
|
|||
"@rollup/plugin-node-resolve": "15.2.3",
|
||||
"@rollup/plugin-replace": "5.0.5",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.13",
|
||||
"@types/chromecast-caf-receiver": "6.0.14",
|
||||
"@types/chromecast-caf-sender": "1.0.9",
|
||||
"@types/color-name": "1.1.3",
|
||||
"@types/color-name": "1.1.4",
|
||||
"@types/glob": "8.1.0",
|
||||
"@types/html-minifier-terser": "7.0.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/leaflet": "1.9.9",
|
||||
"@types/leaflet": "1.9.12",
|
||||
"@types/leaflet-draw": "1.0.11",
|
||||
"@types/luxon": "3.4.2",
|
||||
"@types/mocha": "10.0.6",
|
||||
"@types/qrcode": "1.5.5",
|
||||
"@types/serve-handler": "6.1.4",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
"@types/tar": "6.1.12",
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "7.6.0",
|
||||
"@typescript-eslint/parser": "7.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.7.1",
|
||||
"@typescript-eslint/parser": "7.7.1",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@web/dev-server-rollup": "0.4.1",
|
||||
"babel-loader": "9.1.3",
|
||||
|
@ -219,7 +219,7 @@
|
|||
"lint-staged": "15.2.2",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.template": "4.5.0",
|
||||
"magic-string": "0.30.9",
|
||||
"magic-string": "0.30.10",
|
||||
"map-stream": "0.0.7",
|
||||
"mocha": "10.4.0",
|
||||
"object-hash": "3.0.0",
|
||||
|
@ -234,9 +234,9 @@
|
|||
"sinon": "17.0.1",
|
||||
"source-map-url": "0.4.1",
|
||||
"systemjs": "6.14.3",
|
||||
"tar": "7.0.0",
|
||||
"tar": "7.0.1",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"transform-async-modules-webpack-plugin": "1.0.4",
|
||||
"transform-async-modules-webpack-plugin": "1.1.0",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.4.5",
|
||||
"webpack": "5.91.0",
|
||||
|
@ -245,7 +245,7 @@
|
|||
"webpack-manifest-plugin": "5.0.0",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "6.0.1",
|
||||
"workbox-build": "7.0.0"
|
||||
"workbox-build": "7.1.0"
|
||||
},
|
||||
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
|
||||
"resolutions": {
|
||||
|
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20240404.1"
|
||||
version = "20240426.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
|
|
@ -187,11 +187,14 @@ export const computeStateDisplayFromEntityAttributes = (
|
|||
if (
|
||||
[
|
||||
"button",
|
||||
"conversation",
|
||||
"event",
|
||||
"image",
|
||||
"input_button",
|
||||
"notify",
|
||||
"scene",
|
||||
"stt",
|
||||
"tag",
|
||||
"tts",
|
||||
"wake_word",
|
||||
].includes(domain) ||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { mdiArrowDown, mdiArrowUp } from "@mdi/js";
|
||||
import { mdiArrowDown, mdiArrowUp, mdiChevronUp } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import {
|
||||
customElement,
|
||||
|
@ -22,7 +22,9 @@ import { styleMap } from "lit/directives/style-map";
|
|||
import memoizeOne from "memoize-one";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { groupBy } from "../../common/util/group-by";
|
||||
import { nextRender } from "../../common/util/render-status";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
|
@ -32,17 +34,6 @@ import type { HaCheckbox } from "../ha-checkbox";
|
|||
import "../ha-svg-icon";
|
||||
import "../search-input";
|
||||
import { filterData, sortData } from "./sort-filter";
|
||||
import { groupBy } from "../../common/util/group-by";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"selection-changed": SelectionChangedEvent;
|
||||
"row-click": RowClickedEvent;
|
||||
"sorting-changed": SortingChangedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export interface RowClickedEvent {
|
||||
id: string;
|
||||
|
@ -52,6 +43,10 @@ export interface SelectionChangedEvent {
|
|||
value: string[];
|
||||
}
|
||||
|
||||
export interface CollapsedChangedEvent {
|
||||
value: string[];
|
||||
}
|
||||
|
||||
export interface SortingChangedEvent {
|
||||
column: string;
|
||||
direction: SortingDirection;
|
||||
|
@ -142,10 +137,14 @@ export class HaDataTable extends LitElement {
|
|||
|
||||
@property() public groupColumn?: string;
|
||||
|
||||
@property({ attribute: false }) public groupOrder?: string[];
|
||||
|
||||
@property() public sortColumn?: string;
|
||||
|
||||
@property() public sortDirection: SortingDirection = null;
|
||||
|
||||
@property({ attribute: false }) public initialCollapsedGroups?: string[];
|
||||
|
||||
@state() private _filterable = false;
|
||||
|
||||
@state() private _filter = "";
|
||||
|
@ -158,6 +157,8 @@ export class HaDataTable extends LitElement {
|
|||
|
||||
@state() private _items: DataTableRowData[] = [];
|
||||
|
||||
@state() private _collapsedGroups: string[] = [];
|
||||
|
||||
private _checkableRowsCount?: number;
|
||||
|
||||
private _checkedRows: string[] = [];
|
||||
|
@ -213,17 +214,19 @@ export class HaDataTable extends LitElement {
|
|||
(column) => column.filterable
|
||||
);
|
||||
|
||||
for (const columnId in this.columns) {
|
||||
if (this.columns[columnId].direction) {
|
||||
this.sortDirection = this.columns[columnId].direction!;
|
||||
this.sortColumn = columnId;
|
||||
if (!this.sortColumn) {
|
||||
for (const columnId in this.columns) {
|
||||
if (this.columns[columnId].direction) {
|
||||
this.sortDirection = this.columns[columnId].direction!;
|
||||
this.sortColumn = columnId;
|
||||
|
||||
fireEvent(this, "sorting-changed", {
|
||||
column: columnId,
|
||||
direction: this.sortDirection,
|
||||
});
|
||||
fireEvent(this, "sorting-changed", {
|
||||
column: columnId,
|
||||
direction: this.sortDirection,
|
||||
});
|
||||
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -248,13 +251,23 @@ export class HaDataTable extends LitElement {
|
|||
).length;
|
||||
}
|
||||
|
||||
if (!this.hasUpdated && this.initialCollapsedGroups) {
|
||||
this._collapsedGroups = this.initialCollapsedGroups;
|
||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
||||
} else if (properties.has("groupColumn")) {
|
||||
this._collapsedGroups = [];
|
||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
||||
}
|
||||
|
||||
if (
|
||||
properties.has("data") ||
|
||||
properties.has("columns") ||
|
||||
properties.has("_filter") ||
|
||||
properties.has("sortColumn") ||
|
||||
properties.has("sortDirection") ||
|
||||
properties.has("groupColumn")
|
||||
properties.has("groupColumn") ||
|
||||
properties.has("groupOrder") ||
|
||||
properties.has("_collapsedGroups")
|
||||
) {
|
||||
this._sortFilterData();
|
||||
}
|
||||
|
@ -447,6 +460,8 @@ export class HaDataTable extends LitElement {
|
|||
}
|
||||
return html`
|
||||
<div
|
||||
@mouseover=${this._setTitle}
|
||||
@focus=${this._setTitle}
|
||||
role=${column.main ? "rowheader" : "cell"}
|
||||
class="mdc-data-table__cell ${classMap({
|
||||
"mdc-data-table__cell--flex": column.type === "flex",
|
||||
|
@ -514,11 +529,7 @@ export class HaDataTable extends LitElement {
|
|||
}
|
||||
|
||||
if (this.appendRow || this.hasFab || this.groupColumn) {
|
||||
const items = [...data];
|
||||
|
||||
if (this.appendRow) {
|
||||
items.push({ append: true, content: this.appendRow });
|
||||
}
|
||||
let items = [...data];
|
||||
|
||||
if (this.groupColumn) {
|
||||
const grouped = groupBy(items, (item) => item[this.groupColumn!]);
|
||||
|
@ -530,13 +541,24 @@ export class HaDataTable extends LitElement {
|
|||
const sorted: {
|
||||
[key: string]: DataTableRowData[];
|
||||
} = Object.keys(grouped)
|
||||
.sort((a, b) =>
|
||||
stringCompare(
|
||||
.sort((a, b) => {
|
||||
const orderA = this.groupOrder?.indexOf(a) ?? -1;
|
||||
const orderB = this.groupOrder?.indexOf(b) ?? -1;
|
||||
if (orderA !== orderB) {
|
||||
if (orderA === -1) {
|
||||
return 1;
|
||||
}
|
||||
if (orderB === -1) {
|
||||
return -1;
|
||||
}
|
||||
return orderA - orderB;
|
||||
}
|
||||
return stringCompare(
|
||||
["", "-", "—"].includes(a) ? "zzz" : a,
|
||||
["", "-", "—"].includes(b) ? "zzz" : b,
|
||||
this.hass.locale.language
|
||||
)
|
||||
)
|
||||
);
|
||||
})
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = grouped[key];
|
||||
return obj;
|
||||
|
@ -552,23 +574,39 @@ export class HaDataTable extends LitElement {
|
|||
content: html`<div
|
||||
class="mdc-data-table__cell group-header"
|
||||
role="cell"
|
||||
.group=${groupName}
|
||||
@click=${this._collapseGroup}
|
||||
>
|
||||
${groupName === UNDEFINED_GROUP_KEY ? "" : groupName || ""}
|
||||
<ha-icon-button
|
||||
.path=${mdiChevronUp}
|
||||
class=${this._collapsedGroups.includes(groupName)
|
||||
? "collapsed"
|
||||
: ""}
|
||||
>
|
||||
</ha-icon-button>
|
||||
${groupName === UNDEFINED_GROUP_KEY
|
||||
? this.hass.localize("ui.components.data-table.ungrouped")
|
||||
: groupName || ""}
|
||||
</div>`,
|
||||
});
|
||||
}
|
||||
|
||||
groupedItems.push(...rows);
|
||||
if (!this._collapsedGroups.includes(groupName)) {
|
||||
groupedItems.push(...rows);
|
||||
}
|
||||
});
|
||||
|
||||
this._items = groupedItems;
|
||||
} else {
|
||||
this._items = items;
|
||||
items = groupedItems;
|
||||
}
|
||||
|
||||
if (this.appendRow) {
|
||||
items.push({ append: true, content: this.appendRow });
|
||||
}
|
||||
|
||||
if (this.hasFab) {
|
||||
this._items = [...this._items, { empty: true }];
|
||||
items.push({ empty: true });
|
||||
}
|
||||
|
||||
this._items = items;
|
||||
} else {
|
||||
this._items = data;
|
||||
}
|
||||
|
@ -649,6 +687,13 @@ export class HaDataTable extends LitElement {
|
|||
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
|
||||
};
|
||||
|
||||
private _setTitle(ev: Event) {
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
if (target.scrollWidth > target.offsetWidth) {
|
||||
target.setAttribute("title", target.innerText);
|
||||
}
|
||||
}
|
||||
|
||||
private _checkedRowsChanged() {
|
||||
// force scroller to update, change it's items
|
||||
if (this._items.length) {
|
||||
|
@ -679,6 +724,18 @@ export class HaDataTable extends LitElement {
|
|||
this._savedScrollPos = (e.target as HTMLDivElement).scrollTop;
|
||||
}
|
||||
|
||||
private _collapseGroup = (ev: Event) => {
|
||||
const groupName = (ev.currentTarget as any).group;
|
||||
if (this._collapsedGroups.includes(groupName)) {
|
||||
this._collapsedGroups = this._collapsedGroups.filter(
|
||||
(grp) => grp !== groupName
|
||||
);
|
||||
} else {
|
||||
this._collapsedGroups = [...this._collapsedGroups, groupName];
|
||||
}
|
||||
fireEvent(this, "collapsed-changed", { value: this._collapsedGroups });
|
||||
};
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
|
@ -931,8 +988,21 @@ export class HaDataTable extends LitElement {
|
|||
|
||||
.group-header {
|
||||
padding-top: 12px;
|
||||
padding-left: 12px;
|
||||
padding-inline-start: 12px;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.group-header ha-icon-button {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.group-header ha-icon-button.collapsed {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
:host {
|
||||
|
@ -1031,4 +1101,12 @@ declare global {
|
|||
interface HTMLElementTagNameMap {
|
||||
"ha-data-table": HaDataTable;
|
||||
}
|
||||
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"selection-changed": SelectionChangedEvent;
|
||||
"row-click": RowClickedEvent;
|
||||
"sorting-changed": SortingChangedEvent;
|
||||
"collapsed-changed": CollapsedChangedEvent;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -409,7 +409,7 @@ export class HaEntityPicker extends LitElement {
|
|||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (newValue.startsWith(CREATE_ID)) {
|
||||
if (newValue && newValue.startsWith(CREATE_ID)) {
|
||||
const domain = newValue.substring(CREATE_ID.length);
|
||||
showHelperDetailDialog(this, {
|
||||
domain,
|
||||
|
|
|
@ -67,6 +67,9 @@ export class HaControlSlider extends LitElement {
|
|||
@property({ attribute: "tooltip-mode" })
|
||||
public tooltipMode: TooltipMode = "interaction";
|
||||
|
||||
@property({ attribute: "touch-action" })
|
||||
public touchAction?: string;
|
||||
|
||||
@property({ type: Number })
|
||||
public value?: number;
|
||||
|
||||
|
@ -152,7 +155,7 @@ export class HaControlSlider extends LitElement {
|
|||
setupListeners() {
|
||||
if (this.slider && !this._mc) {
|
||||
this._mc = new Manager(this.slider, {
|
||||
touchAction: this.vertical ? "pan-x" : "pan-y",
|
||||
touchAction: this.touchAction ?? (this.vertical ? "pan-x" : "pan-y"),
|
||||
});
|
||||
this._mc.add(
|
||||
new Pan({
|
||||
|
|
|
@ -33,6 +33,9 @@ export class HaControlSwitch extends LitElement {
|
|||
// SVG icon path (if you need a non SVG icon instead, use the provided off icon slot to pass an <ha-icon slot="icon-off"> in)
|
||||
@property({ type: String }) pathOff?: string;
|
||||
|
||||
@property({ attribute: "touch-action" })
|
||||
public touchAction?: string;
|
||||
|
||||
private _mc?: HammerManager;
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues): void {
|
||||
|
@ -73,7 +76,7 @@ export class HaControlSwitch extends LitElement {
|
|||
setupListeners() {
|
||||
if (this.switch && !this._mc) {
|
||||
this._mc = new Manager(this.switch, {
|
||||
touchAction: this.vertical ? "pan-x" : "pan-y",
|
||||
touchAction: this.touchAction ?? (this.vertical ? "pan-x" : "pan-y"),
|
||||
});
|
||||
this._mc.add(
|
||||
new Swipe({
|
||||
|
|
|
@ -69,7 +69,7 @@ export class HaFilterDevices extends LitElement {
|
|||
@value-changed=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
<mwc-list class="ha-scrollbar">
|
||||
<mwc-list class="ha-scrollbar" multi>
|
||||
<lit-virtualizer
|
||||
.items=${this._devices(
|
||||
this.hass.devices,
|
||||
|
@ -94,7 +94,7 @@ export class HaFilterDevices extends LitElement {
|
|||
? nothing
|
||||
: html`<ha-check-list-item
|
||||
.value=${device.id}
|
||||
.selected=${this.value?.includes(device.id)}
|
||||
.selected=${this.value?.includes(device.id) ?? false}
|
||||
>
|
||||
${computeDeviceName(device, this.hass)}
|
||||
</ha-check-list-item>`;
|
||||
|
|
|
@ -0,0 +1,198 @@
|
|||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { domainToName } from "../data/integration";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-domain-icon";
|
||||
import "./search-input-outlined";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
|
||||
@customElement("ha-filter-domains")
|
||||
export class HaFilterDomains extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@state() private _filter?: string;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
leftChevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.headers.domain"
|
||||
)}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
.path=${mdiFilterVariantRemove}
|
||||
@click=${this._clearFilter}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`<search-input-outlined
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
<mwc-list
|
||||
class="ha-scrollbar"
|
||||
@click=${this._handleItemClick}
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._domains(this.hass.states, this._filter),
|
||||
(i) => i,
|
||||
(domain) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${domain}
|
||||
.selected=${(this.value || []).includes(domain)}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-domain-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
brandFallback
|
||||
></ha-domain-icon>
|
||||
${domainToName(this.hass.localize, domain)}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</mwc-list> `
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private _domains = memoizeOne((states, filter) => {
|
||||
const domains = new Set<string>();
|
||||
Object.keys(states).forEach((entityId) => {
|
||||
domains.add(computeDomain(entityId));
|
||||
});
|
||||
return Array.from(domains)
|
||||
.filter((domain) => !filter || domain.toLowerCase().includes(filter))
|
||||
.sort((a, b) => stringCompare(a, b, this.hass.locale.language));
|
||||
});
|
||||
|
||||
protected updated(changed) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||
`${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _handleItemClick(ev) {
|
||||
const listItem = ev.target.closest("ha-check-list-item");
|
||||
const value = listItem?.value;
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
if (this.value?.includes(value)) {
|
||||
this.value = this.value?.filter((val) => val !== value);
|
||||
} else {
|
||||
this.value = [...(this.value || []), value];
|
||||
}
|
||||
|
||||
listItem.selected = this.value.includes(value);
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: this.value,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _clearFilter(ev) {
|
||||
ev.preventDefault();
|
||||
this.value = undefined;
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: undefined,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value.toLowerCase();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
:host([expanded]) {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--ha-card-border-radius: 0;
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header ha-icon-button {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: 0;
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
background-color: var(--primary-color);
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
search-input-outlined {
|
||||
display: block;
|
||||
padding: 0 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-filter-domains": HaFilterDomains;
|
||||
}
|
||||
}
|
|
@ -71,7 +71,7 @@ export class HaFilterEntities extends LitElement {
|
|||
@value-changed=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
<mwc-list class="ha-scrollbar">
|
||||
<mwc-list class="ha-scrollbar" multi>
|
||||
<lit-virtualizer
|
||||
.items=${this._entities(
|
||||
this.hass.states,
|
||||
|
@ -108,7 +108,7 @@ export class HaFilterEntities extends LitElement {
|
|||
? nothing
|
||||
: html`<ha-check-list-item
|
||||
.value=${entity.entity_id}
|
||||
.selected=${this.value?.includes(entity.entity_id)}
|
||||
.selected=${this.value?.includes(entity.entity_id) ?? false}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
|
|
|
@ -55,7 +55,11 @@ export class HaFilterIntegrations extends LitElement {
|
|||
@value-changed=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
<mwc-list class="ha-scrollbar" @click=${this._handleItemClick}>
|
||||
<mwc-list
|
||||
class="ha-scrollbar"
|
||||
@click=${this._handleItemClick}
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._integrations(this._manifests, this._filter, this.value),
|
||||
(i) => i.domain,
|
||||
|
|
|
@ -62,8 +62,8 @@ export class HaFilterStates extends LitElement {
|
|||
(item) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${item.value}
|
||||
.selected=${this.value?.includes(item.value)}
|
||||
.graphic=${hasIcon ? "icon" : undefined}
|
||||
.selected=${this.value?.includes(item.value) ?? false}
|
||||
.graphic=${hasIcon ? "icon" : null}
|
||||
>
|
||||
${item.icon
|
||||
? html`<ha-icon
|
||||
|
|
|
@ -71,6 +71,10 @@ export const computeInitialHaFormData = (
|
|||
if (selector.country?.countries?.length) {
|
||||
data[field.name] = selector.country.countries[0];
|
||||
}
|
||||
} else if ("language" in selector) {
|
||||
if (selector.language?.languages?.length) {
|
||||
data[field.name] = selector.language.languages[0];
|
||||
}
|
||||
} else if ("duration" in selector) {
|
||||
data[field.name] = {
|
||||
hours: 0,
|
||||
|
@ -93,7 +97,9 @@ export const computeInitialHaFormData = (
|
|||
) {
|
||||
data[field.name] = {};
|
||||
} else {
|
||||
throw new Error("Selector not supported in initial form data");
|
||||
throw new Error(
|
||||
`Selector ${Object.keys(selector)[0]} not supported in initial form data`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,13 +1,29 @@
|
|||
import { FormfieldBase } from "@material/mwc-formfield/mwc-formfield-base";
|
||||
import { styles } from "@material/mwc-formfield/mwc-formfield.css";
|
||||
import { css } from "lit";
|
||||
import { css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-formfield")
|
||||
export class HaFormfield extends FormfieldBase {
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
protected override render() {
|
||||
const classes = {
|
||||
"mdc-form-field--align-end": this.alignEnd,
|
||||
"mdc-form-field--space-between": this.spaceBetween,
|
||||
"mdc-form-field--nowrap": this.nowrap,
|
||||
};
|
||||
|
||||
return html` <div class="mdc-form-field ${classMap(classes)}">
|
||||
<slot></slot>
|
||||
<label class="mdc-label" @click=${this._labelClick}
|
||||
><slot name="label">${this.label}</slot></label
|
||||
>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected _labelClick() {
|
||||
const input = this.input as HTMLInputElement | undefined;
|
||||
if (!input) return;
|
||||
|
@ -39,6 +55,9 @@ export class HaFormfield extends FormfieldBase {
|
|||
margin-inline-end: 10px;
|
||||
margin-inline-start: inline;
|
||||
}
|
||||
.mdc-form-field {
|
||||
align-items: var(--ha-formfield-align-items, center);
|
||||
}
|
||||
.mdc-form-field > label {
|
||||
direction: var(--direction);
|
||||
margin-inline-start: 0;
|
||||
|
|
|
@ -82,7 +82,7 @@ export class HaSortable extends LitElement {
|
|||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._shouldBeDestroy = false;
|
||||
if (this.hasUpdated) {
|
||||
if (this.hasUpdated && !this.disabled) {
|
||||
this._createSortable();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
|||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ThemeMode } from "../../types";
|
||||
import "../ha-input-helper-text";
|
||||
import "./ha-map";
|
||||
import type { HaMap } from "./ha-map";
|
||||
|
@ -61,7 +61,8 @@ export class HaLocationsEditor extends LitElement {
|
|||
|
||||
@property({ type: Number }) public zoom = 16;
|
||||
|
||||
@property({ type: Boolean }) public darkMode = false;
|
||||
@property({ attribute: "theme-mode", type: String })
|
||||
public themeMode: ThemeMode = "auto";
|
||||
|
||||
@state() private _locationMarkers?: Record<string, Marker | Circle>;
|
||||
|
||||
|
@ -133,7 +134,7 @@ export class HaLocationsEditor extends LitElement {
|
|||
.layers=${this._getLayers(this._circles, this._locationMarkers)}
|
||||
.zoom=${this.zoom}
|
||||
.autoFit=${this.autoFit}
|
||||
?darkMode=${this.darkMode}
|
||||
.themeMode=${this.themeMode}
|
||||
></ha-map>
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
|
|
|
@ -1,32 +1,32 @@
|
|||
import { isToday } from "date-fns";
|
||||
import type {
|
||||
Circle,
|
||||
CircleMarker,
|
||||
LatLngTuple,
|
||||
LatLngExpression,
|
||||
LatLngTuple,
|
||||
Layer,
|
||||
Map,
|
||||
Marker,
|
||||
Polyline,
|
||||
} from "leaflet";
|
||||
import { isToday } from "date-fns";
|
||||
import { css, CSSResultGroup, PropertyValues, ReactiveElement } from "lit";
|
||||
import { CSSResultGroup, PropertyValues, ReactiveElement, css } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { formatDateTime } from "../../common/datetime/format_date_time";
|
||||
import {
|
||||
formatTimeWeekday,
|
||||
formatTimeWithSeconds,
|
||||
} from "../../common/datetime/format_time";
|
||||
import {
|
||||
LeafletModuleType,
|
||||
setupLeafletMap,
|
||||
} from "../../common/dom/setup-leaflet-map";
|
||||
import {
|
||||
formatTimeWithSeconds,
|
||||
formatTimeWeekday,
|
||||
} from "../../common/datetime/format_time";
|
||||
import { formatDateTime } from "../../common/datetime/format_date_time";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { loadPolyfillIfNeeded } from "../../resources/resize-observer.polyfill";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { HomeAssistant, ThemeMode } from "../../types";
|
||||
import { isTouch } from "../../util/is_touch";
|
||||
import "../ha-icon-button";
|
||||
import "./ha-entity-marker";
|
||||
import { isTouch } from "../../util/is_touch";
|
||||
|
||||
const getEntityId = (entity: string | HaMapEntity): string =>
|
||||
typeof entity === "string" ? entity : entity.entity_id;
|
||||
|
@ -69,7 +69,8 @@ export class HaMap extends ReactiveElement {
|
|||
|
||||
@property({ type: Boolean }) public fitZones = false;
|
||||
|
||||
@property({ type: Boolean }) public darkMode = false;
|
||||
@property({ attribute: "theme-mode", type: String })
|
||||
public themeMode: ThemeMode = "auto";
|
||||
|
||||
@property({ type: Number }) public zoom = 14;
|
||||
|
||||
|
@ -154,7 +155,7 @@ export class HaMap extends ReactiveElement {
|
|||
}
|
||||
|
||||
if (
|
||||
!changedProps.has("darkMode") &&
|
||||
!changedProps.has("themeMode") &&
|
||||
(!changedProps.has("hass") ||
|
||||
(oldHass && oldHass.themes?.darkMode === this.hass.themes?.darkMode))
|
||||
) {
|
||||
|
@ -163,12 +164,18 @@ export class HaMap extends ReactiveElement {
|
|||
this._updateMapStyle();
|
||||
}
|
||||
|
||||
private get _darkMode() {
|
||||
return (
|
||||
this.themeMode === "dark" ||
|
||||
(this.themeMode === "auto" && Boolean(this.hass.themes.darkMode))
|
||||
);
|
||||
}
|
||||
|
||||
private _updateMapStyle(): void {
|
||||
const darkMode = this.darkMode || (this.hass.themes.darkMode ?? false);
|
||||
const forcedDark = this.darkMode;
|
||||
const map = this.renderRoot.querySelector("#map");
|
||||
map!.classList.toggle("dark", darkMode);
|
||||
map!.classList.toggle("forced-dark", forcedDark);
|
||||
map!.classList.toggle("dark", this._darkMode);
|
||||
map!.classList.toggle("forced-dark", this.themeMode === "dark");
|
||||
map!.classList.toggle("forced-light", this.themeMode === "light");
|
||||
}
|
||||
|
||||
private async _loadMap(): Promise<void> {
|
||||
|
@ -398,8 +405,7 @@ export class HaMap extends ReactiveElement {
|
|||
"--dark-primary-color"
|
||||
);
|
||||
|
||||
const className =
|
||||
this.darkMode || this.hass.themes.darkMode ? "dark" : "light";
|
||||
const className = this._darkMode ? "dark" : "light";
|
||||
|
||||
for (const entity of this.entities) {
|
||||
const stateObj = hass.states[getEntityId(entity)];
|
||||
|
@ -543,27 +549,30 @@ export class HaMap extends ReactiveElement {
|
|||
background: #090909;
|
||||
}
|
||||
#map.forced-dark {
|
||||
color: #ffffff;
|
||||
--map-filter: invert(0.9) hue-rotate(170deg) brightness(1.5)
|
||||
contrast(1.2) saturate(0.3);
|
||||
}
|
||||
#map.forced-light {
|
||||
background: #ffffff;
|
||||
color: #000000;
|
||||
--map-filter: invert(0);
|
||||
}
|
||||
#map:active {
|
||||
cursor: grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
.light {
|
||||
color: #000000;
|
||||
}
|
||||
.dark {
|
||||
color: #ffffff;
|
||||
}
|
||||
.leaflet-tile-pane {
|
||||
filter: var(--map-filter);
|
||||
}
|
||||
.dark .leaflet-bar a {
|
||||
background-color: var(--card-background-color, #1c1c1c);
|
||||
background-color: #1c1c1c;
|
||||
color: #ffffff;
|
||||
}
|
||||
.dark .leaflet-bar a:hover {
|
||||
background-color: #313131;
|
||||
}
|
||||
.leaflet-marker-draggable {
|
||||
cursor: move !important;
|
||||
}
|
||||
|
|
|
@ -797,6 +797,7 @@ export class HaAutomationTracer extends LitElement {
|
|||
description: html`${this.hass.localize(
|
||||
`ui.panel.config.automation.trace.messages.${message}`,
|
||||
{
|
||||
reason: this.trace.script_execution,
|
||||
time: renderFinishedAt(),
|
||||
executiontime: renderRuntime(),
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { EntityFilter } from "../common/entity/entity_filter";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
type StrictConnectionMode = "disabled" | "guard_page" | "drop_connection";
|
||||
|
||||
interface CloudStatusNotLoggedIn {
|
||||
logged_in: false;
|
||||
cloud: "disconnected" | "connecting" | "connected";
|
||||
|
@ -19,6 +21,7 @@ export interface CloudPreferences {
|
|||
alexa_enabled: boolean;
|
||||
remote_enabled: boolean;
|
||||
remote_allow_remote_enable: boolean;
|
||||
strict_connection: StrictConnectionMode;
|
||||
google_secure_devices_pin: string | undefined;
|
||||
cloudhooks: { [webhookId: string]: CloudWebhook };
|
||||
alexa_report_state: boolean;
|
||||
|
@ -141,6 +144,7 @@ export const updateCloudPref = (
|
|||
google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"];
|
||||
tts_default_voice?: CloudPreferences["tts_default_voice"];
|
||||
remote_allow_remote_enable?: CloudPreferences["remote_allow_remote_enable"];
|
||||
strict_connection?: CloudPreferences["strict_connection"];
|
||||
}
|
||||
) =>
|
||||
hass.callWS({
|
||||
|
|
|
@ -23,6 +23,8 @@ export interface ConfigEntry {
|
|||
pref_disable_polling: boolean;
|
||||
disabled_by: "user" | null;
|
||||
reason: string | null;
|
||||
error_reason_translation_key: string | null;
|
||||
error_reason_translation_placeholders: Record<string, string> | null;
|
||||
}
|
||||
|
||||
export type ConfigEntryMutableParams = Partial<
|
||||
|
|
|
@ -422,7 +422,8 @@ export const computeHistory = (
|
|||
entityIds: string[],
|
||||
localize: LocalizeFunc,
|
||||
sensorNumericalDeviceClasses: string[],
|
||||
splitDeviceClasses = false
|
||||
splitDeviceClasses = false,
|
||||
forceNumeric = false
|
||||
): HistoryResult => {
|
||||
const lineChartDevices: { [unit: string]: HistoryStates } = {};
|
||||
const timelineDevices: TimelineEntity[] = [];
|
||||
|
@ -468,6 +469,7 @@ export const computeHistory = (
|
|||
let unit: string | undefined;
|
||||
|
||||
const isNumeric =
|
||||
forceNumeric ||
|
||||
isNumericFromDomain(domain) ||
|
||||
(currentState != null &&
|
||||
isNumericFromAttributes(currentState.attributes)) ||
|
||||
|
|
|
@ -5,9 +5,7 @@ import {
|
|||
import { getExtendedEntityRegistryEntry } from "./entity_registry";
|
||||
import { showEnterCodeDialog } from "../dialogs/enter-code/show-enter-code-dialog";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export const FORMAT_TEXT = "text";
|
||||
export const FORMAT_NUMBER = "number";
|
||||
import { UNAVAILABLE } from "./entity";
|
||||
|
||||
export const enum LockEntityFeature {
|
||||
OPEN = 1,
|
||||
|
@ -24,6 +22,33 @@ export interface LockEntity extends HassEntityBase {
|
|||
|
||||
type ProtectedLockService = "lock" | "unlock" | "open";
|
||||
|
||||
export function isLocked(stateObj: LockEntity) {
|
||||
return stateObj.state === "locked";
|
||||
}
|
||||
|
||||
export function isUnlocking(stateObj: LockEntity) {
|
||||
return stateObj.state === "unlocking";
|
||||
}
|
||||
|
||||
export function isLocking(stateObj: LockEntity) {
|
||||
return stateObj.state === "locking";
|
||||
}
|
||||
|
||||
export function isJammed(stateObj: LockEntity) {
|
||||
return stateObj.state === "jammed";
|
||||
}
|
||||
|
||||
export function isAvailable(stateObj: LockEntity) {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return false;
|
||||
}
|
||||
const assumedState = stateObj.attributes.assumed_state === true;
|
||||
return (
|
||||
assumedState ||
|
||||
(!isLocking(stateObj) && !isUnlocking(stateObj) && !isJammed(stateObj))
|
||||
);
|
||||
}
|
||||
|
||||
export const callProtectedLockService = async (
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
|
|
|
@ -78,6 +78,7 @@ class LightColorTempPicker extends LitElement {
|
|||
|
||||
return html`
|
||||
<ha-control-slider
|
||||
touch-action="none"
|
||||
inverted
|
||||
vertical
|
||||
.value=${this._ctPickerValue}
|
||||
|
|
|
@ -16,21 +16,23 @@ class MoreInfoCounter extends LitElement {
|
|||
return nothing;
|
||||
}
|
||||
|
||||
const disabled = isUnavailableState(this.stateObj!.state);
|
||||
const disabled = isUnavailableState(this.stateObj.state);
|
||||
|
||||
return html`
|
||||
<div class="actions">
|
||||
<mwc-button
|
||||
.action=${"increment"}
|
||||
@click=${this._handleActionClick}
|
||||
.disabled=${disabled}
|
||||
.disabled=${disabled ||
|
||||
Number(this.stateObj.state) === this.stateObj.attributes.maximum}
|
||||
>
|
||||
${this.hass!.localize("ui.card.counter.actions.increment")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
.action=${"decrement"}
|
||||
@click=${this._handleActionClick}
|
||||
.disabled=${disabled}
|
||||
.disabled=${disabled ||
|
||||
Number(this.stateObj.state) === this.stateObj.attributes.minimum}
|
||||
>
|
||||
${this.hass!.localize("ui.card.counter.actions.decrement")}
|
||||
</mwc-button>
|
||||
|
|
|
@ -189,6 +189,7 @@ class MoreInfoLawnMower extends LitElement {
|
|||
.flex-horizontal {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.space-around {
|
||||
justify-content: space-around;
|
||||
|
|
|
@ -9,11 +9,12 @@ import "../../../components/ha-control-button";
|
|||
import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-outlined-icon-button";
|
||||
import "../../../components/ha-state-icon";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import {
|
||||
LockEntity,
|
||||
LockEntityFeature,
|
||||
callProtectedLockService,
|
||||
isAvailable,
|
||||
isJammed,
|
||||
} from "../../../data/lock";
|
||||
import "../../../state-control/lock/ha-state-control-lock-toggle";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
@ -85,15 +86,13 @@ class MoreInfoLock extends LitElement {
|
|||
"--state-color": color,
|
||||
};
|
||||
|
||||
const isJammed = this.stateObj.state === "jammed";
|
||||
|
||||
return html`
|
||||
<ha-more-info-state-header
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-more-info-state-header>
|
||||
<div class="controls" style=${styleMap(style)}>
|
||||
${this.stateObj.state === "jammed"
|
||||
${isJammed(this.stateObj)
|
||||
? html`
|
||||
<div class="status">
|
||||
<span></span>
|
||||
|
@ -125,7 +124,7 @@ class MoreInfoLock extends LitElement {
|
|||
`
|
||||
: html`
|
||||
<ha-control-button
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
.disabled=${!isAvailable(this.stateObj)}
|
||||
class="open-button ${this._buttonState}"
|
||||
@click=${this._open}
|
||||
>
|
||||
|
@ -139,7 +138,7 @@ class MoreInfoLock extends LitElement {
|
|||
: nothing}
|
||||
</div>
|
||||
<div>
|
||||
${isJammed
|
||||
${isJammed(this.stateObj)
|
||||
? html`
|
||||
<ha-control-button-group class="jammed">
|
||||
<ha-control-button @click=${this._unlock}>
|
||||
|
|
|
@ -19,6 +19,7 @@ import "../../../components/ha-select";
|
|||
import "../../../components/ha-slider";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
|
||||
import { isUnavailableState } from "../../../data/entity";
|
||||
import {
|
||||
MediaPickedEvent,
|
||||
MediaPlayerEntity,
|
||||
|
@ -62,7 +63,8 @@ class MoreInfoMediaPlayer extends LitElement {
|
|||
`
|
||||
)}
|
||||
</div>
|
||||
${supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
|
||||
${!isUnavailableState(stateObj.state) &&
|
||||
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
|
||||
? html`
|
||||
<mwc-button
|
||||
.label=${this.hass.localize(
|
||||
|
|
|
@ -142,7 +142,7 @@ class MoreInfoVacuum extends LitElement {
|
|||
"ui.dialogs.more_info_control.vacuum.commands"
|
||||
)}
|
||||
</div>
|
||||
<div class="flex-horizontal">
|
||||
<div class="flex-horizontal space-around">
|
||||
${VACUUM_COMMANDS.filter((item) =>
|
||||
item.isVisible(stateObj)
|
||||
).map(
|
||||
|
@ -327,6 +327,9 @@ class MoreInfoVacuum extends LitElement {
|
|||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.space-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,19 @@ import { fireEvent } from "../common/dom/fire_event";
|
|||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { showAutomationEditor } from "../data/automation";
|
||||
import { HomeAssistantMain } from "../layouts/home-assistant-main";
|
||||
import type { EMIncomingMessageCommands } from "./external_messaging";
|
||||
import type {
|
||||
EMIncomingMessageBarCodeScanAborted,
|
||||
EMIncomingMessageBarCodeScanResult,
|
||||
EMIncomingMessageCommands,
|
||||
} from "./external_messaging";
|
||||
|
||||
const barCodeListeners = new Set<
|
||||
(
|
||||
msg:
|
||||
| EMIncomingMessageBarCodeScanResult
|
||||
| EMIncomingMessageBarCodeScanAborted
|
||||
) => boolean
|
||||
>();
|
||||
|
||||
export const attachExternalToApp = (hassMainEl: HomeAssistantMain) => {
|
||||
window.addEventListener("haptic", (ev) =>
|
||||
|
@ -24,6 +36,19 @@ export const attachExternalToApp = (hassMainEl: HomeAssistantMain) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const addExternalBarCodeListener = (
|
||||
listener: (
|
||||
msg:
|
||||
| EMIncomingMessageBarCodeScanResult
|
||||
| EMIncomingMessageBarCodeScanAborted
|
||||
) => boolean
|
||||
) => {
|
||||
barCodeListeners.add(listener);
|
||||
return () => {
|
||||
barCodeListeners.delete(listener);
|
||||
};
|
||||
};
|
||||
|
||||
const handleExternalMessage = (
|
||||
hassMainEl: HomeAssistantMain,
|
||||
msg: EMIncomingMessageCommands
|
||||
|
@ -88,6 +113,22 @@ const handleExternalMessage = (
|
|||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else if (msg.command === "bar_code/scan_result") {
|
||||
barCodeListeners.forEach((listener) => listener(msg));
|
||||
bus.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else if (msg.command === "bar_code/aborted") {
|
||||
barCodeListeners.forEach((listener) => listener(msg));
|
||||
bus.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -37,9 +37,11 @@ interface EMOutgoingMessageConfigGet extends EMMessage {
|
|||
|
||||
interface EMOutgoingMessageBarCodeScan extends EMMessage {
|
||||
type: "bar_code/scan";
|
||||
title: string;
|
||||
description: string;
|
||||
alternative_option_label?: string;
|
||||
payload: {
|
||||
title: string;
|
||||
description: string;
|
||||
alternative_option_label?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageBarCodeClose extends EMMessage {
|
||||
|
@ -48,7 +50,9 @@ interface EMOutgoingMessageBarCodeClose extends EMMessage {
|
|||
|
||||
interface EMOutgoingMessageBarCodeNotify extends EMMessage {
|
||||
type: "bar_code/notify";
|
||||
message: string;
|
||||
payload: {
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface EMOutgoingMessageMatterCommission extends EMMessage {
|
||||
|
|
|
@ -41,14 +41,6 @@ import type { HomeAssistant, Route } from "../types";
|
|||
import "./hass-tabs-subpage";
|
||||
import type { PageNavigation } from "./hass-tabs-subpage";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"search-changed": { value: string };
|
||||
"clear-filter": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("hass-tabs-subpage-data-table")
|
||||
export class HaTabsSubpageDataTable extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
@ -63,6 +55,8 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||
|
||||
@property({ type: Boolean, attribute: "main-page" }) public mainPage = false;
|
||||
|
||||
@property({ attribute: false }) public initialCollapsedGroups: string[] = [];
|
||||
|
||||
/**
|
||||
* Object with the columns.
|
||||
* @type {Object}
|
||||
|
@ -166,8 +160,15 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||
|
||||
@property({ type: Boolean }) public showFilters = false;
|
||||
|
||||
@property({ attribute: false }) public initialSorting?: {
|
||||
column: string;
|
||||
direction: SortingDirection;
|
||||
};
|
||||
|
||||
@property() public initialGroupColumn?: string;
|
||||
|
||||
@property({ attribute: false }) public groupOrder?: string[];
|
||||
|
||||
@state() private _sortColumn?: string;
|
||||
|
||||
@state() private _sortDirection: SortingDirection = null;
|
||||
|
@ -190,9 +191,16 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||
this._dataTable.clearSelection();
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
protected willUpdate() {
|
||||
if (this.hasUpdated) {
|
||||
return;
|
||||
}
|
||||
if (this.initialGroupColumn) {
|
||||
this._groupColumn = this.initialGroupColumn;
|
||||
this._setGroupColumn(this.initialGroupColumn);
|
||||
}
|
||||
if (this.initialSorting) {
|
||||
this._sortColumn = this.initialSorting.column;
|
||||
this._sortDirection = this.initialSorting.direction;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,6 +426,8 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||
.sortColumn=${this._sortColumn}
|
||||
.sortDirection=${this._sortDirection}
|
||||
.groupColumn=${this._groupColumn}
|
||||
.groupOrder=${this.groupOrder}
|
||||
.initialCollapsedGroups=${this.initialCollapsedGroups}
|
||||
>
|
||||
${!this.narrow
|
||||
? html`
|
||||
|
@ -560,10 +570,20 @@ export class HaTabsSubpageDataTable extends LitElement {
|
|||
this._sortDirection = null;
|
||||
}
|
||||
this._sortColumn = this._sortDirection === null ? undefined : columnId;
|
||||
|
||||
fireEvent(this, "sorting-changed", {
|
||||
column: columnId,
|
||||
direction: this._sortDirection,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleGroupBy(ev) {
|
||||
this._groupColumn = ev.currentTarget.value;
|
||||
this._setGroupColumn(ev.currentTarget.value);
|
||||
}
|
||||
|
||||
private _setGroupColumn(columnId: string) {
|
||||
this._groupColumn = columnId;
|
||||
fireEvent(this, "grouping-changed", { value: columnId });
|
||||
}
|
||||
|
||||
private _enableSelectMode() {
|
||||
|
@ -819,4 +839,11 @@ declare global {
|
|||
interface HTMLElementTagNameMap {
|
||||
"hass-tabs-subpage-data-table": HaTabsSubpageDataTable;
|
||||
}
|
||||
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"search-changed": { value: string };
|
||||
"grouping-changed": { value: string };
|
||||
"clear-filter": undefined;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -169,10 +169,6 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
|||
// @ts-ignore
|
||||
this._loadHassTranslations(this.hass!.language, "entity");
|
||||
|
||||
// Backwards compatibility for custom integrations
|
||||
// @ts-ignore
|
||||
this._loadHassTranslations(this.hass!.language, "state");
|
||||
|
||||
document.addEventListener(
|
||||
"visibilitychange",
|
||||
() => this._checkVisibility(),
|
||||
|
|
|
@ -41,7 +41,7 @@ import type { HomeAssistant } from "../types";
|
|||
import { onBoardingStyles } from "./styles";
|
||||
|
||||
const AMSTERDAM: [number, number] = [52.3731339, 4.8903147];
|
||||
const mql = matchMedia("(prefers-color-scheme: dark)");
|
||||
const darkMql = matchMedia("(prefers-color-scheme: dark)");
|
||||
const LOCATION_MARKER_ID = "location";
|
||||
|
||||
@customElement("onboarding-location")
|
||||
|
@ -199,7 +199,7 @@ class OnboardingLocation extends LitElement {
|
|||
this._highlightedMarker
|
||||
)}
|
||||
zoom="14"
|
||||
.darkMode=${mql.matches}
|
||||
.themeMode=${darkMql.matches ? "dark" : "light"}
|
||||
.disabled=${this._working}
|
||||
@location-updated=${this._locationChanged}
|
||||
@marker-clicked=${this._markerClicked}
|
||||
|
|
|
@ -511,6 +511,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
mdiPlus,
|
||||
mdiRobotHappy,
|
||||
mdiTag,
|
||||
mdiTextureBox,
|
||||
mdiToggleSwitch,
|
||||
mdiToggleSwitchOffOutline,
|
||||
mdiTransitConnection,
|
||||
|
@ -37,15 +38,21 @@ import { computeCssColor } from "../../../common/color/compute-color";
|
|||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
import "../../../components/chips/ha-assist-chip";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
|
@ -63,6 +70,7 @@ import type { HaMenu } from "../../../components/ha-menu";
|
|||
import "../../../components/ha-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import {
|
||||
AutomationEntity,
|
||||
deleteAutomation,
|
||||
|
@ -100,15 +108,12 @@ import { haStyle } from "../../../resources/styles";
|
|||
import { HomeAssistant, Route, ServiceCallResponse } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity";
|
||||
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import { showNewAutomationDialog } from "./show-dialog-new-automation";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
|
||||
type AutomationItem = AutomationEntity & {
|
||||
name: string;
|
||||
|
@ -156,6 +161,19 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||
|
||||
@state() private _overflowAutomation?: AutomationItem;
|
||||
|
||||
@storage({ key: "automation-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "automation-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({
|
||||
key: "automation-table-collapsed",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@query("#overflow-menu") private _overflowMenu!: HaMenu;
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
|
@ -388,6 +406,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||
</div>
|
||||
</ha-menu-item>`;
|
||||
|
||||
const labelItems = html`${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((entityId) =>
|
||||
|
@ -424,9 +443,46 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</div></ha-menu-item
|
||||
>`;
|
||||
const labelsInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 700) ||
|
||||
|
||||
const areaItems = html`${Object.values(this.hass.areas).map(
|
||||
(area) =>
|
||||
html`<ha-menu-item
|
||||
.value=${area.area_id}
|
||||
@click=${this._handleBulkArea}
|
||||
>
|
||||
${area.icon
|
||||
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
<div slot="headline">${area.name}</div>
|
||||
</ha-menu-item>`
|
||||
)}
|
||||
<ha-menu-item .value=${null} @click=${this._handleBulkArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-menu-item>
|
||||
<md-divider role="separator" tabindex="-1"></md-divider>
|
||||
<ha-menu-item @click=${this._bulkCreateArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-menu-item>`;
|
||||
|
||||
const areasInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 900) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
|
||||
const labelsInOverflow =
|
||||
areasInOverflow &&
|
||||
(!this._sizeController.value || this._sizeController.value < 700);
|
||||
|
||||
const automations = this._automations(
|
||||
this.automations,
|
||||
this._entityReg,
|
||||
|
@ -468,7 +524,12 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||
this.hass.localize,
|
||||
this.hass.locale
|
||||
)}
|
||||
initialGroupColumn="category"
|
||||
.initialGroupColumn=${this._activeGrouping || "category"}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
.data=${automations}
|
||||
.empty=${!this.automations.length}
|
||||
@row-click=${this._handleRowClicked}
|
||||
|
@ -576,6 +637,22 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${labelItems}
|
||||
</ha-button-menu-new>`}
|
||||
${areasInOverflow
|
||||
? nothing
|
||||
: html`<ha-button-menu-new slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${areaItems}
|
||||
</ha-button-menu-new>`}`
|
||||
: nothing
|
||||
}
|
||||
|
@ -640,6 +717,24 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
|||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.narrow || areasInOverflow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-menu-item>
|
||||
<ha-menu slot="menu">${areaItems}</ha-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
<ha-menu-item @click=${this._handleBulkEnable}>
|
||||
<ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
|
@ -1169,6 +1264,46 @@ ${rejected
|
|||
}
|
||||
}
|
||||
|
||||
private async _handleBulkArea(ev) {
|
||||
const area = ev.currentTarget.value;
|
||||
this._bulkAddArea(area);
|
||||
}
|
||||
|
||||
private async _bulkAddArea(area: string) {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
promises.push(
|
||||
updateEntityRegistryEntry(this.hass, entityId, {
|
||||
area_id: area,
|
||||
})
|
||||
);
|
||||
});
|
||||
const result = await Promise.allSettled(promises);
|
||||
if (hasRejectedItems(result)) {
|
||||
const rejected = rejectedItems(result);
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.common.multiselect.failed", {
|
||||
number: rejected.length,
|
||||
}),
|
||||
text: html`<pre>
|
||||
${rejected
|
||||
.map((r) => r.reason.message || r.reason.code || r.reason)
|
||||
.join("\r\n")}</pre
|
||||
>`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _bulkCreateArea() {
|
||||
showAreaRegistryDetailDialog(this, {
|
||||
createEntry: async (values) => {
|
||||
const area = await createAreaRegistryEntry(this.hass, values);
|
||||
this._bulkAddArea(area.area_id);
|
||||
return area;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleBulkEnable() {
|
||||
const promises: Promise<ServiceCallResponse>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
|
@ -1236,6 +1371,18 @@ ${rejected
|
|||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
this._activeGrouping = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCollapseChanged(ev: CustomEvent) {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
|
|
@ -97,7 +97,7 @@ export class HaManualAutomationEditor extends LitElement {
|
|||
<ha-automation-trigger
|
||||
role="region"
|
||||
aria-labelledby="triggers-heading"
|
||||
.triggers=${this.config.trigger}
|
||||
.triggers=${this.config.trigger || []}
|
||||
.path=${["trigger"]}
|
||||
@value-changed=${this._triggerChanged}
|
||||
@item-moved=${this._itemMoved}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
|
|||
import "../../../../../components/device/ha-device-picker";
|
||||
import "../../../../../components/device/ha-device-trigger-picker";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import { computeInitialHaFormData } from "../../../../../components/ha-form/compute-initial-ha-form-data";
|
||||
import { fullEntitiesContext } from "../../../../../data/context";
|
||||
import {
|
||||
deviceAutomationsEqual,
|
||||
|
@ -44,7 +45,9 @@ export class HaDeviceTrigger extends LitElement {
|
|||
|
||||
private _extraFieldsData = memoizeOne(
|
||||
(trigger: DeviceTrigger, capabilities: DeviceCapabilities) => {
|
||||
const extraFieldsData: Record<string, any> = {};
|
||||
const extraFieldsData = computeInitialHaFormData(
|
||||
capabilities.extra_fields
|
||||
);
|
||||
capabilities.extra_fields.forEach((item) => {
|
||||
if (trigger[item.name] !== undefined) {
|
||||
extraFieldsData![item.name] = trigger[item.name];
|
||||
|
|
|
@ -24,6 +24,7 @@ import { extractSearchParam } from "../../../common/url/search-params";
|
|||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/entity/ha-entity-toggle";
|
||||
import "../../../components/ha-button";
|
||||
|
@ -54,6 +55,7 @@ import { documentationUrl } from "../../../util/documentation-url";
|
|||
import { showToast } from "../../../util/toast";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showAddBlueprintDialog } from "./show-dialog-import-blueprint";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
|
||||
type BlueprintMetaDataPath = BlueprintMetaData & {
|
||||
path: string;
|
||||
|
@ -92,8 +94,24 @@ class HaBlueprintOverview extends LitElement {
|
|||
Blueprints
|
||||
>;
|
||||
|
||||
@storage({ key: "blueprint-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "blueprint-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({
|
||||
key: "blueprint-table-collapsed",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
private _processedBlueprints = memoizeOne(
|
||||
(blueprints: Record<string, Blueprints>): BlueprintMetaDataPath[] => {
|
||||
(
|
||||
blueprints: Record<string, Blueprints>,
|
||||
localize: LocalizeFunc
|
||||
): BlueprintMetaDataPath[] => {
|
||||
const result: any[] = [];
|
||||
Object.entries(blueprints).forEach(([type, typeBlueprints]) =>
|
||||
Object.entries(typeBlueprints).forEach(([path, blueprint]) => {
|
||||
|
@ -101,6 +119,9 @@ class HaBlueprintOverview extends LitElement {
|
|||
result.push({
|
||||
name: blueprint.error,
|
||||
type,
|
||||
translated_type: localize(
|
||||
`ui.panel.config.blueprint.overview.types.${type as "automation" | "script"}`
|
||||
),
|
||||
error: true,
|
||||
path,
|
||||
fullpath: `${type}/${path}`,
|
||||
|
@ -109,6 +130,9 @@ class HaBlueprintOverview extends LitElement {
|
|||
result.push({
|
||||
...blueprint.metadata,
|
||||
type,
|
||||
translated_type: localize(
|
||||
`ui.panel.config.blueprint.overview.types.${type as "automation" | "script"}`
|
||||
),
|
||||
error: false,
|
||||
path,
|
||||
fullpath: `${type}/${path}`,
|
||||
|
@ -140,14 +164,11 @@ class HaBlueprintOverview extends LitElement {
|
|||
`
|
||||
: undefined,
|
||||
},
|
||||
type: {
|
||||
translated_type: {
|
||||
title: localize("ui.panel.config.blueprint.overview.headers.type"),
|
||||
template: (blueprint) =>
|
||||
html`${this.hass.localize(
|
||||
`ui.panel.config.blueprint.overview.types.${blueprint.type}`
|
||||
)}`,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
hidden: narrow,
|
||||
direction: "asc",
|
||||
width: "10%",
|
||||
|
@ -256,7 +277,7 @@ class HaBlueprintOverview extends LitElement {
|
|||
this.hass.language,
|
||||
this.hass.localize
|
||||
)}
|
||||
.data=${this._processedBlueprints(this.blueprints)}
|
||||
.data=${this._processedBlueprints(this.blueprints, this.hass.localize)}
|
||||
id="fullpath"
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.config.blueprint.overview.no_blueprints"
|
||||
|
@ -281,6 +302,12 @@ class HaBlueprintOverview extends LitElement {
|
|||
>
|
||||
</a>
|
||||
</div>`}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
|
@ -341,9 +368,10 @@ class HaBlueprintOverview extends LitElement {
|
|||
}
|
||||
|
||||
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
|
||||
const blueprint = this._processedBlueprints(this.blueprints).find(
|
||||
(b) => b.fullpath === ev.detail.id
|
||||
)!;
|
||||
const blueprint = this._processedBlueprints(
|
||||
this.blueprints,
|
||||
this.hass.localize
|
||||
).find((b) => b.fullpath === ev.detail.id)!;
|
||||
if (blueprint.error) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.blueprint.overview.error", {
|
||||
|
@ -502,6 +530,18 @@ class HaBlueprintOverview extends LitElement {
|
|||
fireEvent(this, "reload-blueprints");
|
||||
};
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
this._activeGrouping = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCollapseChanged(ev: CustomEvent) {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return haStyle;
|
||||
}
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
import { mdiContentCopy, mdiHelpCircle } from "@mdi/js";
|
||||
import { mdiContentCopy, mdiEye, mdiEyeOff, mdiHelpCircle } from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-formfield";
|
||||
import "../../../../components/ha-radio";
|
||||
import "../../../../components/ha-settings-row";
|
||||
import "../../../../components/ha-switch";
|
||||
// eslint-disable-next-line
|
||||
import { formatDate } from "../../../../common/datetime/format_date";
|
||||
import type { HaRadio } from "../../../../components/ha-radio";
|
||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
||||
import {
|
||||
CloudStatusLoggedIn,
|
||||
|
@ -20,6 +23,7 @@ import {
|
|||
} from "../../../../data/cloud";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showToast } from "../../../../util/toast";
|
||||
import { showAlertDialog } from "../../../lovelace/custom-card-helpers";
|
||||
import { showCloudCertificateDialog } from "../dialog-cloud-certificate/show-dialog-cloud-certificate";
|
||||
|
||||
@customElement("cloud-remote-pref")
|
||||
|
@ -28,12 +32,14 @@ export class CloudRemotePref extends LitElement {
|
|||
|
||||
@property({ attribute: false }) public cloudStatus?: CloudStatusLoggedIn;
|
||||
|
||||
@state() private _unmaskedUrl = false;
|
||||
|
||||
protected render() {
|
||||
if (!this.cloudStatus) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const { remote_enabled, remote_allow_remote_enable } =
|
||||
const { remote_enabled, remote_allow_remote_enable, strict_connection } =
|
||||
this.cloudStatus.prefs;
|
||||
|
||||
const {
|
||||
|
@ -108,35 +114,180 @@ export class CloudRemotePref extends LitElement {
|
|||
)}
|
||||
></ha-alert>
|
||||
`
|
||||
: ""}
|
||||
${this.hass.localize("ui.panel.config.cloud.account.remote.info")}
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.cloud.account.remote.${
|
||||
remote_connected
|
||||
? "instance_is_available"
|
||||
: "instance_will_be_available"
|
||||
}`
|
||||
)}
|
||||
<a
|
||||
href="https://${remote_domain}"
|
||||
target="_blank"
|
||||
class="break-word"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.nabu_casa_url"
|
||||
)}</a
|
||||
>.
|
||||
<ha-svg-icon
|
||||
.url=${`https://${remote_domain}`}
|
||||
@click=${this._copyURL}
|
||||
.path=${mdiContentCopy}
|
||||
></ha-svg-icon>
|
||||
: strict_connection === "drop_connection"
|
||||
? html`<ha-alert
|
||||
alert-type="warning"
|
||||
.title=${this.hass.localize(
|
||||
`ui.panel.config.cloud.account.remote.drop_connection_warning_title`
|
||||
)}
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.cloud.account.remote.drop_connection_warning`
|
||||
)}</ha-alert
|
||||
>`
|
||||
: nothing}
|
||||
<p>
|
||||
${this.hass.localize("ui.panel.config.cloud.account.remote.info")}
|
||||
</p>
|
||||
${remote_connected
|
||||
? nothing
|
||||
: html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.info_instance_will_be_available"
|
||||
)}
|
||||
</p>
|
||||
`}
|
||||
<div class="url-container">
|
||||
<div class="textfield-container">
|
||||
<ha-textfield
|
||||
.value=${this._unmaskedUrl
|
||||
? `https://${remote_domain}`
|
||||
: "https://•••••••••••••••••.ui.nabu.casa"}
|
||||
readonly
|
||||
.suffix=${
|
||||
// reserve some space for the icon.
|
||||
html`<div style="width: 24px"></div>`
|
||||
}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
class="toggle-unmasked-url"
|
||||
toggles
|
||||
.label=${this.hass.localize(
|
||||
`ui.panel.config.cloud.account.remote.${this._unmaskedUrl ? "hide" : "show"}_url`
|
||||
)}
|
||||
@click=${this._toggleUnmaskedUrl}
|
||||
.path=${this._unmaskedUrl ? mdiEyeOff : mdiEye}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<ha-button
|
||||
.url=${`https://${remote_domain}`}
|
||||
@click=${this._copyURL}
|
||||
unelevated
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.copy_link"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
|
||||
<ha-expansion-panel
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.advanced_options"
|
||||
"ui.panel.config.cloud.account.remote.security_options"
|
||||
)}
|
||||
>
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_secondary"
|
||||
)}
|
||||
</span>
|
||||
</ha-settings-row>
|
||||
|
||||
<div class="strict-connection-container">
|
||||
<ha-formfield>
|
||||
<ha-radio
|
||||
name="strict-connection-mode"
|
||||
value="disabled"
|
||||
.checked=${strict_connection === "disabled"}
|
||||
@change=${this._strictConnectionModeChanged}
|
||||
></ha-radio>
|
||||
<div slot="label">
|
||||
<div class="primary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_option_disabled"
|
||||
)}
|
||||
</div>
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_option_disabled_secondary"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ha-formfield>
|
||||
|
||||
<ha-formfield>
|
||||
<ha-radio
|
||||
name="strict-connection-mode"
|
||||
value="guard_page"
|
||||
.checked=${strict_connection === "guard_page"}
|
||||
@change=${this._strictConnectionModeChanged}
|
||||
></ha-radio>
|
||||
<div slot="label">
|
||||
<div class="primary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_option_guard_page"
|
||||
)}
|
||||
</div>
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_option_guard_page_secondary"
|
||||
)}
|
||||
<br /><br />
|
||||
⚠️
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_option_guard_page_warning"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ha-formfield>
|
||||
|
||||
<ha-formfield>
|
||||
<ha-radio
|
||||
name="strict-connection-mode"
|
||||
value="drop_connection"
|
||||
.checked=${strict_connection === "drop_connection"}
|
||||
@change=${this._strictConnectionModeChanged}
|
||||
></ha-radio>
|
||||
<div slot="label">
|
||||
<div class="primary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_option_drop_connection"
|
||||
)}
|
||||
</div>
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_option_drop_connection_secondary"
|
||||
)}
|
||||
<br /><br />
|
||||
⚠️
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_option_drop_connection_warning"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
|
||||
${strict_connection !== "disabled"
|
||||
? html`
|
||||
<ha-settings-row>
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_link"
|
||||
)}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_link_secondary"
|
||||
)}</span
|
||||
>
|
||||
<ha-button @click=${this._createLoginUrl}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_create_link"
|
||||
)}</ha-button
|
||||
>
|
||||
</ha-settings-row>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<hr />
|
||||
<ha-settings-row>
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
|
@ -153,6 +304,7 @@ export class CloudRemotePref extends LitElement {
|
|||
@change=${this._toggleAllowRemoteEnabledChanged}
|
||||
></ha-switch>
|
||||
</ha-settings-row>
|
||||
<hr />
|
||||
<ha-settings-row>
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
|
@ -193,6 +345,10 @@ export class CloudRemotePref extends LitElement {
|
|||
});
|
||||
}
|
||||
|
||||
private _toggleUnmaskedUrl(): void {
|
||||
this._unmaskedUrl = !this._unmaskedUrl;
|
||||
}
|
||||
|
||||
private async _toggleChanged(ev) {
|
||||
const toggle = ev.target as HaSwitch;
|
||||
|
||||
|
@ -223,6 +379,24 @@ export class CloudRemotePref extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
private async _strictConnectionModeChanged(ev) {
|
||||
const toggle = ev.target as HaRadio;
|
||||
|
||||
if (ev.target.value === this.cloudStatus?.prefs.strict_connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateCloudPref(this.hass, {
|
||||
strict_connection: ev.target.value,
|
||||
});
|
||||
fireEvent(this, "ha-refresh-cloud-status");
|
||||
} catch (err: any) {
|
||||
alert(err.message);
|
||||
toggle.checked = !toggle.checked;
|
||||
}
|
||||
}
|
||||
|
||||
private async _copyURL(ev): Promise<void> {
|
||||
const url = ev.currentTarget.url;
|
||||
await copyToClipboard(url);
|
||||
|
@ -231,6 +405,45 @@ export class CloudRemotePref extends LitElement {
|
|||
});
|
||||
}
|
||||
|
||||
private async _createLoginUrl() {
|
||||
try {
|
||||
const result = await this.hass.callService(
|
||||
"cloud",
|
||||
"create_temporary_strict_connection_url",
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
true
|
||||
);
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_link"
|
||||
),
|
||||
text: html`${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.strict_connection_link_created_message"
|
||||
)}
|
||||
<div
|
||||
style="display: flex; align-items: center; gap: 8px; margin-top: 8px;"
|
||||
>
|
||||
<ha-textfield .value=${result.response.url} readonly></ha-textfield>
|
||||
<ha-button
|
||||
style="flex-basis: 180px;"
|
||||
.url=${result.response.url}
|
||||
@click=${this._copyURL}
|
||||
unelevated
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiContentCopy}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.copy_link"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>`,
|
||||
});
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, { text: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.preparing {
|
||||
|
@ -241,8 +454,8 @@ export class CloudRemotePref extends LitElement {
|
|||
}
|
||||
.header-actions {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
inset-inline-end: 24px;
|
||||
right: 16px;
|
||||
inset-inline-end: 16px;
|
||||
inset-inline-start: initial;
|
||||
top: 24px;
|
||||
display: flex;
|
||||
|
@ -276,17 +489,71 @@ export class CloudRemotePref extends LitElement {
|
|||
.card-actions a {
|
||||
text-decoration: none;
|
||||
}
|
||||
ha-svg-icon {
|
||||
--mdc-icon-size: 18px;
|
||||
color: var(--secondary-text-color);
|
||||
cursor: pointer;
|
||||
ha-expansion-panel {
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-formfield {
|
||||
margin-top: 8px;
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--expansion-panel-content-padding: 0 16px;
|
||||
--expansion-panel-summary-padding: 0 16px;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.url-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.textfield-container {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
.textfield-container ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
.toggle-unmasked-url {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 8px;
|
||||
--mdc-icon-button-size: 40px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
ha-formfield {
|
||||
margin-left: -12px;
|
||||
margin-inline-start: -12px;
|
||||
--ha-formfield-align-items: start;
|
||||
}
|
||||
.strict-connection-container {
|
||||
gap: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.strict-connection-container ha-formfield {
|
||||
--ha-formfield-align-items: start;
|
||||
}
|
||||
.strict-connection-container .primary {
|
||||
font-size: 14px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.strict-connection-container .secondary {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
background-color: var(--divider-color);
|
||||
margin: 8px 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,11 +82,11 @@ class HaConfigSectionUpdates extends LitElement {
|
|||
>
|
||||
${this.hass.localize("ui.panel.config.updates.show_skipped")}
|
||||
</ha-check-list-item>
|
||||
${this._supervisorInfo?.channel !== "dev"
|
||||
${this._supervisorInfo && this._supervisorInfo.channel !== "dev"
|
||||
? html`
|
||||
<li divider role="separator"></li>
|
||||
<mwc-list-item @request-selected=${this._toggleBeta}>
|
||||
${this._supervisorInfo?.channel === "stable"
|
||||
${this._supervisorInfo.channel === "stable"
|
||||
? this.hass.localize("ui.panel.config.updates.join_beta")
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.updates.leave_beta"
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import { consume } from "@lit-labs/context";
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import { mdiChevronRight, mdiMenuDown, mdiPlus } from "@mdi/js";
|
||||
import {
|
||||
mdiChevronRight,
|
||||
mdiDotsVertical,
|
||||
mdiMenuDown,
|
||||
mdiPlus,
|
||||
mdiTextureBox,
|
||||
} from "@mdi/js";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
|
@ -10,10 +16,12 @@ import {
|
|||
nothing,
|
||||
} from "lit";
|
||||
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import {
|
||||
|
@ -22,10 +30,15 @@ import {
|
|||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/entity/ha-battery-icon";
|
||||
|
@ -41,6 +54,7 @@ import "../../../components/ha-filter-states";
|
|||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { ConfigEntry, sortConfigEntries } from "../../../data/config_entries";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import {
|
||||
|
@ -65,15 +79,12 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
|||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { brandsUrl } from "../../../util/brands-url";
|
||||
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import "../integrations/ha-integration-overflow-menu";
|
||||
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
import { showAlertDialog } from "../../lovelace/custom-card-helpers";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
|
||||
interface DeviceRowData extends DeviceRegistryEntry {
|
||||
device?: DeviceRowData;
|
||||
|
@ -117,6 +128,19 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||
@state()
|
||||
_labels!: LabelRegistryEntry[];
|
||||
|
||||
@storage({ key: "devices-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "devices-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({ key: "devices-table-collapsed", state: false, subscribe: false })
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
});
|
||||
|
||||
private _ignoreLocationChange = false;
|
||||
|
||||
public connectedCallback() {
|
||||
|
@ -549,6 +573,41 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||
this._labels
|
||||
);
|
||||
|
||||
const areasInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 700) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
|
||||
const areaItems = html`${Object.values(this.hass.areas).map(
|
||||
(area) =>
|
||||
html`<ha-menu-item
|
||||
.value=${area.area_id}
|
||||
@click=${this._handleBulkArea}
|
||||
>
|
||||
${area.icon
|
||||
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
<div slot="headline">${area.name}</div>
|
||||
</ha-menu-item>`
|
||||
)}
|
||||
<ha-menu-item .value=${null} @click=${this._handleBulkArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-menu-item>
|
||||
<md-divider role="separator" tabindex="-1"></md-divider>
|
||||
<ha-menu-item @click=${this._bulkCreateArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-menu-item>`;
|
||||
|
||||
const labelItems = html`${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((deviceId) =>
|
||||
|
@ -614,8 +673,14 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||
Array.isArray(val) ? val.length : val
|
||||
)
|
||||
).length}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@clear-filter=${this._clearFilter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
@row-click=${this._handleRowClicked}
|
||||
clickable
|
||||
hasFab
|
||||
|
@ -684,36 +749,77 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||
|
||||
${!this.narrow
|
||||
? html`<ha-button-menu-new slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${labelItems}
|
||||
</ha-button-menu-new>`
|
||||
: html` <ha-button-menu-new has-overflow slot="selection-bar"
|
||||
><ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
slot="trigger"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${labelItems}
|
||||
</ha-button-menu-new>
|
||||
|
||||
${areasInOverflow
|
||||
? nothing
|
||||
: html`<ha-button-menu-new slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${areaItems}
|
||||
</ha-button-menu-new>`}`
|
||||
: nothing}
|
||||
${this.narrow || areasInOverflow
|
||||
? html`<ha-button-menu-new has-overflow slot="selection-bar">
|
||||
${this.narrow
|
||||
? html`<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_action"
|
||||
)}
|
||||
slot="trigger"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>`
|
||||
: html`<ha-icon-button
|
||||
.path=${mdiDotsVertical}
|
||||
.label=${"ui.panel.config.automation.picker.bulk_action"}
|
||||
slot="trigger"
|
||||
></ha-icon-button>`}
|
||||
${this.narrow
|
||||
? html` <ha-sub-menu>
|
||||
<ha-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-menu-item>
|
||||
<ha-menu slot="menu">${labelItems}</ha-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing}
|
||||
<ha-sub-menu>
|
||||
<ha-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.picker.bulk_actions.add_label"
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
|
@ -721,9 +827,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-menu-item>
|
||||
<ha-menu slot="menu">${labelItems}</ha-menu>
|
||||
<ha-menu slot="menu">${areaItems}</ha-menu>
|
||||
</ha-sub-menu>
|
||||
</ha-button-menu-new>`}
|
||||
</ha-button-menu-new>`
|
||||
: nothing}
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
|
@ -809,6 +916,46 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
|||
this._selected = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _handleBulkArea(ev) {
|
||||
const area = ev.currentTarget.value;
|
||||
this._bulkAddArea(area);
|
||||
}
|
||||
|
||||
private async _bulkAddArea(area: string) {
|
||||
const promises: Promise<DeviceRegistryEntry>[] = [];
|
||||
this._selected.forEach((deviceId) => {
|
||||
promises.push(
|
||||
updateDeviceRegistryEntry(this.hass, deviceId, {
|
||||
area_id: area,
|
||||
})
|
||||
);
|
||||
});
|
||||
const result = await Promise.allSettled(promises);
|
||||
if (hasRejectedItems(result)) {
|
||||
const rejected = rejectedItems(result);
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.common.multiselect.failed", {
|
||||
number: rejected.length,
|
||||
}),
|
||||
text: html`<pre>
|
||||
${rejected
|
||||
.map((r) => r.reason.message || r.reason.code || r.reason)
|
||||
.join("\r\n")}</pre
|
||||
>`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _bulkCreateArea() {
|
||||
showAreaRegistryDetailDialog(this, {
|
||||
createEntry: async (values) => {
|
||||
const area = await createAreaRegistryEntry(this.hass, values);
|
||||
this._bulkAddArea(area.area_id);
|
||||
return area;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async _handleBulkLabel(ev) {
|
||||
const label = ev.currentTarget.value;
|
||||
const action = ev.currentTarget.action;
|
||||
|
@ -855,9 +1002,24 @@ ${rejected
|
|||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
this._activeGrouping = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCollapseChanged(ev: CustomEvent) {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
hass-tabs-subpage-data-table {
|
||||
--data-table-row-height: 60px;
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import { ifDefined } from "lit/directives/if-defined";
|
|||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoize from "memoize-one";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
|
@ -37,16 +38,22 @@ import {
|
|||
protocolIntegrationPicked,
|
||||
} from "../../../common/integrations/protocolIntegrationPicked";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-filter-devices";
|
||||
import "../../../components/ha-filter-domains";
|
||||
import "../../../components/ha-filter-floor-areas";
|
||||
import "../../../components/ha-filter-integrations";
|
||||
import "../../../components/ha-filter-labels";
|
||||
|
@ -66,6 +73,11 @@ import {
|
|||
removeEntityRegistryEntry,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../data/entity_registry";
|
||||
import {
|
||||
EntitySources,
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../../data/entity_sources";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import {
|
||||
LabelRegistryEntry,
|
||||
createLabelRegistryEntry,
|
||||
|
@ -86,14 +98,6 @@ import { configSections } from "../ha-panel-config";
|
|||
import "../integrations/ha-integration-overflow-menu";
|
||||
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import {
|
||||
EntitySources,
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../../data/entity_sources";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
|
||||
export interface StateEntity
|
||||
extends Omit<EntityRegistryEntry, "id" | "unique_id"> {
|
||||
|
@ -110,6 +114,7 @@ export interface EntityRow extends StateEntity {
|
|||
status: string | undefined;
|
||||
area?: string;
|
||||
localized_platform: string;
|
||||
domain: string;
|
||||
label_entries: LabelRegistryEntry[];
|
||||
}
|
||||
|
||||
|
@ -149,6 +154,19 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@storage({ key: "entities-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "entities-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({
|
||||
key: "entities-table-collapsed",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@query("hass-tabs-subpage-data-table", true)
|
||||
private _dataTable!: HaTabsSubpageDataTable;
|
||||
|
||||
|
@ -261,6 +279,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||
filterable: true,
|
||||
width: "20%",
|
||||
},
|
||||
domain: {
|
||||
title: localize("ui.panel.config.entities.picker.headers.domain"),
|
||||
sortable: false,
|
||||
hidden: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
},
|
||||
area: {
|
||||
title: localize("ui.panel.config.entities.picker.headers.area"),
|
||||
sortable: true,
|
||||
|
@ -419,6 +444,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||
entryIds.includes(entity.config_entry_id))
|
||||
);
|
||||
filter.value!.forEach((domain) => filteredDomains.add(domain));
|
||||
} else if (key === "ha-filter-domains" && filter.value?.length) {
|
||||
filteredEntities = filteredEntities.filter((entity) =>
|
||||
filter.value?.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
} else if (key === "ha-filter-labels" && filter.value?.length) {
|
||||
filteredEntities = filteredEntities.filter((entity) =>
|
||||
entity.labels.some((lbl) => filter.value!.includes(lbl))
|
||||
|
@ -467,9 +496,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||
),
|
||||
unavailable,
|
||||
restored,
|
||||
localized_platform:
|
||||
localize(`component.${entry.platform}.title`) || entry.platform,
|
||||
localized_platform: domainToName(localize, entry.platform),
|
||||
area: area ? area.name : "—",
|
||||
domain: domainToName(localize, computeDomain(entry.entity_id)),
|
||||
status: restored
|
||||
? localize("ui.panel.config.entities.picker.status.restored")
|
||||
: unavailable
|
||||
|
@ -594,6 +623,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
|||
.filter=${this._filter}
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
clickable
|
||||
@clear-filter=${this._clearFilter}
|
||||
|
@ -752,6 +787,15 @@ ${
|
|||
.narrow=${this.narrow}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-devices>
|
||||
<ha-filter-domains
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-domains"]?.value}
|
||||
@data-table-filter-changed=${this._filterChanged}
|
||||
slot="filter-pane"
|
||||
.expanded=${this._expandedFilter === "ha-filter-domains"}
|
||||
.narrow=${this.narrow}
|
||||
@expanded-changed=${this._filterExpanded}
|
||||
></ha-filter-domains>
|
||||
<ha-filter-integrations
|
||||
.hass=${this.hass}
|
||||
.value=${this._filters["ha-filter-integrations"]?.value}
|
||||
|
@ -1196,6 +1240,18 @@ ${rejected
|
|||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
this._activeGrouping = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCollapseChanged(ev: CustomEvent) {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
|
@ -40,6 +41,7 @@ import {
|
|||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/ha-fab";
|
||||
|
@ -139,6 +141,19 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@storage({ key: "helpers-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "helpers-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({
|
||||
key: "helpers-table-collapsed",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@state() private _stateItems: HassEntity[] = [];
|
||||
|
||||
@state() private _entityEntries?: Record<string, EntityRegistryEntry>;
|
||||
|
@ -525,7 +540,12 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
|||
).length}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.data=${helpers}
|
||||
initialGroupColumn="category"
|
||||
.initialGroupColumn=${this._activeGrouping || "category"}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
.activeFilters=${this._activeFilters}
|
||||
@clear-filter=${this._clearFilter}
|
||||
@row-click=${this._openEditDialog}
|
||||
|
@ -1020,6 +1040,18 @@ ${rejected
|
|||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
this._activeGrouping = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCollapseChanged(ev: CustomEvent) {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
|
|
@ -37,6 +37,7 @@ import {
|
|||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { until } from "lit/directives/until";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { isDevVersion } from "../../../common/config/version";
|
||||
|
@ -550,10 +551,24 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
|||
`ui.panel.config.integrations.config_entry.state.${item.state}`,
|
||||
];
|
||||
if (item.reason) {
|
||||
this.hass.loadBackendTranslation("config", item.domain);
|
||||
stateTextExtra = html`${this.hass.localize(
|
||||
`component.${item.domain}.config.error.${item.reason}`
|
||||
) || item.reason}`;
|
||||
if (item.error_reason_translation_key) {
|
||||
const lokalisePromExc = this.hass
|
||||
.loadBackendTranslation("exceptions", item.domain)
|
||||
.then((localize) =>
|
||||
localize(
|
||||
`component.${item.domain}.exceptions.${item.error_reason_translation_key}.message`,
|
||||
item.error_reason_translation_placeholders ?? undefined
|
||||
)
|
||||
);
|
||||
stateTextExtra = html`${until(lokalisePromExc)}`;
|
||||
} else {
|
||||
const lokalisePromError = this.hass
|
||||
.loadBackendTranslation("config", item.domain)
|
||||
.then((localize) =>
|
||||
localize(`component.${item.domain}.config.error.${item.reason}`)
|
||||
);
|
||||
stateTextExtra = html`${until(lokalisePromError, item.reason)}`;
|
||||
}
|
||||
} else {
|
||||
stateTextExtra = html`
|
||||
<br />
|
||||
|
|
|
@ -47,6 +47,7 @@ class DialogZHAReconfigureDevice extends LitElement {
|
|||
|
||||
public showDialog(params: ZHAReconfigureDeviceDialogParams): void {
|
||||
this._params = params;
|
||||
this._clusterConfigurationStatuses = new Map();
|
||||
this._stages = undefined;
|
||||
}
|
||||
|
||||
|
|
|
@ -424,7 +424,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
|||
? {
|
||||
physics: {
|
||||
barnesHut: {
|
||||
springConstant: 0.05,
|
||||
springConstant: 0,
|
||||
avoidOverlap: 10,
|
||||
damping: 0.09,
|
||||
},
|
||||
|
|
|
@ -10,15 +10,17 @@ import { LitElement, PropertyValues, html, nothing } from "lit";
|
|||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-relative-time";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-relative-time";
|
||||
import {
|
||||
LabelRegistryEntry,
|
||||
LabelRegistryEntryMutableParams,
|
||||
|
@ -35,7 +37,7 @@ import "../../../layouts/hass-tabs-subpage-data-table";
|
|||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showLabelDetailDialog } from "./show-dialog-label-detail";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
|
||||
@customElement("ha-config-labels")
|
||||
export class HaConfigLabels extends LitElement {
|
||||
|
@ -49,6 +51,13 @@ export class HaConfigLabels extends LitElement {
|
|||
|
||||
@state() private _labels: LabelRegistryEntry[] = [];
|
||||
|
||||
@storage({
|
||||
key: "labels-table-sort",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
private _columns = memoizeOne((localize: LocalizeFunc) => {
|
||||
const columns: DataTableColumnContainer<LabelRegistryEntry> = {
|
||||
icon: {
|
||||
|
@ -149,6 +158,8 @@ export class HaConfigLabels extends LitElement {
|
|||
.data=${this._data(this._labels)}
|
||||
.noDataText=${this.hass.localize("ui.panel.config.labels.no_labels")}
|
||||
hasFab
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@row-click=${this._editLabel}
|
||||
clickable
|
||||
id="label_id"
|
||||
|
@ -268,6 +279,10 @@ export class HaConfigLabels extends LitElement {
|
|||
`/config/automation/dashboard?historyBack=1&label=${label.label_id}`
|
||||
);
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
|
@ -16,6 +16,7 @@ import { stringCompare } from "../../../../common/string/compare";
|
|||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../../components/data-table/ha-data-table";
|
||||
import "../../../../components/ha-clickable-list-item";
|
||||
import "../../../../components/ha-fab";
|
||||
|
@ -46,6 +47,7 @@ import { showNewDashboardDialog } from "../../dashboard/show-dialog-new-dashboar
|
|||
import { lovelaceTabs } from "../ha-config-lovelace";
|
||||
import { showDashboardConfigureStrategyDialog } from "./show-dialog-lovelace-dashboard-configure-strategy";
|
||||
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
|
||||
type DataTableItem = Pick<
|
||||
LovelaceDashboard,
|
||||
|
@ -68,6 +70,13 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||
|
||||
@state() private _dashboards: LovelaceDashboard[] = [];
|
||||
|
||||
@storage({
|
||||
key: "lovelace-dashboards-table-sort",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
public willUpdate() {
|
||||
if (!this.hasUpdated) {
|
||||
this.hass.loadFragmentTranslation("lovelace");
|
||||
|
@ -293,6 +302,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||
this.hass.localize
|
||||
)}
|
||||
.data=${this._getItems(this._dashboards)}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@row-click=${this._editDashboard}
|
||||
id="url_path"
|
||||
hasFab
|
||||
|
@ -440,6 +451,10 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
|
@ -10,9 +10,11 @@ import {
|
|||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoize from "memoize-one";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../../components/data-table/ha-data-table";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-fab";
|
||||
|
@ -33,10 +35,10 @@ import "../../../../layouts/hass-subpage";
|
|||
import "../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../../types";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import { loadLovelaceResources } from "../../../lovelace/common/load-resources";
|
||||
import { lovelaceResourcesTabs } from "../ha-config-lovelace";
|
||||
import { showResourceDetailDialog } from "./show-dialog-lovelace-resource-detail";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
|
||||
@customElement("ha-config-lovelace-resources")
|
||||
export class HaConfigLovelaceRescources extends LitElement {
|
||||
|
@ -50,6 +52,13 @@ export class HaConfigLovelaceRescources extends LitElement {
|
|||
|
||||
@state() private _resources: LovelaceResource[] = [];
|
||||
|
||||
@storage({
|
||||
key: "lovelace-resources-table-sort",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
private _columns = memoize(
|
||||
(
|
||||
_language,
|
||||
|
@ -127,6 +136,8 @@ export class HaConfigLovelaceRescources extends LitElement {
|
|||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.resources.picker.no_resources"
|
||||
)}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@row-click=${this._editResource}
|
||||
hasFab
|
||||
clickable
|
||||
|
@ -237,6 +248,10 @@ export class HaConfigLovelaceRescources extends LitElement {
|
|||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
mdiPlay,
|
||||
mdiPlus,
|
||||
mdiTag,
|
||||
mdiTextureBox,
|
||||
} from "@mdi/js";
|
||||
import { differenceInDays } from "date-fns";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
|
@ -32,14 +33,20 @@ import memoizeOne from "memoize-one";
|
|||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/ha-button";
|
||||
|
@ -55,6 +62,7 @@ import "../../../components/ha-menu-item";
|
|||
import "../../../components/ha-state-icon";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import {
|
||||
CategoryRegistryEntry,
|
||||
createCategoryRegistryEntry,
|
||||
|
@ -91,14 +99,11 @@ import { haStyle } from "../../../resources/styles";
|
|||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
|
||||
type SceneItem = SceneEntity & {
|
||||
name: string;
|
||||
|
@ -144,6 +149,19 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg!: EntityRegistryEntry[];
|
||||
|
||||
@storage({ key: "scene-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "scene-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({
|
||||
key: "scene-table-collapsed",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
});
|
||||
|
@ -391,6 +409,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||
</div>
|
||||
</ha-menu-item>`;
|
||||
|
||||
const labelItems = html` ${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((entityId) =>
|
||||
|
@ -427,9 +446,46 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</div></ha-menu-item
|
||||
>`;
|
||||
const labelsInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 700) ||
|
||||
|
||||
const areaItems = html`${Object.values(this.hass.areas).map(
|
||||
(area) =>
|
||||
html`<ha-menu-item
|
||||
.value=${area.area_id}
|
||||
@click=${this._handleBulkArea}
|
||||
>
|
||||
${area.icon
|
||||
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
<div slot="headline">${area.name}</div>
|
||||
</ha-menu-item>`
|
||||
)}
|
||||
<ha-menu-item .value=${null} @click=${this._handleBulkArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-menu-item>
|
||||
<md-divider role="separator" tabindex="-1"></md-divider>
|
||||
<ha-menu-item @click=${this._bulkCreateArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-menu-item>`;
|
||||
|
||||
const areasInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 900) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
|
||||
const labelsInOverflow =
|
||||
areasInOverflow &&
|
||||
(!this._sizeController.value || this._sizeController.value < 700);
|
||||
|
||||
const scenes = this._scenes(
|
||||
this.scenes,
|
||||
this._entityReg,
|
||||
|
@ -438,6 +494,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||
this._labels,
|
||||
this._filteredScenes
|
||||
);
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
|
@ -463,7 +520,12 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||
).length}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
id="entity_id"
|
||||
initialGroupColumn="category"
|
||||
.initialGroupColumn=${this._activeGrouping || "category"}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
.data=${scenes}
|
||||
.empty=${!this.scenes.length}
|
||||
.activeFilters=${this._activeFilters}
|
||||
|
@ -562,9 +624,25 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${labelItems}
|
||||
</ha-button-menu-new>`}
|
||||
${areasInOverflow
|
||||
? nothing
|
||||
: html`<ha-button-menu-new slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${areaItems}
|
||||
</ha-button-menu-new>`}`
|
||||
: nothing}
|
||||
${this.narrow || labelsInOverflow
|
||||
${this.narrow || areasInOverflow
|
||||
? html`
|
||||
<ha-button-menu-new has-overflow slot="selection-bar">
|
||||
${
|
||||
|
@ -610,8 +688,8 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||
: nothing
|
||||
}
|
||||
${
|
||||
this.narrow || this.hass.dockedSidebar === "docked"
|
||||
? html` <ha-sub-menu>
|
||||
this.narrow || labelsInOverflow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
|
@ -627,6 +705,24 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
|||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.narrow || areasInOverflow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-menu-item>
|
||||
<ha-menu slot="menu">${areaItems}</ha-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
</ha-button-menu-new>`
|
||||
: nothing}
|
||||
${!this.scenes.length
|
||||
|
@ -855,6 +951,46 @@ ${rejected
|
|||
}
|
||||
}
|
||||
|
||||
private async _handleBulkArea(ev) {
|
||||
const area = ev.currentTarget.value;
|
||||
this._bulkAddArea(area);
|
||||
}
|
||||
|
||||
private async _bulkAddArea(area: string) {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
promises.push(
|
||||
updateEntityRegistryEntry(this.hass, entityId, {
|
||||
area_id: area,
|
||||
})
|
||||
);
|
||||
});
|
||||
const result = await Promise.allSettled(promises);
|
||||
if (hasRejectedItems(result)) {
|
||||
const rejected = rejectedItems(result);
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.common.multiselect.failed", {
|
||||
number: rejected.length,
|
||||
}),
|
||||
text: html`<pre>
|
||||
${rejected
|
||||
.map((r) => r.reason.message || r.reason.code || r.reason)
|
||||
.join("\r\n")}</pre
|
||||
>`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _bulkCreateArea() {
|
||||
showAreaRegistryDetailDialog(this, {
|
||||
createEntry: async (values) => {
|
||||
const area = await createAreaRegistryEntry(this.hass, values);
|
||||
this._bulkAddArea(area.area_id);
|
||||
return area;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _editCategory(scene: any) {
|
||||
const entityReg = this._entityReg.find(
|
||||
(reg) => reg.entity_id === scene.entity_id
|
||||
|
@ -975,6 +1111,18 @@ ${rejected
|
|||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
this._activeGrouping = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCollapseChanged(ev: CustomEvent) {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
mdiPlus,
|
||||
mdiScriptText,
|
||||
mdiTag,
|
||||
mdiTextureBox,
|
||||
mdiTransitConnection,
|
||||
} from "@mdi/js";
|
||||
import { differenceInDays } from "date-fns";
|
||||
|
@ -33,14 +34,20 @@ import { computeCssColor } from "../../../common/color/compute-color";
|
|||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
||||
import { relativeTime } from "../../../common/datetime/relative_time";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-labels";
|
||||
import "../../../components/ha-fab";
|
||||
|
@ -55,6 +62,7 @@ import "../../../components/ha-icon-overflow-menu";
|
|||
import "../../../components/ha-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import {
|
||||
CategoryRegistryEntry,
|
||||
createCategoryRegistryEntry,
|
||||
|
@ -92,15 +100,12 @@ import { haStyle } from "../../../resources/styles";
|
|||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
|
||||
import { showNewAutomationDialog } from "../automation/show-dialog-new-automation";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../../common/util/promise-all-settled-results";
|
||||
|
||||
type ScriptItem = ScriptEntity & {
|
||||
name: string;
|
||||
|
@ -148,6 +153,19 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
|||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityReg!: EntityRegistryEntry[];
|
||||
|
||||
@storage({ key: "script-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "script-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({
|
||||
key: "script-table-collapsed",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
private _sizeController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width,
|
||||
});
|
||||
|
@ -403,6 +421,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
|||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||
</div>
|
||||
</ha-menu-item>`;
|
||||
|
||||
const labelItems = html`${this._labels?.map((label) => {
|
||||
const color = label.color ? computeCssColor(label.color) : undefined;
|
||||
const selected = this._selected.every((entityId) =>
|
||||
|
@ -439,9 +458,46 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
|||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</div></ha-menu-item
|
||||
>`;
|
||||
const labelsInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 700) ||
|
||||
|
||||
const areaItems = html`${Object.values(this.hass.areas).map(
|
||||
(area) =>
|
||||
html`<ha-menu-item
|
||||
.value=${area.area_id}
|
||||
@click=${this._handleBulkArea}
|
||||
>
|
||||
${area.icon
|
||||
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
<div slot="headline">${area.name}</div>
|
||||
</ha-menu-item>`
|
||||
)}
|
||||
<ha-menu-item .value=${null} @click=${this._handleBulkArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.no_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-menu-item>
|
||||
<md-divider role="separator" tabindex="-1"></md-divider>
|
||||
<ha-menu-item @click=${this._bulkCreateArea}>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.add_area"
|
||||
)}
|
||||
</div>
|
||||
</ha-menu-item>`;
|
||||
|
||||
const areasInOverflow =
|
||||
(this._sizeController.value && this._sizeController.value < 900) ||
|
||||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
|
||||
|
||||
const labelsInOverflow =
|
||||
areasInOverflow &&
|
||||
(!this._sizeController.value || this._sizeController.value < 700);
|
||||
|
||||
const scripts = this._scripts(
|
||||
this.scripts,
|
||||
this._entityReg,
|
||||
|
@ -462,7 +518,12 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
|||
{ number: scripts.length }
|
||||
)}
|
||||
hasFilters
|
||||
initialGroupColumn="category"
|
||||
.initialGroupColumn=${this._activeGrouping || "category"}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
|
@ -588,9 +649,25 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
|||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${labelItems}
|
||||
</ha-button-menu-new>`}
|
||||
${areasInOverflow
|
||||
? nothing
|
||||
: html`<ha-button-menu-new slot="selection-bar">
|
||||
<ha-assist-chip
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${areaItems}
|
||||
</ha-button-menu-new>`}`
|
||||
: nothing}
|
||||
${this.narrow || labelsInOverflow
|
||||
${this.narrow || areasInOverflow
|
||||
? html`
|
||||
<ha-button-menu-new has-overflow slot="selection-bar">
|
||||
${
|
||||
|
@ -636,8 +713,8 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
|||
: nothing
|
||||
}
|
||||
${
|
||||
this.narrow || this.hass.dockedSidebar === "docked"
|
||||
? html` <ha-sub-menu>
|
||||
this.narrow || labelsInOverflow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
|
@ -653,6 +730,24 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
|||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.narrow || areasInOverflow
|
||||
? html`<ha-sub-menu>
|
||||
<ha-menu-item slot="item">
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.devices.picker.bulk_actions.move_area"
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-menu-item>
|
||||
<ha-menu slot="menu">${areaItems}</ha-menu>
|
||||
</ha-sub-menu>`
|
||||
: nothing
|
||||
}
|
||||
</ha-button-menu-new>`
|
||||
: nothing}
|
||||
${!this.scripts.length
|
||||
|
@ -1091,6 +1186,58 @@ ${rejected
|
|||
});
|
||||
}
|
||||
|
||||
private async _handleBulkArea(ev) {
|
||||
const area = ev.currentTarget.value;
|
||||
this._bulkAddArea(area);
|
||||
}
|
||||
|
||||
private async _bulkAddArea(area: string) {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
promises.push(
|
||||
updateEntityRegistryEntry(this.hass, entityId, {
|
||||
area_id: area,
|
||||
})
|
||||
);
|
||||
});
|
||||
const result = await Promise.allSettled(promises);
|
||||
if (hasRejectedItems(result)) {
|
||||
const rejected = rejectedItems(result);
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.common.multiselect.failed", {
|
||||
number: rejected.length,
|
||||
}),
|
||||
text: html`<pre>
|
||||
${rejected
|
||||
.map((r) => r.reason.message || r.reason.code || r.reason)
|
||||
.join("\r\n")}</pre
|
||||
>`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _bulkCreateArea() {
|
||||
showAreaRegistryDetailDialog(this, {
|
||||
createEntry: async (values) => {
|
||||
const area = await createAreaRegistryEntry(this.hass, values);
|
||||
this._bulkAddArea(area.area_id);
|
||||
return area;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
this._activeGrouping = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCollapseChanged(ev: CustomEvent) {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
|
|
@ -122,7 +122,7 @@ export class HaManualScriptEditor extends LitElement {
|
|||
<ha-automation-action
|
||||
role="region"
|
||||
aria-labelledby="sequence-heading"
|
||||
.actions=${this.config.sequence}
|
||||
.actions=${this.config.sequence || []}
|
||||
.path=${["sequence"]}
|
||||
@value-changed=${this._sequenceChanged}
|
||||
@item-moved=${this._itemMoved}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { LocalizeFunc } from "../../../common/translations/localize";
|
|||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/data-table/ha-data-table-icon";
|
||||
import "../../../components/ha-fab";
|
||||
|
@ -25,6 +26,7 @@ import { HomeAssistant, Route } from "../../../types";
|
|||
import { configSections } from "../ha-panel-config";
|
||||
import { showAddUserDialog } from "./show-dialog-add-user";
|
||||
import { showUserDetailDialog } from "./show-dialog-user-detail";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
|
||||
@customElement("ha-config-users")
|
||||
export class HaConfigUsers extends LitElement {
|
||||
|
@ -38,6 +40,19 @@ export class HaConfigUsers extends LitElement {
|
|||
|
||||
@state() private _users: User[] = [];
|
||||
|
||||
@storage({ key: "users-table-sort", state: false, subscribe: false })
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@storage({ key: "users-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({
|
||||
key: "users-table-collapsed",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => {
|
||||
const columns: DataTableColumnContainer<User> = {
|
||||
|
@ -70,16 +85,14 @@ export class HaConfigUsers extends LitElement {
|
|||
hidden: narrow,
|
||||
template: (user) => html`${user.username || "—"}`,
|
||||
},
|
||||
group_ids: {
|
||||
group: {
|
||||
title: localize("ui.panel.config.users.picker.headers.group"),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
groupable: true,
|
||||
width: "20%",
|
||||
direction: "asc",
|
||||
hidden: narrow,
|
||||
template: (user) => html`
|
||||
${localize(`groups.${user.group_ids[0]}`)}
|
||||
`,
|
||||
},
|
||||
is_active: {
|
||||
title: this.hass.localize(
|
||||
|
@ -164,7 +177,13 @@ export class HaConfigUsers extends LitElement {
|
|||
backPath="/config"
|
||||
.tabs=${configSections.persons}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.data=${this._users}
|
||||
.data=${this._userData(this._users, this.hass.localize)}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
.initialCollapsedGroups=${this._activeCollapsed}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
@row-click=${this._editUser}
|
||||
hasFab
|
||||
clickable
|
||||
|
@ -181,6 +200,13 @@ export class HaConfigUsers extends LitElement {
|
|||
`;
|
||||
}
|
||||
|
||||
private _userData = memoizeOne((users: User[], localize: LocalizeFunc) =>
|
||||
users.map((user) => ({
|
||||
...user,
|
||||
group: localize(`groups.${user.group_ids[0]}`),
|
||||
}))
|
||||
);
|
||||
|
||||
private async _fetchUsers() {
|
||||
this._users = await fetchUsers(this.hass);
|
||||
|
||||
|
@ -245,6 +271,18 @@ export class HaConfigUsers extends LitElement {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleGroupingChanged(ev: CustomEvent) {
|
||||
this._activeGrouping = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleCollapseChanged(ev: CustomEvent) {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
DataTableRowData,
|
||||
RowClickedEvent,
|
||||
SelectionChangedEvent,
|
||||
SortingChangedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-fab";
|
||||
import { AlexaEntity, fetchCloudAlexaEntities } from "../../../data/alexa";
|
||||
|
@ -52,6 +53,7 @@ import "./expose/expose-assistant-icon";
|
|||
import { voiceAssistantTabs } from "./ha-config-voice-assistants";
|
||||
import { showExposeEntityDialog } from "./show-dialog-expose-entity";
|
||||
import { showVoiceSettingsDialog } from "./show-dialog-voice-settings";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
|
||||
@customElement("ha-config-voice-assistants-expose")
|
||||
export class VoiceAssistantsExpose extends LitElement {
|
||||
|
@ -87,6 +89,13 @@ export class VoiceAssistantsExpose extends LitElement {
|
|||
string[] | undefined
|
||||
>;
|
||||
|
||||
@storage({
|
||||
key: "voice-expose-table-sort",
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@query("hass-tabs-subpage-data-table", true)
|
||||
private _dataTable!: HaTabsSubpageDataTable;
|
||||
|
||||
|
@ -505,6 +514,8 @@ export class VoiceAssistantsExpose extends LitElement {
|
|||
selectable
|
||||
.selected=${this._selectedEntities.length}
|
||||
clickable
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
@clear-filter=${this._clearFilter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
|
@ -696,6 +707,10 @@ export class VoiceAssistantsExpose extends LitElement {
|
|||
navigate(window.location.pathname, { replace: true });
|
||||
}
|
||||
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
|
|
@ -421,6 +421,7 @@ class HaPanelHistory extends LitElement {
|
|||
[],
|
||||
this.hass.localize,
|
||||
sensorNumericDeviceClasses,
|
||||
true,
|
||||
true
|
||||
);
|
||||
// remap states array to statistics array
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
import { mdiLock, mdiLockOpen } from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
|
||||
import "../../../components/ha-control-button";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import {
|
||||
callProtectedLockService,
|
||||
isAvailable,
|
||||
isLocking,
|
||||
isUnlocking,
|
||||
isLocked,
|
||||
} from "../../../data/lock";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCardFeature } from "../types";
|
||||
import { LockCommandsCardFeatureConfig } from "./types";
|
||||
import { forwardHaptic } from "../../../data/haptics";
|
||||
|
||||
export const supportsLockCommandsCardFeature = (stateObj: HassEntity) => {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return domain === "lock";
|
||||
};
|
||||
|
||||
@customElement("hui-lock-commands-card-feature")
|
||||
class HuiLockCommandsCardFeature
|
||||
extends LitElement
|
||||
implements LovelaceCardFeature
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@state() private _config?: LockCommandsCardFeatureConfig;
|
||||
|
||||
static getStubConfig(): LockCommandsCardFeatureConfig {
|
||||
return {
|
||||
type: "lock-commands",
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: LockCommandsCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _onTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
const service = ev.target.dataset.service;
|
||||
if (!this.hass || !this.stateObj || !service) {
|
||||
return;
|
||||
}
|
||||
forwardHaptic("light");
|
||||
callProtectedLockService(this, this.hass, this.stateObj, service);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.stateObj ||
|
||||
!supportsLockCommandsCardFeature(this.stateObj)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-control-button-group>
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize("ui.card.lock.lock")}
|
||||
.disabled=${!isAvailable(this.stateObj) || isLocked(this.stateObj)}
|
||||
@click=${this._onTap}
|
||||
data-service="lock"
|
||||
class=${classMap({
|
||||
pulse: isLocking(this.stateObj) || isUnlocking(this.stateObj),
|
||||
})}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiLock}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize("ui.card.lock.unlock")}
|
||||
.disabled=${!isAvailable(this.stateObj) || !isLocked(this.stateObj)}
|
||||
@click=${this._onTap}
|
||||
data-service="unlock"
|
||||
class=${classMap({
|
||||
pulse: isLocking(this.stateObj) || isUnlocking(this.stateObj),
|
||||
})}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiLockOpen}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
</ha-control-button-group>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.pulse {
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
ha-control-button-group {
|
||||
margin: 0 12px 12px 12px;
|
||||
--control-button-group-spacing: 12px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-lock-commands-card-feature": HuiLockCommandsCardFeature;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
import { mdiCheck } from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-control-button";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import {
|
||||
LockEntityFeature,
|
||||
callProtectedLockService,
|
||||
isAvailable,
|
||||
} from "../../../data/lock";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCardFeature } from "../types";
|
||||
import { LockOpenDoorCardFeatureConfig } from "./types";
|
||||
|
||||
export const supportsLockOpenDoorCardFeature = (stateObj: HassEntity) => {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return domain === "lock" && supportsFeature(stateObj, LockEntityFeature.OPEN);
|
||||
};
|
||||
|
||||
const CONFIRM_TIMEOUT_SECOND = 5;
|
||||
const OPENED_TIMEOUT_SECOND = 3;
|
||||
|
||||
type ButtonState = "normal" | "confirm" | "success";
|
||||
|
||||
@customElement("hui-lock-open-door-card-feature")
|
||||
class HuiLockOpenDoorCardFeature
|
||||
extends LitElement
|
||||
implements LovelaceCardFeature
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: HassEntity;
|
||||
|
||||
@state() public _buttonState: ButtonState = "normal";
|
||||
|
||||
@state() private _config?: LockOpenDoorCardFeatureConfig;
|
||||
|
||||
private _buttonTimeout?: number;
|
||||
|
||||
static getStubConfig(): LockOpenDoorCardFeatureConfig {
|
||||
return {
|
||||
type: "lock-open-door",
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: LockOpenDoorCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _setButtonState(buttonState: ButtonState, timeoutSecond?: number) {
|
||||
clearTimeout(this._buttonTimeout);
|
||||
this._buttonState = buttonState;
|
||||
if (timeoutSecond) {
|
||||
this._buttonTimeout = window.setTimeout(() => {
|
||||
this._buttonState = "normal";
|
||||
}, timeoutSecond * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
private async _open() {
|
||||
if (this._buttonState !== "confirm") {
|
||||
this._setButtonState("confirm", CONFIRM_TIMEOUT_SECOND);
|
||||
return;
|
||||
}
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return;
|
||||
}
|
||||
callProtectedLockService(this, this.hass, this.stateObj!, "open");
|
||||
|
||||
this._setButtonState("success", OPENED_TIMEOUT_SECOND);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.stateObj ||
|
||||
!supportsLockOpenDoorCardFeature(this.stateObj)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this._buttonState === "success"
|
||||
? html`
|
||||
<p class="open-success">
|
||||
<ha-svg-icon path=${mdiCheck}></ha-svg-icon>
|
||||
${this.hass.localize("ui.card.lock.open_door_success")}
|
||||
</p>
|
||||
`
|
||||
: html`
|
||||
<ha-control-button-group>
|
||||
<ha-control-button
|
||||
.disabled=${!isAvailable(this.stateObj)}
|
||||
class="open-button ${this._buttonState}"
|
||||
@click=${this._open}
|
||||
>
|
||||
${this._buttonState === "confirm"
|
||||
? this.hass.localize("ui.card.lock.open_door_confirm")
|
||||
: this.hass.localize("ui.card.lock.open_door")}
|
||||
</ha-control-button>
|
||||
</ha-control-button-group>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-control-button {
|
||||
font-size: 14px;
|
||||
}
|
||||
ha-control-button-group {
|
||||
margin: 0 12px 12px 12px;
|
||||
--control-button-group-spacing: 12px;
|
||||
}
|
||||
.open-button {
|
||||
width: 130px;
|
||||
}
|
||||
.open-button.confirm {
|
||||
--control-button-background-color: var(--warning-color);
|
||||
}
|
||||
.open-success {
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
color: var(--success-color);
|
||||
margin: 0 12px 12px 12px;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
ha-control-button-group + ha-attributes:not([empty]) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-lock-open-door-card-feature": HuiLockOpenDoorCardFeature;
|
||||
}
|
||||
}
|
|
@ -26,6 +26,14 @@ export interface LightColorTempCardFeatureConfig {
|
|||
type: "light-color-temp";
|
||||
}
|
||||
|
||||
export interface LockCommandsCardFeatureConfig {
|
||||
type: "lock-commands";
|
||||
}
|
||||
|
||||
export interface LockOpenDoorCardFeatureConfig {
|
||||
type: "lock-open-door";
|
||||
}
|
||||
|
||||
export interface FanPresetModesCardFeatureConfig {
|
||||
type: "fan-preset-modes";
|
||||
style?: "dropdown" | "icons";
|
||||
|
@ -143,6 +151,8 @@ export type LovelaceCardFeatureConfig =
|
|||
| LawnMowerCommandsCardFeatureConfig
|
||||
| LightBrightnessCardFeatureConfig
|
||||
| LightColorTempCardFeatureConfig
|
||||
| LockCommandsCardFeatureConfig
|
||||
| LockOpenDoorCardFeatureConfig
|
||||
| NumericInputCardFeatureConfig
|
||||
| SelectOptionsCardFeatureConfig
|
||||
| TargetHumidityCardFeatureConfig
|
||||
|
|
|
@ -138,7 +138,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||
includeDomains
|
||||
);
|
||||
|
||||
return { type: "map", entities: foundEntities };
|
||||
return { type: "map", entities: foundEntities, theme_mode: "auto" };
|
||||
}
|
||||
|
||||
protected render() {
|
||||
|
@ -151,6 +151,17 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||
(${this._error.code})
|
||||
</ha-alert>`;
|
||||
}
|
||||
|
||||
const isDarkMode =
|
||||
this._config.dark_mode || this._config.theme_mode === "dark"
|
||||
? true
|
||||
: this._config.theme_mode === "light"
|
||||
? false
|
||||
: this.hass.themes.darkMode;
|
||||
|
||||
const themeMode =
|
||||
this._config.theme_mode || (this._config.dark_mode ? "dark" : "auto");
|
||||
|
||||
return html`
|
||||
<ha-card id="card" .header=${this._config.title}>
|
||||
<div id="root">
|
||||
|
@ -161,7 +172,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||
.paths=${this._getHistoryPaths(this._config, this._stateHistory)}
|
||||
.autoFit=${this._config.auto_fit || false}
|
||||
.fitZones=${this._config.fit_zones}
|
||||
?darkMode=${this._config.dark_mode}
|
||||
.themeMode=${themeMode}
|
||||
interactiveZones
|
||||
renderPassive
|
||||
></ha-map>
|
||||
|
@ -170,6 +181,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||
"ui.panel.lovelace.cards.map.reset_focus"
|
||||
)}
|
||||
.path=${mdiImageFilterCenterFocus}
|
||||
style=${isDarkMode ? "color:#ffffff" : "color:#000000"}
|
||||
@click=${this._fitMap}
|
||||
tabindex="0"
|
||||
></ha-icon-button>
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ActionConfig } from "../../../data/lovelace/config/action";
|
|||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { Statistic, StatisticType } from "../../../data/recorder";
|
||||
import { ForecastType } from "../../../data/weather";
|
||||
import { FullCalendarView, TranslationDict } from "../../../types";
|
||||
import { FullCalendarView, ThemeMode, TranslationDict } from "../../../types";
|
||||
import { LovelaceCardFeatureConfig } from "../card-features/types";
|
||||
import { LegacyStateFilter } from "../common/evaluate-filter";
|
||||
import { Condition, LegacyCondition } from "../common/validate-condition";
|
||||
|
@ -314,6 +314,7 @@ export interface MapCardConfig extends LovelaceCardConfig {
|
|||
hours_to_show?: number;
|
||||
geo_location_sources?: string[];
|
||||
dark_mode?: boolean;
|
||||
theme_mode?: ThemeMode;
|
||||
}
|
||||
|
||||
export interface MarkdownCardConfig extends LovelaceCardConfig {
|
||||
|
|
|
@ -14,6 +14,8 @@ import "../card-features/hui-humidifier-toggle-card-feature";
|
|||
import "../card-features/hui-lawn-mower-commands-card-feature";
|
||||
import "../card-features/hui-light-brightness-card-feature";
|
||||
import "../card-features/hui-light-color-temp-card-feature";
|
||||
import "../card-features/hui-lock-commands-card-feature";
|
||||
import "../card-features/hui-lock-open-door-card-feature";
|
||||
import "../card-features/hui-numeric-input-card-feature";
|
||||
import "../card-features/hui-select-options-card-feature";
|
||||
import "../card-features/hui-target-temperature-card-feature";
|
||||
|
@ -45,6 +47,8 @@ const TYPES: Set<LovelaceCardFeatureConfig["type"]> = new Set([
|
|||
"lawn-mower-commands",
|
||||
"light-brightness",
|
||||
"light-color-temp",
|
||||
"lock-commands",
|
||||
"lock-open-door",
|
||||
"numeric-input",
|
||||
"select-options",
|
||||
"target-humidity",
|
||||
|
|
|
@ -35,6 +35,8 @@ import { supportsHumidifierToggleCardFeature } from "../../card-features/hui-hum
|
|||
import { supportsLawnMowerCommandCardFeature } from "../../card-features/hui-lawn-mower-commands-card-feature";
|
||||
import { supportsLightBrightnessCardFeature } from "../../card-features/hui-light-brightness-card-feature";
|
||||
import { supportsLightColorTempCardFeature } from "../../card-features/hui-light-color-temp-card-feature";
|
||||
import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature";
|
||||
import { supportsLockOpenDoorCardFeature } from "../../card-features/hui-lock-open-door-card-feature";
|
||||
import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature";
|
||||
import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature";
|
||||
import { supportsTargetHumidityCardFeature } from "../../card-features/hui-target-humidity-card-feature";
|
||||
|
@ -56,8 +58,8 @@ const UI_FEATURE_TYPES = [
|
|||
"climate-preset-modes",
|
||||
"cover-open-close",
|
||||
"cover-position",
|
||||
"cover-tilt-position",
|
||||
"cover-tilt",
|
||||
"cover-tilt-position",
|
||||
"fan-preset-modes",
|
||||
"fan-speed",
|
||||
"humidifier-modes",
|
||||
|
@ -65,6 +67,8 @@ const UI_FEATURE_TYPES = [
|
|||
"lawn-mower-commands",
|
||||
"light-brightness",
|
||||
"light-color-temp",
|
||||
"lock-commands",
|
||||
"lock-open-door",
|
||||
"numeric-input",
|
||||
"select-options",
|
||||
"target-humidity",
|
||||
|
@ -111,6 +115,8 @@ const SUPPORTS_FEATURE_TYPES: Record<
|
|||
"lawn-mower-commands": supportsLawnMowerCommandCardFeature,
|
||||
"light-brightness": supportsLightBrightnessCardFeature,
|
||||
"light-color-temp": supportsLightColorTempCardFeature,
|
||||
"lock-commands": supportsLockCommandsCardFeature,
|
||||
"lock-open-door": supportsLockOpenDoorCardFeature,
|
||||
"numeric-input": supportsNumericInputCardFeature,
|
||||
"select-options": supportsSelectOptionsCardFeature,
|
||||
"target-humidity": supportsTargetHumidityCardFeature,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { mdiPalette } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import {
|
||||
|
@ -11,6 +12,7 @@ import {
|
|||
string,
|
||||
union,
|
||||
} from "superstruct";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { hasLocation } from "../../../../common/entity/has_location";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
|
@ -28,6 +30,7 @@ import { processEditorEntities } from "../process-editor-entities";
|
|||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { EntitiesEditorEvent } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
|
||||
export const mapEntitiesConfigStruct = union([
|
||||
object({
|
||||
|
@ -50,30 +53,11 @@ const cardConfigStruct = assign(
|
|||
hours_to_show: optional(number()),
|
||||
geo_location_sources: optional(array(string())),
|
||||
auto_fit: optional(boolean()),
|
||||
theme_mode: optional(string()),
|
||||
})
|
||||
);
|
||||
|
||||
const SCHEMA = [
|
||||
{ name: "title", selector: { text: {} } },
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{ name: "aspect_ratio", selector: { text: {} } },
|
||||
{
|
||||
name: "default_zoom",
|
||||
default: DEFAULT_ZOOM,
|
||||
selector: { number: { mode: "box", min: 0 } },
|
||||
},
|
||||
{ name: "dark_mode", selector: { boolean: {} } },
|
||||
{
|
||||
name: "hours_to_show",
|
||||
default: DEFAULT_HOURS_TO_SHOW,
|
||||
selector: { number: { mode: "box", min: 0 } },
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
const themeModes = ["auto", "light", "dark"] as const;
|
||||
|
||||
@customElement("hui-map-card-editor")
|
||||
export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
|
||||
|
@ -83,8 +67,68 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
|
|||
|
||||
@state() private _configEntities?: EntityConfig[];
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
{ name: "title", selector: { text: {} } },
|
||||
{
|
||||
name: "",
|
||||
type: "expandable",
|
||||
iconPath: mdiPalette,
|
||||
title: localize(`ui.panel.lovelace.editor.card.map.appearance`),
|
||||
schema: [
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
schema: [
|
||||
{ name: "aspect_ratio", selector: { text: {} } },
|
||||
{
|
||||
name: "default_zoom",
|
||||
default: DEFAULT_ZOOM,
|
||||
selector: { number: { mode: "box", min: 0 } },
|
||||
},
|
||||
{
|
||||
name: "theme_mode",
|
||||
default: "auto",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: themeModes.map((themeMode) => ({
|
||||
value: themeMode,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.map.theme_modes.${themeMode}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "hours_to_show",
|
||||
default: DEFAULT_HOURS_TO_SHOW,
|
||||
selector: { number: { mode: "box", min: 0 } },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const
|
||||
);
|
||||
|
||||
public setConfig(config: MapCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
|
||||
// Migrate legacy dark_mode to theme_mode
|
||||
if (!this._config && !("theme_mode" in config)) {
|
||||
config = { ...config };
|
||||
if (config.dark_mode) {
|
||||
config.theme_mode = "dark";
|
||||
} else {
|
||||
config.theme_mode = "auto";
|
||||
}
|
||||
delete config.dark_mode;
|
||||
fireEvent(this, "config-changed", { config: config });
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
this._configEntities = config.entities
|
||||
? processEditorEntities(config.entities)
|
||||
|
@ -104,33 +148,32 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
|
|||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${SCHEMA}
|
||||
.schema=${this._schema(this.hass.localize)}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
<div class="card-config">
|
||||
<hui-entity-editor
|
||||
.hass=${this.hass}
|
||||
.entities=${this._configEntities}
|
||||
.entityFilter=${hasLocation}
|
||||
@entities-changed=${this._entitiesValueChanged}
|
||||
></hui-entity-editor>
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.map.geo_location_sources"
|
||||
)}
|
||||
</h3>
|
||||
<div class="geo_location_sources">
|
||||
<hui-input-list-editor
|
||||
.inputLabel=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.map.source"
|
||||
)}
|
||||
.hass=${this.hass}
|
||||
.value=${this._geo_location_sources}
|
||||
@value-changed=${this._geoSourcesChanged}
|
||||
></hui-input-list-editor>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hui-entity-editor
|
||||
.hass=${this.hass}
|
||||
.entities=${this._configEntities}
|
||||
.entityFilter=${hasLocation}
|
||||
@entities-changed=${this._entitiesValueChanged}
|
||||
></hui-entity-editor>
|
||||
|
||||
<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.map.geo_location_sources"
|
||||
)}
|
||||
</h3>
|
||||
|
||||
<hui-input-list-editor
|
||||
.inputLabel=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.map.source"
|
||||
)}
|
||||
.hass=${this.hass}
|
||||
.value=${this._geo_location_sources}
|
||||
@value-changed=${this._geoSourcesChanged}
|
||||
></hui-input-list-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -170,9 +213,14 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
|
|||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "dark_mode":
|
||||
case "theme_mode":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.map.${schema.name}`
|
||||
);
|
||||
case "default_zoom":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.map.${schema.name}`
|
||||
|
@ -185,16 +233,7 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
|
|||
};
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
configElementStyle,
|
||||
css`
|
||||
.geo_location_sources {
|
||||
padding-left: 20px;
|
||||
padding-inline-start: 20px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
`,
|
||||
];
|
||||
return [configElementStyle, css``];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -102,6 +102,7 @@ export const derivedStyles = {
|
|||
"mdc-theme-error": "var(--error-color)",
|
||||
"app-header-text-color": "var(--text-primary-color)",
|
||||
"app-header-background-color": "var(--primary-color)",
|
||||
"app-theme-color": "var(--primary-color)",
|
||||
"mdc-checkbox-unchecked-color": "rgba(var(--rgb-primary-text-color), 0.54)",
|
||||
"mdc-checkbox-disabled-color": "var(--disabled-text-color)",
|
||||
"mdc-radio-unchecked-color": "rgba(var(--rgb-primary-text-color), 0.54)",
|
||||
|
|
|
@ -41,6 +41,7 @@ export class HaStateControlCoverPosition extends LitElement {
|
|||
|
||||
return html`
|
||||
<ha-control-slider
|
||||
touch-action="none"
|
||||
vertical
|
||||
.value=${this.value}
|
||||
min="0"
|
||||
|
|
|
@ -78,6 +78,7 @@ export class HaStateControlInfoCoverTiltPosition extends LitElement {
|
|||
|
||||
return html`
|
||||
<ha-control-slider
|
||||
touch-action="none"
|
||||
vertical
|
||||
.value=${this.value}
|
||||
min="0"
|
||||
|
|
|
@ -106,6 +106,7 @@ export class HaStateControlCoverToggle extends LitElement {
|
|||
|
||||
return html`
|
||||
<ha-control-switch
|
||||
touch-action="none"
|
||||
vertical
|
||||
reversed
|
||||
.checked=${isOn}
|
||||
|
|
|
@ -111,6 +111,7 @@ export class HaStateControlFanSpeed extends LitElement {
|
|||
|
||||
return html`
|
||||
<ha-control-slider
|
||||
touch-action="none"
|
||||
vertical
|
||||
min="0"
|
||||
max="100"
|
||||
|
|
|
@ -108,6 +108,7 @@ export class HaStateControlToggle extends LitElement {
|
|||
|
||||
return html`
|
||||
<ha-control-switch
|
||||
touch-action="none"
|
||||
.pathOn=${this.iconPathOn || mdiFlash}
|
||||
.pathOff=${this.iconPathOff || mdiFlashOff}
|
||||
vertical
|
||||
|
|
|
@ -59,6 +59,7 @@ export class HaStateControlLightBrightness extends LitElement {
|
|||
|
||||
return html`
|
||||
<ha-control-slider
|
||||
touch-action="none"
|
||||
vertical
|
||||
.value=${this.value}
|
||||
min="1"
|
||||
|
|
|
@ -118,6 +118,7 @@ export class HaStateControlLockToggle extends LitElement {
|
|||
|
||||
return html`
|
||||
<ha-control-switch
|
||||
touch-action="none"
|
||||
vertical
|
||||
reversed
|
||||
.checked=${this._isOn}
|
||||
|
|
|
@ -40,6 +40,7 @@ export class HaStateControlValvePosition extends LitElement {
|
|||
|
||||
return html`
|
||||
<ha-control-slider
|
||||
touch-action="none"
|
||||
vertical
|
||||
.value=${this.value}
|
||||
min="0"
|
||||
|
|
|
@ -106,6 +106,7 @@ export class HaStateControlValveToggle extends LitElement {
|
|||
|
||||
return html`
|
||||
<ha-control-switch
|
||||
touch-action="none"
|
||||
vertical
|
||||
reversed
|
||||
.checked=${isOn}
|
||||
|
|
|
@ -83,7 +83,8 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
|||
service,
|
||||
serviceData,
|
||||
target,
|
||||
notifyOnError = true
|
||||
notifyOnError = true,
|
||||
returnResponse = false
|
||||
) => {
|
||||
if (__DEV__ || this.hass?.debugConnection) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
@ -101,7 +102,8 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
|||
domain,
|
||||
service,
|
||||
serviceData ?? {},
|
||||
target
|
||||
target,
|
||||
returnResponse
|
||||
)) as ServiceCallResponse;
|
||||
} catch (err: any) {
|
||||
if (
|
||||
|
|
|
@ -130,9 +130,8 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
|||
|
||||
const themeMeta = document.querySelector("meta[name=theme-color]");
|
||||
const computedStyles = getComputedStyle(document.documentElement);
|
||||
const headerColor = computedStyles.getPropertyValue(
|
||||
"--app-header-background-color"
|
||||
);
|
||||
const themeMetaColor =
|
||||
computedStyles.getPropertyValue("--app-theme-color");
|
||||
|
||||
document.documentElement.style.backgroundColor =
|
||||
computedStyles.getPropertyValue("--primary-background-color");
|
||||
|
@ -145,7 +144,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
|||
);
|
||||
}
|
||||
const themeColor =
|
||||
headerColor?.trim() ||
|
||||
themeMetaColor?.trim() ||
|
||||
(themeMeta.getAttribute("default-content") as string);
|
||||
themeMeta.setAttribute("content", themeColor);
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
|||
// eslint-disable-next-line: variable-name
|
||||
private __coreProgress?: string;
|
||||
|
||||
private __loadedFragmetTranslations: Set<string> = new Set();
|
||||
private __loadedFragmentTranslations: Set<string> = new Set();
|
||||
|
||||
private __loadedTranslations: {
|
||||
// track what things have been loaded
|
||||
|
@ -262,7 +262,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
|||
document.querySelector("html")!.setAttribute("lang", hass.language);
|
||||
this._applyDirection(hass);
|
||||
this._loadCoreTranslations(hass.language);
|
||||
this.__loadedFragmetTranslations = new Set();
|
||||
this.__loadedFragmentTranslations = new Set();
|
||||
this._loadFragmentTranslations(hass.language, hass.panelUrl);
|
||||
}
|
||||
|
||||
|
@ -385,12 +385,12 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
|||
return undefined;
|
||||
}
|
||||
|
||||
if (this.__loadedFragmetTranslations.has(fragment)) {
|
||||
if (this.__loadedFragmentTranslations.has(fragment)) {
|
||||
return this.hass!.localize;
|
||||
}
|
||||
this.__loadedFragmetTranslations.add(fragment);
|
||||
this.__loadedFragmentTranslations.add(fragment);
|
||||
const result = await getTranslation(fragment, language);
|
||||
return this._updateResources(result.language, result.data);
|
||||
return this._updateResources(language, result.data);
|
||||
}
|
||||
|
||||
private async _loadCoreTranslations(language: string) {
|
||||
|
@ -402,7 +402,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
|||
this.__coreProgress = language;
|
||||
try {
|
||||
const result = await getTranslation(null, language);
|
||||
await this._updateResources(result.language, result.data);
|
||||
await this._updateResources(language, result.data);
|
||||
} finally {
|
||||
this.__coreProgress = undefined;
|
||||
}
|
||||
|
|
|
@ -783,7 +783,8 @@
|
|||
"no-data": "No data",
|
||||
"filtering_by": "Filtering by",
|
||||
"hidden": "{number} hidden",
|
||||
"clear": "Clear"
|
||||
"clear": "Clear",
|
||||
"ungrouped": "Ungrouped"
|
||||
},
|
||||
"media-browser": {
|
||||
"tts": {
|
||||
|
@ -2949,7 +2950,7 @@
|
|||
"event": "[%key:ui::panel::config::automation::editor::triggers::type::homeassistant::event%]",
|
||||
"sunrise": "Sunrise",
|
||||
"sunset": "Sunset",
|
||||
"offset": "Offset (optional)",
|
||||
"offset": "Offset in seconds or HH:MM:SS (optional)",
|
||||
"description": {
|
||||
"picker": "When the sun sets or rises.",
|
||||
"sets": "When the sun sets{hasDuration, select, \n true { offset by {duration}} \n other {}\n }",
|
||||
|
@ -3829,25 +3830,42 @@
|
|||
}
|
||||
},
|
||||
"remote": {
|
||||
"title": "Remote control",
|
||||
"title": "Remote access",
|
||||
"connected": "Connected",
|
||||
"not_connected": "Not connected",
|
||||
"reconnecting": "Not connected. Trying to reconnect.",
|
||||
"access_is_being_prepared": "Remote control is being prepared. We will notify you when it's ready.",
|
||||
"access_is_being_prepared": "Remote access is being prepared. We will notify you when it's ready.",
|
||||
"cerificate_loading": "Your certificate is loading.",
|
||||
"cerificate_loaded": "Your certificate is loaded, waiting for validation.",
|
||||
"cerificate_error": "There was an error generating the certificate, check your logs.",
|
||||
"info": "Home Assistant Cloud provides a secure remote connection to your instance while away from home.",
|
||||
"instance_is_available": "Your instance is available at your",
|
||||
"instance_will_be_available": "Your instance will be available at your",
|
||||
"info": "Home Assistant Cloud provides a secure remote access to your instance while away from home. For more information on remote access and these settings visit our security documentation.",
|
||||
"info_instance_will_be_available": "Your instance will be available at your Nabu Casa URL.",
|
||||
"link_learn_how_it_works": "Learn how it works",
|
||||
"nabu_casa_url": "Nabu Casa URL",
|
||||
"advanced_options": "Advanced options",
|
||||
"external_activation": "Allow external activation of remote control",
|
||||
"external_activation_secondary": "Allows you to turn on remote control from your Nabu Casa account page, even if you're outside your local network",
|
||||
"certificate_info": "Certificate info",
|
||||
"certificate_expire": "Will be renewed at {date}",
|
||||
"more_info": "More info"
|
||||
"show_url": "Show full URL",
|
||||
"hide_url": "Hide URL",
|
||||
"copy_link": "Copy link",
|
||||
"security_options": "Security options",
|
||||
"strict_connection": "Remote login access",
|
||||
"strict_connection_secondary": "Choose what happens when new devices visit your remote access link.",
|
||||
"strict_connection_option_disabled": "Show login page",
|
||||
"strict_connection_option_disabled_secondary": "Any new device visiting your remote access link are presented with a login page.",
|
||||
"strict_connection_option_guard_page": "Block remote logins",
|
||||
"strict_connection_option_guard_page_secondary": "New devices must log in with a temporary access link. Devices accessing the link that are not logged in will be presented with a page explaining the restrictions.",
|
||||
"strict_connection_option_guard_page_warning": "This prevents outsiders from trying to log in to your system but also your own devices if they have not logged in previously.",
|
||||
"strict_connection_option_drop_connection": "Block remote logins and show nothing",
|
||||
"strict_connection_option_drop_connection_secondary": "This is the same as the above setting but instead provides a blank page for additional security.",
|
||||
"strict_connection_option_drop_connection_warning": "This prevents outsiders from snooping the remote web address and trying to log in, but it may appear as if there is no system running when users try to access it.",
|
||||
"external_activation": "Allow external activation of remote access",
|
||||
"external_activation_secondary": "If you disable remote access on this page, having this setting enabled allows you to reactivate it remotely via your Nabu Casa account.",
|
||||
"drop_connection_warning_title": "Remote log in has been deactivated",
|
||||
"drop_connection_warning": "The below security options may make it appear the system is not running.",
|
||||
"strict_connection_link": "Provide temporary login access",
|
||||
"strict_connection_link_secondary": "This provides a link for new devices to login for the next hour.",
|
||||
"strict_connection_create_link": "Create link",
|
||||
"strict_connection_link_created_message": "Give this link to the person you want to give remote access to the login page of your Home Assistant instance.",
|
||||
"certificate_info": "Certificate information",
|
||||
"certificate_expire": "Certificate renewal at {date}",
|
||||
"more_info": "More details"
|
||||
},
|
||||
"alexa": {
|
||||
"title": "Alexa",
|
||||
|
@ -4034,7 +4052,12 @@
|
|||
"confirm_delete_integration": "Are you sure you want to remove this device from {integration}?",
|
||||
"picker": {
|
||||
"search": "Search {number} devices",
|
||||
"state": "State"
|
||||
"state": "State",
|
||||
"bulk_actions": {
|
||||
"move_area": "Move to area",
|
||||
"no_area": "No area",
|
||||
"add_area": "Add area"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entities": {
|
||||
|
@ -4061,7 +4084,8 @@
|
|||
"integration": "Integration",
|
||||
"area": "Area",
|
||||
"disabled_by": "Disabled by",
|
||||
"status": "Status"
|
||||
"status": "Status",
|
||||
"domain": "Domain"
|
||||
},
|
||||
"selected": "{number} selected",
|
||||
"enable_selected": {
|
||||
|
@ -5824,7 +5848,14 @@
|
|||
"name": "Map",
|
||||
"geo_location_sources": "Geolocation sources",
|
||||
"dark_mode": "Dark mode?",
|
||||
"default_zoom": "Default zoom",
|
||||
"appearance": "Appearance",
|
||||
"theme_mode": "Theme Mode",
|
||||
"theme_modes": {
|
||||
"auto": "Auto",
|
||||
"light": "Light",
|
||||
"dark": "Dark"
|
||||
},
|
||||
"default_zoom": "Default Zoom",
|
||||
"source": "Source",
|
||||
"description": "The Map card that allows you to display entities on a map."
|
||||
},
|
||||
|
@ -5950,6 +5981,12 @@
|
|||
"light-color-temp": {
|
||||
"label": "Light color temperature"
|
||||
},
|
||||
"lock-commands": {
|
||||
"label": "Lock commands"
|
||||
},
|
||||
"lock-open-door": {
|
||||
"label": "Lock open door"
|
||||
},
|
||||
"vacuum-commands": {
|
||||
"label": "Vacuum commands",
|
||||
"commands": "Commands",
|
||||
|
|
|
@ -139,6 +139,8 @@ export type FullCalendarView =
|
|||
| "dayGridDay"
|
||||
| "listWeek";
|
||||
|
||||
export type ThemeMode = "auto" | "light" | "dark";
|
||||
|
||||
export interface ToggleButton {
|
||||
label: string;
|
||||
iconPath?: string;
|
||||
|
@ -190,6 +192,7 @@ export interface Context {
|
|||
|
||||
export interface ServiceCallResponse {
|
||||
context: Context;
|
||||
response?: any;
|
||||
}
|
||||
|
||||
export interface ServiceCallRequest {
|
||||
|
@ -241,7 +244,8 @@ export interface HomeAssistant {
|
|||
service: ServiceCallRequest["service"],
|
||||
serviceData?: ServiceCallRequest["serviceData"],
|
||||
target?: ServiceCallRequest["target"],
|
||||
notifyOnError?: boolean
|
||||
notifyOnError?: boolean,
|
||||
returnResponse?: boolean
|
||||
): Promise<ServiceCallResponse>;
|
||||
callApi<T>(
|
||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||
|
|
Loading…
Reference in New Issue