Card Editor: Special Row doesn't show warning (#7093)

This commit is contained in:
Zack Barett 2020-09-24 11:36:43 -05:00 committed by GitHub
parent 245c825cbf
commit a349e34bc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 398 additions and 19 deletions

View File

@ -24,7 +24,7 @@ import type {
import type { HomeAssistant } from "../../../../types";
import { handleStructError } from "../../common/structs/handle-errors";
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 { GUISupportError } from "../gui-support-error";
import type { GUIModeChangedEvent } from "../types";
@ -38,7 +38,7 @@ export interface ConfigChangedEvent {
declare global {
interface HASSDomEvents {
"entities-changed": {
entities: EntityConfig[];
entities: LovelaceRowConfig[];
};
"config-changed": ConfigChangedEvent;
"GUImode-changed": GUIModeChangedEvent;

View File

@ -4,26 +4,36 @@ import "@polymer/paper-listbox/paper-listbox";
import {
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import {
array,
assert,
boolean,
object,
optional,
string,
union,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
import "../../../../components/entity/state-badge";
import "../../../../components/ha-card";
import "../../../../components/ha-formfield";
import "../../../../components/ha-icon";
import "../../../../components/ha-switch";
import "../../../../components/ha-formfield";
import { HomeAssistant } from "../../../../types";
import {
EntitiesCardConfig,
EntitiesCardEntityConfig,
} from "../../cards/types";
import "../../components/hui-entity-editor";
import "../../components/hui-theme-select-editor";
import { headerFooterConfigStructs } from "../../header-footer/types";
import { LovelaceCardEditor } from "../../types";
import "../hui-entities-card-row-editor";
import { processEditorEntities } from "../process-editor-entities";
import {
EditorTarget,
@ -31,16 +41,6 @@ import {
EntitiesEditorEvent,
} from "../types";
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({
type: string(),
@ -127,11 +127,12 @@ export class HuiEntitiesCardEditor extends LitElement
</ha-formfield>
</div>
</div>
<hui-entity-editor
<hui-entities-card-row-editor
.hass=${this.hass}
.entities=${this._configEntities}
@entities-changed=${this._valueChanged}
></hui-entity-editor>
></hui-entities-card-row-editor>
`;
}

View 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;
}
}

View File

@ -1,4 +1,12 @@
import { boolean, object, optional, string, union } from "superstruct";
import {
any,
array,
boolean,
object,
optional,
string,
union,
} from "superstruct";
import {
ActionConfig,
LovelaceCardConfig,
@ -76,6 +84,86 @@ export const actionConfigStruct = 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([
object({
entity: EntityId,
@ -90,4 +178,13 @@ export const entitiesConfigStruct = union([
double_tap_action: optional(actionConfigStruct),
}),
EntityId,
buttonEntitiesRowConfigStruct,
castEntitiesRowConfigStruct,
conditionalEntitiesRowConfigStruct,
dividerEntitiesRowConfigStruct,
sectionEntitiesRowConfigStruct,
webLinkEntitiesRowConfigStruct,
buttonsEntitiesRowConfigStruct,
attributeEntitiesRowConfigStruct,
callServiceEntitiesRowConfigStruct,
]);

View File

@ -2266,7 +2266,20 @@
"name": "Entities",
"show_header_toggle": "Show Header Toggle?",
"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": {
"name": "Entity",