Config UI: improve create loadpoint (#27665)
This commit is contained in:
parent
b816ae43c0
commit
e4b93e14cc
12 changed files with 546 additions and 469 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
40
assets/js/components/Config/DeviceRefBox.vue
Normal file
40
assets/js/components/Config/DeviceRefBox.vue
Normal 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
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue