mirror of
https://github.com/home-assistant/frontend.git
synced 2025-10-08 19:29:53 +00:00
Compare commits
2 Commits
fix-menu-o
...
refactor_l
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c632204939 | ||
![]() |
52fdbfe44c |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Build resources
|
||||
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
|
||||
- name: Setup lint cache
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.0.1
|
||||
with:
|
||||
path: |
|
||||
node_modules/.cache/prettier
|
||||
|
@@ -72,8 +72,6 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
id: "sensor.co2_intensity",
|
||||
name: null,
|
||||
icon: null,
|
||||
labels: [],
|
||||
categories: {},
|
||||
platform: "co2signal",
|
||||
hidden_by: null,
|
||||
entity_category: null,
|
||||
@@ -90,8 +88,6 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
id: "sensor.co2_intensity",
|
||||
name: null,
|
||||
icon: null,
|
||||
labels: [],
|
||||
categories: {},
|
||||
platform: "co2signal",
|
||||
hidden_by: null,
|
||||
entity_category: null,
|
||||
|
@@ -4,11 +4,4 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
export const mockAreaRegistry = (
|
||||
hass: MockHomeAssistant,
|
||||
data: AreaRegistryEntry[] = []
|
||||
) => {
|
||||
hass.mockWS("config/area_registry/list", () => data);
|
||||
const areas = {};
|
||||
data.forEach((area) => {
|
||||
areas[area.area_id] = area;
|
||||
});
|
||||
hass.updateHass({ areas });
|
||||
};
|
||||
) => hass.mockWS("config/area_registry/list", () => data);
|
||||
|
@@ -4,11 +4,4 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
export const mockDeviceRegistry = (
|
||||
hass: MockHomeAssistant,
|
||||
data: DeviceRegistryEntry[] = []
|
||||
) => {
|
||||
hass.mockWS("config/device_registry/list", () => data);
|
||||
const devices = {};
|
||||
data.forEach((device) => {
|
||||
devices[device.id] = device;
|
||||
});
|
||||
hass.updateHass({ devices });
|
||||
};
|
||||
) => hass.mockWS("config/device_registry/list", () => data);
|
||||
|
@@ -1,7 +0,0 @@
|
||||
import { FloorRegistryEntry } from "../../../src/data/floor_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockFloorRegistry = (
|
||||
hass: MockHomeAssistant,
|
||||
data: FloorRegistryEntry[] = []
|
||||
) => hass.mockWS("config/floor_registry/list", () => data);
|
@@ -1,7 +0,0 @@
|
||||
import { LabelRegistryEntry } from "../../../src/data/label_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockLabelRegistry = (
|
||||
hass: MockHomeAssistant,
|
||||
data: LabelRegistryEntry[] = []
|
||||
) => hass.mockWS("config/label_registry/list", () => data);
|
@@ -162,7 +162,7 @@ export class DemoHaBarButton extends LitElement {
|
||||
}
|
||||
.custom-group {
|
||||
--control-button-group-thickness: 100px;
|
||||
--control-button-group-border-radius: 36px;
|
||||
--control-button-group-border-radius: 18px;
|
||||
--control-button-group-spacing: 20px;
|
||||
}
|
||||
.custom-group ha-control-button {
|
||||
|
@@ -94,7 +94,7 @@ export class DemoHarControlNumberButtons extends LitElement {
|
||||
--control-number-buttons-background-color: #2196f3;
|
||||
--control-number-buttons-background-opacity: 0.1;
|
||||
--control-number-buttons-thickness: 100px;
|
||||
--control-number-buttons-border-radius: 36px;
|
||||
--control-number-buttons-border-radius: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -186,8 +186,8 @@ export class DemoHaControlSelect extends LitElement {
|
||||
.custom {
|
||||
--mdc-icon-size: 24px;
|
||||
--control-select-color: var(--state-fan-active-color);
|
||||
--control-select-thickness: 130px;
|
||||
--control-select-border-radius: 48px;
|
||||
--control-select-thickness: 100px;
|
||||
--control-select-border-radius: 24px;
|
||||
}
|
||||
.vertical-selects {
|
||||
height: 300px;
|
||||
|
@@ -150,8 +150,8 @@ export class DemoHaBarSlider extends LitElement {
|
||||
--control-slider-color: #ffcf4c;
|
||||
--control-slider-background: #ffcf4c;
|
||||
--control-slider-background-opacity: 0.2;
|
||||
--control-slider-thickness: 130px;
|
||||
--control-slider-border-radius: 48px;
|
||||
--control-slider-thickness: 100px;
|
||||
--control-slider-border-radius: 24px;
|
||||
}
|
||||
.vertical-sliders {
|
||||
height: 300px;
|
||||
|
@@ -117,8 +117,8 @@ export class DemoHaControlSwitch extends LitElement {
|
||||
.custom {
|
||||
--control-switch-on-color: var(--green-color);
|
||||
--control-switch-off-color: var(--red-color);
|
||||
--control-switch-thickness: 130px;
|
||||
--control-switch-border-radius: 48px;
|
||||
--control-switch-thickness: 100px;
|
||||
--control-switch-border-radius: 24px;
|
||||
--control-switch-padding: 6px;
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
|
@@ -59,7 +59,6 @@ const DEVICES = [
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: "backyard",
|
||||
@@ -78,7 +77,6 @@ const DEVICES = [
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: null,
|
||||
@@ -97,37 +95,30 @@ const DEVICES = [
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
},
|
||||
];
|
||||
|
||||
const AREAS: AreaRegistryEntry[] = [
|
||||
{
|
||||
area_id: "backyard",
|
||||
floor_id: null,
|
||||
name: "Backyard",
|
||||
icon: null,
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: "bedroom",
|
||||
floor_id: null,
|
||||
name: "Bedroom",
|
||||
icon: "mdi:bed",
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: "livingroom",
|
||||
floor_id: null,
|
||||
name: "Livingroom",
|
||||
icon: "mdi:sofa",
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
@@ -17,10 +17,6 @@ import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import "../../components/demo-black-white-row";
|
||||
import { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
||||
import { LabelRegistryEntry } from "../../../../src/data/label_registry";
|
||||
import { mockFloorRegistry } from "../../../../demo/src/stubs/floor_registry";
|
||||
import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("alarm_control_panel", "alarm", "disarmed", {
|
||||
@@ -59,7 +55,6 @@ const DEVICES = [
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: "backyard",
|
||||
@@ -78,7 +73,6 @@ const DEVICES = [
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: null,
|
||||
@@ -97,76 +91,30 @@ const DEVICES = [
|
||||
hw_version: null,
|
||||
via_device_id: null,
|
||||
serial_number: null,
|
||||
labels: [],
|
||||
},
|
||||
];
|
||||
|
||||
const AREAS: AreaRegistryEntry[] = [
|
||||
{
|
||||
area_id: "backyard",
|
||||
floor_id: "ground",
|
||||
name: "Backyard",
|
||||
icon: null,
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: "bedroom",
|
||||
floor_id: "first",
|
||||
name: "Bedroom",
|
||||
icon: "mdi:bed",
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
},
|
||||
{
|
||||
area_id: "livingroom",
|
||||
floor_id: "ground",
|
||||
name: "Livingroom",
|
||||
icon: "mdi:sofa",
|
||||
picture: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
},
|
||||
];
|
||||
|
||||
const FLOORS: FloorRegistryEntry[] = [
|
||||
{
|
||||
floor_id: "ground",
|
||||
name: "Ground floor",
|
||||
level: 0,
|
||||
icon: null,
|
||||
aliases: [],
|
||||
},
|
||||
{
|
||||
floor_id: "first",
|
||||
name: "First floor",
|
||||
level: 1,
|
||||
icon: "mdi:numeric-1",
|
||||
aliases: [],
|
||||
},
|
||||
{
|
||||
floor_id: "second",
|
||||
name: "Second floor",
|
||||
level: 2,
|
||||
icon: "mdi:numeric-2",
|
||||
aliases: [],
|
||||
},
|
||||
];
|
||||
|
||||
const LABELS: LabelRegistryEntry[] = [
|
||||
{
|
||||
label_id: "energy",
|
||||
name: "Energy",
|
||||
icon: null,
|
||||
color: "yellow",
|
||||
},
|
||||
{
|
||||
label_id: "entertainment",
|
||||
name: "Entertainment",
|
||||
icon: "mdi:popcorn",
|
||||
color: "blue",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -177,12 +125,7 @@ const SCHEMAS: {
|
||||
{
|
||||
name: "One of each",
|
||||
input: {
|
||||
label: { name: "Label", selector: { label: {} } },
|
||||
floor: { name: "Floor", selector: { floor: {} } },
|
||||
area: { name: "Area", selector: { area: {} } },
|
||||
device: { name: "Device", selector: { device: {} } },
|
||||
entity: { name: "Entity", selector: { entity: {} } },
|
||||
target: { name: "Target", selector: { target: {} } },
|
||||
state: {
|
||||
name: "State",
|
||||
selector: { state: { entity_id: "alarm_control_panel.alarm" } },
|
||||
@@ -191,12 +134,15 @@ const SCHEMAS: {
|
||||
name: "Attribute",
|
||||
selector: { attribute: { entity_id: "" } },
|
||||
},
|
||||
device: { name: "Device", selector: { device: {} } },
|
||||
config_entry: {
|
||||
name: "Integration",
|
||||
selector: { config_entry: {} },
|
||||
},
|
||||
duration: { name: "Duration", selector: { duration: {} } },
|
||||
addon: { name: "Addon", selector: { addon: {} } },
|
||||
area: { name: "Area", selector: { area: {} } },
|
||||
target: { name: "Target", selector: { target: {} } },
|
||||
number_box: {
|
||||
name: "Number Box",
|
||||
selector: {
|
||||
@@ -345,8 +291,6 @@ const SCHEMAS: {
|
||||
entity: { name: "Entity", selector: { entity: { multiple: true } } },
|
||||
device: { name: "Device", selector: { device: { multiple: true } } },
|
||||
area: { name: "Area", selector: { area: { multiple: true } } },
|
||||
floor: { name: "Floor", selector: { floor: { multiple: true } } },
|
||||
label: { name: "Label", selector: { label: { multiple: true } } },
|
||||
select: {
|
||||
name: "Select Multiple",
|
||||
selector: {
|
||||
@@ -403,8 +347,6 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
mockDeviceRegistry(hass, DEVICES);
|
||||
mockConfigEntries(hass);
|
||||
mockAreaRegistry(hass, AREAS);
|
||||
mockFloorRegistry(hass, FLOORS);
|
||||
mockLabelRegistry(hass, LABELS);
|
||||
mockHassioSupervisor(hass);
|
||||
hass.mockWS("auth/sign_path", (params) => params);
|
||||
hass.mockWS("media_player/browse_media", this._browseMedia);
|
||||
|
@@ -36,45 +36,6 @@ const ENTITIES = [
|
||||
friendly_name: "Nest",
|
||||
supported_features: 43,
|
||||
}),
|
||||
getEntity("climate", "overkiz_radiator", "heat", {
|
||||
current_temperature: 18,
|
||||
min_temp: 7,
|
||||
max_temp: 35,
|
||||
temperature: 20,
|
||||
hvac_modes: ["heat", "auto", "off"],
|
||||
friendly_name: "Overkiz radiator",
|
||||
supported_features: 17,
|
||||
preset_mode: "comfort",
|
||||
preset_modes: [
|
||||
"none",
|
||||
"frost_protection",
|
||||
"eco",
|
||||
"comfort",
|
||||
"comfort-1",
|
||||
"comfort-2",
|
||||
"auto",
|
||||
"boost",
|
||||
"external",
|
||||
"prog",
|
||||
],
|
||||
}),
|
||||
getEntity("climate", "overkiz_towel_dryer", "heat", {
|
||||
current_temperature: null,
|
||||
min_temp: 7,
|
||||
max_temp: 35,
|
||||
hvac_modes: ["heat", "off"],
|
||||
friendly_name: "Overkiz towel dryer",
|
||||
supported_features: 16,
|
||||
preset_mode: "eco",
|
||||
preset_modes: [
|
||||
"none",
|
||||
"frost_protection",
|
||||
"eco",
|
||||
"comfort",
|
||||
"comfort-1",
|
||||
"comfort-2",
|
||||
],
|
||||
}),
|
||||
getEntity("climate", "sensibo", "fan_only", {
|
||||
current_temperature: null,
|
||||
temperature: null,
|
||||
@@ -100,6 +61,8 @@ const CONFIGS = [
|
||||
config: `
|
||||
- type: thermostat
|
||||
entity: climate.ecobee
|
||||
- type: thermostat
|
||||
entity: climate.nest
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -109,66 +72,6 @@ const CONFIGS = [
|
||||
entity: climate.nest
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Feature example",
|
||||
config: `
|
||||
- type: thermostat
|
||||
entity: climate.overkiz_radiator
|
||||
features:
|
||||
- type: climate-hvac-modes
|
||||
hvac_modes:
|
||||
- heat
|
||||
- 'off'
|
||||
- auto
|
||||
- type: climate-preset-modes
|
||||
style: icons
|
||||
preset_modes:
|
||||
- none
|
||||
- frost_protection
|
||||
- eco
|
||||
- comfort
|
||||
- comfort-1
|
||||
- comfort-2
|
||||
- auto
|
||||
- boost
|
||||
- external
|
||||
- prog
|
||||
- type: climate-preset-modes
|
||||
style: dropdown
|
||||
preset_modes:
|
||||
- none
|
||||
- frost_protection
|
||||
- eco
|
||||
- comfort
|
||||
- comfort-1
|
||||
- comfort-2
|
||||
- auto
|
||||
- boost
|
||||
- external
|
||||
- prog
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Preset only example",
|
||||
config: `
|
||||
- type: thermostat
|
||||
entity: climate.overkiz_towel_dryer
|
||||
features:
|
||||
- type: climate-hvac-modes
|
||||
hvac_modes:
|
||||
- heat
|
||||
- 'off'
|
||||
- type: climate-preset-modes
|
||||
style: icons
|
||||
preset_modes:
|
||||
- none
|
||||
- frost_protection
|
||||
- eco
|
||||
- comfort
|
||||
- comfort-1
|
||||
- comfort-2
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Fan only example",
|
||||
config: `
|
||||
|
@@ -406,7 +406,6 @@ export class DemoEntityState extends LitElement {
|
||||
entity_id: "select.speed",
|
||||
translation_key: "speed",
|
||||
platform: "demo",
|
||||
labels: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@@ -199,8 +199,6 @@ const createEntityRegistryEntries = (
|
||||
has_entity_name: false,
|
||||
unique_id: "updater",
|
||||
options: null,
|
||||
labels: [],
|
||||
categories: {},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -224,7 +222,6 @@ const createDeviceRegistryEntries = (
|
||||
name_by_user: null,
|
||||
disabled_by: null,
|
||||
configuration_url: null,
|
||||
labels: [],
|
||||
},
|
||||
];
|
||||
|
||||
|
@@ -11,7 +11,7 @@ import "../../components/demo-more-infos";
|
||||
import { ClimateEntityFeature } from "../../../../src/data/climate";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("climate", "radiator", "heat", {
|
||||
getEntity("climate", "thermostat", "heat", {
|
||||
friendly_name: "Basic heater",
|
||||
hvac_modes: ["heat", "off"],
|
||||
hvac_mode: "heat",
|
||||
@@ -80,24 +80,6 @@ const ENTITIES = [
|
||||
max_humidity: 100,
|
||||
humidity: 50,
|
||||
}),
|
||||
getEntity("climate", "towel_dryer", "heat", {
|
||||
friendly_name: "Preset only heater",
|
||||
hvac_modes: ["heat", "off"],
|
||||
hvac_mode: "heat",
|
||||
preset_modes: [
|
||||
"none",
|
||||
"frost_protection",
|
||||
"eco",
|
||||
"comfort",
|
||||
"comfort-1",
|
||||
"comfort-2",
|
||||
],
|
||||
preset_mode: "eco",
|
||||
current_temperature: null,
|
||||
min_temp: 7,
|
||||
max_temp: 35,
|
||||
supported_features: ClimateEntityFeature.PRESET_MODE,
|
||||
}),
|
||||
getEntity("climate", "unavailable", "unavailable", {
|
||||
friendly_name: "Unavailable heater",
|
||||
hvac_modes: ["heat", "off"],
|
||||
|
46
package.json
46
package.json
@@ -25,22 +25,22 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.24.1",
|
||||
"@braintree/sanitize-url": "7.0.1",
|
||||
"@babel/runtime": "7.24.0",
|
||||
"@braintree/sanitize-url": "7.0.0",
|
||||
"@codemirror/autocomplete": "6.15.0",
|
||||
"@codemirror/commands": "6.3.3",
|
||||
"@codemirror/language": "6.10.1",
|
||||
"@codemirror/legacy-modes": "6.3.3",
|
||||
"@codemirror/search": "6.5.6",
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@codemirror/view": "6.26.1",
|
||||
"@codemirror/view": "6.26.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.12.3",
|
||||
"@formatjs/intl-datetimeformat": "6.12.2",
|
||||
"@formatjs/intl-displaynames": "6.6.6",
|
||||
"@formatjs/intl-getcanonicallocales": "2.3.0",
|
||||
"@formatjs/intl-listformat": "7.5.5",
|
||||
"@formatjs/intl-locale": "3.4.5",
|
||||
"@formatjs/intl-numberformat": "8.10.1",
|
||||
"@formatjs/intl-numberformat": "8.10.0",
|
||||
"@formatjs/intl-pluralrules": "5.2.12",
|
||||
"@formatjs/intl-relativetimeformat": "11.2.12",
|
||||
"@fullcalendar/core": "6.1.11",
|
||||
@@ -89,8 +89,8 @@
|
||||
"@polymer/paper-tabs": "3.1.0",
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.3.10",
|
||||
"@vaadin/vaadin-themable-mixin": "24.3.10",
|
||||
"@vaadin/combo-box": "24.3.9",
|
||||
"@vaadin/vaadin-themable-mixin": "24.3.9",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@@ -101,7 +101,7 @@
|
||||
"chart.js": "4.4.2",
|
||||
"color-name": "2.0.0",
|
||||
"comlink": "4.4.1",
|
||||
"core-js": "3.36.1",
|
||||
"core-js": "3.36.0",
|
||||
"cropperjs": "1.6.1",
|
||||
"date-fns": "2.30.0",
|
||||
"date-fns-tz": "2.0.1",
|
||||
@@ -111,7 +111,7 @@
|
||||
"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.1.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.5.11",
|
||||
"js-yaml": "4.1.0",
|
||||
@@ -150,15 +150,15 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.24.3",
|
||||
"@babel/core": "7.24.0",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.1",
|
||||
"@babel/plugin-proposal-decorators": "7.24.1",
|
||||
"@babel/plugin-transform-runtime": "7.24.3",
|
||||
"@babel/preset-env": "7.24.3",
|
||||
"@babel/preset-typescript": "7.24.1",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.12.2",
|
||||
"@babel/plugin-proposal-decorators": "7.24.0",
|
||||
"@babel/plugin-transform-runtime": "7.24.0",
|
||||
"@babel/preset-env": "7.24.0",
|
||||
"@babel/preset-typescript": "7.23.3",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.12.1",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@lokalise/node-api": "12.3.0",
|
||||
"@lokalise/node-api": "12.2.1",
|
||||
"@octokit/auth-oauth-device": "7.0.1",
|
||||
"@octokit/plugin-retry": "7.0.3",
|
||||
"@octokit/rest": "20.0.2",
|
||||
@@ -185,8 +185,8 @@
|
||||
"@types/tar": "6.1.11",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "7.4.0",
|
||||
"@typescript-eslint/parser": "7.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.2.0",
|
||||
"@typescript-eslint/parser": "7.2.0",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@web/dev-server-rollup": "0.4.1",
|
||||
"babel-loader": "9.1.3",
|
||||
@@ -235,16 +235,16 @@
|
||||
"sinon": "17.0.1",
|
||||
"source-map-url": "0.4.1",
|
||||
"systemjs": "6.14.3",
|
||||
"tar": "6.2.1",
|
||||
"tar": "6.2.0",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"transform-async-modules-webpack-plugin": "1.0.4",
|
||||
"transform-async-modules-webpack-plugin": "1.0.2",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.4.3",
|
||||
"typescript": "5.4.2",
|
||||
"vinyl-buffer": "1.0.1",
|
||||
"vinyl-source-stream": "2.0.0",
|
||||
"webpack": "5.91.0",
|
||||
"webpack": "5.90.3",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.0.4",
|
||||
"webpack-dev-server": "5.0.3",
|
||||
"webpack-manifest-plugin": "5.0.0",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "6.0.1",
|
||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 7.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 19 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.6 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 6.2 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 52 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 69 KiB |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20240402.0"
|
||||
version = "20240228.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -231,7 +231,6 @@ export const SENSOR_ENTITIES = [
|
||||
"calendar",
|
||||
"camera",
|
||||
"device_tracker",
|
||||
"image",
|
||||
"weather",
|
||||
];
|
||||
|
||||
|
@@ -37,20 +37,3 @@ export const calcDateProperty = (
|
||||
locale.time_zone === TimeZone.server
|
||||
? (calcZonedDate(date, config.time_zone, fn, options) as number | boolean)
|
||||
: fn(date, options);
|
||||
|
||||
export const calcDateDifferenceProperty = (
|
||||
endDate: Date,
|
||||
startDate: Date,
|
||||
fn: (date: Date, options?: any) => boolean | number,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) =>
|
||||
calcDateProperty(
|
||||
endDate,
|
||||
fn,
|
||||
locale,
|
||||
config,
|
||||
locale.time_zone === TimeZone.server
|
||||
? utcToZonedTime(startDate, config.time_zone)
|
||||
: startDate
|
||||
);
|
||||
|
@@ -4,24 +4,22 @@ import { css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-assist-chip")
|
||||
// @ts-ignore
|
||||
export class HaAssistChip extends MdAssistChip {
|
||||
@property({ type: Boolean, reflect: true }) filled = false;
|
||||
|
||||
@property({ type: Boolean }) active = false;
|
||||
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-assist-chip-container-shape: var(
|
||||
--ha-assist-chip-container-shape,
|
||||
16px
|
||||
);
|
||||
--md-assist-chip-container-shape: 16px;
|
||||
--md-assist-chip-outline-color: var(--outline-color);
|
||||
--md-assist-chip-label-text-weight: 400;
|
||||
--ha-assist-chip-filled-container-color: rgba(
|
||||
var(--rgb-primary-text-color),
|
||||
0.15
|
||||
);
|
||||
}
|
||||
/** Material 3 doesn't have a filled chip, so we have to make our own **/
|
||||
.filled {
|
||||
@@ -33,28 +31,10 @@ export class HaAssistChip extends MdAssistChip {
|
||||
background-color: var(--ha-assist-chip-filled-container-color);
|
||||
}
|
||||
/** Set the size of mdc icons **/
|
||||
::slotted([slot="icon"]),
|
||||
::slotted([slot="trailingIcon"]) {
|
||||
::slotted([slot="icon"]) {
|
||||
display: flex;
|
||||
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||
}
|
||||
|
||||
.trailing.icon ::slotted(*),
|
||||
.trailing.icon svg {
|
||||
margin-inline-end: unset;
|
||||
margin-inline-start: var(--_icon-label-space);
|
||||
}
|
||||
::before {
|
||||
background: var(--ha-assist-chip-container-color);
|
||||
opacity: var(--ha-assist-chip-container-opacity);
|
||||
}
|
||||
:where(.active)::before {
|
||||
background: var(--ha-assist-chip-active-container-color);
|
||||
opacity: var(--ha-assist-chip-active-container-opacity);
|
||||
}
|
||||
.label {
|
||||
font-family: Roboto, sans-serif;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -65,30 +45,6 @@ export class HaAssistChip extends MdAssistChip {
|
||||
|
||||
return super.renderOutline();
|
||||
}
|
||||
|
||||
protected override getContainerClasses() {
|
||||
return {
|
||||
...super.getContainerClasses(),
|
||||
active: this.active,
|
||||
};
|
||||
}
|
||||
|
||||
protected override renderPrimaryContent() {
|
||||
return html`
|
||||
<span class="leading icon" aria-hidden="true">
|
||||
${this.renderLeadingIcon()}
|
||||
</span>
|
||||
<span class="label">${this.label}</span>
|
||||
<span class="touch"></span>
|
||||
<span class="trailing leading icon" aria-hidden="true">
|
||||
${this.renderTrailingIcon()}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
protected renderTrailingIcon() {
|
||||
return html`<slot name="trailing-icon"></slot>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -19,16 +19,12 @@ export class HaInputChip extends MdInputChip {
|
||||
var(--rgb-primary-text-color),
|
||||
0.15
|
||||
);
|
||||
--ha-input-chip-selected-container-opacity: 1;
|
||||
}
|
||||
/** Set the size of mdc icons **/
|
||||
::slotted([slot="icon"]) {
|
||||
display: flex;
|
||||
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||
}
|
||||
.selected::before {
|
||||
opacity: var(--ha-input-chip-selected-container-opacity);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -1,130 +0,0 @@
|
||||
import { css, html, LitElement, nothing, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { LabelRegistryEntry } from "../../data/label_registry";
|
||||
import { computeCssColor } from "../../common/color/compute-color";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../ha-label";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
|
||||
@customElement("ha-data-table-labels")
|
||||
class HaDataTableLabels extends LitElement {
|
||||
@property({ attribute: false }) public labels!: LabelRegistryEntry[];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const labels = this.labels.sort((a, b) => stringCompare(a.name, b.name));
|
||||
return html`
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
labels.slice(0, 2),
|
||||
(label) => label.label_id,
|
||||
(label) => this._renderLabel(label, true)
|
||||
)}
|
||||
${labels.length > 2
|
||||
? html`<ha-button-menu
|
||||
absolute
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click=${this._handleIconOverflowMenuOpened}
|
||||
@closed=${this._handleIconOverflowMenuClosed}
|
||||
>
|
||||
<ha-label slot="trigger" class="plus" dense>
|
||||
+${labels.length - 2}
|
||||
</ha-label>
|
||||
${repeat(
|
||||
labels.slice(2),
|
||||
(label) => label.label_id,
|
||||
(label) => html`
|
||||
<ha-list-item @click=${this._labelClicked} .item=${label}>
|
||||
${this._renderLabel(label, false)}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-button-menu>`
|
||||
: nothing}
|
||||
</ha-chip-set>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderLabel(label: LabelRegistryEntry, clickAction: boolean) {
|
||||
const color = label?.color ? computeCssColor(label.color) : undefined;
|
||||
return html`
|
||||
<ha-label
|
||||
dense
|
||||
role="button"
|
||||
tabindex="0"
|
||||
.item=${label}
|
||||
@click=${clickAction ? this._labelClicked : undefined}
|
||||
@keydown=${clickAction ? this._labelClicked : undefined}
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
>
|
||||
${label?.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
`;
|
||||
}
|
||||
|
||||
private _labelClicked(ev) {
|
||||
ev.stopPropagation();
|
||||
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
|
||||
return;
|
||||
}
|
||||
const label = (ev.currentTarget as any).item as LabelRegistryEntry;
|
||||
fireEvent(this, "label-clicked", { label });
|
||||
}
|
||||
|
||||
protected _handleIconOverflowMenuOpened(e) {
|
||||
e.stopPropagation();
|
||||
// If this component is used inside a data table, the z-index of the row
|
||||
// needs to be increased. Otherwise the ha-button-menu would be displayed
|
||||
// underneath the next row in the table.
|
||||
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
|
||||
if (row) {
|
||||
row.style.zIndex = "1";
|
||||
}
|
||||
}
|
||||
|
||||
protected _handleIconOverflowMenuClosed() {
|
||||
const row = this.closest(".mdc-data-table__row") as HTMLDivElement | null;
|
||||
if (row) {
|
||||
row.style.zIndex = "";
|
||||
}
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
margin-top: 4px;
|
||||
height: 22px;
|
||||
}
|
||||
ha-chip-set {
|
||||
position: fixed;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
ha-label {
|
||||
--ha-label-background-color: var(--color, var(--grey-color));
|
||||
--ha-label-background-opacity: 0.5;
|
||||
}
|
||||
ha-button-menu {
|
||||
border-radius: 10px;
|
||||
}
|
||||
.plus {
|
||||
--ha-label-background-color: transparent;
|
||||
border: 1px solid var(--divider-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-data-table-labels": HaDataTableLabels;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"label-clicked": { label: LabelRegistryEntry };
|
||||
}
|
||||
}
|
@@ -32,7 +32,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";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -68,20 +67,13 @@ export interface DataTableSortColumnData {
|
||||
filterKey?: string;
|
||||
valueColumn?: string;
|
||||
direction?: SortingDirection;
|
||||
groupable?: boolean;
|
||||
}
|
||||
|
||||
export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
|
||||
main?: boolean;
|
||||
title: TemplateResult | string;
|
||||
label?: TemplateResult | string;
|
||||
type?:
|
||||
| "numeric"
|
||||
| "icon"
|
||||
| "icon-button"
|
||||
| "overflow"
|
||||
| "overflow-menu"
|
||||
| "flex";
|
||||
type?: "numeric" | "icon" | "icon-button" | "overflow-menu" | "flex";
|
||||
template?: (row: T) => TemplateResult | string | typeof nothing;
|
||||
width?: string;
|
||||
maxWidth?: string;
|
||||
@@ -103,8 +95,6 @@ export interface SortableColumnContainer {
|
||||
[key: string]: ClonedDataTableColumnData;
|
||||
}
|
||||
|
||||
const UNDEFINED_GROUP_KEY = "zzzzz_undefined";
|
||||
|
||||
@customElement("ha-data-table")
|
||||
export class HaDataTable extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -139,16 +129,14 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@property({ type: String }) public filter = "";
|
||||
|
||||
@property() public groupColumn?: string;
|
||||
|
||||
@property() public sortColumn?: string;
|
||||
|
||||
@property() public sortDirection: SortingDirection = null;
|
||||
|
||||
@state() private _filterable = false;
|
||||
|
||||
@state() private _filter = "";
|
||||
|
||||
@state() private _sortColumn?: string;
|
||||
|
||||
@state() private _sortDirection: SortingDirection = null;
|
||||
|
||||
@state() private _filteredData: DataTableRowData[] = [];
|
||||
|
||||
@state() private _headerHeight = 0;
|
||||
@@ -181,13 +169,6 @@ export class HaDataTable extends LitElement {
|
||||
this._checkedRowsChanged();
|
||||
}
|
||||
|
||||
public selectAll(): void {
|
||||
this._checkedRows = this._filteredData
|
||||
.filter((data) => data.selectable !== false)
|
||||
.map((data) => data[this.id]);
|
||||
this._checkedRowsChanged();
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this._items.length) {
|
||||
@@ -214,14 +195,8 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
this._sortDirection = this.columns[columnId].direction!;
|
||||
this._sortColumn = columnId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -251,16 +226,11 @@ export class HaDataTable extends LitElement {
|
||||
properties.has("data") ||
|
||||
properties.has("columns") ||
|
||||
properties.has("_filter") ||
|
||||
properties.has("sortColumn") ||
|
||||
properties.has("sortDirection") ||
|
||||
properties.has("groupColumn")
|
||||
properties.has("_sortColumn") ||
|
||||
properties.has("_sortDirection")
|
||||
) {
|
||||
this._sortFilterData();
|
||||
}
|
||||
|
||||
if (properties.has("selectable")) {
|
||||
this._items = [...this._items];
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -293,79 +263,75 @@ export class HaDataTable extends LitElement {
|
||||
})}
|
||||
>
|
||||
<div class="mdc-data-table__header-row" role="row" aria-rowindex="1">
|
||||
<slot name="header-row">
|
||||
${this.selectable
|
||||
? html`
|
||||
<div
|
||||
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
|
||||
role="columnheader"
|
||||
>
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleHeaderRowCheckboxClick}
|
||||
.indeterminate=${this._checkedRows.length &&
|
||||
this._checkedRows.length !== this._checkableRowsCount}
|
||||
.checked=${this._checkedRows.length &&
|
||||
this._checkedRows.length === this._checkableRowsCount}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${Object.entries(this.columns).map(([key, column]) => {
|
||||
if (column.hidden) {
|
||||
return "";
|
||||
}
|
||||
const sorted = key === this.sortColumn;
|
||||
const classes = {
|
||||
"mdc-data-table__header-cell--numeric":
|
||||
column.type === "numeric",
|
||||
"mdc-data-table__header-cell--icon": column.type === "icon",
|
||||
"mdc-data-table__header-cell--icon-button":
|
||||
column.type === "icon-button",
|
||||
"mdc-data-table__header-cell--overflow-menu":
|
||||
column.type === "overflow-menu",
|
||||
"mdc-data-table__header-cell--overflow":
|
||||
column.type === "overflow",
|
||||
sortable: Boolean(column.sortable),
|
||||
"not-sorted": Boolean(column.sortable && !sorted),
|
||||
grows: Boolean(column.grows),
|
||||
};
|
||||
return html`
|
||||
${this.selectable
|
||||
? html`
|
||||
<div
|
||||
aria-label=${ifDefined(column.label)}
|
||||
class="mdc-data-table__header-cell ${classMap(classes)}"
|
||||
style=${column.width
|
||||
? styleMap({
|
||||
[column.grows ? "minWidth" : "width"]: column.width,
|
||||
maxWidth: column.maxWidth || "",
|
||||
})
|
||||
: ""}
|
||||
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
|
||||
role="columnheader"
|
||||
aria-sort=${ifDefined(
|
||||
sorted
|
||||
? this.sortDirection === "desc"
|
||||
? "descending"
|
||||
: "ascending"
|
||||
: undefined
|
||||
)}
|
||||
@click=${this._handleHeaderClick}
|
||||
.columnId=${key}
|
||||
>
|
||||
${column.sortable
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${sorted && this.sortDirection === "desc"
|
||||
? mdiArrowDown
|
||||
: mdiArrowUp}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
<span>${column.title}</span>
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleHeaderRowCheckboxClick}
|
||||
.indeterminate=${this._checkedRows.length &&
|
||||
this._checkedRows.length !== this._checkableRowsCount}
|
||||
.checked=${this._checkedRows.length &&
|
||||
this._checkedRows.length === this._checkableRowsCount}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</slot>
|
||||
`
|
||||
: ""}
|
||||
${Object.entries(this.columns).map(([key, column]) => {
|
||||
if (column.hidden) {
|
||||
return "";
|
||||
}
|
||||
const sorted = key === this._sortColumn;
|
||||
const classes = {
|
||||
"mdc-data-table__header-cell--numeric":
|
||||
column.type === "numeric",
|
||||
"mdc-data-table__header-cell--icon": column.type === "icon",
|
||||
"mdc-data-table__header-cell--icon-button":
|
||||
column.type === "icon-button",
|
||||
"mdc-data-table__header-cell--overflow-menu":
|
||||
column.type === "overflow-menu",
|
||||
sortable: Boolean(column.sortable),
|
||||
"not-sorted": Boolean(column.sortable && !sorted),
|
||||
grows: Boolean(column.grows),
|
||||
};
|
||||
return html`
|
||||
<div
|
||||
aria-label=${ifDefined(column.label)}
|
||||
class="mdc-data-table__header-cell ${classMap(classes)}"
|
||||
style=${column.width
|
||||
? styleMap({
|
||||
[column.grows ? "minWidth" : "width"]: column.width,
|
||||
maxWidth: column.maxWidth || "",
|
||||
})
|
||||
: ""}
|
||||
role="columnheader"
|
||||
aria-sort=${ifDefined(
|
||||
sorted
|
||||
? this._sortDirection === "desc"
|
||||
? "descending"
|
||||
: "ascending"
|
||||
: undefined
|
||||
)}
|
||||
@click=${this._handleHeaderClick}
|
||||
.columnId=${key}
|
||||
>
|
||||
${column.sortable
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${sorted && this._sortDirection === "desc"
|
||||
? mdiArrowDown
|
||||
: mdiArrowUp}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
<span>${column.title}</span>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
${!this._filteredData.length
|
||||
? html`
|
||||
@@ -393,7 +359,7 @@ export class HaDataTable extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _keyFunction = (row: DataTableRowData) => row?.[this.id] || row;
|
||||
private _keyFunction = (row: DataTableRowData) => row[this.id] || row;
|
||||
|
||||
private _renderRow = (row: DataTableRowData, index: number) => {
|
||||
// not sure how this happens...
|
||||
@@ -442,7 +408,7 @@ export class HaDataTable extends LitElement {
|
||||
: ""}
|
||||
${Object.entries(this.columns).map(([key, column]) => {
|
||||
if (column.hidden) {
|
||||
return nothing;
|
||||
return "";
|
||||
}
|
||||
return html`
|
||||
<div
|
||||
@@ -455,7 +421,6 @@ export class HaDataTable extends LitElement {
|
||||
column.type === "icon-button",
|
||||
"mdc-data-table__cell--overflow-menu":
|
||||
column.type === "overflow-menu",
|
||||
"mdc-data-table__cell--overflow": column.type === "overflow",
|
||||
grows: Boolean(column.grows),
|
||||
forceLTR: Boolean(column.forceLTR),
|
||||
})}"
|
||||
@@ -488,12 +453,12 @@ export class HaDataTable extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
const prom = this.sortColumn
|
||||
const prom = this._sortColumn
|
||||
? sortData(
|
||||
filteredData,
|
||||
this._sortColumns[this.sortColumn],
|
||||
this.sortDirection,
|
||||
this.sortColumn,
|
||||
this._sortColumns[this._sortColumn],
|
||||
this._sortDirection,
|
||||
this._sortColumn,
|
||||
this.hass.locale.language
|
||||
)
|
||||
: filteredData;
|
||||
@@ -512,56 +477,17 @@ export class HaDataTable extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.appendRow || this.hasFab || this.groupColumn) {
|
||||
if (this.appendRow || this.hasFab) {
|
||||
const items = [...data];
|
||||
|
||||
if (this.appendRow) {
|
||||
items.push({ append: true, content: this.appendRow });
|
||||
}
|
||||
|
||||
if (this.groupColumn) {
|
||||
const grouped = groupBy(items, (item) => item[this.groupColumn!]);
|
||||
if (grouped.undefined) {
|
||||
// make sure ungrouped items are at the bottom
|
||||
grouped[UNDEFINED_GROUP_KEY] = grouped.undefined;
|
||||
delete grouped.undefined;
|
||||
}
|
||||
const sorted: {
|
||||
[key: string]: DataTableRowData[];
|
||||
} = Object.keys(grouped)
|
||||
.sort()
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = grouped[key];
|
||||
return obj;
|
||||
}, {});
|
||||
const groupedItems: DataTableRowData[] = [];
|
||||
Object.entries(sorted).forEach(([groupName, rows]) => {
|
||||
if (
|
||||
groupName !== UNDEFINED_GROUP_KEY ||
|
||||
Object.keys(sorted).length > 1
|
||||
) {
|
||||
groupedItems.push({
|
||||
append: true,
|
||||
content: html`<div
|
||||
class="mdc-data-table__cell group-header"
|
||||
role="cell"
|
||||
>
|
||||
${groupName === UNDEFINED_GROUP_KEY ? "" : groupName || ""}
|
||||
</div>`,
|
||||
});
|
||||
}
|
||||
|
||||
groupedItems.push(...rows);
|
||||
});
|
||||
|
||||
this._items = groupedItems;
|
||||
} else {
|
||||
this._items = items;
|
||||
}
|
||||
|
||||
if (this.hasFab) {
|
||||
this._items = [...this._items, { empty: true }];
|
||||
items.push({ empty: true });
|
||||
}
|
||||
this._items = items;
|
||||
} else {
|
||||
this._items = data;
|
||||
}
|
||||
@@ -581,26 +507,29 @@ export class HaDataTable extends LitElement {
|
||||
if (!this.columns[columnId].sortable) {
|
||||
return;
|
||||
}
|
||||
if (!this.sortDirection || this.sortColumn !== columnId) {
|
||||
this.sortDirection = "asc";
|
||||
} else if (this.sortDirection === "asc") {
|
||||
this.sortDirection = "desc";
|
||||
if (!this._sortDirection || this._sortColumn !== columnId) {
|
||||
this._sortDirection = "asc";
|
||||
} else if (this._sortDirection === "asc") {
|
||||
this._sortDirection = "desc";
|
||||
} else {
|
||||
this.sortDirection = null;
|
||||
this._sortDirection = null;
|
||||
}
|
||||
|
||||
this.sortColumn = this.sortDirection === null ? undefined : columnId;
|
||||
this._sortColumn = this._sortDirection === null ? undefined : columnId;
|
||||
|
||||
fireEvent(this, "sorting-changed", {
|
||||
column: columnId,
|
||||
direction: this.sortDirection,
|
||||
direction: this._sortDirection,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleHeaderRowCheckboxClick(ev: Event) {
|
||||
const checkbox = ev.target as HaCheckbox;
|
||||
if (checkbox.checked) {
|
||||
this.selectAll();
|
||||
this._checkedRows = this._filteredData
|
||||
.filter((data) => data.selectable !== false)
|
||||
.map((data) => data[this.id]);
|
||||
this._checkedRowsChanged();
|
||||
} else {
|
||||
this._checkedRows = [];
|
||||
this._checkedRowsChanged();
|
||||
@@ -623,19 +552,8 @@ export class HaDataTable extends LitElement {
|
||||
};
|
||||
|
||||
private _handleRowClick = (ev: Event) => {
|
||||
if (
|
||||
ev
|
||||
.composedPath()
|
||||
.find((el) =>
|
||||
[
|
||||
"ha-checkbox",
|
||||
"mwc-button",
|
||||
"ha-button",
|
||||
"ha-icon-button",
|
||||
"ha-assist-chip",
|
||||
].includes((el as HTMLElement).localName)
|
||||
)
|
||||
) {
|
||||
const target = ev.target as HTMLElement;
|
||||
if (["HA-CHECKBOX", "MWC-BUTTON"].includes(target.tagName)) {
|
||||
return;
|
||||
}
|
||||
const rowId = (ev.currentTarget as any).rowId;
|
||||
@@ -711,7 +629,7 @@ export class HaDataTable extends LitElement {
|
||||
.mdc-data-table__row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: var(--data-table-row-height, 52px);
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.mdc-data-table__row ~ .mdc-data-table__row {
|
||||
@@ -737,6 +655,7 @@ export class HaDataTable extends LitElement {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.mdc-data-table__header-row::-webkit-scrollbar {
|
||||
@@ -890,9 +809,7 @@ export class HaDataTable extends LitElement {
|
||||
padding-inline-start: initial;
|
||||
}
|
||||
.mdc-data-table__cell--overflow-menu,
|
||||
.mdc-data-table__cell--overflow,
|
||||
.mdc-data-table__header-cell--overflow-menu,
|
||||
.mdc-data-table__header-cell--overflow {
|
||||
.mdc-data-table__header-cell--overflow-menu {
|
||||
overflow: initial;
|
||||
}
|
||||
.mdc-data-table__cell--icon-button a {
|
||||
@@ -922,12 +839,6 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
/* custom from here */
|
||||
|
||||
.group-header {
|
||||
padding-top: 12px;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import { mdiSofa } from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { showAreaFilterDialog } from "../dialogs/area-filter/show-area-filter-dialog";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-icon-next";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
import "./ha-icon-next";
|
||||
|
||||
export type AreaFilterValue = {
|
||||
hidden?: string[];
|
||||
@@ -51,7 +51,7 @@ export class HaAreaPicker extends LitElement {
|
||||
@keydown=${this._edit}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiTextureBox}></ha-svg-icon>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiSofa}></ha-svg-icon>
|
||||
<span>${this.label}</span>
|
||||
<span slot="secondary">${description}</span>
|
||||
<ha-icon-next
|
||||
|
@@ -1,504 +0,0 @@
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { LitElement, PropertyValues, TemplateResult, html } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import {
|
||||
ScorableTextItem,
|
||||
fuzzyFilterSort,
|
||||
} from "../common/string/filter/sequence-matching";
|
||||
import { AreaRegistryEntry } from "../data/area_registry";
|
||||
import {
|
||||
DeviceEntityDisplayLookup,
|
||||
DeviceRegistryEntry,
|
||||
getDeviceEntityDisplayLookup,
|
||||
} from "../data/device_registry";
|
||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||
import {
|
||||
FloorRegistryEntry,
|
||||
getFloorAreaLookup,
|
||||
subscribeFloorRegistry,
|
||||
} from "../data/floor_registry";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-list-item";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
type ScorableAreaFloorEntry = ScorableTextItem & FloorAreaEntry;
|
||||
|
||||
interface FloorAreaEntry {
|
||||
id: string | null;
|
||||
name: string;
|
||||
icon: string | null;
|
||||
strings: string[];
|
||||
type: "floor" | "area";
|
||||
hasFloor?: boolean;
|
||||
level: number | null;
|
||||
}
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<FloorAreaEntry> = (item) =>
|
||||
html`<ha-list-item
|
||||
graphic="icon"
|
||||
style=${item.type === "area" && item.hasFloor
|
||||
? "--mdc-list-side-padding-left: 48px;"
|
||||
: ""}
|
||||
>
|
||||
${item.type === "floor"
|
||||
? html`<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>`
|
||||
: item.icon
|
||||
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
${item.name}
|
||||
</ha-list-item>`;
|
||||
|
||||
@customElement("ha-area-floor-picker")
|
||||
export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
/**
|
||||
* Show only areas with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show no areas with entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only areas with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
/**
|
||||
* List of areas to be excluded.
|
||||
* @type {Array}
|
||||
* @attr exclude-areas
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-areas" })
|
||||
public excludeAreas?: string[];
|
||||
|
||||
/**
|
||||
* List of floors to be excluded.
|
||||
* @type {Array}
|
||||
* @attr exclude-floors
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-floors" })
|
||||
public excludeFloors?: string[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property({ attribute: false })
|
||||
public entityFilter?: (entity: HassEntity) => boolean;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@state() private _floors?: FloorRegistryEntry[];
|
||||
|
||||
@state() private _opened?: boolean;
|
||||
|
||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||
|
||||
private _init = false;
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [
|
||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||
this._floors = floors;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.open();
|
||||
}
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.focus();
|
||||
}
|
||||
|
||||
private _getAreas = memoizeOne(
|
||||
(
|
||||
floors: FloorRegistryEntry[],
|
||||
areas: AreaRegistryEntry[],
|
||||
devices: DeviceRegistryEntry[],
|
||||
entities: EntityRegistryDisplayEntry[],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
deviceFilter: this["deviceFilter"],
|
||||
entityFilter: this["entityFilter"],
|
||||
excludeAreas: this["excludeAreas"],
|
||||
excludeFloors: this["excludeFloors"]
|
||||
): FloorAreaEntry[] => {
|
||||
if (!areas.length && !floors.length) {
|
||||
return [
|
||||
{
|
||||
id: "no_areas",
|
||||
type: "area",
|
||||
name: this.hass.localize("ui.components.area-picker.no_areas"),
|
||||
icon: null,
|
||||
strings: [],
|
||||
level: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||
|
||||
if (
|
||||
includeDomains ||
|
||||
excludeDomains ||
|
||||
includeDeviceClasses ||
|
||||
deviceFilter ||
|
||||
entityFilter
|
||||
) {
|
||||
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
|
||||
inputDevices = devices;
|
||||
inputEntities = entities.filter((entity) => entity.area_id);
|
||||
|
||||
if (includeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return true;
|
||||
}
|
||||
return entities.every(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (deviceFilter) {
|
||||
inputDevices = inputDevices!.filter((device) =>
|
||||
deviceFilter!(device)
|
||||
);
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter(stateObj);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter!(stateObj);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let outputAreas = areas;
|
||||
|
||||
let areaIds: string[] | undefined;
|
||||
|
||||
if (inputDevices) {
|
||||
areaIds = inputDevices
|
||||
.filter((device) => device.area_id)
|
||||
.map((device) => device.area_id!);
|
||||
}
|
||||
|
||||
if (inputEntities) {
|
||||
areaIds = (areaIds ?? []).concat(
|
||||
inputEntities
|
||||
.filter((entity) => entity.area_id)
|
||||
.map((entity) => entity.area_id!)
|
||||
);
|
||||
}
|
||||
|
||||
if (areaIds) {
|
||||
outputAreas = outputAreas.filter((area) =>
|
||||
areaIds!.includes(area.area_id)
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeAreas) {
|
||||
outputAreas = outputAreas.filter(
|
||||
(area) => !excludeAreas!.includes(area.area_id)
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeFloors) {
|
||||
outputAreas = outputAreas.filter(
|
||||
(area) => !area.floor_id || !excludeFloors!.includes(area.floor_id)
|
||||
);
|
||||
}
|
||||
|
||||
if (!outputAreas.length) {
|
||||
return [
|
||||
{
|
||||
id: "no_areas",
|
||||
type: "area",
|
||||
name: this.hass.localize("ui.components.area-picker.no_match"),
|
||||
icon: null,
|
||||
strings: [],
|
||||
level: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const floorAreaLookup = getFloorAreaLookup(outputAreas);
|
||||
const unassisgnedAreas = Object.values(outputAreas).filter(
|
||||
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
const floorAreaEntries: Array<
|
||||
[FloorRegistryEntry | undefined, AreaRegistryEntry[]]
|
||||
> = Object.entries(floorAreaLookup)
|
||||
.map(([floorId, floorAreas]) => {
|
||||
const floor = floors.find((fl) => fl.floor_id === floorId)!;
|
||||
return [floor, floorAreas] as const;
|
||||
})
|
||||
.sort(([floorA], [floorB]) => {
|
||||
if (floorA.level !== floorB.level) {
|
||||
return (floorA.level ?? 0) - (floorB.level ?? 0);
|
||||
}
|
||||
return stringCompare(floorA.name, floorB.name);
|
||||
});
|
||||
|
||||
const output: FloorAreaEntry[] = [];
|
||||
|
||||
floorAreaEntries.forEach(([floor, floorAreas]) => {
|
||||
if (floor) {
|
||||
output.push({
|
||||
id: floor.floor_id,
|
||||
type: "floor",
|
||||
name: floor.name,
|
||||
icon: floor.icon,
|
||||
strings: [floor.floor_id, ...floor.aliases, floor.name],
|
||||
level: floor.level,
|
||||
});
|
||||
}
|
||||
output.push(
|
||||
...floorAreas.map((area) => ({
|
||||
id: area.area_id,
|
||||
type: "area" as const,
|
||||
name: area.name,
|
||||
icon: area.icon,
|
||||
strings: [area.area_id, ...area.aliases, area.name],
|
||||
hasFloor: true,
|
||||
level: null,
|
||||
}))
|
||||
);
|
||||
});
|
||||
|
||||
if (!output.length && !unassisgnedAreas.length) {
|
||||
output.push({
|
||||
id: "no_areas",
|
||||
type: "area",
|
||||
name: this.hass.localize(
|
||||
"ui.components.area-picker.unassigned_areas"
|
||||
),
|
||||
icon: null,
|
||||
strings: [],
|
||||
level: null,
|
||||
});
|
||||
}
|
||||
|
||||
output.push(
|
||||
...unassisgnedAreas.map((area) => ({
|
||||
id: area.area_id,
|
||||
type: "area" as const,
|
||||
name: area.name,
|
||||
icon: area.icon,
|
||||
strings: [area.area_id, ...area.aliases, area.name],
|
||||
level: null,
|
||||
}))
|
||||
);
|
||||
|
||||
return output;
|
||||
}
|
||||
);
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (
|
||||
(!this._init && this.hass && this._floors) ||
|
||||
(this._init && changedProps.has("_opened") && this._opened)
|
||||
) {
|
||||
this._init = true;
|
||||
const areas = this._getAreas(
|
||||
this._floors!,
|
||||
Object.values(this.hass.areas),
|
||||
Object.values(this.hass.devices),
|
||||
Object.values(this.hass.entities),
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter,
|
||||
this.entityFilter,
|
||||
this.excludeAreas,
|
||||
this.excludeFloors
|
||||
);
|
||||
this.comboBox.items = areas;
|
||||
this.comboBox.filteredItems = areas;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-combo-box
|
||||
.hass=${this.hass}
|
||||
.helper=${this.helper}
|
||||
item-value-path="id"
|
||||
item-id-path="id"
|
||||
item-label-path="name"
|
||||
.value=${this._value}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.label=${this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.area-picker.area")
|
||||
: this.label}
|
||||
.placeholder=${this.placeholder
|
||||
? this.hass.areas[this.placeholder]?.name
|
||||
: undefined}
|
||||
.renderer=${rowRenderer}
|
||||
@filter-changed=${this._filterChanged}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._areaChanged}
|
||||
>
|
||||
</ha-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
const target = ev.target as HaComboBox;
|
||||
const filterString = ev.detail.value;
|
||||
if (!filterString) {
|
||||
this.comboBox.filteredItems = this.comboBox.items;
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredItems = fuzzyFilterSort<ScorableAreaFloorEntry>(
|
||||
filterString,
|
||||
target.items || []
|
||||
);
|
||||
|
||||
this.comboBox.filteredItems = filteredItems;
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||
this._opened = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _areaChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
|
||||
if (newValue === "no_areas") {
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = this.comboBox.selectedItem;
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
id: selected.id,
|
||||
type: selected.type,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-area-floor-picker": HaAreaFloorPicker;
|
||||
}
|
||||
}
|
@@ -1,15 +1,14 @@
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { LitElement, PropertyValues, TemplateResult, html } from "lit";
|
||||
import { html, LitElement, nothing, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import {
|
||||
ScorableTextItem,
|
||||
fuzzyFilterSort,
|
||||
ScorableTextItem,
|
||||
} from "../common/string/filter/sequence-matching";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
@@ -21,8 +20,10 @@ import {
|
||||
getDeviceEntityDisplayLookup,
|
||||
} from "../data/device_registry";
|
||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showPromptDialog,
|
||||
} from "../dialogs/generic/show-dialog-box";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-combo-box";
|
||||
@@ -36,18 +37,14 @@ type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry;
|
||||
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) =>
|
||||
html`<ha-list-item
|
||||
graphic="icon"
|
||||
class=${classMap({ "add-new": item.area_id === ADD_NEW_ID })}
|
||||
class=${classMap({ "add-new": item.area_id === "add_new" })}
|
||||
>
|
||||
${item.icon
|
||||
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon slot="graphic" .path=${mdiTextureBox}></ha-svg-icon>`}
|
||||
: nothing}
|
||||
${item.name}
|
||||
</ha-list-item>`;
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
const NO_ITEMS_ID = "___NO_ITEMS___";
|
||||
const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___";
|
||||
|
||||
@customElement("ha-area-picker")
|
||||
export class HaAreaPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -136,6 +133,18 @@ export class HaAreaPicker extends LitElement {
|
||||
noAdd: this["noAdd"],
|
||||
excludeAreas: this["excludeAreas"]
|
||||
): AreaRegistryEntry[] => {
|
||||
if (!areas.length) {
|
||||
return [
|
||||
{
|
||||
area_id: "no_areas",
|
||||
name: this.hass.localize("ui.components.area-picker.no_areas"),
|
||||
picture: null,
|
||||
icon: null,
|
||||
aliases: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||
@@ -272,13 +281,11 @@ export class HaAreaPicker extends LitElement {
|
||||
if (!outputAreas.length) {
|
||||
outputAreas = [
|
||||
{
|
||||
area_id: NO_ITEMS_ID,
|
||||
floor_id: null,
|
||||
name: this.hass.localize("ui.components.area-picker.no_areas"),
|
||||
area_id: "no_areas",
|
||||
name: this.hass.localize("ui.components.area-picker.no_match"),
|
||||
picture: null,
|
||||
icon: null,
|
||||
aliases: [],
|
||||
labels: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -288,13 +295,11 @@ export class HaAreaPicker extends LitElement {
|
||||
: [
|
||||
...outputAreas,
|
||||
{
|
||||
area_id: ADD_NEW_ID,
|
||||
floor_id: null,
|
||||
area_id: "add_new",
|
||||
name: this.hass.localize("ui.components.area-picker.add_new"),
|
||||
picture: null,
|
||||
icon: "mdi:plus",
|
||||
aliases: [],
|
||||
labels: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -362,40 +367,20 @@ export class HaAreaPicker extends LitElement {
|
||||
|
||||
const filteredItems = fuzzyFilterSort<ScorableAreaRegistryEntry>(
|
||||
filterString,
|
||||
target.items?.filter(
|
||||
(item) => ![NO_ITEMS_ID, ADD_NEW_ID].includes(item.label_id)
|
||||
) || []
|
||||
target.items || []
|
||||
);
|
||||
if (filteredItems.length === 0) {
|
||||
if (!this.noAdd) {
|
||||
this.comboBox.filteredItems = [
|
||||
{
|
||||
area_id: NO_ITEMS_ID,
|
||||
floor_id: null,
|
||||
name: this.hass.localize("ui.components.area-picker.no_match"),
|
||||
icon: null,
|
||||
picture: null,
|
||||
labels: [],
|
||||
aliases: [],
|
||||
},
|
||||
] as AreaRegistryEntry[];
|
||||
} else {
|
||||
this._suggestion = filterString;
|
||||
this.comboBox.filteredItems = [
|
||||
{
|
||||
area_id: ADD_NEW_SUGGESTION_ID,
|
||||
floor_id: null,
|
||||
name: this.hass.localize(
|
||||
"ui.components.area-picker.add_new_sugestion",
|
||||
{ name: this._suggestion }
|
||||
),
|
||||
icon: "mdi:plus",
|
||||
picture: null,
|
||||
labels: [],
|
||||
aliases: [],
|
||||
},
|
||||
] as AreaRegistryEntry[];
|
||||
}
|
||||
if (!this.noAdd && filteredItems?.length === 0) {
|
||||
this._suggestion = filterString;
|
||||
this.comboBox.filteredItems = [
|
||||
{
|
||||
area_id: "add_new_suggestion",
|
||||
name: this.hass.localize(
|
||||
"ui.components.area-picker.add_new_sugestion",
|
||||
{ name: this._suggestion }
|
||||
),
|
||||
picture: null,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
this.comboBox.filteredItems = filteredItems;
|
||||
}
|
||||
@@ -413,13 +398,11 @@ export class HaAreaPicker extends LitElement {
|
||||
ev.stopPropagation();
|
||||
let newValue = ev.detail.value;
|
||||
|
||||
if (newValue === NO_ITEMS_ID) {
|
||||
if (newValue === "no_areas") {
|
||||
newValue = "";
|
||||
this.comboBox.setInputValue("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (![ADD_NEW_SUGGESTION_ID, ADD_NEW_ID].includes(newValue)) {
|
||||
if (!["add_new_suggestion", "add_new"].includes(newValue)) {
|
||||
if (newValue !== this._value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
@@ -427,12 +410,25 @@ export class HaAreaPicker extends LitElement {
|
||||
}
|
||||
|
||||
(ev.target as any).value = this._value;
|
||||
|
||||
showAreaRegistryDetailDialog(this, {
|
||||
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
|
||||
createEntry: async (values) => {
|
||||
showPromptDialog(this, {
|
||||
title: this.hass.localize("ui.components.area-picker.add_dialog.title"),
|
||||
text: this.hass.localize("ui.components.area-picker.add_dialog.text"),
|
||||
confirmText: this.hass.localize(
|
||||
"ui.components.area-picker.add_dialog.add"
|
||||
),
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.components.area-picker.add_dialog.name"
|
||||
),
|
||||
defaultValue:
|
||||
newValue === "add_new_suggestion" ? this._suggestion : undefined,
|
||||
confirm: async (name) => {
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const area = await createAreaRegistryEntry(this.hass, values);
|
||||
const area = await createAreaRegistryEntry(this.hass, {
|
||||
name,
|
||||
});
|
||||
const areas = [...Object.values(this.hass.areas), area];
|
||||
this.comboBox.filteredItems = this._getAreas(
|
||||
areas,
|
||||
@@ -452,16 +448,18 @@ export class HaAreaPicker extends LitElement {
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.components.area-picker.failed_create_area"
|
||||
"ui.components.area-picker.add_dialog.failed_create_area"
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
cancel: () => {
|
||||
this._setValue(undefined);
|
||||
this._suggestion = undefined;
|
||||
this.comboBox.setInputValue("");
|
||||
},
|
||||
});
|
||||
|
||||
this._suggestion = undefined;
|
||||
this.comboBox.setInputValue("");
|
||||
}
|
||||
|
||||
private _setValue(value?: string) {
|
||||
|
@@ -1,89 +0,0 @@
|
||||
import { Button } from "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
|
||||
import type { HaIconButton } from "./ha-icon-button";
|
||||
import "./ha-menu";
|
||||
import type { HaMenu } from "./ha-menu";
|
||||
|
||||
@customElement("ha-button-menu-new")
|
||||
export class HaButtonMenuNew extends LitElement {
|
||||
protected readonly [FOCUS_TARGET];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public positioning?: "fixed" | "absolute" | "popover";
|
||||
|
||||
@property({ type: Boolean, attribute: "has-overflow" }) public hasOverflow =
|
||||
false;
|
||||
|
||||
@query("ha-menu", true) private _menu!: HaMenu;
|
||||
|
||||
public get items() {
|
||||
return this._menu.items;
|
||||
}
|
||||
|
||||
public override focus() {
|
||||
if (this._menu.open) {
|
||||
this._menu.focus();
|
||||
} else {
|
||||
this._triggerButton?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div @click=${this._handleClick}>
|
||||
<slot name="trigger" @slotchange=${this._setTriggerAria}></slot>
|
||||
</div>
|
||||
<ha-menu
|
||||
.positioning=${this.positioning}
|
||||
.hasOverflow=${this.hasOverflow}
|
||||
>
|
||||
<slot></slot>
|
||||
</ha-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleClick(): void {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
this._menu.anchorElement = this;
|
||||
if (this._menu.open) {
|
||||
this._menu.close();
|
||||
} else {
|
||||
this._menu.show();
|
||||
}
|
||||
}
|
||||
|
||||
private get _triggerButton() {
|
||||
return this.querySelector(
|
||||
'ha-icon-button[slot="trigger"], mwc-button[slot="trigger"], ha-assist-chip[slot="trigger"]'
|
||||
) as HaIconButton | Button | null;
|
||||
}
|
||||
|
||||
private _setTriggerAria() {
|
||||
if (this._triggerButton) {
|
||||
this._triggerButton.ariaHasPopup = "menu";
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
::slotted([disabled]) {
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-button-menu-new": HaButtonMenuNew;
|
||||
}
|
||||
}
|
221
src/components/ha-button-related-filter-menu.ts
Normal file
221
src/components/ha-button-related-filter-menu.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import type { Corner } from "@material/mwc-menu";
|
||||
import "@material/mwc-menu/mwc-menu-surface";
|
||||
import { mdiFilterVariant } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { computeDeviceName } from "../data/device_registry";
|
||||
import { findRelated, RelatedResult } from "../data/search";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./device/ha-device-picker";
|
||||
import "./entity/ha-entity-picker";
|
||||
import "./ha-area-picker";
|
||||
import "./ha-icon-button";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"related-changed": {
|
||||
value?: FilterValue;
|
||||
items?: RelatedResult;
|
||||
filter?: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface FilterValue {
|
||||
area?: string;
|
||||
device?: string;
|
||||
entity?: string;
|
||||
}
|
||||
|
||||
@customElement("ha-button-related-filter-menu")
|
||||
export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public corner: Corner = "BOTTOM_START";
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ attribute: false }) public value?: FilterValue;
|
||||
|
||||
/**
|
||||
* Show no entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-icon-button
|
||||
@click=${this._handleClick}
|
||||
.label=${this.hass.localize("ui.components.related-filter-menu.filter")}
|
||||
.path=${mdiFilterVariant}
|
||||
></ha-icon-button>
|
||||
<mwc-menu-surface
|
||||
.open=${this._open}
|
||||
.anchor=${this}
|
||||
.fullwidth=${this.narrow}
|
||||
.corner=${this.corner}
|
||||
@closed=${this._onClosed}
|
||||
@input=${stopPropagation}
|
||||
>
|
||||
<ha-area-picker
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.related-filter-menu.filter_by_area"
|
||||
)}
|
||||
.hass=${this.hass}
|
||||
.value=${this.value?.area}
|
||||
no-add
|
||||
@value-changed=${this._areaPicked}
|
||||
@click=${this._preventDefault}
|
||||
></ha-area-picker>
|
||||
<ha-device-picker
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.related-filter-menu.filter_by_device"
|
||||
)}
|
||||
.hass=${this.hass}
|
||||
.value=${this.value?.device}
|
||||
@value-changed=${this._devicePicked}
|
||||
@click=${this._preventDefault}
|
||||
></ha-device-picker>
|
||||
<ha-entity-picker
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.related-filter-menu.filter_by_entity"
|
||||
)}
|
||||
.hass=${this.hass}
|
||||
.value=${this.value?.entity}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
@value-changed=${this._entityPicked}
|
||||
@click=${this._preventDefault}
|
||||
></ha-entity-picker>
|
||||
</mwc-menu-surface>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleClick(): void {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
private _onClosed(ev): void {
|
||||
ev.stopPropagation();
|
||||
this._open = false;
|
||||
}
|
||||
|
||||
private _preventDefault(ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
private async _entityPicked(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const entityId = ev.detail.value;
|
||||
if (!entityId) {
|
||||
fireEvent(this, "related-changed", { value: undefined });
|
||||
return;
|
||||
}
|
||||
const filter = this.hass.localize(
|
||||
"ui.components.related-filter-menu.filtered_by_entity",
|
||||
{
|
||||
entity_name: computeStateName(
|
||||
(ev.currentTarget as any).comboBox.selectedItem
|
||||
),
|
||||
}
|
||||
);
|
||||
const items = await findRelated(this.hass, "entity", entityId);
|
||||
fireEvent(this, "related-changed", {
|
||||
value: { entity: entityId },
|
||||
filter,
|
||||
items,
|
||||
});
|
||||
}
|
||||
|
||||
private async _devicePicked(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const deviceId = ev.detail.value;
|
||||
if (!deviceId) {
|
||||
fireEvent(this, "related-changed", { value: undefined });
|
||||
return;
|
||||
}
|
||||
const filter = this.hass.localize(
|
||||
"ui.components.related-filter-menu.filtered_by_device",
|
||||
{
|
||||
device_name: computeDeviceName(
|
||||
(ev.currentTarget as any).comboBox.selectedItem,
|
||||
this.hass
|
||||
),
|
||||
}
|
||||
);
|
||||
const items = await findRelated(this.hass, "device", deviceId);
|
||||
|
||||
fireEvent(this, "related-changed", {
|
||||
value: { device: deviceId },
|
||||
filter,
|
||||
items,
|
||||
});
|
||||
}
|
||||
|
||||
private async _areaPicked(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const areaId = ev.detail.value;
|
||||
if (!areaId) {
|
||||
fireEvent(this, "related-changed", { value: undefined });
|
||||
return;
|
||||
}
|
||||
const filter = this.hass.localize(
|
||||
"ui.components.related-filter-menu.filtered_by_area",
|
||||
{ area_name: (ev.currentTarget as any).comboBox.selectedItem.name }
|
||||
);
|
||||
const items = await findRelated(this.hass, "area", areaId);
|
||||
fireEvent(this, "related-changed", {
|
||||
value: { area: areaId },
|
||||
filter,
|
||||
items,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
--mdc-menu-min-width: 250px;
|
||||
}
|
||||
ha-area-picker,
|
||||
ha-device-picker,
|
||||
ha-entity-picker {
|
||||
display: block;
|
||||
width: 300px;
|
||||
padding: 4px 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
ha-area-picker {
|
||||
padding-top: 16px;
|
||||
}
|
||||
ha-entity-picker {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
:host([narrow]) ha-area-picker,
|
||||
:host([narrow]) ha-device-picker,
|
||||
:host([narrow]) ha-entity-picker {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-button-related-filter-menu": HaRelatedFilterButtonMenu;
|
||||
}
|
||||
}
|
@@ -84,7 +84,6 @@ export class HaControlButton extends LitElement {
|
||||
--control-button-background-color: var(--disabled-color);
|
||||
--control-button-background-opacity: 0.2;
|
||||
--control-button-border-radius: 10px;
|
||||
--control-button-padding: 8px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--primary-text-color);
|
||||
width: 40px;
|
||||
@@ -96,20 +95,16 @@ export class HaControlButton extends LitElement {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: var(--control-button-border-radius);
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: var(--control-button-padding);
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
line-height: inherit;
|
||||
font-family: Roboto;
|
||||
font-weight: 500;
|
||||
line-height: 0;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
background: none;
|
||||
@@ -131,8 +126,6 @@ export class HaControlButton extends LitElement {
|
||||
background-color 180ms ease-in-out,
|
||||
opacity 180ms ease-in-out;
|
||||
opacity: var(--control-button-background-opacity);
|
||||
pointer-events: none;
|
||||
white-space: normal;
|
||||
}
|
||||
.button {
|
||||
transition: color 180ms ease-in-out;
|
||||
@@ -140,7 +133,6 @@ export class HaControlButton extends LitElement {
|
||||
}
|
||||
.button ::slotted(*) {
|
||||
pointer-events: none;
|
||||
opacity: 0.95;
|
||||
}
|
||||
.button:disabled {
|
||||
cursor: not-allowed;
|
||||
|
@@ -529,7 +529,7 @@ export class HaControlSlider extends LitElement {
|
||||
0,
|
||||
0
|
||||
);
|
||||
border-radius: 0 8px 8px 0;
|
||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
||||
}
|
||||
.slider .slider-track-bar:after {
|
||||
top: 0;
|
||||
@@ -546,7 +546,7 @@ export class HaControlSlider extends LitElement {
|
||||
0,
|
||||
0
|
||||
);
|
||||
border-radius: 8px 0 0 8px;
|
||||
border-radius: var(--border-radius) 0 0 var(--border-radius);
|
||||
}
|
||||
.slider .slider-track-bar.end::after {
|
||||
right: initial;
|
||||
@@ -561,7 +561,7 @@ export class HaControlSlider extends LitElement {
|
||||
calc((1 - var(--value, 0)) * var(--slider-size)),
|
||||
0
|
||||
);
|
||||
border-radius: 8px 8px 0 0;
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
:host([vertical]) .slider .slider-track-bar:after {
|
||||
top: var(--handle-margin);
|
||||
@@ -579,7 +579,7 @@ export class HaControlSlider extends LitElement {
|
||||
calc((0 - var(--value, 0)) * var(--slider-size)),
|
||||
0
|
||||
);
|
||||
border-radius: 0 0 8px 8px;
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
}
|
||||
:host([vertical]) .slider .slider-track-bar.end::after {
|
||||
top: initial;
|
||||
|
@@ -139,12 +139,12 @@ export class HaDialog extends DialogBase {
|
||||
}
|
||||
.header_button {
|
||||
position: absolute;
|
||||
right: -12px;
|
||||
top: -12px;
|
||||
right: -8px;
|
||||
top: -8px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: -12px;
|
||||
inset-inline-end: -8px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.dialog-actions {
|
||||
|
@@ -83,11 +83,13 @@ export class HaExpansionPanel extends LitElement {
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("expanded")) {
|
||||
if (changedProps.has("expanded") && this.expanded) {
|
||||
this._showContent = this.expanded;
|
||||
setTimeout(() => {
|
||||
// Verify we're still expanded
|
||||
this._container.style.overflow = this.expanded ? "initial" : "hidden";
|
||||
if (this.expanded) {
|
||||
this._container.style.overflow = "initial";
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
@@ -1,175 +0,0 @@
|
||||
import { SelectedDetail } from "@material/mwc-list";
|
||||
import "@material/mwc-menu/mwc-menu-surface";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { findRelated, RelatedResult } from "../data/search";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import { Blueprints, fetchBlueprints } from "../data/blueprint";
|
||||
|
||||
@customElement("ha-filter-blueprints")
|
||||
export class HaFilterBlueprints extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@property() public type?: "automation" | "script";
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@state() private _blueprints?: Blueprints;
|
||||
|
||||
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.blueprint.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._blueprints && this._shouldRender
|
||||
? html`
|
||||
<mwc-list
|
||||
@selected=${this._blueprintsSelected}
|
||||
multi
|
||||
class="ha-scrollbar"
|
||||
>
|
||||
${Object.entries(this._blueprints).map(([id, blueprint]) =>
|
||||
"error" in blueprint
|
||||
? nothing
|
||||
: html`<ha-check-list-item
|
||||
.value=${id}
|
||||
.selected=${(this.value || []).includes(id)}
|
||||
>
|
||||
${blueprint.metadata.name || id}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</mwc-list>
|
||||
`
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
protected async firstUpdated() {
|
||||
if (!this.type) {
|
||||
return;
|
||||
}
|
||||
this._blueprints = await fetchBlueprints(this.hass, this.type);
|
||||
}
|
||||
|
||||
protected updated(changed) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (this.narrow || !this.expanded) return;
|
||||
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||
`${this.clientHeight - 49}px`;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private async _blueprintsSelected(
|
||||
ev: CustomEvent<SelectedDetail<Set<number>>>
|
||||
) {
|
||||
const blueprints = this._blueprints!;
|
||||
const relatedPromises: Promise<RelatedResult>[] = [];
|
||||
|
||||
if (!ev.detail.index.size) {
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const value: string[] = [];
|
||||
|
||||
for (const index of ev.detail.index) {
|
||||
const blueprintId = Object.keys(blueprints)[index];
|
||||
value.push(blueprintId);
|
||||
if (this.type) {
|
||||
relatedPromises.push(
|
||||
findRelated(this.hass, `${this.type}_blueprint`, blueprintId)
|
||||
);
|
||||
}
|
||||
}
|
||||
this.value = value;
|
||||
const results = await Promise.all(relatedPromises);
|
||||
const items: Set<string> = new Set();
|
||||
for (const result of results) {
|
||||
if (result[this.type!]) {
|
||||
result[this.type!]!.forEach((item) => items.add(item));
|
||||
}
|
||||
}
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value,
|
||||
items: this.type ? items : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-filter-blueprints": HaFilterBlueprints;
|
||||
}
|
||||
}
|
@@ -1,316 +0,0 @@
|
||||
import { ActionDetail, SelectedDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
mdiTag,
|
||||
} from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import {
|
||||
CategoryRegistryEntry,
|
||||
createCategoryRegistryEntry,
|
||||
deleteCategoryRegistryEntry,
|
||||
subscribeCategoryRegistry,
|
||||
updateCategoryRegistryEntry,
|
||||
} from "../data/category_registry";
|
||||
import { showConfirmationDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { showCategoryRegistryDetailDialog } from "../panels/config/category/show-dialog-category-registry-detail";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
import "./ha-list-item";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
|
||||
@customElement("ha-filter-categories")
|
||||
export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@property() public scope?: string;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state() private _categories: CategoryRegistryEntry[] = [];
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
protected hassSubscribeRequiredHostProps = ["scope"];
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [
|
||||
subscribeCategoryRegistry(
|
||||
this.hass.connection,
|
||||
this.scope!,
|
||||
(categories) => {
|
||||
this._categories = categories;
|
||||
}
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
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.category.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`
|
||||
<mwc-list
|
||||
@selected=${this._categorySelected}
|
||||
class="ha-scrollbar"
|
||||
activatable
|
||||
>
|
||||
${this._categories.length > 0
|
||||
? html`<ha-list-item
|
||||
.selected=${!this.value?.length}
|
||||
.activated=${!this.value?.length}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.category.filter.show_all"
|
||||
)}</ha-list-item
|
||||
>`
|
||||
: nothing}
|
||||
${this._categories.map(
|
||||
(category) =>
|
||||
html`<ha-list-item
|
||||
.value=${category.category_id}
|
||||
.selected=${this.value?.includes(category.category_id)}
|
||||
.activated=${this.value?.includes(category.category_id)}
|
||||
graphic="icon"
|
||||
hasMeta
|
||||
>
|
||||
${category.icon
|
||||
? html`<ha-icon
|
||||
slot="graphic"
|
||||
.icon=${category.icon}
|
||||
></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
.path=${mdiTag}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>`}
|
||||
${category.name}
|
||||
<ha-button-menu
|
||||
@click=${stopPropagation}
|
||||
@action=${this._handleAction}
|
||||
slot="meta"
|
||||
fixed
|
||||
.categoryId=${category.category_id}
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
<mwc-list-item graphic="icon"
|
||||
><ha-svg-icon
|
||||
.path=${mdiPencil}
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.category.editor.edit"
|
||||
)}</mwc-list-item
|
||||
>
|
||||
<mwc-list-item graphic="icon" class="warning"
|
||||
><ha-svg-icon
|
||||
class="warning"
|
||||
.path=${mdiDelete}
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.category.editor.delete"
|
||||
)}</mwc-list-item
|
||||
>
|
||||
</ha-button-menu>
|
||||
</ha-list-item>`
|
||||
)}
|
||||
</mwc-list>
|
||||
`
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
${this.expanded
|
||||
? html`<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this._addCategory}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.category.editor.add")}
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changed) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||
`${this.clientHeight - (49 + 48)}px`;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||
const categoryId = (ev.currentTarget as any).categoryId;
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._editCategory(categoryId);
|
||||
break;
|
||||
case 1:
|
||||
this._deleteCategory(categoryId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _editCategory(id: string) {
|
||||
showCategoryRegistryDetailDialog(this, {
|
||||
scope: this.scope!,
|
||||
entry: this._categories.find((cat) => cat.category_id === id),
|
||||
updateEntry: (updates) =>
|
||||
updateCategoryRegistryEntry(this.hass, this.scope!, id, updates),
|
||||
});
|
||||
}
|
||||
|
||||
private async _deleteCategory(id: string) {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.category.editor.confirm_delete"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.category.editor.confirm_delete_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
});
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteCategoryRegistryEntry(this.hass, this.scope!, id);
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
} catch (err: any) {
|
||||
alert(`Failed to delete: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private _addCategory() {
|
||||
if (!this.scope) {
|
||||
return;
|
||||
}
|
||||
showCategoryRegistryDetailDialog(this, {
|
||||
scope: this.scope,
|
||||
createEntry: (values) =>
|
||||
createCategoryRegistryEntry(this.hass, this.scope!, values),
|
||||
});
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private async _categorySelected(ev: CustomEvent<SelectedDetail<number>>) {
|
||||
if (!ev.detail.index) {
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
return;
|
||||
}
|
||||
const index = ev.detail.index - 1;
|
||||
|
||||
const val = this._categories![index]?.category_id;
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
this.value = [val];
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: this.value,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
position: relative;
|
||||
}
|
||||
: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;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
mwc-list {
|
||||
--mdc-list-item-meta-size: auto;
|
||||
--mdc-list-side-padding-right: 4px;
|
||||
--mdc-icon-button-size: 36px;
|
||||
}
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.add {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-filter-categories": HaFilterCategories;
|
||||
}
|
||||
}
|
@@ -1,209 +0,0 @@
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { computeDeviceName } from "../data/device_registry";
|
||||
import { findRelated, RelatedResult } from "../data/search";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-check-list-item";
|
||||
import { loadVirtualizer } from "../resources/virtualizer";
|
||||
|
||||
@customElement("ha-filter-devices")
|
||||
export class HaFilterDevices extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@property() public type?: keyof RelatedResult;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
public willUpdate(properties: PropertyValues) {
|
||||
super.willUpdate(properties);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
loadVirtualizer();
|
||||
}
|
||||
}
|
||||
|
||||
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.devices.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`<mwc-list class="ha-scrollbar">
|
||||
<lit-virtualizer
|
||||
.items=${this._devices(this.hass.devices, this.value)}
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${this._renderItem}
|
||||
@click=${this._handleItemClick}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
</mwc-list>`
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private _keyFunction = (device) => device?.id;
|
||||
|
||||
private _renderItem = (device) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${device.id}
|
||||
.selected=${this.value?.includes(device.id)}
|
||||
>
|
||||
${computeDeviceName(device, this.hass)}
|
||||
</ha-check-list-item>`;
|
||||
|
||||
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);
|
||||
this._findRelated();
|
||||
}
|
||||
|
||||
protected updated(changed) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||
`${this.clientHeight - 49}px`;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _devices = memoizeOne((devices: HomeAssistant["devices"], _value) => {
|
||||
const values = Object.values(devices);
|
||||
return values.sort((a, b) =>
|
||||
stringCompare(
|
||||
a.name_by_user || a.name || "",
|
||||
b.name_by_user || b.name || "",
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
private async _findRelated() {
|
||||
const relatedPromises: Promise<RelatedResult>[] = [];
|
||||
|
||||
if (!this.value?.length) {
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const value: string[] = [];
|
||||
|
||||
for (const deviceId of this.value) {
|
||||
value.push(deviceId);
|
||||
if (this.type) {
|
||||
relatedPromises.push(findRelated(this.hass, "device", deviceId));
|
||||
}
|
||||
}
|
||||
this.value = value;
|
||||
const results = await Promise.all(relatedPromises);
|
||||
const items: Set<string> = new Set();
|
||||
for (const result of results) {
|
||||
if (result[this.type!]) {
|
||||
result[this.type!]!.forEach((item) => items.add(item));
|
||||
}
|
||||
}
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value,
|
||||
items: this.type ? items : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
ha-check-list-item {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-filter-devices": HaFilterDevices;
|
||||
}
|
||||
}
|
@@ -1,228 +0,0 @@
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { findRelated, RelatedResult } from "../data/search";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-state-icon";
|
||||
import "./ha-check-list-item";
|
||||
import { loadVirtualizer } from "../resources/virtualizer";
|
||||
|
||||
@customElement("ha-filter-entities")
|
||||
export class HaFilterEntities extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@property() public type?: keyof RelatedResult;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
public willUpdate(properties: PropertyValues) {
|
||||
super.willUpdate(properties);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
loadVirtualizer();
|
||||
}
|
||||
}
|
||||
|
||||
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.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`
|
||||
<mwc-list class="ha-scrollbar">
|
||||
<lit-virtualizer
|
||||
.items=${this._entities(
|
||||
this.hass.states,
|
||||
this.type,
|
||||
this.value
|
||||
)}
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${this._renderItem}
|
||||
@click=${this._handleItemClick}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
</mwc-list>
|
||||
`
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changed) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||
`${this.clientHeight - 49}px`;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _keyFunction = (entity) => entity?.entity_id;
|
||||
|
||||
private _renderItem = (entity) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${entity.entity_id}
|
||||
.selected=${this.value?.includes(entity.entity_id)}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-state-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.stateObj=${entity}
|
||||
></ha-state-icon>
|
||||
${computeStateName(entity)}
|
||||
</ha-check-list-item>`;
|
||||
|
||||
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);
|
||||
this._findRelated();
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _entities = memoizeOne(
|
||||
(states: HomeAssistant["states"], type: this["type"], _value) => {
|
||||
const values = Object.values(states);
|
||||
return values
|
||||
.filter(
|
||||
(entityState) => !type || computeStateDomain(entityState) !== type
|
||||
)
|
||||
.sort((a, b) =>
|
||||
stringCompare(
|
||||
computeStateName(a),
|
||||
computeStateName(b),
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
private async _findRelated() {
|
||||
const relatedPromises: Promise<RelatedResult>[] = [];
|
||||
|
||||
if (!this.value?.length) {
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const value: string[] = [];
|
||||
|
||||
for (const entityId of this.value) {
|
||||
value.push(entityId);
|
||||
if (this.type) {
|
||||
relatedPromises.push(findRelated(this.hass, "entity", entityId));
|
||||
}
|
||||
}
|
||||
this.value = value;
|
||||
const results = await Promise.all(relatedPromises);
|
||||
const items: Set<string> = new Set();
|
||||
for (const result of results) {
|
||||
if (result[this.type!]) {
|
||||
result[this.type!]!.forEach((item) => items.add(item));
|
||||
}
|
||||
}
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value,
|
||||
items: this.type ? items : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-filter-entities": HaFilterEntities;
|
||||
}
|
||||
}
|
@@ -1,295 +0,0 @@
|
||||
import "@material/mwc-menu/mwc-menu-surface";
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
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 {
|
||||
FloorRegistryEntry,
|
||||
getFloorAreaLookup,
|
||||
subscribeFloorRegistry,
|
||||
} from "../data/floor_registry";
|
||||
import { findRelated, RelatedResult } from "../data/search";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-filter-floor-areas")
|
||||
export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: {
|
||||
floors?: string[];
|
||||
areas?: string[];
|
||||
};
|
||||
|
||||
@property() public type?: keyof RelatedResult;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
@state() private _floors?: FloorRegistryEntry[];
|
||||
|
||||
protected render() {
|
||||
const areas = this._areas(this.hass.areas, this._floors);
|
||||
|
||||
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.areas.caption")}
|
||||
${this.value?.areas?.length || this.value?.floors?.length
|
||||
? html`<div class="badge">
|
||||
${(this.value?.areas?.length || 0) +
|
||||
(this.value?.floors?.length || 0)}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`
|
||||
<mwc-list class="ha-scrollbar">
|
||||
${repeat(
|
||||
areas?.floors || [],
|
||||
(floor) => floor.floor_id,
|
||||
(floor) => html`
|
||||
<ha-check-list-item
|
||||
.value=${floor.floor_id}
|
||||
.type=${"floors"}
|
||||
.selected=${this.value?.floors?.includes(
|
||||
floor.floor_id
|
||||
) || false}
|
||||
graphic="icon"
|
||||
@request-selected=${this._handleItemClick}
|
||||
>
|
||||
<ha-floor-icon
|
||||
slot="graphic"
|
||||
.floor=${floor}
|
||||
></ha-floor-icon>
|
||||
${floor.name}
|
||||
</ha-check-list-item>
|
||||
${repeat(
|
||||
floor.areas,
|
||||
(area) => area.area_id,
|
||||
(area) => this._renderArea(area)
|
||||
)}
|
||||
`
|
||||
)}
|
||||
${repeat(
|
||||
areas?.unassisgnedAreas,
|
||||
(area) => area.area_id,
|
||||
(area) => this._renderArea(area)
|
||||
)}
|
||||
</mwc-list>
|
||||
`
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderArea(area) {
|
||||
return html`<ha-check-list-item
|
||||
.value=${area.area_id}
|
||||
.selected=${this.value?.areas?.includes(area.area_id) || false}
|
||||
.type=${"areas"}
|
||||
graphic="icon"
|
||||
class=${area.floor_id ? "floor" : ""}
|
||||
@request-selected=${this._handleItemClick}
|
||||
>
|
||||
${area.icon
|
||||
? html`<ha-icon slot="graphic" .icon=${area.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiTextureBox}
|
||||
></ha-svg-icon>`}
|
||||
${area.name}
|
||||
</ha-check-list-item>`;
|
||||
}
|
||||
|
||||
private _handleItemClick(ev) {
|
||||
ev.stopPropagation();
|
||||
|
||||
const listItem = ev.currentTarget;
|
||||
const type = listItem?.type;
|
||||
const value = listItem?.value;
|
||||
|
||||
if (ev.detail.selected === listItem.selected || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.value?.[type]?.includes(value)) {
|
||||
this.value = {
|
||||
...this.value,
|
||||
[type]: this.value[type].filter((val) => val !== value),
|
||||
};
|
||||
} else {
|
||||
if (!this.value) {
|
||||
this.value = {};
|
||||
}
|
||||
this.value = {
|
||||
...this.value,
|
||||
[type]: [...(this.value[type] || []), value],
|
||||
};
|
||||
}
|
||||
|
||||
listItem.selected = this.value[type]?.includes(value);
|
||||
|
||||
this._findRelated();
|
||||
}
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [
|
||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||
this._floors = floors;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changed) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||
`${this.clientHeight - 49}px`;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _areas = memoizeOne(
|
||||
(areaReg: HomeAssistant["areas"], floors?: FloorRegistryEntry[]) => {
|
||||
const areas = Object.values(areaReg);
|
||||
|
||||
const floorAreaLookup = getFloorAreaLookup(areas);
|
||||
|
||||
const unassisgnedAreas = areas.filter(
|
||||
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
||||
);
|
||||
return {
|
||||
floors: floors?.map((floor) => ({
|
||||
...floor,
|
||||
areas: floorAreaLookup[floor.floor_id] || [],
|
||||
})),
|
||||
unassisgnedAreas: unassisgnedAreas,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
private async _findRelated() {
|
||||
const relatedPromises: Promise<RelatedResult>[] = [];
|
||||
|
||||
if (
|
||||
!this.value ||
|
||||
(!this.value.areas?.length && !this.value.floors?.length)
|
||||
) {
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: {},
|
||||
items: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.value.areas) {
|
||||
for (const areaId of this.value.areas) {
|
||||
if (this.type) {
|
||||
relatedPromises.push(findRelated(this.hass, "area", areaId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.value.floors) {
|
||||
for (const floorId of this.value.floors) {
|
||||
if (this.type) {
|
||||
relatedPromises.push(findRelated(this.hass, "floor", floorId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const results = await Promise.all(relatedPromises);
|
||||
const items: Set<string> = new Set();
|
||||
for (const result of results) {
|
||||
if (result[this.type!]) {
|
||||
result[this.type!]!.forEach((item) => items.add(item));
|
||||
}
|
||||
}
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: this.value,
|
||||
items: this.type ? items : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
ha-check-list-item {
|
||||
--mdc-list-item-graphic-margin: 16px;
|
||||
}
|
||||
.floor {
|
||||
padding-left: 32px;
|
||||
padding-inline-start: 32px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-filter-floor-areas": HaFilterFloorAreas;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"data-table-filter-changed": { value: any; items: Set<string> | undefined };
|
||||
}
|
||||
}
|
@@ -1,189 +0,0 @@
|
||||
import { SelectedDetail } from "@material/mwc-list";
|
||||
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 {
|
||||
fetchIntegrationManifests,
|
||||
IntegrationManifest,
|
||||
} from "../data/integration";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-domain-icon";
|
||||
|
||||
@customElement("ha-filter-integrations")
|
||||
export class HaFilterIntegrations 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 _manifests?: IntegrationManifest[];
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
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.integrations.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._manifests && this._shouldRender
|
||||
? html`
|
||||
<mwc-list
|
||||
@selected=${this._integrationsSelected}
|
||||
multi
|
||||
class="ha-scrollbar"
|
||||
>
|
||||
${repeat(
|
||||
this._integrations(this._manifests, this.value),
|
||||
(i) => i.domain,
|
||||
(integration) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${integration.domain}
|
||||
.selected=${(this.value || []).includes(
|
||||
integration.domain
|
||||
)}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-domain-icon
|
||||
slot="graphic"
|
||||
.hass=${this.hass}
|
||||
.domain=${integration.domain}
|
||||
brandFallback
|
||||
></ha-domain-icon>
|
||||
${integration.name || integration.domain}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</mwc-list>
|
||||
`
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changed) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||
`${this.clientHeight - 49}px`;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
protected async firstUpdated() {
|
||||
this._manifests = await fetchIntegrationManifests(this.hass);
|
||||
}
|
||||
|
||||
private _integrations = memoizeOne(
|
||||
(manifest: IntegrationManifest[], _value) =>
|
||||
manifest
|
||||
.filter(
|
||||
(mnfst) =>
|
||||
!mnfst.integration_type ||
|
||||
!["entity", "system", "hardware"].includes(mnfst.integration_type)
|
||||
)
|
||||
.sort((a, b) =>
|
||||
stringCompare(
|
||||
a.name || a.domain,
|
||||
b.name || b.domain,
|
||||
this.hass.locale.language
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
private async _integrationsSelected(
|
||||
ev: CustomEvent<SelectedDetail<Set<number>>>
|
||||
) {
|
||||
const integrations = this._integrations(this._manifests!, this.value);
|
||||
|
||||
if (!ev.detail.index.size) {
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const value: string[] = [];
|
||||
|
||||
for (const index of ev.detail.index) {
|
||||
const domain = integrations[index].domain;
|
||||
value.push(domain);
|
||||
}
|
||||
this.value = value;
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-filter-integrations": HaFilterIntegrations;
|
||||
}
|
||||
}
|
@@ -1,214 +0,0 @@
|
||||
import { SelectedDetail } from "@material/mwc-list";
|
||||
import "@material/mwc-menu/mwc-menu-surface";
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { computeCssColor } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import {
|
||||
LabelRegistryEntry,
|
||||
createLabelRegistryEntry,
|
||||
subscribeLabelRegistry,
|
||||
} from "../data/label_registry";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-icon";
|
||||
import "./ha-label";
|
||||
|
||||
@customElement("ha-filter-labels")
|
||||
export class HaFilterLabels extends SubscribeMixin(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 _labels: LabelRegistryEntry[] = [];
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [
|
||||
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||
this._labels = labels;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
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.labels.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`
|
||||
<mwc-list
|
||||
@selected=${this._labelSelected}
|
||||
class="ha-scrollbar"
|
||||
multi
|
||||
>
|
||||
${repeat(
|
||||
this._labels,
|
||||
(label) => label.label_id,
|
||||
(label) => {
|
||||
const color = label.color
|
||||
? computeCssColor(label.color)
|
||||
: undefined;
|
||||
return html`<ha-check-list-item
|
||||
.value=${label.label_id}
|
||||
.selected=${(this.value || []).includes(label.label_id)}
|
||||
hasMeta
|
||||
>
|
||||
<ha-label style=${color ? `--color: ${color}` : ""}>
|
||||
${label.icon
|
||||
? html`<ha-icon
|
||||
slot="icon"
|
||||
.icon=${label.icon}
|
||||
></ha-icon>`
|
||||
: nothing}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
</ha-check-list-item>`;
|
||||
}
|
||||
)}
|
||||
</mwc-list>
|
||||
`
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
${this.expanded
|
||||
? html`<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this._addLabel}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.labels.add_label")}
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changed) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||
`${this.clientHeight - (49 + 48)}px`;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _addLabel() {
|
||||
showLabelDetailDialog(this, {
|
||||
createEntry: (values) => createLabelRegistryEntry(this.hass, values),
|
||||
});
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private async _labelSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||
if (!ev.detail.index.size) {
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const value: string[] = [];
|
||||
|
||||
for (const index of ev.detail.index) {
|
||||
const labelId = this._labels[index].label_id;
|
||||
value.push(labelId);
|
||||
}
|
||||
this.value = value;
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
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;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
}
|
||||
ha-label {
|
||||
--ha-label-background-color: var(--color, var(--grey-color));
|
||||
--ha-label-background-opacity: 0.5;
|
||||
}
|
||||
.add {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-filter-labels": HaFilterLabels;
|
||||
}
|
||||
}
|
@@ -1,165 +0,0 @@
|
||||
import { SelectedDetail } from "@material/mwc-list";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-icon";
|
||||
|
||||
@customElement("ha-filter-states")
|
||||
export class HaFilterStates extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@property({ attribute: false }) public states?: {
|
||||
value: any;
|
||||
label?: string;
|
||||
icon?: string;
|
||||
}[];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state() private _shouldRender = false;
|
||||
|
||||
protected render() {
|
||||
if (!this.states) {
|
||||
return nothing;
|
||||
}
|
||||
const hasIcon = this.states.find((item) => item.icon);
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
leftChevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-will-change=${this._expandedWillChange}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.label}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`
|
||||
<mwc-list
|
||||
@selected=${this._statesSelected}
|
||||
multi
|
||||
class="ha-scrollbar"
|
||||
>
|
||||
${this.states.map(
|
||||
(item) =>
|
||||
html`<ha-check-list-item
|
||||
.value=${item.value}
|
||||
.selected=${this.value?.includes(item.value)}
|
||||
.graphic=${hasIcon ? "icon" : undefined}
|
||||
>
|
||||
${item.icon
|
||||
? html`<ha-icon
|
||||
slot="graphic"
|
||||
.icon=${item.icon}
|
||||
></ha-icon>`
|
||||
: nothing}
|
||||
${item.label}
|
||||
</ha-check-list-item>`
|
||||
)}
|
||||
</mwc-list>
|
||||
`
|
||||
: nothing}
|
||||
</ha-expansion-panel>
|
||||
`;
|
||||
}
|
||||
|
||||
protected updated(changed) {
|
||||
if (changed.has("expanded") && this.expanded) {
|
||||
setTimeout(() => {
|
||||
if (!this.expanded) return;
|
||||
this.renderRoot.querySelector("mwc-list")!.style.height =
|
||||
`${this.clientHeight - 49}px`;
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private _expandedWillChange(ev) {
|
||||
this._shouldRender = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _expandedChanged(ev) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private async _statesSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||
if (!ev.detail.index.size) {
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: [],
|
||||
items: undefined,
|
||||
});
|
||||
this.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const value: string[] = [];
|
||||
|
||||
for (const index of ev.detail.index) {
|
||||
const val = this.states![index].value;
|
||||
value.push(val);
|
||||
}
|
||||
this.value = value;
|
||||
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-filter-states": HaFilterStates;
|
||||
}
|
||||
}
|
@@ -1,56 +0,0 @@
|
||||
import {
|
||||
mdiHome,
|
||||
mdiHomeFloor0,
|
||||
mdiHomeFloor1,
|
||||
mdiHomeFloor2,
|
||||
mdiHomeFloor3,
|
||||
mdiHomeFloorNegative1,
|
||||
} from "@mdi/js";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { FloorRegistryEntry } from "../data/floor_registry";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
export const floorDefaultIconPath = (
|
||||
floor: Pick<FloorRegistryEntry, "level">
|
||||
) => {
|
||||
switch (floor.level) {
|
||||
case 0:
|
||||
return mdiHomeFloor0;
|
||||
case 1:
|
||||
return mdiHomeFloor1;
|
||||
case 2:
|
||||
return mdiHomeFloor2;
|
||||
case 3:
|
||||
return mdiHomeFloor3;
|
||||
case -1:
|
||||
return mdiHomeFloorNegative1;
|
||||
}
|
||||
return mdiHome;
|
||||
};
|
||||
|
||||
@customElement("ha-floor-icon")
|
||||
export class HaFloorIcon extends LitElement {
|
||||
@property({ attribute: false }) public floor!: Pick<
|
||||
FloorRegistryEntry,
|
||||
"icon" | "level"
|
||||
>;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
protected render() {
|
||||
if (this.floor.icon) {
|
||||
return html`<ha-icon .icon=${this.floor.icon}></ha-icon>`;
|
||||
}
|
||||
const defaultPath = floorDefaultIconPath(this.floor);
|
||||
|
||||
return html`<ha-svg-icon .path=${defaultPath}></ha-svg-icon>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-floor-icon": HaFloorIcon;
|
||||
}
|
||||
}
|
@@ -1,490 +0,0 @@
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { LitElement, PropertyValues, TemplateResult, html } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import {
|
||||
ScorableTextItem,
|
||||
fuzzyFilterSort,
|
||||
} from "../common/string/filter/sequence-matching";
|
||||
import { AreaRegistryEntry } from "../data/area_registry";
|
||||
import {
|
||||
DeviceEntityDisplayLookup,
|
||||
DeviceRegistryEntry,
|
||||
getDeviceEntityDisplayLookup,
|
||||
} from "../data/device_registry";
|
||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||
import {
|
||||
FloorRegistryEntry,
|
||||
createFloorRegistryEntry,
|
||||
getFloorAreaLookup,
|
||||
subscribeFloorRegistry,
|
||||
} from "../data/floor_registry";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { showFloorRegistryDetailDialog } from "../panels/config/areas/show-dialog-floor-registry-detail";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
import "./ha-floor-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-list-item";
|
||||
|
||||
type ScorableFloorRegistryEntry = ScorableTextItem & FloorRegistryEntry;
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
const NO_FLOORS_ID = "___NO_FLOORS___";
|
||||
const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___";
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) =>
|
||||
html`<ha-list-item
|
||||
graphic="icon"
|
||||
class=${classMap({ "add-new": item.floor_id === ADD_NEW_ID })}
|
||||
>
|
||||
<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>
|
||||
${item.name}
|
||||
</ha-list-item>`;
|
||||
|
||||
@customElement("ha-floor-picker")
|
||||
export class HaFloorPicker extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-add" })
|
||||
public noAdd = false;
|
||||
|
||||
/**
|
||||
* Show only floors with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show no floors with entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only floors with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
/**
|
||||
* List of floors to be excluded.
|
||||
* @type {Array}
|
||||
* @attr exclude-floors
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-floor" })
|
||||
public excludeFloors?: string[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property({ attribute: false })
|
||||
public entityFilter?: (entity: HassEntity) => boolean;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@state() private _opened?: boolean;
|
||||
|
||||
@state() private _floors?: FloorRegistryEntry[];
|
||||
|
||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||
|
||||
private _suggestion?: string;
|
||||
|
||||
private _init = false;
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.open();
|
||||
}
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.focus();
|
||||
}
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [
|
||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||
this._floors = floors;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
private _getFloors = memoizeOne(
|
||||
(
|
||||
floors: FloorRegistryEntry[],
|
||||
areas: AreaRegistryEntry[],
|
||||
devices: DeviceRegistryEntry[],
|
||||
entities: EntityRegistryDisplayEntry[],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
deviceFilter: this["deviceFilter"],
|
||||
entityFilter: this["entityFilter"],
|
||||
noAdd: this["noAdd"],
|
||||
excludeFloors: this["excludeFloors"]
|
||||
): FloorRegistryEntry[] => {
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||
|
||||
if (
|
||||
includeDomains ||
|
||||
excludeDomains ||
|
||||
includeDeviceClasses ||
|
||||
deviceFilter ||
|
||||
entityFilter
|
||||
) {
|
||||
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
|
||||
inputDevices = devices;
|
||||
inputEntities = entities.filter((entity) => entity.area_id);
|
||||
|
||||
if (includeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return true;
|
||||
}
|
||||
return entities.every(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (deviceFilter) {
|
||||
inputDevices = inputDevices!.filter((device) =>
|
||||
deviceFilter!(device)
|
||||
);
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter(stateObj);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter!(stateObj);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let outputFloors = floors;
|
||||
|
||||
let areaIds: string[] | undefined;
|
||||
|
||||
if (inputDevices) {
|
||||
areaIds = inputDevices
|
||||
.filter((device) => device.area_id)
|
||||
.map((device) => device.area_id!);
|
||||
}
|
||||
|
||||
if (inputEntities) {
|
||||
areaIds = (areaIds ?? []).concat(
|
||||
inputEntities
|
||||
.filter((entity) => entity.area_id)
|
||||
.map((entity) => entity.area_id!)
|
||||
);
|
||||
}
|
||||
|
||||
if (areaIds) {
|
||||
const floorAreaLookup = getFloorAreaLookup(areas);
|
||||
outputFloors = outputFloors.filter((floor) =>
|
||||
floorAreaLookup[floor.floor_id]?.some((area) =>
|
||||
areaIds!.includes(area.area_id)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeFloors) {
|
||||
outputFloors = outputFloors.filter(
|
||||
(floor) => !excludeFloors!.includes(floor.floor_id)
|
||||
);
|
||||
}
|
||||
|
||||
if (!outputFloors.length) {
|
||||
outputFloors = [
|
||||
{
|
||||
floor_id: NO_FLOORS_ID,
|
||||
name: this.hass.localize("ui.components.floor-picker.no_floors"),
|
||||
icon: null,
|
||||
level: null,
|
||||
aliases: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return noAdd
|
||||
? outputFloors
|
||||
: [
|
||||
...outputFloors,
|
||||
{
|
||||
floor_id: ADD_NEW_ID,
|
||||
name: this.hass.localize("ui.components.floor-picker.add_new"),
|
||||
icon: "mdi:plus",
|
||||
level: null,
|
||||
aliases: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (
|
||||
(!this._init && this.hass && this._floors) ||
|
||||
(this._init && changedProps.has("_opened") && this._opened)
|
||||
) {
|
||||
this._init = true;
|
||||
const floors = this._getFloors(
|
||||
this._floors!,
|
||||
Object.values(this.hass.areas),
|
||||
Object.values(this.hass.devices),
|
||||
Object.values(this.hass.entities),
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter,
|
||||
this.entityFilter,
|
||||
this.noAdd,
|
||||
this.excludeFloors
|
||||
).map((floor) => ({
|
||||
...floor,
|
||||
strings: [floor.floor_id, floor.name, ...floor.aliases],
|
||||
}));
|
||||
this.comboBox.items = floors;
|
||||
this.comboBox.filteredItems = floors;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-combo-box
|
||||
.hass=${this.hass}
|
||||
.helper=${this.helper}
|
||||
item-value-path="floor_id"
|
||||
item-id-path="floor_id"
|
||||
item-label-path="name"
|
||||
.value=${this._value}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.label=${this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.floor-picker.floor")
|
||||
: this.label}
|
||||
.placeholder=${this.placeholder
|
||||
? this._floors?.find((floor) => floor.floor_id === this.placeholder)
|
||||
?.name
|
||||
: undefined}
|
||||
.renderer=${rowRenderer}
|
||||
@filter-changed=${this._filterChanged}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._floorChanged}
|
||||
>
|
||||
</ha-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
const target = ev.target as HaComboBox;
|
||||
const filterString = ev.detail.value;
|
||||
if (!filterString) {
|
||||
this.comboBox.filteredItems = this.comboBox.items;
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredItems = fuzzyFilterSort<ScorableFloorRegistryEntry>(
|
||||
filterString,
|
||||
target.items?.filter(
|
||||
(item) => ![NO_FLOORS_ID, ADD_NEW_ID].includes(item.label_id)
|
||||
) || []
|
||||
);
|
||||
if (filteredItems.length === 0) {
|
||||
if (this.noAdd) {
|
||||
this.comboBox.filteredItems = [
|
||||
{
|
||||
floor_id: NO_FLOORS_ID,
|
||||
name: this.hass.localize("ui.components.floor-picker.no_match"),
|
||||
icon: null,
|
||||
level: null,
|
||||
aliases: [],
|
||||
},
|
||||
] as FloorRegistryEntry[];
|
||||
} else {
|
||||
this._suggestion = filterString;
|
||||
this.comboBox.filteredItems = [
|
||||
{
|
||||
floor_id: ADD_NEW_SUGGESTION_ID,
|
||||
name: this.hass.localize(
|
||||
"ui.components.floor-picker.add_new_sugestion",
|
||||
{ name: this._suggestion }
|
||||
),
|
||||
icon: "mdi:plus",
|
||||
level: null,
|
||||
aliases: [],
|
||||
},
|
||||
] as FloorRegistryEntry[];
|
||||
}
|
||||
} else {
|
||||
this.comboBox.filteredItems = filteredItems;
|
||||
}
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||
this._opened = ev.detail.value;
|
||||
}
|
||||
|
||||
private _floorChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
let newValue = ev.detail.value;
|
||||
|
||||
if (newValue === NO_FLOORS_ID) {
|
||||
newValue = "";
|
||||
this.comboBox.setInputValue("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (![ADD_NEW_SUGGESTION_ID, ADD_NEW_ID].includes(newValue)) {
|
||||
if (newValue !== this._value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
(ev.target as any).value = this._value;
|
||||
|
||||
showFloorRegistryDetailDialog(this, {
|
||||
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
|
||||
createEntry: async (values) => {
|
||||
try {
|
||||
const floor = await createFloorRegistryEntry(this.hass, values);
|
||||
const floors = [...this._floors!, floor];
|
||||
this.comboBox.filteredItems = this._getFloors(
|
||||
floors,
|
||||
Object.values(this.hass.areas)!,
|
||||
Object.values(this.hass.devices)!,
|
||||
Object.values(this.hass.entities)!,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter,
|
||||
this.entityFilter,
|
||||
this.noAdd,
|
||||
this.excludeFloors
|
||||
);
|
||||
await this.updateComplete;
|
||||
await this.comboBox.updateComplete;
|
||||
this._setValue(floor.floor_id);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.components.floor-picker.failed_create_floor"
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this._suggestion = undefined;
|
||||
this.comboBox.setInputValue("");
|
||||
}
|
||||
|
||||
private _setValue(value?: string) {
|
||||
this.value = value;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-floor-picker": HaFloorPicker;
|
||||
}
|
||||
}
|
@@ -1,169 +0,0 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-floor-picker";
|
||||
|
||||
@customElement("ha-floors-picker")
|
||||
export class HaFloorsPicker extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Array }) public value?: string[];
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-add" })
|
||||
public noAdd = false;
|
||||
|
||||
/**
|
||||
* Show only floors with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show no floors with entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only floors with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property({ attribute: false })
|
||||
public entityFilter?: (entity: HassEntity) => boolean;
|
||||
|
||||
@property({ attribute: "picked-floor-label" })
|
||||
public pickedFloorLabel?: string;
|
||||
|
||||
@property({ attribute: "pick-floor-label" })
|
||||
public pickFloorLabel?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const currentFloors = this._currentFloors;
|
||||
return html`
|
||||
${currentFloors.map(
|
||||
(floor) => html`
|
||||
<div>
|
||||
<ha-floor-picker
|
||||
.curValue=${floor}
|
||||
.noAdd=${this.noAdd}
|
||||
.hass=${this.hass}
|
||||
.value=${floor}
|
||||
.label=${this.pickedFloorLabel}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._floorChanged}
|
||||
></ha-floor-picker>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<div>
|
||||
<ha-floor-picker
|
||||
.noAdd=${this.noAdd}
|
||||
.hass=${this.hass}
|
||||
.label=${this.pickFloorLabel}
|
||||
.helper=${this.helper}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeDomains=${this.excludeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.disabled=${this.disabled}
|
||||
.placeholder=${this.placeholder}
|
||||
.required=${this.required && !currentFloors.length}
|
||||
@value-changed=${this._addFloor}
|
||||
.excludeFloors=${currentFloors}
|
||||
></ha-floor-picker>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _currentFloors(): string[] {
|
||||
return this.value || [];
|
||||
}
|
||||
|
||||
private async _updateFloors(floors) {
|
||||
this.value = floors;
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: floors,
|
||||
});
|
||||
}
|
||||
|
||||
private _floorChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const curValue = (ev.currentTarget as any).curValue;
|
||||
const newValue = ev.detail.value;
|
||||
if (newValue === curValue) {
|
||||
return;
|
||||
}
|
||||
const currentFloors = this._currentFloors;
|
||||
if (!newValue || currentFloors.includes(newValue)) {
|
||||
this._updateFloors(currentFloors.filter((ent) => ent !== curValue));
|
||||
return;
|
||||
}
|
||||
this._updateFloors(
|
||||
currentFloors.map((ent) => (ent === curValue ? newValue : ent))
|
||||
);
|
||||
}
|
||||
|
||||
private _addFloor(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
|
||||
const toAdd = ev.detail.value;
|
||||
if (!toAdd) {
|
||||
return;
|
||||
}
|
||||
(ev.currentTarget as any).value = "";
|
||||
const currentFloors = this._currentFloors;
|
||||
if (currentFloors.includes(toAdd)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateFloors([...currentFloors, toAdd]);
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
div {
|
||||
margin-top: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-floors-picker": HaFloorsPicker;
|
||||
}
|
||||
}
|
@@ -118,7 +118,7 @@ export class HaIconPicker extends LitElement {
|
||||
<ha-icon .icon=${this._value || this.placeholder} slot="icon">
|
||||
</ha-icon>
|
||||
`
|
||||
: html`<slot slot="icon" name="fallback"></slot>`}
|
||||
: html`<slot name="fallback"></slot>`}
|
||||
</ha-combo-box>
|
||||
`;
|
||||
}
|
||||
|
@@ -1,491 +0,0 @@
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import {
|
||||
ScorableTextItem,
|
||||
fuzzyFilterSort,
|
||||
} from "../common/string/filter/sequence-matching";
|
||||
import {
|
||||
DeviceEntityDisplayLookup,
|
||||
DeviceRegistryEntry,
|
||||
getDeviceEntityDisplayLookup,
|
||||
} from "../data/device_registry";
|
||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||
import {
|
||||
LabelRegistryEntry,
|
||||
createLabelRegistryEntry,
|
||||
subscribeLabelRegistry,
|
||||
} from "../data/label_registry";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-list-item";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
type ScorableLabelItem = ScorableTextItem & LabelRegistryEntry;
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
const NO_LABELS_ID = "___NO_LABELS___";
|
||||
const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___";
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<LabelRegistryEntry> = (item) =>
|
||||
html`<ha-list-item
|
||||
graphic="icon"
|
||||
class=${classMap({ "add-new": item.label_id === ADD_NEW_ID })}
|
||||
>
|
||||
${item.icon
|
||||
? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>`
|
||||
: nothing}
|
||||
${item.name}
|
||||
</ha-list-item>`;
|
||||
|
||||
@customElement("ha-label-picker")
|
||||
export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-add" })
|
||||
public noAdd = false;
|
||||
|
||||
/**
|
||||
* Show only labels with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show no labels with entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only labels with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
/**
|
||||
* List of labels to be excluded.
|
||||
* @type {Array}
|
||||
* @attr exclude-labels
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-label" })
|
||||
public excludeLabels?: string[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property({ attribute: false })
|
||||
public entityFilter?: (entity: HassEntity) => boolean;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@state() private _opened?: boolean;
|
||||
|
||||
@state() private _labels?: LabelRegistryEntry[];
|
||||
|
||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
||||
|
||||
private _suggestion?: string;
|
||||
|
||||
private _init = false;
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.open();
|
||||
}
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
await this.comboBox?.focus();
|
||||
}
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [
|
||||
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||
this._labels = labels;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
private _getLabels = memoizeOne(
|
||||
(
|
||||
labels: LabelRegistryEntry[],
|
||||
areas: HomeAssistant["areas"],
|
||||
devices: DeviceRegistryEntry[],
|
||||
entities: EntityRegistryDisplayEntry[],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
deviceFilter: this["deviceFilter"],
|
||||
entityFilter: this["entityFilter"],
|
||||
noAdd: this["noAdd"],
|
||||
excludeLabels: this["excludeLabels"]
|
||||
): LabelRegistryEntry[] => {
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||
|
||||
if (
|
||||
includeDomains ||
|
||||
excludeDomains ||
|
||||
includeDeviceClasses ||
|
||||
deviceFilter ||
|
||||
entityFilter
|
||||
) {
|
||||
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
|
||||
inputDevices = devices;
|
||||
inputEntities = entities.filter((entity) => entity.labels.length > 0);
|
||||
|
||||
if (includeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return true;
|
||||
}
|
||||
return entities.every(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (deviceFilter) {
|
||||
inputDevices = inputDevices!.filter((device) =>
|
||||
deviceFilter!(device)
|
||||
);
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter(stateObj);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter!(stateObj);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let outputLabels = labels;
|
||||
const usedLabels = new Set<string>();
|
||||
|
||||
let areaIds: string[] | undefined;
|
||||
|
||||
if (inputDevices) {
|
||||
areaIds = inputDevices
|
||||
.filter((device) => device.area_id)
|
||||
.map((device) => device.area_id!);
|
||||
|
||||
inputDevices.forEach((device) => {
|
||||
device.labels.forEach((label) => usedLabels.add(label));
|
||||
});
|
||||
}
|
||||
|
||||
if (inputEntities) {
|
||||
areaIds = (areaIds ?? []).concat(
|
||||
inputEntities
|
||||
.filter((entity) => entity.area_id)
|
||||
.map((entity) => entity.area_id!)
|
||||
);
|
||||
inputEntities.forEach((entity) => {
|
||||
entity.labels.forEach((label) => usedLabels.add(label));
|
||||
});
|
||||
}
|
||||
|
||||
if (areaIds) {
|
||||
areaIds.forEach((areaId) => {
|
||||
const area = areas[areaId];
|
||||
area.labels.forEach((label) => usedLabels.add(label));
|
||||
});
|
||||
}
|
||||
|
||||
if (excludeLabels) {
|
||||
outputLabels = outputLabels.filter(
|
||||
(label) => !excludeLabels!.includes(label.label_id)
|
||||
);
|
||||
}
|
||||
|
||||
if (inputDevices || inputEntities) {
|
||||
outputLabels = outputLabels.filter((label) =>
|
||||
usedLabels.has(label.label_id)
|
||||
);
|
||||
}
|
||||
|
||||
if (!outputLabels.length) {
|
||||
outputLabels = [
|
||||
{
|
||||
label_id: NO_LABELS_ID,
|
||||
name: this.hass.localize("ui.components.label-picker.no_match"),
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return noAdd
|
||||
? outputLabels
|
||||
: [
|
||||
...outputLabels,
|
||||
{
|
||||
label_id: ADD_NEW_ID,
|
||||
name: this.hass.localize("ui.components.label-picker.add_new"),
|
||||
icon: "mdi:plus",
|
||||
color: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (
|
||||
(!this._init && this.hass && this._labels) ||
|
||||
(this._init && changedProps.has("_opened") && this._opened)
|
||||
) {
|
||||
this._init = true;
|
||||
const items = this._getLabels(
|
||||
this._labels!,
|
||||
this.hass.areas,
|
||||
Object.values(this.hass.devices),
|
||||
Object.values(this.hass.entities),
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter,
|
||||
this.entityFilter,
|
||||
this.noAdd,
|
||||
this.excludeLabels
|
||||
).map((label) => ({
|
||||
...label,
|
||||
strings: [label.label_id, label.name],
|
||||
}));
|
||||
|
||||
this.comboBox.items = items;
|
||||
this.comboBox.filteredItems = items;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-combo-box
|
||||
.hass=${this.hass}
|
||||
.helper=${this.helper}
|
||||
item-value-path="label_id"
|
||||
item-id-path="label_id"
|
||||
item-label-path="name"
|
||||
.value=${this._value}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.label=${this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.label-picker.label")
|
||||
: this.label}
|
||||
.placeholder=${this.placeholder
|
||||
? this._labels?.find((label) => label.label_id === this.placeholder)
|
||||
?.name
|
||||
: undefined}
|
||||
.renderer=${rowRenderer}
|
||||
@filter-changed=${this._filterChanged}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._labelChanged}
|
||||
>
|
||||
</ha-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
const target = ev.target as HaComboBox;
|
||||
const filterString = ev.detail.value;
|
||||
if (!filterString) {
|
||||
this.comboBox.filteredItems = this.comboBox.items;
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredItems = fuzzyFilterSort<ScorableLabelItem>(
|
||||
filterString,
|
||||
target.items?.filter(
|
||||
(item) => ![NO_LABELS_ID, ADD_NEW_ID].includes(item.label_id)
|
||||
) || []
|
||||
);
|
||||
if (filteredItems.length === 0) {
|
||||
if (this.noAdd) {
|
||||
this.comboBox.filteredItems = [
|
||||
{
|
||||
label_id: NO_LABELS_ID,
|
||||
name: this.hass.localize("ui.components.label-picker.no_match"),
|
||||
icon: null,
|
||||
color: null,
|
||||
},
|
||||
] as ScorableLabelItem[];
|
||||
} else {
|
||||
this._suggestion = filterString;
|
||||
this.comboBox.filteredItems = [
|
||||
{
|
||||
label_id: ADD_NEW_SUGGESTION_ID,
|
||||
name: this.hass.localize(
|
||||
"ui.components.label-picker.add_new_sugestion",
|
||||
{ name: this._suggestion }
|
||||
),
|
||||
icon: "mdi:plus",
|
||||
color: null,
|
||||
},
|
||||
] as ScorableLabelItem[];
|
||||
}
|
||||
} else {
|
||||
this.comboBox.filteredItems = filteredItems;
|
||||
}
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
||||
this._opened = ev.detail.value;
|
||||
}
|
||||
|
||||
private _labelChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
let newValue = ev.detail.value;
|
||||
|
||||
if (newValue === NO_LABELS_ID) {
|
||||
newValue = "";
|
||||
this.comboBox.setInputValue("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (![ADD_NEW_SUGGESTION_ID, ADD_NEW_ID].includes(newValue)) {
|
||||
if (newValue !== this._value) {
|
||||
this._setValue(newValue);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
(ev.target as any).value = this._value;
|
||||
|
||||
showLabelDetailDialog(this, {
|
||||
entry: undefined,
|
||||
suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "",
|
||||
createEntry: async (values) => {
|
||||
const label = await createLabelRegistryEntry(this.hass, values);
|
||||
const labels = [...this._labels!, label];
|
||||
this.comboBox.filteredItems = this._getLabels(
|
||||
labels,
|
||||
this.hass.areas!,
|
||||
Object.values(this.hass.devices)!,
|
||||
Object.values(this.hass.entities)!,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
this.deviceFilter,
|
||||
this.entityFilter,
|
||||
this.noAdd,
|
||||
this.excludeLabels
|
||||
);
|
||||
await this.updateComplete;
|
||||
await this.comboBox.updateComplete;
|
||||
this._setValue(label.label_id);
|
||||
return label;
|
||||
},
|
||||
});
|
||||
|
||||
this._suggestion = undefined;
|
||||
this.comboBox.setInputValue("");
|
||||
}
|
||||
|
||||
private _setValue(value?: string) {
|
||||
this.value = value;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-label-picker": HaLabelPicker;
|
||||
}
|
||||
}
|
@@ -1,17 +1,13 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "@material/web/ripple/ripple";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-label")
|
||||
class HaLabel extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) dense = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<span class="content">
|
||||
<span class="label">
|
||||
<slot name="icon"></slot>
|
||||
<slot></slot>
|
||||
<md-ripple></md-ripple>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
@@ -26,10 +22,8 @@ class HaLabel extends LitElement {
|
||||
var(--rgb-primary-text-color),
|
||||
0.15
|
||||
);
|
||||
--ha-label-background-opacity: 1;
|
||||
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.label {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@@ -41,23 +35,9 @@ class HaLabel extends LitElement {
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
border-radius: 18px;
|
||||
color: var(--ha-label-text-color);
|
||||
--mdc-icon-size: 12px;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
.content > * {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
:host:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
background-color: var(--ha-label-background-color);
|
||||
opacity: var(--ha-label-background-opacity);
|
||||
color: var(--ha-label-text-color);
|
||||
--mdc-icon-size: 18px;
|
||||
}
|
||||
::slotted([slot="icon"]) {
|
||||
margin-right: 8px;
|
||||
@@ -65,23 +45,11 @@ class HaLabel extends LitElement {
|
||||
margin-inline-start: -8px;
|
||||
margin-inline-end: 8px;
|
||||
display: flex;
|
||||
color: var(--ha-label-icon-color);
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
:host([dense]) {
|
||||
height: 20px;
|
||||
padding: 0 12px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
:host([dense]) ::slotted([slot="icon"]) {
|
||||
margin-right: 4px;
|
||||
margin-left: -4px;
|
||||
margin-inline-start: -4px;
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -1,216 +0,0 @@
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { LitElement, TemplateResult, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { computeCssColor } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import {
|
||||
LabelRegistryEntry,
|
||||
subscribeLabelRegistry,
|
||||
updateLabelRegistryEntry,
|
||||
} from "../data/label_registry";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./chips/ha-chip-set";
|
||||
import "./chips/ha-input-chip";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-label-picker";
|
||||
import type { HaLabelPicker } from "./ha-label-picker";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
|
||||
@customElement("ha-labels-picker")
|
||||
export class HaLabelsPicker extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property({ type: Boolean, attribute: "no-add" })
|
||||
public noAdd = false;
|
||||
|
||||
/**
|
||||
* Show only labels with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show no labels with entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only labels with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
/**
|
||||
* List of labels to be excluded.
|
||||
* @type {Array}
|
||||
* @attr exclude-labels
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-label" })
|
||||
public excludeLabels?: string[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property({ attribute: false })
|
||||
public entityFilter?: (entity: HassEntity) => boolean;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@state() private _labels?: { [id: string]: LabelRegistryEntry };
|
||||
|
||||
@query("ha-label-picker", true) public labelPicker!: HaLabelPicker;
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this.labelPicker?.open();
|
||||
}
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
await this.labelPicker?.focus();
|
||||
}
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [
|
||||
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||
const lookUp = {};
|
||||
labels.forEach((label) => {
|
||||
lookUp[label.label_id] = label;
|
||||
});
|
||||
this._labels = lookUp;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const labels = this.value
|
||||
?.map((id) => this._labels?.[id])
|
||||
.sort((a, b) =>
|
||||
stringCompare(a?.name || "", b?.name || "", this.hass.locale.language)
|
||||
);
|
||||
return html`
|
||||
${labels?.length
|
||||
? html`<ha-chip-set>
|
||||
${repeat(
|
||||
labels,
|
||||
(label) => label?.label_id,
|
||||
(label, idx) => {
|
||||
const color = label?.color
|
||||
? computeCssColor(label.color)
|
||||
: undefined;
|
||||
return html`
|
||||
<ha-input-chip
|
||||
.idx=${idx}
|
||||
.item=${label}
|
||||
@remove=${this._removeItem}
|
||||
@click=${this._openDetail}
|
||||
.label=${label?.name}
|
||||
selected
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
>
|
||||
${label?.icon
|
||||
? html`<ha-icon
|
||||
slot="icon"
|
||||
.icon=${label.icon}
|
||||
></ha-icon>`
|
||||
: nothing}
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</ha-chip-set>`
|
||||
: nothing}
|
||||
<ha-label-picker
|
||||
.hass=${this.hass}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.label=${this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.label-picker.add_label")
|
||||
: this.label}
|
||||
.placeholder=${this.placeholder}
|
||||
.excludeLabels=${this.value}
|
||||
@value-changed=${this._labelChanged}
|
||||
>
|
||||
</ha-label-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || [];
|
||||
}
|
||||
|
||||
private _removeItem(ev) {
|
||||
this._value.splice(ev.target.idx, 1);
|
||||
this._setValue([...this._value]);
|
||||
}
|
||||
|
||||
private _openDetail(ev) {
|
||||
const label = ev.target.item;
|
||||
showLabelDetailDialog(this, {
|
||||
entry: label,
|
||||
updateEntry: async (values) => {
|
||||
const updated = await updateLabelRegistryEntry(
|
||||
this.hass,
|
||||
label.label_id,
|
||||
values
|
||||
);
|
||||
return updated;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _labelChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const newValue = ev.detail.value;
|
||||
if (!newValue || this._value.includes(newValue)) {
|
||||
return;
|
||||
}
|
||||
this._setValue([...this._value, newValue]);
|
||||
this.labelPicker.value = "";
|
||||
}
|
||||
|
||||
private _setValue(value?: string[]) {
|
||||
this.value = value;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-chip-set {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
ha-input-chip {
|
||||
--md-input-chip-selected-container-color: var(--color, var(--grey-color));
|
||||
--ha-input-chip-selected-container-opacity: 0.5;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-labels-picker": HaLabelsPicker;
|
||||
}
|
||||
}
|
@@ -1,44 +0,0 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import "element-internals-polyfill";
|
||||
import { CSSResult, css } from "lit";
|
||||
import { MdMenuItem } from "@material/web/menu/menu-item";
|
||||
|
||||
@customElement("ha-menu-item")
|
||||
export class HaMenuItem extends MdMenuItem {
|
||||
static override styles: CSSResult[] = [
|
||||
...MdMenuItem.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-on-primary: var(--primary-text-color);
|
||||
--md-sys-color-secondary: var(--secondary-text-color);
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
--md-sys-color-secondary-container: rgba(
|
||||
var(--rgb-primary-color),
|
||||
0.15
|
||||
);
|
||||
--md-sys-color-on-secondary-container: var(--text-primary-color);
|
||||
--mdc-icon-size: 16px;
|
||||
|
||||
--md-sys-color-on-primary-container: var(--primary-text-color);
|
||||
--md-sys-color-on-secondary-container: var(--primary-text-color);
|
||||
}
|
||||
:host(.warning) {
|
||||
--md-menu-item-label-text-color: var(--error-color);
|
||||
--md-menu-item-leading-icon-color: var(--error-color);
|
||||
}
|
||||
::slotted([slot="headline"]) {
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-menu-item": HaMenuItem;
|
||||
}
|
||||
}
|
@@ -1,22 +0,0 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import "element-internals-polyfill";
|
||||
import { CSSResult, css } from "lit";
|
||||
import { MdMenu } from "@material/web/menu/menu";
|
||||
|
||||
@customElement("ha-menu")
|
||||
export class HaMenu extends MdMenu {
|
||||
static override styles: CSSResult[] = [
|
||||
...MdMenu.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-surface-container: var(--card-background-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-menu": HaMenu;
|
||||
}
|
||||
}
|
@@ -1,41 +0,0 @@
|
||||
import { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field";
|
||||
import "element-internals-polyfill";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-outlined-text-field")
|
||||
export class HaOutlinedTextField extends MdOutlinedTextField {
|
||||
static override styles = [
|
||||
...super.styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-outlined-text-field-input-text-color: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
--md-outlined-field-outline-color: var(--outline-color);
|
||||
--md-outlined-field-focus-outline-color: var(--primary-color);
|
||||
--md-outlined-field-hover-outline-color: var(--outline-hover-color);
|
||||
}
|
||||
:host([dense]) {
|
||||
--md-outlined-field-top-space: 5.5px;
|
||||
--md-outlined-field-bottom-space: 5.5px;
|
||||
--md-outlined-field-container-shape-start-start: 10px;
|
||||
--md-outlined-field-container-shape-start-end: 10px;
|
||||
--md-outlined-field-container-shape-end-end: 10px;
|
||||
--md-outlined-field-container-shape-end-start: 10px;
|
||||
--md-outlined-field-focus-outline-width: 1px;
|
||||
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||
}
|
||||
.input {
|
||||
font-family: Roboto, sans-serif;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-outlined-text-field": HaOutlinedTextField;
|
||||
}
|
||||
}
|
@@ -87,12 +87,8 @@ export class HaAreaSelector extends LitElement {
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
no-add
|
||||
.deviceFilter=${this.selector.area?.device
|
||||
? this._filterDevices
|
||||
: undefined}
|
||||
.entityFilter=${this.selector.area?.entity
|
||||
? this._filterEntities
|
||||
: undefined}
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-area-picker>
|
||||
@@ -106,12 +102,8 @@ export class HaAreaSelector extends LitElement {
|
||||
.helper=${this.helper}
|
||||
.pickAreaLabel=${this.label}
|
||||
no-add
|
||||
.deviceFilter=${this.selector.area?.device
|
||||
? this._filterDevices
|
||||
: undefined}
|
||||
.entityFilter=${this.selector.area?.entity
|
||||
? this._filterEntities
|
||||
: undefined}
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-areas-picker>
|
||||
|
@@ -1,153 +0,0 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, PropertyValues, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import type { DeviceRegistryEntry } from "../../data/device_registry";
|
||||
import { getDeviceIntegrationLookup } from "../../data/device_registry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
EntitySources,
|
||||
fetchEntitySourcesWithCache,
|
||||
} from "../../data/entity_sources";
|
||||
import type { FloorSelector } from "../../data/selector";
|
||||
import {
|
||||
filterSelectorDevices,
|
||||
filterSelectorEntities,
|
||||
} from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-floor-picker";
|
||||
import "../ha-floors-picker";
|
||||
|
||||
@customElement("ha-selector-floor")
|
||||
export class HaFloorSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: FloorSelector;
|
||||
|
||||
@property() public value?: any;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup);
|
||||
|
||||
private _hasIntegration(selector: FloorSelector) {
|
||||
return (
|
||||
(selector.floor?.entity &&
|
||||
ensureArray(selector.floor.entity).some(
|
||||
(filter) => filter.integration
|
||||
)) ||
|
||||
(selector.floor?.device &&
|
||||
ensureArray(selector.floor.device).some((device) => device.integration))
|
||||
);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (changedProperties.has("selector") && this.value !== undefined) {
|
||||
if (this.selector.floor?.multiple && !Array.isArray(this.value)) {
|
||||
this.value = [this.value];
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
} else if (!this.selector.floor?.multiple && Array.isArray(this.value)) {
|
||||
this.value = this.value[0];
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
if (
|
||||
changedProperties.has("selector") &&
|
||||
this._hasIntegration(this.selector) &&
|
||||
!this._entitySources
|
||||
) {
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
this._entitySources = sources;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._hasIntegration(this.selector) && !this._entitySources) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this.selector.floor?.multiple) {
|
||||
return html`
|
||||
<ha-floor-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
no-add
|
||||
.deviceFilter=${this.selector.floor?.device
|
||||
? this._filterDevices
|
||||
: undefined}
|
||||
.entityFilter=${this.selector.floor?.entity
|
||||
? this._filterEntities
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-floor-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-floors-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.pickFloorLabel=${this.label}
|
||||
no-add
|
||||
.deviceFilter=${this.selector.floor?.device
|
||||
? this._filterDevices
|
||||
: undefined}
|
||||
.entityFilter=${this.selector.floor?.entity
|
||||
? this._filterEntities
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-floors-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
if (!this.selector.floor?.entity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return ensureArray(this.selector.floor.entity).some((filter) =>
|
||||
filterSelectorEntities(filter, entity, this._entitySources)
|
||||
);
|
||||
};
|
||||
|
||||
private _filterDevices = (device: DeviceRegistryEntry): boolean => {
|
||||
if (!this.selector.floor?.device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const deviceIntegrations = this._entitySources
|
||||
? this._deviceIntegrationLookup(
|
||||
this._entitySources,
|
||||
Object.values(this.hass.entities)
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return ensureArray(this.selector.floor.device).some((filter) =>
|
||||
filterSelectorDevices(filter, device, deviceIntegrations)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-floor": HaFloorSelector;
|
||||
}
|
||||
}
|
@@ -1,83 +0,0 @@
|
||||
import { CSSResultGroup, LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { LabelSelector } from "../../data/selector";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-labels-picker";
|
||||
|
||||
@customElement("ha-selector-label")
|
||||
export class HaLabelSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public value?: string | string[];
|
||||
|
||||
@property() public name?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ attribute: false }) public selector!: LabelSelector;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected render() {
|
||||
if (this.selector.label.multiple) {
|
||||
return html`
|
||||
<ha-labels-picker
|
||||
.hass=${this.hass}
|
||||
.value=${ensureArray(this.value ?? [])}
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label}
|
||||
@value-changed=${this._handleChange}
|
||||
>
|
||||
</ha-labels-picker>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<ha-label-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label}
|
||||
@value-changed=${this._handleChange}
|
||||
>
|
||||
</ha-label-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleChange(ev) {
|
||||
let value = ev.detail.value;
|
||||
if (this.value === value) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
(value === "" || (Array.isArray(value) && value.length === 0)) &&
|
||||
!this.required
|
||||
) {
|
||||
value = undefined;
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-labels-picker {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-label": HaLabelSelector;
|
||||
}
|
||||
}
|
@@ -2,7 +2,7 @@ import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { UiColorSelector } from "../../data/selector";
|
||||
import "../ha-color-picker";
|
||||
import "../../panels/lovelace/components/hui-color-picker";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-selector-ui_color")
|
||||
@@ -19,14 +19,13 @@ export class HaSelectorUiColor extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-color-picker
|
||||
<hui-color-picker
|
||||
.label=${this.label}
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.defaultColor=${this.selector.ui_color?.default_color}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-color-picker>
|
||||
></hui-color-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -30,8 +30,6 @@ const LOAD_ELEMENTS = {
|
||||
entity: () => import("./ha-selector-entity"),
|
||||
statistic: () => import("./ha-selector-statistic"),
|
||||
file: () => import("./ha-selector-file"),
|
||||
floor: () => import("./ha-selector-floor"),
|
||||
label: () => import("./ha-selector-label"),
|
||||
language: () => import("./ha-selector-language"),
|
||||
navigation: () => import("./ha-selector-navigation"),
|
||||
number: () => import("./ha-selector-number"),
|
||||
|
@@ -30,8 +30,6 @@ import {
|
||||
entityMeetsTargetSelector,
|
||||
expandAreaTarget,
|
||||
expandDeviceTarget,
|
||||
expandFloorTarget,
|
||||
expandLabelTarget,
|
||||
Selector,
|
||||
} from "../data/selector";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
@@ -60,12 +58,20 @@ const showOptionalToggle = (field) =>
|
||||
!("boolean" in field.selector && field.default);
|
||||
|
||||
interface ExtHassService extends Omit<HassService, "fields"> {
|
||||
fields: Array<
|
||||
Omit<HassService["fields"][string], "selector"> & {
|
||||
key: string;
|
||||
selector?: Selector;
|
||||
}
|
||||
>;
|
||||
fields: {
|
||||
key: string;
|
||||
name?: string;
|
||||
description: string;
|
||||
required?: boolean;
|
||||
advanced?: boolean;
|
||||
default?: any;
|
||||
example?: any;
|
||||
filter?: {
|
||||
supported_features?: number[];
|
||||
attribute?: Record<string, any[]>;
|
||||
};
|
||||
selector?: Selector;
|
||||
}[];
|
||||
hasSelector: string[];
|
||||
}
|
||||
|
||||
@@ -269,42 +275,10 @@ export class HaServiceControl extends LitElement {
|
||||
ensureArray(
|
||||
value?.target?.device_id || value?.data?.device_id
|
||||
)?.slice() || [];
|
||||
const targetAreas =
|
||||
ensureArray(value?.target?.area_id || value?.data?.area_id)?.slice() ||
|
||||
[];
|
||||
const targetFloors = ensureArray(
|
||||
value?.target?.floor_id || value?.data?.floor_id
|
||||
const targetAreas = ensureArray(
|
||||
value?.target?.area_id || value?.data?.area_id
|
||||
)?.slice();
|
||||
const targetLabels = ensureArray(
|
||||
value?.target?.label_id || value?.data?.label_id
|
||||
)?.slice();
|
||||
if (targetLabels) {
|
||||
targetLabels.forEach((labelId) => {
|
||||
const expanded = expandLabelTarget(
|
||||
this.hass,
|
||||
labelId,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
targetSelector
|
||||
);
|
||||
targetDevices.push(...expanded.devices);
|
||||
targetEntities.push(...expanded.entities);
|
||||
targetAreas.push(...expanded.areas);
|
||||
});
|
||||
}
|
||||
if (targetFloors) {
|
||||
targetFloors.forEach((floorId) => {
|
||||
const expanded = expandFloorTarget(
|
||||
this.hass,
|
||||
floorId,
|
||||
this.hass.areas,
|
||||
targetSelector
|
||||
);
|
||||
targetAreas.push(...expanded.areas);
|
||||
});
|
||||
}
|
||||
if (targetAreas.length) {
|
||||
if (targetAreas) {
|
||||
targetAreas.forEach((areaId) => {
|
||||
const expanded = expandAreaTarget(
|
||||
this.hass,
|
||||
|
@@ -83,7 +83,7 @@ export class HaSortable extends LitElement {
|
||||
super.connectedCallback();
|
||||
this._shouldBeDestroy = false;
|
||||
if (this.hasUpdated) {
|
||||
this._createSortable();
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,38 +0,0 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import "element-internals-polyfill";
|
||||
import { CSSResult, css } from "lit";
|
||||
import { MdSubMenu } from "@material/web/menu/sub-menu";
|
||||
|
||||
@customElement("ha-sub-menu")
|
||||
// @ts-expect-error
|
||||
export class HaSubMenu extends MdSubMenu {
|
||||
static override styles: CSSResult[] = [
|
||||
MdSubMenu.styles,
|
||||
css`
|
||||
:host {
|
||||
--ha-icon-display: block;
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-sys-color-on-primary: var(--primary-text-color);
|
||||
--md-sys-color-secondary: var(--secondary-text-color);
|
||||
--md-sys-color-surface: var(--card-background-color);
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
--md-sys-color-secondary-container: rgba(
|
||||
var(--rgb-primary-color),
|
||||
0.15
|
||||
);
|
||||
--md-sys-color-on-secondary-container: var(--text-primary-color);
|
||||
--mdc-icon-size: 16px;
|
||||
|
||||
--md-sys-color-on-primary-container: var(--primary-text-color);
|
||||
--md-sys-color-on-secondary-container: var(--primary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-sub-menu": HaSubMenu;
|
||||
}
|
||||
}
|
@@ -6,57 +6,38 @@ import "@material/mwc-menu/mwc-menu-surface";
|
||||
import {
|
||||
mdiClose,
|
||||
mdiDevices,
|
||||
mdiHome,
|
||||
mdiLabel,
|
||||
mdiPlus,
|
||||
mdiTextureBox,
|
||||
mdiSofa,
|
||||
mdiUnfoldMoreVertical,
|
||||
} from "@mdi/js";
|
||||
import { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
|
||||
import {
|
||||
HassEntity,
|
||||
HassServiceTarget,
|
||||
UnsubscribeFunc,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing, unsafeCSS } from "lit";
|
||||
import { HassEntity, HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing, unsafeCSS } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { computeCssColor } from "../common/color/compute-color";
|
||||
import { hex2rgb } from "../common/color/convert-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { isValidEntityId } from "../common/entity/valid_entity_id";
|
||||
import { AreaRegistryEntry } from "../data/area_registry";
|
||||
import {
|
||||
DeviceRegistryEntry,
|
||||
computeDeviceName,
|
||||
DeviceRegistryEntry,
|
||||
} from "../data/device_registry";
|
||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
|
||||
import {
|
||||
FloorRegistryEntry,
|
||||
subscribeFloorRegistry,
|
||||
} from "../data/floor_registry";
|
||||
import {
|
||||
LabelRegistryEntry,
|
||||
subscribeLabelRegistry,
|
||||
} from "../data/label_registry";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./device/ha-device-picker";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./entity/ha-entity-picker";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./entity/ha-entity-picker";
|
||||
import "./ha-area-floor-picker";
|
||||
import { floorDefaultIconPath } from "./ha-floor-icon";
|
||||
import "./ha-area-picker";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-target-picker")
|
||||
export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
export class HaTargetPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: HassServiceTarget;
|
||||
@@ -91,33 +72,14 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean }) public addOnTop = false;
|
||||
|
||||
@state() private _addMode?:
|
||||
| "area_id"
|
||||
| "entity_id"
|
||||
| "device_id"
|
||||
| "label_id";
|
||||
@state() private _addMode?: "area_id" | "entity_id" | "device_id";
|
||||
|
||||
@query("#input") private _inputElement?;
|
||||
|
||||
@query(".add-container", true) private _addContainer?: HTMLDivElement;
|
||||
|
||||
@state() private _floors?: FloorRegistryEntry[];
|
||||
|
||||
@state() private _labels?: LabelRegistryEntry[];
|
||||
|
||||
private _opened = false;
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [
|
||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||
this._floors = floors;
|
||||
}),
|
||||
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||
this._labels = labels;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this.addOnTop) {
|
||||
return html` ${this._renderChips()} ${this._renderItems()} `;
|
||||
@@ -128,21 +90,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
private _renderItems() {
|
||||
return html`
|
||||
<div class="mdc-chip-set items">
|
||||
${this.value?.floor_id
|
||||
? ensureArray(this.value.floor_id).map((floor_id) => {
|
||||
const floor = this._floors?.find(
|
||||
(flr) => flr.floor_id === floor_id
|
||||
);
|
||||
return this._renderChip(
|
||||
"floor_id",
|
||||
floor_id,
|
||||
floor?.name || floor_id,
|
||||
undefined,
|
||||
floor?.icon,
|
||||
floor ? floorDefaultIconPath(floor) : mdiHome
|
||||
);
|
||||
})
|
||||
: ""}
|
||||
${this.value?.area_id
|
||||
? ensureArray(this.value.area_id).map((area_id) => {
|
||||
const area = this.hass.areas![area_id];
|
||||
@@ -152,10 +99,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
area?.name || area_id,
|
||||
undefined,
|
||||
area?.icon,
|
||||
mdiTextureBox
|
||||
mdiSofa
|
||||
);
|
||||
})
|
||||
: nothing}
|
||||
: ""}
|
||||
${this.value?.device_id
|
||||
? ensureArray(this.value.device_id).map((device_id) => {
|
||||
const device = this.hass.devices![device_id];
|
||||
@@ -168,7 +115,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
mdiDevices
|
||||
);
|
||||
})
|
||||
: nothing}
|
||||
: ""}
|
||||
${this.value?.entity_id
|
||||
? ensureArray(this.value.entity_id).map((entity_id) => {
|
||||
const entity = this.hass.states[entity_id];
|
||||
@@ -179,35 +126,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
entity
|
||||
);
|
||||
})
|
||||
: nothing}
|
||||
${this.value?.label_id
|
||||
? ensureArray(this.value.label_id).map((label_id) => {
|
||||
const label = this._labels?.find(
|
||||
(lbl) => lbl.label_id === label_id
|
||||
);
|
||||
let color = label?.color
|
||||
? computeCssColor(label.color)
|
||||
: undefined;
|
||||
if (color?.startsWith("var(")) {
|
||||
const computedStyles = getComputedStyle(this);
|
||||
color = computedStyles.getPropertyValue(
|
||||
color.substring(4, color.length - 1)
|
||||
);
|
||||
}
|
||||
if (color?.startsWith("#")) {
|
||||
color = hex2rgb(color).join(",");
|
||||
}
|
||||
return this._renderChip(
|
||||
"label_id",
|
||||
label_id,
|
||||
label ? label.name : label_id,
|
||||
undefined,
|
||||
label?.icon,
|
||||
mdiLabel,
|
||||
color
|
||||
);
|
||||
})
|
||||
: nothing}
|
||||
: ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -275,26 +194,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mdc-chip label_id add"
|
||||
.type=${"label_id"}
|
||||
@click=${this._showPicker}
|
||||
>
|
||||
<div class="mdc-chip__ripple"></div>
|
||||
<ha-svg-icon
|
||||
class="mdc-chip__icon mdc-chip__icon--leading"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
<span role="gridcell">
|
||||
<span role="button" tabindex="0" class="mdc-chip__primary-action">
|
||||
<span class="mdc-chip__text"
|
||||
>${this.hass.localize(
|
||||
"ui.components.target-picker.add_label_id"
|
||||
)}</span
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
${this._renderPicker()}
|
||||
</div>
|
||||
${this.helper
|
||||
@@ -308,22 +207,18 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _renderChip(
|
||||
type: "floor_id" | "area_id" | "device_id" | "entity_id" | "label_id",
|
||||
type: "area_id" | "device_id" | "entity_id",
|
||||
id: string,
|
||||
name: string,
|
||||
entityState?: HassEntity,
|
||||
icon?: string | null,
|
||||
fallbackIconPath?: string,
|
||||
color?: string
|
||||
fallbackIconPath?: string
|
||||
) {
|
||||
return html`
|
||||
<div
|
||||
class="mdc-chip ${classMap({
|
||||
[type]: true,
|
||||
})}"
|
||||
style=${color
|
||||
? `--color: rgb(${color}); --background-color: rgba(${color}, .5)`
|
||||
: ""}
|
||||
>
|
||||
${icon
|
||||
? html`<ha-icon
|
||||
@@ -401,7 +296,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
@input=${stopPropagation}
|
||||
>${this._addMode === "area_id"
|
||||
? html`
|
||||
<ha-area-floor-picker
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"area_id"}
|
||||
@@ -414,10 +309,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeAreas=${ensureArray(this.value?.area_id)}
|
||||
.excludeFloors=${ensureArray(this.value?.floor_id)}
|
||||
@value-changed=${this._targetPicked}
|
||||
@click=${this._preventDefault}
|
||||
></ha-area-floor-picker>
|
||||
></ha-area-picker>
|
||||
`
|
||||
: this._addMode === "device_id"
|
||||
? html`
|
||||
@@ -437,42 +331,23 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
@click=${this._preventDefault}
|
||||
></ha-device-picker>
|
||||
`
|
||||
: this._addMode === "label_id"
|
||||
? html`
|
||||
<ha-label-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"label_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_label_id"
|
||||
)}
|
||||
no-add
|
||||
.deviceFilter=${this.deviceFilter}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeLabels=${ensureArray(this.value?.label_id)}
|
||||
@value-changed=${this._targetPicked}
|
||||
@click=${this._preventDefault}
|
||||
></ha-label-picker>
|
||||
`
|
||||
: html`
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"entity_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_entity_id"
|
||||
)}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeEntities=${ensureArray(this.value?.entity_id)}
|
||||
@value-changed=${this._targetPicked}
|
||||
@click=${this._preventDefault}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>
|
||||
`}</mwc-menu-surface
|
||||
: html`
|
||||
<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
id="input"
|
||||
.type=${"entity_id"}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.target-picker.add_entity_id"
|
||||
)}
|
||||
.entityFilter=${this.entityFilter}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.excludeEntities=${ensureArray(this.value?.entity_id)}
|
||||
@value-changed=${this._targetPicked}
|
||||
@click=${this._preventDefault}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>
|
||||
`}</mwc-menu-surface
|
||||
>`;
|
||||
}
|
||||
|
||||
@@ -481,24 +356,18 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
if (!ev.detail.value) {
|
||||
return;
|
||||
}
|
||||
let value = ev.detail.value;
|
||||
const value = ev.detail.value;
|
||||
const target = ev.currentTarget;
|
||||
let type = target.type;
|
||||
|
||||
if (type === "entity_id" && !isValidEntityId(value)) {
|
||||
if (target.type === "entity_id" && !isValidEntityId(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "area_id") {
|
||||
value = ev.detail.value.id;
|
||||
type = `${ev.detail.value.type}_id`;
|
||||
}
|
||||
|
||||
target.value = "";
|
||||
if (
|
||||
this.value &&
|
||||
this.value[type] &&
|
||||
ensureArray(this.value[type]).includes(value)
|
||||
this.value[target.type] &&
|
||||
ensureArray(this.value[target.type]).includes(value)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -506,31 +375,19 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
value: this.value
|
||||
? {
|
||||
...this.value,
|
||||
[type]: this.value[type]
|
||||
? [...ensureArray(this.value[type]), value]
|
||||
[target.type]: this.value[target.type]
|
||||
? [...ensureArray(this.value[target.type]), value]
|
||||
: value,
|
||||
}
|
||||
: { [type]: value },
|
||||
: { [target.type]: value },
|
||||
});
|
||||
}
|
||||
|
||||
private _handleExpand(ev) {
|
||||
const target = ev.currentTarget as any;
|
||||
const newAreas: string[] = [];
|
||||
const newDevices: string[] = [];
|
||||
const newEntities: string[] = [];
|
||||
|
||||
if (target.type === "floor_id") {
|
||||
Object.values(this.hass.areas).forEach((area) => {
|
||||
if (
|
||||
area.floor_id === target.id &&
|
||||
!this.value!.area_id?.includes(area.area_id) &&
|
||||
this._areaMeetsFilter(area)
|
||||
) {
|
||||
newAreas.push(area.area_id);
|
||||
}
|
||||
});
|
||||
} else if (target.type === "area_id") {
|
||||
if (target.type === "area_id") {
|
||||
Object.values(this.hass.devices).forEach((device) => {
|
||||
if (
|
||||
device.area_id === target.id &&
|
||||
@@ -559,34 +416,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
newEntities.push(entity.entity_id);
|
||||
}
|
||||
});
|
||||
} else if (target.type === "label_id") {
|
||||
Object.values(this.hass.areas).forEach((area) => {
|
||||
if (
|
||||
area.labels.includes(target.id) &&
|
||||
!this.value!.area_id?.includes(area.area_id) &&
|
||||
this._areaMeetsFilter(area)
|
||||
) {
|
||||
newAreas.push(area.area_id);
|
||||
}
|
||||
});
|
||||
Object.values(this.hass.devices).forEach((device) => {
|
||||
if (
|
||||
device.labels.includes(target.id) &&
|
||||
!this.value!.device_id?.includes(device.id) &&
|
||||
this._deviceMeetsFilter(device)
|
||||
) {
|
||||
newDevices.push(device.id);
|
||||
}
|
||||
});
|
||||
Object.values(this.hass.entities).forEach((entity) => {
|
||||
if (
|
||||
entity.labels.includes(target.id) &&
|
||||
!this.value!.entity_id?.includes(entity.entity_id) &&
|
||||
this._entityRegMeetsFilter(entity)
|
||||
) {
|
||||
newEntities.push(entity.entity_id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -597,9 +426,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
if (newDevices.length) {
|
||||
value = this._addItems(value, "device_id", newDevices);
|
||||
}
|
||||
if (newAreas.length) {
|
||||
value = this._addItems(value, "area_id", newAreas);
|
||||
}
|
||||
value = this._removeItem(value, target.type, target.id);
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
@@ -669,33 +495,44 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
private _areaMeetsFilter(area: AreaRegistryEntry): boolean {
|
||||
const areaDevices = Object.values(this.hass.devices).filter(
|
||||
(device) => device.area_id === area.area_id
|
||||
);
|
||||
|
||||
if (areaDevices.some((device) => this._deviceMeetsFilter(device))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const areaEntities = Object.values(this.hass.entities).filter(
|
||||
(entity) => entity.area_id === area.area_id
|
||||
);
|
||||
|
||||
if (areaEntities.some((entity) => this._entityRegMeetsFilter(entity))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _deviceMeetsFilter(device: DeviceRegistryEntry): boolean {
|
||||
const devEntities = Object.values(this.hass.entities).filter(
|
||||
(entity) => entity.device_id === device.id
|
||||
);
|
||||
|
||||
if (!devEntities.some((entity) => this._entityRegMeetsFilter(entity))) {
|
||||
return false;
|
||||
if (this.includeDomains) {
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!devEntities.some((entity) =>
|
||||
this.includeDomains!.includes(computeDomain(entity.entity_id))
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.includeDeviceClasses) {
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!devEntities.some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
this.includeDeviceClasses!.includes(
|
||||
stateObj.attributes.device_class
|
||||
)
|
||||
);
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.deviceFilter) {
|
||||
@@ -704,6 +541,19 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.entityFilter) {
|
||||
if (
|
||||
!devEntities.some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return this.entityFilter!(stateObj);
|
||||
})
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -791,8 +641,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
--mdc-icon-size: 20px;
|
||||
border-radius: 50%;
|
||||
padding: 6px;
|
||||
margin-left: -13px !important;
|
||||
margin-inline-start: -13px !important;
|
||||
margin-left: -14px !important;
|
||||
margin-inline-start: -14px !important;
|
||||
margin-inline-end: 4px !important;
|
||||
direction: var(--direction);
|
||||
}
|
||||
@@ -801,19 +651,16 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: initial;
|
||||
}
|
||||
.mdc-chip.area_id:not(.add),
|
||||
.mdc-chip.floor_id:not(.add) {
|
||||
border: 1px solid #fed6a4;
|
||||
.mdc-chip.area_id:not(.add) {
|
||||
border: 2px solid #fed6a4;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
.mdc-chip.area_id:not(.add) .mdc-chip__icon--leading,
|
||||
.mdc-chip.area_id.add,
|
||||
.mdc-chip.floor_id:not(.add) .mdc-chip__icon--leading,
|
||||
.mdc-chip.floor_id.add {
|
||||
.mdc-chip.area_id.add {
|
||||
background: #fed6a4;
|
||||
}
|
||||
.mdc-chip.device_id:not(.add) {
|
||||
border: 1px solid #a8e1fb;
|
||||
border: 2px solid #a8e1fb;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
.mdc-chip.device_id:not(.add) .mdc-chip__icon--leading,
|
||||
@@ -821,21 +668,13 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
background: #a8e1fb;
|
||||
}
|
||||
.mdc-chip.entity_id:not(.add) {
|
||||
border: 1px solid #d2e7b9;
|
||||
border: 2px solid #d2e7b9;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
.mdc-chip.entity_id:not(.add) .mdc-chip__icon--leading,
|
||||
.mdc-chip.entity_id.add {
|
||||
background: #d2e7b9;
|
||||
}
|
||||
.mdc-chip.label_id:not(.add) {
|
||||
border: 1px solid var(--color, #e0e0e0);
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
.mdc-chip.label_id:not(.add) .mdc-chip__icon--leading,
|
||||
.mdc-chip.label_id.add {
|
||||
background: var(--background-color, #e0e0e0);
|
||||
}
|
||||
.mdc-chip:hover {
|
||||
z-index: 5;
|
||||
}
|
||||
@@ -851,7 +690,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
ha-entity-picker,
|
||||
ha-device-picker,
|
||||
ha-area-floor-picker {
|
||||
ha-area-picker {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
@@ -1,96 +0,0 @@
|
||||
import { mdiMagnify } from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-outlined-text-field";
|
||||
import type { HaOutlinedTextField } from "./ha-outlined-text-field";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("search-input-outlined")
|
||||
class SearchInputOutlined extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public filter?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public suffix = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public autofocus = false;
|
||||
|
||||
@property({ type: String })
|
||||
public label?: string;
|
||||
|
||||
@property({ type: String })
|
||||
public placeholder?: string;
|
||||
|
||||
public focus() {
|
||||
this._input?.focus();
|
||||
}
|
||||
|
||||
@query("ha-outlined-text-field", true) private _input!: HaOutlinedTextField;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const placeholder =
|
||||
this.placeholder || this.hass.localize("ui.common.search");
|
||||
|
||||
return html`
|
||||
<ha-outlined-text-field
|
||||
.autofocus=${this.autofocus}
|
||||
.aria-label=${this.label || this.hass.localize("ui.common.search")}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.filter || ""}
|
||||
icon
|
||||
.iconTrailing=${this.filter || this.suffix}
|
||||
@input=${this._filterInputChanged}
|
||||
dense
|
||||
>
|
||||
<slot name="prefix" slot="leading-icon">
|
||||
<ha-svg-icon
|
||||
tabindex="-1"
|
||||
class="prefix"
|
||||
.path=${mdiMagnify}
|
||||
></ha-svg-icon>
|
||||
</slot>
|
||||
</ha-outlined-text-field>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _filterChanged(value: string) {
|
||||
fireEvent(this, "value-changed", { value: String(value) });
|
||||
}
|
||||
|
||||
private async _filterInputChanged(e) {
|
||||
this._filterChanged(e.target.value);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
/* For iOS */
|
||||
z-index: 0;
|
||||
}
|
||||
ha-outlined-text-field {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
ha-svg-icon,
|
||||
ha-icon-button {
|
||||
display: flex;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-svg-icon {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"search-input-outlined": SearchInputOutlined;
|
||||
}
|
||||
}
|
@@ -7,11 +7,9 @@ export { subscribeAreaRegistry } from "./ws-area_registry";
|
||||
|
||||
export interface AreaRegistryEntry {
|
||||
area_id: string;
|
||||
floor_id: string | null;
|
||||
name: string;
|
||||
picture: string | null;
|
||||
icon: string | null;
|
||||
labels: string[];
|
||||
aliases: string[];
|
||||
}
|
||||
|
||||
@@ -25,11 +23,9 @@ export interface AreaDeviceLookup {
|
||||
|
||||
export interface AreaRegistryEntryMutableParams {
|
||||
name: string;
|
||||
floor_id?: string | null;
|
||||
picture?: string | null;
|
||||
icon?: string | null;
|
||||
aliases?: string[];
|
||||
labels?: string[];
|
||||
}
|
||||
|
||||
export const createAreaRegistryEntry = (
|
||||
|
@@ -1,86 +0,0 @@
|
||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
|
||||
export interface CategoryRegistryEntry {
|
||||
category_id: string;
|
||||
name: string;
|
||||
icon: string | null;
|
||||
}
|
||||
|
||||
export interface CategoryRegistryEntryMutableParams {
|
||||
name: string;
|
||||
icon?: string | null;
|
||||
}
|
||||
|
||||
export const fetchCategoryRegistry = (conn: Connection, scope: string) =>
|
||||
conn
|
||||
.sendMessagePromise<CategoryRegistryEntry[]>({
|
||||
type: "config/category_registry/list",
|
||||
scope,
|
||||
})
|
||||
.then((categories) =>
|
||||
categories.sort((ent1, ent2) => stringCompare(ent1.name, ent2.name))
|
||||
);
|
||||
|
||||
export const subscribeCategoryRegistry = (
|
||||
conn: Connection,
|
||||
scope: string,
|
||||
onChange: (floors: CategoryRegistryEntry[]) => void
|
||||
) =>
|
||||
createCollection<CategoryRegistryEntry[]>(
|
||||
`_categoryRegistry_${scope}`,
|
||||
(conn2: Connection) => fetchCategoryRegistry(conn2, scope),
|
||||
(conn2: Connection, store: Store<CategoryRegistryEntry[]>) =>
|
||||
conn2.subscribeEvents(
|
||||
debounce(
|
||||
() =>
|
||||
fetchCategoryRegistry(conn2, scope).then(
|
||||
(categories: CategoryRegistryEntry[]) =>
|
||||
store.setState(categories, true)
|
||||
),
|
||||
500,
|
||||
true
|
||||
),
|
||||
"category_registry_updated"
|
||||
),
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
||||
export const createCategoryRegistryEntry = (
|
||||
hass: HomeAssistant,
|
||||
scope: string,
|
||||
values: CategoryRegistryEntryMutableParams
|
||||
) =>
|
||||
hass.callWS<CategoryRegistryEntry>({
|
||||
type: "config/category_registry/create",
|
||||
scope,
|
||||
...values,
|
||||
});
|
||||
|
||||
export const updateCategoryRegistryEntry = (
|
||||
hass: HomeAssistant,
|
||||
scope: string,
|
||||
category_id: string,
|
||||
updates: Partial<CategoryRegistryEntryMutableParams>
|
||||
) =>
|
||||
hass.callWS<CategoryRegistryEntry>({
|
||||
type: "config/category_registry/update",
|
||||
scope,
|
||||
category_id,
|
||||
...updates,
|
||||
});
|
||||
|
||||
export const deleteCategoryRegistryEntry = (
|
||||
hass: HomeAssistant,
|
||||
scope: string,
|
||||
category_id: string
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "config/category_registry/delete",
|
||||
scope,
|
||||
category_id,
|
||||
});
|
@@ -1,4 +1,5 @@
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
export interface CloudTTSInfo {
|
||||
@@ -26,21 +27,27 @@ export const getCloudTtsLanguages = (info?: CloudTTSInfo) => {
|
||||
return languages;
|
||||
};
|
||||
|
||||
export const getCloudTtsSupportedVoices = (
|
||||
export const getCloudTtsSupportedGenders = (
|
||||
language: string,
|
||||
info: CloudTTSInfo | undefined
|
||||
info: CloudTTSInfo | undefined,
|
||||
localize: LocalizeFunc
|
||||
) => {
|
||||
const voices: Array<string> = [];
|
||||
const genders: Array<[string, string]> = [];
|
||||
|
||||
if (!info) {
|
||||
return voices;
|
||||
return genders;
|
||||
}
|
||||
|
||||
for (const [curLang, voice] of info.languages) {
|
||||
for (const [curLang, gender] of info.languages) {
|
||||
if (curLang === language) {
|
||||
voices.push(voice);
|
||||
genders.push([
|
||||
gender,
|
||||
gender === "male" || gender === "female"
|
||||
? localize(`ui.components.media-browser.tts.gender_${gender}`)
|
||||
: gender,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return voices.sort((a, b) => caseInsensitiveStringCompare(a, b));
|
||||
return genders.sort((a, b) => caseInsensitiveStringCompare(a[1], b[1]));
|
||||
};
|
||||
|
@@ -20,7 +20,6 @@ export interface DeviceRegistryEntry {
|
||||
manufacturer: string | null;
|
||||
model: string | null;
|
||||
name: string | null;
|
||||
labels: string[];
|
||||
sw_version: string | null;
|
||||
hw_version: string | null;
|
||||
serial_number: string | null;
|
||||
@@ -44,7 +43,6 @@ export interface DeviceRegistryEntryMutableParams {
|
||||
area_id?: string | null;
|
||||
name_by_user?: string | null;
|
||||
disabled_by?: string | null;
|
||||
labels?: string[];
|
||||
}
|
||||
|
||||
export const fallbackDeviceName = (
|
||||
@@ -142,7 +140,7 @@ export const getDeviceEntityDisplayLookup = (
|
||||
|
||||
export const getDeviceIntegrationLookup = (
|
||||
entitySources: EntitySources,
|
||||
entities: EntityRegistryDisplayEntry[] | EntityRegistryEntry[]
|
||||
entities: EntityRegistryDisplayEntry[]
|
||||
): Record<string, string[]> => {
|
||||
const deviceIntegrations: Record<string, string[]> = {};
|
||||
|
||||
|
@@ -11,11 +11,7 @@ import {
|
||||
isLastDayOfMonth,
|
||||
} from "date-fns/esm";
|
||||
import { Collection, getCollection } from "home-assistant-js-websocket";
|
||||
import {
|
||||
calcDate,
|
||||
calcDateProperty,
|
||||
calcDateDifferenceProperty,
|
||||
} from "../common/datetime/calc_date";
|
||||
import { calcDate, calcDateProperty } from "../common/datetime/calc_date";
|
||||
import { formatTime24h } from "../common/datetime/format_time";
|
||||
import { groupBy } from "../common/util/group-by";
|
||||
import { HomeAssistant } from "../types";
|
||||
@@ -447,12 +443,12 @@ const getEnergyData = async (
|
||||
addMonths,
|
||||
hass.locale,
|
||||
hass.config,
|
||||
-(calcDateDifferenceProperty(
|
||||
-(calcDateProperty(
|
||||
end || new Date(),
|
||||
start,
|
||||
differenceInMonths,
|
||||
hass.locale,
|
||||
hass.config
|
||||
hass.config,
|
||||
start
|
||||
) as number) - 1
|
||||
);
|
||||
} else {
|
||||
|
@@ -10,7 +10,7 @@ import { computeDomain } from "../common/entity/compute_domain";
|
||||
|
||||
export { subscribeEntityRegistryDisplay } from "./ws-entity_registry_display";
|
||||
|
||||
type EntityCategory = "config" | "diagnostic";
|
||||
type entityCategory = "config" | "diagnostic";
|
||||
|
||||
export interface EntityRegistryDisplayEntry {
|
||||
entity_id: string;
|
||||
@@ -18,9 +18,8 @@ export interface EntityRegistryDisplayEntry {
|
||||
icon?: string;
|
||||
device_id?: string;
|
||||
area_id?: string;
|
||||
labels: string[];
|
||||
hidden?: boolean;
|
||||
entity_category?: EntityCategory;
|
||||
entity_category?: entityCategory;
|
||||
translation_key?: string;
|
||||
platform?: string;
|
||||
display_precision?: number;
|
||||
@@ -31,7 +30,6 @@ export interface EntityRegistryDisplayEntryResponse {
|
||||
ei: string;
|
||||
di?: string;
|
||||
ai?: string;
|
||||
lb: string[];
|
||||
ec?: number;
|
||||
en?: string;
|
||||
ic?: string;
|
||||
@@ -40,7 +38,7 @@ export interface EntityRegistryDisplayEntryResponse {
|
||||
hb?: boolean;
|
||||
dp?: number;
|
||||
}[];
|
||||
entity_categories: Record<number, EntityCategory>;
|
||||
entity_categories: Record<number, entityCategory>;
|
||||
}
|
||||
|
||||
export interface EntityRegistryEntry {
|
||||
@@ -52,16 +50,14 @@ export interface EntityRegistryEntry {
|
||||
config_entry_id: string | null;
|
||||
device_id: string | null;
|
||||
area_id: string | null;
|
||||
labels: string[];
|
||||
disabled_by: "user" | "device" | "integration" | "config_entry" | null;
|
||||
hidden_by: Exclude<EntityRegistryEntry["disabled_by"], "config_entry">;
|
||||
entity_category: EntityCategory | null;
|
||||
entity_category: entityCategory | null;
|
||||
has_entity_name: boolean;
|
||||
original_name?: string;
|
||||
unique_id: string;
|
||||
translation_key?: string;
|
||||
options: EntityRegistryOptions | null;
|
||||
categories: { [scope: string]: string };
|
||||
}
|
||||
|
||||
export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
|
||||
@@ -137,8 +133,6 @@ export interface EntityRegistryEntryUpdateParams {
|
||||
| WeatherEntityOptions
|
||||
| LightEntityOptions;
|
||||
aliases?: string[];
|
||||
labels?: string[];
|
||||
categories?: { [scope: string]: string | null };
|
||||
}
|
||||
|
||||
const batteryPriorities = ["sensor", "binary_sensor"];
|
||||
|
@@ -1,133 +0,0 @@
|
||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { AreaRegistryEntry } from "./area_registry";
|
||||
|
||||
export { subscribeAreaRegistry } from "./ws-area_registry";
|
||||
|
||||
export interface FloorRegistryEntry {
|
||||
floor_id: string;
|
||||
name: string;
|
||||
level: number | null;
|
||||
icon: string | null;
|
||||
aliases: string[];
|
||||
}
|
||||
|
||||
export interface FloorAreaLookup {
|
||||
[floorId: string]: AreaRegistryEntry[];
|
||||
}
|
||||
|
||||
export interface FloorRegistryEntryMutableParams {
|
||||
name: string;
|
||||
level?: number | null;
|
||||
icon?: string | null;
|
||||
aliases?: string[];
|
||||
}
|
||||
|
||||
const fetchFloorRegistry = (conn: Connection) =>
|
||||
conn
|
||||
.sendMessagePromise({
|
||||
type: "config/floor_registry/list",
|
||||
})
|
||||
.then((floors) =>
|
||||
(floors as FloorRegistryEntry[]).sort((ent1, ent2) => {
|
||||
if (ent1.level !== ent2.level) {
|
||||
return (ent1.level ?? 9999) - (ent2.level ?? 9999);
|
||||
}
|
||||
return stringCompare(ent1.name, ent2.name);
|
||||
})
|
||||
);
|
||||
|
||||
const subscribeFloorRegistryUpdates = (
|
||||
conn: Connection,
|
||||
store: Store<FloorRegistryEntry[]>
|
||||
) =>
|
||||
conn.subscribeEvents(
|
||||
debounce(
|
||||
() =>
|
||||
fetchFloorRegistry(conn).then((areas: FloorRegistryEntry[]) =>
|
||||
store.setState(areas, true)
|
||||
),
|
||||
500,
|
||||
true
|
||||
),
|
||||
"floor_registry_updated"
|
||||
);
|
||||
|
||||
export const subscribeFloorRegistry = (
|
||||
conn: Connection,
|
||||
onChange: (floors: FloorRegistryEntry[]) => void
|
||||
) =>
|
||||
createCollection<FloorRegistryEntry[]>(
|
||||
"_floorRegistry",
|
||||
fetchFloorRegistry,
|
||||
subscribeFloorRegistryUpdates,
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
||||
export const createFloorRegistryEntry = (
|
||||
hass: HomeAssistant,
|
||||
values: FloorRegistryEntryMutableParams
|
||||
) =>
|
||||
hass.callWS<FloorRegistryEntry>({
|
||||
type: "config/floor_registry/create",
|
||||
...values,
|
||||
});
|
||||
|
||||
export const updateFloorRegistryEntry = (
|
||||
hass: HomeAssistant,
|
||||
floorId: string,
|
||||
updates: Partial<FloorRegistryEntryMutableParams>
|
||||
) =>
|
||||
hass.callWS<AreaRegistryEntry>({
|
||||
type: "config/floor_registry/update",
|
||||
floor_id: floorId,
|
||||
...updates,
|
||||
});
|
||||
|
||||
export const deleteFloorRegistryEntry = (
|
||||
hass: HomeAssistant,
|
||||
floorId: string
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "config/floor_registry/delete",
|
||||
floor_id: floorId,
|
||||
});
|
||||
|
||||
export const getFloorAreaLookup = (
|
||||
areas: AreaRegistryEntry[]
|
||||
): FloorAreaLookup => {
|
||||
const floorAreaLookup: FloorAreaLookup = {};
|
||||
for (const area of areas) {
|
||||
if (!area.floor_id) {
|
||||
continue;
|
||||
}
|
||||
if (!(area.floor_id in floorAreaLookup)) {
|
||||
floorAreaLookup[area.floor_id] = [];
|
||||
}
|
||||
floorAreaLookup[area.floor_id].push(area);
|
||||
}
|
||||
return floorAreaLookup;
|
||||
};
|
||||
|
||||
export const floorCompare =
|
||||
(entries?: FloorRegistryEntry[], order?: string[]) =>
|
||||
(a: string, b: string) => {
|
||||
const indexA = order ? order.indexOf(a) : -1;
|
||||
const indexB = order ? order.indexOf(b) : -1;
|
||||
if (indexA === -1 && indexB === -1) {
|
||||
const nameA = entries?.[a]?.name ?? a;
|
||||
const nameB = entries?.[b]?.name ?? b;
|
||||
return stringCompare(nameA, nameB);
|
||||
}
|
||||
if (indexA === -1) {
|
||||
return 1;
|
||||
}
|
||||
if (indexB === -1) {
|
||||
return -1;
|
||||
}
|
||||
return indexA - indexB;
|
||||
};
|
@@ -1,86 +0,0 @@
|
||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
|
||||
export interface LabelRegistryEntry {
|
||||
label_id: string;
|
||||
name: string;
|
||||
icon: string | null;
|
||||
color: string | null;
|
||||
}
|
||||
|
||||
export interface LabelRegistryEntryMutableParams {
|
||||
name: string;
|
||||
icon?: string | null;
|
||||
color?: string | null;
|
||||
}
|
||||
|
||||
export const fetchLabelRegistry = (conn: Connection) =>
|
||||
conn
|
||||
.sendMessagePromise({
|
||||
type: "config/label_registry/list",
|
||||
})
|
||||
.then((labels) =>
|
||||
(labels as LabelRegistryEntry[]).sort((ent1, ent2) =>
|
||||
stringCompare(ent1.name, ent2.name)
|
||||
)
|
||||
);
|
||||
|
||||
export const subscribeLabelRegistryUpdates = (
|
||||
conn: Connection,
|
||||
store: Store<LabelRegistryEntry[]>
|
||||
) =>
|
||||
conn.subscribeEvents(
|
||||
debounce(
|
||||
() =>
|
||||
fetchLabelRegistry(conn).then((labels: LabelRegistryEntry[]) =>
|
||||
store.setState(labels, true)
|
||||
),
|
||||
500,
|
||||
true
|
||||
),
|
||||
"label_registry_updated"
|
||||
);
|
||||
|
||||
export const subscribeLabelRegistry = (
|
||||
conn: Connection,
|
||||
onChange: (labels: LabelRegistryEntry[]) => void
|
||||
) =>
|
||||
createCollection<LabelRegistryEntry[]>(
|
||||
"_labelRegistry",
|
||||
fetchLabelRegistry,
|
||||
subscribeLabelRegistryUpdates,
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
||||
export const createLabelRegistryEntry = (
|
||||
hass: HomeAssistant,
|
||||
values: LabelRegistryEntryMutableParams
|
||||
) =>
|
||||
hass.callWS<LabelRegistryEntry>({
|
||||
type: "config/label_registry/create",
|
||||
...values,
|
||||
});
|
||||
|
||||
export const updateLabelRegistryEntry = (
|
||||
hass: HomeAssistant,
|
||||
labelId: string,
|
||||
updates: Partial<LabelRegistryEntryMutableParams>
|
||||
) =>
|
||||
hass.callWS<LabelRegistryEntry>({
|
||||
type: "config/label_registry/update",
|
||||
label_id: labelId,
|
||||
...updates,
|
||||
});
|
||||
|
||||
export const deleteLabelRegistryEntry = (
|
||||
hass: HomeAssistant,
|
||||
labelId: string
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "config/label_registry/delete",
|
||||
label_id: labelId,
|
||||
});
|
@@ -18,7 +18,6 @@ export interface LovelaceBaseViewConfig {
|
||||
visible?: boolean | ShowViewConfig[];
|
||||
subview?: boolean;
|
||||
back_path?: string;
|
||||
max_columns?: number; // Only used for section view, it should move to a section view config type when the views will have dedicated editor.
|
||||
}
|
||||
|
||||
export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
|
||||
|
@@ -42,8 +42,6 @@ const targetStruct = object({
|
||||
entity_id: optional(union([string(), array(string())])),
|
||||
device_id: optional(union([string(), array(string())])),
|
||||
area_id: optional(union([string(), array(string())])),
|
||||
floor_id: optional(union([string(), array(string())])),
|
||||
label_id: optional(union([string(), array(string())])),
|
||||
});
|
||||
|
||||
export const serviceActionStruct: Describe<ServiceAction> = assign(
|
||||
|
@@ -26,7 +26,6 @@ export type ItemType =
|
||||
| "config_entry"
|
||||
| "device"
|
||||
| "entity"
|
||||
| "floor"
|
||||
| "group"
|
||||
| "scene"
|
||||
| "script"
|
||||
|
@@ -8,10 +8,7 @@ import {
|
||||
DeviceRegistryEntry,
|
||||
getDeviceIntegrationLookup,
|
||||
} from "./device_registry";
|
||||
import {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "./entity_registry";
|
||||
import { EntityRegistryDisplayEntry } from "./entity_registry";
|
||||
import { EntitySources } from "./entity_sources";
|
||||
|
||||
export type Selector =
|
||||
@@ -31,14 +28,12 @@ export type Selector =
|
||||
| DateSelector
|
||||
| DateTimeSelector
|
||||
| DeviceSelector
|
||||
| FloorSelector
|
||||
| LegacyDeviceSelector
|
||||
| DurationSelector
|
||||
| EntitySelector
|
||||
| LegacyEntitySelector
|
||||
| FileSelector
|
||||
| IconSelector
|
||||
| LabelSelector
|
||||
| LanguageSelector
|
||||
| LocationSelector
|
||||
| MediaSelector
|
||||
@@ -171,14 +166,6 @@ export interface DeviceSelector {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface FloorSelector {
|
||||
floor: {
|
||||
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
|
||||
device?: DeviceSelectorFilter | readonly DeviceSelectorFilter[];
|
||||
multiple?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface LegacyDeviceSelector {
|
||||
device: DeviceSelector["device"] & {
|
||||
/**
|
||||
@@ -255,12 +242,6 @@ export interface IconSelector {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface LabelSelector {
|
||||
label: {
|
||||
multiple?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LanguageSelector {
|
||||
language: {
|
||||
languages?: string[];
|
||||
@@ -440,95 +421,9 @@ export interface UiActionSelector {
|
||||
|
||||
export interface UiColorSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
ui_color: { default_color?: boolean } | null;
|
||||
ui_color: {} | null;
|
||||
}
|
||||
|
||||
export const expandLabelTarget = (
|
||||
hass: HomeAssistant,
|
||||
labelId: string,
|
||||
areas: HomeAssistant["areas"],
|
||||
devices: HomeAssistant["devices"],
|
||||
entities: HomeAssistant["entities"],
|
||||
targetSelector: TargetSelector,
|
||||
entitySources?: EntitySources
|
||||
) => {
|
||||
const newEntities: string[] = [];
|
||||
const newDevices: string[] = [];
|
||||
const newAreas: string[] = [];
|
||||
|
||||
Object.values(areas).forEach((area) => {
|
||||
if (
|
||||
area.labels.includes(labelId) &&
|
||||
areaMeetsTargetSelector(
|
||||
hass,
|
||||
entities,
|
||||
devices,
|
||||
area.area_id,
|
||||
targetSelector,
|
||||
entitySources
|
||||
)
|
||||
) {
|
||||
newAreas.push(area.area_id);
|
||||
}
|
||||
});
|
||||
|
||||
Object.values(devices).forEach((device) => {
|
||||
if (
|
||||
device.labels.includes(labelId) &&
|
||||
deviceMeetsTargetSelector(
|
||||
hass,
|
||||
Object.values(entities),
|
||||
device,
|
||||
targetSelector,
|
||||
entitySources
|
||||
)
|
||||
) {
|
||||
newDevices.push(device.id);
|
||||
}
|
||||
});
|
||||
|
||||
Object.values(entities).forEach((entity) => {
|
||||
if (
|
||||
entity.labels.includes(labelId) &&
|
||||
entityMeetsTargetSelector(
|
||||
hass.states[entity.entity_id],
|
||||
targetSelector,
|
||||
entitySources
|
||||
)
|
||||
) {
|
||||
newEntities.push(entity.entity_id);
|
||||
}
|
||||
});
|
||||
|
||||
return { areas: newAreas, devices: newDevices, entities: newEntities };
|
||||
};
|
||||
|
||||
export const expandFloorTarget = (
|
||||
hass: HomeAssistant,
|
||||
floorId: string,
|
||||
areas: HomeAssistant["areas"],
|
||||
targetSelector: TargetSelector,
|
||||
entitySources?: EntitySources
|
||||
) => {
|
||||
const newAreas: string[] = [];
|
||||
Object.values(areas).forEach((area) => {
|
||||
if (
|
||||
area.floor_id === floorId &&
|
||||
areaMeetsTargetSelector(
|
||||
hass,
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
area.area_id,
|
||||
targetSelector,
|
||||
entitySources
|
||||
)
|
||||
) {
|
||||
newAreas.push(area.area_id);
|
||||
}
|
||||
});
|
||||
return { areas: newAreas };
|
||||
};
|
||||
|
||||
export const expandAreaTarget = (
|
||||
hass: HomeAssistant,
|
||||
areaId: string,
|
||||
@@ -634,7 +529,7 @@ export const areaMeetsTargetSelector = (
|
||||
|
||||
export const deviceMeetsTargetSelector = (
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryDisplayEntry[] | EntityRegistryEntry[],
|
||||
entityRegistry: EntityRegistryDisplayEntry[],
|
||||
device: DeviceRegistryEntry,
|
||||
targetSelector: TargetSelector,
|
||||
entitySources?: EntitySources
|
||||
|
@@ -189,8 +189,8 @@ class LightColorTempPicker extends LitElement {
|
||||
height: 45vh;
|
||||
max-height: 320px;
|
||||
min-height: 200px;
|
||||
--control-slider-thickness: 130px;
|
||||
--control-slider-border-radius: 48px;
|
||||
--control-slider-thickness: 100px;
|
||||
--control-slider-border-radius: 24px;
|
||||
--control-slider-color: var(--primary-color);
|
||||
--control-slider-background: -webkit-linear-gradient(
|
||||
top,
|
||||
|
@@ -1,12 +1,10 @@
|
||||
import { mdiCheck } from "@mdi/js";
|
||||
import { mdiDoorOpen, mdiLock, mdiLockOff } from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-attributes";
|
||||
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";
|
||||
@@ -20,49 +18,14 @@ import type { HomeAssistant } from "../../../types";
|
||||
import "../components/ha-more-info-state-header";
|
||||
import { moreInfoControlStyle } from "../components/more-info-control-style";
|
||||
|
||||
const CONFIRM_TIMEOUT_SECOND = 5;
|
||||
const OPENED_TIMEOUT_SECOND = 3;
|
||||
|
||||
type ButtonState = "normal" | "confirm" | "success";
|
||||
|
||||
@customElement("more-info-lock")
|
||||
class MoreInfoLock extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?: LockEntity;
|
||||
|
||||
@state() public _buttonState: ButtonState = "normal";
|
||||
|
||||
private _buttonTimeout?: number;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
callProtectedLockService(this, this.hass, this.stateObj!, "open");
|
||||
|
||||
this._setButtonState("success", OPENED_TIMEOUT_SECOND);
|
||||
}
|
||||
|
||||
private _resetButtonState() {
|
||||
this._setButtonState("normal");
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._resetButtonState();
|
||||
}
|
||||
|
||||
private async _lock() {
|
||||
@@ -82,7 +45,7 @@ class MoreInfoLock extends LitElement {
|
||||
|
||||
const color = stateColorCss(this.stateObj);
|
||||
const style = {
|
||||
"--state-color": color,
|
||||
"--icon-color": color,
|
||||
};
|
||||
|
||||
const isJammed = this.stateObj.state === "jammed";
|
||||
@@ -93,70 +56,74 @@ class MoreInfoLock extends LitElement {
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-more-info-state-header>
|
||||
<div class="controls" style=${styleMap(style)}>
|
||||
${this.stateObj.state === "jammed"
|
||||
? html`
|
||||
<div class="status">
|
||||
<span></span>
|
||||
<div class="icon">
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-state-icon>
|
||||
${
|
||||
this.stateObj.state === "jammed"
|
||||
? html`
|
||||
<div class="status">
|
||||
<span></span>
|
||||
<div class="icon">
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-state-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<ha-state-control-lock-toggle
|
||||
@lock-service-called=${this._resetButtonState}
|
||||
.stateObj=${this.stateObj}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</ha-state-control-lock-toggle>
|
||||
`}
|
||||
${supportsOpen
|
||||
? html`
|
||||
<div class="buttons">
|
||||
${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
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
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>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<div>
|
||||
${isJammed
|
||||
? html`
|
||||
<ha-control-button-group class="jammed">
|
||||
<ha-control-button @click=${this._unlock}>
|
||||
${this.hass.localize("ui.card.lock.unlock")}
|
||||
</ha-control-button>
|
||||
<ha-control-button @click=${this._lock}>
|
||||
${this.hass.localize("ui.card.lock.lock")}
|
||||
</ha-control-button>
|
||||
</ha-control-button-group>
|
||||
`
|
||||
: nothing}
|
||||
<ha-attributes
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
extra-filters="code_format"
|
||||
></ha-attributes>
|
||||
`
|
||||
: html`
|
||||
<ha-state-control-lock-toggle
|
||||
.stateObj=${this.stateObj}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
</ha-state-control-lock-toggle>
|
||||
`
|
||||
}
|
||||
${
|
||||
supportsOpen || isJammed
|
||||
? html`
|
||||
<div class="buttons">
|
||||
${supportsOpen
|
||||
? html`
|
||||
<ha-outlined-icon-button
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
.title=${this.hass.localize("ui.card.lock.open")}
|
||||
.ariaLabel=${this.hass.localize("ui.card.lock.open")}
|
||||
@click=${this._open}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDoorOpen}></ha-svg-icon>
|
||||
</ha-outlined-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
${isJammed
|
||||
? html`
|
||||
<ha-outlined-icon-button
|
||||
.title=${this.hass.localize("ui.card.lock.lock")}
|
||||
.ariaLabel=${this.hass.localize("ui.card.lock.lock")}
|
||||
@click=${this._lock}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiLock}></ha-svg-icon>
|
||||
</ha-outlined-icon-button>
|
||||
<ha-outlined-icon-button
|
||||
.title=${this.hass.localize("ui.card.lock.unlock")}
|
||||
.ariaLabel=${this.hass.localize(
|
||||
"ui.card.lock.unlock"
|
||||
)}
|
||||
@click=${this._unlock}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiLockOff}></ha-svg-icon>
|
||||
</ha-outlined-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<ha-attributes
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
extra-filters="code_format"
|
||||
></ha-attributes>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -164,36 +131,6 @@ class MoreInfoLock extends LitElement {
|
||||
return [
|
||||
moreInfoControlStyle,
|
||||
css`
|
||||
ha-control-button {
|
||||
font-size: 14px;
|
||||
height: 60px;
|
||||
--control-button-border-radius: 24px;
|
||||
}
|
||||
.open-button {
|
||||
width: 100px;
|
||||
--control-button-background-color: var(--state-color);
|
||||
}
|
||||
.open-button.confirm {
|
||||
--control-button-background-color: var(--warning-color);
|
||||
}
|
||||
.open-success {
|
||||
line-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
font-weight: 500;
|
||||
color: var(--success-color);
|
||||
}
|
||||
ha-control-button-group.jammed {
|
||||
--control-button-group-thickness: 60px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-control-button-group + ha-attributes:not([empty]) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
@@ -218,7 +155,7 @@ class MoreInfoLock extends LitElement {
|
||||
position: relative;
|
||||
--mdc-icon-size: 80px;
|
||||
animation: pulse 1s infinite;
|
||||
color: var(--state-color);
|
||||
color: var(--icon-color);
|
||||
border-radius: 50%;
|
||||
width: 144px;
|
||||
height: 144px;
|
||||
@@ -234,7 +171,7 @@ class MoreInfoLock extends LitElement {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 50%;
|
||||
background-color: var(--state-color);
|
||||
background-color: var(--icon-color);
|
||||
transition: background-color 180ms ease-in-out;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
@@ -77,8 +77,6 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_VIEW: View = "info";
|
||||
|
||||
@customElement("ha-more-info-dialog")
|
||||
export class MoreInfoDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -87,9 +85,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
|
||||
@state() private _entityId?: string | null;
|
||||
|
||||
@state() private _currView: View = DEFAULT_VIEW;
|
||||
|
||||
@state() private _initialView: View = DEFAULT_VIEW;
|
||||
@state() private _currView: View = "info";
|
||||
|
||||
@state() private _childView?: ChildView;
|
||||
|
||||
@@ -106,8 +102,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
this._currView = params.view || DEFAULT_VIEW;
|
||||
this._initialView = params.view || DEFAULT_VIEW;
|
||||
this._currView = params.view || "info";
|
||||
this._childView = undefined;
|
||||
this.large = false;
|
||||
this._loadEntityRegistryEntry();
|
||||
@@ -132,7 +127,6 @@ export class MoreInfoDialog extends LitElement {
|
||||
this._entry = undefined;
|
||||
this._childView = undefined;
|
||||
this._infoEditMode = false;
|
||||
this._initialView = DEFAULT_VIEW;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -189,15 +183,10 @@ export class MoreInfoDialog extends LitElement {
|
||||
if (this._childView) {
|
||||
this._childView = undefined;
|
||||
} else {
|
||||
this.setView(this._initialView);
|
||||
this.setView("info");
|
||||
}
|
||||
}
|
||||
|
||||
private _resetInitialView() {
|
||||
this._initialView = DEFAULT_VIEW;
|
||||
this.setView(DEFAULT_VIEW);
|
||||
}
|
||||
|
||||
private _goToHistory() {
|
||||
this.setView("history");
|
||||
}
|
||||
@@ -273,10 +262,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
|
||||
const title = this._childView?.viewTitle ?? name;
|
||||
|
||||
const isDefaultView = this._currView === DEFAULT_VIEW && !this._childView;
|
||||
const isSpecificInitialView =
|
||||
this._initialView !== DEFAULT_VIEW && !this._childView;
|
||||
const showCloseIcon = isDefaultView || isSpecificInitialView;
|
||||
const isInfoView = this._currView === "info" && !this._childView;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
@@ -288,7 +274,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
flexContent
|
||||
>
|
||||
<ha-dialog-header slot="heading">
|
||||
${showCloseIcon
|
||||
${isInfoView
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
@@ -311,7 +297,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
<span slot="title" .title=${title} @click=${this._enlarge}>
|
||||
${title}
|
||||
</span>
|
||||
${isDefaultView
|
||||
${isInfoView
|
||||
? html`
|
||||
${this.shouldShowHistory(domain)
|
||||
? html`
|
||||
@@ -421,34 +407,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
: isSpecificInitialView
|
||||
? html`
|
||||
<ha-button-menu
|
||||
corner="BOTTOM_END"
|
||||
menuCorner="END"
|
||||
slot="actionItems"
|
||||
@closed=${stopPropagation}
|
||||
fixed
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
@request-selected=${this._resetInitialView}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.more_info_control.info")}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiInformationOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
`
|
||||
: nothing}
|
||||
: nothing}
|
||||
</ha-dialog-header>
|
||||
<div
|
||||
class="content"
|
||||
|
@@ -776,8 +776,8 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
float: var(--float-end);
|
||||
text-align: right;
|
||||
border-bottom-right-radius: 0px;
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-primary-color);
|
||||
background-color: var(--light-primary-color);
|
||||
color: var(--text-light-primary-color, var(--primary-text-color));
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
@@ -787,8 +787,8 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
margin-inline-start: initial;
|
||||
float: var(--float-start);
|
||||
border-bottom-left-radius: 0px;
|
||||
background-color: var(--secondary-background-color);
|
||||
color: var(--primary-text-color);
|
||||
background-color: var(--primary-color);
|
||||
color: var(--text-primary-color);
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
|
@@ -142,12 +142,9 @@ class HassSubpage extends LitElement {
|
||||
right: calc(16px + env(safe-area-inset-right));
|
||||
inset-inline-end: calc(16px + env(safe-area-inset-right));
|
||||
inset-inline-start: initial;
|
||||
|
||||
bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
:host([narrow]) #fab.tabs {
|
||||
bottom: calc(84px + env(safe-area-inset-bottom));
|
||||
|
@@ -1,42 +1,15 @@
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/web/divider/divider";
|
||||
import {
|
||||
mdiArrowDown,
|
||||
mdiArrowUp,
|
||||
mdiClose,
|
||||
mdiFilterVariant,
|
||||
mdiFilterVariantRemove,
|
||||
mdiFormatListChecks,
|
||||
mdiMenuDown,
|
||||
} from "@mdi/js";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/chips/ha-assist-chip";
|
||||
import "../components/chips/ha-filter-chip";
|
||||
import "../components/data-table/ha-data-table";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
DataTableRowData,
|
||||
HaDataTable,
|
||||
SortingDirection,
|
||||
} from "../components/data-table/ha-data-table";
|
||||
import "../components/ha-button-menu-new";
|
||||
import "../components/ha-dialog";
|
||||
import { HaMenu } from "../components/ha-menu";
|
||||
import "../components/ha-menu-item";
|
||||
import "../components/search-input-outlined";
|
||||
import type { HomeAssistant, Route } from "../types";
|
||||
import "./hass-tabs-subpage";
|
||||
import type { PageNavigation } from "./hass-tabs-subpage";
|
||||
@@ -114,16 +87,22 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
@property() public searchLabel?: string;
|
||||
|
||||
/**
|
||||
* Number of active filters.
|
||||
* @type {Number}
|
||||
* List of strings that show what the data is currently filtered by.
|
||||
* @type {Array}
|
||||
*/
|
||||
@property({ type: Number }) public filters?;
|
||||
@property({ type: Array }) public activeFilters?;
|
||||
|
||||
/**
|
||||
* Number of current selections.
|
||||
* Text to how how many items are hidden.
|
||||
* @type {String}
|
||||
*/
|
||||
@property() public hiddenLabel?: string;
|
||||
|
||||
/**
|
||||
* How many items are hidden because of active filters.
|
||||
* @type {Number}
|
||||
*/
|
||||
@property({ type: Number }) public selected?;
|
||||
@property({ type: Number }) public numHidden = 0;
|
||||
|
||||
/**
|
||||
* What path to use when the back button is pressed.
|
||||
@@ -159,126 +138,57 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
@property({ attribute: false }) public tabs: PageNavigation[] = [];
|
||||
|
||||
/**
|
||||
* Show the filter menu.
|
||||
* Force hides the filter menu.
|
||||
* @type {Boolean}
|
||||
*/
|
||||
@property({ type: Boolean }) public hasFilters = false;
|
||||
|
||||
@property({ type: Boolean }) public showFilters = false;
|
||||
|
||||
@property() public initialGroupColumn?: string;
|
||||
|
||||
@state() private _sortColumn?: string;
|
||||
|
||||
@state() private _sortDirection: SortingDirection = null;
|
||||
|
||||
@state() private _groupColumn?: string;
|
||||
|
||||
@state() private _selectMode = false;
|
||||
@property({ type: Boolean }) public hideFilterMenu = false;
|
||||
|
||||
@query("ha-data-table", true) private _dataTable!: HaDataTable;
|
||||
|
||||
@query("#group-by-menu") private _groupByMenu!: HaMenu;
|
||||
|
||||
@query("#sort-by-menu") private _sortByMenu!: HaMenu;
|
||||
|
||||
private _showPaneController = new ResizeController(this, {
|
||||
callback: (entries) => entries[0]?.contentRect.width > 750,
|
||||
});
|
||||
|
||||
public clearSelection() {
|
||||
this._dataTable.clearSelection();
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
if (this.initialGroupColumn) {
|
||||
this._groupColumn = this.initialGroupColumn;
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleGroupBy() {
|
||||
this._groupByMenu.open = !this._groupByMenu.open;
|
||||
}
|
||||
|
||||
private _toggleSortBy() {
|
||||
this._sortByMenu.open = !this._sortByMenu.open;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const localize = this.localizeFunc || this.hass.localize;
|
||||
const showPane = this._showPaneController.value ?? !this.narrow;
|
||||
const filterButton = this.hasFilters
|
||||
? html`<div class="relative">
|
||||
<ha-assist-chip
|
||||
.label=${localize("ui.components.subpage-data-table.filters")}
|
||||
.active=${this.filters}
|
||||
@click=${this._toggleFilters}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiFilterVariant}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this.filters
|
||||
? html`<div class="badge">${this.filters}</div>`
|
||||
: nothing}
|
||||
</div>`
|
||||
: nothing;
|
||||
const hiddenLabel = this.numHidden
|
||||
? this.hiddenLabel ||
|
||||
this.hass.localize("ui.components.data-table.hidden", {
|
||||
number: this.numHidden,
|
||||
}) ||
|
||||
this.numHidden
|
||||
: undefined;
|
||||
|
||||
const selectModeBtn =
|
||||
this.selectable && !this._selectMode
|
||||
? html`<ha-assist-chip
|
||||
class="has-dropdown select-mode-chip"
|
||||
.active=${this._selectMode}
|
||||
@click=${this._enableSelectMode}
|
||||
.title=${localize(
|
||||
"ui.components.subpage-data-table.enter_selection_mode"
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiFormatListChecks}></ha-svg-icon>
|
||||
</ha-assist-chip>`
|
||||
: nothing;
|
||||
const filterInfo = this.activeFilters
|
||||
? html`${this.hass.localize("ui.components.data-table.filtering_by")}
|
||||
${this.activeFilters.join(", ")}
|
||||
${hiddenLabel ? `(${hiddenLabel})` : ""}`
|
||||
: hiddenLabel;
|
||||
|
||||
const searchBar = html`<search-input-outlined
|
||||
const headerToolbar = html`<search-input
|
||||
.hass=${this.hass}
|
||||
.filter=${this.filter}
|
||||
.suffix=${!this.narrow}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.searchLabel}
|
||||
.placeholder=${this.searchLabel}
|
||||
>
|
||||
</search-input-outlined>`;
|
||||
|
||||
const sortByMenu = Object.values(this.columns).find((col) => col.sortable)
|
||||
? html`
|
||||
<ha-assist-chip
|
||||
.label=${localize("ui.components.subpage-data-table.sort_by", {
|
||||
sortColumn: this._sortColumn
|
||||
? ` ${this.columns[this._sortColumn].title || this.columns[this._sortColumn].label}`
|
||||
: "",
|
||||
})}
|
||||
id="sort-by-anchor"
|
||||
@click=${this._toggleSortBy}
|
||||
${!this.narrow
|
||||
? html`<div
|
||||
class="filters"
|
||||
slot="suffix"
|
||||
@click=${this._preventDefault}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`
|
||||
: nothing;
|
||||
|
||||
const groupByMenu = Object.values(this.columns).find((col) => col.groupable)
|
||||
? html`
|
||||
<ha-assist-chip
|
||||
.label=${localize("ui.components.subpage-data-table.group_by", {
|
||||
groupColumn: this._groupColumn
|
||||
? ` ${this.columns[this._groupColumn].title || this.columns[this._groupColumn].label}`
|
||||
: "",
|
||||
})}
|
||||
id="group-by-anchor"
|
||||
@click=${this._toggleGroupBy}
|
||||
>
|
||||
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon
|
||||
></ha-assist-chip>
|
||||
`
|
||||
: nothing;
|
||||
${filterInfo
|
||||
? html`<div class="active-filters">
|
||||
${filterInfo}
|
||||
<mwc-button @click=${this._clearFilter}>
|
||||
${this.hass.localize("ui.components.data-table.clear")}
|
||||
</mwc-button>
|
||||
</div>`
|
||||
: ""}
|
||||
<slot name="filter-menu"></slot>
|
||||
</div>`
|
||||
: ""}
|
||||
</search-input>`;
|
||||
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
@@ -292,133 +202,34 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
.tabs=${this.tabs}
|
||||
.mainPage=${this.mainPage}
|
||||
.supervisor=${this.supervisor}
|
||||
.pane=${showPane && this.showFilters}
|
||||
@sorting-changed=${this._sortingChanged}
|
||||
>
|
||||
${this._selectMode
|
||||
? html`<div class="selection-bar" slot="toolbar">
|
||||
<div class="selection-controls">
|
||||
<ha-icon-button
|
||||
.path=${mdiClose}
|
||||
@click=${this._disableSelectMode}
|
||||
.label=${localize(
|
||||
"ui.components.subpage-data-table.exit_selection_mode"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
<ha-button-menu-new positioning="absolute">
|
||||
<ha-assist-chip
|
||||
.label=${localize(
|
||||
"ui.components.subpage-data-table.select"
|
||||
)}
|
||||
slot="trigger"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFormatListChecks}
|
||||
></ha-svg-icon>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${mdiMenuDown}
|
||||
></ha-svg-icon
|
||||
></ha-assist-chip>
|
||||
<ha-menu-item .value=${undefined} @click=${this._selectAll}
|
||||
>${localize("ui.components.subpage-data-table.select_all")}
|
||||
</ha-menu-item>
|
||||
<ha-menu-item .value=${undefined} @click=${this._selectNone}
|
||||
>${localize("ui.components.subpage-data-table.select_none")}
|
||||
</ha-menu-item>
|
||||
<md-divider role="separator" tabindex="-1"></md-divider>
|
||||
<ha-menu-item
|
||||
.value=${undefined}
|
||||
@click=${this._disableSelectMode}
|
||||
>${localize(
|
||||
"ui.components.subpage-data-table.close_select_mode"
|
||||
)}
|
||||
</ha-menu-item>
|
||||
</ha-button-menu-new>
|
||||
<p>
|
||||
${localize("ui.components.subpage-data-table.selected", {
|
||||
selected: this.selected || "0",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div class="center-vertical">
|
||||
<slot name="selection-bar"></slot>
|
||||
</div>
|
||||
</div>`
|
||||
: nothing}
|
||||
${this.showFilters
|
||||
? !showPane
|
||||
? html`<ha-dialog
|
||||
open
|
||||
hideActions
|
||||
.heading=${localize("ui.components.subpage-data-table.filters")}
|
||||
>
|
||||
<ha-dialog-header slot="heading">
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.path=${mdiClose}
|
||||
@click=${this._toggleFilters}
|
||||
.label=${localize(
|
||||
"ui.components.subpage-data-table.close_filter"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
<span slot="title"
|
||||
>${localize(
|
||||
"ui.components.subpage-data-table.filters"
|
||||
)}</span
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="actionItems"
|
||||
@click=${this._clearFilters}
|
||||
.path=${mdiFilterVariantRemove}
|
||||
.label=${localize(
|
||||
"ui.components.subpage-data-table.clear_filter"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</ha-dialog-header>
|
||||
<div class="filter-dialog-content">
|
||||
<slot name="filter-pane"></slot></div
|
||||
></ha-dialog>`
|
||||
: html`<div class="pane" slot="pane">
|
||||
<div class="table-header">
|
||||
<ha-assist-chip
|
||||
.label=${localize(
|
||||
"ui.components.subpage-data-table.filters"
|
||||
)}
|
||||
active
|
||||
@click=${this._toggleFilters}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFilterVariant}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
<ha-icon-button
|
||||
.path=${mdiFilterVariantRemove}
|
||||
@click=${this._clearFilters}
|
||||
.label=${localize(
|
||||
"ui.components.subpage-data-table.clear_filter"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<div class="pane-content">
|
||||
<slot name="filter-pane"></slot>
|
||||
</div>
|
||||
</div>`
|
||||
: nothing}
|
||||
${this.empty
|
||||
? html`<div class="center">
|
||||
<slot name="empty">${this.noDataText}</slot>
|
||||
</div>`
|
||||
: html`<div slot="toolbar-icon">
|
||||
<slot name="toolbar-icon"></slot>
|
||||
</div>
|
||||
: html`${!this.hideFilterMenu
|
||||
? html`
|
||||
<div slot="toolbar-icon">
|
||||
${this.narrow
|
||||
? html`
|
||||
<div class="filter-menu">
|
||||
${this.numHidden || this.activeFilters
|
||||
? html`<span class="badge"
|
||||
>${this.numHidden || "!"}</span
|
||||
>`
|
||||
: ""}
|
||||
<slot name="filter-menu"></slot>
|
||||
</div>
|
||||
`
|
||||
: ""}<slot name="toolbar-icon"></slot>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this.narrow
|
||||
? html`
|
||||
<div slot="header">
|
||||
<slot name="header">
|
||||
<div class="search-toolbar">${searchBar}</div>
|
||||
<div class="search-toolbar">${headerToolbar}</div>
|
||||
</slot>
|
||||
</div>
|
||||
`
|
||||
@@ -429,136 +240,30 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
.data=${this.data}
|
||||
.noDataText=${this.noDataText}
|
||||
.filter=${this.filter}
|
||||
.selectable=${this._selectMode}
|
||||
.selectable=${this.selectable}
|
||||
.hasFab=${this.hasFab}
|
||||
.id=${this.id}
|
||||
.clickable=${this.clickable}
|
||||
.appendRow=${this.appendRow}
|
||||
.sortColumn=${this._sortColumn}
|
||||
.sortDirection=${this._sortDirection}
|
||||
.groupColumn=${this._groupColumn}
|
||||
>
|
||||
${!this.narrow
|
||||
? html`
|
||||
<div slot="header">
|
||||
<slot name="header">
|
||||
<div class="table-header">
|
||||
${this.hasFilters && !this.showFilters
|
||||
? html`${filterButton}`
|
||||
: nothing}${selectModeBtn}${searchBar}${groupByMenu}${sortByMenu}
|
||||
</div>
|
||||
<div class="table-header">${headerToolbar}</div>
|
||||
</slot>
|
||||
</div>
|
||||
`
|
||||
: html`<div slot="header"></div>
|
||||
<div slot="header-row" class="narrow-header-row">
|
||||
${this.hasFilters && !this.showFilters
|
||||
? html`${filterButton}`
|
||||
: nothing}
|
||||
${selectModeBtn}${groupByMenu}${sortByMenu}
|
||||
</div>`}
|
||||
: html` <div slot="header"></div> `}
|
||||
</ha-data-table>`}
|
||||
|
||||
<div slot="fab"><slot name="fab"></slot></div>
|
||||
</hass-tabs-subpage>
|
||||
<ha-menu anchor="group-by-anchor" id="group-by-menu" positioning="fixed">
|
||||
${Object.entries(this.columns).map(([id, column]) =>
|
||||
column.groupable
|
||||
? html`
|
||||
<ha-menu-item
|
||||
.value=${id}
|
||||
@click=${this._handleGroupBy}
|
||||
.selected=${id === this._groupColumn}
|
||||
class=${classMap({ selected: id === this._groupColumn })}
|
||||
>
|
||||
${column.title || column.label}
|
||||
</ha-menu-item>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
<md-divider role="separator" tabindex="-1"></md-divider>
|
||||
<ha-menu-item
|
||||
.value=${undefined}
|
||||
@click=${this._handleGroupBy}
|
||||
.selected=${this._groupColumn === undefined}
|
||||
class=${classMap({ selected: this._groupColumn === undefined })}
|
||||
>
|
||||
${localize("ui.components.subpage-data-table.dont_group_by")}
|
||||
</ha-menu-item>
|
||||
</ha-menu>
|
||||
<ha-menu anchor="sort-by-anchor" id="sort-by-menu" positioning="fixed">
|
||||
${Object.entries(this.columns).map(([id, column]) =>
|
||||
column.sortable
|
||||
? html`
|
||||
<ha-menu-item
|
||||
.value=${id}
|
||||
@click=${this._handleSortBy}
|
||||
keep-open
|
||||
.selected=${id === this._sortColumn}
|
||||
class=${classMap({ selected: id === this._sortColumn })}
|
||||
>
|
||||
${this._sortColumn === id
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${this._sortDirection === "desc"
|
||||
? mdiArrowDown
|
||||
: mdiArrowUp}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
${column.title || column.label}
|
||||
</ha-menu-item>
|
||||
`
|
||||
: nothing
|
||||
)}
|
||||
</ha-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
private _clearFilters() {
|
||||
fireEvent(this, "clear-filter");
|
||||
}
|
||||
|
||||
private _toggleFilters() {
|
||||
this.showFilters = !this.showFilters;
|
||||
}
|
||||
|
||||
private _sortingChanged(ev) {
|
||||
this._sortDirection = ev.detail.direction;
|
||||
this._sortColumn = this._sortDirection ? ev.detail.column : undefined;
|
||||
}
|
||||
|
||||
private _handleSortBy(ev) {
|
||||
const columnId = ev.currentTarget.value;
|
||||
if (!this._sortDirection || this._sortColumn !== columnId) {
|
||||
this._sortDirection = "asc";
|
||||
} else if (this._sortDirection === "asc") {
|
||||
this._sortDirection = "desc";
|
||||
} else {
|
||||
this._sortDirection = null;
|
||||
}
|
||||
this._sortColumn = this._sortDirection === null ? undefined : columnId;
|
||||
}
|
||||
|
||||
private _handleGroupBy(ev) {
|
||||
this._groupColumn = ev.currentTarget.value;
|
||||
}
|
||||
|
||||
private _enableSelectMode() {
|
||||
this._selectMode = true;
|
||||
}
|
||||
|
||||
private _disableSelectMode() {
|
||||
this._selectMode = false;
|
||||
this._dataTable.clearSelection();
|
||||
}
|
||||
|
||||
private _selectAll() {
|
||||
this._dataTable.selectAll();
|
||||
}
|
||||
|
||||
private _selectNone() {
|
||||
this._dataTable.clearSelection();
|
||||
private _preventDefault(ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
@@ -569,56 +274,54 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
fireEvent(this, "search-changed", { value: this.filter });
|
||||
}
|
||||
|
||||
private _clearFilter() {
|
||||
fireEvent(this, "clear-filter");
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ha-data-table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
--data-table-border-width: 0;
|
||||
}
|
||||
:host(:not([narrow])) ha-data-table,
|
||||
.pane {
|
||||
:host(:not([narrow])) ha-data-table {
|
||||
height: calc(100vh - 1px - var(--header-height));
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pane-content {
|
||||
height: calc(100vh - 1px - var(--header-height) - var(--header-height));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:host([narrow]) hass-tabs-subpage {
|
||||
--main-title-margin: 0;
|
||||
}
|
||||
:host([narrow]) {
|
||||
--expansion-panel-summary-padding: 0 16px;
|
||||
}
|
||||
.table-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
--mdc-shape-small: 0;
|
||||
height: 56px;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
gap: 16px;
|
||||
box-sizing: border-box;
|
||||
background: var(--primary-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
search-input-outlined {
|
||||
flex: 1;
|
||||
}
|
||||
.search-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
search-input {
|
||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
||||
--text-field-overflow: visible;
|
||||
z-index: 5;
|
||||
}
|
||||
.table-header search-input {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
.search-toolbar search-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
color: var(--secondary-text-color);
|
||||
--mdc-ripple-color: transparant;
|
||||
}
|
||||
.filters {
|
||||
--mdc-text-field-fill-color: var(--input-fill-color);
|
||||
--mdc-text-field-idle-line-color: var(--input-idle-line-color);
|
||||
@@ -679,6 +382,9 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
top: 4px;
|
||||
font-size: 0.65em;
|
||||
}
|
||||
.filter-menu {
|
||||
position: relative;
|
||||
}
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -689,104 +395,6 @@ export class HaTabsSubpageDataTable extends LitElement {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
inset-inline-end: -4px;
|
||||
inset-inline-start: initial;
|
||||
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);
|
||||
}
|
||||
|
||||
.narrow-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 0 16px;
|
||||
overflow-x: scroll;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.selection-bar {
|
||||
background: rgba(var(--rgb-primary-color), 0.1);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
--ha-assist-chip-container-color: var(--primary-background-color);
|
||||
}
|
||||
|
||||
.selection-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.selection-controls p {
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
|
||||
.center-vertical {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
}
|
||||
|
||||
.select-mode-chip {
|
||||
--md-assist-chip-icon-label-space: 0;
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: calc(
|
||||
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
|
||||
);
|
||||
--mdc-dialog-max-width: calc(
|
||||
100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
|
||||
);
|
||||
--mdc-dialog-min-height: 100%;
|
||||
--mdc-dialog-max-height: 100%;
|
||||
--vertical-align-dialog: flex-end;
|
||||
--ha-dialog-border-radius: 0;
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
.filter-dialog-content {
|
||||
height: calc(100vh - 1px - var(--header-height));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#sort-by-anchor,
|
||||
#group-by-anchor,
|
||||
ha-button-menu-new ha-assist-chip {
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -4,7 +4,6 @@ import {
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
@@ -58,8 +57,6 @@ class HassTabsSubpage extends LitElement {
|
||||
@property({ type: Boolean, reflect: true, attribute: "is-wide" })
|
||||
public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public pane = false;
|
||||
|
||||
@state() private _activeTab?: PageNavigation;
|
||||
|
||||
// @ts-ignore
|
||||
@@ -131,63 +128,50 @@ class HassTabsSubpage extends LitElement {
|
||||
const showTabs = tabs.length > 1;
|
||||
return html`
|
||||
<div class="toolbar">
|
||||
<slot name="toolbar">
|
||||
<div class="toolbar-content">
|
||||
${this.mainPage || (!this.backPath && history.state?.root)
|
||||
? html`
|
||||
<ha-menu-button
|
||||
.hassio=${this.supervisor}
|
||||
${this.mainPage || (!this.backPath && history.state?.root)
|
||||
? html`
|
||||
<ha-menu-button
|
||||
.hassio=${this.supervisor}
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`
|
||||
: this.backPath
|
||||
? html`
|
||||
<a href=${this.backPath}>
|
||||
<ha-icon-button-arrow-prev
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`
|
||||
: this.backPath
|
||||
? html`
|
||||
<a href=${this.backPath}>
|
||||
<ha-icon-button-arrow-prev
|
||||
.hass=${this.hass}
|
||||
></ha-icon-button-arrow-prev>
|
||||
</a>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
.hass=${this.hass}
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`}
|
||||
${this.narrow || !showTabs
|
||||
? html`<div class="main-title">
|
||||
<slot name="header">${!showTabs ? tabs[0] : ""}</slot>
|
||||
</div>`
|
||||
: ""}
|
||||
${showTabs && !this.narrow
|
||||
? html`<div id="tabbar">${tabs}</div>`
|
||||
: ""}
|
||||
<div id="toolbar-icon">
|
||||
<slot name="toolbar-icon"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
${showTabs && this.narrow
|
||||
? html`<div id="tabbar" class="bottom-bar">${tabs}</div>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="container">
|
||||
${this.pane
|
||||
? html`<div class="pane">
|
||||
<div class="shadow-container"></div>
|
||||
<div class="ha-scrollbar">
|
||||
<slot name="pane"></slot>
|
||||
</div>
|
||||
></ha-icon-button-arrow-prev>
|
||||
</a>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button-arrow-prev
|
||||
.hass=${this.hass}
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`}
|
||||
${this.narrow || !showTabs
|
||||
? html`<div class="main-title">
|
||||
<slot name="header">${!showTabs ? tabs[0] : ""}</slot>
|
||||
</div>`
|
||||
: nothing}
|
||||
<div
|
||||
class="content ha-scrollbar ${classMap({ tabs: showTabs })}"
|
||||
@scroll=${this._saveScrollPos}
|
||||
>
|
||||
<slot></slot>
|
||||
: ""}
|
||||
${showTabs
|
||||
? html`
|
||||
<div id="tabbar" class=${classMap({ "bottom-bar": this.narrow })}>
|
||||
${tabs}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<div id="toolbar-icon">
|
||||
<slot name="toolbar-icon"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="content ha-scrollbar ${classMap({ tabs: showTabs })}"
|
||||
@scroll=${this._saveScrollPos}
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div id="fab" class=${classMap({ tabs: showTabs })}>
|
||||
<slot name="fab"></slot>
|
||||
</div>
|
||||
@@ -222,15 +206,6 @@ class HassTabsSubpage extends LitElement {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
height: calc(100% - var(--header-height));
|
||||
}
|
||||
|
||||
:host([narrow]) .container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
ha-menu-button {
|
||||
margin-right: 24px;
|
||||
margin-inline-end: 24px;
|
||||
@@ -238,22 +213,18 @@ class HassTabsSubpage extends LitElement {
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
height: var(--header-height);
|
||||
background-color: var(--sidebar-background-color);
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.toolbar-content {
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@media (max-width: 599px) {
|
||||
.toolbar-content {
|
||||
.toolbar {
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
@@ -326,6 +297,10 @@ class HassTabsSubpage extends LitElement {
|
||||
margin-right: env(safe-area-inset-right);
|
||||
margin-inline-start: env(safe-area-inset-left);
|
||||
margin-inline-end: env(safe-area-inset-right);
|
||||
height: calc(100% - 1px - var(--header-height));
|
||||
height: calc(
|
||||
100% - 1px - var(--header-height) - env(safe-area-inset-bottom)
|
||||
);
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
@@ -344,10 +319,6 @@ class HassTabsSubpage extends LitElement {
|
||||
inset-inline-start: initial;
|
||||
bottom: calc(16px + env(safe-area-inset-bottom));
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
:host([narrow]) #fab.tabs {
|
||||
bottom: calc(84px + env(safe-area-inset-bottom));
|
||||
@@ -358,21 +329,6 @@ class HassTabsSubpage extends LitElement {
|
||||
inset-inline-end: 24px;
|
||||
inset-inline-start: initial;
|
||||
}
|
||||
|
||||
.pane {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
border-inline-end: 1px solid var(--divider-color);
|
||||
border-inline-start: initial;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 0 0 var(--sidepane-width, 250px);
|
||||
width: var(--sidepane-width, 250px);
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
.pane .ha-scrollbar {
|
||||
flex: 1;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -27,7 +27,7 @@ class NotificationManager extends LitElement {
|
||||
@query("ha-toast") private _toast!: HaToast | undefined;
|
||||
|
||||
public async showDialog(parameters: ShowToastParams) {
|
||||
if (this._parameters && this._parameters.message !== parameters.message) {
|
||||
if (this._parameters) {
|
||||
this._parameters = undefined;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@@ -10,9 +10,7 @@ import "../../../components/ha-picture-upload";
|
||||
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-floor-picker";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/ha-labels-picker";
|
||||
import { AreaRegistryEntryMutableParams } from "../../../data/area_registry";
|
||||
import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
@@ -33,14 +31,10 @@ class DialogAreaDetail extends LitElement {
|
||||
|
||||
@state() private _aliases!: string[];
|
||||
|
||||
@state() private _labels!: string[];
|
||||
|
||||
@state() private _picture!: string | null;
|
||||
|
||||
@state() private _icon!: string | null;
|
||||
|
||||
@state() private _floor!: string | null;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _params?: AreaRegistryDetailDialogParams;
|
||||
@@ -52,14 +46,10 @@ class DialogAreaDetail extends LitElement {
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
this._name = this._params.entry
|
||||
? this._params.entry.name
|
||||
: this._params.suggestedName || "";
|
||||
this._name = this._params.entry ? this._params.entry.name : "";
|
||||
this._aliases = this._params.entry ? this._params.entry.aliases : [];
|
||||
this._labels = this._params.entry ? this._params.entry.labels : [];
|
||||
this._picture = this._params.entry?.picture || null;
|
||||
this._icon = this._params.entry?.icon || null;
|
||||
this._floor = this._params.entry?.floor_id || null;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
@@ -122,19 +112,6 @@ class DialogAreaDetail extends LitElement {
|
||||
.label=${this.hass.localize("ui.panel.config.areas.editor.icon")}
|
||||
></ha-icon-picker>
|
||||
|
||||
<ha-floor-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._floor}
|
||||
@value-changed=${this._floorChanged}
|
||||
.label=${this.hass.localize("ui.panel.config.areas.editor.floor")}
|
||||
></ha-floor-picker>
|
||||
|
||||
<ha-labels-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._labels}
|
||||
@value-changed=${this._labelsChanged}
|
||||
></ha-labels-picker>
|
||||
|
||||
<ha-picture-upload
|
||||
.hass=${this.hass}
|
||||
.value=${this._picture}
|
||||
@@ -186,21 +163,11 @@ class DialogAreaDetail extends LitElement {
|
||||
this._name = ev.target.value;
|
||||
}
|
||||
|
||||
private _floorChanged(ev) {
|
||||
this._error = undefined;
|
||||
this._floor = ev.detail.value;
|
||||
}
|
||||
|
||||
private _iconChanged(ev) {
|
||||
this._error = undefined;
|
||||
this._icon = ev.detail.value;
|
||||
}
|
||||
|
||||
private _labelsChanged(ev) {
|
||||
this._error = undefined;
|
||||
this._labels = ev.detail.value;
|
||||
}
|
||||
|
||||
private _pictureChanged(ev: ValueChangedEvent<string | null>) {
|
||||
this._error = undefined;
|
||||
this._picture = (ev.target as HaPictureUpload).value;
|
||||
@@ -214,8 +181,6 @@ class DialogAreaDetail extends LitElement {
|
||||
name: this._name.trim(),
|
||||
picture: this._picture || (create ? undefined : null),
|
||||
icon: this._icon || (create ? undefined : null),
|
||||
floor_id: this._floor || (create ? undefined : null),
|
||||
labels: this._labels || null,
|
||||
aliases: this._aliases,
|
||||
};
|
||||
if (create) {
|
||||
@@ -243,8 +208,6 @@ class DialogAreaDetail extends LitElement {
|
||||
css`
|
||||
ha-textfield,
|
||||
ha-icon-picker,
|
||||
ha-floor-picker,
|
||||
ha-labels-picker,
|
||||
ha-picture-upload {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
|
@@ -1,232 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-aliases-editor";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-picture-upload";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-textfield";
|
||||
import { FloorRegistryEntryMutableParams } from "../../../data/floor_registry";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { FloorRegistryDetailDialogParams } from "./show-dialog-floor-registry-detail";
|
||||
|
||||
class DialogFloorDetail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _name!: string;
|
||||
|
||||
@state() private _aliases!: string[];
|
||||
|
||||
@state() private _icon!: string | null;
|
||||
|
||||
@state() private _level!: number | null;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _params?: FloorRegistryDetailDialogParams;
|
||||
|
||||
@state() private _submitting?: boolean;
|
||||
|
||||
public async showDialog(
|
||||
params: FloorRegistryDetailDialogParams
|
||||
): Promise<void> {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
this._name = this._params.entry
|
||||
? this._params.entry.name
|
||||
: this._params.suggestedName || "";
|
||||
this._aliases = this._params.entry?.aliases || [];
|
||||
this._icon = this._params.entry?.icon || null;
|
||||
this._level = this._params.entry?.level ?? null;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._error = "";
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
const entry = this._params.entry;
|
||||
const nameInvalid = !this._isNameValid();
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
entry
|
||||
? this.hass.localize("ui.panel.config.floors.editor.update_floor")
|
||||
: this.hass.localize("ui.panel.config.floors.editor.create_floor")
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<div class="form">
|
||||
${entry
|
||||
? html`
|
||||
<ha-settings-row>
|
||||
<span slot="heading">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.floors.editor.floor_id"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description">${entry.floor_id}</span>
|
||||
</ha-settings-row>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-textfield
|
||||
.value=${this._name}
|
||||
@input=${this._nameChanged}
|
||||
.label=${this.hass.localize("ui.panel.config.floors.editor.name")}
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.panel.config.floors.editor.name_required"
|
||||
)}
|
||||
required
|
||||
dialogInitialFocus
|
||||
></ha-textfield>
|
||||
|
||||
<ha-textfield
|
||||
.value=${this._level}
|
||||
@input=${this._levelChanged}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.floors.editor.level"
|
||||
)}
|
||||
type="number"
|
||||
></ha-textfield>
|
||||
|
||||
<ha-icon-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this._icon}
|
||||
@value-changed=${this._iconChanged}
|
||||
.label=${this.hass.localize("ui.panel.config.areas.editor.icon")}
|
||||
>
|
||||
${!this._icon
|
||||
? html`
|
||||
<ha-floor-icon
|
||||
slot="fallback"
|
||||
.floor=${{ level: this._level }}
|
||||
></ha-floor-icon>
|
||||
`
|
||||
: nothing}
|
||||
</ha-icon-picker>
|
||||
|
||||
<h3 class="header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.floors.editor.aliases_section"
|
||||
)}
|
||||
</h3>
|
||||
|
||||
<p class="description">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.floors.editor.aliases_description"
|
||||
)}
|
||||
</p>
|
||||
<ha-aliases-editor
|
||||
.hass=${this.hass}
|
||||
.aliases=${this._aliases}
|
||||
@value-changed=${this._aliasesChanged}
|
||||
></ha-aliases-editor>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${nameInvalid || this._submitting}
|
||||
>
|
||||
${entry
|
||||
? this.hass.localize("ui.common.save")
|
||||
: this.hass.localize("ui.common.add")}
|
||||
</mwc-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _isNameValid() {
|
||||
return this._name.trim() !== "";
|
||||
}
|
||||
|
||||
private _nameChanged(ev) {
|
||||
this._error = undefined;
|
||||
this._name = ev.target.value;
|
||||
}
|
||||
|
||||
private _levelChanged(ev) {
|
||||
this._error = undefined;
|
||||
this._level = ev.target.value === "" ? null : Number(ev.target.value);
|
||||
}
|
||||
|
||||
private _iconChanged(ev) {
|
||||
this._error = undefined;
|
||||
this._icon = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
this._submitting = true;
|
||||
const create = !this._params!.entry;
|
||||
try {
|
||||
const values: FloorRegistryEntryMutableParams = {
|
||||
name: this._name.trim(),
|
||||
icon: this._icon || (create ? undefined : null),
|
||||
level: this._level,
|
||||
aliases: this._aliases,
|
||||
};
|
||||
if (create) {
|
||||
await this._params!.createEntry!(values);
|
||||
} else {
|
||||
await this._params!.updateEntry!(values);
|
||||
}
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error =
|
||||
err.message ||
|
||||
this.hass.localize("ui.panel.config.floors.editor.unknown_error");
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _aliasesChanged(ev: CustomEvent): void {
|
||||
this._aliases = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-floor-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-floor-registry-detail": DialogFloorDetail;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("dialog-floor-registry-detail", DialogFloorDetail);
|
@@ -215,12 +215,7 @@ class HaConfigAreaPage extends LitElement {
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${html`${area.icon
|
||||
? html`<ha-icon
|
||||
.icon=${area.icon}
|
||||
style="margin-inline-end: 8px;"
|
||||
></ha-icon>`
|
||||
: nothing}${area.name}`}
|
||||
.header=${area.name}
|
||||
>
|
||||
<ha-button-menu slot="toolbar-icon">
|
||||
<ha-icon-button
|
||||
|
@@ -1,12 +1,4 @@
|
||||
import { ActionDetail } from "@material/mwc-list";
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiHelpCircle,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { mdiHelpCircle, mdiPlus } from "@mdi/js";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
@@ -15,34 +7,19 @@ import {
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { formatListWithAnds } from "../../../common/string/format-list";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-floor-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-sortable";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
createAreaRegistryEntry,
|
||||
updateAreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
import {
|
||||
FloorRegistryEntry,
|
||||
createFloorRegistryEntry,
|
||||
deleteFloorRegistryEntry,
|
||||
getFloorAreaLookup,
|
||||
subscribeFloorRegistry,
|
||||
updateFloorRegistryEntry,
|
||||
} from "../../../data/floor_registry";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
@@ -50,14 +27,9 @@ import {
|
||||
loadAreaRegistryDetailDialog,
|
||||
showAreaRegistryDetailDialog,
|
||||
} from "./show-dialog-area-registry-detail";
|
||||
import { showFloorRegistryDetailDialog } from "./show-dialog-floor-registry-detail";
|
||||
|
||||
const UNASSIGNED_PATH = ["__unassigned__"];
|
||||
|
||||
const SORT_OPTIONS = { sort: false, delay: 500, delayOnTouchOnly: true };
|
||||
|
||||
@customElement("ha-config-areas-dashboard")
|
||||
export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
||||
export class HaConfigAreasDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public isWide = false;
|
||||
@@ -66,14 +38,11 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _floors?: FloorRegistryEntry[];
|
||||
|
||||
private _processAreas = memoizeOne(
|
||||
(
|
||||
areas: HomeAssistant["areas"],
|
||||
devices: HomeAssistant["devices"],
|
||||
entities: HomeAssistant["entities"],
|
||||
floors: FloorRegistryEntry[]
|
||||
entities: HomeAssistant["entities"]
|
||||
) => {
|
||||
const processArea = (area: AreaRegistryEntry) => {
|
||||
let noDevicesInArea = 0;
|
||||
@@ -104,40 +73,18 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
||||
};
|
||||
};
|
||||
|
||||
const floorAreaLookup = getFloorAreaLookup(Object.values(areas));
|
||||
const unassisgnedAreas = Object.values(areas).filter(
|
||||
(area) => !area.floor_id || !floorAreaLookup[area.floor_id]
|
||||
);
|
||||
return {
|
||||
floors: floors.map((floor) => ({
|
||||
...floor,
|
||||
areas: (floorAreaLookup[floor.floor_id] || []).map(processArea),
|
||||
})),
|
||||
unassisgnedAreas: unassisgnedAreas.map(processArea),
|
||||
};
|
||||
return Object.values(areas).map(processArea);
|
||||
}
|
||||
);
|
||||
|
||||
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
|
||||
return [
|
||||
subscribeFloorRegistry(this.hass.connection, (floors) => {
|
||||
this._floors = floors;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const areasAndFloors =
|
||||
!this.hass.areas ||
|
||||
!this.hass.devices ||
|
||||
!this.hass.entities ||
|
||||
!this._floors
|
||||
const areas =
|
||||
!this.hass.areas || !this.hass.devices || !this.hass.entities
|
||||
? undefined
|
||||
: this._processAreas(
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this._floors
|
||||
this.hass.entities
|
||||
);
|
||||
|
||||
return html`
|
||||
@@ -156,94 +103,12 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
||||
@click=${this._showHelp}
|
||||
></ha-icon-button>
|
||||
<div class="container">
|
||||
${areasAndFloors?.floors.map(
|
||||
(floor) =>
|
||||
html`<div class="floor">
|
||||
<div class="header">
|
||||
<h2>
|
||||
<ha-floor-icon .floor=${floor}></ha-floor-icon>
|
||||
${floor.name}
|
||||
</h2>
|
||||
<ha-button-menu
|
||||
.floor=${floor}
|
||||
@action=${this._handleFloorAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-list-item graphic="icon"
|
||||
><ha-svg-icon
|
||||
.path=${mdiPencil}
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.floor.edit_floor"
|
||||
)}</ha-list-item
|
||||
>
|
||||
<ha-list-item class="warning" graphic="icon"
|
||||
><ha-svg-icon
|
||||
class="warning"
|
||||
.path=${mdiDelete}
|
||||
slot="graphic"
|
||||
></ha-svg-icon
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.floor.delete_floor"
|
||||
)}</ha-list-item
|
||||
>
|
||||
</ha-button-menu>
|
||||
</div>
|
||||
<ha-sortable
|
||||
handle-selector="a"
|
||||
draggable-selector="a"
|
||||
@item-moved=${this._areaMoved}
|
||||
group="floor"
|
||||
.options=${SORT_OPTIONS}
|
||||
.path=${[floor.floor_id]}
|
||||
>
|
||||
<div class="areas">
|
||||
${floor.areas.map((area) => this._renderArea(area))}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
</div>`
|
||||
)}
|
||||
${areasAndFloors?.unassisgnedAreas.length
|
||||
? html`<div class="floor">
|
||||
<div class="header">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.unassigned_areas"
|
||||
)}
|
||||
</h2>
|
||||
</div>
|
||||
<ha-sortable
|
||||
handle-selector="a"
|
||||
draggable-selector="a"
|
||||
@item-moved=${this._areaMoved}
|
||||
group="floor"
|
||||
.options=${SORT_OPTIONS}
|
||||
.path=${UNASSIGNED_PATH}
|
||||
>
|
||||
<div class="areas">
|
||||
${areasAndFloors?.unassisgnedAreas.map((area) =>
|
||||
this._renderArea(area)
|
||||
)}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
${areas?.length
|
||||
? html`<div class="areas">
|
||||
${areas.map((area) => this._renderArea(area))}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
class="floor"
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.areas.picker.create_floor"
|
||||
)}
|
||||
extended
|
||||
@click=${this._createFloor}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
.label=${this.hass.localize(
|
||||
@@ -305,66 +170,6 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
||||
loadAreaRegistryDetailDialog();
|
||||
}
|
||||
|
||||
private async _areaMoved(ev) {
|
||||
const areasAndFloors = this._processAreas(
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this._floors!
|
||||
);
|
||||
let area: AreaRegistryEntry;
|
||||
if (ev.detail.oldPath === UNASSIGNED_PATH) {
|
||||
area = areasAndFloors.unassisgnedAreas[ev.detail.oldIndex];
|
||||
} else {
|
||||
const oldFloor = areasAndFloors.floors!.find(
|
||||
(floor) => floor.floor_id === ev.detail.oldPath[0]
|
||||
);
|
||||
area = oldFloor!.areas[ev.detail.oldIndex];
|
||||
}
|
||||
|
||||
await updateAreaRegistryEntry(this.hass, area.area_id, {
|
||||
floor_id:
|
||||
ev.detail.newPath === UNASSIGNED_PATH ? null : ev.detail.newPath[0],
|
||||
});
|
||||
}
|
||||
|
||||
private _handleFloorAction(ev: CustomEvent<ActionDetail>) {
|
||||
const floor = (ev.currentTarget as any).floor;
|
||||
switch (ev.detail.index) {
|
||||
case 0:
|
||||
this._editFloor(floor);
|
||||
break;
|
||||
case 1:
|
||||
this._deleteFloor(floor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _createFloor() {
|
||||
this._openFloorDialog();
|
||||
}
|
||||
|
||||
private _editFloor(floor) {
|
||||
this._openFloorDialog(floor);
|
||||
}
|
||||
|
||||
private async _deleteFloor(floor) {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.areas.picker.floor.confirm_delete"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.areas.picker.floor.confirm_delete_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
});
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
await deleteFloorRegistryEntry(this.hass, floor.floor_id);
|
||||
}
|
||||
|
||||
private _createArea() {
|
||||
this._openAreaDialog();
|
||||
}
|
||||
@@ -394,37 +199,12 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _openFloorDialog(entry?: FloorRegistryEntry) {
|
||||
showFloorRegistryDetailDialog(this, {
|
||||
entry,
|
||||
createEntry: async (values) =>
|
||||
createFloorRegistryEntry(this.hass!, values),
|
||||
updateEntry: async (values) =>
|
||||
updateFloorRegistryEntry(this.hass!, entry!.floor_id, values),
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.container {
|
||||
padding: 8px 16px 16px;
|
||||
margin: 0 auto 64px auto;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: var(--secondary-text-color);
|
||||
padding-inline-start: 8px;
|
||||
}
|
||||
.header h2 {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-top: 28px;
|
||||
}
|
||||
.header ha-icon {
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.areas {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
@@ -469,12 +249,6 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
|
||||
min-height: 16px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.floor {
|
||||
--primary-color: var(--secondary-text-color);
|
||||
}
|
||||
.warning {
|
||||
color: var(--error-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -6,7 +6,6 @@ import {
|
||||
|
||||
export interface AreaRegistryDetailDialogParams {
|
||||
entry?: AreaRegistryEntry;
|
||||
suggestedName?: string;
|
||||
createEntry?: (values: AreaRegistryEntryMutableParams) => Promise<unknown>;
|
||||
updateEntry?: (
|
||||
updates: Partial<AreaRegistryEntryMutableParams>
|
||||
|
@@ -1,28 +0,0 @@
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import {
|
||||
FloorRegistryEntry,
|
||||
FloorRegistryEntryMutableParams,
|
||||
} from "../../../data/floor_registry";
|
||||
|
||||
export interface FloorRegistryDetailDialogParams {
|
||||
entry?: FloorRegistryEntry;
|
||||
suggestedName?: string;
|
||||
createEntry?: (values: FloorRegistryEntryMutableParams) => Promise<unknown>;
|
||||
updateEntry?: (
|
||||
updates: Partial<FloorRegistryEntryMutableParams>
|
||||
) => Promise<unknown>;
|
||||
}
|
||||
|
||||
export const loadFloorRegistryDetailDialog = () =>
|
||||
import("./dialog-floor-registry-detail");
|
||||
|
||||
export const showFloorRegistryDetailDialog = (
|
||||
element: HTMLElement,
|
||||
systemLogDetailParams: FloorRegistryDetailDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-floor-registry-detail",
|
||||
dialogImport: loadFloorRegistryDetailDialog,
|
||||
dialogParams: systemLogDetailParams,
|
||||
});
|
||||
};
|
@@ -1,3 +1,4 @@
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import "@material/web/divider/divider";
|
||||
import { mdiClose, mdiContentPaste, mdiPlus } from "@mdi/js";
|
||||
import Fuse, { IFuseOptions } from "fuse.js";
|
||||
@@ -556,7 +557,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
||||
></ha-svg-icon
|
||||
><ha-svg-icon slot="end" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-list-item-new>
|
||||
<md-divider role="separator" tabindex="-1"></md-divider>`
|
||||
<md-divider></md-divider>`
|
||||
: ""}
|
||||
${repeat(
|
||||
items,
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,6 @@ import {
|
||||
import "../../../layouts/hass-loading-screen";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
|
||||
@customElement("ha-config-backup")
|
||||
@@ -49,13 +48,9 @@ class HaConfigBackup extends LitElement {
|
||||
@state() private _backupData?: BackupData;
|
||||
|
||||
private _columns = memoize(
|
||||
(
|
||||
narrow,
|
||||
_language,
|
||||
localize: LocalizeFunc
|
||||
): DataTableColumnContainer<BackupContent> => ({
|
||||
(narrow, _language): DataTableColumnContainer<BackupContent> => ({
|
||||
name: {
|
||||
title: localize("ui.panel.config.backup.name"),
|
||||
title: this.hass.localize("ui.panel.config.backup.name"),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
@@ -65,7 +60,7 @@ class HaConfigBackup extends LitElement {
|
||||
<div class="secondary">${backup.path}</div>`,
|
||||
},
|
||||
size: {
|
||||
title: localize("ui.panel.config.backup.size"),
|
||||
title: this.hass.localize("ui.panel.config.backup.size"),
|
||||
width: "15%",
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
@@ -73,7 +68,7 @@ class HaConfigBackup extends LitElement {
|
||||
template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB",
|
||||
},
|
||||
date: {
|
||||
title: localize("ui.panel.config.backup.created"),
|
||||
title: this.hass.localize("ui.panel.config.backup.created"),
|
||||
width: "15%",
|
||||
direction: "desc",
|
||||
hidden: narrow,
|
||||
@@ -144,11 +139,7 @@ class HaConfigBackup extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config/system"
|
||||
.route=${this.route}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
this.hass.language,
|
||||
this.hass.localize
|
||||
)}
|
||||
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||
.data=${this._getItems(this._backupData.backups)}
|
||||
.noDataText=${this.hass.localize("ui.panel.config.backup.no_backups")}
|
||||
.searchLabel=${this.hass.localize(
|
||||
|
@@ -49,7 +49,6 @@ import {
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import { HomeAssistant, Route } from "../../../types";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
@@ -121,13 +120,11 @@ class HaBlueprintOverview extends LitElement {
|
||||
);
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(
|
||||
narrow,
|
||||
_language,
|
||||
localize: LocalizeFunc
|
||||
): DataTableColumnContainer<BlueprintMetaDataPath> => ({
|
||||
(narrow, _language): DataTableColumnContainer<BlueprintMetaDataPath> => ({
|
||||
name: {
|
||||
title: localize("ui.panel.config.blueprint.overview.headers.name"),
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.blueprint.overview.headers.name"
|
||||
),
|
||||
main: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
@@ -141,7 +138,9 @@ class HaBlueprintOverview extends LitElement {
|
||||
: undefined,
|
||||
},
|
||||
type: {
|
||||
title: localize("ui.panel.config.blueprint.overview.headers.type"),
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.blueprint.overview.headers.type"
|
||||
),
|
||||
template: (blueprint) =>
|
||||
html`${this.hass.localize(
|
||||
`ui.panel.config.blueprint.overview.types.${blueprint.type}`
|
||||
@@ -153,7 +152,9 @@ class HaBlueprintOverview extends LitElement {
|
||||
width: "10%",
|
||||
},
|
||||
path: {
|
||||
title: localize("ui.panel.config.blueprint.overview.headers.file_name"),
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.blueprint.overview.headers.file_name"
|
||||
),
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
hidden: narrow,
|
||||
@@ -251,11 +252,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.columns=${this._columns(
|
||||
this.narrow,
|
||||
this.hass.language,
|
||||
this.hass.localize
|
||||
)}
|
||||
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||
.data=${this._processedBlueprints(this.blueprints)}
|
||||
id="fullpath"
|
||||
.noDataText=${this.hass.localize(
|
||||
@@ -264,7 +261,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
hasFab
|
||||
clickable
|
||||
@row-click=${this._handleRowClicked}
|
||||
.appendRow=${html`<div
|
||||
.appendRow=${html` <div
|
||||
class="mdc-data-table__cell"
|
||||
style="width: 100%; text-align: center;"
|
||||
role="cell"
|
||||
|
@@ -1,132 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-alert";
|
||||
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-textfield";
|
||||
import { updateEntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "./ha-category-picker";
|
||||
import { AssignCategoryDialogParams } from "./show-dialog-assign-category";
|
||||
|
||||
@customElement("dialog-assign-category")
|
||||
class DialogAssignCategory extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _scope?: string;
|
||||
|
||||
@state() private _category?: string;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _params?: AssignCategoryDialogParams;
|
||||
|
||||
@state() private _submitting?: boolean;
|
||||
|
||||
public showDialog(params: AssignCategoryDialogParams): void {
|
||||
this._params = params;
|
||||
this._scope = params.scope;
|
||||
this._category = params.entityReg.categories[params.scope];
|
||||
this._error = undefined;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._error = "";
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
const entry = this._params.entityReg.categories[this._params.scope];
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
entry
|
||||
? this.hass.localize("ui.panel.config.category.assign.edit")
|
||||
: this.hass.localize("ui.panel.config.category.assign.assign")
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
<div class="form">
|
||||
<ha-category-picker
|
||||
.hass=${this.hass}
|
||||
.scope=${this._scope}
|
||||
.value=${this._category}
|
||||
@value-changed=${this._categoryChanged}
|
||||
></ha-category-picker>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button slot="secondaryAction" @click=${this.closeDialog}>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateEntry}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.common.save")}
|
||||
</mwc-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _categoryChanged(ev: CustomEvent): void {
|
||||
if (!ev.detail.value) {
|
||||
this._category = undefined;
|
||||
}
|
||||
this._category = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
this._submitting = true;
|
||||
this._error = undefined;
|
||||
try {
|
||||
await updateEntityRegistryEntry(
|
||||
this.hass,
|
||||
this._params!.entityReg.entity_id,
|
||||
{
|
||||
categories: { [this._scope!]: this._category || null },
|
||||
}
|
||||
);
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error =
|
||||
err.message ||
|
||||
this.hass.localize("ui.panel.config.category.assign.unknown_error");
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-textfield,
|
||||
ha-icon-picker {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-assign-category": DialogAssignCategory;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user