Config UI: improve create loadpoint (#27665)

This commit is contained in:
Michael Geers 2026-02-25 08:44:22 +01:00 • committed by GitHub
parent b816ae43c0
commit e4b93e14cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 546 additions and 469 deletions

View file

@ -80,6 +80,8 @@
--bs-danger: var(--evcc-red);
--bs-danger-rgb: var(--evcc-red-rgb);
--bs-form-invalid-border-color: var(--bs-danger);
--bs-body-font-size: 14px;
--bs-font-sans-serif:
Montserrat, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans",

View file

@ -2,8 +2,11 @@
<button
ref="tooltip"
type="button"
class="btn btn-sm btn-outline-secondary position-relative border-0 p-2 edit-button"
:class="{ 'opacity-25': !editable, invisible: noEditButton }"
class="btn btn-sm position-relative border-0 p-2 edit-button"
:class="[
danger ? 'btn-outline-danger' : 'btn-outline-secondary',
{ 'opacity-25': !editable, invisible: noEditButton },
]"
data-bs-toggle="tooltip"
data-bs-html="true"
:title="tooltipTitle"
@ -31,6 +34,7 @@ export default {
editable: Boolean,
noEditButton: Boolean,
badge: Boolean,
danger: Boolean,
},
emits: ["edit"],
data() {

View file

@ -0,0 +1,40 @@
<template>
<label
class="root d-flex align-items-center justify-content-between"
:class="[compact ? 'py-0 px-2' : 'py-2 px-3', { invalid: error }]"
@click="$emit('edit')"
>
<div class="flex-grow-1 text-truncate">
<slot>
<span>{{ title }}</span>
</slot>
</div>
<DeviceCardEditIcon :editable="true" :danger="error" @edit="$emit('edit')" />
</label>
</template>
<script lang="ts">
import DeviceCardEditIcon from "./DeviceCardEditIcon.vue";
export default {
name: "DeviceRefBox",
components: { DeviceCardEditIcon },
props: {
title: { type: String, default: "" },
error: { type: Boolean, default: false },
compact: { type: Boolean, default: false },
},
emits: ["edit"],
};
</script>
<style scoped>
.root {
border: var(--bs-border-width) solid var(--bs-border-color);
border-radius: var(--bs-border-radius);
cursor: pointer;
}
.root.invalid {
border-color: var(--bs-form-invalid-border-color);
}
</style>

File diff suppressed because it is too large Load diff

View file

@ -62,20 +62,13 @@
</div>
<div v-else>
<div v-for="(m, index) in messengers" :key="index" class="my-4">
<div
class="d-flex align-items-center justify-content-between py-2 px-4 border rounded"
<DeviceRefBox
:data-testid="`messenger-box-${index}`"
@edit="openMessenger(m.id)"
>
<div class="flex-grow-1">
<small class="text-muted">#{{ index + 1 }}</small>
<span class="fw-semibold mx-3">{{ messengerType(m) }}</span>
</div>
<DeviceCardEditIcon
:editable="true"
:no-edit-button="false"
@edit="openMessenger(m.id)"
/>
</div>
<small class="text-muted">#{{ index + 1 }}</small>
<span class="fw-semibold mx-3">{{ messengerType(m) }}</span>
</DeviceRefBox>
</div>
<button
type="button"
@ -97,7 +90,7 @@ import "@h2d2/shopicons/es/regular/plus";
import JsonModal from "../JsonModal.vue";
import EventItem from "./EventItem.vue";
import { type PropType } from "vue";
import DeviceCardEditIcon from "../DeviceCardEditIcon.vue";
import DeviceRefBox from "../DeviceRefBox.vue";
import { capitalize } from "./utils";
import { openModal } from "@/configModal";
@ -106,7 +99,7 @@ export default {
components: {
JsonModal,
EventItem,
DeviceCardEditIcon,
DeviceRefBox,
},
props: {
messengers: { type: Array as PropType<ConfigMessenger[]>, required: true },

View file

@ -53,7 +53,7 @@ export default defineComponent({
autofocus: { type: Boolean, default: true },
configModalName: String,
},
emits: ["open", "opened", "close", "closed", "visibilitychange"],
emits: ["open", "opened", "close", "closed", "dismiss", "visibilitychange"],
data() {
return {
isModalVisible: false,
@ -112,7 +112,9 @@ export default defineComponent({
this.$emit("closed");
this.isModalVisible = false;
if (this.configModalName) {
onModalHidden(this.configModalName);
if (onModalHidden(this.configModalName)) {
this.$emit("dismiss");
}
}
},
open() {

View file

@ -40,15 +40,16 @@ export function unregisterModal(name: string): void {
}
// Called by GenericModal on hidden.bs.modal (user ESC/backdrop)
export function onModalHidden(name: string): void {
export function onModalHidden(name: string): boolean {
if (_dismissingViaRoute.has(name)) {
_dismissingViaRoute.delete(name);
return;
return false;
}
// User dismissed via backdrop/ESC — sync route
if (configModal.stack.some((m) => m.name === name)) {
closeModal();
}
return true;
}
// Reactive fade direction for a named modal

View file

@ -329,6 +329,7 @@
:circuits="circuits"
:hasDeviceError="hasDeviceError"
@changed="loadpointChanged"
@dismissed="loadpointDismissed"
/>
<VehicleModal :is-sponsor="isSponsor" @vehicle-changed="vehicleChanged" />
<MeterModal :is-sponsor="isSponsor" @changed="meterChanged" />
@ -847,6 +848,11 @@ export default defineComponent({
await this.loadLoadpoints();
this.loadDirty();
},
async loadpointDismissed() {
await this.loadChargers();
await this.loadMeters();
this.updateValues();
},
vehicleChanged() {
this.loadVehicles();
this.loadDirty();

View file

@ -324,6 +324,8 @@
},
"defaultModeHelpKeep": "Zuletzt ausgewählter Modus wird beibehalten.",
"defaultModeLabel": "Standard-Modus",
"defaultsHint": "Standardmodus, PV-Überschussverhalten und elektrische Details verwenden sinnvolle Standardwerte.",
"defaultsHintLink": "Einstellungen anpassen",
"delete": "Löschen",
"electricalSubtitle": "Im Zweifelsfall Elektriker fragen.",
"electricalTitle": "Elektrisch",
@ -358,7 +360,6 @@
"priorityHelp": "Höherer Prioritäten haben Vorrang beim PV-Überschuss.",
"priorityLabel": "Priorität",
"save": "Speichern",
"showAllSettings": "Alle Einstellungen anzeigen",
"solarBehaviorCustomHelp": "Definiere eigene Ein- und Ausschaltschwellen und Verzögerungen.",
"solarBehaviorDefaultHelp": "Start nach {enableDelay} ausreichend vorhandenem Überschuss. Stop wenn für {disableDelay} nicht genug Überschuss vorhanden ist.",
"solarBehaviorLabel": "PV-Überschuss",

View file

@ -324,6 +324,8 @@
},
"defaultModeHelpKeep": "Keeps the last selected mode.",
"defaultModeLabel": "Default mode",
"defaultsHint": "Default mode, solar behaviour and electrical details use sensible defaults.",
"defaultsHintLink": "Adjust settings",
"delete": "Delete",
"electricalSubtitle": "When in doubt, ask your electrician.",
"electricalTitle": "Electrical",
@ -358,7 +360,6 @@
"priorityHelp": "Higher priority get preferred access to solar surplus.",
"priorityLabel": "Priority",
"save": "Save",
"showAllSettings": "Show all settings",
"solarBehaviorCustomHelp": "Define your own enable and disable thresholds and delays.",
"solarBehaviorDefaultHelp": "Start after {enableDelay} of sufficient surplus. Stop when there is not enough surplus for {disableDelay}.",
"solarBehaviorLabel": "Solar",

View file

@ -93,8 +93,8 @@ test.describe("fatal config handling", async () => {
// open modal and delete meter
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByRole("textbox", { name: "Energy meter" })).toHaveClass(/is-invalid/);
await lpModal.getByRole("textbox", { name: "Energy meter" }).click();
await expect(lpModal.getByText("Shelly 1PM")).toBeVisible();
await lpModal.getByText("Shelly 1PM").click();
await expectModalVisible(meterModal);
await meterModal.getByRole("button", { name: "Delete" }).click();
await expectModalHidden(meterModal);

View file

@ -98,10 +98,8 @@ test.describe("charging loadpoint", async () => {
await lpModal.getByTestId("chargerPower-22kw").click();
// update charger mode
await expect(lpModal.getByRole("textbox", { name: "Charger" })).toHaveValue(
"Demo charger [db:1]"
);
await lpModal.getByRole("textbox", { name: "Charger" }).click();
await expect(lpModal.getByText("Demo charger")).toBeVisible();
await lpModal.getByText("Demo charger").click();
await chargerModal.getByLabel("Charge status").selectOption("A");
await chargerModal.getByRole("button", { name: "Save" }).click();
await expectModalHidden(chargerModal);
@ -341,7 +339,7 @@ test.describe("charging loadpoint", async () => {
// delete charger
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await lpModal.getByRole("textbox", { name: "Charger" }).click();
await lpModal.getByText("Demo charger").click();
const chargerModal = page.getByTestId("charger-modal");
await expectModalVisible(chargerModal);
await chargerModal.getByRole("button", { name: "Delete" }).click();
@ -361,7 +359,7 @@ test.describe("charging loadpoint", async () => {
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await lpModal.getByRole("button", { name: "Add charging point" }).click();
await expect(lpModal.getByRole("textbox", { name: "Title" })).toHaveValue("Garage");
await expect(lpModal.getByLabel("Title")).toHaveValue("Garage");
await expect(lpModal).toContainText("Configuring a charger is required.");
});
@ -381,7 +379,7 @@ test.describe("charging loadpoint", async () => {
// delete charger
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await lpModal.getByRole("textbox", { name: "Meter" }).click();
await lpModal.getByText("Demo meter").click();
const meterModal = page.getByTestId("meter-modal");
await expectModalVisible(meterModal);
await meterModal.getByRole("button", { name: "Delete" }).click();
@ -398,7 +396,7 @@ test.describe("charging loadpoint", async () => {
await expect(page.getByTestId("loadpoint")).not.toContainText("11.0 kW");
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByRole("textbox", { name: "Title" })).toHaveValue("Garage");
await expect(lpModal.getByLabel("Title")).toHaveValue("Garage");
await expect(lpModal.getByRole("button", { name: "Add dedicated energy meter" })).toBeVisible();
});
@ -499,11 +497,9 @@ power:
await lpEntry.getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await expect(lpModal.getByLabel("Charger").first()).toHaveValue("User-defined charger [db:1]");
await expect(lpModal.getByLabel("Energy meter").first()).toHaveValue(
"User-defined device [db:2]"
);
await lpModal.getByLabel("Charger").first().click();
await expect(lpModal.getByText("User-defined charger")).toBeVisible();
await expect(lpModal.getByText("User-defined device")).toBeVisible();
await lpModal.getByText("User-defined charger").click();
await expectModalVisible(chargerModal);
await expect(chargerModal.getByLabel("Manufacturer")).toHaveValue("User-defined charger");
@ -644,7 +640,7 @@ temp:
// delete heater
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expectModalVisible(lpModal);
await lpModal.getByRole("textbox", { name: "Heater" }).click();
await lpModal.getByText("Demo heat pump").click();
const chargerModal = page.getByTestId("charger-modal");
await expectModalVisible(chargerModal);
await chargerModal.getByRole("button", { name: "Delete" }).click();