Add experimental sections view (#19846)
This commit is contained in:
parent
47f7cf5419
commit
d95bf64edf
|
@ -20,14 +20,14 @@ function findNestedItem(
|
|||
}, obj);
|
||||
}
|
||||
|
||||
export function nestedArrayMove<T>(
|
||||
obj: T | T[],
|
||||
export function nestedArrayMove<A>(
|
||||
obj: A,
|
||||
oldIndex: number,
|
||||
newIndex: number,
|
||||
oldPath?: ItemPath,
|
||||
newPath?: ItemPath
|
||||
): T | T[] {
|
||||
const newObj = Array.isArray(obj) ? [...obj] : { ...obj };
|
||||
): A {
|
||||
const newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
|
||||
const from = oldPath ? findNestedItem(newObj, oldPath) : newObj;
|
||||
const to = newPath ? findNestedItem(newObj, newPath, true) : newObj;
|
||||
|
||||
|
|
|
@ -14,9 +14,16 @@ declare global {
|
|||
oldPath?: ItemPath;
|
||||
newPath?: ItemPath;
|
||||
};
|
||||
"drag-start": undefined;
|
||||
"drag-end": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export type HaSortableOptions = Omit<
|
||||
SortableInstance.SortableOptions,
|
||||
"onStart" | "onChoose" | "onEnd"
|
||||
>;
|
||||
|
||||
@customElement("ha-sortable")
|
||||
export class HaSortable extends LitElement {
|
||||
private _sortable?: SortableInstance;
|
||||
|
@ -36,14 +43,17 @@ export class HaSortable extends LitElement {
|
|||
@property({ type: String, attribute: "handle-selector" })
|
||||
public handleSelector?: string;
|
||||
|
||||
@property({ type: String, attribute: "group" })
|
||||
public group?: string;
|
||||
|
||||
@property({ type: Number, attribute: "swap-threshold" })
|
||||
public swapThreshold?: number;
|
||||
@property({ type: String })
|
||||
public group?: string | SortableInstance.GroupOptions;
|
||||
|
||||
@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>) {
|
||||
if (changedProperties.has("disabled")) {
|
||||
|
@ -114,26 +124,20 @@ export class HaSortable extends LitElement {
|
|||
|
||||
const options: SortableInstance.Options = {
|
||||
animation: 150,
|
||||
swapThreshold: 1,
|
||||
...this.options,
|
||||
onChoose: this._handleChoose,
|
||||
onStart: this._handleStart,
|
||||
onEnd: this._handleEnd,
|
||||
};
|
||||
|
||||
if (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) {
|
||||
options.handle = this.handleSelector;
|
||||
}
|
||||
if (this.draggableSelector) {
|
||||
options.draggable = this.draggableSelector;
|
||||
if (this.invertSwap !== undefined) {
|
||||
options.invertSwap = this.invertSwap;
|
||||
}
|
||||
if (this.group) {
|
||||
options.group = this.group;
|
||||
|
@ -143,8 +147,9 @@ export class HaSortable extends LitElement {
|
|||
}
|
||||
|
||||
private _handleEnd = async (evt: SortableEvent) => {
|
||||
fireEvent(this, "drag-end");
|
||||
// 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);
|
||||
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) => {
|
||||
if (!this.rollback) return;
|
||||
(evt.item as any).placeholder = document.createComment("sort-placeholder");
|
||||
evt.item.after((evt.item as any).placeholder);
|
||||
};
|
||||
|
|
|
@ -10,8 +10,10 @@ import {
|
|||
LovelaceCard,
|
||||
} from "../panels/lovelace/types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { LovelaceSectionConfig } from "./lovelace/config/section";
|
||||
import { fetchConfig, LegacyLovelaceConfig } from "./lovelace/config/types";
|
||||
import { LovelaceViewConfig } from "./lovelace/config/view";
|
||||
import { HuiSection } from "../panels/lovelace/sections/hui-section";
|
||||
|
||||
export interface LovelacePanelConfig {
|
||||
mode: "yaml" | "storage";
|
||||
|
@ -24,10 +26,21 @@ export interface LovelaceViewElement extends HTMLElement {
|
|||
index?: number;
|
||||
cards?: Array<LovelaceCard | HuiErrorCard>;
|
||||
badges?: LovelaceBadge[];
|
||||
sections?: HuiSection[];
|
||||
isStrategy: boolean;
|
||||
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 & {
|
||||
event_type: "lovelace_updated";
|
||||
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 { LovelaceCardConfig } from "./card";
|
||||
import type { LovelaceSectionRawConfig } from "./section";
|
||||
import type { LovelaceStrategyConfig } from "./strategy";
|
||||
|
||||
export interface ShowViewConfig {
|
||||
|
@ -23,6 +24,7 @@ export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
|
|||
type?: string;
|
||||
badges?: Array<string | LovelaceBadgeConfig>;
|
||||
cards?: LovelaceCardConfig[];
|
||||
sections?: LovelaceSectionRawConfig[];
|
||||
}
|
||||
|
||||
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 { LovelaceRow } from "../../../lovelace/entity-rows/types";
|
||||
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")
|
||||
export class HaDeviceEntitiesCard extends LitElement {
|
||||
|
@ -235,6 +238,9 @@ export class HaDeviceEntitiesCard extends LitElement {
|
|||
computeCards(this.hass.states, entities, {
|
||||
title: this.deviceName,
|
||||
}),
|
||||
computeSection(entities, {
|
||||
title: this.deviceName,
|
||||
}),
|
||||
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 {
|
||||
if (config.entity && !isValidEntityId(config.entity)) {
|
||||
throw new Error("Invalid entity");
|
||||
|
|
|
@ -72,6 +72,10 @@ class HuiSensorCard extends HuiEntityCard {
|
|||
super.setConfig(entityCardConfig);
|
||||
}
|
||||
|
||||
public getSize(): [number, number] {
|
||||
return [2, 2];
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
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) {
|
||||
handleAction(this, this.hass!, this._config!, ev.detail.action!);
|
||||
}
|
||||
|
@ -441,12 +453,16 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
|||
.secondary=${localizedState}
|
||||
></ha-tile-info>
|
||||
</div>
|
||||
<hui-card-features
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.color=${this._config.color}
|
||||
.features=${this._config.features}
|
||||
></hui-card-features>
|
||||
${this._config.features
|
||||
? html`
|
||||
<hui-card-features
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.color=${this._config.color}
|
||||
.features=${this._config.features}
|
||||
></hui-card-features>
|
||||
`
|
||||
: nothing}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
@ -469,6 +485,9 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
|||
transition:
|
||||
box-shadow 180ms ease-in-out,
|
||||
border-color 180ms ease-in-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
ha-card.active {
|
||||
--tile-color: var(--state-icon-color);
|
||||
|
|
|
@ -545,7 +545,7 @@ export interface TileCardConfig extends LovelaceCardConfig {
|
|||
state_content?: string | string[];
|
||||
icon?: string;
|
||||
color?: string;
|
||||
show_entity_picture?: string;
|
||||
show_entity_picture?: boolean;
|
||||
vertical?: boolean;
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
|
|
|
@ -8,12 +8,14 @@ import { stripPrefixFromEntityName } from "../../../common/entity/strip_prefix_f
|
|||
import { stringCompare } from "../../../common/string/compare";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import type { AreaFilterValue } from "../../../components/ha-area-filter";
|
||||
import { areaCompare } from "../../../data/area_registry";
|
||||
import {
|
||||
EnergyPreferences,
|
||||
GridSourceTypeEnergyPreference,
|
||||
} from "../../../data/energy";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import { computeUserInitials } from "../../../data/user";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
@ -25,10 +27,10 @@ import {
|
|||
PictureCardConfig,
|
||||
PictureEntityCardConfig,
|
||||
ThermostatCardConfig,
|
||||
TileCardConfig,
|
||||
} from "../cards/types";
|
||||
import { EntityConfig } from "../entity-rows/types";
|
||||
import { ButtonsHeaderFooterConfig } from "../header-footer/types";
|
||||
import { areaCompare } from "../../../data/area_registry";
|
||||
|
||||
const HIDE_DOMAIN = new Set([
|
||||
"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 = (
|
||||
states: HassEntities,
|
||||
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 { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { saveConfig } from "../../../data/lovelace/config/types";
|
||||
import { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showPromptDialog,
|
||||
|
@ -41,10 +40,15 @@ import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"
|
|||
import {
|
||||
addCard,
|
||||
deleteCard,
|
||||
moveCard,
|
||||
moveCardToPosition,
|
||||
swapCard,
|
||||
moveCardToContainer,
|
||||
moveCardToIndex,
|
||||
} from "../editor/config-util";
|
||||
import {
|
||||
LovelaceCardPath,
|
||||
findLovelaceCards,
|
||||
getLovelaceContainerPath,
|
||||
parseLovelaceCardPath,
|
||||
} from "../editor/lovelace-path";
|
||||
import { showSelectViewDialog } from "../editor/select-view/show-select-view-dialog";
|
||||
import { Lovelace, LovelaceCard } from "../types";
|
||||
|
||||
|
@ -54,7 +58,7 @@ export class HuiCardOptions extends LitElement {
|
|||
|
||||
@property({ attribute: false }) public lovelace?: Lovelace;
|
||||
|
||||
@property({ type: Array }) public path?: [number, number];
|
||||
@property({ type: Array }) public path?: LovelaceCardPath;
|
||||
|
||||
@queryAssignedNodes() private _assignedNodes?: NodeListOf<LovelaceCard>;
|
||||
|
||||
|
@ -76,17 +80,21 @@ export class HuiCardOptions extends LitElement {
|
|||
if (!changedProps.has("path") || !this.path) {
|
||||
return;
|
||||
}
|
||||
const { viewIndex } = parseLovelaceCardPath(this.path);
|
||||
this.classList.toggle(
|
||||
"panel",
|
||||
this.lovelace!.config.views[this.path![0]].panel
|
||||
this.lovelace!.config.views[viewIndex].panel
|
||||
);
|
||||
}
|
||||
|
||||
private get _currentView() {
|
||||
return this.lovelace!.config.views[this.path![0]] as LovelaceViewConfig;
|
||||
private get _cards() {
|
||||
const containerPath = getLovelaceContainerPath(this.path!);
|
||||
return findLovelaceCards(this.lovelace!.config, containerPath)!;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const { cardIndex } = parseLovelaceCardPath(this.path!);
|
||||
|
||||
return html`
|
||||
<div class="card"><slot></slot></div>
|
||||
<ha-card>
|
||||
|
@ -107,7 +115,7 @@ export class HuiCardOptions extends LitElement {
|
|||
.path=${mdiMinus}
|
||||
class="move-arrow"
|
||||
@click=${this._decreaseCardPosiion}
|
||||
?disabled=${this.path![1] === 0}
|
||||
?disabled=${cardIndex === 0}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
@click=${this._changeCardPosition}
|
||||
|
@ -115,7 +123,7 @@ export class HuiCardOptions extends LitElement {
|
|||
"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
|
||||
.label=${this.hass!.localize(
|
||||
|
@ -124,8 +132,7 @@ export class HuiCardOptions extends LitElement {
|
|||
.path=${mdiPlus}
|
||||
class="move-arrow"
|
||||
@click=${this._increaseCardPosition}
|
||||
.disabled=${this._currentView.cards!.length ===
|
||||
this.path![1] + 1}
|
||||
.disabled=${this._cards!.length === cardIndex + 1}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
|
@ -271,13 +278,14 @@ export class HuiCardOptions extends LitElement {
|
|||
}
|
||||
|
||||
private _duplicateCard(): void {
|
||||
const path = this.path!;
|
||||
const cardConfig = this._currentView.cards![path[1]];
|
||||
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: [path[0], null],
|
||||
newCardConfig: cardConfig,
|
||||
path: containerPath,
|
||||
cardConfig,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -291,30 +299,29 @@ export class HuiCardOptions extends LitElement {
|
|||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private _decreaseCardPosiion(): void {
|
||||
const lovelace = this.lovelace!;
|
||||
const path = this.path!;
|
||||
lovelace.saveConfig(
|
||||
swapCard(lovelace.config, path, [path[0], path[1] - 1])
|
||||
);
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
lovelace.saveConfig(moveCardToIndex(lovelace.config, path, cardIndex - 1));
|
||||
}
|
||||
|
||||
private _increaseCardPosition(): void {
|
||||
const lovelace = this.lovelace!;
|
||||
const path = this.path!;
|
||||
lovelace.saveConfig(
|
||||
swapCard(lovelace.config, path, [path[0], path[1] + 1])
|
||||
);
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
lovelace.saveConfig(moveCardToIndex(lovelace.config, path, cardIndex + 1));
|
||||
}
|
||||
|
||||
private async _changeCardPosition(): Promise<void> {
|
||||
const lovelace = this.lovelace!;
|
||||
const path = this.path!;
|
||||
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const positionString = await showPromptDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.change_position.title"
|
||||
|
@ -324,7 +331,7 @@ export class HuiCardOptions extends LitElement {
|
|||
),
|
||||
inputType: "number",
|
||||
inputMin: "1",
|
||||
placeholder: String(path[1] + 1),
|
||||
placeholder: String(cardIndex + 1),
|
||||
});
|
||||
|
||||
if (!positionString) return;
|
||||
|
@ -333,7 +340,8 @@ export class HuiCardOptions extends LitElement {
|
|||
|
||||
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 {
|
||||
|
@ -345,20 +353,17 @@ export class HuiCardOptions extends LitElement {
|
|||
viewSelectedCallback: async (urlPath, selectedDashConfig, viewIndex) => {
|
||||
if (urlPath === this.lovelace!.urlPath) {
|
||||
this.lovelace!.saveConfig(
|
||||
moveCard(this.lovelace!.config, this.path!, [viewIndex])
|
||||
moveCardToContainer(this.lovelace!.config, this.path!, [viewIndex])
|
||||
);
|
||||
showSaveSuccessToast(this, this.hass!);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { cardIndex } = parseLovelaceCardPath(this.path!);
|
||||
await saveConfig(
|
||||
this.hass!,
|
||||
urlPath,
|
||||
addCard(
|
||||
selectedDashConfig,
|
||||
[viewIndex],
|
||||
this._currentView.cards![this.path![1]]
|
||||
)
|
||||
addCard(selectedDashConfig, [viewIndex], this._cards[cardIndex])
|
||||
);
|
||||
this.lovelace!.saveConfig(
|
||||
deleteCard(this.lovelace!.config, this.path!)
|
||||
|
|
|
@ -24,6 +24,7 @@ const ALWAYS_LOADED_TYPES = new Set([
|
|||
"entity-button",
|
||||
"glance",
|
||||
"grid",
|
||||
"section",
|
||||
"light",
|
||||
"sensor",
|
||||
"thermostat",
|
||||
|
|
|
@ -1,27 +1,31 @@
|
|||
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 { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import {
|
||||
isCustomType,
|
||||
stripCustomPrefix,
|
||||
} from "../../../data/lovelace_custom_cards";
|
||||
import { LovelaceCardFeatureConfig } from "../card-features/types";
|
||||
import type { HuiErrorCard } from "../cards/hui-error-card";
|
||||
import type { ErrorCardConfig } from "../cards/types";
|
||||
import { LovelaceElement, LovelaceElementConfig } from "../elements/types";
|
||||
import { LovelaceRow, LovelaceRowConfig } from "../entity-rows/types";
|
||||
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||
import { LovelaceCardFeatureConfig } from "../card-features/types";
|
||||
import {
|
||||
LovelaceBadge,
|
||||
LovelaceCard,
|
||||
LovelaceCardConstructor,
|
||||
LovelaceCardFeature,
|
||||
LovelaceCardFeatureConstructor,
|
||||
LovelaceHeaderFooter,
|
||||
LovelaceHeaderFooterConstructor,
|
||||
LovelaceRowConstructor,
|
||||
LovelaceCardFeature,
|
||||
LovelaceCardFeatureConstructor,
|
||||
} from "../types";
|
||||
|
||||
const TIMEOUT = 2000;
|
||||
|
@ -62,6 +66,11 @@ interface CreateElementConfigTypes {
|
|||
element: LovelaceCardFeature;
|
||||
constructor: LovelaceCardFeatureConstructor;
|
||||
};
|
||||
section: {
|
||||
config: LovelaceSectionConfig;
|
||||
element: LovelaceSectionElement;
|
||||
constructor: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
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 = {
|
||||
panel: () => import("../views/hui-panel-view"),
|
||||
sidebar: () => import("../views/hui-sidebar-view"),
|
||||
sections: () => import("../views/hui-sections-view"),
|
||||
};
|
||||
|
||||
export const createViewElement = (
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { LovelacePanelConfig } from "../../../data/lovelace";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import {
|
||||
LovelaceConfig,
|
||||
fetchConfig,
|
||||
|
@ -15,6 +16,7 @@ export const addEntitiesToLovelaceView = async (
|
|||
element: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
cardConfig: LovelaceCardConfig[],
|
||||
sectionConfig?: LovelaceSectionConfig,
|
||||
entities?: string[]
|
||||
) => {
|
||||
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
|
||||
showSuggestCardDialog(element, {
|
||||
cardConfig,
|
||||
sectionConfig,
|
||||
entities,
|
||||
yaml: true,
|
||||
});
|
||||
|
@ -93,6 +96,7 @@ export const addEntitiesToLovelaceView = async (
|
|||
if (!storageDashs.length && lovelaceConfig.views.length === 1) {
|
||||
showSuggestCardDialog(element, {
|
||||
cardConfig,
|
||||
sectionConfig,
|
||||
lovelaceConfig: lovelaceConfig!,
|
||||
saveConfig: async (newConfig: LovelaceConfig): Promise<void> => {
|
||||
try {
|
||||
|
@ -116,6 +120,7 @@ export const addEntitiesToLovelaceView = async (
|
|||
viewSelectedCallback: (newUrlPath, selectedDashConfig, viewIndex) => {
|
||||
showSuggestCardDialog(element, {
|
||||
cardConfig,
|
||||
sectionConfig,
|
||||
lovelaceConfig: selectedDashConfig,
|
||||
saveConfig: async (newConfig: LovelaceConfig): Promise<void> => {
|
||||
try {
|
||||
|
|
|
@ -15,6 +15,7 @@ import { until } from "lit/directives/until";
|
|||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import "../../../../components/ha-circular-progress";
|
||||
import "../../../../components/search-input";
|
||||
import { isUnavailableState } from "../../../../data/entity";
|
||||
|
@ -46,6 +47,8 @@ interface CardElement {
|
|||
export class HuiCardPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public suggestedCards?: string[];
|
||||
|
||||
@storage({
|
||||
key: "lovelaceClipboard",
|
||||
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() {
|
||||
if (
|
||||
!this.hass ||
|
||||
|
@ -102,6 +128,10 @@ export class HuiCardPicker extends LitElement {
|
|||
return nothing;
|
||||
}
|
||||
|
||||
const suggestedCards = this._suggestedCards(this._cards);
|
||||
const othersCards = this._otherCards(this._cards);
|
||||
const customCardsItems = this._customCards(this._cards);
|
||||
|
||||
return html`
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
|
@ -119,39 +149,49 @@ export class HuiCardPicker extends LitElement {
|
|||
})}
|
||||
>
|
||||
<div class="cards-container">
|
||||
${this._clipboard && !this._filter
|
||||
? 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>
|
||||
`
|
||||
${this._filter
|
||||
? this._filterCards(this._cards, this._filter).map(
|
||||
(cardElement: CardElement) => cardElement.element
|
||||
)
|
||||
: html`
|
||||
${suggestedCards.length > 0
|
||||
? html`
|
||||
<div class="cards-container-header">
|
||||
${this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.suggested_cards`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${this._renderClipboardCard()}
|
||||
${suggestedCards.map(
|
||||
(cardElement: CardElement) => cardElement.element
|
||||
)}
|
||||
`
|
||||
: nothing}
|
||||
${this._filterCards(this._cards, this._filter).map(
|
||||
(cardElement: CardElement) => cardElement.element
|
||||
)}
|
||||
${suggestedCards.length > 0
|
||||
? html`
|
||||
<div class="cards-container-header">
|
||||
${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 class="cards-container">
|
||||
<div
|
||||
|
@ -218,8 +258,24 @@ export class HuiCardPicker extends LitElement {
|
|||
description: this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.${card.type}.description`
|
||||
),
|
||||
isSuggested: this.suggestedCards?.includes(card.type) || false,
|
||||
...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) {
|
||||
cards = cards.concat(
|
||||
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) {
|
||||
const value = ev.detail.value;
|
||||
|
||||
|
@ -381,6 +468,14 @@ export class HuiCardPicker extends LitElement {
|
|||
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 {
|
||||
display: grid;
|
||||
grid-gap: 8px 8px;
|
||||
|
@ -455,6 +550,23 @@ export class HuiCardPicker extends LitElement {
|
|||
.manual {
|
||||
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 "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-header";
|
||||
import {
|
||||
isStrategySection,
|
||||
LovelaceSectionConfig,
|
||||
} from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
computeCards,
|
||||
computeSection,
|
||||
} from "../../common/generate-lovelace-config";
|
||||
import "./hui-card-picker";
|
||||
import "./hui-entity-picker-table";
|
||||
import { CreateCardDialogParams } from "./show-create-card-dialog";
|
||||
import { showEditCardDialog } from "./show-edit-card-dialog";
|
||||
import { showSuggestCardDialog } from "./show-suggest-card-dialog";
|
||||
import { computeCards } from "../../common/generate-lovelace-config";
|
||||
import {
|
||||
findLovelaceContainer,
|
||||
parseLovelaceContainerPath,
|
||||
} from "../lovelace-path";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
|
@ -42,7 +53,9 @@ export class HuiCreateDialogCard
|
|||
|
||||
@state() private _params?: CreateCardDialogParams;
|
||||
|
||||
@state() private _viewConfig!: LovelaceViewConfig;
|
||||
@state() private _containerConfig!:
|
||||
| LovelaceViewConfig
|
||||
| LovelaceSectionConfig;
|
||||
|
||||
@state() private _selectedEntities: string[] = [];
|
||||
|
||||
|
@ -50,8 +63,17 @@ export class HuiCreateDialogCard
|
|||
|
||||
public async showDialog(params: CreateCardDialogParams): Promise<void> {
|
||||
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 {
|
||||
|
@ -67,10 +89,10 @@ export class HuiCreateDialogCard
|
|||
return nothing;
|
||||
}
|
||||
|
||||
const title = this._viewConfig.title
|
||||
const title = this._containerConfig.title
|
||||
? this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.pick_card_view_title",
|
||||
{ name: `"${this._viewConfig.title}"` }
|
||||
"ui.panel.lovelace.editor.edit_card.pick_card_title",
|
||||
{ name: `"${this._containerConfig.title}"` }
|
||||
)
|
||||
: this.hass!.localize("ui.panel.lovelace.editor.edit_card.pick_card");
|
||||
|
||||
|
@ -112,6 +134,7 @@ export class HuiCreateDialogCard
|
|||
this._currTabIndex === 0
|
||||
? html`
|
||||
<hui-card-picker
|
||||
.suggestedCards=${this._params.suggestedCards}
|
||||
.lovelace=${this._params.lovelaceConfig}
|
||||
.hass=${this.hass}
|
||||
@config-changed=${this._handleCardPicked}
|
||||
|
@ -214,8 +237,8 @@ export class HuiCreateDialogCard
|
|||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this._params!.lovelaceConfig,
|
||||
saveConfig: this._params!.saveConfig,
|
||||
path: [this._params!.path[0], null],
|
||||
newCardConfig: config,
|
||||
path: this._params!.path,
|
||||
cardConfig: config,
|
||||
});
|
||||
|
||||
this.closeDialog();
|
||||
|
@ -248,12 +271,36 @@ export class HuiCreateDialogCard
|
|||
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, {
|
||||
lovelaceConfig: this._params!.lovelaceConfig,
|
||||
saveConfig: this._params!.saveConfig,
|
||||
path: this._params!.path as [number],
|
||||
entities: this._selectedEntities,
|
||||
cardConfig,
|
||||
sectionConfig,
|
||||
});
|
||||
|
||||
this.closeDialog();
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { mdiClose, mdiHelpCircle } from "@mdi/js";
|
||||
import deepFreeze from "deep-freeze";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
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-header";
|
||||
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 type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
|
@ -24,18 +32,12 @@ import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
|||
import { addCard, replaceCard } from "../config-util";
|
||||
import { getCardDocumentationURL } from "../get-card-documentation-url";
|
||||
import type { ConfigChangedEvent } from "../hui-element-editor";
|
||||
import { findLovelaceContainer } from "../lovelace-path";
|
||||
import type { GUIModeChangedEvent } from "../types";
|
||||
import "./hui-card-element-editor";
|
||||
import type { HuiCardElementEditor } from "./hui-card-element-editor";
|
||||
import "./hui-card-preview";
|
||||
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 {
|
||||
// for fire event
|
||||
|
@ -61,7 +63,9 @@ export class HuiDialogEditCard
|
|||
|
||||
@state() private _cardConfig?: LovelaceCardConfig;
|
||||
|
||||
@state() private _viewConfig!: LovelaceViewConfig;
|
||||
@state() private _containerConfig!:
|
||||
| LovelaceViewConfig
|
||||
| LovelaceSectionConfig;
|
||||
|
||||
@state() private _saving = false;
|
||||
|
||||
|
@ -84,18 +88,29 @@ export class HuiDialogEditCard
|
|||
this._params = params;
|
||||
this._GUImode = true;
|
||||
this._guiModeAvailable = true;
|
||||
const [view, card] = params.path;
|
||||
this._viewConfig = params.lovelaceConfig.views[view];
|
||||
this._cardConfig =
|
||||
params.newCardConfig ??
|
||||
(card !== null ? this._viewConfig.cards![card] : undefined);
|
||||
|
||||
const containerConfig = findLovelaceContainer(
|
||||
params.lovelaceConfig,
|
||||
params.path
|
||||
);
|
||||
|
||||
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;
|
||||
if (this._cardConfig && !Object.isFrozen(this._cardConfig)) {
|
||||
this._cardConfig = deepFreeze(this._cardConfig);
|
||||
}
|
||||
if (params.newCardConfig) {
|
||||
this._dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
public closeDialog(): boolean {
|
||||
|
@ -171,10 +186,10 @@ export class HuiDialogEditCard
|
|||
{ type: cardName }
|
||||
);
|
||||
} else if (!this._cardConfig) {
|
||||
heading = this._viewConfig.title
|
||||
heading = this._containerConfig.title
|
||||
? this.hass!.localize(
|
||||
"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");
|
||||
} else {
|
||||
|
@ -369,13 +384,13 @@ export class HuiDialogEditCard
|
|||
return;
|
||||
}
|
||||
this._saving = true;
|
||||
const [view, card] = this._params!.path;
|
||||
const path = this._params!.path;
|
||||
await this._params!.saveConfig(
|
||||
card === null
|
||||
? addCard(this._params!.lovelaceConfig, [view], this._cardConfig!)
|
||||
"cardConfig" in this._params!
|
||||
? addCard(this._params!.lovelaceConfig, path, this._cardConfig!)
|
||||
: replaceCard(
|
||||
this._params!.lovelaceConfig,
|
||||
[view, card],
|
||||
[...path, this._params!.cardIndex],
|
||||
this._cardConfig!
|
||||
)
|
||||
);
|
||||
|
|
|
@ -5,13 +5,20 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
|||
import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import { isStrategyView } from "../../../../data/lovelace/config/view";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
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 { showCreateCardDialog } from "./show-create-card-dialog";
|
||||
import { SuggestCardDialogParams } from "./show-suggest-card-dialog";
|
||||
import { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
|
||||
@customElement("hui-dialog-suggest-card")
|
||||
export class HuiDialogSuggestCard extends LitElement {
|
||||
|
@ -21,6 +28,8 @@ export class HuiDialogSuggestCard extends LitElement {
|
|||
|
||||
@state() private _cardConfig?: LovelaceCardConfig[];
|
||||
|
||||
@state() private _sectionConfig?: LovelaceSectionConfig;
|
||||
|
||||
@state() private _saving = false;
|
||||
|
||||
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||
|
@ -28,9 +37,13 @@ export class HuiDialogSuggestCard extends LitElement {
|
|||
public showDialog(params: SuggestCardDialogParams): void {
|
||||
this._params = params;
|
||||
this._cardConfig = params.cardConfig;
|
||||
this._sectionConfig = params.sectionConfig;
|
||||
if (!Object.isFrozen(this._cardConfig)) {
|
||||
this._cardConfig = deepFreeze(this._cardConfig);
|
||||
}
|
||||
if (!Object.isFrozen(this._sectionConfig)) {
|
||||
this._sectionConfig = deepFreeze(this._sectionConfig);
|
||||
}
|
||||
if (this._yamlEditor) {
|
||||
this._yamlEditor.setValue(this._cardConfig);
|
||||
}
|
||||
|
@ -42,6 +55,45 @@ export class HuiDialogSuggestCard extends LitElement {
|
|||
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() {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
|
@ -56,20 +108,7 @@ export class HuiDialogSuggestCard extends LitElement {
|
|||
)}
|
||||
>
|
||||
<div>
|
||||
${this._cardConfig
|
||||
? html`
|
||||
<div class="element-preview">
|
||||
${this._cardConfig.map(
|
||||
(cardConfig) => html`
|
||||
<hui-card-preview
|
||||
.hass=${this.hass}
|
||||
.config=${cardConfig}
|
||||
></hui-card-preview>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this._renderPreview()}
|
||||
${this._params.yaml && this._cardConfig
|
||||
? html`
|
||||
<div class="editor">
|
||||
|
@ -79,7 +118,7 @@ export class HuiDialogSuggestCard extends LitElement {
|
|||
></ha-yaml-editor>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
|
@ -146,7 +185,8 @@ export class HuiDialogSuggestCard extends LitElement {
|
|||
.element-preview {
|
||||
position: relative;
|
||||
}
|
||||
hui-card-preview {
|
||||
hui-card-preview,
|
||||
hui-section {
|
||||
padding-top: 8px;
|
||||
margin: 4px auto;
|
||||
max-width: 390px;
|
||||
|
@ -178,6 +218,32 @@ export class HuiDialogSuggestCard extends LitElement {
|
|||
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> {
|
||||
if (
|
||||
!this._params?.lovelaceConfig ||
|
||||
|
@ -188,13 +254,12 @@ export class HuiDialogSuggestCard extends LitElement {
|
|||
return;
|
||||
}
|
||||
this._saving = true;
|
||||
await this._params!.saveConfig(
|
||||
addCards(
|
||||
this._params!.lovelaceConfig,
|
||||
this._params!.path as [number],
|
||||
this._cardConfig
|
||||
)
|
||||
|
||||
const newConfig = this._computeNewConfig(
|
||||
this._params.lovelaceConfig,
|
||||
this._params.path
|
||||
);
|
||||
await this._params!.saveConfig(newConfig);
|
||||
this._saving = false;
|
||||
showSaveSuccessToast(this, this.hass);
|
||||
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 type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import { LovelaceContainerPath } from "../lovelace-path";
|
||||
|
||||
export interface CreateCardDialogParams {
|
||||
lovelaceConfig: LovelaceConfig;
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import { LovelaceContainerPath } from "../lovelace-path";
|
||||
|
||||
export interface EditCardDialogParams {
|
||||
export type EditCardDialogParams = {
|
||||
lovelaceConfig: LovelaceConfig;
|
||||
saveConfig: (config: LovelaceConfig) => void;
|
||||
path: [number, number | null];
|
||||
// If specified, the card will be replaced with the new card.
|
||||
newCardConfig?: LovelaceCardConfig;
|
||||
}
|
||||
path: LovelaceContainerPath;
|
||||
} & (
|
||||
| {
|
||||
cardIndex: number;
|
||||
}
|
||||
| {
|
||||
cardConfig: LovelaceCardConfig;
|
||||
}
|
||||
);
|
||||
|
||||
export const importEditCardDialog = () => import("./hui-dialog-edit-card");
|
||||
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import { LovelaceContainerPath } from "../lovelace-path";
|
||||
|
||||
export interface SuggestCardDialogParams {
|
||||
lovelaceConfig?: LovelaceConfig;
|
||||
yaml?: boolean;
|
||||
saveConfig?: (config: LovelaceConfig) => void;
|
||||
path?: [number];
|
||||
path?: LovelaceContainerPath;
|
||||
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");
|
||||
|
|
|
@ -1,296 +1,160 @@
|
|||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section";
|
||||
import { LovelaceConfig } from "../../../data/lovelace/config/types";
|
||||
import {
|
||||
LovelaceViewConfig,
|
||||
isStrategyView,
|
||||
} from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import {
|
||||
LovelaceCardPath,
|
||||
LovelaceContainerPath,
|
||||
findLovelaceCards,
|
||||
findLovelaceContainer,
|
||||
getLovelaceContainerPath,
|
||||
parseLovelaceCardPath,
|
||||
parseLovelaceContainerPath,
|
||||
updateLovelaceCards,
|
||||
updateLovelaceContainer,
|
||||
} from "./lovelace-path";
|
||||
|
||||
export const addCard = (
|
||||
config: LovelaceConfig,
|
||||
path: [number],
|
||||
path: LovelaceContainerPath,
|
||||
cardConfig: LovelaceCardConfig
|
||||
): LovelaceConfig => {
|
||||
const [viewIndex] = path;
|
||||
const views: LovelaceViewConfig[] = [];
|
||||
|
||||
config.views.forEach((viewConf, index) => {
|
||||
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,
|
||||
};
|
||||
const cards = findLovelaceCards(config, path);
|
||||
const newCards = cards ? [...cards, cardConfig] : [cardConfig];
|
||||
const newConfig = updateLovelaceCards(config, path, newCards);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
export const addCards = (
|
||||
config: LovelaceConfig,
|
||||
path: [number],
|
||||
path: LovelaceContainerPath,
|
||||
cardConfigs: LovelaceCardConfig[]
|
||||
): LovelaceConfig => {
|
||||
const [viewIndex] = path;
|
||||
const views: LovelaceViewConfig[] = [];
|
||||
|
||||
config.views.forEach((viewConf, index) => {
|
||||
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,
|
||||
};
|
||||
const cards = findLovelaceCards(config, path);
|
||||
const newCards = cards ? [...cards, ...cardConfigs] : [...cardConfigs];
|
||||
const newConfig = updateLovelaceCards(config, path, newCards);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
export const replaceCard = (
|
||||
config: LovelaceConfig,
|
||||
path: [number, number],
|
||||
path: LovelaceCardPath,
|
||||
cardConfig: LovelaceCardConfig
|
||||
): LovelaceConfig => {
|
||||
const [viewIndex, cardIndex] = path;
|
||||
const views: LovelaceViewConfig[] = [];
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const containerPath = getLovelaceContainerPath(path);
|
||||
|
||||
config.views.forEach((viewConf, index) => {
|
||||
if (index !== viewIndex) {
|
||||
views.push(config.views[index]);
|
||||
return;
|
||||
}
|
||||
const cards = findLovelaceCards(config, containerPath);
|
||||
|
||||
if (isStrategyView(viewConf)) {
|
||||
throw new Error("You cannot replace a card in a strategy view.");
|
||||
}
|
||||
const newCards = (cards ?? []).map((origConf, ind) =>
|
||||
ind === cardIndex ? cardConfig : origConf
|
||||
);
|
||||
|
||||
views.push({
|
||||
...viewConf,
|
||||
cards: (viewConf.cards || []).map((origConf, ind) =>
|
||||
ind === cardIndex ? cardConfig : origConf
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
...config,
|
||||
views,
|
||||
};
|
||||
const newConfig = updateLovelaceCards(config, containerPath, newCards);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
export const deleteCard = (
|
||||
config: LovelaceConfig,
|
||||
path: [number, number]
|
||||
path: LovelaceCardPath
|
||||
): LovelaceConfig => {
|
||||
const [viewIndex, cardIndex] = path;
|
||||
const views: LovelaceViewConfig[] = [];
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const containerPath = getLovelaceContainerPath(path);
|
||||
|
||||
config.views.forEach((viewConf, index) => {
|
||||
if (index !== viewIndex) {
|
||||
views.push(config.views[index]);
|
||||
return;
|
||||
}
|
||||
const cards = findLovelaceCards(config, containerPath);
|
||||
|
||||
if (isStrategyView(viewConf)) {
|
||||
throw new Error("You cannot delete a card in a strategy view.");
|
||||
}
|
||||
const newCards = (cards ?? []).filter((_origConf, ind) => ind !== cardIndex);
|
||||
|
||||
views.push({
|
||||
...viewConf,
|
||||
cards: (viewConf.cards || []).filter(
|
||||
(_origConf, ind) => ind !== cardIndex
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
...config,
|
||||
views,
|
||||
};
|
||||
const newConfig = updateLovelaceCards(config, containerPath, newCards);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
export const insertCard = (
|
||||
config: LovelaceConfig,
|
||||
path: [number, number],
|
||||
path: LovelaceCardPath,
|
||||
cardConfig: LovelaceCardConfig
|
||||
) => {
|
||||
const [viewIndex, cardIndex] = path;
|
||||
const views: LovelaceViewConfig[] = [];
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const containerPath = getLovelaceContainerPath(path);
|
||||
|
||||
config.views.forEach((viewConf, index) => {
|
||||
if (index !== viewIndex) {
|
||||
views.push(config.views[index]);
|
||||
return;
|
||||
}
|
||||
const cards = findLovelaceCards(config, containerPath);
|
||||
|
||||
if (isStrategyView(viewConf)) {
|
||||
throw new Error("You cannot insert a card in a strategy view.");
|
||||
}
|
||||
const newCards = cards
|
||||
? [...cards.slice(0, cardIndex), cardConfig, ...cards.slice(cardIndex)]
|
||||
: [cardConfig];
|
||||
|
||||
const cards = viewConf.cards
|
||||
? [
|
||||
...viewConf.cards.slice(0, cardIndex),
|
||||
cardConfig,
|
||||
...viewConf.cards.slice(cardIndex),
|
||||
]
|
||||
: [cardConfig];
|
||||
|
||||
views.push({
|
||||
...viewConf,
|
||||
cards,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
...config,
|
||||
views,
|
||||
};
|
||||
const newConfig = updateLovelaceCards(config, containerPath, newCards);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
export const swapCard = (
|
||||
export const moveCardToIndex = (
|
||||
config: LovelaceConfig,
|
||||
path1: [number, number],
|
||||
path2: [number, number]
|
||||
path: LovelaceCardPath,
|
||||
index: number
|
||||
): LovelaceConfig => {
|
||||
const origView1 = config.views[path1[0]];
|
||||
const origView2 = config.views[path2[0]];
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
const containerPath = getLovelaceContainerPath(path);
|
||||
|
||||
if (isStrategyView(origView1) || isStrategyView(origView2)) {
|
||||
throw new Error("You cannot move swap cards in a strategy view.");
|
||||
}
|
||||
const cards = findLovelaceCards(config, containerPath);
|
||||
|
||||
const card1 = origView1.cards![path1[1]];
|
||||
const card2 = origView2.cards![path2[1]];
|
||||
const newCards = cards ? [...cards] : [];
|
||||
|
||||
const newView1 = {
|
||||
...origView1,
|
||||
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 oldIndex = cardIndex;
|
||||
const newIndex = Math.max(Math.min(index, newCards.length - 1), 0);
|
||||
|
||||
const card = newCards[oldIndex];
|
||||
newCards.splice(oldIndex, 1);
|
||||
newCards.splice(newIndex, 0, card);
|
||||
|
||||
const newView = {
|
||||
...view,
|
||||
cards: newCards,
|
||||
};
|
||||
const newConfig = updateLovelaceCards(config, containerPath, newCards);
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
return {
|
||||
...config,
|
||||
views: config.views.map((origView, index) =>
|
||||
index === path[0] ? newView : origView
|
||||
),
|
||||
};
|
||||
export const moveCardToContainer = (
|
||||
config: LovelaceConfig,
|
||||
fromPath: LovelaceCardPath,
|
||||
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 = (
|
||||
config: LovelaceConfig,
|
||||
fromPath: [number, number],
|
||||
toPath: [number]
|
||||
fromPath: LovelaceCardPath,
|
||||
toPath: LovelaceCardPath
|
||||
): LovelaceConfig => {
|
||||
if (fromPath[0] === toPath[0]) {
|
||||
throw new Error("You cannot move a card to the view it is in.");
|
||||
}
|
||||
const fromView = config.views[fromPath[0]];
|
||||
const toView = config.views[toPath[0]];
|
||||
const { cardIndex: fromCardIndex } = parseLovelaceCardPath(fromPath);
|
||||
const fromContainerPath = getLovelaceContainerPath(fromPath);
|
||||
const cards = findLovelaceCards(config, fromContainerPath);
|
||||
const card = cards![fromCardIndex];
|
||||
|
||||
if (isStrategyView(fromView)) {
|
||||
throw new Error("You cannot move a card from a strategy view.");
|
||||
}
|
||||
let newConfig = deleteCard(config, fromPath);
|
||||
newConfig = insertCard(newConfig, toPath, card);
|
||||
|
||||
if (isStrategyView(toView)) {
|
||||
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
|
||||
),
|
||||
};
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
export const addView = (
|
||||
|
@ -356,3 +220,84 @@ export const deleteView = (
|
|||
...config,
|
||||
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 { HomeAssistant } from "../../../types";
|
||||
import { showDeleteSuccessToast } from "../../../util/toast-deleted-success";
|
||||
import { Lovelace } from "../types";
|
||||
import { showDeleteCardDialog } from "./card-editor/show-delete-card-dialog";
|
||||
import { deleteCard, insertCard } from "./config-util";
|
||||
import {
|
||||
LovelaceCardPath,
|
||||
findLovelaceContainer,
|
||||
getLovelaceContainerPath,
|
||||
parseLovelaceCardPath,
|
||||
} from "./lovelace-path";
|
||||
|
||||
export async function confDeleteCard(
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
lovelace: Lovelace,
|
||||
path: [number, number]
|
||||
path: LovelaceCardPath
|
||||
): Promise<void> {
|
||||
const view = lovelace.config.views[path[0]];
|
||||
if (isStrategyView(view)) {
|
||||
throw new Error("Deleting cards in a strategy view is not supported.");
|
||||
const containerPath = getLovelaceContainerPath(path);
|
||||
const { cardIndex } = parseLovelaceCardPath(path);
|
||||
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, {
|
||||
cardConfig,
|
||||
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;
|
||||
showElement?: boolean;
|
||||
isCustom?: boolean;
|
||||
isSuggested?: boolean;
|
||||
}
|
||||
|
||||
export interface HeaderFooter {
|
||||
|
|
|
@ -21,7 +21,10 @@ import "../card-editor/hui-entity-picker-table";
|
|||
import { showSuggestCardDialog } from "../card-editor/show-suggest-card-dialog";
|
||||
import { showSelectViewDialog } from "../select-view/show-select-view-dialog";
|
||||
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")
|
||||
export class HuiUnusedEntities extends LitElement {
|
||||
|
@ -132,6 +135,8 @@ export class HuiUnusedEntities extends LitElement {
|
|||
this._selectedEntities,
|
||||
{}
|
||||
);
|
||||
const sectionConfig = computeSection(this._selectedEntities, {});
|
||||
|
||||
if (this.lovelace.config.views.length === 1) {
|
||||
showSuggestCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config!,
|
||||
|
@ -139,6 +144,7 @@ export class HuiUnusedEntities extends LitElement {
|
|||
path: [0],
|
||||
entities: this._selectedEntities,
|
||||
cardConfig,
|
||||
sectionConfig,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -152,6 +158,7 @@ export class HuiUnusedEntities extends LitElement {
|
|||
path: [viewIndex],
|
||||
entities: this._selectedEntities,
|
||||
cardConfig,
|
||||
sectionConfig,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -6,13 +6,14 @@ import { slugify } from "../../../../common/string/slugify";
|
|||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
DEFAULT_VIEW_LAYOUT,
|
||||
SECTION_VIEW_LAYOUT,
|
||||
PANEL_VIEW_LAYOUT,
|
||||
SIDEBAR_VIEW_LAYOUT,
|
||||
} from "../../views/const";
|
||||
import { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
|
@ -53,6 +54,7 @@ export class HuiViewEditor extends LitElement {
|
|||
DEFAULT_VIEW_LAYOUT,
|
||||
SIDEBAR_VIEW_LAYOUT,
|
||||
PANEL_VIEW_LAYOUT,
|
||||
SECTION_VIEW_LAYOUT,
|
||||
] as const
|
||||
).map((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 {
|
||||
LovelaceDashboardStrategy,
|
||||
LovelaceSectionStrategy,
|
||||
LovelaceStrategy,
|
||||
LovelaceViewStrategy,
|
||||
} from "./types";
|
||||
|
@ -27,13 +28,15 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
|
|||
"original-states": () => import("./original-states-view-strategy"),
|
||||
energy: () => import("../../energy/strategies/energy-view-strategy"),
|
||||
},
|
||||
section: {},
|
||||
};
|
||||
|
||||
export type LovelaceStrategyConfigType = "dashboard" | "view";
|
||||
export type LovelaceStrategyConfigType = "dashboard" | "view" | "section";
|
||||
|
||||
type Strategies = {
|
||||
dashboard: LovelaceDashboardStrategy;
|
||||
view: LovelaceViewStrategy;
|
||||
section: LovelaceSectionStrategy;
|
||||
};
|
||||
|
||||
type StrategyConfig<T extends LovelaceStrategyConfigType> = AsyncReturnType<
|
||||
|
@ -163,6 +166,24 @@ export const generateLovelaceViewStrategy = async (
|
|||
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
|
||||
*/
|
||||
|
@ -175,11 +196,24 @@ export const expandLovelaceConfigStrategies = async (
|
|||
: { ...config };
|
||||
|
||||
newConfig.views = await Promise.all(
|
||||
newConfig.views.map((view) =>
|
||||
isStrategyView(view)
|
||||
? generateLovelaceViewStrategy(view.strategy, hass)
|
||||
: view
|
||||
)
|
||||
newConfig.views.map(async (view) => {
|
||||
const newView = isStrategyView(view)
|
||||
? await generateLovelaceViewStrategy(view.strategy, hass)
|
||||
: { ...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;
|
||||
|
|
|
@ -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 { LovelaceConfig } from "../../../data/lovelace/config/types";
|
||||
import { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceGenericElementEditor } from "../types";
|
||||
|
@ -15,6 +16,9 @@ export interface LovelaceDashboardStrategy
|
|||
export interface LovelaceViewStrategy
|
||||
extends LovelaceStrategy<LovelaceViewConfig> {}
|
||||
|
||||
export interface LovelaceSectionStrategy
|
||||
extends LovelaceStrategy<LovelaceSectionConfig> {}
|
||||
|
||||
export interface LovelaceStrategyEditor extends LovelaceGenericElementEditor {
|
||||
setConfig(config: LovelaceStrategyConfig): void;
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ export interface LovelaceCard extends HTMLElement {
|
|||
isPanel?: boolean;
|
||||
editMode?: boolean;
|
||||
getCardSize(): number | Promise<number>;
|
||||
getGridSize?(): [number, number];
|
||||
setConfig(config: LovelaceCardConfig): void;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
export const DEFAULT_VIEW_LAYOUT = "masonry";
|
||||
export const PANEL_VIEW_LAYOUT = "panel";
|
||||
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 { customElement, property, state } from "lit/decorators";
|
||||
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/ha-svg-icon";
|
||||
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 {
|
||||
createErrorBadgeConfig,
|
||||
|
@ -20,25 +28,25 @@ import {
|
|||
import { createViewElement } from "../create-element/create-view-element";
|
||||
import { showCreateCardDialog } from "../editor/card-editor/show-create-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 { 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 type { Lovelace, LovelaceBadge, LovelaceCard } from "../types";
|
||||
import { PANEL_VIEW_LAYOUT, DEFAULT_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";
|
||||
import { DEFAULT_VIEW_LAYOUT, PANEL_VIEW_LAYOUT } from "./const";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"ll-create-card": undefined;
|
||||
"ll-edit-card": { path: [number, number] };
|
||||
"ll-delete-card": { path: [number, number]; confirm: boolean };
|
||||
"ll-create-card": { suggested?: string[] } | undefined;
|
||||
"ll-edit-card": { path: LovelaceCardPath };
|
||||
"ll-delete-card": { path: LovelaceCardPath; confirm: boolean };
|
||||
}
|
||||
interface HTMLElementEventMap {
|
||||
"ll-create-card": HASSDomEvent<HASSDomEvents["ll-create-card"]>;
|
||||
|
@ -61,6 +69,8 @@ export class HUIView extends ReactiveElement {
|
|||
|
||||
@state() private _badges: LovelaceBadge[] = [];
|
||||
|
||||
@state() private _sections: HuiSection[] = [];
|
||||
|
||||
private _layoutElementType?: string;
|
||||
|
||||
private _layoutElement?: LovelaceViewElement;
|
||||
|
@ -108,6 +118,27 @@ export class HUIView extends ReactiveElement {
|
|||
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() {
|
||||
return this;
|
||||
}
|
||||
|
@ -139,7 +170,7 @@ export class HUIView extends ReactiveElement {
|
|||
}
|
||||
}
|
||||
|
||||
protected update(changedProperties) {
|
||||
protected update(changedProperties: PropertyValues) {
|
||||
super.update(changedProperties);
|
||||
|
||||
// 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;
|
||||
|
||||
const oldHass = changedProperties.get("hass") as
|
||||
|
@ -181,6 +220,14 @@ export class HUIView extends ReactiveElement {
|
|||
}
|
||||
if (changedProperties.has("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")) {
|
||||
this._layoutElement.cards = this._cards;
|
||||
|
@ -220,6 +267,7 @@ export class HUIView extends ReactiveElement {
|
|||
|
||||
this._createBadges(viewConfig);
|
||||
this._createCards(viewConfig);
|
||||
this._createSections(viewConfig);
|
||||
this._layoutElement!.isStrategy = isStrategy;
|
||||
this._layoutElement!.hass = this.hass;
|
||||
this._layoutElement!.narrow = this.narrow;
|
||||
|
@ -227,6 +275,7 @@ export class HUIView extends ReactiveElement {
|
|||
this._layoutElement!.index = this.index;
|
||||
this._layoutElement!.cards = this._cards;
|
||||
this._layoutElement!.badges = this._badges;
|
||||
this._layoutElement!.sections = this._sections;
|
||||
|
||||
applyThemesOnElement(this, this.hass.themes, viewConfig.theme);
|
||||
this._viewConfigTheme = viewConfig.theme;
|
||||
|
@ -242,18 +291,21 @@ export class HUIView extends ReactiveElement {
|
|||
private _createLayoutElement(config: LovelaceViewConfig): void {
|
||||
this._layoutElement = createViewElement(config) as LovelaceViewElement;
|
||||
this._layoutElementType = config.type;
|
||||
this._layoutElement.addEventListener("ll-create-card", () => {
|
||||
this._layoutElement.addEventListener("ll-create-card", (ev) => {
|
||||
showCreateCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: [this.index],
|
||||
suggestedCards: ev.detail?.suggested,
|
||||
});
|
||||
});
|
||||
this._layoutElement.addEventListener("ll-edit-card", (ev) => {
|
||||
const { cardIndex } = parseLovelaceCardPath(ev.detail.path);
|
||||
showEditCardDialog(this, {
|
||||
lovelaceConfig: this.lovelace.config,
|
||||
saveConfig: this.lovelace.saveConfig,
|
||||
path: ev.detail.path,
|
||||
path: [this.index],
|
||||
cardIndex,
|
||||
});
|
||||
});
|
||||
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(
|
||||
cardElToReplace: LovelaceCard,
|
||||
config: LovelaceCardConfig
|
||||
|
@ -343,6 +408,23 @@ export class HUIView extends ReactiveElement {
|
|||
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 {
|
||||
|
|
|
@ -5096,7 +5096,8 @@
|
|||
"types": {
|
||||
"masonry": "Masonry (default)",
|
||||
"sidebar": "Sidebar",
|
||||
"panel": "Panel (1 card)"
|
||||
"panel": "Panel (1 card)",
|
||||
"sections": "Sections (experimental)"
|
||||
},
|
||||
"subview": "Subview",
|
||||
"subview_helper": "Subviews don't appear in tabs and have a back button.",
|
||||
|
@ -5112,7 +5113,7 @@
|
|||
"header": "Card configuration",
|
||||
"typed_header": "{type} Card configuration",
|
||||
"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",
|
||||
"unsaved_changes": "You have unsaved changes",
|
||||
"confirm_cancel": "Are you sure you want to cancel?",
|
||||
|
@ -5150,6 +5151,23 @@
|
|||
"no_config": "No config found.",
|
||||
"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": {
|
||||
"header": "We created a suggestion for you",
|
||||
"create_own": "Pick different card",
|
||||
|
@ -5455,7 +5473,10 @@
|
|||
"state": "State",
|
||||
"secondary_info_attribute": "Secondary info attribute",
|
||||
"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": {
|
||||
"name": "Map",
|
||||
|
|
|
@ -1,63 +1,12 @@
|
|||
import { assert } from "chai";
|
||||
|
||||
import { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
|
||||
import {
|
||||
swapCard,
|
||||
moveCard,
|
||||
moveCardToContainer,
|
||||
swapView,
|
||||
} from "../../../../src/panels/lovelace/editor/config-util";
|
||||
import { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
|
||||
|
||||
describe("swapCard", () => {
|
||||
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", () => {
|
||||
describe("moveCardToContainer", () => {
|
||||
it("move a card to an empty view", () => {
|
||||
const config: LovelaceConfig = {
|
||||
views: [
|
||||
|
@ -68,7 +17,7 @@ describe("moveCard", () => {
|
|||
],
|
||||
};
|
||||
|
||||
const result = moveCard(config, [1, 0], [0]);
|
||||
const result = moveCardToContainer(config, [1, 0], [0]);
|
||||
const expected: LovelaceConfig = {
|
||||
views: [
|
||||
{
|
||||
|
@ -94,7 +43,7 @@ describe("moveCard", () => {
|
|||
],
|
||||
};
|
||||
|
||||
const result = moveCard(config, [1, 0], [0]);
|
||||
const result = moveCardToContainer(config, [1, 0], [0]);
|
||||
const expected: LovelaceConfig = {
|
||||
views: [
|
||||
{
|
||||
|
@ -121,12 +70,12 @@ describe("moveCard", () => {
|
|||
};
|
||||
|
||||
const result = () => {
|
||||
moveCard(config, [1, 0], [1]);
|
||||
moveCardToContainer(config, [1, 0], [1]);
|
||||
};
|
||||
assert.throws(
|
||||
result,
|
||||
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