Add experimental sections view (#19846)
This commit is contained in:
parent
47f7cf5419
commit
d95bf64edf
|
@ -20,14 +20,14 @@ function findNestedItem(
|
||||||
}, obj);
|
}, obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nestedArrayMove<T>(
|
export function nestedArrayMove<A>(
|
||||||
obj: T | T[],
|
obj: A,
|
||||||
oldIndex: number,
|
oldIndex: number,
|
||||||
newIndex: number,
|
newIndex: number,
|
||||||
oldPath?: ItemPath,
|
oldPath?: ItemPath,
|
||||||
newPath?: ItemPath
|
newPath?: ItemPath
|
||||||
): T | T[] {
|
): A {
|
||||||
const newObj = Array.isArray(obj) ? [...obj] : { ...obj };
|
const newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
|
||||||
const from = oldPath ? findNestedItem(newObj, oldPath) : newObj;
|
const from = oldPath ? findNestedItem(newObj, oldPath) : newObj;
|
||||||
const to = newPath ? findNestedItem(newObj, newPath, true) : newObj;
|
const to = newPath ? findNestedItem(newObj, newPath, true) : newObj;
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,16 @@ declare global {
|
||||||
oldPath?: ItemPath;
|
oldPath?: ItemPath;
|
||||||
newPath?: ItemPath;
|
newPath?: ItemPath;
|
||||||
};
|
};
|
||||||
|
"drag-start": undefined;
|
||||||
|
"drag-end": undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HaSortableOptions = Omit<
|
||||||
|
SortableInstance.SortableOptions,
|
||||||
|
"onStart" | "onChoose" | "onEnd"
|
||||||
|
>;
|
||||||
|
|
||||||
@customElement("ha-sortable")
|
@customElement("ha-sortable")
|
||||||
export class HaSortable extends LitElement {
|
export class HaSortable extends LitElement {
|
||||||
private _sortable?: SortableInstance;
|
private _sortable?: SortableInstance;
|
||||||
|
@ -36,14 +43,17 @@ export class HaSortable extends LitElement {
|
||||||
@property({ type: String, attribute: "handle-selector" })
|
@property({ type: String, attribute: "handle-selector" })
|
||||||
public handleSelector?: string;
|
public handleSelector?: string;
|
||||||
|
|
||||||
@property({ type: String, attribute: "group" })
|
@property({ type: String })
|
||||||
public group?: string;
|
public group?: string | SortableInstance.GroupOptions;
|
||||||
|
|
||||||
@property({ type: Number, attribute: "swap-threshold" })
|
|
||||||
public swapThreshold?: number;
|
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "invert-swap" })
|
@property({ type: Boolean, attribute: "invert-swap" })
|
||||||
public invertSwap?: boolean;
|
public invertSwap: boolean = false;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public options?: HaSortableOptions;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
public rollback: boolean = true;
|
||||||
|
|
||||||
protected updated(changedProperties: PropertyValues<this>) {
|
protected updated(changedProperties: PropertyValues<this>) {
|
||||||
if (changedProperties.has("disabled")) {
|
if (changedProperties.has("disabled")) {
|
||||||
|
@ -114,26 +124,20 @@ export class HaSortable extends LitElement {
|
||||||
|
|
||||||
const options: SortableInstance.Options = {
|
const options: SortableInstance.Options = {
|
||||||
animation: 150,
|
animation: 150,
|
||||||
swapThreshold: 1,
|
...this.options,
|
||||||
onChoose: this._handleChoose,
|
onChoose: this._handleChoose,
|
||||||
|
onStart: this._handleStart,
|
||||||
onEnd: this._handleEnd,
|
onEnd: this._handleEnd,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.draggableSelector) {
|
if (this.draggableSelector) {
|
||||||
options.draggable = this.draggableSelector;
|
options.draggable = this.draggableSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.swapThreshold !== undefined) {
|
|
||||||
options.swapThreshold = this.swapThreshold;
|
|
||||||
}
|
|
||||||
if (this.invertSwap !== undefined) {
|
|
||||||
options.invertSwap = this.invertSwap;
|
|
||||||
}
|
|
||||||
if (this.handleSelector) {
|
if (this.handleSelector) {
|
||||||
options.handle = this.handleSelector;
|
options.handle = this.handleSelector;
|
||||||
}
|
}
|
||||||
if (this.draggableSelector) {
|
if (this.invertSwap !== undefined) {
|
||||||
options.draggable = this.draggableSelector;
|
options.invertSwap = this.invertSwap;
|
||||||
}
|
}
|
||||||
if (this.group) {
|
if (this.group) {
|
||||||
options.group = this.group;
|
options.group = this.group;
|
||||||
|
@ -143,8 +147,9 @@ export class HaSortable extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleEnd = async (evt: SortableEvent) => {
|
private _handleEnd = async (evt: SortableEvent) => {
|
||||||
|
fireEvent(this, "drag-end");
|
||||||
// put back in original location
|
// put back in original location
|
||||||
if ((evt.item as any).placeholder) {
|
if (this.rollback && (evt.item as any).placeholder) {
|
||||||
(evt.item as any).placeholder.replaceWith(evt.item);
|
(evt.item as any).placeholder.replaceWith(evt.item);
|
||||||
delete (evt.item as any).placeholder;
|
delete (evt.item as any).placeholder;
|
||||||
}
|
}
|
||||||
|
@ -170,7 +175,12 @@ export class HaSortable extends LitElement {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private _handleStart = () => {
|
||||||
|
fireEvent(this, "drag-start");
|
||||||
|
};
|
||||||
|
|
||||||
private _handleChoose = (evt: SortableEvent) => {
|
private _handleChoose = (evt: SortableEvent) => {
|
||||||
|
if (!this.rollback) return;
|
||||||
(evt.item as any).placeholder = document.createComment("sort-placeholder");
|
(evt.item as any).placeholder = document.createComment("sort-placeholder");
|
||||||
evt.item.after((evt.item as any).placeholder);
|
evt.item.after((evt.item as any).placeholder);
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,8 +10,10 @@ import {
|
||||||
LovelaceCard,
|
LovelaceCard,
|
||||||
} from "../panels/lovelace/types";
|
} from "../panels/lovelace/types";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
import { LovelaceSectionConfig } from "./lovelace/config/section";
|
||||||
import { fetchConfig, LegacyLovelaceConfig } from "./lovelace/config/types";
|
import { fetchConfig, LegacyLovelaceConfig } from "./lovelace/config/types";
|
||||||
import { LovelaceViewConfig } from "./lovelace/config/view";
|
import { LovelaceViewConfig } from "./lovelace/config/view";
|
||||||
|
import { HuiSection } from "../panels/lovelace/sections/hui-section";
|
||||||
|
|
||||||
export interface LovelacePanelConfig {
|
export interface LovelacePanelConfig {
|
||||||
mode: "yaml" | "storage";
|
mode: "yaml" | "storage";
|
||||||
|
@ -24,10 +26,21 @@ export interface LovelaceViewElement extends HTMLElement {
|
||||||
index?: number;
|
index?: number;
|
||||||
cards?: Array<LovelaceCard | HuiErrorCard>;
|
cards?: Array<LovelaceCard | HuiErrorCard>;
|
||||||
badges?: LovelaceBadge[];
|
badges?: LovelaceBadge[];
|
||||||
|
sections?: HuiSection[];
|
||||||
isStrategy: boolean;
|
isStrategy: boolean;
|
||||||
setConfig(config: LovelaceViewConfig): void;
|
setConfig(config: LovelaceViewConfig): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LovelaceSectionElement extends HTMLElement {
|
||||||
|
hass?: HomeAssistant;
|
||||||
|
lovelace?: Lovelace;
|
||||||
|
viewIndex?: number;
|
||||||
|
index?: number;
|
||||||
|
cards?: Array<LovelaceCard | HuiErrorCard>;
|
||||||
|
isStrategy: boolean;
|
||||||
|
setConfig(config: LovelaceSectionConfig): void;
|
||||||
|
}
|
||||||
|
|
||||||
type LovelaceUpdatedEvent = HassEventBase & {
|
type LovelaceUpdatedEvent = HassEventBase & {
|
||||||
event_type: "lovelace_updated";
|
event_type: "lovelace_updated";
|
||||||
data: {
|
data: {
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import type { LovelaceCardConfig } from "./card";
|
||||||
|
import type { LovelaceStrategyConfig } from "./strategy";
|
||||||
|
|
||||||
|
export interface LovelaceBaseSectionConfig {
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
|
||||||
|
type?: string;
|
||||||
|
cards?: LovelaceCardConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LovelaceStrategySectionConfig
|
||||||
|
extends LovelaceBaseSectionConfig {
|
||||||
|
strategy: LovelaceStrategyConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LovelaceSectionRawConfig =
|
||||||
|
| LovelaceSectionConfig
|
||||||
|
| LovelaceStrategySectionConfig;
|
||||||
|
|
||||||
|
export function isStrategySection(
|
||||||
|
section: LovelaceSectionRawConfig
|
||||||
|
): section is LovelaceStrategySectionConfig {
|
||||||
|
return "strategy" in section;
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import type { LovelaceBadgeConfig } from "./badge";
|
import type { LovelaceBadgeConfig } from "./badge";
|
||||||
import type { LovelaceCardConfig } from "./card";
|
import type { LovelaceCardConfig } from "./card";
|
||||||
|
import type { LovelaceSectionRawConfig } from "./section";
|
||||||
import type { LovelaceStrategyConfig } from "./strategy";
|
import type { LovelaceStrategyConfig } from "./strategy";
|
||||||
|
|
||||||
export interface ShowViewConfig {
|
export interface ShowViewConfig {
|
||||||
|
@ -23,6 +24,7 @@ export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
|
||||||
type?: string;
|
type?: string;
|
||||||
badges?: Array<string | LovelaceBadgeConfig>;
|
badges?: Array<string | LovelaceBadgeConfig>;
|
||||||
cards?: LovelaceCardConfig[];
|
cards?: LovelaceCardConfig[];
|
||||||
|
sections?: LovelaceSectionRawConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LovelaceStrategyViewConfig extends LovelaceBaseViewConfig {
|
export interface LovelaceStrategyViewConfig extends LovelaceBaseViewConfig {
|
||||||
|
|
|
@ -27,7 +27,10 @@ import { addEntitiesToLovelaceView } from "../../../lovelace/editor/add-entities
|
||||||
import type { LovelaceRowConfig } from "../../../lovelace/entity-rows/types";
|
import type { LovelaceRowConfig } from "../../../lovelace/entity-rows/types";
|
||||||
import { LovelaceRow } from "../../../lovelace/entity-rows/types";
|
import { LovelaceRow } from "../../../lovelace/entity-rows/types";
|
||||||
import { EntityRegistryStateEntry } from "../ha-config-device-page";
|
import { EntityRegistryStateEntry } from "../ha-config-device-page";
|
||||||
import { computeCards } from "../../../lovelace/common/generate-lovelace-config";
|
import {
|
||||||
|
computeCards,
|
||||||
|
computeSection,
|
||||||
|
} from "../../../lovelace/common/generate-lovelace-config";
|
||||||
|
|
||||||
@customElement("ha-device-entities-card")
|
@customElement("ha-device-entities-card")
|
||||||
export class HaDeviceEntitiesCard extends LitElement {
|
export class HaDeviceEntitiesCard extends LitElement {
|
||||||
|
@ -235,6 +238,9 @@ export class HaDeviceEntitiesCard extends LitElement {
|
||||||
computeCards(this.hass.states, entities, {
|
computeCards(this.hass.states, entities, {
|
||||||
title: this.deviceName,
|
title: this.deviceName,
|
||||||
}),
|
}),
|
||||||
|
computeSection(entities, {
|
||||||
|
title: this.deviceName,
|
||||||
|
}),
|
||||||
entities
|
entities
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,6 +147,16 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getGridSize(): [number, number] {
|
||||||
|
if (
|
||||||
|
(this._config?.show_icon && this._config?.show_name) ||
|
||||||
|
this._config?.show_state
|
||||||
|
) {
|
||||||
|
return [2, 2];
|
||||||
|
}
|
||||||
|
return [1, 1];
|
||||||
|
}
|
||||||
|
|
||||||
public setConfig(config: ButtonCardConfig): void {
|
public setConfig(config: ButtonCardConfig): void {
|
||||||
if (config.entity && !isValidEntityId(config.entity)) {
|
if (config.entity && !isValidEntityId(config.entity)) {
|
||||||
throw new Error("Invalid entity");
|
throw new Error("Invalid entity");
|
||||||
|
|
|
@ -72,6 +72,10 @@ class HuiSensorCard extends HuiEntityCard {
|
||||||
super.setConfig(entityCardConfig);
|
super.setConfig(entityCardConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getSize(): [number, number] {
|
||||||
|
return [2, 2];
|
||||||
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return [
|
return [
|
||||||
HuiEntityCard.styles,
|
HuiEntityCard.styles,
|
||||||
|
|
|
@ -124,6 +124,18 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getGridSize(): [number, number] {
|
||||||
|
const width = 2;
|
||||||
|
let height = 1;
|
||||||
|
if (this._config?.features?.length) {
|
||||||
|
height += Math.ceil((this._config.features.length * 2) / 3);
|
||||||
|
}
|
||||||
|
if (this._config?.vertical) {
|
||||||
|
height++;
|
||||||
|
}
|
||||||
|
return [width, height];
|
||||||
|
}
|
||||||
|
|
||||||
private _handleAction(ev: ActionHandlerEvent) {
|
private _handleAction(ev: ActionHandlerEvent) {
|
||||||
handleAction(this, this.hass!, this._config!, ev.detail.action!);
|
handleAction(this, this.hass!, this._config!, ev.detail.action!);
|
||||||
}
|
}
|
||||||
|
@ -441,12 +453,16 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||||
.secondary=${localizedState}
|
.secondary=${localizedState}
|
||||||
></ha-tile-info>
|
></ha-tile-info>
|
||||||
</div>
|
</div>
|
||||||
<hui-card-features
|
${this._config.features
|
||||||
.hass=${this.hass}
|
? html`
|
||||||
.stateObj=${stateObj}
|
<hui-card-features
|
||||||
.color=${this._config.color}
|
.hass=${this.hass}
|
||||||
.features=${this._config.features}
|
.stateObj=${stateObj}
|
||||||
></hui-card-features>
|
.color=${this._config.color}
|
||||||
|
.features=${this._config.features}
|
||||||
|
></hui-card-features>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -469,6 +485,9 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||||
transition:
|
transition:
|
||||||
box-shadow 180ms ease-in-out,
|
box-shadow 180ms ease-in-out,
|
||||||
border-color 180ms ease-in-out;
|
border-color 180ms ease-in-out;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
ha-card.active {
|
ha-card.active {
|
||||||
--tile-color: var(--state-icon-color);
|
--tile-color: var(--state-icon-color);
|
||||||
|
|
|
@ -545,7 +545,7 @@ export interface TileCardConfig extends LovelaceCardConfig {
|
||||||
state_content?: string | string[];
|
state_content?: string | string[];
|
||||||
icon?: string;
|
icon?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
show_entity_picture?: string;
|
show_entity_picture?: boolean;
|
||||||
vertical?: boolean;
|
vertical?: boolean;
|
||||||
tap_action?: ActionConfig;
|
tap_action?: ActionConfig;
|
||||||
hold_action?: ActionConfig;
|
hold_action?: ActionConfig;
|
||||||
|
|
|
@ -8,12 +8,14 @@ import { stripPrefixFromEntityName } from "../../../common/entity/strip_prefix_f
|
||||||
import { stringCompare } from "../../../common/string/compare";
|
import { stringCompare } from "../../../common/string/compare";
|
||||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||||
import type { AreaFilterValue } from "../../../components/ha-area-filter";
|
import type { AreaFilterValue } from "../../../components/ha-area-filter";
|
||||||
|
import { areaCompare } from "../../../data/area_registry";
|
||||||
import {
|
import {
|
||||||
EnergyPreferences,
|
EnergyPreferences,
|
||||||
GridSourceTypeEnergyPreference,
|
GridSourceTypeEnergyPreference,
|
||||||
} from "../../../data/energy";
|
} from "../../../data/energy";
|
||||||
import { domainToName } from "../../../data/integration";
|
import { domainToName } from "../../../data/integration";
|
||||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||||
|
import { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||||
import { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
import { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||||
import { computeUserInitials } from "../../../data/user";
|
import { computeUserInitials } from "../../../data/user";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
|
@ -25,10 +27,10 @@ import {
|
||||||
PictureCardConfig,
|
PictureCardConfig,
|
||||||
PictureEntityCardConfig,
|
PictureEntityCardConfig,
|
||||||
ThermostatCardConfig,
|
ThermostatCardConfig,
|
||||||
|
TileCardConfig,
|
||||||
} from "../cards/types";
|
} from "../cards/types";
|
||||||
import { EntityConfig } from "../entity-rows/types";
|
import { EntityConfig } from "../entity-rows/types";
|
||||||
import { ButtonsHeaderFooterConfig } from "../header-footer/types";
|
import { ButtonsHeaderFooterConfig } from "../header-footer/types";
|
||||||
import { areaCompare } from "../../../data/area_registry";
|
|
||||||
|
|
||||||
const HIDE_DOMAIN = new Set([
|
const HIDE_DOMAIN = new Set([
|
||||||
"automation",
|
"automation",
|
||||||
|
@ -100,6 +102,24 @@ const splitByAreaDevice = (
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const computeSection = (
|
||||||
|
entityIds: string[],
|
||||||
|
sectionOptions?: Partial<LovelaceSectionConfig>
|
||||||
|
): LovelaceSectionConfig => ({
|
||||||
|
type: "grid",
|
||||||
|
cards: entityIds.map(
|
||||||
|
(entity) =>
|
||||||
|
({
|
||||||
|
type: "tile",
|
||||||
|
entity,
|
||||||
|
show_entity_picture: ["person", "camera", "image"].includes(
|
||||||
|
computeDomain(entity)
|
||||||
|
),
|
||||||
|
}) as TileCardConfig
|
||||||
|
),
|
||||||
|
...sectionOptions,
|
||||||
|
});
|
||||||
|
|
||||||
export const computeCards = (
|
export const computeCards = (
|
||||||
states: HassEntities,
|
states: HassEntities,
|
||||||
entityIds: string[],
|
entityIds: string[],
|
||||||
|
|
|
@ -0,0 +1,303 @@
|
||||||
|
import "@material/mwc-button";
|
||||||
|
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||||
|
import {
|
||||||
|
mdiContentCopy,
|
||||||
|
mdiContentCut,
|
||||||
|
mdiContentDuplicate,
|
||||||
|
mdiDelete,
|
||||||
|
mdiDotsVertical,
|
||||||
|
mdiPencil,
|
||||||
|
} from "@mdi/js";
|
||||||
|
import deepClone from "deep-clone-simple";
|
||||||
|
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { storage } from "../../../common/decorators/storage";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import "../../../components/ha-button-menu";
|
||||||
|
import "../../../components/ha-icon-button";
|
||||||
|
import "../../../components/ha-list-item";
|
||||||
|
import "../../../components/ha-svg-icon";
|
||||||
|
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||||
|
import { haStyle } from "../../../resources/styles";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||||
|
import {
|
||||||
|
LovelaceCardPath,
|
||||||
|
findLovelaceCards,
|
||||||
|
getLovelaceContainerPath,
|
||||||
|
parseLovelaceCardPath,
|
||||||
|
} from "../editor/lovelace-path";
|
||||||
|
import { Lovelace } from "../types";
|
||||||
|
|
||||||
|
@customElement("hui-card-edit-mode")
|
||||||
|
export class HuiCardEditMode extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public lovelace!: Lovelace;
|
||||||
|
|
||||||
|
@property({ type: Array }) public path!: LovelaceCardPath;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public hiddenOverlay = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
public _menuOpened: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
public _hover: boolean = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
public _focused: boolean = false;
|
||||||
|
|
||||||
|
@storage({
|
||||||
|
key: "lovelaceClipboard",
|
||||||
|
state: false,
|
||||||
|
subscribe: false,
|
||||||
|
storage: "sessionStorage",
|
||||||
|
})
|
||||||
|
protected _clipboard?: LovelaceCardConfig;
|
||||||
|
|
||||||
|
private get _cards() {
|
||||||
|
const containerPath = getLovelaceContainerPath(this.path!);
|
||||||
|
return findLovelaceCards(this.lovelace!.config, containerPath)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _touchStarted = false;
|
||||||
|
|
||||||
|
protected firstUpdated(): void {
|
||||||
|
this.addEventListener("focus", () => {
|
||||||
|
this._focused = true;
|
||||||
|
});
|
||||||
|
this.addEventListener("blur", () => {
|
||||||
|
this._focused = false;
|
||||||
|
});
|
||||||
|
this.addEventListener("touchstart", () => {
|
||||||
|
this._touchStarted = true;
|
||||||
|
});
|
||||||
|
this.addEventListener("touchend", () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this._touchStarted = false;
|
||||||
|
}, 10);
|
||||||
|
});
|
||||||
|
this.addEventListener("mouseenter", () => {
|
||||||
|
if (this._touchStarted) return;
|
||||||
|
this._hover = true;
|
||||||
|
});
|
||||||
|
this.addEventListener("mouseout", () => {
|
||||||
|
this._hover = false;
|
||||||
|
});
|
||||||
|
this.addEventListener("click", () => {
|
||||||
|
this._hover = true;
|
||||||
|
document.addEventListener("click", this._documentClicked);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
document.removeEventListener("click", this._documentClicked);
|
||||||
|
}
|
||||||
|
|
||||||
|
_documentClicked = (ev) => {
|
||||||
|
this._hover = ev.composedPath().includes(this);
|
||||||
|
document.removeEventListener("click", this._documentClicked);
|
||||||
|
};
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
const showOverlay =
|
||||||
|
(this._hover || this._menuOpened || this._focused) && !this.hiddenOverlay;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="card-wrapper" inert><slot></slot></div>
|
||||||
|
<div class="card-overlay ${classMap({ visible: showOverlay })}">
|
||||||
|
<div
|
||||||
|
class="edit"
|
||||||
|
@click=${this._editCard}
|
||||||
|
@keydown=${this._editCard}
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div class="edit-overlay"></div>
|
||||||
|
<ha-svg-icon class="edit" .path=${mdiPencil}> </ha-svg-icon>
|
||||||
|
</div>
|
||||||
|
<ha-button-menu
|
||||||
|
class="more"
|
||||||
|
corner="BOTTOM_END"
|
||||||
|
menuCorner="END"
|
||||||
|
.path=${[this.path!]}
|
||||||
|
@action=${this._handleAction}
|
||||||
|
@opened=${this._handleOpened}
|
||||||
|
@closed=${this._handleClosed}
|
||||||
|
>
|
||||||
|
<ha-icon-button slot="trigger" .path=${mdiDotsVertical}>
|
||||||
|
</ha-icon-button>
|
||||||
|
<ha-list-item graphic="icon">
|
||||||
|
<ha-svg-icon
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiContentDuplicate}
|
||||||
|
></ha-svg-icon>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.edit_card.duplicate"
|
||||||
|
)}
|
||||||
|
</ha-list-item>
|
||||||
|
<ha-list-item graphic="icon">
|
||||||
|
<ha-svg-icon slot="graphic" .path=${mdiContentCopy}></ha-svg-icon>
|
||||||
|
${this.hass.localize("ui.panel.lovelace.editor.edit_card.copy")}
|
||||||
|
</ha-list-item>
|
||||||
|
<ha-list-item graphic="icon">
|
||||||
|
<ha-svg-icon slot="graphic" .path=${mdiContentCut}></ha-svg-icon>
|
||||||
|
${this.hass.localize("ui.panel.lovelace.editor.edit_card.cut")}
|
||||||
|
</ha-list-item>
|
||||||
|
<li divider role="separator"></li>
|
||||||
|
<ha-list-item graphic="icon" class="warning">
|
||||||
|
${this.hass.localize("ui.panel.lovelace.editor.edit_card.delete")}
|
||||||
|
<ha-svg-icon
|
||||||
|
class="warning"
|
||||||
|
slot="graphic"
|
||||||
|
.path=${mdiDelete}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</ha-list-item>
|
||||||
|
</ha-button-menu>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleOpened() {
|
||||||
|
this._menuOpened = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleClosed() {
|
||||||
|
this._menuOpened = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleAction(ev: CustomEvent<ActionDetail>) {
|
||||||
|
switch (ev.detail.index) {
|
||||||
|
case 0:
|
||||||
|
this._duplicateCard();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
this._copyCard();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
this._cutCard();
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
this._deleteCard(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _duplicateCard(): void {
|
||||||
|
const { cardIndex } = parseLovelaceCardPath(this.path!);
|
||||||
|
const containerPath = getLovelaceContainerPath(this.path!);
|
||||||
|
const cardConfig = this._cards![cardIndex];
|
||||||
|
showEditCardDialog(this, {
|
||||||
|
lovelaceConfig: this.lovelace!.config,
|
||||||
|
saveConfig: this.lovelace!.saveConfig,
|
||||||
|
path: containerPath,
|
||||||
|
cardConfig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _editCard(ev): void {
|
||||||
|
if (ev.defaultPrevented) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
fireEvent(this, "ll-edit-card", { path: this.path! });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _cutCard(): void {
|
||||||
|
this._copyCard();
|
||||||
|
this._deleteCard(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _copyCard(): void {
|
||||||
|
const { cardIndex } = parseLovelaceCardPath(this.path!);
|
||||||
|
const cardConfig = this._cards[cardIndex];
|
||||||
|
this._clipboard = deepClone(cardConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _deleteCard(confirm: boolean): void {
|
||||||
|
fireEvent(this, "ll-delete-card", { path: this.path!, confirm });
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
.card-overlay {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: opacity 180ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-overlay.visible {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-wrapper {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit {
|
||||||
|
outline: none !important;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--ha-card-border-radius, 12px);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.edit-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0.8;
|
||||||
|
background-color: var(--primary-background-color);
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
border-radius: var(--ha-card-border-radius, 12px);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.edit ha-svg-icon {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--secondary-background-color);
|
||||||
|
--mdc-icon-size: 24px;
|
||||||
|
}
|
||||||
|
.more {
|
||||||
|
position: absolute;
|
||||||
|
right: -6px;
|
||||||
|
top: -6px;
|
||||||
|
}
|
||||||
|
.more ha-icon-button {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--secondary-background-color);
|
||||||
|
--mdc-icon-button-size: 32px;
|
||||||
|
--mdc-icon-size: 20px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-card-edit-mode": HuiCardEditMode;
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,7 +28,6 @@ import "../../../components/ha-icon-button";
|
||||||
import "../../../components/ha-list-item";
|
import "../../../components/ha-list-item";
|
||||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||||
import { saveConfig } from "../../../data/lovelace/config/types";
|
import { saveConfig } from "../../../data/lovelace/config/types";
|
||||||
import { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
|
||||||
import {
|
import {
|
||||||
showAlertDialog,
|
showAlertDialog,
|
||||||
showPromptDialog,
|
showPromptDialog,
|
||||||
|
@ -41,10 +40,15 @@ import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"
|
||||||
import {
|
import {
|
||||||
addCard,
|
addCard,
|
||||||
deleteCard,
|
deleteCard,
|
||||||
moveCard,
|
moveCardToContainer,
|
||||||
moveCardToPosition,
|
moveCardToIndex,
|
||||||
swapCard,
|
|
||||||
} from "../editor/config-util";
|
} from "../editor/config-util";
|
||||||
|
import {
|
||||||
|
LovelaceCardPath,
|
||||||
|
findLovelaceCards,
|
||||||
|
getLovelaceContainerPath,
|
||||||
|
parseLovelaceCardPath,
|
||||||
|
} from "../editor/lovelace-path";
|
||||||
import { showSelectViewDialog } from "../editor/select-view/show-select-view-dialog";
|
import { showSelectViewDialog } from "../editor/select-view/show-select-view-dialog";
|
||||||
import { Lovelace, LovelaceCard } from "../types";
|
import { Lovelace, LovelaceCard } from "../types";
|
||||||
|
|
||||||
|
@ -54,7 +58,7 @@ export class HuiCardOptions extends LitElement {
|
||||||
|
|
||||||
@property({ attribute: false }) public lovelace?: Lovelace;
|
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||||
|
|
||||||
@property({ type: Array }) public path?: [number, number];
|
@property({ type: Array }) public path?: LovelaceCardPath;
|
||||||
|
|
||||||
@queryAssignedNodes() private _assignedNodes?: NodeListOf<LovelaceCard>;
|
@queryAssignedNodes() private _assignedNodes?: NodeListOf<LovelaceCard>;
|
||||||
|
|
||||||
|
@ -76,17 +80,21 @@ export class HuiCardOptions extends LitElement {
|
||||||
if (!changedProps.has("path") || !this.path) {
|
if (!changedProps.has("path") || !this.path) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const { viewIndex } = parseLovelaceCardPath(this.path);
|
||||||
this.classList.toggle(
|
this.classList.toggle(
|
||||||
"panel",
|
"panel",
|
||||||
this.lovelace!.config.views[this.path![0]].panel
|
this.lovelace!.config.views[viewIndex].panel
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _currentView() {
|
private get _cards() {
|
||||||
return this.lovelace!.config.views[this.path![0]] as LovelaceViewConfig;
|
const containerPath = getLovelaceContainerPath(this.path!);
|
||||||
|
return findLovelaceCards(this.lovelace!.config, containerPath)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
|
const { cardIndex } = parseLovelaceCardPath(this.path!);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="card"><slot></slot></div>
|
<div class="card"><slot></slot></div>
|
||||||
<ha-card>
|
<ha-card>
|
||||||
|
@ -107,7 +115,7 @@ export class HuiCardOptions extends LitElement {
|
||||||
.path=${mdiMinus}
|
.path=${mdiMinus}
|
||||||
class="move-arrow"
|
class="move-arrow"
|
||||||
@click=${this._decreaseCardPosiion}
|
@click=${this._decreaseCardPosiion}
|
||||||
?disabled=${this.path![1] === 0}
|
?disabled=${cardIndex === 0}
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
@click=${this._changeCardPosition}
|
@click=${this._changeCardPosition}
|
||||||
|
@ -115,7 +123,7 @@ export class HuiCardOptions extends LitElement {
|
||||||
"ui.panel.lovelace.editor.edit_card.change_position"
|
"ui.panel.lovelace.editor.edit_card.change_position"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div class="position-badge">${this.path![1] + 1}</div>
|
<div class="position-badge">${cardIndex + 1}</div>
|
||||||
</ha-icon-button>
|
</ha-icon-button>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
.label=${this.hass!.localize(
|
.label=${this.hass!.localize(
|
||||||
|
@ -124,8 +132,7 @@ export class HuiCardOptions extends LitElement {
|
||||||
.path=${mdiPlus}
|
.path=${mdiPlus}
|
||||||
class="move-arrow"
|
class="move-arrow"
|
||||||
@click=${this._increaseCardPosition}
|
@click=${this._increaseCardPosition}
|
||||||
.disabled=${this._currentView.cards!.length ===
|
.disabled=${this._cards!.length === cardIndex + 1}
|
||||||
this.path![1] + 1}
|
|
||||||
></ha-icon-button>
|
></ha-icon-button>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
@ -271,13 +278,14 @@ export class HuiCardOptions extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _duplicateCard(): void {
|
private _duplicateCard(): void {
|
||||||
const path = this.path!;
|
const { cardIndex } = parseLovelaceCardPath(this.path!);
|
||||||
const cardConfig = this._currentView.cards![path[1]];
|
const containerPath = getLovelaceContainerPath(this.path!);
|
||||||
|
const cardConfig = this._cards![cardIndex];
|
||||||
showEditCardDialog(this, {
|
showEditCardDialog(this, {
|
||||||
lovelaceConfig: this.lovelace!.config,
|
lovelaceConfig: this.lovelace!.config,
|
||||||
saveConfig: this.lovelace!.saveConfig,
|
saveConfig: this.lovelace!.saveConfig,
|
||||||
path: [path[0], null],
|
path: containerPath,
|
||||||
newCardConfig: cardConfig,
|
cardConfig,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -291,30 +299,29 @@ export class HuiCardOptions extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _copyCard(): void {
|
private _copyCard(): void {
|
||||||
const cardConfig = this._currentView.cards![this.path![1]];
|
const { cardIndex } = parseLovelaceCardPath(this.path!);
|
||||||
|
const cardConfig = this._cards[cardIndex];
|
||||||
this._clipboard = deepClone(cardConfig);
|
this._clipboard = deepClone(cardConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _decreaseCardPosiion(): void {
|
private _decreaseCardPosiion(): void {
|
||||||
const lovelace = this.lovelace!;
|
const lovelace = this.lovelace!;
|
||||||
const path = this.path!;
|
const path = this.path!;
|
||||||
lovelace.saveConfig(
|
const { cardIndex } = parseLovelaceCardPath(path);
|
||||||
swapCard(lovelace.config, path, [path[0], path[1] - 1])
|
lovelace.saveConfig(moveCardToIndex(lovelace.config, path, cardIndex - 1));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _increaseCardPosition(): void {
|
private _increaseCardPosition(): void {
|
||||||
const lovelace = this.lovelace!;
|
const lovelace = this.lovelace!;
|
||||||
const path = this.path!;
|
const path = this.path!;
|
||||||
lovelace.saveConfig(
|
const { cardIndex } = parseLovelaceCardPath(path);
|
||||||
swapCard(lovelace.config, path, [path[0], path[1] + 1])
|
lovelace.saveConfig(moveCardToIndex(lovelace.config, path, cardIndex + 1));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _changeCardPosition(): Promise<void> {
|
private async _changeCardPosition(): Promise<void> {
|
||||||
const lovelace = this.lovelace!;
|
const lovelace = this.lovelace!;
|
||||||
const path = this.path!;
|
const path = this.path!;
|
||||||
|
const { cardIndex } = parseLovelaceCardPath(path);
|
||||||
const positionString = await showPromptDialog(this, {
|
const positionString = await showPromptDialog(this, {
|
||||||
title: this.hass!.localize(
|
title: this.hass!.localize(
|
||||||
"ui.panel.lovelace.editor.change_position.title"
|
"ui.panel.lovelace.editor.change_position.title"
|
||||||
|
@ -324,7 +331,7 @@ export class HuiCardOptions extends LitElement {
|
||||||
),
|
),
|
||||||
inputType: "number",
|
inputType: "number",
|
||||||
inputMin: "1",
|
inputMin: "1",
|
||||||
placeholder: String(path[1] + 1),
|
placeholder: String(cardIndex + 1),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!positionString) return;
|
if (!positionString) return;
|
||||||
|
@ -333,7 +340,8 @@ export class HuiCardOptions extends LitElement {
|
||||||
|
|
||||||
if (isNaN(position)) return;
|
if (isNaN(position)) return;
|
||||||
|
|
||||||
lovelace.saveConfig(moveCardToPosition(lovelace.config, path, position));
|
const newIndex = position - 1;
|
||||||
|
lovelace.saveConfig(moveCardToIndex(lovelace.config, path, newIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _moveCard(): void {
|
private _moveCard(): void {
|
||||||
|
@ -345,20 +353,17 @@ export class HuiCardOptions extends LitElement {
|
||||||
viewSelectedCallback: async (urlPath, selectedDashConfig, viewIndex) => {
|
viewSelectedCallback: async (urlPath, selectedDashConfig, viewIndex) => {
|
||||||
if (urlPath === this.lovelace!.urlPath) {
|
if (urlPath === this.lovelace!.urlPath) {
|
||||||
this.lovelace!.saveConfig(
|
this.lovelace!.saveConfig(
|
||||||
moveCard(this.lovelace!.config, this.path!, [viewIndex])
|
moveCardToContainer(this.lovelace!.config, this.path!, [viewIndex])
|
||||||
);
|
);
|
||||||
showSaveSuccessToast(this, this.hass!);
|
showSaveSuccessToast(this, this.hass!);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const { cardIndex } = parseLovelaceCardPath(this.path!);
|
||||||
await saveConfig(
|
await saveConfig(
|
||||||
this.hass!,
|
this.hass!,
|
||||||
urlPath,
|
urlPath,
|
||||||
addCard(
|
addCard(selectedDashConfig, [viewIndex], this._cards[cardIndex])
|
||||||
selectedDashConfig,
|
|
||||||
[viewIndex],
|
|
||||||
this._currentView.cards![this.path![1]]
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
this.lovelace!.saveConfig(
|
this.lovelace!.saveConfig(
|
||||||
deleteCard(this.lovelace!.config, this.path!)
|
deleteCard(this.lovelace!.config, this.path!)
|
||||||
|
|
|
@ -24,6 +24,7 @@ const ALWAYS_LOADED_TYPES = new Set([
|
||||||
"entity-button",
|
"entity-button",
|
||||||
"glance",
|
"glance",
|
||||||
"grid",
|
"grid",
|
||||||
|
"section",
|
||||||
"light",
|
"light",
|
||||||
"sensor",
|
"sensor",
|
||||||
"thermostat",
|
"thermostat",
|
||||||
|
|
|
@ -1,27 +1,31 @@
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import { LovelaceViewElement } from "../../../data/lovelace";
|
import {
|
||||||
|
LovelaceSectionElement,
|
||||||
|
LovelaceViewElement,
|
||||||
|
} from "../../../data/lovelace";
|
||||||
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||||
|
import { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||||
import { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
import { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||||
import {
|
import {
|
||||||
isCustomType,
|
isCustomType,
|
||||||
stripCustomPrefix,
|
stripCustomPrefix,
|
||||||
} from "../../../data/lovelace_custom_cards";
|
} from "../../../data/lovelace_custom_cards";
|
||||||
|
import { LovelaceCardFeatureConfig } from "../card-features/types";
|
||||||
import type { HuiErrorCard } from "../cards/hui-error-card";
|
import type { HuiErrorCard } from "../cards/hui-error-card";
|
||||||
import type { ErrorCardConfig } from "../cards/types";
|
import type { ErrorCardConfig } from "../cards/types";
|
||||||
import { LovelaceElement, LovelaceElementConfig } from "../elements/types";
|
import { LovelaceElement, LovelaceElementConfig } from "../elements/types";
|
||||||
import { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types";
|
import { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types";
|
||||||
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||||
import { LovelaceCardFeatureConfig } from "../card-features/types";
|
|
||||||
import {
|
import {
|
||||||
LovelaceBadge,
|
LovelaceBadge,
|
||||||
LovelaceCard,
|
LovelaceCard,
|
||||||
LovelaceCardConstructor,
|
LovelaceCardConstructor,
|
||||||
|
LovelaceCardFeature,
|
||||||
|
LovelaceCardFeatureConstructor,
|
||||||
LovelaceHeaderFooter,
|
LovelaceHeaderFooter,
|
||||||
LovelaceHeaderFooterConstructor,
|
LovelaceHeaderFooterConstructor,
|
||||||
LovelaceRowConstructor,
|
LovelaceRowConstructor,
|
||||||
LovelaceCardFeature,
|
|
||||||
LovelaceCardFeatureConstructor,
|
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
const TIMEOUT = 2000;
|
const TIMEOUT = 2000;
|
||||||
|
@ -62,6 +66,11 @@ interface CreateElementConfigTypes {
|
||||||
element: LovelaceCardFeature;
|
element: LovelaceCardFeature;
|
||||||
constructor: LovelaceCardFeatureConstructor;
|
constructor: LovelaceCardFeatureConstructor;
|
||||||
};
|
};
|
||||||
|
section: {
|
||||||
|
config: LovelaceSectionConfig;
|
||||||
|
element: LovelaceSectionElement;
|
||||||
|
constructor: unknown;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createErrorCardElement = (config: ErrorCardConfig) => {
|
export const createErrorCardElement = (config: ErrorCardConfig) => {
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { LovelaceSectionElement } from "../../../data/lovelace";
|
||||||
|
import { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||||
|
import { HuiErrorCard } from "../cards/hui-error-card";
|
||||||
|
import "../sections/hui-grid-section";
|
||||||
|
import { createLovelaceElement } from "./create-element-base";
|
||||||
|
|
||||||
|
const ALWAYS_LOADED_LAYOUTS = new Set(["grid"]);
|
||||||
|
|
||||||
|
const LAZY_LOAD_LAYOUTS = {};
|
||||||
|
|
||||||
|
export const createSectionElement = (
|
||||||
|
config: LovelaceSectionConfig
|
||||||
|
): LovelaceSectionElement | HuiErrorCard =>
|
||||||
|
createLovelaceElement(
|
||||||
|
"section",
|
||||||
|
config,
|
||||||
|
ALWAYS_LOADED_LAYOUTS,
|
||||||
|
LAZY_LOAD_LAYOUTS
|
||||||
|
);
|
|
@ -9,6 +9,7 @@ const ALWAYS_LOADED_LAYOUTS = new Set(["masonry"]);
|
||||||
const LAZY_LOAD_LAYOUTS = {
|
const LAZY_LOAD_LAYOUTS = {
|
||||||
panel: () => import("../views/hui-panel-view"),
|
panel: () => import("../views/hui-panel-view"),
|
||||||
sidebar: () => import("../views/hui-sidebar-view"),
|
sidebar: () => import("../views/hui-sidebar-view"),
|
||||||
|
sections: () => import("../views/hui-sections-view"),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createViewElement = (
|
export const createViewElement = (
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { LovelacePanelConfig } from "../../../data/lovelace";
|
import { LovelacePanelConfig } from "../../../data/lovelace";
|
||||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||||
|
import { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||||
import {
|
import {
|
||||||
LovelaceConfig,
|
LovelaceConfig,
|
||||||
fetchConfig,
|
fetchConfig,
|
||||||
|
@ -15,6 +16,7 @@ export const addEntitiesToLovelaceView = async (
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cardConfig: LovelaceCardConfig[],
|
cardConfig: LovelaceCardConfig[],
|
||||||
|
sectionConfig?: LovelaceSectionConfig,
|
||||||
entities?: string[]
|
entities?: string[]
|
||||||
) => {
|
) => {
|
||||||
hass.loadFragmentTranslation("lovelace");
|
hass.loadFragmentTranslation("lovelace");
|
||||||
|
@ -71,6 +73,7 @@ export const addEntitiesToLovelaceView = async (
|
||||||
// all storage dashboards are generated, but we have YAML dashboards just show the YAML config
|
// all storage dashboards are generated, but we have YAML dashboards just show the YAML config
|
||||||
showSuggestCardDialog(element, {
|
showSuggestCardDialog(element, {
|
||||||
cardConfig,
|
cardConfig,
|
||||||
|
sectionConfig,
|
||||||
entities,
|
entities,
|
||||||
yaml: true,
|
yaml: true,
|
||||||
});
|
});
|
||||||
|
@ -93,6 +96,7 @@ export const addEntitiesToLovelaceView = async (
|
||||||
if (!storageDashs.length && lovelaceConfig.views.length === 1) {
|
if (!storageDashs.length && lovelaceConfig.views.length === 1) {
|
||||||
showSuggestCardDialog(element, {
|
showSuggestCardDialog(element, {
|
||||||
cardConfig,
|
cardConfig,
|
||||||
|
sectionConfig,
|
||||||
lovelaceConfig: lovelaceConfig!,
|
lovelaceConfig: lovelaceConfig!,
|
||||||
saveConfig: async (newConfig: LovelaceConfig): Promise<void> => {
|
saveConfig: async (newConfig: LovelaceConfig): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
@ -116,6 +120,7 @@ export const addEntitiesToLovelaceView = async (
|
||||||
viewSelectedCallback: (newUrlPath, selectedDashConfig, viewIndex) => {
|
viewSelectedCallback: (newUrlPath, selectedDashConfig, viewIndex) => {
|
||||||
showSuggestCardDialog(element, {
|
showSuggestCardDialog(element, {
|
||||||
cardConfig,
|
cardConfig,
|
||||||
|
sectionConfig,
|
||||||
lovelaceConfig: selectedDashConfig,
|
lovelaceConfig: selectedDashConfig,
|
||||||
saveConfig: async (newConfig: LovelaceConfig): Promise<void> => {
|
saveConfig: async (newConfig: LovelaceConfig): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { until } from "lit/directives/until";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { storage } from "../../../../common/decorators/storage";
|
import { storage } from "../../../../common/decorators/storage";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import { stringCompare } from "../../../../common/string/compare";
|
||||||
import "../../../../components/ha-circular-progress";
|
import "../../../../components/ha-circular-progress";
|
||||||
import "../../../../components/search-input";
|
import "../../../../components/search-input";
|
||||||
import { isUnavailableState } from "../../../../data/entity";
|
import { isUnavailableState } from "../../../../data/entity";
|
||||||
|
@ -46,6 +47,8 @@ interface CardElement {
|
||||||
export class HuiCardPicker extends LitElement {
|
export class HuiCardPicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public suggestedCards?: string[];
|
||||||
|
|
||||||
@storage({
|
@storage({
|
||||||
key: "lovelaceClipboard",
|
key: "lovelaceClipboard",
|
||||||
state: true,
|
state: true,
|
||||||
|
@ -92,6 +95,29 @@ export class HuiCardPicker extends LitElement {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private _suggestedCards = memoizeOne(
|
||||||
|
(cardElements: CardElement[]): CardElement[] =>
|
||||||
|
cardElements.filter(
|
||||||
|
(cardElement: CardElement) => cardElement.card.isSuggested
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
private _customCards = memoizeOne(
|
||||||
|
(cardElements: CardElement[]): CardElement[] =>
|
||||||
|
cardElements.filter(
|
||||||
|
(cardElement: CardElement) =>
|
||||||
|
cardElement.card.isCustom && !cardElement.card.isSuggested
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
private _otherCards = memoizeOne(
|
||||||
|
(cardElements: CardElement[]): CardElement[] =>
|
||||||
|
cardElements.filter(
|
||||||
|
(cardElement: CardElement) =>
|
||||||
|
!cardElement.card.isSuggested && !cardElement.card.isCustom
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (
|
if (
|
||||||
!this.hass ||
|
!this.hass ||
|
||||||
|
@ -102,6 +128,10 @@ export class HuiCardPicker extends LitElement {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const suggestedCards = this._suggestedCards(this._cards);
|
||||||
|
const othersCards = this._otherCards(this._cards);
|
||||||
|
const customCardsItems = this._customCards(this._cards);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<search-input
|
<search-input
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
@ -119,39 +149,49 @@ export class HuiCardPicker extends LitElement {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div class="cards-container">
|
<div class="cards-container">
|
||||||
${this._clipboard && !this._filter
|
${this._filter
|
||||||
? html`
|
? this._filterCards(this._cards, this._filter).map(
|
||||||
${until(
|
(cardElement: CardElement) => cardElement.element
|
||||||
this._renderCardElement(
|
)
|
||||||
{
|
: html`
|
||||||
type: this._clipboard.type,
|
${suggestedCards.length > 0
|
||||||
showElement: true,
|
? html`
|
||||||
isCustom: false,
|
<div class="cards-container-header">
|
||||||
name: this.hass!.localize(
|
${this.hass!.localize(
|
||||||
"ui.panel.lovelace.editor.card.generic.paste"
|
`ui.panel.lovelace.editor.card.generic.suggested_cards`
|
||||||
),
|
)}
|
||||||
description: `${this.hass!.localize(
|
</div>
|
||||||
"ui.panel.lovelace.editor.card.generic.paste_description",
|
`
|
||||||
{
|
: nothing}
|
||||||
type: this._clipboard.type,
|
${this._renderClipboardCard()}
|
||||||
}
|
${suggestedCards.map(
|
||||||
)}`,
|
(cardElement: CardElement) => cardElement.element
|
||||||
},
|
|
||||||
this._clipboard
|
|
||||||
),
|
|
||||||
html`
|
|
||||||
<div class="card spinner">
|
|
||||||
<ha-circular-progress
|
|
||||||
indeterminate
|
|
||||||
></ha-circular-progress>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
)}
|
||||||
`
|
${suggestedCards.length > 0
|
||||||
: nothing}
|
? html`
|
||||||
${this._filterCards(this._cards, this._filter).map(
|
<div class="cards-container-header">
|
||||||
(cardElement: CardElement) => cardElement.element
|
${this.hass!.localize(
|
||||||
)}
|
`ui.panel.lovelace.editor.card.generic.other_cards`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${othersCards.map(
|
||||||
|
(cardElement: CardElement) => cardElement.element
|
||||||
|
)}
|
||||||
|
${customCardsItems.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="cards-container-header">
|
||||||
|
${this.hass!.localize(
|
||||||
|
`ui.panel.lovelace.editor.card.generic.custom_cards`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${customCardsItems.map(
|
||||||
|
(cardElement: CardElement) => cardElement.element
|
||||||
|
)}
|
||||||
|
`}
|
||||||
</div>
|
</div>
|
||||||
<div class="cards-container">
|
<div class="cards-container">
|
||||||
<div
|
<div
|
||||||
|
@ -218,8 +258,24 @@ export class HuiCardPicker extends LitElement {
|
||||||
description: this.hass!.localize(
|
description: this.hass!.localize(
|
||||||
`ui.panel.lovelace.editor.card.${card.type}.description`
|
`ui.panel.lovelace.editor.card.${card.type}.description`
|
||||||
),
|
),
|
||||||
|
isSuggested: this.suggestedCards?.includes(card.type) || false,
|
||||||
...card,
|
...card,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
cards = cards.sort((a, b) => {
|
||||||
|
if (a.isSuggested && !b.isSuggested) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (!a.isSuggested && b.isSuggested) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return stringCompare(
|
||||||
|
a.name || a.type,
|
||||||
|
b.name || b.type,
|
||||||
|
this.hass?.language
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
if (customCards.length > 0) {
|
if (customCards.length > 0) {
|
||||||
cards = cards.concat(
|
cards = cards.concat(
|
||||||
customCards.map((ccard: CustomCardEntry) => ({
|
customCards.map((ccard: CustomCardEntry) => ({
|
||||||
|
@ -244,6 +300,37 @@ export class HuiCardPicker extends LitElement {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _renderClipboardCard() {
|
||||||
|
if (!this._clipboard) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html` ${until(
|
||||||
|
this._renderCardElement(
|
||||||
|
{
|
||||||
|
type: this._clipboard.type,
|
||||||
|
showElement: true,
|
||||||
|
isCustom: false,
|
||||||
|
name: this.hass!.localize(
|
||||||
|
"ui.panel.lovelace.editor.card.generic.paste"
|
||||||
|
),
|
||||||
|
description: `${this.hass!.localize(
|
||||||
|
"ui.panel.lovelace.editor.card.generic.paste_description",
|
||||||
|
{
|
||||||
|
type: this._clipboard.type,
|
||||||
|
}
|
||||||
|
)}`,
|
||||||
|
},
|
||||||
|
this._clipboard
|
||||||
|
),
|
||||||
|
html`
|
||||||
|
<div class="card spinner">
|
||||||
|
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
|
||||||
private _handleSearchChange(ev: CustomEvent) {
|
private _handleSearchChange(ev: CustomEvent) {
|
||||||
const value = ev.detail.value;
|
const value = ev.detail.value;
|
||||||
|
|
||||||
|
@ -381,6 +468,14 @@ export class HuiCardPicker extends LitElement {
|
||||||
margin: var(--card-picker-search-margin);
|
margin: var(--card-picker-search-margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cards-container-header {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 12px 8px 4px 8px;
|
||||||
|
margin: 0;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
.cards-container {
|
.cards-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-gap: 8px 8px;
|
grid-gap: 8px 8px;
|
||||||
|
@ -455,6 +550,23 @@ export class HuiCardPicker extends LitElement {
|
||||||
.manual {
|
.manual {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px
|
||||||
|
inset-inline-start: 8px;
|
||||||
|
inset-inline-end: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
--mdc-icon-size: 16px;
|
||||||
|
line-height: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
.icon.custom {
|
||||||
|
background: var(--warning-color);
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,16 +12,27 @@ import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||||
import { DataTableRowData } from "../../../../components/data-table/ha-data-table";
|
import { DataTableRowData } from "../../../../components/data-table/ha-data-table";
|
||||||
import "../../../../components/ha-dialog";
|
import "../../../../components/ha-dialog";
|
||||||
import "../../../../components/ha-dialog-header";
|
import "../../../../components/ha-dialog-header";
|
||||||
|
import {
|
||||||
|
isStrategySection,
|
||||||
|
LovelaceSectionConfig,
|
||||||
|
} from "../../../../data/lovelace/config/section";
|
||||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||||
import { haStyleDialog } from "../../../../resources/styles";
|
import { haStyleDialog } from "../../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import {
|
||||||
|
computeCards,
|
||||||
|
computeSection,
|
||||||
|
} from "../../common/generate-lovelace-config";
|
||||||
import "./hui-card-picker";
|
import "./hui-card-picker";
|
||||||
import "./hui-entity-picker-table";
|
import "./hui-entity-picker-table";
|
||||||
import { CreateCardDialogParams } from "./show-create-card-dialog";
|
import { CreateCardDialogParams } from "./show-create-card-dialog";
|
||||||
import { showEditCardDialog } from "./show-edit-card-dialog";
|
import { showEditCardDialog } from "./show-edit-card-dialog";
|
||||||
import { showSuggestCardDialog } from "./show-suggest-card-dialog";
|
import { showSuggestCardDialog } from "./show-suggest-card-dialog";
|
||||||
import { computeCards } from "../../common/generate-lovelace-config";
|
import {
|
||||||
|
findLovelaceContainer,
|
||||||
|
parseLovelaceContainerPath,
|
||||||
|
} from "../lovelace-path";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
|
@ -42,7 +53,9 @@ export class HuiCreateDialogCard
|
||||||
|
|
||||||
@state() private _params?: CreateCardDialogParams;
|
@state() private _params?: CreateCardDialogParams;
|
||||||
|
|
||||||
@state() private _viewConfig!: LovelaceViewConfig;
|
@state() private _containerConfig!:
|
||||||
|
| LovelaceViewConfig
|
||||||
|
| LovelaceSectionConfig;
|
||||||
|
|
||||||
@state() private _selectedEntities: string[] = [];
|
@state() private _selectedEntities: string[] = [];
|
||||||
|
|
||||||
|
@ -50,8 +63,17 @@ export class HuiCreateDialogCard
|
||||||
|
|
||||||
public async showDialog(params: CreateCardDialogParams): Promise<void> {
|
public async showDialog(params: CreateCardDialogParams): Promise<void> {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
const [view] = params.path;
|
|
||||||
this._viewConfig = params.lovelaceConfig.views[view];
|
const containerConfig = findLovelaceContainer(
|
||||||
|
params.lovelaceConfig,
|
||||||
|
params.path
|
||||||
|
);
|
||||||
|
|
||||||
|
if ("strategy" in containerConfig) {
|
||||||
|
throw new Error("Can't edit strategy");
|
||||||
|
}
|
||||||
|
|
||||||
|
this._containerConfig = containerConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): boolean {
|
public closeDialog(): boolean {
|
||||||
|
@ -67,10 +89,10 @@ export class HuiCreateDialogCard
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = this._viewConfig.title
|
const title = this._containerConfig.title
|
||||||
? this.hass!.localize(
|
? this.hass!.localize(
|
||||||
"ui.panel.lovelace.editor.edit_card.pick_card_view_title",
|
"ui.panel.lovelace.editor.edit_card.pick_card_title",
|
||||||
{ name: `"${this._viewConfig.title}"` }
|
{ name: `"${this._containerConfig.title}"` }
|
||||||
)
|
)
|
||||||
: this.hass!.localize("ui.panel.lovelace.editor.edit_card.pick_card");
|
: this.hass!.localize("ui.panel.lovelace.editor.edit_card.pick_card");
|
||||||
|
|
||||||
|
@ -112,6 +134,7 @@ export class HuiCreateDialogCard
|
||||||
this._currTabIndex === 0
|
this._currTabIndex === 0
|
||||||
? html`
|
? html`
|
||||||
<hui-card-picker
|
<hui-card-picker
|
||||||
|
.suggestedCards=${this._params.suggestedCards}
|
||||||
.lovelace=${this._params.lovelaceConfig}
|
.lovelace=${this._params.lovelaceConfig}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@config-changed=${this._handleCardPicked}
|
@config-changed=${this._handleCardPicked}
|
||||||
|
@ -214,8 +237,8 @@ export class HuiCreateDialogCard
|
||||||
showEditCardDialog(this, {
|
showEditCardDialog(this, {
|
||||||
lovelaceConfig: this._params!.lovelaceConfig,
|
lovelaceConfig: this._params!.lovelaceConfig,
|
||||||
saveConfig: this._params!.saveConfig,
|
saveConfig: this._params!.saveConfig,
|
||||||
path: [this._params!.path[0], null],
|
path: this._params!.path,
|
||||||
newCardConfig: config,
|
cardConfig: config,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
|
@ -248,12 +271,36 @@ export class HuiCreateDialogCard
|
||||||
this._selectedEntities,
|
this._selectedEntities,
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let sectionOptions: Partial<LovelaceSectionConfig> = {};
|
||||||
|
|
||||||
|
const { sectionIndex } = parseLovelaceContainerPath(this._params!.path);
|
||||||
|
const isSection = sectionIndex !== undefined;
|
||||||
|
|
||||||
|
// If we are in a section, we want to keep the section options for the preview
|
||||||
|
if (isSection) {
|
||||||
|
const containerConfig = findLovelaceContainer(
|
||||||
|
this._params!.lovelaceConfig!,
|
||||||
|
this._params!.path!
|
||||||
|
) as LovelaceSectionConfig;
|
||||||
|
if (!isStrategySection(containerConfig)) {
|
||||||
|
const { cards, title, ...rest } = containerConfig;
|
||||||
|
sectionOptions = rest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sectionConfig = computeSection(
|
||||||
|
this._selectedEntities,
|
||||||
|
sectionOptions
|
||||||
|
);
|
||||||
|
|
||||||
showSuggestCardDialog(this, {
|
showSuggestCardDialog(this, {
|
||||||
lovelaceConfig: this._params!.lovelaceConfig,
|
lovelaceConfig: this._params!.lovelaceConfig,
|
||||||
saveConfig: this._params!.saveConfig,
|
saveConfig: this._params!.saveConfig,
|
||||||
path: this._params!.path as [number],
|
path: this._params!.path as [number],
|
||||||
entities: this._selectedEntities,
|
entities: this._selectedEntities,
|
||||||
cardConfig,
|
cardConfig,
|
||||||
|
sectionConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { mdiClose, mdiHelpCircle } from "@mdi/js";
|
import { mdiClose, mdiHelpCircle } from "@mdi/js";
|
||||||
import deepFreeze from "deep-freeze";
|
import deepFreeze from "deep-freeze";
|
||||||
import {
|
import {
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
|
||||||
LitElement,
|
LitElement,
|
||||||
nothing,
|
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
|
css,
|
||||||
|
html,
|
||||||
|
nothing,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||||
|
@ -16,6 +16,14 @@ import "../../../../components/ha-circular-progress";
|
||||||
import "../../../../components/ha-dialog";
|
import "../../../../components/ha-dialog";
|
||||||
import "../../../../components/ha-dialog-header";
|
import "../../../../components/ha-dialog-header";
|
||||||
import "../../../../components/ha-icon-button";
|
import "../../../../components/ha-icon-button";
|
||||||
|
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||||
|
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||||
|
import { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||||
|
import {
|
||||||
|
getCustomCardEntry,
|
||||||
|
isCustomType,
|
||||||
|
stripCustomPrefix,
|
||||||
|
} from "../../../../data/lovelace_custom_cards";
|
||||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||||
import { haStyleDialog } from "../../../../resources/styles";
|
import { haStyleDialog } from "../../../../resources/styles";
|
||||||
|
@ -24,18 +32,12 @@ import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||||
import { addCard, replaceCard } from "../config-util";
|
import { addCard, replaceCard } from "../config-util";
|
||||||
import { getCardDocumentationURL } from "../get-card-documentation-url";
|
import { getCardDocumentationURL } from "../get-card-documentation-url";
|
||||||
import type { ConfigChangedEvent } from "../hui-element-editor";
|
import type { ConfigChangedEvent } from "../hui-element-editor";
|
||||||
|
import { findLovelaceContainer } from "../lovelace-path";
|
||||||
import type { GUIModeChangedEvent } from "../types";
|
import type { GUIModeChangedEvent } from "../types";
|
||||||
import "./hui-card-element-editor";
|
import "./hui-card-element-editor";
|
||||||
import type { HuiCardElementEditor } from "./hui-card-element-editor";
|
import type { HuiCardElementEditor } from "./hui-card-element-editor";
|
||||||
import "./hui-card-preview";
|
import "./hui-card-preview";
|
||||||
import type { EditCardDialogParams } from "./show-edit-card-dialog";
|
import type { EditCardDialogParams } from "./show-edit-card-dialog";
|
||||||
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
|
||||||
import { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
|
||||||
import {
|
|
||||||
getCustomCardEntry,
|
|
||||||
isCustomType,
|
|
||||||
stripCustomPrefix,
|
|
||||||
} from "../../../../data/lovelace_custom_cards";
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// for fire event
|
// for fire event
|
||||||
|
@ -61,7 +63,9 @@ export class HuiDialogEditCard
|
||||||
|
|
||||||
@state() private _cardConfig?: LovelaceCardConfig;
|
@state() private _cardConfig?: LovelaceCardConfig;
|
||||||
|
|
||||||
@state() private _viewConfig!: LovelaceViewConfig;
|
@state() private _containerConfig!:
|
||||||
|
| LovelaceViewConfig
|
||||||
|
| LovelaceSectionConfig;
|
||||||
|
|
||||||
@state() private _saving = false;
|
@state() private _saving = false;
|
||||||
|
|
||||||
|
@ -84,18 +88,29 @@ export class HuiDialogEditCard
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._GUImode = true;
|
this._GUImode = true;
|
||||||
this._guiModeAvailable = true;
|
this._guiModeAvailable = true;
|
||||||
const [view, card] = params.path;
|
|
||||||
this._viewConfig = params.lovelaceConfig.views[view];
|
const containerConfig = findLovelaceContainer(
|
||||||
this._cardConfig =
|
params.lovelaceConfig,
|
||||||
params.newCardConfig ??
|
params.path
|
||||||
(card !== null ? this._viewConfig.cards![card] : undefined);
|
);
|
||||||
|
|
||||||
|
if ("strategy" in containerConfig) {
|
||||||
|
throw new Error("Can't edit strategy");
|
||||||
|
}
|
||||||
|
|
||||||
|
this._containerConfig = containerConfig;
|
||||||
|
|
||||||
|
if ("cardConfig" in params) {
|
||||||
|
this._cardConfig = params.cardConfig;
|
||||||
|
this._dirty = true;
|
||||||
|
} else {
|
||||||
|
this._cardConfig = this._containerConfig.cards?.[params.cardIndex];
|
||||||
|
}
|
||||||
|
|
||||||
this.large = false;
|
this.large = false;
|
||||||
if (this._cardConfig && !Object.isFrozen(this._cardConfig)) {
|
if (this._cardConfig && !Object.isFrozen(this._cardConfig)) {
|
||||||
this._cardConfig = deepFreeze(this._cardConfig);
|
this._cardConfig = deepFreeze(this._cardConfig);
|
||||||
}
|
}
|
||||||
if (params.newCardConfig) {
|
|
||||||
this._dirty = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeDialog(): boolean {
|
public closeDialog(): boolean {
|
||||||
|
@ -171,10 +186,10 @@ export class HuiDialogEditCard
|
||||||
{ type: cardName }
|
{ type: cardName }
|
||||||
);
|
);
|
||||||
} else if (!this._cardConfig) {
|
} else if (!this._cardConfig) {
|
||||||
heading = this._viewConfig.title
|
heading = this._containerConfig.title
|
||||||
? this.hass!.localize(
|
? this.hass!.localize(
|
||||||
"ui.panel.lovelace.editor.edit_card.pick_card_view_title",
|
"ui.panel.lovelace.editor.edit_card.pick_card_view_title",
|
||||||
{ name: this._viewConfig.title }
|
{ name: this._containerConfig.title }
|
||||||
)
|
)
|
||||||
: this.hass!.localize("ui.panel.lovelace.editor.edit_card.pick_card");
|
: this.hass!.localize("ui.panel.lovelace.editor.edit_card.pick_card");
|
||||||
} else {
|
} else {
|
||||||
|
@ -369,13 +384,13 @@ export class HuiDialogEditCard
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._saving = true;
|
this._saving = true;
|
||||||
const [view, card] = this._params!.path;
|
const path = this._params!.path;
|
||||||
await this._params!.saveConfig(
|
await this._params!.saveConfig(
|
||||||
card === null
|
"cardConfig" in this._params!
|
||||||
? addCard(this._params!.lovelaceConfig, [view], this._cardConfig!)
|
? addCard(this._params!.lovelaceConfig, path, this._cardConfig!)
|
||||||
: replaceCard(
|
: replaceCard(
|
||||||
this._params!.lovelaceConfig,
|
this._params!.lovelaceConfig,
|
||||||
[view, card],
|
[...path, this._params!.cardIndex],
|
||||||
this._cardConfig!
|
this._cardConfig!
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,13 +5,20 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import "../../../../components/ha-yaml-editor";
|
import "../../../../components/ha-yaml-editor";
|
||||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||||
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||||
|
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||||
|
import { isStrategyView } from "../../../../data/lovelace/config/view";
|
||||||
import { haStyleDialog } from "../../../../resources/styles";
|
import { haStyleDialog } from "../../../../resources/styles";
|
||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant } from "../../../../types";
|
||||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
||||||
import { addCards } from "../config-util";
|
import { addCards, addSection } from "../config-util";
|
||||||
|
import {
|
||||||
|
LovelaceContainerPath,
|
||||||
|
parseLovelaceContainerPath,
|
||||||
|
} from "../lovelace-path";
|
||||||
import "./hui-card-preview";
|
import "./hui-card-preview";
|
||||||
import { showCreateCardDialog } from "./show-create-card-dialog";
|
import { showCreateCardDialog } from "./show-create-card-dialog";
|
||||||
import { SuggestCardDialogParams } from "./show-suggest-card-dialog";
|
import { SuggestCardDialogParams } from "./show-suggest-card-dialog";
|
||||||
|
import { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||||
|
|
||||||
@customElement("hui-dialog-suggest-card")
|
@customElement("hui-dialog-suggest-card")
|
||||||
export class HuiDialogSuggestCard extends LitElement {
|
export class HuiDialogSuggestCard extends LitElement {
|
||||||
|
@ -21,6 +28,8 @@ export class HuiDialogSuggestCard extends LitElement {
|
||||||
|
|
||||||
@state() private _cardConfig?: LovelaceCardConfig[];
|
@state() private _cardConfig?: LovelaceCardConfig[];
|
||||||
|
|
||||||
|
@state() private _sectionConfig?: LovelaceSectionConfig;
|
||||||
|
|
||||||
@state() private _saving = false;
|
@state() private _saving = false;
|
||||||
|
|
||||||
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||||
|
@ -28,9 +37,13 @@ export class HuiDialogSuggestCard extends LitElement {
|
||||||
public showDialog(params: SuggestCardDialogParams): void {
|
public showDialog(params: SuggestCardDialogParams): void {
|
||||||
this._params = params;
|
this._params = params;
|
||||||
this._cardConfig = params.cardConfig;
|
this._cardConfig = params.cardConfig;
|
||||||
|
this._sectionConfig = params.sectionConfig;
|
||||||
if (!Object.isFrozen(this._cardConfig)) {
|
if (!Object.isFrozen(this._cardConfig)) {
|
||||||
this._cardConfig = deepFreeze(this._cardConfig);
|
this._cardConfig = deepFreeze(this._cardConfig);
|
||||||
}
|
}
|
||||||
|
if (!Object.isFrozen(this._sectionConfig)) {
|
||||||
|
this._sectionConfig = deepFreeze(this._sectionConfig);
|
||||||
|
}
|
||||||
if (this._yamlEditor) {
|
if (this._yamlEditor) {
|
||||||
this._yamlEditor.setValue(this._cardConfig);
|
this._yamlEditor.setValue(this._cardConfig);
|
||||||
}
|
}
|
||||||
|
@ -42,6 +55,45 @@ export class HuiDialogSuggestCard extends LitElement {
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get _viewSupportsSection(): boolean {
|
||||||
|
if (!this._params?.lovelaceConfig || !this._params?.path) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { viewIndex } = parseLovelaceContainerPath(this._params.path);
|
||||||
|
const viewConfig = this._params!.lovelaceConfig.views[viewIndex];
|
||||||
|
|
||||||
|
return !isStrategyView(viewConfig) && viewConfig.type === "sections";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderPreview() {
|
||||||
|
if (this._sectionConfig && this._viewSupportsSection) {
|
||||||
|
return html`
|
||||||
|
<div class="element-preview">
|
||||||
|
<hui-section
|
||||||
|
.hass=${this.hass}
|
||||||
|
.config=${this._sectionConfig}
|
||||||
|
></hui-section>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
if (this._cardConfig) {
|
||||||
|
return html`
|
||||||
|
<div class="element-preview">
|
||||||
|
${this._cardConfig.map(
|
||||||
|
(cardConfig) => html`
|
||||||
|
<hui-card-preview
|
||||||
|
.hass=${this.hass}
|
||||||
|
.config=${cardConfig}
|
||||||
|
></hui-card-preview>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
if (!this._params) {
|
if (!this._params) {
|
||||||
return nothing;
|
return nothing;
|
||||||
|
@ -56,20 +108,7 @@ export class HuiDialogSuggestCard extends LitElement {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
${this._cardConfig
|
${this._renderPreview()}
|
||||||
? html`
|
|
||||||
<div class="element-preview">
|
|
||||||
${this._cardConfig.map(
|
|
||||||
(cardConfig) => html`
|
|
||||||
<hui-card-preview
|
|
||||||
.hass=${this.hass}
|
|
||||||
.config=${cardConfig}
|
|
||||||
></hui-card-preview>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${this._params.yaml && this._cardConfig
|
${this._params.yaml && this._cardConfig
|
||||||
? html`
|
? html`
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
|
@ -79,7 +118,7 @@ export class HuiDialogSuggestCard extends LitElement {
|
||||||
></ha-yaml-editor>
|
></ha-yaml-editor>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
<mwc-button
|
<mwc-button
|
||||||
slot="secondaryAction"
|
slot="secondaryAction"
|
||||||
|
@ -146,7 +185,8 @@ export class HuiDialogSuggestCard extends LitElement {
|
||||||
.element-preview {
|
.element-preview {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
hui-card-preview {
|
hui-card-preview,
|
||||||
|
hui-section {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
margin: 4px auto;
|
margin: 4px auto;
|
||||||
max-width: 390px;
|
max-width: 390px;
|
||||||
|
@ -178,6 +218,32 @@ export class HuiDialogSuggestCard extends LitElement {
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _computeNewConfig(
|
||||||
|
config: LovelaceConfig,
|
||||||
|
path: LovelaceContainerPath
|
||||||
|
): LovelaceConfig {
|
||||||
|
if (!this._viewSupportsSection) {
|
||||||
|
return addCards(config, path, this._cardConfig!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { viewIndex, sectionIndex } = parseLovelaceContainerPath(path);
|
||||||
|
|
||||||
|
// If container is a view, add a section
|
||||||
|
if (sectionIndex === undefined) {
|
||||||
|
const newSection = this._sectionConfig ?? {
|
||||||
|
type: "grid",
|
||||||
|
cards: this._cardConfig,
|
||||||
|
};
|
||||||
|
return addSection(config, viewIndex, newSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else add cards to section
|
||||||
|
const newCards = this._sectionConfig
|
||||||
|
? this._sectionConfig.cards || []
|
||||||
|
: this._cardConfig!;
|
||||||
|
return addCards(config, [viewIndex, sectionIndex], newCards);
|
||||||
|
}
|
||||||
|
|
||||||
private async _save(): Promise<void> {
|
private async _save(): Promise<void> {
|
||||||
if (
|
if (
|
||||||
!this._params?.lovelaceConfig ||
|
!this._params?.lovelaceConfig ||
|
||||||
|
@ -188,13 +254,12 @@ export class HuiDialogSuggestCard extends LitElement {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._saving = true;
|
this._saving = true;
|
||||||
await this._params!.saveConfig(
|
|
||||||
addCards(
|
const newConfig = this._computeNewConfig(
|
||||||
this._params!.lovelaceConfig,
|
this._params.lovelaceConfig,
|
||||||
this._params!.path as [number],
|
this._params.path
|
||||||
this._cardConfig
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
await this._params!.saveConfig(newConfig);
|
||||||
this._saving = false;
|
this._saving = false;
|
||||||
showSaveSuccessToast(this, this.hass);
|
showSaveSuccessToast(this, this.hass);
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { PropertyValues, ReactiveElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { LovelaceSectionElement } from "../../../../data/lovelace";
|
||||||
|
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||||
|
import { HomeAssistant } from "../../../../types";
|
||||||
|
import { createSectionElement } from "../../create-element/create-section-element";
|
||||||
|
import { createErrorSectionConfig } from "../../sections/hui-error-section";
|
||||||
|
import { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||||
|
|
||||||
|
@customElement("hui-section-preview")
|
||||||
|
export class HuiSectionPreview extends ReactiveElement {
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public lovelace?: LovelaceConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public config?: LovelaceSectionConfig;
|
||||||
|
|
||||||
|
private _element?: LovelaceSectionElement;
|
||||||
|
|
||||||
|
private get _error() {
|
||||||
|
return this._element?.tagName === "HUI-ERROR-SECTION";
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.addEventListener("ll-rebuild", () => {
|
||||||
|
this._cleanup();
|
||||||
|
if (this.config) {
|
||||||
|
this._createSection(this.config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected update(changedProperties: PropertyValues) {
|
||||||
|
super.update(changedProperties);
|
||||||
|
|
||||||
|
if (changedProperties.has("config")) {
|
||||||
|
const oldConfig = changedProperties.get("config") as
|
||||||
|
| undefined
|
||||||
|
| LovelaceSectionConfig;
|
||||||
|
|
||||||
|
if (!this.config) {
|
||||||
|
this._cleanup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.type) {
|
||||||
|
this._createSection(createErrorSectionConfig("No section type found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._element) {
|
||||||
|
this._createSection(this.config);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// in case the element was an error element we always want to recreate it
|
||||||
|
if (!this._error && oldConfig && this.config.type === oldConfig.type) {
|
||||||
|
try {
|
||||||
|
this._element.setConfig(this.config);
|
||||||
|
} catch (err: any) {
|
||||||
|
this._createSection(createErrorSectionConfig(err.message));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._createSection(this.config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedProperties.has("hass")) {
|
||||||
|
if (this._element) {
|
||||||
|
this._element.hass = this.hass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createSection(configValue: LovelaceSectionConfig): void {
|
||||||
|
this._cleanup();
|
||||||
|
this._element = createSectionElement(configValue) as LovelaceSectionElement;
|
||||||
|
|
||||||
|
if (this.hass) {
|
||||||
|
this._element!.hass = this.hass;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.appendChild(this._element!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _cleanup() {
|
||||||
|
if (!this._element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.removeChild(this._element);
|
||||||
|
this._element = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-section-preview": HuiSectionPreview;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||||
|
import { LovelaceContainerPath } from "../lovelace-path";
|
||||||
|
|
||||||
export interface CreateCardDialogParams {
|
export interface CreateCardDialogParams {
|
||||||
lovelaceConfig: LovelaceConfig;
|
lovelaceConfig: LovelaceConfig;
|
||||||
saveConfig: (config: LovelaceConfig) => void;
|
saveConfig: (config: LovelaceConfig) => void;
|
||||||
path: [number];
|
path: LovelaceContainerPath;
|
||||||
|
suggestedCards?: string[];
|
||||||
entities?: string[]; // We can pass entity id's that will be added to the config when a card is picked
|
entities?: string[]; // We can pass entity id's that will be added to the config when a card is picked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||||
|
import { LovelaceContainerPath } from "../lovelace-path";
|
||||||
|
|
||||||
export interface EditCardDialogParams {
|
export type EditCardDialogParams = {
|
||||||
lovelaceConfig: LovelaceConfig;
|
lovelaceConfig: LovelaceConfig;
|
||||||
saveConfig: (config: LovelaceConfig) => void;
|
saveConfig: (config: LovelaceConfig) => void;
|
||||||
path: [number, number | null];
|
path: LovelaceContainerPath;
|
||||||
// If specified, the card will be replaced with the new card.
|
} & (
|
||||||
newCardConfig?: LovelaceCardConfig;
|
| {
|
||||||
}
|
cardIndex: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
cardConfig: LovelaceCardConfig;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const importEditCardDialog = () => import("./hui-dialog-edit-card");
|
export const importEditCardDialog = () => import("./hui-dialog-edit-card");
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||||
|
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||||
import { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
import { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||||
|
import { LovelaceContainerPath } from "../lovelace-path";
|
||||||
|
|
||||||
export interface SuggestCardDialogParams {
|
export interface SuggestCardDialogParams {
|
||||||
lovelaceConfig?: LovelaceConfig;
|
lovelaceConfig?: LovelaceConfig;
|
||||||
yaml?: boolean;
|
yaml?: boolean;
|
||||||
saveConfig?: (config: LovelaceConfig) => void;
|
saveConfig?: (config: LovelaceConfig) => void;
|
||||||
path?: [number];
|
path?: LovelaceContainerPath;
|
||||||
entities?: string[]; // We pass this to create dialog when user chooses "Pick own"
|
entities?: string[]; // We pass this to create dialog when user chooses "Pick own"
|
||||||
cardConfig: LovelaceCardConfig[]; // We can pass a suggested config
|
cardConfig: LovelaceCardConfig[]; // We can pass a suggested config,s
|
||||||
|
sectionConfig?: LovelaceSectionConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const importSuggestCardDialog = () => import("./hui-dialog-suggest-card");
|
const importSuggestCardDialog = () => import("./hui-dialog-suggest-card");
|
||||||
|
|
|
@ -1,296 +1,160 @@
|
||||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||||
|
import { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
|
||||||
import { LovelaceConfig } from "../../../data/lovelace/config/types";
|
import { LovelaceConfig } from "../../../data/lovelace/config/types";
|
||||||
import {
|
import {
|
||||||
LovelaceViewConfig,
|
LovelaceViewConfig,
|
||||||
isStrategyView,
|
isStrategyView,
|
||||||
} from "../../../data/lovelace/config/view";
|
} from "../../../data/lovelace/config/view";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import {
|
||||||
|
LovelaceCardPath,
|
||||||
|
LovelaceContainerPath,
|
||||||
|
findLovelaceCards,
|
||||||
|
findLovelaceContainer,
|
||||||
|
getLovelaceContainerPath,
|
||||||
|
parseLovelaceCardPath,
|
||||||
|
parseLovelaceContainerPath,
|
||||||
|
updateLovelaceCards,
|
||||||
|
updateLovelaceContainer,
|
||||||
|
} from "./lovelace-path";
|
||||||
|
|
||||||
export const addCard = (
|
export const addCard = (
|
||||||
config: LovelaceConfig,
|
config: LovelaceConfig,
|
||||||
path: [number],
|
path: LovelaceContainerPath,
|
||||||
cardConfig: LovelaceCardConfig
|
cardConfig: LovelaceCardConfig
|
||||||
): LovelaceConfig => {
|
): LovelaceConfig => {
|
||||||
const [viewIndex] = path;
|
const cards = findLovelaceCards(config, path);
|
||||||
const views: LovelaceViewConfig[] = [];
|
const newCards = cards ? [...cards, cardConfig] : [cardConfig];
|
||||||
|
const newConfig = updateLovelaceCards(config, path, newCards);
|
||||||
config.views.forEach((viewConf, index) => {
|
return newConfig;
|
||||||
if (index !== viewIndex) {
|
|
||||||
views.push(config.views[index]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStrategyView(viewConf)) {
|
|
||||||
throw new Error("You cannot add a card in a strategy view.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const cards = viewConf.cards
|
|
||||||
? [...viewConf.cards, cardConfig]
|
|
||||||
: [cardConfig];
|
|
||||||
|
|
||||||
views.push({
|
|
||||||
...viewConf,
|
|
||||||
cards,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...config,
|
|
||||||
views,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addCards = (
|
export const addCards = (
|
||||||
config: LovelaceConfig,
|
config: LovelaceConfig,
|
||||||
path: [number],
|
path: LovelaceContainerPath,
|
||||||
cardConfigs: LovelaceCardConfig[]
|
cardConfigs: LovelaceCardConfig[]
|
||||||
): LovelaceConfig => {
|
): LovelaceConfig => {
|
||||||
const [viewIndex] = path;
|
const cards = findLovelaceCards(config, path);
|
||||||
const views: LovelaceViewConfig[] = [];
|
const newCards = cards ? [...cards, ...cardConfigs] : [...cardConfigs];
|
||||||
|
const newConfig = updateLovelaceCards(config, path, newCards);
|
||||||
config.views.forEach((viewConf, index) => {
|
return newConfig;
|
||||||
if (index !== viewIndex) {
|
|
||||||
views.push(config.views[index]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStrategyView(viewConf)) {
|
|
||||||
throw new Error("You cannot add cards in a strategy view.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const cards = viewConf.cards
|
|
||||||
? [...viewConf.cards, ...cardConfigs]
|
|
||||||
: [...cardConfigs];
|
|
||||||
|
|
||||||
views.push({
|
|
||||||
...viewConf,
|
|
||||||
cards,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...config,
|
|
||||||
views,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const replaceCard = (
|
export const replaceCard = (
|
||||||
config: LovelaceConfig,
|
config: LovelaceConfig,
|
||||||
path: [number, number],
|
path: LovelaceCardPath,
|
||||||
cardConfig: LovelaceCardConfig
|
cardConfig: LovelaceCardConfig
|
||||||
): LovelaceConfig => {
|
): LovelaceConfig => {
|
||||||
const [viewIndex, cardIndex] = path;
|
const { cardIndex } = parseLovelaceCardPath(path);
|
||||||
const views: LovelaceViewConfig[] = [];
|
const containerPath = getLovelaceContainerPath(path);
|
||||||
|
|
||||||
config.views.forEach((viewConf, index) => {
|
const cards = findLovelaceCards(config, containerPath);
|
||||||
if (index !== viewIndex) {
|
|
||||||
views.push(config.views[index]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStrategyView(viewConf)) {
|
const newCards = (cards ?? []).map((origConf, ind) =>
|
||||||
throw new Error("You cannot replace a card in a strategy view.");
|
ind === cardIndex ? cardConfig : origConf
|
||||||
}
|
);
|
||||||
|
|
||||||
views.push({
|
const newConfig = updateLovelaceCards(config, containerPath, newCards);
|
||||||
...viewConf,
|
return newConfig;
|
||||||
cards: (viewConf.cards || []).map((origConf, ind) =>
|
|
||||||
ind === cardIndex ? cardConfig : origConf
|
|
||||||
),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...config,
|
|
||||||
views,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteCard = (
|
export const deleteCard = (
|
||||||
config: LovelaceConfig,
|
config: LovelaceConfig,
|
||||||
path: [number, number]
|
path: LovelaceCardPath
|
||||||
): LovelaceConfig => {
|
): LovelaceConfig => {
|
||||||
const [viewIndex, cardIndex] = path;
|
const { cardIndex } = parseLovelaceCardPath(path);
|
||||||
const views: LovelaceViewConfig[] = [];
|
const containerPath = getLovelaceContainerPath(path);
|
||||||
|
|
||||||
config.views.forEach((viewConf, index) => {
|
const cards = findLovelaceCards(config, containerPath);
|
||||||
if (index !== viewIndex) {
|
|
||||||
views.push(config.views[index]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStrategyView(viewConf)) {
|
const newCards = (cards ?? []).filter((_origConf, ind) => ind !== cardIndex);
|
||||||
throw new Error("You cannot delete a card in a strategy view.");
|
|
||||||
}
|
|
||||||
|
|
||||||
views.push({
|
const newConfig = updateLovelaceCards(config, containerPath, newCards);
|
||||||
...viewConf,
|
return newConfig;
|
||||||
cards: (viewConf.cards || []).filter(
|
|
||||||
(_origConf, ind) => ind !== cardIndex
|
|
||||||
),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...config,
|
|
||||||
views,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const insertCard = (
|
export const insertCard = (
|
||||||
config: LovelaceConfig,
|
config: LovelaceConfig,
|
||||||
path: [number, number],
|
path: LovelaceCardPath,
|
||||||
cardConfig: LovelaceCardConfig
|
cardConfig: LovelaceCardConfig
|
||||||
) => {
|
) => {
|
||||||
const [viewIndex, cardIndex] = path;
|
const { cardIndex } = parseLovelaceCardPath(path);
|
||||||
const views: LovelaceViewConfig[] = [];
|
const containerPath = getLovelaceContainerPath(path);
|
||||||
|
|
||||||
config.views.forEach((viewConf, index) => {
|
const cards = findLovelaceCards(config, containerPath);
|
||||||
if (index !== viewIndex) {
|
|
||||||
views.push(config.views[index]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isStrategyView(viewConf)) {
|
const newCards = cards
|
||||||
throw new Error("You cannot insert a card in a strategy view.");
|
? [...cards.slice(0, cardIndex), cardConfig, ...cards.slice(cardIndex)]
|
||||||
}
|
: [cardConfig];
|
||||||
|
|
||||||
const cards = viewConf.cards
|
const newConfig = updateLovelaceCards(config, containerPath, newCards);
|
||||||
? [
|
return newConfig;
|
||||||
...viewConf.cards.slice(0, cardIndex),
|
|
||||||
cardConfig,
|
|
||||||
...viewConf.cards.slice(cardIndex),
|
|
||||||
]
|
|
||||||
: [cardConfig];
|
|
||||||
|
|
||||||
views.push({
|
|
||||||
...viewConf,
|
|
||||||
cards,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...config,
|
|
||||||
views,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const swapCard = (
|
export const moveCardToIndex = (
|
||||||
config: LovelaceConfig,
|
config: LovelaceConfig,
|
||||||
path1: [number, number],
|
path: LovelaceCardPath,
|
||||||
path2: [number, number]
|
index: number
|
||||||
): LovelaceConfig => {
|
): LovelaceConfig => {
|
||||||
const origView1 = config.views[path1[0]];
|
const { cardIndex } = parseLovelaceCardPath(path);
|
||||||
const origView2 = config.views[path2[0]];
|
const containerPath = getLovelaceContainerPath(path);
|
||||||
|
|
||||||
if (isStrategyView(origView1) || isStrategyView(origView2)) {
|
const cards = findLovelaceCards(config, containerPath);
|
||||||
throw new Error("You cannot move swap cards in a strategy view.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const card1 = origView1.cards![path1[1]];
|
const newCards = cards ? [...cards] : [];
|
||||||
const card2 = origView2.cards![path2[1]];
|
|
||||||
|
|
||||||
const newView1 = {
|
const oldIndex = cardIndex;
|
||||||
...origView1,
|
const newIndex = Math.max(Math.min(index, newCards.length - 1), 0);
|
||||||
cards: origView1.cards!.map((origCard, index) =>
|
|
||||||
index === path1[1] ? card2 : origCard
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedOrigView2 = path1[0] === path2[0] ? newView1 : origView2;
|
|
||||||
const newView2 = {
|
|
||||||
...updatedOrigView2,
|
|
||||||
cards: updatedOrigView2.cards!.map((origCard, index) =>
|
|
||||||
index === path2[1] ? card1 : origCard
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...config,
|
|
||||||
views: config.views.map((origView, index) =>
|
|
||||||
index === path2[0] ? newView2 : index === path1[0] ? newView1 : origView
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const moveCardToPosition = (
|
|
||||||
config: LovelaceConfig,
|
|
||||||
path: [number, number],
|
|
||||||
position: number
|
|
||||||
): LovelaceConfig => {
|
|
||||||
const view = config.views[path[0]];
|
|
||||||
|
|
||||||
if (isStrategyView(view)) {
|
|
||||||
throw new Error("You cannot move a card in a strategy view.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldIndex = path[1];
|
|
||||||
const newIndex = Math.max(Math.min(position - 1, view.cards!.length - 1), 0);
|
|
||||||
|
|
||||||
const newCards = [...view.cards!];
|
|
||||||
|
|
||||||
const card = newCards[oldIndex];
|
const card = newCards[oldIndex];
|
||||||
newCards.splice(oldIndex, 1);
|
newCards.splice(oldIndex, 1);
|
||||||
newCards.splice(newIndex, 0, card);
|
newCards.splice(newIndex, 0, card);
|
||||||
|
|
||||||
const newView = {
|
const newConfig = updateLovelaceCards(config, containerPath, newCards);
|
||||||
...view,
|
return newConfig;
|
||||||
cards: newCards,
|
};
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
export const moveCardToContainer = (
|
||||||
...config,
|
config: LovelaceConfig,
|
||||||
views: config.views.map((origView, index) =>
|
fromPath: LovelaceCardPath,
|
||||||
index === path[0] ? newView : origView
|
toPath: LovelaceContainerPath
|
||||||
),
|
): LovelaceConfig => {
|
||||||
};
|
const {
|
||||||
|
cardIndex: fromCardIndex,
|
||||||
|
viewIndex: fromViewIndex,
|
||||||
|
sectionIndex: fromSectionIndex,
|
||||||
|
} = parseLovelaceCardPath(fromPath);
|
||||||
|
const { viewIndex: toViewIndex, sectionIndex: toSectionIndex } =
|
||||||
|
parseLovelaceContainerPath(toPath);
|
||||||
|
|
||||||
|
if (fromViewIndex === toViewIndex && fromSectionIndex === toSectionIndex) {
|
||||||
|
throw new Error("You cannot move a card to the view or section it is in.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromContainerPath = getLovelaceContainerPath(fromPath);
|
||||||
|
const cards = findLovelaceCards(config, fromContainerPath);
|
||||||
|
const card = cards![fromCardIndex];
|
||||||
|
|
||||||
|
let newConfig = addCard(config, toPath, card);
|
||||||
|
newConfig = deleteCard(newConfig, fromPath);
|
||||||
|
|
||||||
|
return newConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const moveCard = (
|
export const moveCard = (
|
||||||
config: LovelaceConfig,
|
config: LovelaceConfig,
|
||||||
fromPath: [number, number],
|
fromPath: LovelaceCardPath,
|
||||||
toPath: [number]
|
toPath: LovelaceCardPath
|
||||||
): LovelaceConfig => {
|
): LovelaceConfig => {
|
||||||
if (fromPath[0] === toPath[0]) {
|
const { cardIndex: fromCardIndex } = parseLovelaceCardPath(fromPath);
|
||||||
throw new Error("You cannot move a card to the view it is in.");
|
const fromContainerPath = getLovelaceContainerPath(fromPath);
|
||||||
}
|
const cards = findLovelaceCards(config, fromContainerPath);
|
||||||
const fromView = config.views[fromPath[0]];
|
const card = cards![fromCardIndex];
|
||||||
const toView = config.views[toPath[0]];
|
|
||||||
|
|
||||||
if (isStrategyView(fromView)) {
|
let newConfig = deleteCard(config, fromPath);
|
||||||
throw new Error("You cannot move a card from a strategy view.");
|
newConfig = insertCard(newConfig, toPath, card);
|
||||||
}
|
|
||||||
|
|
||||||
if (isStrategyView(toView)) {
|
return newConfig;
|
||||||
throw new Error("You cannot move a card to a strategy view.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const card = fromView.cards![fromPath[1]];
|
|
||||||
|
|
||||||
const newView1 = {
|
|
||||||
...fromView,
|
|
||||||
cards: (fromView.cards || []).filter(
|
|
||||||
(_origConf, ind) => ind !== fromPath[1]
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const cards = toView.cards ? [...toView.cards, card] : [card];
|
|
||||||
|
|
||||||
const newView2 = {
|
|
||||||
...toView,
|
|
||||||
cards,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...config,
|
|
||||||
views: config.views.map((origView, index) =>
|
|
||||||
index === toPath[0]
|
|
||||||
? newView2
|
|
||||||
: index === fromPath[0]
|
|
||||||
? newView1
|
|
||||||
: origView
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addView = (
|
export const addView = (
|
||||||
|
@ -356,3 +220,84 @@ export const deleteView = (
|
||||||
...config,
|
...config,
|
||||||
views: config.views.filter((_origView, index) => index !== viewIndex),
|
views: config.views.filter((_origView, index) => index !== viewIndex),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const addSection = (
|
||||||
|
config: LovelaceConfig,
|
||||||
|
viewIndex: number,
|
||||||
|
sectionConfig: LovelaceSectionRawConfig
|
||||||
|
): LovelaceConfig => {
|
||||||
|
const view = findLovelaceContainer(config, [viewIndex]) as LovelaceViewConfig;
|
||||||
|
if (isStrategyView(view)) {
|
||||||
|
throw new Error("Deleting sections in a strategy is not supported.");
|
||||||
|
}
|
||||||
|
const sections = view.sections
|
||||||
|
? [...view.sections, sectionConfig]
|
||||||
|
: [sectionConfig];
|
||||||
|
|
||||||
|
const newConfig = updateLovelaceContainer(config, [viewIndex], {
|
||||||
|
...view,
|
||||||
|
sections,
|
||||||
|
});
|
||||||
|
return newConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteSection = (
|
||||||
|
config: LovelaceConfig,
|
||||||
|
viewIndex: number,
|
||||||
|
sectionIndex: number
|
||||||
|
): LovelaceConfig => {
|
||||||
|
const view = findLovelaceContainer(config, [viewIndex]) as LovelaceViewConfig;
|
||||||
|
if (isStrategyView(view)) {
|
||||||
|
throw new Error("Deleting sections in a strategy is not supported.");
|
||||||
|
}
|
||||||
|
const sections = view.sections?.filter(
|
||||||
|
(_origSection, index) => index !== sectionIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
const newConfig = updateLovelaceContainer(config, [viewIndex], {
|
||||||
|
...view,
|
||||||
|
sections,
|
||||||
|
});
|
||||||
|
return newConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const insertSection = (
|
||||||
|
config: LovelaceConfig,
|
||||||
|
viewIndex: number,
|
||||||
|
sectionIndex: number,
|
||||||
|
sectionConfig: LovelaceSectionRawConfig
|
||||||
|
): LovelaceConfig => {
|
||||||
|
const view = findLovelaceContainer(config, [viewIndex]) as LovelaceViewConfig;
|
||||||
|
if (isStrategyView(view)) {
|
||||||
|
throw new Error("Inserting sections in a strategy is not supported.");
|
||||||
|
}
|
||||||
|
const sections = view.sections
|
||||||
|
? [
|
||||||
|
...view.sections.slice(0, sectionIndex),
|
||||||
|
sectionConfig,
|
||||||
|
...view.sections.slice(sectionIndex),
|
||||||
|
]
|
||||||
|
: [sectionConfig];
|
||||||
|
|
||||||
|
const newConfig = updateLovelaceContainer(config, [viewIndex], {
|
||||||
|
...view,
|
||||||
|
sections,
|
||||||
|
});
|
||||||
|
return newConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const moveSection = (
|
||||||
|
config: LovelaceConfig,
|
||||||
|
fromPath: [number, number],
|
||||||
|
toPath: [number, number]
|
||||||
|
): LovelaceConfig => {
|
||||||
|
const section = findLovelaceContainer(
|
||||||
|
config,
|
||||||
|
fromPath
|
||||||
|
) as LovelaceSectionRawConfig;
|
||||||
|
|
||||||
|
let newConfig = deleteSection(config, fromPath[0], fromPath[1]);
|
||||||
|
newConfig = insertSection(newConfig, toPath[0], toPath[1], section);
|
||||||
|
|
||||||
|
return newConfig;
|
||||||
|
};
|
||||||
|
|
|
@ -1,22 +1,29 @@
|
||||||
import { isStrategyView } from "../../../data/lovelace/config/view";
|
|
||||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { showDeleteSuccessToast } from "../../../util/toast-deleted-success";
|
import { showDeleteSuccessToast } from "../../../util/toast-deleted-success";
|
||||||
import { Lovelace } from "../types";
|
import { Lovelace } from "../types";
|
||||||
import { showDeleteCardDialog } from "./card-editor/show-delete-card-dialog";
|
import { showDeleteCardDialog } from "./card-editor/show-delete-card-dialog";
|
||||||
import { deleteCard, insertCard } from "./config-util";
|
import { deleteCard, insertCard } from "./config-util";
|
||||||
|
import {
|
||||||
|
LovelaceCardPath,
|
||||||
|
findLovelaceContainer,
|
||||||
|
getLovelaceContainerPath,
|
||||||
|
parseLovelaceCardPath,
|
||||||
|
} from "./lovelace-path";
|
||||||
|
|
||||||
export async function confDeleteCard(
|
export async function confDeleteCard(
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
lovelace: Lovelace,
|
lovelace: Lovelace,
|
||||||
path: [number, number]
|
path: LovelaceCardPath
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const view = lovelace.config.views[path[0]];
|
const containerPath = getLovelaceContainerPath(path);
|
||||||
if (isStrategyView(view)) {
|
const { cardIndex } = parseLovelaceCardPath(path);
|
||||||
throw new Error("Deleting cards in a strategy view is not supported.");
|
const containerConfig = findLovelaceContainer(lovelace.config, containerPath);
|
||||||
|
if ("strategy" in containerConfig) {
|
||||||
|
throw new Error("Deleting cards in a strategy is not supported.");
|
||||||
}
|
}
|
||||||
const cardConfig = view.cards![path[1]];
|
const cardConfig = containerConfig.cards![cardIndex];
|
||||||
showDeleteCardDialog(element, {
|
showDeleteCardDialog(element, {
|
||||||
cardConfig,
|
cardConfig,
|
||||||
deleteCard: async () => {
|
deleteCard: async () => {
|
||||||
|
|
|
@ -0,0 +1,197 @@
|
||||||
|
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||||
|
import {
|
||||||
|
LovelaceSectionRawConfig,
|
||||||
|
isStrategySection,
|
||||||
|
} from "../../../data/lovelace/config/section";
|
||||||
|
import { LovelaceConfig } from "../../../data/lovelace/config/types";
|
||||||
|
import {
|
||||||
|
LovelaceViewRawConfig,
|
||||||
|
isStrategyView,
|
||||||
|
} from "../../../data/lovelace/config/view";
|
||||||
|
|
||||||
|
export type LovelaceCardPath = [number, number] | [number, number, number];
|
||||||
|
export type LovelaceContainerPath = [number] | [number, number];
|
||||||
|
|
||||||
|
export const parseLovelaceCardPath = (
|
||||||
|
path: LovelaceCardPath
|
||||||
|
): { viewIndex: number; sectionIndex?: number; cardIndex: number } => {
|
||||||
|
if (path.length === 2) {
|
||||||
|
return {
|
||||||
|
viewIndex: path[0],
|
||||||
|
cardIndex: path[1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
viewIndex: path[0],
|
||||||
|
sectionIndex: path[1],
|
||||||
|
cardIndex: path[2],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseLovelaceContainerPath = (
|
||||||
|
path: LovelaceContainerPath
|
||||||
|
): { viewIndex: number; sectionIndex?: number } => {
|
||||||
|
if (path.length === 1) {
|
||||||
|
return {
|
||||||
|
viewIndex: path[0],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
viewIndex: path[0],
|
||||||
|
sectionIndex: path[1],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLovelaceContainerPath = (
|
||||||
|
path: LovelaceCardPath
|
||||||
|
): LovelaceContainerPath => path.slice(0, -1) as LovelaceContainerPath;
|
||||||
|
|
||||||
|
export const findLovelaceContainer = (
|
||||||
|
config: LovelaceConfig,
|
||||||
|
path: LovelaceContainerPath
|
||||||
|
): LovelaceViewRawConfig | LovelaceSectionRawConfig => {
|
||||||
|
const { viewIndex, sectionIndex } = parseLovelaceContainerPath(path);
|
||||||
|
|
||||||
|
const view = config.views[viewIndex];
|
||||||
|
|
||||||
|
if (!view) {
|
||||||
|
throw new Error("View does not exist");
|
||||||
|
}
|
||||||
|
if (sectionIndex === undefined) {
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
if (isStrategyView(view)) {
|
||||||
|
throw new Error("Can not find section in a strategy view");
|
||||||
|
}
|
||||||
|
|
||||||
|
const section = view.sections?.[sectionIndex];
|
||||||
|
|
||||||
|
if (!section) {
|
||||||
|
throw new Error("Section does not exist");
|
||||||
|
}
|
||||||
|
return section;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findLovelaceCards = (
|
||||||
|
config: LovelaceConfig,
|
||||||
|
path: LovelaceContainerPath
|
||||||
|
): LovelaceCardConfig[] | undefined => {
|
||||||
|
const { viewIndex, sectionIndex } = parseLovelaceContainerPath(path);
|
||||||
|
|
||||||
|
const view = config.views[viewIndex];
|
||||||
|
|
||||||
|
if (!view) {
|
||||||
|
throw new Error("View does not exist");
|
||||||
|
}
|
||||||
|
if (isStrategyView(view)) {
|
||||||
|
throw new Error("Can not find cards in a strategy view");
|
||||||
|
}
|
||||||
|
if (sectionIndex === undefined) {
|
||||||
|
return view.cards;
|
||||||
|
}
|
||||||
|
|
||||||
|
const section = view.sections?.[sectionIndex];
|
||||||
|
|
||||||
|
if (!section) {
|
||||||
|
throw new Error("Section does not exist");
|
||||||
|
}
|
||||||
|
if (isStrategySection(section)) {
|
||||||
|
throw new Error("Can not find cards in a strategy section");
|
||||||
|
}
|
||||||
|
return section.cards;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateLovelaceContainer = (
|
||||||
|
config: LovelaceConfig,
|
||||||
|
path: LovelaceContainerPath,
|
||||||
|
containerConfig: LovelaceViewRawConfig | LovelaceSectionRawConfig
|
||||||
|
): LovelaceConfig => {
|
||||||
|
const { viewIndex, sectionIndex } = parseLovelaceContainerPath(path);
|
||||||
|
|
||||||
|
let updated = false;
|
||||||
|
const newViews = config.views.map((view, vIndex) => {
|
||||||
|
if (vIndex !== viewIndex) return view;
|
||||||
|
|
||||||
|
if (sectionIndex === undefined) {
|
||||||
|
updated = true;
|
||||||
|
return containerConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStrategyView(view)) {
|
||||||
|
throw new Error("Can not update section in a strategy view");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view.sections === undefined) {
|
||||||
|
throw new Error("Section does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSections = view.sections.map((section, sIndex) => {
|
||||||
|
if (sIndex !== sectionIndex) return section;
|
||||||
|
updated = true;
|
||||||
|
return containerConfig;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...view,
|
||||||
|
sections: newSections,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
throw new Error("Can not update cards in a non-existing view/section");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
views: newViews,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateLovelaceCards = (
|
||||||
|
config: LovelaceConfig,
|
||||||
|
path: LovelaceContainerPath,
|
||||||
|
cards: LovelaceCardConfig[]
|
||||||
|
): LovelaceConfig => {
|
||||||
|
const { viewIndex, sectionIndex } = parseLovelaceContainerPath(path);
|
||||||
|
|
||||||
|
let updated = false;
|
||||||
|
const newViews = config.views.map((view, vIndex) => {
|
||||||
|
if (vIndex !== viewIndex) return view;
|
||||||
|
if (isStrategyView(view)) {
|
||||||
|
throw new Error("Can not update cards in a strategy view");
|
||||||
|
}
|
||||||
|
if (sectionIndex === undefined) {
|
||||||
|
updated = true;
|
||||||
|
return {
|
||||||
|
...view,
|
||||||
|
cards,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view.sections === undefined) {
|
||||||
|
throw new Error("Section does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSections = view.sections.map((section, sIndex) => {
|
||||||
|
if (sIndex !== sectionIndex) return section;
|
||||||
|
if (isStrategySection(section)) {
|
||||||
|
throw new Error("Can not update cards in a strategy section");
|
||||||
|
}
|
||||||
|
updated = true;
|
||||||
|
return {
|
||||||
|
...section,
|
||||||
|
cards,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
...view,
|
||||||
|
sections: newSections,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
throw new Error("Can not update cards in a non-existing view/section");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
views: newViews,
|
||||||
|
};
|
||||||
|
};
|
|
@ -62,6 +62,7 @@ export interface Card {
|
||||||
description?: string;
|
description?: string;
|
||||||
showElement?: boolean;
|
showElement?: boolean;
|
||||||
isCustom?: boolean;
|
isCustom?: boolean;
|
||||||
|
isSuggested?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HeaderFooter {
|
export interface HeaderFooter {
|
||||||
|
|
|
@ -21,7 +21,10 @@ import "../card-editor/hui-entity-picker-table";
|
||||||
import { showSuggestCardDialog } from "../card-editor/show-suggest-card-dialog";
|
import { showSuggestCardDialog } from "../card-editor/show-suggest-card-dialog";
|
||||||
import { showSelectViewDialog } from "../select-view/show-select-view-dialog";
|
import { showSelectViewDialog } from "../select-view/show-select-view-dialog";
|
||||||
import { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
import { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||||
import { computeCards } from "../../common/generate-lovelace-config";
|
import {
|
||||||
|
computeCards,
|
||||||
|
computeSection,
|
||||||
|
} from "../../common/generate-lovelace-config";
|
||||||
|
|
||||||
@customElement("hui-unused-entities")
|
@customElement("hui-unused-entities")
|
||||||
export class HuiUnusedEntities extends LitElement {
|
export class HuiUnusedEntities extends LitElement {
|
||||||
|
@ -132,6 +135,8 @@ export class HuiUnusedEntities extends LitElement {
|
||||||
this._selectedEntities,
|
this._selectedEntities,
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
const sectionConfig = computeSection(this._selectedEntities, {});
|
||||||
|
|
||||||
if (this.lovelace.config.views.length === 1) {
|
if (this.lovelace.config.views.length === 1) {
|
||||||
showSuggestCardDialog(this, {
|
showSuggestCardDialog(this, {
|
||||||
lovelaceConfig: this.lovelace.config!,
|
lovelaceConfig: this.lovelace.config!,
|
||||||
|
@ -139,6 +144,7 @@ export class HuiUnusedEntities extends LitElement {
|
||||||
path: [0],
|
path: [0],
|
||||||
entities: this._selectedEntities,
|
entities: this._selectedEntities,
|
||||||
cardConfig,
|
cardConfig,
|
||||||
|
sectionConfig,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -152,6 +158,7 @@ export class HuiUnusedEntities extends LitElement {
|
||||||
path: [viewIndex],
|
path: [viewIndex],
|
||||||
entities: this._selectedEntities,
|
entities: this._selectedEntities,
|
||||||
cardConfig,
|
cardConfig,
|
||||||
|
sectionConfig,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,13 +6,14 @@ import { slugify } from "../../../../common/string/slugify";
|
||||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||||
import "../../../../components/ha-form/ha-form";
|
import "../../../../components/ha-form/ha-form";
|
||||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||||
|
import { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import {
|
import {
|
||||||
DEFAULT_VIEW_LAYOUT,
|
DEFAULT_VIEW_LAYOUT,
|
||||||
|
SECTION_VIEW_LAYOUT,
|
||||||
PANEL_VIEW_LAYOUT,
|
PANEL_VIEW_LAYOUT,
|
||||||
SIDEBAR_VIEW_LAYOUT,
|
SIDEBAR_VIEW_LAYOUT,
|
||||||
} from "../../views/const";
|
} from "../../views/const";
|
||||||
import { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
|
@ -53,6 +54,7 @@ export class HuiViewEditor extends LitElement {
|
||||||
DEFAULT_VIEW_LAYOUT,
|
DEFAULT_VIEW_LAYOUT,
|
||||||
SIDEBAR_VIEW_LAYOUT,
|
SIDEBAR_VIEW_LAYOUT,
|
||||||
PANEL_VIEW_LAYOUT,
|
PANEL_VIEW_LAYOUT,
|
||||||
|
SECTION_VIEW_LAYOUT,
|
||||||
] as const
|
] as const
|
||||||
).map((type) => ({
|
).map((type) => ({
|
||||||
value: type,
|
value: type,
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const GRID_SECTION_LAYOUT = "grid";
|
||||||
|
export const DEFAULT_SECTION_LAYOUT = GRID_SECTION_LAYOUT;
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import "../../../components/ha-label-badge";
|
||||||
|
import "../../../components/ha-svg-icon";
|
||||||
|
import { LovelaceSectionElement } from "../../../data/lovelace";
|
||||||
|
import { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
|
||||||
|
export interface ErrorSectionConfig extends LovelaceSectionConfig {
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createErrorSectionElement = (config: ErrorSectionConfig) => {
|
||||||
|
const el = document.createElement(
|
||||||
|
"hui-error-section"
|
||||||
|
) as LovelaceSectionElement;
|
||||||
|
el.setConfig(config);
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createErrorSectionConfig = (
|
||||||
|
error: string
|
||||||
|
): ErrorSectionConfig => ({
|
||||||
|
type: "error",
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
@customElement("hui-error-section")
|
||||||
|
export class HuiErrorSection
|
||||||
|
extends LitElement
|
||||||
|
implements LovelaceSectionElement
|
||||||
|
{
|
||||||
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public isStrategy = false;
|
||||||
|
|
||||||
|
@state() private _config?: ErrorSectionConfig;
|
||||||
|
|
||||||
|
public setConfig(config: ErrorSectionConfig): void {
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._config) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Todo improve
|
||||||
|
return html`
|
||||||
|
<h1>Error</h1>
|
||||||
|
<p>${this._config.error}</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-error-section": HuiErrorSection;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,246 @@
|
||||||
|
import { mdiPlus } from "@mdi/js";
|
||||||
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
|
import { property, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import type { HaSortableOptions } from "../../../components/ha-sortable";
|
||||||
|
import { LovelaceSectionElement } from "../../../data/lovelace";
|
||||||
|
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||||
|
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||||
|
import { haStyle } from "../../../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import { HuiErrorCard } from "../cards/hui-error-card";
|
||||||
|
import "../components/hui-card-edit-mode";
|
||||||
|
import { moveCard } from "../editor/config-util";
|
||||||
|
import type { Lovelace, LovelaceCard } from "../types";
|
||||||
|
|
||||||
|
const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
|
||||||
|
delay: 200,
|
||||||
|
delayOnTouchOnly: true,
|
||||||
|
direction: "vertical",
|
||||||
|
invertedSwapThreshold: 0.7,
|
||||||
|
} as HaSortableOptions;
|
||||||
|
|
||||||
|
export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||||
|
|
||||||
|
@property({ type: Number }) public index?: number;
|
||||||
|
|
||||||
|
@property({ type: Number }) public viewIndex?: number;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public isStrategy = false;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public cards: Array<
|
||||||
|
LovelaceCard | HuiErrorCard
|
||||||
|
> = [];
|
||||||
|
|
||||||
|
@state() _config?: LovelaceSectionConfig;
|
||||||
|
|
||||||
|
@state() _dragging = false;
|
||||||
|
|
||||||
|
public setConfig(config: LovelaceSectionConfig): void {
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _cardConfigKeys = new WeakMap<LovelaceCardConfig, string>();
|
||||||
|
|
||||||
|
private _getKey(cardConfig: LovelaceCardConfig) {
|
||||||
|
if (!this._cardConfigKeys.has(cardConfig)) {
|
||||||
|
this._cardConfigKeys.set(cardConfig, Math.random().toString());
|
||||||
|
}
|
||||||
|
return this._cardConfigKeys.get(cardConfig)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.cards || !this._config) return nothing;
|
||||||
|
|
||||||
|
const cardsConfig = this._config?.cards ?? [];
|
||||||
|
|
||||||
|
const editMode = Boolean(this.lovelace?.editMode && !this.isStrategy);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${this._config.title || this.lovelace?.editMode
|
||||||
|
? html`
|
||||||
|
<h2
|
||||||
|
class="title ${classMap({
|
||||||
|
placeholder: !this._config.title,
|
||||||
|
})}"
|
||||||
|
>
|
||||||
|
${this._config.title ||
|
||||||
|
this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.section.unnamed_section"
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<ha-sortable
|
||||||
|
.disabled=${!editMode}
|
||||||
|
@item-moved=${this._cardMoved}
|
||||||
|
@drag-start=${this._dragStart}
|
||||||
|
@drag-end=${this._dragEnd}
|
||||||
|
group="card"
|
||||||
|
draggable-selector=".card"
|
||||||
|
.path=${[this.viewIndex, this.index]}
|
||||||
|
.rollback=${false}
|
||||||
|
.options=${CARD_SORTABLE_OPTIONS}
|
||||||
|
invert-swap
|
||||||
|
>
|
||||||
|
<div class="container ${classMap({ "edit-mode": editMode })}">
|
||||||
|
${repeat(
|
||||||
|
cardsConfig,
|
||||||
|
(cardConfig) => this._getKey(cardConfig),
|
||||||
|
(_cardConfig, idx) => {
|
||||||
|
const card = this.cards![idx];
|
||||||
|
(card as any).editMode = editMode;
|
||||||
|
const size = card && (card as any).getGridSize?.();
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="card"
|
||||||
|
style=${styleMap({
|
||||||
|
"--column-size": size?.[0],
|
||||||
|
"--row-size": size?.[1],
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${editMode
|
||||||
|
? html`
|
||||||
|
<hui-card-edit-mode
|
||||||
|
.hass=${this.hass}
|
||||||
|
.lovelace=${this.lovelace}
|
||||||
|
.path=${[this.viewIndex, this.index, idx]}
|
||||||
|
.hiddenOverlay=${this._dragging}
|
||||||
|
>
|
||||||
|
${card}
|
||||||
|
</hui-card-edit-mode>
|
||||||
|
`
|
||||||
|
: card}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
${editMode
|
||||||
|
? html`
|
||||||
|
<button
|
||||||
|
class="add"
|
||||||
|
@click=${this._addCard}
|
||||||
|
aria-label=${this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.section.add_card"
|
||||||
|
)}
|
||||||
|
.title=${this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.section.add_card"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
</ha-sortable>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _cardMoved(ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const { oldIndex, newIndex, oldPath, newPath } = ev.detail;
|
||||||
|
const newConfig = moveCard(
|
||||||
|
this.lovelace!.config,
|
||||||
|
[...oldPath, oldIndex] as [number, number, number],
|
||||||
|
[...newPath, newIndex] as [number, number, number]
|
||||||
|
);
|
||||||
|
this.lovelace!.saveConfig(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dragStart() {
|
||||||
|
this._dragging = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dragEnd() {
|
||||||
|
this._dragging = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addCard() {
|
||||||
|
fireEvent(this, "ll-create-card", { suggested: ["tile"] });
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
--column-count: 4;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--column-count), minmax(0, 1fr));
|
||||||
|
grid-auto-rows: minmax(66px, auto);
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.edit-mode {
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: var(--ha-card-border-radius, 12px);
|
||||||
|
border: 2px dashed var(--divider-color);
|
||||||
|
min-height: 66px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0px;
|
||||||
|
letter-spacing: 0.1px;
|
||||||
|
line-height: 32px;
|
||||||
|
min-height: 32px;
|
||||||
|
display: block;
|
||||||
|
padding: 24px 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title.placeholder {
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: var(--ha-card-border-radius, 12px);
|
||||||
|
position: relative;
|
||||||
|
grid-row: span var(--row-size, 1);
|
||||||
|
grid-column: span var(--column-size, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add {
|
||||||
|
outline: none;
|
||||||
|
grid-row: span var(--row-size, 1);
|
||||||
|
grid-column: span var(--column-size, 2);
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--ha-card-border-radius, 12px);
|
||||||
|
border: 2px dashed var(--primary-color);
|
||||||
|
height: 66px;
|
||||||
|
order: 1;
|
||||||
|
}
|
||||||
|
.add:focus {
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
.sortable-ghost {
|
||||||
|
border-radius: var(--ha-card-border-radius, 12px);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-grid-section": GridSection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("hui-grid-section", GridSection);
|
|
@ -0,0 +1,247 @@
|
||||||
|
import { PropertyValues, ReactiveElement } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import "../../../components/ha-svg-icon";
|
||||||
|
import type { LovelaceSectionElement } from "../../../data/lovelace";
|
||||||
|
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||||
|
import {
|
||||||
|
LovelaceSectionConfig,
|
||||||
|
LovelaceSectionRawConfig,
|
||||||
|
isStrategySection,
|
||||||
|
} from "../../../data/lovelace/config/section";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import type { HuiErrorCard } from "../cards/hui-error-card";
|
||||||
|
import { createCardElement } from "../create-element/create-card-element";
|
||||||
|
import {
|
||||||
|
createErrorCardConfig,
|
||||||
|
createErrorCardElement,
|
||||||
|
} from "../create-element/create-element-base";
|
||||||
|
import { createSectionElement } from "../create-element/create-section-element";
|
||||||
|
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||||
|
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||||
|
import { deleteCard } from "../editor/config-util";
|
||||||
|
import { confDeleteCard } from "../editor/delete-card";
|
||||||
|
import { parseLovelaceCardPath } from "../editor/lovelace-path";
|
||||||
|
import { generateLovelaceSectionStrategy } from "../strategies/get-strategy";
|
||||||
|
import type { Lovelace, LovelaceCard } from "../types";
|
||||||
|
import { DEFAULT_SECTION_LAYOUT } from "./const";
|
||||||
|
|
||||||
|
@customElement("hui-section")
|
||||||
|
export class HuiSection extends ReactiveElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public lovelace!: Lovelace;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public config!: LovelaceSectionRawConfig;
|
||||||
|
|
||||||
|
@property({ type: Number }) public index!: number;
|
||||||
|
|
||||||
|
@property({ type: Number }) public viewIndex!: number;
|
||||||
|
|
||||||
|
@state() private _cards: Array<LovelaceCard | HuiErrorCard> = [];
|
||||||
|
|
||||||
|
private _layoutElementType?: string;
|
||||||
|
|
||||||
|
private _layoutElement?: LovelaceSectionElement;
|
||||||
|
|
||||||
|
// Public to make demo happy
|
||||||
|
public createCardElement(cardConfig: LovelaceCardConfig) {
|
||||||
|
const element = createCardElement(cardConfig) as LovelaceCard;
|
||||||
|
try {
|
||||||
|
element.hass = this.hass;
|
||||||
|
} catch (e: any) {
|
||||||
|
return createErrorCardElement(
|
||||||
|
createErrorCardConfig(e.message, cardConfig)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
element.addEventListener(
|
||||||
|
"ll-rebuild",
|
||||||
|
(ev: Event) => {
|
||||||
|
// In edit mode let it go to hui-root and rebuild whole section.
|
||||||
|
if (!this.lovelace!.editMode) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._rebuildCard(element, cardConfig);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public willUpdate(changedProperties: PropertyValues<typeof this>): void {
|
||||||
|
super.willUpdate(changedProperties);
|
||||||
|
|
||||||
|
/*
|
||||||
|
We need to handle the following use cases:
|
||||||
|
- initialization: create layout element, populate
|
||||||
|
- config changed to section with same layout element
|
||||||
|
- config changed to section with different layout element
|
||||||
|
- forwarded properties hass/narrow/lovelace/cards change
|
||||||
|
- cards change if one is rebuild when it was loaded later
|
||||||
|
- lovelace changes if edit mode is enabled or config has changed
|
||||||
|
*/
|
||||||
|
|
||||||
|
const oldConfig = changedProperties.get("config");
|
||||||
|
|
||||||
|
// If config has changed, create element if necessary and set all values.
|
||||||
|
if (
|
||||||
|
changedProperties.has("config") &&
|
||||||
|
(!oldConfig || this.config !== oldConfig)
|
||||||
|
) {
|
||||||
|
this._initializeConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected update(changedProperties) {
|
||||||
|
super.update(changedProperties);
|
||||||
|
|
||||||
|
// If no layout element, we're still creating one
|
||||||
|
if (this._layoutElement) {
|
||||||
|
// Config has not changed. Just props
|
||||||
|
if (changedProperties.has("hass")) {
|
||||||
|
this._cards.forEach((element) => {
|
||||||
|
try {
|
||||||
|
element.hass = this.hass;
|
||||||
|
} catch (e: any) {
|
||||||
|
this._rebuildCard(element, createErrorCardConfig(e.message, null));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._layoutElement.hass = this.hass;
|
||||||
|
}
|
||||||
|
if (changedProperties.has("lovelace")) {
|
||||||
|
this._layoutElement.lovelace = this.lovelace;
|
||||||
|
}
|
||||||
|
if (changedProperties.has("_cards")) {
|
||||||
|
this._layoutElement.cards = this._cards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _initializeConfig() {
|
||||||
|
let sectionConfig = { ...this.config };
|
||||||
|
let isStrategy = false;
|
||||||
|
|
||||||
|
if (isStrategySection(sectionConfig)) {
|
||||||
|
isStrategy = true;
|
||||||
|
sectionConfig = await generateLovelaceSectionStrategy(
|
||||||
|
sectionConfig.strategy,
|
||||||
|
this.hass!
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
sectionConfig = {
|
||||||
|
...sectionConfig,
|
||||||
|
type: sectionConfig.type || DEFAULT_SECTION_LAYOUT,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a new layout element if necessary.
|
||||||
|
let addLayoutElement = false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this._layoutElement ||
|
||||||
|
this._layoutElementType !== sectionConfig.type
|
||||||
|
) {
|
||||||
|
addLayoutElement = true;
|
||||||
|
this._createLayoutElement(sectionConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._createCards(sectionConfig);
|
||||||
|
this._layoutElement!.isStrategy = isStrategy;
|
||||||
|
this._layoutElement!.hass = this.hass;
|
||||||
|
this._layoutElement!.lovelace = this.lovelace;
|
||||||
|
this._layoutElement!.index = this.index;
|
||||||
|
this._layoutElement!.viewIndex = this.viewIndex;
|
||||||
|
this._layoutElement!.cards = this._cards;
|
||||||
|
|
||||||
|
if (addLayoutElement) {
|
||||||
|
while (this.lastChild) {
|
||||||
|
this.removeChild(this.lastChild);
|
||||||
|
}
|
||||||
|
this.appendChild(this._layoutElement!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createLayoutElement(config: LovelaceSectionConfig): void {
|
||||||
|
this._layoutElement = createSectionElement(
|
||||||
|
config
|
||||||
|
) as LovelaceSectionElement;
|
||||||
|
this._layoutElementType = config.type;
|
||||||
|
this._layoutElement.addEventListener("ll-create-card", (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
showCreateCardDialog(this, {
|
||||||
|
lovelaceConfig: this.lovelace.config,
|
||||||
|
saveConfig: this.lovelace.saveConfig,
|
||||||
|
path: [this.viewIndex, this.index],
|
||||||
|
suggestedCards: ev.detail?.suggested,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this._layoutElement.addEventListener("ll-edit-card", (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
||||||
|
showEditCardDialog(this, {
|
||||||
|
lovelaceConfig: this.lovelace.config,
|
||||||
|
saveConfig: this.lovelace.saveConfig,
|
||||||
|
path: [this.viewIndex, this.index],
|
||||||
|
cardIndex,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this._layoutElement.addEventListener("ll-delete-card", (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
if (ev.detail.confirm) {
|
||||||
|
confDeleteCard(this, this.hass!, this.lovelace!, ev.detail.path);
|
||||||
|
} else {
|
||||||
|
const newLovelace = deleteCard(this.lovelace!.config, ev.detail.path);
|
||||||
|
this.lovelace.saveConfig(newLovelace);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createCards(config: LovelaceSectionConfig): void {
|
||||||
|
if (!config || !config.cards || !Array.isArray(config.cards)) {
|
||||||
|
this._cards = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._cards = config.cards.map((cardConfig) => {
|
||||||
|
const element = this.createCardElement(cardConfig);
|
||||||
|
try {
|
||||||
|
element.hass = this.hass;
|
||||||
|
} catch (e: any) {
|
||||||
|
return createErrorCardElement(
|
||||||
|
createErrorCardConfig(e.message, cardConfig)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _rebuildCard(
|
||||||
|
cardElToReplace: LovelaceCard,
|
||||||
|
config: LovelaceCardConfig
|
||||||
|
): void {
|
||||||
|
let newCardEl = this.createCardElement(config);
|
||||||
|
try {
|
||||||
|
newCardEl.hass = this.hass;
|
||||||
|
} catch (e: any) {
|
||||||
|
newCardEl = createErrorCardElement(
|
||||||
|
createErrorCardConfig(e.message, config)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (cardElToReplace.parentElement) {
|
||||||
|
cardElToReplace.parentElement!.replaceChild(newCardEl, cardElToReplace);
|
||||||
|
}
|
||||||
|
this._cards = this._cards!.map((curCardEl) =>
|
||||||
|
curCardEl === cardElToReplace ? newCardEl : curCardEl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-section": HuiSection;
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import { AsyncReturnType, HomeAssistant } from "../../../types";
|
||||||
import { cleanLegacyStrategyConfig, isLegacyStrategy } from "./legacy-strategy";
|
import { cleanLegacyStrategyConfig, isLegacyStrategy } from "./legacy-strategy";
|
||||||
import {
|
import {
|
||||||
LovelaceDashboardStrategy,
|
LovelaceDashboardStrategy,
|
||||||
|
LovelaceSectionStrategy,
|
||||||
LovelaceStrategy,
|
LovelaceStrategy,
|
||||||
LovelaceViewStrategy,
|
LovelaceViewStrategy,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
@ -27,13 +28,15 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
|
||||||
"original-states": () => import("./original-states-view-strategy"),
|
"original-states": () => import("./original-states-view-strategy"),
|
||||||
energy: () => import("../../energy/strategies/energy-view-strategy"),
|
energy: () => import("../../energy/strategies/energy-view-strategy"),
|
||||||
},
|
},
|
||||||
|
section: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LovelaceStrategyConfigType = "dashboard" | "view";
|
export type LovelaceStrategyConfigType = "dashboard" | "view" | "section";
|
||||||
|
|
||||||
type Strategies = {
|
type Strategies = {
|
||||||
dashboard: LovelaceDashboardStrategy;
|
dashboard: LovelaceDashboardStrategy;
|
||||||
view: LovelaceViewStrategy;
|
view: LovelaceViewStrategy;
|
||||||
|
section: LovelaceSectionStrategy;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StrategyConfig<T extends LovelaceStrategyConfigType> = AsyncReturnType<
|
type StrategyConfig<T extends LovelaceStrategyConfigType> = AsyncReturnType<
|
||||||
|
@ -163,6 +166,24 @@ export const generateLovelaceViewStrategy = async (
|
||||||
hass
|
hass
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const generateLovelaceSectionStrategy = async (
|
||||||
|
strategyConfig: LovelaceStrategyConfig,
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<LovelaceViewConfig> =>
|
||||||
|
generateStrategy(
|
||||||
|
"section",
|
||||||
|
(err) => ({
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
type: "markdown",
|
||||||
|
content: `Error loading the section strategy:\n> ${err}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
strategyConfig,
|
||||||
|
hass
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find all references to strategies and replaces them with the generated output
|
* Find all references to strategies and replaces them with the generated output
|
||||||
*/
|
*/
|
||||||
|
@ -175,11 +196,24 @@ export const expandLovelaceConfigStrategies = async (
|
||||||
: { ...config };
|
: { ...config };
|
||||||
|
|
||||||
newConfig.views = await Promise.all(
|
newConfig.views = await Promise.all(
|
||||||
newConfig.views.map((view) =>
|
newConfig.views.map(async (view) => {
|
||||||
isStrategyView(view)
|
const newView = isStrategyView(view)
|
||||||
? generateLovelaceViewStrategy(view.strategy, hass)
|
? await generateLovelaceViewStrategy(view.strategy, hass)
|
||||||
: view
|
: { ...view };
|
||||||
)
|
|
||||||
|
if (newView.sections) {
|
||||||
|
newView.sections = await Promise.all(
|
||||||
|
newView.sections.map(async (section) => {
|
||||||
|
const newSection = isStrategyView(section)
|
||||||
|
? await generateLovelaceSectionStrategy(section.strategy, hass)
|
||||||
|
: { ...section };
|
||||||
|
return newSection;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newView;
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return newConfig;
|
return newConfig;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { LovelaceConfig } from "../../../data/lovelace/config/types";
|
import { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||||
import { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
import { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||||
|
import { LovelaceConfig } from "../../../data/lovelace/config/types";
|
||||||
import { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
import { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { LovelaceGenericElementEditor } from "../types";
|
import { LovelaceGenericElementEditor } from "../types";
|
||||||
|
@ -15,6 +16,9 @@ export interface LovelaceDashboardStrategy
|
||||||
export interface LovelaceViewStrategy
|
export interface LovelaceViewStrategy
|
||||||
extends LovelaceStrategy<LovelaceViewConfig> {}
|
extends LovelaceStrategy<LovelaceViewConfig> {}
|
||||||
|
|
||||||
|
export interface LovelaceSectionStrategy
|
||||||
|
extends LovelaceStrategy<LovelaceSectionConfig> {}
|
||||||
|
|
||||||
export interface LovelaceStrategyEditor extends LovelaceGenericElementEditor {
|
export interface LovelaceStrategyEditor extends LovelaceGenericElementEditor {
|
||||||
setConfig(config: LovelaceStrategyConfig): void;
|
setConfig(config: LovelaceStrategyConfig): void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ export interface LovelaceCard extends HTMLElement {
|
||||||
isPanel?: boolean;
|
isPanel?: boolean;
|
||||||
editMode?: boolean;
|
editMode?: boolean;
|
||||||
getCardSize(): number | Promise<number>;
|
getCardSize(): number | Promise<number>;
|
||||||
|
getGridSize?(): [number, number];
|
||||||
setConfig(config: LovelaceCardConfig): void;
|
setConfig(config: LovelaceCardConfig): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
export const DEFAULT_VIEW_LAYOUT = "masonry";
|
export const DEFAULT_VIEW_LAYOUT = "masonry";
|
||||||
export const PANEL_VIEW_LAYOUT = "panel";
|
export const PANEL_VIEW_LAYOUT = "panel";
|
||||||
export const SIDEBAR_VIEW_LAYOUT = "sidebar";
|
export const SIDEBAR_VIEW_LAYOUT = "sidebar";
|
||||||
export const VIEWS_NO_BADGE_SUPPORT = [PANEL_VIEW_LAYOUT, SIDEBAR_VIEW_LAYOUT];
|
export const SECTION_VIEW_LAYOUT = "sections";
|
||||||
|
export const VIEWS_NO_BADGE_SUPPORT = [
|
||||||
|
PANEL_VIEW_LAYOUT,
|
||||||
|
SIDEBAR_VIEW_LAYOUT,
|
||||||
|
SECTION_VIEW_LAYOUT,
|
||||||
|
];
|
||||||
|
|
|
@ -0,0 +1,322 @@
|
||||||
|
import { mdiArrowAll, mdiDelete, mdiPencil, mdiViewGridPlus } from "@mdi/js";
|
||||||
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import "../../../components/ha-icon-button";
|
||||||
|
import "../../../components/ha-sortable";
|
||||||
|
import "../../../components/ha-svg-icon";
|
||||||
|
import type { LovelaceViewElement } from "../../../data/lovelace";
|
||||||
|
import { LovelaceSectionConfig as LovelaceRawSectionConfig } from "../../../data/lovelace/config/section";
|
||||||
|
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||||
|
import {
|
||||||
|
showConfirmationDialog,
|
||||||
|
showPromptDialog,
|
||||||
|
} from "../../../dialogs/generic/show-dialog-box";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import { addSection, deleteSection, moveSection } from "../editor/config-util";
|
||||||
|
import {
|
||||||
|
findLovelaceContainer,
|
||||||
|
updateLovelaceContainer,
|
||||||
|
} from "../editor/lovelace-path";
|
||||||
|
import { HuiSection } from "../sections/hui-section";
|
||||||
|
import type { Lovelace } from "../types";
|
||||||
|
|
||||||
|
@customElement("hui-sections-view")
|
||||||
|
export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||||
|
|
||||||
|
@property({ type: Number }) public index?: number;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public isStrategy = false;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public sections: HuiSection[] = [];
|
||||||
|
|
||||||
|
@state() private _config?: LovelaceViewConfig;
|
||||||
|
|
||||||
|
public setConfig(config: LovelaceViewConfig): void {
|
||||||
|
this._config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _sectionConfigKeys = new WeakMap<LovelaceRawSectionConfig, string>();
|
||||||
|
|
||||||
|
private _getKey(sectionConfig: LovelaceRawSectionConfig) {
|
||||||
|
if (!this._sectionConfigKeys.has(sectionConfig)) {
|
||||||
|
this._sectionConfigKeys.set(sectionConfig, Math.random().toString());
|
||||||
|
}
|
||||||
|
return this._sectionConfigKeys.get(sectionConfig)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.lovelace) return nothing;
|
||||||
|
|
||||||
|
const sectionsConfig = this._config?.sections ?? [];
|
||||||
|
|
||||||
|
const editMode = this.lovelace.editMode;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-sortable
|
||||||
|
.disabled=${!editMode}
|
||||||
|
@item-moved=${this._sectionMoved}
|
||||||
|
group="section"
|
||||||
|
handle-selector=".handle"
|
||||||
|
draggable-selector=".section"
|
||||||
|
.rollback=${false}
|
||||||
|
>
|
||||||
|
<div class="container">
|
||||||
|
${repeat(
|
||||||
|
sectionsConfig,
|
||||||
|
(sectionConfig) => this._getKey(sectionConfig),
|
||||||
|
(_sectionConfig, idx) => {
|
||||||
|
const section = this.sections[idx];
|
||||||
|
(section as any).itemPath = [idx];
|
||||||
|
return html`
|
||||||
|
<div class="section">
|
||||||
|
${editMode
|
||||||
|
? html`
|
||||||
|
<div class="section-overlay">
|
||||||
|
<div class="section-actions">
|
||||||
|
<ha-svg-icon
|
||||||
|
aria-hidden="true"
|
||||||
|
class="handle"
|
||||||
|
.path=${mdiArrowAll}
|
||||||
|
></ha-svg-icon>
|
||||||
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize("ui.common.edit")}
|
||||||
|
@click=${this._editSection}
|
||||||
|
.index=${idx}
|
||||||
|
.path=${mdiPencil}
|
||||||
|
></ha-icon-button>
|
||||||
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize("ui.common.delete")}
|
||||||
|
@click=${this._deleteSection}
|
||||||
|
.index=${idx}
|
||||||
|
.path=${mdiDelete}
|
||||||
|
></ha-icon-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<div class="section-wrapper">${section}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
${editMode
|
||||||
|
? html`
|
||||||
|
<button
|
||||||
|
class="add"
|
||||||
|
@click=${this._addSection}
|
||||||
|
aria-label=${this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.section.add_section"
|
||||||
|
)}
|
||||||
|
.title=${this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.section.add_section"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiViewGridPlus}></ha-svg-icon>
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
|
</ha-sortable>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addSection(): void {
|
||||||
|
const newConfig = addSection(this.lovelace!.config, this.index!, {
|
||||||
|
type: "grid",
|
||||||
|
cards: [],
|
||||||
|
});
|
||||||
|
this.lovelace!.saveConfig(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _editSection(ev) {
|
||||||
|
const index = ev.currentTarget.index;
|
||||||
|
|
||||||
|
const path = [this.index!, index] as [number, number];
|
||||||
|
|
||||||
|
const section = findLovelaceContainer(
|
||||||
|
this.lovelace!.config,
|
||||||
|
path
|
||||||
|
) as LovelaceRawSectionConfig;
|
||||||
|
|
||||||
|
const newTitle = !section.title;
|
||||||
|
|
||||||
|
const title = await showPromptDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
`ui.panel.lovelace.editor.edit_section_title.${newTitle ? "title_new" : "title"}`
|
||||||
|
),
|
||||||
|
inputLabel: this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.edit_section_title.input_label"
|
||||||
|
),
|
||||||
|
inputType: "string",
|
||||||
|
defaultValue: section.title,
|
||||||
|
confirmText: newTitle
|
||||||
|
? this.hass.localize("ui.common.add")
|
||||||
|
: this.hass.localize("ui.common.save"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (title === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newConfig = updateLovelaceContainer(this.lovelace!.config, path, {
|
||||||
|
...section,
|
||||||
|
title: title || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.lovelace!.saveConfig(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _deleteSection(ev) {
|
||||||
|
const index = ev.currentTarget.index;
|
||||||
|
|
||||||
|
const path = [this.index!, index] as [number, number];
|
||||||
|
|
||||||
|
const section = findLovelaceContainer(
|
||||||
|
this.lovelace!.config,
|
||||||
|
path
|
||||||
|
) as LovelaceRawSectionConfig;
|
||||||
|
|
||||||
|
const title = section.title;
|
||||||
|
const cardCount = section.cards?.length;
|
||||||
|
|
||||||
|
if (title || cardCount) {
|
||||||
|
const sectionName = title?.trim()
|
||||||
|
? this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.delete_section.named_section",
|
||||||
|
{ name: title }
|
||||||
|
)
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.delete_section.unnamed_section"
|
||||||
|
);
|
||||||
|
|
||||||
|
const content = cardCount
|
||||||
|
? this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.delete_section.text_section_and_cards",
|
||||||
|
{
|
||||||
|
section: sectionName,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.delete_section.text_section_only",
|
||||||
|
{
|
||||||
|
section: sectionName,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const confirm = await showConfirmationDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.delete_section.title"
|
||||||
|
),
|
||||||
|
text: content,
|
||||||
|
confirmText: this.hass.localize("ui.common.delete"),
|
||||||
|
destructive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirm) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newConfig = deleteSection(this.lovelace!.config, this.index!, index);
|
||||||
|
this.lovelace!.saveConfig(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _sectionMoved(ev: CustomEvent) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
|
|
||||||
|
const newConfig = moveSection(
|
||||||
|
this.lovelace!.config,
|
||||||
|
[this.index!, oldIndex],
|
||||||
|
[this.index!, newIndex]
|
||||||
|
);
|
||||||
|
this.lovelace!.saveConfig(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
position: relative;
|
||||||
|
border-radius: var(--ha-card-border-radius, 12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
--column-count: 3;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--column-count), minmax(0, 1fr));
|
||||||
|
gap: 8px 20px;
|
||||||
|
max-width: 1400px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.container {
|
||||||
|
--column-count: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.container {
|
||||||
|
--column-count: 1;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
opacity: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
background-color: rgba(var(--rgb-card-background-color), 0.3);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: var(--secondary-background-color);
|
||||||
|
--mdc-icon-button-size: 36px;
|
||||||
|
--mdc-icon-size: 20px;
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.handle {
|
||||||
|
cursor: grab;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add {
|
||||||
|
margin-top: calc(66px + 8px);
|
||||||
|
outline: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--ha-card-border-radius, 12px);
|
||||||
|
border: 2px dashed var(--primary-color);
|
||||||
|
order: 1;
|
||||||
|
height: 66px;
|
||||||
|
padding: 8px;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add:focus {
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-ghost {
|
||||||
|
border-radius: var(--ha-card-border-radius, 12px);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-sections-view": SectionsView;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,17 @@
|
||||||
import { PropertyValues, ReactiveElement } from "lit";
|
import { PropertyValues, ReactiveElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||||
|
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||||
import "../../../components/entity/ha-state-label-badge";
|
import "../../../components/entity/ha-state-label-badge";
|
||||||
import "../../../components/ha-svg-icon";
|
import "../../../components/ha-svg-icon";
|
||||||
import type { LovelaceViewElement } from "../../../data/lovelace";
|
import type { LovelaceViewElement } from "../../../data/lovelace";
|
||||||
|
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||||
|
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||||
|
import { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||||
|
import {
|
||||||
|
LovelaceViewConfig,
|
||||||
|
isStrategyView,
|
||||||
|
} from "../../../data/lovelace/config/view";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import {
|
import {
|
||||||
createErrorBadgeConfig,
|
createErrorBadgeConfig,
|
||||||
|
@ -20,25 +28,25 @@ import {
|
||||||
import { createViewElement } from "../create-element/create-view-element";
|
import { createViewElement } from "../create-element/create-view-element";
|
||||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
import { showCreateCardDialog } from "../editor/card-editor/show-create-card-dialog";
|
||||||
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog";
|
||||||
import { confDeleteCard } from "../editor/delete-card";
|
|
||||||
import { deleteCard } from "../editor/config-util";
|
import { deleteCard } from "../editor/config-util";
|
||||||
|
import { confDeleteCard } from "../editor/delete-card";
|
||||||
|
import {
|
||||||
|
LovelaceCardPath,
|
||||||
|
parseLovelaceCardPath,
|
||||||
|
} from "../editor/lovelace-path";
|
||||||
|
import { createErrorSectionConfig } from "../sections/hui-error-section";
|
||||||
|
import "../sections/hui-section";
|
||||||
|
import type { HuiSection } from "../sections/hui-section";
|
||||||
import { generateLovelaceViewStrategy } from "../strategies/get-strategy";
|
import { generateLovelaceViewStrategy } from "../strategies/get-strategy";
|
||||||
import type { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
|
import type { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
|
||||||
import { PANEL_VIEW_LAYOUT, DEFAULT_VIEW_LAYOUT } from "./const";
|
import { DEFAULT_VIEW_LAYOUT, PANEL_VIEW_LAYOUT } from "./const";
|
||||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
|
||||||
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
|
||||||
import {
|
|
||||||
LovelaceViewConfig,
|
|
||||||
isStrategyView,
|
|
||||||
} from "../../../data/lovelace/config/view";
|
|
||||||
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// for fire event
|
// for fire event
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
"ll-create-card": undefined;
|
"ll-create-card": { suggested?: string[] } | undefined;
|
||||||
"ll-edit-card": { path: [number, number] };
|
"ll-edit-card": { path: LovelaceCardPath };
|
||||||
"ll-delete-card": { path: [number, number]; confirm: boolean };
|
"ll-delete-card": { path: LovelaceCardPath; confirm: boolean };
|
||||||
}
|
}
|
||||||
interface HTMLElementEventMap {
|
interface HTMLElementEventMap {
|
||||||
"ll-create-card": HASSDomEvent<HASSDomEvents["ll-create-card"]>;
|
"ll-create-card": HASSDomEvent<HASSDomEvents["ll-create-card"]>;
|
||||||
|
@ -61,6 +69,8 @@ export class HUIView extends ReactiveElement {
|
||||||
|
|
||||||
@state() private _badges: LovelaceBadge[] = [];
|
@state() private _badges: LovelaceBadge[] = [];
|
||||||
|
|
||||||
|
@state() private _sections: HuiSection[] = [];
|
||||||
|
|
||||||
private _layoutElementType?: string;
|
private _layoutElementType?: string;
|
||||||
|
|
||||||
private _layoutElement?: LovelaceViewElement;
|
private _layoutElement?: LovelaceViewElement;
|
||||||
|
@ -108,6 +118,27 @@ export class HUIView extends ReactiveElement {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Public to make demo happy
|
||||||
|
public createSectionElement(sectionConfig: LovelaceSectionConfig) {
|
||||||
|
const element = document.createElement("hui-section");
|
||||||
|
element.hass = this.hass;
|
||||||
|
element.lovelace = this.lovelace;
|
||||||
|
element.config = sectionConfig;
|
||||||
|
element.viewIndex = this.index;
|
||||||
|
element.addEventListener(
|
||||||
|
"ll-rebuild",
|
||||||
|
(ev: Event) => {
|
||||||
|
// In edit mode let it go to hui-root and rebuild whole view.
|
||||||
|
if (!this.lovelace!.editMode) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._rebuildSection(element, sectionConfig);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
protected createRenderRoot() {
|
protected createRenderRoot() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -139,7 +170,7 @@ export class HUIView extends ReactiveElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected update(changedProperties) {
|
protected update(changedProperties: PropertyValues) {
|
||||||
super.update(changedProperties);
|
super.update(changedProperties);
|
||||||
|
|
||||||
// If no layout element, we're still creating one
|
// If no layout element, we're still creating one
|
||||||
|
@ -162,6 +193,14 @@ export class HUIView extends ReactiveElement {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this._sections.forEach((element) => {
|
||||||
|
try {
|
||||||
|
element.hass = this.hass;
|
||||||
|
} catch (e: any) {
|
||||||
|
this._rebuildSection(element, createErrorSectionConfig(e.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this._layoutElement.hass = this.hass;
|
this._layoutElement.hass = this.hass;
|
||||||
|
|
||||||
const oldHass = changedProperties.get("hass") as
|
const oldHass = changedProperties.get("hass") as
|
||||||
|
@ -181,6 +220,14 @@ export class HUIView extends ReactiveElement {
|
||||||
}
|
}
|
||||||
if (changedProperties.has("lovelace")) {
|
if (changedProperties.has("lovelace")) {
|
||||||
this._layoutElement.lovelace = this.lovelace;
|
this._layoutElement.lovelace = this.lovelace;
|
||||||
|
this._sections.forEach((element) => {
|
||||||
|
try {
|
||||||
|
element.hass = this.hass;
|
||||||
|
element.lovelace = this.lovelace;
|
||||||
|
} catch (e: any) {
|
||||||
|
this._rebuildSection(element, createErrorSectionConfig(e.message));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (changedProperties.has("_cards")) {
|
if (changedProperties.has("_cards")) {
|
||||||
this._layoutElement.cards = this._cards;
|
this._layoutElement.cards = this._cards;
|
||||||
|
@ -220,6 +267,7 @@ export class HUIView extends ReactiveElement {
|
||||||
|
|
||||||
this._createBadges(viewConfig);
|
this._createBadges(viewConfig);
|
||||||
this._createCards(viewConfig);
|
this._createCards(viewConfig);
|
||||||
|
this._createSections(viewConfig);
|
||||||
this._layoutElement!.isStrategy = isStrategy;
|
this._layoutElement!.isStrategy = isStrategy;
|
||||||
this._layoutElement!.hass = this.hass;
|
this._layoutElement!.hass = this.hass;
|
||||||
this._layoutElement!.narrow = this.narrow;
|
this._layoutElement!.narrow = this.narrow;
|
||||||
|
@ -227,6 +275,7 @@ export class HUIView extends ReactiveElement {
|
||||||
this._layoutElement!.index = this.index;
|
this._layoutElement!.index = this.index;
|
||||||
this._layoutElement!.cards = this._cards;
|
this._layoutElement!.cards = this._cards;
|
||||||
this._layoutElement!.badges = this._badges;
|
this._layoutElement!.badges = this._badges;
|
||||||
|
this._layoutElement!.sections = this._sections;
|
||||||
|
|
||||||
applyThemesOnElement(this, this.hass.themes, viewConfig.theme);
|
applyThemesOnElement(this, this.hass.themes, viewConfig.theme);
|
||||||
this._viewConfigTheme = viewConfig.theme;
|
this._viewConfigTheme = viewConfig.theme;
|
||||||
|
@ -242,18 +291,21 @@ export class HUIView extends ReactiveElement {
|
||||||
private _createLayoutElement(config: LovelaceViewConfig): void {
|
private _createLayoutElement(config: LovelaceViewConfig): void {
|
||||||
this._layoutElement = createViewElement(config) as LovelaceViewElement;
|
this._layoutElement = createViewElement(config) as LovelaceViewElement;
|
||||||
this._layoutElementType = config.type;
|
this._layoutElementType = config.type;
|
||||||
this._layoutElement.addEventListener("ll-create-card", () => {
|
this._layoutElement.addEventListener("ll-create-card", (ev) => {
|
||||||
showCreateCardDialog(this, {
|
showCreateCardDialog(this, {
|
||||||
lovelaceConfig: this.lovelace.config,
|
lovelaceConfig: this.lovelace.config,
|
||||||
saveConfig: this.lovelace.saveConfig,
|
saveConfig: this.lovelace.saveConfig,
|
||||||
path: [this.index],
|
path: [this.index],
|
||||||
|
suggestedCards: ev.detail?.suggested,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this._layoutElement.addEventListener("ll-edit-card", (ev) => {
|
this._layoutElement.addEventListener("ll-edit-card", (ev) => {
|
||||||
|
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
||||||
showEditCardDialog(this, {
|
showEditCardDialog(this, {
|
||||||
lovelaceConfig: this.lovelace.config,
|
lovelaceConfig: this.lovelace.config,
|
||||||
saveConfig: this.lovelace.saveConfig,
|
saveConfig: this.lovelace.saveConfig,
|
||||||
path: ev.detail.path,
|
path: [this.index],
|
||||||
|
cardIndex,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this._layoutElement.addEventListener("ll-delete-card", (ev) => {
|
this._layoutElement.addEventListener("ll-delete-card", (ev) => {
|
||||||
|
@ -303,6 +355,19 @@ export class HUIView extends ReactiveElement {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _createSections(config: LovelaceViewConfig): void {
|
||||||
|
if (!config || !config.sections || !Array.isArray(config.sections)) {
|
||||||
|
this._sections = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._sections = config.sections.map((sectionConfig, index) => {
|
||||||
|
const element = this.createSectionElement(sectionConfig);
|
||||||
|
element.index = index;
|
||||||
|
return element;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private _rebuildCard(
|
private _rebuildCard(
|
||||||
cardElToReplace: LovelaceCard,
|
cardElToReplace: LovelaceCard,
|
||||||
config: LovelaceCardConfig
|
config: LovelaceCardConfig
|
||||||
|
@ -343,6 +408,23 @@ export class HUIView extends ReactiveElement {
|
||||||
curBadgeEl === badgeElToReplace ? newBadgeEl : curBadgeEl
|
curBadgeEl === badgeElToReplace ? newBadgeEl : curBadgeEl
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _rebuildSection(
|
||||||
|
sectionElToReplace: HuiSection,
|
||||||
|
config: LovelaceSectionConfig
|
||||||
|
): void {
|
||||||
|
const newSectionEl = this.createSectionElement(config);
|
||||||
|
newSectionEl.index = sectionElToReplace.index;
|
||||||
|
if (sectionElToReplace.parentElement) {
|
||||||
|
sectionElToReplace.parentElement!.replaceChild(
|
||||||
|
newSectionEl,
|
||||||
|
sectionElToReplace
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this._sections = this._sections!.map((curSectionEl) =>
|
||||||
|
curSectionEl === sectionElToReplace ? newSectionEl : curSectionEl
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
|
@ -5096,7 +5096,8 @@
|
||||||
"types": {
|
"types": {
|
||||||
"masonry": "Masonry (default)",
|
"masonry": "Masonry (default)",
|
||||||
"sidebar": "Sidebar",
|
"sidebar": "Sidebar",
|
||||||
"panel": "Panel (1 card)"
|
"panel": "Panel (1 card)",
|
||||||
|
"sections": "Sections (experimental)"
|
||||||
},
|
},
|
||||||
"subview": "Subview",
|
"subview": "Subview",
|
||||||
"subview_helper": "Subviews don't appear in tabs and have a back button.",
|
"subview_helper": "Subviews don't appear in tabs and have a back button.",
|
||||||
|
@ -5112,7 +5113,7 @@
|
||||||
"header": "Card configuration",
|
"header": "Card configuration",
|
||||||
"typed_header": "{type} Card configuration",
|
"typed_header": "{type} Card configuration",
|
||||||
"pick_card": "Which card would you like to add?",
|
"pick_card": "Which card would you like to add?",
|
||||||
"pick_card_view_title": "Which card would you like to add to your {name} view?",
|
"pick_card_title": "Which card would you like to add to {name}",
|
||||||
"toggle_editor": "Toggle editor",
|
"toggle_editor": "Toggle editor",
|
||||||
"unsaved_changes": "You have unsaved changes",
|
"unsaved_changes": "You have unsaved changes",
|
||||||
"confirm_cancel": "Are you sure you want to cancel?",
|
"confirm_cancel": "Are you sure you want to cancel?",
|
||||||
|
@ -5150,6 +5151,23 @@
|
||||||
"no_config": "No config found.",
|
"no_config": "No config found.",
|
||||||
"no_views": "No views in this dashboard."
|
"no_views": "No views in this dashboard."
|
||||||
},
|
},
|
||||||
|
"section": {
|
||||||
|
"unnamed_section": "Unnamed section",
|
||||||
|
"add_card": "[%key:ui::panel::lovelace::editor::edit_card::add%]",
|
||||||
|
"add_section": "Add section"
|
||||||
|
},
|
||||||
|
"delete_section": {
|
||||||
|
"title": "Delete section",
|
||||||
|
"named_section": "\"{name}\" section",
|
||||||
|
"unnamed_section": "This section",
|
||||||
|
"text_section_only": "{section} will be deleted.",
|
||||||
|
"text_section_and_cards": "{section} and all its cards will be deleted."
|
||||||
|
},
|
||||||
|
"edit_section_title": {
|
||||||
|
"title": "Edit name",
|
||||||
|
"title_new": "Add name",
|
||||||
|
"input_label": "Name"
|
||||||
|
},
|
||||||
"suggest_card": {
|
"suggest_card": {
|
||||||
"header": "We created a suggestion for you",
|
"header": "We created a suggestion for you",
|
||||||
"create_own": "Pick different card",
|
"create_own": "Pick different card",
|
||||||
|
@ -5455,7 +5473,10 @@
|
||||||
"state": "State",
|
"state": "State",
|
||||||
"secondary_info_attribute": "Secondary info attribute",
|
"secondary_info_attribute": "Secondary info attribute",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"state_color": "Color icons based on state?"
|
"state_color": "Color icons based on state?",
|
||||||
|
"suggested_cards": "Suggested cards",
|
||||||
|
"other_cards": "Other cards",
|
||||||
|
"custom_cards": "Custom cards"
|
||||||
},
|
},
|
||||||
"map": {
|
"map": {
|
||||||
"name": "Map",
|
"name": "Map",
|
||||||
|
|
|
@ -1,63 +1,12 @@
|
||||||
import { assert } from "chai";
|
import { assert } from "chai";
|
||||||
|
|
||||||
|
import { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
|
||||||
import {
|
import {
|
||||||
swapCard,
|
moveCardToContainer,
|
||||||
moveCard,
|
|
||||||
swapView,
|
swapView,
|
||||||
} from "../../../../src/panels/lovelace/editor/config-util";
|
} from "../../../../src/panels/lovelace/editor/config-util";
|
||||||
import { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
|
|
||||||
|
|
||||||
describe("swapCard", () => {
|
describe("moveCardToContainer", () => {
|
||||||
it("swaps 2 cards in same view", () => {
|
|
||||||
const config: LovelaceConfig = {
|
|
||||||
views: [
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
cards: [{ type: "card1" }, { type: "card2" }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = swapCard(config, [1, 0], [1, 1]);
|
|
||||||
const expected = {
|
|
||||||
views: [
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
cards: [{ type: "card2" }, { type: "card1" }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
assert.deepEqual(expected, result);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("swaps 2 cards in different views", () => {
|
|
||||||
const config: LovelaceConfig = {
|
|
||||||
views: [
|
|
||||||
{
|
|
||||||
cards: [{ type: "v1-c1" }, { type: "v1-c2" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cards: [{ type: "v2-c1" }, { type: "v2-c2" }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = swapCard(config, [0, 0], [1, 1]);
|
|
||||||
const expected: LovelaceConfig = {
|
|
||||||
views: [
|
|
||||||
{
|
|
||||||
cards: [{ type: "v2-c2" }, { type: "v1-c2" }],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
cards: [{ type: "v2-c1" }, { type: "v1-c1" }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
assert.deepEqual(expected, result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("moveCard", () => {
|
|
||||||
it("move a card to an empty view", () => {
|
it("move a card to an empty view", () => {
|
||||||
const config: LovelaceConfig = {
|
const config: LovelaceConfig = {
|
||||||
views: [
|
views: [
|
||||||
|
@ -68,7 +17,7 @@ describe("moveCard", () => {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = moveCard(config, [1, 0], [0]);
|
const result = moveCardToContainer(config, [1, 0], [0]);
|
||||||
const expected: LovelaceConfig = {
|
const expected: LovelaceConfig = {
|
||||||
views: [
|
views: [
|
||||||
{
|
{
|
||||||
|
@ -94,7 +43,7 @@ describe("moveCard", () => {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = moveCard(config, [1, 0], [0]);
|
const result = moveCardToContainer(config, [1, 0], [0]);
|
||||||
const expected: LovelaceConfig = {
|
const expected: LovelaceConfig = {
|
||||||
views: [
|
views: [
|
||||||
{
|
{
|
||||||
|
@ -121,12 +70,12 @@ describe("moveCard", () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = () => {
|
const result = () => {
|
||||||
moveCard(config, [1, 0], [1]);
|
moveCardToContainer(config, [1, 0], [1]);
|
||||||
};
|
};
|
||||||
assert.throws(
|
assert.throws(
|
||||||
result,
|
result,
|
||||||
Error,
|
Error,
|
||||||
"You cannot move a card to the view it is in."
|
"You cannot move a card to the view or section it is in."
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue