Config UI: persist product name, keep device type (#19808)

This commit is contained in:
Michael Geers 2025-03-15 14:59:40 +01:00 • committed by GitHub
parent 3b30b2773a
commit 6a4ec7335e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 90 additions and 29 deletions

View file

@ -10,9 +10,10 @@
<form ref="form" class="container mx-0 px-0">
<FormRow id="chargerTemplate" :label="$t('config.charger.template')">
<select
v-if="isNew"
id="chargerTemplate"
ref="templateSelect"
v-model="templateName"
:disabled="!isNew"
class="form-select w-100"
@change="templateChanged"
>
@ -45,6 +46,13 @@
</option>
</optgroup>
</select>
<input
v-else
type="text"
:value="productName"
disabled
class="form-control w-100"
/>
</FormRow>
<p v-if="loadingTemplate">Loading ...</p>
<SponsorTokenRequired v-if="sponsorTokenRequired" />
@ -250,6 +258,9 @@ export default {
description() {
return this.template?.Requirements?.Description;
},
productName() {
return this.values.deviceProduct || this.templateName;
},
sponsorTokenRequired() {
const list = this.template?.Requirements?.EVCC || [];
return list.includes("sponsorship") && !this.isSponsor;
@ -298,6 +309,10 @@ export default {
try {
const charger = (await api.get(`config/devices/charger/${this.id}`)).data.result;
this.values = charger.config;
// convert structure to flat list
// TODO: adjust GET response to match POST/PUT formats
this.values.type = charger.type;
this.values.deviceProduct = charger.deviceProduct;
this.applyDefaultsFromTemplate();
this.templateName = this.values.template;
} catch (e) {
@ -317,6 +332,7 @@ export default {
},
async loadTemplate() {
this.template = null;
if (!this.templateName) return;
this.loadingTemplate = true;
try {
const opts = {
@ -342,6 +358,13 @@ export default {
});
},
async create() {
// persist selected template product
if (this.template) {
const select = this.$refs.templateSelect;
const name = select.options[select.selectedIndex].text;
this.values.deviceProduct = name;
}
if (this.testUnknown) {
const success = await this.test(this.testCharger);
if (!success) return;

View file

@ -632,7 +632,7 @@ export default {
},
chargerTitle() {
if (!this.charger) return "";
const title = this.charger.config?.template || "unknown";
const title = this.charger.deviceProduct || this.charger.config?.template || "unknown";
return `${title} [${this.values.charger}]`;
},
chargerStatus() {
@ -651,7 +651,7 @@ export default {
const name = this.values.meter;
if (!name) return "";
const meter = this.meters.find((m) => m.name === name);
const title = meter?.config?.template || "unknown";
const title = meter?.deviceProduct || meter?.config?.template || "unknown";
return `${title} [${name}]`;
},
isDeletable() {

View file

@ -39,6 +39,7 @@
<select
v-if="isNew"
id="meterTemplate"
ref="templateSelect"
v-model="templateName"
class="form-select w-100"
@change="templateChanged"
@ -377,6 +378,13 @@ export default {
});
},
async create() {
// persist selected template product
if (this.template) {
const select = this.$refs.templateSelect;
const name = select.options[select.selectedIndex].text;
this.values.deviceProduct = name;
}
if (this.testUnknown) {
const success = await this.test(this.testMeter);
if (!success) return;
@ -390,8 +398,7 @@ export default {
this.$emit("updated");
this.close();
} catch (e) {
console.error(e);
alert("create failed");
this.handleCreateError(e);
}
this.saving = false;
},
@ -417,8 +424,7 @@ export default {
this.$emit("updated");
this.close();
} catch (e) {
console.error(e);
alert("update failed");
this.handleUpdateError(e);
}
this.saving = false;
},
@ -429,8 +435,7 @@ export default {
this.$emit("updated");
this.close();
} catch (e) {
console.error(e);
alert("delete failed");
this.handleRemoveError(e);
}
},
open() {
@ -443,11 +448,8 @@ export default {
selectType(type) {
this.selectedType = type;
},
templateChanged(event) {
templateChanged() {
this.reset(true);
const select = event.target;
const name = select.options[select.selectedIndex].text;
this.values.deviceProduct = name;
},
},
};

View file

@ -9,14 +9,15 @@
<form ref="form" class="container mx-0 px-0">
<FormRow id="vehicleTemplate" :label="$t('config.vehicle.template')">
<select
v-if="isNew"
id="vehicleTemplate"
ref="templateSelect"
v-model="templateName"
:disabled="!isNew"
class="form-select w-100"
@change="templateChanged"
>
<option value="offline">
{{ $t("config.vehicle.offline") }}
<option :value="templateOptions.offline.template">
{{ templateOptions.offline.name }}
</option>
<option disabled>----------</option>
<optgroup :label="$t('config.vehicle.online')">
@ -47,6 +48,13 @@
</option>
</optgroup>
</select>
<input
v-else
type="text"
:value="productName"
disabled
class="form-control w-100"
/>
</FormRow>
<p v-if="loadingTemplate">Loading ...</p>
<Markdown v-if="description" :markdown="description" class="my-4" />
@ -273,11 +281,11 @@ export default {
isModalVisible: false,
templates: [],
products: [],
saving: false,
templateName: null,
template: null,
values: { ...initialValues },
saving: false,
loadingTemplate: false,
values: { ...initialValues },
};
},
computed: {
@ -286,6 +294,7 @@ export default {
online: this.products.filter((p) => !p.group && p.template !== "offline"),
generic: this.products.filter((p) => p.group === "generic"),
scooter: this.products.filter((p) => p.group === "scooter"),
offline: this.products.find((p) => p.template === "offline") || {},
};
},
templateParams() {
@ -316,6 +325,9 @@ export default {
description() {
return this.template?.Requirements?.Description;
},
productName() {
return this.values.deviceProduct || this.templateName;
},
apiData() {
const data = {
template: this.templateName,
@ -372,6 +384,10 @@ export default {
try {
const vehicle = (await api.get(`config/devices/vehicle/${this.id}`)).data.result;
this.values = vehicle.config;
// convert structure to flat list
// TODO: adjust GET response to match POST/PUT formats
this.values.type = vehicle.type;
this.values.deviceProduct = vehicle.deviceProduct;
this.applyDefaultsFromTemplate();
this.templateName = this.values.template;
} catch (e) {
@ -379,6 +395,9 @@ export default {
}
},
async loadProducts() {
if (!this.isModalVisible) {
return;
}
try {
const opts = { params: { lang: this.$i18n?.locale } };
this.products = (await api.get("config/products/vehicle", opts)).data.result;
@ -388,6 +407,7 @@ export default {
},
async loadTemplate() {
this.template = null;
if (!this.templateName) return;
this.loadingTemplate = true;
try {
const opts = {
@ -396,7 +416,8 @@ export default {
name: this.templateName,
},
};
this.template = (await api.get("config/templates/vehicle", opts)).data.result;
const result = await api.get("config/templates/vehicle", opts);
this.template = result.data.result;
this.applyDefaultsFromTemplate();
} catch (e) {
console.error(e);
@ -412,10 +433,17 @@ export default {
});
},
async create() {
// persist selected template product
if (this.template) {
const select = this.$refs.templateSelect;
const name = select.options[select.selectedIndex].text;
this.values.deviceProduct = name;
}
if (this.testUnknown) {
const success = await this.test(this.testVehicle);
if (!success) return;
await sleep(250);
await sleep(100);
}
this.saving = true;
try {
@ -423,8 +451,7 @@ export default {
this.$emit("vehicle-changed");
this.closed();
} catch (e) {
console.error(e);
alert("create failed");
this.handleCreateError(e);
}
this.saving = false;
},
@ -450,8 +477,7 @@ export default {
this.$emit("vehicle-changed");
this.closed();
} catch (e) {
console.error(e);
alert("update failed");
this.handleUpdateError(e);
}
this.saving = false;
},
@ -461,8 +487,7 @@ export default {
this.$emit("vehicle-changed");
this.closed();
} catch (e) {
console.error(e);
alert("delete failed");
this.handleRemoveError(e);
}
},
open() {

View file

@ -357,7 +357,6 @@ validate = "prüfen"
cancel = "Abbrechen"
delete = "Fahrzeug löschen"
generic = "Weitere Integrationen"
offline = "Generisches Fahrzeug"
online = "Fahrzeuge mit Schnittstelle"
save = "Speichern"
scooter = "Elektroroller"

View file

@ -356,7 +356,6 @@ validate = "validate"
cancel = "Cancel"
delete = "Delete"
generic = "Other integrations"
offline = "Generic vehicle"
online = "Vehicles with online API"
save = "Save"
scooter = "Scooter"

View file

@ -1,7 +1,8 @@
template: offline
products:
- description:
generic: Offline
en: Generic vehicle
de: Generisches Fahrzeug
group: generic
params:
- preset: vehicle-common

View file

@ -104,6 +104,16 @@ test.describe("loadpoint", async () => {
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expect(lpModal).toBeVisible();
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 chargerModal.getByLabel("Charge status").selectOption("A");
await chargerModal.getByRole("button", { name: "Save" }).click();
await expect(chargerModal).not.toBeVisible();
await lpModal.getByRole("button", { name: "Save" }).click();
await expect(lpModal).not.toBeVisible();
@ -111,6 +121,8 @@ test.describe("loadpoint", async () => {
await restart(CONFIG_EMPTY);
await page.reload();
await expect(page.getByTestId("loadpoint")).toHaveCount(1);
await expect(page.getByTestId("loadpoint")).toContainText("not connected");
await page.getByTestId("loadpoint").getByRole("button", { name: "edit" }).click();
await expect(lpModal).toBeVisible();
await expect(lpModal.getByTestId("chargerPower-22kw")).toHaveClass(/active/);