mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Improve element editor and migrate heading-entity editor (#22034)
* Extract load config element * Improve error by using ha-alert * Create hui-hase-editor * Migrate heading entity form to its own editor * Rename editor * Rename * Rename * Move heading entity to its own component * Fix default action for heading entity
This commit is contained in:
parent
a759767d79
commit
c4a700a55c
113
src/panels/lovelace/cards/heading/hui-heading-entity.ts
Normal file
113
src/panels/lovelace/cards/heading/hui-heading-entity.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-state-icon";
|
||||
import { ActionHandlerEvent } from "../../../../data/lovelace/action_handler";
|
||||
import "../../../../state-display/state-display";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { actionHandler } from "../../common/directives/action-handler-directive";
|
||||
import { handleAction } from "../../common/handle-action";
|
||||
import { hasAction } from "../../common/has-action";
|
||||
import type { HeadingEntityConfig } from "../types";
|
||||
|
||||
@customElement("hui-heading-entity")
|
||||
export class HuiHeadingEntity extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config!: HeadingEntityConfig | string;
|
||||
|
||||
private _handleAction(ev: ActionHandlerEvent) {
|
||||
const config: HeadingEntityConfig = {
|
||||
tap_action: {
|
||||
action: "none",
|
||||
},
|
||||
...this._config(this.config),
|
||||
};
|
||||
handleAction(this, this.hass!, config, ev.detail.action!);
|
||||
}
|
||||
|
||||
private _config = memoizeOne(
|
||||
(configOrString: HeadingEntityConfig | string): HeadingEntityConfig => {
|
||||
const config =
|
||||
typeof configOrString === "string"
|
||||
? { entity: configOrString }
|
||||
: configOrString;
|
||||
|
||||
return {
|
||||
tap_action: {
|
||||
action: "none",
|
||||
},
|
||||
...config,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const config = this._config(this.config);
|
||||
|
||||
const stateObj = this.hass!.states[config.entity];
|
||||
|
||||
if (!stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const actionable = hasAction(config.tap_action);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="entity"
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler()}
|
||||
role=${ifDefined(actionable ? "button" : undefined)}
|
||||
tabindex=${ifDefined(actionable ? "0" : undefined)}
|
||||
>
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.icon=${config.icon}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-icon>
|
||||
<state-display
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.content=${config.content || "state"}
|
||||
></state-display>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
.entity {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
color: var(--secondary-text-color);
|
||||
font-family: Roboto;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: 0.1px;
|
||||
--mdc-icon-size: 14px;
|
||||
}
|
||||
.entity ha-state-icon {
|
||||
--ha-icon-display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-heading-entity": HuiHeadingEntity;
|
||||
}
|
||||
}
|
@ -16,7 +16,8 @@ import type {
|
||||
LovelaceCardEditor,
|
||||
LovelaceLayoutOptions,
|
||||
} from "../types";
|
||||
import type { HeadingCardConfig, HeadingCardEntityConfig } from "./types";
|
||||
import "./heading/hui-heading-entity";
|
||||
import type { HeadingCardConfig } from "./types";
|
||||
|
||||
@customElement("hui-heading-card")
|
||||
export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
@ -91,8 +92,11 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
${this._config.entities?.length
|
||||
? html`
|
||||
<div class="entities">
|
||||
${this._config.entities.map((config) =>
|
||||
this._renderEntity(config)
|
||||
${this._config.entities.map(
|
||||
(config) => html`
|
||||
<hui-heading-entity .config=${config} .hass=${this.hass}>
|
||||
</hui-heading-entity>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
@ -102,54 +106,6 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleEntityAction(ev: ActionHandlerEvent) {
|
||||
const config = {
|
||||
tap_action: {
|
||||
action: "none",
|
||||
},
|
||||
...(ev.currentTarget as any).config,
|
||||
};
|
||||
|
||||
handleAction(this, this.hass!, config, ev.detail.action!);
|
||||
}
|
||||
|
||||
_renderEntity(entityConfig: string | HeadingCardEntityConfig) {
|
||||
const config =
|
||||
typeof entityConfig === "string"
|
||||
? { entity: entityConfig }
|
||||
: entityConfig;
|
||||
|
||||
const stateObj = this.hass!.states[config.entity];
|
||||
|
||||
if (!stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const actionable = hasAction(config.tap_action || { action: "none" });
|
||||
|
||||
return html`
|
||||
<div
|
||||
.config=${config}
|
||||
class="entity"
|
||||
@action=${this._handleEntityAction}
|
||||
.actionHandler=${actionHandler()}
|
||||
role=${ifDefined(actionable ? "button" : undefined)}
|
||||
tabindex=${ifDefined(actionable ? "0" : undefined)}
|
||||
>
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.icon=${config.icon}
|
||||
.stateObj=${stateObj}
|
||||
></ha-state-icon>
|
||||
<state-display
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.content=${config.content || "state"}
|
||||
></state-display>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-card {
|
||||
@ -231,24 +187,6 @@ export class HuiHeadingCard extends LitElement implements LovelaceCard {
|
||||
justify-content: flex-end;
|
||||
gap: 4px 10px;
|
||||
}
|
||||
.entities .entity {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
color: var(--secondary-text-color);
|
||||
font-family: Roboto;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px; /* 142.857% */
|
||||
letter-spacing: 0.1px;
|
||||
--mdc-icon-size: 14px;
|
||||
}
|
||||
.entities .entity ha-state-icon {
|
||||
--ha-icon-display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -503,7 +503,7 @@ export interface TileCardConfig extends LovelaceCardConfig {
|
||||
features?: LovelaceCardFeatureConfig[];
|
||||
}
|
||||
|
||||
export interface HeadingCardEntityConfig {
|
||||
export interface HeadingEntityConfig {
|
||||
entity: string;
|
||||
content?: string | string[];
|
||||
icon?: string;
|
||||
@ -515,5 +515,5 @@ export interface HeadingCardConfig extends LovelaceCardConfig {
|
||||
heading?: string;
|
||||
icon?: string;
|
||||
tap_action?: ActionConfig;
|
||||
entities?: (string | HeadingCardEntityConfig)[];
|
||||
entities?: (string | HeadingEntityConfig)[];
|
||||
}
|
||||
|
@ -5,13 +5,13 @@ import { customElement, state } from "lit/decorators";
|
||||
import { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
|
||||
import { getBadgeElementClass } from "../../create-element/create-badge-element";
|
||||
import type { LovelaceCardEditor, LovelaceConfigForm } from "../../types";
|
||||
import { HuiElementEditor } from "../hui-element-editor";
|
||||
import { HuiTypedElementEditor } from "../hui-typed-element-editor";
|
||||
import "./hui-badge-visibility-editor";
|
||||
|
||||
const tabs = ["config", "visibility"] as const;
|
||||
|
||||
@customElement("hui-badge-element-editor")
|
||||
export class HuiBadgeElementEditor extends HuiElementEditor<LovelaceBadgeConfig> {
|
||||
export class HuiBadgeElementEditor extends HuiTypedElementEditor<LovelaceBadgeConfig> {
|
||||
@state() private _currTab: (typeof tabs)[number] = tabs[0];
|
||||
|
||||
protected async getConfigElement(): Promise<LovelaceCardEditor | undefined> {
|
||||
@ -88,7 +88,7 @@ export class HuiBadgeElementEditor extends HuiElementEditor<LovelaceBadgeConfig>
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
HuiElementEditor.styles,
|
||||
HuiTypedElementEditor.styles,
|
||||
css`
|
||||
mwc-tab-bar {
|
||||
text-transform: uppercase;
|
||||
|
@ -5,7 +5,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import { getCardElementClass } from "../../create-element/create-card-element";
|
||||
import type { LovelaceCardEditor, LovelaceConfigForm } from "../../types";
|
||||
import { HuiElementEditor } from "../hui-element-editor";
|
||||
import { HuiTypedElementEditor } from "../hui-typed-element-editor";
|
||||
import "./hui-card-layout-editor";
|
||||
import "./hui-card-visibility-editor";
|
||||
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
@ -13,7 +13,7 @@ import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section"
|
||||
const tabs = ["config", "visibility", "layout"] as const;
|
||||
|
||||
@customElement("hui-card-element-editor")
|
||||
export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> {
|
||||
export class HuiCardElementEditor extends HuiTypedElementEditor<LovelaceCardConfig> {
|
||||
@property({ type: Boolean, attribute: "show-visibility-tab" })
|
||||
public showVisibilityTab = false;
|
||||
|
||||
@ -119,7 +119,7 @@ export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
HuiElementEditor.styles,
|
||||
HuiTypedElementEditor.styles,
|
||||
css`
|
||||
mwc-tab-bar {
|
||||
text-transform: uppercase;
|
||||
|
@ -8,11 +8,10 @@ import {
|
||||
array,
|
||||
assert,
|
||||
assign,
|
||||
literal,
|
||||
enums,
|
||||
object,
|
||||
optional,
|
||||
string,
|
||||
union,
|
||||
} from "superstruct";
|
||||
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
@ -24,17 +23,14 @@ import type {
|
||||
} from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type {
|
||||
HeadingCardConfig,
|
||||
HeadingCardEntityConfig,
|
||||
} from "../../cards/types";
|
||||
import type { HeadingCardConfig, HeadingEntityConfig } from "../../cards/types";
|
||||
import { UiAction } from "../../components/hui-action-editor";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import "../hui-sub-form-editor";
|
||||
import "../hui-sub-element-editor";
|
||||
import { processEditorEntities } from "../process-editor-entities";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { SubFormEditorData } from "../types";
|
||||
import { SubElementEditorConfig } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
import "./hui-entities-editor";
|
||||
|
||||
@ -43,7 +39,7 @@ const actions: UiAction[] = ["navigate", "url", "perform-action", "none"];
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
heading_style: optional(union([literal("title"), literal("subtitle")])),
|
||||
heading_style: optional(enums(["title", "subtitle"])),
|
||||
heading: optional(string()),
|
||||
icon: optional(string()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
@ -51,13 +47,6 @@ const cardConfigStruct = assign(
|
||||
})
|
||||
);
|
||||
|
||||
const entityConfigStruct = object({
|
||||
entity: string(),
|
||||
content: optional(union([string(), array(string())])),
|
||||
icon: optional(string()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
});
|
||||
|
||||
@customElement("hui-heading-card-editor")
|
||||
export class HuiHeadingCardEditor
|
||||
extends LitElement
|
||||
@ -68,17 +57,13 @@ export class HuiHeadingCardEditor
|
||||
@state() private _config?: HeadingCardConfig;
|
||||
|
||||
@state()
|
||||
private _entityFormEditorData?: SubFormEditorData<HeadingCardEntityConfig>;
|
||||
private _subElementEditorConfig?: SubElementEditorConfig;
|
||||
|
||||
public setConfig(config: HeadingCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
public _assertEntityConfig(config: HeadingCardEntityConfig): void {
|
||||
assert(config, entityConfigStruct);
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
@ -123,68 +108,27 @@ export class HuiHeadingCardEditor
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
private _entitySchema = memoizeOne(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
name: "entity",
|
||||
selector: { entity: {} },
|
||||
},
|
||||
{
|
||||
name: "icon",
|
||||
selector: { icon: {} },
|
||||
context: { icon_entity: "entity" },
|
||||
},
|
||||
{
|
||||
name: "content",
|
||||
selector: { ui_state_content: {} },
|
||||
context: { filter_entity: "entity" },
|
||||
},
|
||||
{
|
||||
name: "interactions",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiGestureTap,
|
||||
schema: [
|
||||
{
|
||||
name: "tap_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return cache(
|
||||
this._entityFormEditorData ? this._renderEntityForm() : this._renderForm()
|
||||
this._subElementEditorConfig
|
||||
? this._renderEntityForm()
|
||||
: this._renderForm()
|
||||
);
|
||||
}
|
||||
|
||||
private _renderEntityForm() {
|
||||
const schema = this._entitySchema();
|
||||
return html`
|
||||
<hui-sub-form-editor
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.entities.form-label"
|
||||
)}
|
||||
<hui-sub-element-editor
|
||||
.hass=${this.hass}
|
||||
.data=${this._entityFormEditorData!.data}
|
||||
.config=${this._subElementEditorConfig}
|
||||
@go-back=${this._goBack}
|
||||
@value-changed=${this._subFormChanged}
|
||||
.schema=${schema}
|
||||
.assertConfig=${this._assertEntityConfig}
|
||||
.computeLabel=${this._computeEntityLabelCallback}
|
||||
@config-changed=${this._subElementChanged}
|
||||
>
|
||||
</hui-sub-form-editor>
|
||||
</hui-sub-element-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -239,7 +183,7 @@ export class HuiHeadingCardEditor
|
||||
|
||||
const config = {
|
||||
...this._config,
|
||||
entities: ev.detail.entities as HeadingCardEntityConfig[],
|
||||
entities: ev.detail.entities as HeadingEntityConfig[],
|
||||
};
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
@ -256,30 +200,30 @@ export class HuiHeadingCardEditor
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _subFormChanged(ev: CustomEvent): void {
|
||||
private _subElementChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = ev.detail.value;
|
||||
const value = ev.detail.config;
|
||||
|
||||
const newEntities = this._config!.entities
|
||||
const newConfigEntities = this._config!.entities
|
||||
? [...this._config!.entities]
|
||||
: [];
|
||||
|
||||
if (!value) {
|
||||
newEntities.splice(this._entityFormEditorData!.index!, 1);
|
||||
newConfigEntities.splice(this._subElementEditorConfig!.index!, 1);
|
||||
this._goBack();
|
||||
} else {
|
||||
newEntities[this._entityFormEditorData!.index!] = value;
|
||||
newConfigEntities[this._subElementEditorConfig!.index!] = value;
|
||||
}
|
||||
|
||||
this._config = { ...this._config!, entities: newEntities };
|
||||
this._config = { ...this._config!, entities: newConfigEntities };
|
||||
|
||||
this._entityFormEditorData = {
|
||||
...this._entityFormEditorData!,
|
||||
data: value,
|
||||
this._subElementEditorConfig = {
|
||||
...this._subElementEditorConfig!,
|
||||
elementConfig: value,
|
||||
};
|
||||
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
@ -287,31 +231,17 @@ export class HuiHeadingCardEditor
|
||||
|
||||
private _editEntity(ev: HASSDomEvent<{ index: number }>): void {
|
||||
const entities = this._entities(this._config!.entities);
|
||||
this._entityFormEditorData = {
|
||||
data: entities[ev.detail.index],
|
||||
this._subElementEditorConfig = {
|
||||
elementConfig: entities[ev.detail.index],
|
||||
index: ev.detail.index,
|
||||
type: "heading-entity",
|
||||
};
|
||||
}
|
||||
|
||||
private _goBack(): void {
|
||||
this._entityFormEditorData = undefined;
|
||||
this._subElementEditorConfig = undefined;
|
||||
}
|
||||
|
||||
private _computeEntityLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._entitySchema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "content":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.heading.entity_config.${schema.name}`
|
||||
);
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
|
@ -2,10 +2,10 @@ import { customElement } from "lit/decorators";
|
||||
import { LovelaceDashboardStrategyConfig } from "../../../../data/lovelace/config/types";
|
||||
import { getLovelaceStrategy } from "../../strategies/get-strategy";
|
||||
import { LovelaceStrategyEditor } from "../../strategies/types";
|
||||
import { HuiElementEditor } from "../hui-element-editor";
|
||||
import { HuiTypedElementEditor } from "../hui-typed-element-editor";
|
||||
|
||||
@customElement("hui-dashboard-strategy-element-editor")
|
||||
export class HuiDashboardStrategyElementEditor extends HuiElementEditor<LovelaceDashboardStrategyConfig> {
|
||||
export class HuiDashboardStrategyElementEditor extends HuiTypedElementEditor<LovelaceDashboardStrategyConfig> {
|
||||
protected async getConfigElement(): Promise<
|
||||
LovelaceStrategyEditor | undefined
|
||||
> {
|
||||
|
@ -2,12 +2,12 @@ import { customElement } from "lit/decorators";
|
||||
import { getRowElementClass } from "../../create-element/create-row-element";
|
||||
import { LovelaceRowConfig } from "../../entity-rows/types";
|
||||
import type { LovelaceRowEditor } from "../../types";
|
||||
import { HuiElementEditor } from "../hui-element-editor";
|
||||
import { HuiTypedElementEditor } from "../hui-typed-element-editor";
|
||||
|
||||
const GENERIC_ROW_TYPE = "generic-row";
|
||||
|
||||
@customElement("hui-row-element-editor")
|
||||
export class HuiRowElementEditor extends HuiElementEditor<LovelaceRowConfig> {
|
||||
export class HuiRowElementEditor extends HuiTypedElementEditor<LovelaceRowConfig> {
|
||||
protected get configElementType(): string | undefined {
|
||||
if (!this.value?.type && "entity" in this.value!) {
|
||||
return GENERIC_ROW_TYPE;
|
||||
|
@ -8,10 +8,10 @@ import type {
|
||||
LovelaceConfigForm,
|
||||
LovelaceCardFeatureEditor,
|
||||
} from "../../types";
|
||||
import { HuiElementEditor } from "../hui-element-editor";
|
||||
import { HuiTypedElementEditor } from "../hui-typed-element-editor";
|
||||
|
||||
@customElement("hui-card-feature-element-editor")
|
||||
export class HuiCardFeatureElementEditor extends HuiElementEditor<
|
||||
export class HuiCardFeatureElementEditor extends HuiTypedElementEditor<
|
||||
LovelaceCardFeatureConfig,
|
||||
LovelaceCardFeatureContext
|
||||
> {
|
||||
|
@ -2,10 +2,10 @@ import { customElement } from "lit/decorators";
|
||||
import { getHeaderFooterElementClass } from "../../create-element/create-header-footer-element";
|
||||
import type { LovelaceHeaderFooterConfig } from "../../header-footer/types";
|
||||
import type { LovelaceHeaderFooterEditor } from "../../types";
|
||||
import { HuiElementEditor } from "../hui-element-editor";
|
||||
import { HuiTypedElementEditor } from "../hui-typed-element-editor";
|
||||
|
||||
@customElement("hui-headerfooter-element-editor")
|
||||
export class HuiHeaderFooterElementEditor extends HuiElementEditor<LovelaceHeaderFooterConfig> {
|
||||
export class HuiHeaderFooterElementEditor extends HuiTypedElementEditor<LovelaceHeaderFooterConfig> {
|
||||
protected async getConfigElement(): Promise<
|
||||
LovelaceHeaderFooterEditor | undefined
|
||||
> {
|
||||
|
@ -0,0 +1,140 @@
|
||||
import { mdiGestureTap } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { array, assert, object, optional, string, union } from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { HeadingCardConfig, HeadingEntityConfig } from "../../cards/types";
|
||||
import type { LovelaceGenericElementEditor } from "../../types";
|
||||
import { configElementStyle } from "../config-elements/config-elements-style";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
|
||||
const entityConfigStruct = object({
|
||||
entity: string(),
|
||||
content: optional(union([string(), array(string())])),
|
||||
icon: optional(string()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
});
|
||||
|
||||
@customElement("hui-heading-entity-editor")
|
||||
export class HuiHeadingEntityEditor
|
||||
extends LitElement
|
||||
implements LovelaceGenericElementEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: HeadingEntityConfig;
|
||||
|
||||
public setConfig(config: HeadingEntityConfig): void {
|
||||
assert(config, entityConfigStruct);
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
name: "entity",
|
||||
selector: { entity: {} },
|
||||
},
|
||||
{
|
||||
name: "icon",
|
||||
selector: { icon: {} },
|
||||
context: { icon_entity: "entity" },
|
||||
},
|
||||
{
|
||||
name: "content",
|
||||
selector: { ui_state_content: {} },
|
||||
context: { filter_entity: "entity" },
|
||||
},
|
||||
{
|
||||
name: "interactions",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiGestureTap,
|
||||
schema: [
|
||||
{
|
||||
name: "tap_action",
|
||||
selector: {
|
||||
ui_action: {
|
||||
default_action: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const schema = this._schema();
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = ev.detail.value as HeadingCardConfig;
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "content":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.heading.entity_config.${schema.name}`
|
||||
);
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
configElementStyle,
|
||||
css`
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
ha-form {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-heading-entity-editor": HuiHeadingEntityEditor;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import { HeadingEntityConfig } from "../../cards/types";
|
||||
import { HuiElementEditor } from "../hui-element-editor";
|
||||
import type { HuiHeadingEntityEditor } from "./hui-heading-entity-editor";
|
||||
|
||||
@customElement("hui-heading-entity-element-editor")
|
||||
export class HuiHeadingEntityElementEditor extends HuiElementEditor<HeadingEntityConfig> {
|
||||
protected async getConfigElement(): Promise<
|
||||
HuiHeadingEntityEditor | undefined
|
||||
> {
|
||||
await import("./hui-heading-entity-editor");
|
||||
return document.createElement("hui-heading-entity-editor");
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-heading-entity-element-editor": HuiHeadingEntityElementEditor;
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import "@material/mwc-button";
|
||||
import { dump, load } from "js-yaml";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
@ -7,6 +6,7 @@ import {
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
@ -16,34 +16,18 @@ import "../../../components/ha-alert";
|
||||
import "../../../components/ha-circular-progress";
|
||||
import "../../../components/ha-code-editor";
|
||||
import type { HaCodeEditor } from "../../../components/ha-code-editor";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
import { LovelaceConfig } from "../../../data/lovelace/config/types";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCardFeatureConfig } from "../card-features/types";
|
||||
import type { LovelaceRowConfig } from "../entity-rows/types";
|
||||
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||
import { LovelaceElementConfig } from "../elements/types";
|
||||
import type {
|
||||
LovelaceConfigForm,
|
||||
LovelaceGenericElementEditor,
|
||||
} from "../types";
|
||||
import "./card-editor/hui-card-visibility-editor";
|
||||
import type { HuiFormEditor } from "./config-elements/hui-form-editor";
|
||||
import "./config-elements/hui-generic-entity-row-editor";
|
||||
import { GUISupportError } from "./gui-support-error";
|
||||
import { EditSubElementEvent, GUIModeChangedEvent } from "./types";
|
||||
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
|
||||
export interface ConfigChangedEvent {
|
||||
config:
|
||||
| LovelaceCardConfig
|
||||
| LovelaceRowConfig
|
||||
| LovelaceHeaderFooterConfig
|
||||
| LovelaceCardFeatureConfig
|
||||
| LovelaceStrategyConfig
|
||||
| LovelaceElementConfig
|
||||
| LovelaceBadgeConfig;
|
||||
export interface ConfigChangedEvent<T extends object = object> {
|
||||
config: T;
|
||||
error?: string;
|
||||
guiModeAvailable?: boolean;
|
||||
}
|
||||
@ -56,17 +40,16 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export interface UIConfigChangedEvent extends Event {
|
||||
export interface UIConfigChangedEvent<T extends object = object> extends Event {
|
||||
detail: {
|
||||
config:
|
||||
| LovelaceCardConfig
|
||||
| LovelaceRowConfig
|
||||
| LovelaceHeaderFooterConfig
|
||||
| LovelaceCardFeatureConfig;
|
||||
config: T;
|
||||
};
|
||||
}
|
||||
|
||||
export abstract class HuiElementEditor<T, C = any> extends LitElement {
|
||||
export abstract class HuiElementEditor<
|
||||
T extends object = object,
|
||||
C = any,
|
||||
> extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public lovelace?: LovelaceConfig;
|
||||
@ -79,8 +62,6 @@ export abstract class HuiElementEditor<T, C = any> extends LitElement {
|
||||
|
||||
@state() private _configElement?: LovelaceGenericElementEditor;
|
||||
|
||||
@state() private _configElementType?: string;
|
||||
|
||||
@state() private _guiMode = true;
|
||||
|
||||
// Error: Configuration broken - do not save
|
||||
@ -199,10 +180,6 @@ export abstract class HuiElementEditor<T, C = any> extends LitElement {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected get configElementType(): string | undefined {
|
||||
return this.value ? (this.value as any).type : undefined;
|
||||
}
|
||||
|
||||
protected renderConfigElement(): TemplateResult {
|
||||
return html`${this._configElement}`;
|
||||
}
|
||||
@ -239,45 +216,51 @@ export abstract class HuiElementEditor<T, C = any> extends LitElement {
|
||||
></ha-code-editor>
|
||||
</div>
|
||||
`}
|
||||
${this._guiSupported === false && this.configElementType
|
||||
${this._guiSupported === false && this._loading === false
|
||||
? html`
|
||||
<div class="info">
|
||||
${this.hass.localize("ui.errors.config.editor_not_available", {
|
||||
type: this.configElementType,
|
||||
})}
|
||||
</div>
|
||||
<ha-alert
|
||||
alert-type="info"
|
||||
.title=${this.hass.localize(
|
||||
"ui.errors.config.visual_editor_not_supported"
|
||||
)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.errors.config.visual_editor_not_supported_reason_type"
|
||||
)}
|
||||
<br />
|
||||
${this.hass.localize("ui.errors.config.edit_in_yaml_supported")}
|
||||
</ha-alert>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
${this.hasError
|
||||
? html`
|
||||
<div class="error">
|
||||
${this.hass.localize("ui.errors.config.error_detected")}:
|
||||
<br />
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this.hass.localize(
|
||||
"ui.errors.config.configuration_error"
|
||||
)}
|
||||
>
|
||||
<ul>
|
||||
${this._errors!.map((error) => html`<li>${error}</li>`)}
|
||||
</ul>
|
||||
</div>
|
||||
</ha-alert>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
${this.hasWarning
|
||||
? html`
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
.title="${this.hass.localize(
|
||||
"ui.errors.config.editor_not_supported"
|
||||
)}:"
|
||||
.title=${this.hass.localize(
|
||||
"ui.errors.config.visual_editor_not_supported"
|
||||
)}
|
||||
>
|
||||
${this._warnings!.length > 0 && this._warnings![0] !== undefined
|
||||
? html` <ul>
|
||||
${this._warnings!.map(
|
||||
(warning) => html`<li>${warning}</li>`
|
||||
)}
|
||||
</ul>`
|
||||
: ""}
|
||||
<ul>
|
||||
${this._warnings!.map((warning) => html`<li>${warning}</li>`)}
|
||||
</ul>
|
||||
${this.hass.localize("ui.errors.config.edit_in_yaml_supported")}
|
||||
</ha-alert>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -300,7 +283,7 @@ export abstract class HuiElementEditor<T, C = any> extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleUIConfigChanged(ev: UIConfigChangedEvent) {
|
||||
private _handleUIConfigChanged(ev: UIConfigChangedEvent<T>) {
|
||||
ev.stopPropagation();
|
||||
const config = ev.detail.config;
|
||||
Object.keys(config).forEach((key) => {
|
||||
@ -319,66 +302,62 @@ export abstract class HuiElementEditor<T, C = any> extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected async unloadConfigElement(): Promise<void> {
|
||||
this._configElement = undefined;
|
||||
this._guiSupported = undefined;
|
||||
}
|
||||
|
||||
protected async loadConfigElement(): Promise<void> {
|
||||
if (this._configElement) return;
|
||||
|
||||
let configElement = await this.getConfigElement();
|
||||
|
||||
if (!configElement) {
|
||||
const form = await this.getConfigForm();
|
||||
if (form) {
|
||||
await import("./config-elements/hui-form-editor");
|
||||
configElement = document.createElement("hui-form-editor");
|
||||
const { schema, assertConfig, computeLabel, computeHelper } = form;
|
||||
(configElement as HuiFormEditor).schema = schema;
|
||||
if (computeLabel) {
|
||||
(configElement as HuiFormEditor).computeLabel = computeLabel;
|
||||
}
|
||||
if (computeHelper) {
|
||||
(configElement as HuiFormEditor).computeHelper = computeHelper;
|
||||
}
|
||||
if (assertConfig) {
|
||||
(configElement as HuiFormEditor).assertConfig = assertConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (configElement) {
|
||||
configElement.hass = this.hass;
|
||||
if ("lovelace" in configElement) {
|
||||
configElement.lovelace = this.lovelace;
|
||||
}
|
||||
configElement.context = this.context;
|
||||
configElement.addEventListener("config-changed", (ev) =>
|
||||
this._handleUIConfigChanged(ev as UIConfigChangedEvent<T>)
|
||||
);
|
||||
this._guiSupported = true;
|
||||
} else {
|
||||
this._guiSupported = false;
|
||||
}
|
||||
|
||||
this._configElement = configElement;
|
||||
}
|
||||
|
||||
private async _updateConfigElement(): Promise<void> {
|
||||
if (!this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
let configElement: LovelaceGenericElementEditor | undefined;
|
||||
|
||||
try {
|
||||
this._errors = undefined;
|
||||
this._warnings = undefined;
|
||||
|
||||
if (this._configElementType !== this.configElementType) {
|
||||
// If the type has changed, we need to load a new GUI editor
|
||||
this._guiSupported = undefined;
|
||||
this._configElement = undefined;
|
||||
|
||||
if (!this.configElementType) {
|
||||
throw new Error(
|
||||
this.hass.localize("ui.errors.config.no_type_provided")
|
||||
);
|
||||
}
|
||||
|
||||
this._configElementType = this.configElementType;
|
||||
|
||||
this._loading = true;
|
||||
configElement = await this.getConfigElement();
|
||||
|
||||
if (!configElement) {
|
||||
const form = await this.getConfigForm();
|
||||
if (form) {
|
||||
await import("./config-elements/hui-form-editor");
|
||||
configElement = document.createElement("hui-form-editor");
|
||||
const { schema, assertConfig, computeLabel, computeHelper } = form;
|
||||
(configElement as HuiFormEditor).schema = schema;
|
||||
if (computeLabel) {
|
||||
(configElement as HuiFormEditor).computeLabel = computeLabel;
|
||||
}
|
||||
if (computeHelper) {
|
||||
(configElement as HuiFormEditor).computeHelper = computeHelper;
|
||||
}
|
||||
if (assertConfig) {
|
||||
(configElement as HuiFormEditor).assertConfig = assertConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (configElement) {
|
||||
configElement.hass = this.hass;
|
||||
if ("lovelace" in configElement) {
|
||||
configElement.lovelace = this.lovelace;
|
||||
}
|
||||
configElement.context = this.context;
|
||||
configElement.addEventListener("config-changed", (ev) =>
|
||||
this._handleUIConfigChanged(ev as UIConfigChangedEvent)
|
||||
);
|
||||
|
||||
this._configElement = configElement;
|
||||
this._guiSupported = true;
|
||||
}
|
||||
}
|
||||
await this.loadConfigElement();
|
||||
|
||||
if (this._configElement) {
|
||||
// Setup GUI editor and check that it can handle the current config
|
||||
@ -428,26 +407,6 @@ export abstract class HuiElementEditor<T, C = any> extends LitElement {
|
||||
ha-code-editor {
|
||||
--code-mirror-max-height: calc(100vh - 245px);
|
||||
}
|
||||
.error,
|
||||
.warning,
|
||||
.info {
|
||||
word-break: break-word;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
.warning {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
.warning ul,
|
||||
.error ul {
|
||||
margin: 4px 0;
|
||||
}
|
||||
.warning li,
|
||||
.error li {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
ha-circular-progress {
|
||||
display: block;
|
||||
margin: auto;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiCodeBraces, mdiListBoxOutline } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
@ -13,14 +12,13 @@ import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-button-prev";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceRowConfig } from "../entity-rows/types";
|
||||
import type { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||
import "./entity-row-editor/hui-row-element-editor";
|
||||
import "./header-footer-editor/hui-header-footer-element-editor";
|
||||
import type { HuiElementEditor } from "./hui-element-editor";
|
||||
import "./feature-editor/hui-card-feature-element-editor";
|
||||
import type { GUIModeChangedEvent, SubElementEditorConfig } from "./types";
|
||||
import "./header-footer-editor/hui-header-footer-element-editor";
|
||||
import "./heading-entity/hui-heading-entity-element-editor";
|
||||
import type { HuiElementEditor } from "./hui-element-editor";
|
||||
import "./picture-element-editor/hui-picture-element-element-editor";
|
||||
import type { GUIModeChangedEvent, SubElementEditorConfig } from "./types";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@ -40,11 +38,14 @@ export class HuiSubElementEditor extends LitElement {
|
||||
|
||||
@state() private _guiMode = true;
|
||||
|
||||
@query(".editor") private _editorElement?: HuiElementEditor<
|
||||
LovelaceRowConfig | LovelaceHeaderFooterConfig
|
||||
>;
|
||||
@query(".editor") private _editorElement?: HuiElementEditor;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const elementType =
|
||||
this.config.elementConfig && "type" in this.config.elementConfig
|
||||
? this.config.elementConfig.type
|
||||
: "";
|
||||
|
||||
return html`
|
||||
<div class="header">
|
||||
<div class="back-title">
|
||||
@ -52,21 +53,21 @@ export class HuiSubElementEditor extends LitElement {
|
||||
.label=${this.hass!.localize("ui.common.back")}
|
||||
@click=${this._goBack}
|
||||
></ha-icon-button-prev>
|
||||
<span slot="title"
|
||||
>${this.config?.type === "element"
|
||||
<span slot="title">
|
||||
${this.config?.type === "element"
|
||||
? this.hass.localize(
|
||||
`ui.panel.lovelace.editor.sub-element-editor.types.element_type`,
|
||||
{
|
||||
type:
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.card.picture-elements.element_types.${this.config?.elementConfig?.type}`
|
||||
) || this.config?.elementConfig?.type,
|
||||
`ui.panel.lovelace.editor.card.picture-elements.element_types.${elementType}`
|
||||
) || elementType,
|
||||
}
|
||||
)
|
||||
: this.hass.localize(
|
||||
`ui.panel.lovelace.editor.sub-element-editor.types.${this.config?.type}`
|
||||
)}</span
|
||||
>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<ha-icon-button
|
||||
class="gui-mode-button"
|
||||
@ -80,54 +81,74 @@ export class HuiSubElementEditor extends LitElement {
|
||||
.path=${this._guiMode ? mdiCodeBraces : mdiListBoxOutline}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
${this.config.type === "row"
|
||||
? html`
|
||||
<hui-row-element-editor
|
||||
class="editor"
|
||||
.hass=${this.hass}
|
||||
.value=${this.config.elementConfig}
|
||||
.context=${this.context}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-row-element-editor>
|
||||
`
|
||||
: this.config.type === "header" || this.config.type === "footer"
|
||||
? html`
|
||||
<hui-headerfooter-element-editor
|
||||
class="editor"
|
||||
.hass=${this.hass}
|
||||
.value=${this.config.elementConfig}
|
||||
.context=${this.context}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-headerfooter-element-editor>
|
||||
`
|
||||
: this.config.type === "feature"
|
||||
? html`
|
||||
<hui-card-feature-element-editor
|
||||
class="editor"
|
||||
.hass=${this.hass}
|
||||
.value=${this.config.elementConfig}
|
||||
.context=${this.context}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-card-feature-element-editor>
|
||||
`
|
||||
: this.config.type === "element"
|
||||
? html`
|
||||
<hui-picture-element-element-editor
|
||||
class="editor"
|
||||
.hass=${this.hass}
|
||||
.value=${this.config.elementConfig}
|
||||
.context=${this.context}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-picture-element-element-editor>
|
||||
`
|
||||
: nothing}
|
||||
${this._renderEditor()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderEditor() {
|
||||
const type = this.config.type;
|
||||
switch (type) {
|
||||
case "row":
|
||||
return html`
|
||||
<hui-row-element-editor
|
||||
class="editor"
|
||||
.hass=${this.hass}
|
||||
.value=${this.config.elementConfig}
|
||||
.context=${this.context}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-row-element-editor>
|
||||
`;
|
||||
case "header":
|
||||
case "footer":
|
||||
return html`
|
||||
<hui-headerfooter-element-editor
|
||||
class="editor"
|
||||
.hass=${this.hass}
|
||||
.value=${this.config.elementConfig}
|
||||
.context=${this.context}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-headerfooter-element-editor>
|
||||
`;
|
||||
case "element":
|
||||
return html`
|
||||
<hui-picture-element-element-editor
|
||||
class="editor"
|
||||
.hass=${this.hass}
|
||||
.value=${this.config.elementConfig}
|
||||
.context=${this.context}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-picture-element-element-editor>
|
||||
`;
|
||||
case "feature":
|
||||
return html`
|
||||
<hui-card-feature-element-editor
|
||||
class="editor"
|
||||
.hass=${this.hass}
|
||||
.value=${this.config.elementConfig}
|
||||
.context=${this.context}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-card-feature-element-editor>
|
||||
`;
|
||||
case "heading-entity":
|
||||
return html`
|
||||
<hui-heading-entity-element-editor
|
||||
class="editor"
|
||||
.hass=${this.hass}
|
||||
.value=${this.config.elementConfig}
|
||||
.context=${this.context}
|
||||
@config-changed=${this._handleConfigChanged}
|
||||
@GUImode-changed=${this._handleGUIModeChanged}
|
||||
></hui-heading-entity-element-editor>
|
||||
`;
|
||||
default:
|
||||
return nothing;
|
||||
}
|
||||
}
|
||||
|
||||
private _goBack(): void {
|
||||
fireEvent(this, "go-back");
|
||||
}
|
||||
|
@ -1,190 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiCodeBraces, mdiListBoxOutline } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-form/ha-form";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-button-prev";
|
||||
import "../../../components/ha-yaml-editor";
|
||||
import "../../../components/ha-alert";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceConfigForm } from "../types";
|
||||
import type { EditSubFormEvent } from "./types";
|
||||
import { handleStructError } from "../../../common/structs/handle-errors";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"go-back": undefined;
|
||||
"edit-sub-form": EditSubFormEvent;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("hui-sub-form-editor")
|
||||
export class HuiSubFormEditor<T = any> extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public data!: T;
|
||||
|
||||
public schema!: LovelaceConfigForm["schema"];
|
||||
|
||||
public assertConfig?: (config: T) => void;
|
||||
|
||||
public computeLabel?: LovelaceConfigForm["computeLabel"];
|
||||
|
||||
public computeHelper?: LovelaceConfigForm["computeHelper"];
|
||||
|
||||
@state() public _yamlMode = false;
|
||||
|
||||
@state() private _errors?: string[];
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const uiAvailable = !this.hasWarning && !this.hasError;
|
||||
|
||||
return html`
|
||||
<div class="header">
|
||||
<div class="back-title">
|
||||
<ha-icon-button-prev
|
||||
.label=${this.hass!.localize("ui.common.back")}
|
||||
@click=${this._goBack}
|
||||
></ha-icon-button-prev>
|
||||
<span slot="title">${this.label}</span>
|
||||
</div>
|
||||
<ha-icon-button
|
||||
class="gui-mode-button"
|
||||
@click=${this._toggleMode}
|
||||
.disabled=${!uiAvailable}
|
||||
.label=${this.hass!.localize(
|
||||
this._yamlMode
|
||||
? "ui.panel.lovelace.editor.edit_card.show_visual_editor"
|
||||
: "ui.panel.lovelace.editor.edit_card.show_code_editor"
|
||||
)}
|
||||
.path=${this._yamlMode ? mdiListBoxOutline : mdiCodeBraces}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
${this._yamlMode
|
||||
? html`
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this.data}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-yaml-editor>
|
||||
`
|
||||
: html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.schema=${this.schema}
|
||||
.computeLabel=${this.computeLabel}
|
||||
.computeHelper=${this.computeHelper}
|
||||
.data=${this.data}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-form>
|
||||
`}
|
||||
${this.hasError
|
||||
? html`
|
||||
<ha-alert alert-type="error">
|
||||
${this.hass.localize("ui.errors.config.error_detected")}:
|
||||
<br />
|
||||
<ul>
|
||||
${this._errors!.map((error) => html`<li>${error}</li>`)}
|
||||
</ul>
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
${this.hasWarning
|
||||
? html`
|
||||
<ha-alert
|
||||
alert-type="warning"
|
||||
.title="${this.hass.localize(
|
||||
"ui.errors.config.editor_not_supported"
|
||||
)}:"
|
||||
>
|
||||
${this._warnings!.length > 0 && this._warnings![0] !== undefined
|
||||
? html`
|
||||
<ul>
|
||||
${this._warnings!.map(
|
||||
(warning) => html`<li>${warning}</li>`
|
||||
)}
|
||||
</ul>
|
||||
`
|
||||
: nothing}
|
||||
${this.hass.localize("ui.errors.config.edit_in_yaml_supported")}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("data")) {
|
||||
if (this.assertConfig) {
|
||||
try {
|
||||
this.assertConfig(this.data);
|
||||
this._warnings = undefined;
|
||||
this._errors = undefined;
|
||||
} catch (err: any) {
|
||||
const msgs = handleStructError(this.hass, err);
|
||||
this._warnings = msgs.warnings ?? [err.message];
|
||||
this._errors = msgs.errors || undefined;
|
||||
this._yamlMode = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get hasWarning(): boolean {
|
||||
return this._warnings !== undefined && this._warnings.length > 0;
|
||||
}
|
||||
|
||||
public get hasError(): boolean {
|
||||
return this._errors !== undefined && this._errors.length > 0;
|
||||
}
|
||||
|
||||
private _goBack(): void {
|
||||
fireEvent(this, "go-back");
|
||||
}
|
||||
|
||||
private _toggleMode(): void {
|
||||
this._yamlMode = !this._yamlMode;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const value = (ev.detail.value ?? (ev.target as any).value ?? {}) as T;
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.back-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-sub-form-editor": HuiSubFormEditor;
|
||||
}
|
||||
}
|
30
src/panels/lovelace/editor/hui-typed-element-editor.ts
Normal file
30
src/panels/lovelace/editor/hui-typed-element-editor.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { state } from "lit/decorators";
|
||||
import { HuiElementEditor } from "./hui-element-editor";
|
||||
|
||||
export abstract class HuiTypedElementEditor<
|
||||
T extends object,
|
||||
C = any,
|
||||
> extends HuiElementEditor<T, C> {
|
||||
@state() private _configElementType?: string;
|
||||
|
||||
protected get configElementType(): string | undefined {
|
||||
return this.value ? (this.value as any).type : undefined;
|
||||
}
|
||||
|
||||
protected async loadConfigElement(): Promise<void> {
|
||||
// If the type has changed, we need to unload the current editor and load the new one
|
||||
if (this._configElementType !== this.configElementType) {
|
||||
this.unloadConfigElement();
|
||||
|
||||
if (!this.configElementType) {
|
||||
throw new Error(
|
||||
this.hass.localize("ui.errors.config.no_type_provided")
|
||||
);
|
||||
}
|
||||
|
||||
this._configElementType = this.configElementType;
|
||||
}
|
||||
|
||||
return super.loadConfigElement();
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import { LovelaceElementConfig } from "../../elements/types";
|
||||
import type { LovelacePictureElementEditor } from "../../types";
|
||||
import { HuiElementEditor } from "../hui-element-editor";
|
||||
import { HuiTypedElementEditor } from "../hui-typed-element-editor";
|
||||
import { getPictureElementClass } from "../../create-element/create-picture-element";
|
||||
|
||||
@customElement("hui-picture-element-element-editor")
|
||||
export class HuiPictureElementElementEditor extends HuiElementEditor<LovelaceElementConfig> {
|
||||
export class HuiPictureElementElementEditor extends HuiTypedElementEditor<LovelaceElementConfig> {
|
||||
protected get configElementType(): string | undefined {
|
||||
return this.value?.type === "action-button"
|
||||
? "service-button"
|
||||
|
@ -9,6 +9,7 @@ import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||
import { LovelaceCardFeatureConfig } from "../card-features/types";
|
||||
import { LovelaceElementConfig } from "../elements/types";
|
||||
import { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
import { HeadingEntityConfig } from "../cards/types";
|
||||
|
||||
export interface YamlChangedEvent extends Event {
|
||||
detail: {
|
||||
@ -95,19 +96,11 @@ export interface SubElementEditorConfig {
|
||||
| LovelaceRowConfig
|
||||
| LovelaceHeaderFooterConfig
|
||||
| LovelaceCardFeatureConfig
|
||||
| LovelaceElementConfig;
|
||||
type: "header" | "footer" | "row" | "feature" | "element";
|
||||
| LovelaceElementConfig
|
||||
| HeadingEntityConfig;
|
||||
type: "header" | "footer" | "row" | "feature" | "element" | "heading-entity";
|
||||
}
|
||||
|
||||
export interface EditSubElementEvent {
|
||||
subElementConfig: SubElementEditorConfig;
|
||||
}
|
||||
|
||||
export interface SubFormEditorData<T = any> {
|
||||
index?: number;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
export interface EditSubFormEvent<T = any> {
|
||||
subFormData: SubFormEditorData<T>;
|
||||
}
|
||||
|
@ -1796,11 +1796,13 @@
|
||||
},
|
||||
"errors": {
|
||||
"config": {
|
||||
"visual_editor_not_supported": "Visual editor not supported",
|
||||
"visual_editor_not_supported_reason_type": "The visual editor is not available for this type of element.",
|
||||
"edit_in_yaml_supported": "You can still edit your config using YAML.",
|
||||
"configuration_error": "Configuration error",
|
||||
"configuration_error_reason": "The configuration is not valid. Check the logs for more information.",
|
||||
"no_type_provided": "No type provided.",
|
||||
"error_detected": "Configuration errors detected",
|
||||
"editor_not_available": "No visual editor available for type ''{type}''.",
|
||||
"editor_not_supported": "Visual editor is not supported for this configuration",
|
||||
"edit_in_yaml_supported": "You can still edit your config in YAML.",
|
||||
"key_missing": "Required key ''{key}'' is missing.",
|
||||
"key_not_expected": "Key ''{key}'' is not expected or not supported by the visual editor.",
|
||||
"key_wrong_type": "The provided value for ''{key}'' is not supported by the visual editor. We support ({type_correct}) but received ({type_wrong}).",
|
||||
@ -6379,6 +6381,7 @@
|
||||
"row": "Entity row editor",
|
||||
"feature": "Feature editor",
|
||||
"element": "Element editor",
|
||||
"heading-entity": "Entity editor",
|
||||
"element_type": "{type} element editor"
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user