mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-26 10:46:35 +00:00
Card Editor: Special Row doesn't show warning (#7093)
This commit is contained in:
parent
245c825cbf
commit
a349e34bc2
@ -24,7 +24,7 @@ import type {
|
|||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import { handleStructError } from "../../common/structs/handle-errors";
|
import { handleStructError } from "../../common/structs/handle-errors";
|
||||||
import { getCardElementClass } from "../../create-element/create-card-element";
|
import { getCardElementClass } from "../../create-element/create-card-element";
|
||||||
import type { EntityConfig } from "../../entity-rows/types";
|
import type { LovelaceRowConfig } from "../../entity-rows/types";
|
||||||
import type { LovelaceCardEditor } from "../../types";
|
import type { LovelaceCardEditor } from "../../types";
|
||||||
import { GUISupportError } from "../gui-support-error";
|
import { GUISupportError } from "../gui-support-error";
|
||||||
import type { GUIModeChangedEvent } from "../types";
|
import type { GUIModeChangedEvent } from "../types";
|
||||||
@ -38,7 +38,7 @@ export interface ConfigChangedEvent {
|
|||||||
declare global {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
"entities-changed": {
|
"entities-changed": {
|
||||||
entities: EntityConfig[];
|
entities: LovelaceRowConfig[];
|
||||||
};
|
};
|
||||||
"config-changed": ConfigChangedEvent;
|
"config-changed": ConfigChangedEvent;
|
||||||
"GUImode-changed": GUIModeChangedEvent;
|
"GUImode-changed": GUIModeChangedEvent;
|
||||||
|
@ -4,26 +4,36 @@ import "@polymer/paper-listbox/paper-listbox";
|
|||||||
import {
|
import {
|
||||||
customElement,
|
customElement,
|
||||||
html,
|
html,
|
||||||
|
internalProperty,
|
||||||
LitElement,
|
LitElement,
|
||||||
property,
|
property,
|
||||||
internalProperty,
|
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
|
import {
|
||||||
|
array,
|
||||||
|
assert,
|
||||||
|
boolean,
|
||||||
|
object,
|
||||||
|
optional,
|
||||||
|
string,
|
||||||
|
union,
|
||||||
|
} from "superstruct";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
|
||||||
import "../../../../components/entity/state-badge";
|
import "../../../../components/entity/state-badge";
|
||||||
import "../../../../components/ha-card";
|
import "../../../../components/ha-card";
|
||||||
|
import "../../../../components/ha-formfield";
|
||||||
import "../../../../components/ha-icon";
|
import "../../../../components/ha-icon";
|
||||||
import "../../../../components/ha-switch";
|
import "../../../../components/ha-switch";
|
||||||
import "../../../../components/ha-formfield";
|
|
||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant } from "../../../../types";
|
||||||
import {
|
import {
|
||||||
EntitiesCardConfig,
|
EntitiesCardConfig,
|
||||||
EntitiesCardEntityConfig,
|
EntitiesCardEntityConfig,
|
||||||
} from "../../cards/types";
|
} from "../../cards/types";
|
||||||
import "../../components/hui-entity-editor";
|
|
||||||
import "../../components/hui-theme-select-editor";
|
import "../../components/hui-theme-select-editor";
|
||||||
import { headerFooterConfigStructs } from "../../header-footer/types";
|
import { headerFooterConfigStructs } from "../../header-footer/types";
|
||||||
import { LovelaceCardEditor } from "../../types";
|
import { LovelaceCardEditor } from "../../types";
|
||||||
|
import "../hui-entities-card-row-editor";
|
||||||
import { processEditorEntities } from "../process-editor-entities";
|
import { processEditorEntities } from "../process-editor-entities";
|
||||||
import {
|
import {
|
||||||
EditorTarget,
|
EditorTarget,
|
||||||
@ -31,16 +41,6 @@ import {
|
|||||||
EntitiesEditorEvent,
|
EntitiesEditorEvent,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { configElementStyle } from "./config-elements-style";
|
import { configElementStyle } from "./config-elements-style";
|
||||||
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
|
|
||||||
import {
|
|
||||||
string,
|
|
||||||
optional,
|
|
||||||
object,
|
|
||||||
boolean,
|
|
||||||
array,
|
|
||||||
union,
|
|
||||||
assert,
|
|
||||||
} from "superstruct";
|
|
||||||
|
|
||||||
const cardConfigStruct = object({
|
const cardConfigStruct = object({
|
||||||
type: string(),
|
type: string(),
|
||||||
@ -127,11 +127,12 @@ export class HuiEntitiesCardEditor extends LitElement
|
|||||||
</ha-formfield>
|
</ha-formfield>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hui-entity-editor
|
|
||||||
|
<hui-entities-card-row-editor
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entities=${this._configEntities}
|
.entities=${this._configEntities}
|
||||||
@entities-changed=${this._valueChanged}
|
@entities-changed=${this._valueChanged}
|
||||||
></hui-entity-editor>
|
></hui-entities-card-row-editor>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
268
src/panels/lovelace/editor/hui-entities-card-row-editor.ts
Normal file
268
src/panels/lovelace/editor/hui-entities-card-row-editor.ts
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
import { mdiClose, mdiDrag } from "@mdi/js";
|
||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
internalProperty,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
import { guard } from "lit-html/directives/guard";
|
||||||
|
import type { SortableEvent } from "sortablejs";
|
||||||
|
import Sortable, {
|
||||||
|
AutoScroll,
|
||||||
|
OnSpill,
|
||||||
|
} from "sortablejs/modular/sortable.core.esm";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import "../../../components/entity/ha-entity-picker";
|
||||||
|
import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker";
|
||||||
|
import "../../../components/ha-icon-button";
|
||||||
|
import { sortableStyles } from "../../../resources/ha-sortable-style";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { EntityConfig, LovelaceRowConfig } from "../entity-rows/types";
|
||||||
|
|
||||||
|
@customElement("hui-entities-card-row-editor")
|
||||||
|
export class HuiEntitiesCardRowEditor extends LitElement {
|
||||||
|
@property({ attribute: false }) protected hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) protected entities?: LovelaceRowConfig[];
|
||||||
|
|
||||||
|
@property() protected label?: string;
|
||||||
|
|
||||||
|
@internalProperty() private _attached = false;
|
||||||
|
|
||||||
|
@internalProperty() private _renderEmptySortable = false;
|
||||||
|
|
||||||
|
private _sortable?: Sortable;
|
||||||
|
|
||||||
|
public connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._attached = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._attached = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this.entities || !this.hass) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<h3>
|
||||||
|
${this.label ||
|
||||||
|
`${this.hass!.localize(
|
||||||
|
"ui.panel.lovelace.editor.card.generic.entities"
|
||||||
|
)} (${this.hass!.localize(
|
||||||
|
"ui.panel.lovelace.editor.card.config.required"
|
||||||
|
)})`}
|
||||||
|
</h3>
|
||||||
|
<div class="entities">
|
||||||
|
${guard([this.entities, this._renderEmptySortable], () =>
|
||||||
|
this._renderEmptySortable
|
||||||
|
? ""
|
||||||
|
: this.entities!.map((entityConf, index) => {
|
||||||
|
return html`
|
||||||
|
<div class="entity">
|
||||||
|
<ha-svg-icon class="handle" .path=${mdiDrag}></ha-svg-icon>
|
||||||
|
${entityConf.type
|
||||||
|
? html`
|
||||||
|
<div class="special-row">
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
${this.hass!.localize(
|
||||||
|
`ui.panel.lovelace.editor.card.entities.entity_row.${entityConf.type}`
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span class="secondary"
|
||||||
|
>${this.hass!.localize(
|
||||||
|
"ui.panel.lovelace.editor.card.entities.edit_special_row"
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<mwc-icon-button
|
||||||
|
aria-label=${this.hass!.localize(
|
||||||
|
"ui.components.entity.entity-picker.clear"
|
||||||
|
)}
|
||||||
|
.index=${index}
|
||||||
|
@click=${this._removeSpecialRow}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||||
|
</mwc-icon-button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<ha-entity-picker
|
||||||
|
allow-custom-entity
|
||||||
|
.hass=${this.hass}
|
||||||
|
.value=${(entityConf as EntityConfig).entity}
|
||||||
|
.index=${index}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
></ha-entity-picker>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ha-entity-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
@value-changed=${this._addEntity}
|
||||||
|
></ha-entity-picker>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(): void {
|
||||||
|
Sortable.mount(OnSpill);
|
||||||
|
Sortable.mount(new AutoScroll());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps: PropertyValues): void {
|
||||||
|
super.updated(changedProps);
|
||||||
|
|
||||||
|
const attachedChanged = changedProps.has("_attached");
|
||||||
|
const entitiesChanged = changedProps.has("entities");
|
||||||
|
|
||||||
|
if (!entitiesChanged && !attachedChanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachedChanged && !this._attached) {
|
||||||
|
// Tear down sortable, if available
|
||||||
|
this._sortable?.destroy();
|
||||||
|
this._sortable = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._sortable && this.entities) {
|
||||||
|
this._createSortable();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entitiesChanged) {
|
||||||
|
this._handleEntitiesChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _handleEntitiesChanged() {
|
||||||
|
this._renderEmptySortable = true;
|
||||||
|
await this.updateComplete;
|
||||||
|
const container = this.shadowRoot!.querySelector(".entities")!;
|
||||||
|
while (container.lastElementChild) {
|
||||||
|
container.removeChild(container.lastElementChild);
|
||||||
|
}
|
||||||
|
this._renderEmptySortable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createSortable() {
|
||||||
|
this._sortable = new Sortable(this.shadowRoot!.querySelector(".entities"), {
|
||||||
|
animation: 150,
|
||||||
|
fallbackClass: "sortable-fallback",
|
||||||
|
handle: ".handle",
|
||||||
|
onEnd: async (evt: SortableEvent) => this._entityMoved(evt),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _addEntity(ev: CustomEvent): Promise<void> {
|
||||||
|
const value = ev.detail.value;
|
||||||
|
if (value === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newConfigEntities = this.entities!.concat({
|
||||||
|
entity: value as string,
|
||||||
|
});
|
||||||
|
(ev.target as HaEntityPicker).value = "";
|
||||||
|
fireEvent(this, "entities-changed", { entities: newConfigEntities });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _entityMoved(ev: SortableEvent): void {
|
||||||
|
if (ev.oldIndex === ev.newIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEntities = this.entities!.concat();
|
||||||
|
|
||||||
|
newEntities.splice(ev.newIndex!, 0, newEntities.splice(ev.oldIndex!, 1)[0]);
|
||||||
|
|
||||||
|
fireEvent(this, "entities-changed", { entities: newEntities });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _removeSpecialRow(ev: CustomEvent): void {
|
||||||
|
const index = (ev.currentTarget as any).index;
|
||||||
|
const newConfigEntities = this.entities!.concat();
|
||||||
|
|
||||||
|
newConfigEntities.splice(index, 1);
|
||||||
|
|
||||||
|
fireEvent(this, "entities-changed", { entities: newConfigEntities });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueChanged(ev: CustomEvent): void {
|
||||||
|
const value = ev.detail.value;
|
||||||
|
const index = (ev.target as any).index;
|
||||||
|
const newConfigEntities = this.entities!.concat();
|
||||||
|
|
||||||
|
if (value === "") {
|
||||||
|
newConfigEntities.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
newConfigEntities[index] = {
|
||||||
|
...newConfigEntities[index],
|
||||||
|
entity: value!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent(this, "entities-changed", { entities: newConfigEntities });
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
sortableStyles,
|
||||||
|
css`
|
||||||
|
.entity {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.entity .handle {
|
||||||
|
padding-right: 8px;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
.entity ha-entity-picker {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.special-row {
|
||||||
|
height: 60px;
|
||||||
|
font-size: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.special-row div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.special-row mwc-icon-button {
|
||||||
|
--mdc-icon-button-size: 36px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-entities-card-row-editor": HuiEntitiesCardRowEditor;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,12 @@
|
|||||||
import { boolean, object, optional, string, union } from "superstruct";
|
import {
|
||||||
|
any,
|
||||||
|
array,
|
||||||
|
boolean,
|
||||||
|
object,
|
||||||
|
optional,
|
||||||
|
string,
|
||||||
|
union,
|
||||||
|
} from "superstruct";
|
||||||
import {
|
import {
|
||||||
ActionConfig,
|
ActionConfig,
|
||||||
LovelaceCardConfig,
|
LovelaceCardConfig,
|
||||||
@ -76,6 +84,86 @@ export const actionConfigStruct = object({
|
|||||||
service_data: optional(object()),
|
service_data: optional(object()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const buttonEntitiesRowConfigStruct = object({
|
||||||
|
type: string(),
|
||||||
|
name: string(),
|
||||||
|
action_name: optional(string()),
|
||||||
|
tap_action: actionConfigStruct,
|
||||||
|
hold_action: optional(actionConfigStruct),
|
||||||
|
double_tap_action: optional(actionConfigStruct),
|
||||||
|
});
|
||||||
|
|
||||||
|
const castEntitiesRowConfigStruct = object({
|
||||||
|
type: string(),
|
||||||
|
view: string(),
|
||||||
|
dashboard: optional(string()),
|
||||||
|
name: optional(string()),
|
||||||
|
icon: optional(string()),
|
||||||
|
hide_if_unavailable: optional(string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const callServiceEntitiesRowConfigStruct = object({
|
||||||
|
type: string(),
|
||||||
|
name: string(),
|
||||||
|
icon: optional(string()),
|
||||||
|
action_name: optional(string()),
|
||||||
|
service: string(),
|
||||||
|
service_data: optional(any()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const conditionalEntitiesRowConfigStruct = object({
|
||||||
|
type: string(),
|
||||||
|
row: any(),
|
||||||
|
conditions: array(
|
||||||
|
object({
|
||||||
|
entity: string(),
|
||||||
|
state: optional(string()),
|
||||||
|
state_not: optional(string()),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const dividerEntitiesRowConfigStruct = object({
|
||||||
|
type: string(),
|
||||||
|
style: optional(any()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const sectionEntitiesRowConfigStruct = object({
|
||||||
|
type: string(),
|
||||||
|
label: optional(string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const webLinkEntitiesRowConfigStruct = object({
|
||||||
|
type: string(),
|
||||||
|
url: string(),
|
||||||
|
name: optional(string()),
|
||||||
|
icon: optional(string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttonsEntitiesRowConfigStruct = object({
|
||||||
|
type: string(),
|
||||||
|
entities: array(
|
||||||
|
union([
|
||||||
|
object({
|
||||||
|
entity: string(),
|
||||||
|
icon: optional(string()),
|
||||||
|
image: optional(string()),
|
||||||
|
name: optional(string()),
|
||||||
|
}),
|
||||||
|
EntityId,
|
||||||
|
])
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const attributeEntitiesRowConfigStruct = object({
|
||||||
|
type: string(),
|
||||||
|
entity: string(),
|
||||||
|
attribute: string(),
|
||||||
|
prefix: optional(string()),
|
||||||
|
suffix: optional(string()),
|
||||||
|
name: optional(string()),
|
||||||
|
});
|
||||||
|
|
||||||
export const entitiesConfigStruct = union([
|
export const entitiesConfigStruct = union([
|
||||||
object({
|
object({
|
||||||
entity: EntityId,
|
entity: EntityId,
|
||||||
@ -90,4 +178,13 @@ export const entitiesConfigStruct = union([
|
|||||||
double_tap_action: optional(actionConfigStruct),
|
double_tap_action: optional(actionConfigStruct),
|
||||||
}),
|
}),
|
||||||
EntityId,
|
EntityId,
|
||||||
|
buttonEntitiesRowConfigStruct,
|
||||||
|
castEntitiesRowConfigStruct,
|
||||||
|
conditionalEntitiesRowConfigStruct,
|
||||||
|
dividerEntitiesRowConfigStruct,
|
||||||
|
sectionEntitiesRowConfigStruct,
|
||||||
|
webLinkEntitiesRowConfigStruct,
|
||||||
|
buttonsEntitiesRowConfigStruct,
|
||||||
|
attributeEntitiesRowConfigStruct,
|
||||||
|
callServiceEntitiesRowConfigStruct,
|
||||||
]);
|
]);
|
||||||
|
@ -2266,7 +2266,20 @@
|
|||||||
"name": "Entities",
|
"name": "Entities",
|
||||||
"show_header_toggle": "Show Header Toggle?",
|
"show_header_toggle": "Show Header Toggle?",
|
||||||
"toggle": "Toggle entities.",
|
"toggle": "Toggle entities.",
|
||||||
"description": "The Entities card is the most common type of card. It groups items together into lists."
|
"description": "The Entities card is the most common type of card. It groups items together into lists.",
|
||||||
|
"special_row": "special row",
|
||||||
|
"edit_special_row": "Edit row using the code editor",
|
||||||
|
"entity_row": {
|
||||||
|
"divider": "Divider",
|
||||||
|
"call-service": "Call Service",
|
||||||
|
"section": "Section",
|
||||||
|
"weblink": "Web Link",
|
||||||
|
"attribute": "Attribute",
|
||||||
|
"buttons": "Buttons",
|
||||||
|
"conditional": "Conditional",
|
||||||
|
"cast": "Cast",
|
||||||
|
"button": "Button"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"name": "Entity",
|
"name": "Entity",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user