Config UI: dynamic default values, remove static HA instance default (#25525)
This commit is contained in:
parent
4fbd4c01a0
commit
65fce6e6b4
13 changed files with 200 additions and 9 deletions
|
|
@ -700,8 +700,20 @@ export default defineComponent({
|
|||
}
|
||||
this.serviceValuesTimer = setTimeout(async () => {
|
||||
this.serviceValues = await fetchServiceValues(this.templateParams, this.values);
|
||||
this.applyServiceDefaults();
|
||||
}, 500);
|
||||
},
|
||||
applyServiceDefaults() {
|
||||
// Auto-apply single service values when field is empty and required
|
||||
Object.keys(this.serviceValues).forEach((paramName) => {
|
||||
const values = this.serviceValues[paramName];
|
||||
const param = this.templateParams.find((p) => p.Name === paramName);
|
||||
// Only auto-apply if exactly one value is returned, field is empty, and field is required
|
||||
if (values?.length === 1 && !this.values[paramName] && param?.Required) {
|
||||
this.values[paramName] = values[0];
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ export function customChargerName(type: ConfigType, isHeating: boolean) {
|
|||
export async function loadServiceValues(path: string) {
|
||||
try {
|
||||
const response = await api.get(`/config/service/${path}`);
|
||||
return response.data as string[];
|
||||
return (response.data as string[]) || [];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -89,7 +89,13 @@
|
|||
:required="required"
|
||||
:autocomplete="masked || datalistId ? 'off' : null"
|
||||
/>
|
||||
<button v-if="showClearButton" type="button" class="form-control-clear" @click="value = ''">
|
||||
<button
|
||||
v-if="showClearButton"
|
||||
type="button"
|
||||
class="form-control-clear"
|
||||
:aria-label="$t('config.general.clear')"
|
||||
@click="value = ''"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<datalist v-if="showDatalist" :id="datalistId">
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import (
|
|||
"github.com/evcc-io/evcc/tariff"
|
||||
"github.com/evcc-io/evcc/util"
|
||||
"github.com/evcc-io/evcc/util/config"
|
||||
_ "github.com/evcc-io/evcc/util/demo"
|
||||
"github.com/evcc-io/evcc/util/locale"
|
||||
"github.com/evcc-io/evcc/util/machine"
|
||||
"github.com/evcc-io/evcc/util/request"
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@
|
|||
"authPerformHint": "Öffnet ein neues Tab. Anschließend hier weiter machen.",
|
||||
"authPrepare": "Verbindung vorbereiten",
|
||||
"cancel": "Abbrechen",
|
||||
"clear": "Löschen",
|
||||
"close": "Schließen",
|
||||
"customHelp": "Erstelle ein benutzerdefiniertes Gerät mit evcc's Plugin-System.",
|
||||
"customOption": "Benutzerdefiniertes Gerät",
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@
|
|||
"authPerformHint": "Will open in a new tab. Return here to continue.",
|
||||
"authPrepare": "Prepare connection",
|
||||
"cancel": "Cancel",
|
||||
"clear": "Clear",
|
||||
"close": "Close",
|
||||
"customHelp": "Create a user-defined device using evcc's plugin system.",
|
||||
"customOption": "User-defined device",
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ params:
|
|||
description:
|
||||
de: Home Assistant Instanz
|
||||
en: Home Assistant Instance
|
||||
default: Home
|
||||
service: homeassistant/homes
|
||||
required: true
|
||||
- name: switch
|
||||
description:
|
||||
de: Entity ID des schaltbaren Geräts
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ products:
|
|||
group: generic
|
||||
requirements:
|
||||
description:
|
||||
en: Requires a running Home Assistant instance with suitable sensor entities. All values are Home Assistant entity IDs (e.g. sensor.*)
|
||||
de: Erfordert eine laufende Home Assistant Instanz mit passenden Sensor-Entitäten. Alle Werte sind Home Assistant Entity IDs (z.B. sensor.*)
|
||||
en: Requires a running Home Assistant instance with suitable sensor entities. All values are Home Assistant entity IDs (e.g. `sensor.*`)
|
||||
de: Erfordert eine laufende Home Assistant Instanz mit passenden Sensor-Entitäten. Alle Werte sind Home Assistant Entity IDs (z.B. `sensor.*`)
|
||||
auth:
|
||||
type: homeassistant
|
||||
params: [home]
|
||||
|
|
@ -20,7 +20,6 @@ params:
|
|||
description:
|
||||
de: Home Assistant Instanz
|
||||
en: Home Assistant Instance
|
||||
default: Home
|
||||
service: homeassistant/homes
|
||||
required: true
|
||||
- name: power
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ products:
|
|||
group: generic
|
||||
requirements:
|
||||
description:
|
||||
en: Requires a running Home Assistant instance with suitable vehicle entities and services. All values are Home Assistant entity IDs (e.g. sensor.*, binary_sensor.*, number.*, script.*)
|
||||
de: Erfordert eine laufende Home Assistant Instanz mit passenden Fahrzeug-Entities und Services. Alle Werte sind Home Assistant Entity IDs (z.B. sensor.*, binary_sensor.*, number.*, script.*)
|
||||
en: Requires a running Home Assistant instance with suitable vehicle entities and services. All values are Home Assistant entity IDs (e.g. `sensor.*`, `binary_sensor.*`, `number.*`, `script.*`)
|
||||
de: Erfordert eine laufende Home Assistant Instanz mit passenden Fahrzeug-Entities und Services. Alle Werte sind Home Assistant Entity IDs (z.B. `sensor.*`, `binary_sensor.*`, `number.*`, `script.*`)
|
||||
auth:
|
||||
type: homeassistant
|
||||
params: [home]
|
||||
|
|
@ -19,7 +19,6 @@ params:
|
|||
description:
|
||||
de: Home Assistant Instanz
|
||||
en: Home Assistant Instance
|
||||
default: Home
|
||||
service: homeassistant/homes
|
||||
required: true
|
||||
- name: soc
|
||||
|
|
|
|||
28
tests/config-param-service-demo.tpl.yaml
Normal file
28
tests/config-param-service-demo.tpl.yaml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
template: service-demo
|
||||
group: generic
|
||||
products:
|
||||
- description:
|
||||
generic: Service Demo Meter
|
||||
params:
|
||||
- name: usage
|
||||
choice: ["grid"]
|
||||
- name: value
|
||||
description:
|
||||
generic: Important value
|
||||
required: true
|
||||
service: demo/single
|
||||
- name: other-value
|
||||
description:
|
||||
generic: Other value
|
||||
service: demo/single
|
||||
- name: country
|
||||
description:
|
||||
generic: Country
|
||||
service: demo/country
|
||||
- name: city
|
||||
description:
|
||||
generic: City
|
||||
service: demo/{country}/city
|
||||
|
||||
render: |
|
||||
type: custom
|
||||
96
tests/config-param-service.spec.ts
Normal file
96
tests/config-param-service.spec.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import type { Page } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
import { enableExperimental, expectModalVisible, getDatalistOptions } from "./utils";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
const templateFlags = [
|
||||
"--disable-auth",
|
||||
"--template-type",
|
||||
"meter",
|
||||
"--template",
|
||||
"tests/config-param-service-demo.tpl.yaml",
|
||||
];
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await start(undefined, undefined, templateFlags);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
async function openMeterModal(page: Page) {
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page, true);
|
||||
await page.getByRole("button", { name: "Add grid meter" }).click();
|
||||
const meterModal = page.getByTestId("meter-modal");
|
||||
await expectModalVisible(meterModal);
|
||||
await meterModal.getByLabel("Manufacturer").selectOption("Service Demo Meter");
|
||||
return meterModal;
|
||||
}
|
||||
|
||||
test.describe("config param service", async () => {
|
||||
test("autocomplete simple", async ({ page }) => {
|
||||
const meterModal = await openMeterModal(page);
|
||||
await expect(meterModal.getByLabel("Important value")).toHaveValue("demo-value");
|
||||
|
||||
const otherValue = meterModal.getByLabel("Other value");
|
||||
await expect(await getDatalistOptions(otherValue)).toEqual(["demo-value"]);
|
||||
|
||||
const country = meterModal.getByLabel("Country");
|
||||
await expect(await getDatalistOptions(country)).toEqual(["germany", "france", "spain"]);
|
||||
});
|
||||
|
||||
test("autocomplete dependent", async ({ page }) => {
|
||||
const meterModal = await openMeterModal(page);
|
||||
await expect(meterModal.getByLabel("Important value")).toHaveValue("demo-value");
|
||||
|
||||
const country = meterModal.getByLabel("Country");
|
||||
const city = meterModal.getByLabel("City");
|
||||
|
||||
// initially empty
|
||||
await expect(await getDatalistOptions(city)).toEqual([]);
|
||||
|
||||
await country.fill("germany");
|
||||
await expect(city).toHaveClass(/form-select/);
|
||||
await expect(await getDatalistOptions(city)).toEqual(["berlin", "munich", "hamburg"]);
|
||||
|
||||
await country.fill("");
|
||||
await expect(city).not.toHaveClass(/form-select/);
|
||||
await expect(await getDatalistOptions(city)).toEqual([]);
|
||||
|
||||
await country.fill("france");
|
||||
await expect(city).toHaveClass(/form-select/);
|
||||
await expect(await getDatalistOptions(city)).toEqual(["paris", "lyon", "marseille"]);
|
||||
|
||||
await country.fill("fantasy");
|
||||
await expect(city).not.toHaveClass(/form-select/);
|
||||
await expect(await getDatalistOptions(city)).toEqual([]);
|
||||
});
|
||||
|
||||
test("auto-apply single service value", async ({ page }) => {
|
||||
const meterModal = await openMeterModal(page);
|
||||
|
||||
// only required single-value field is auto-populated
|
||||
await expect(meterModal.getByLabel("Important value")).toHaveValue("demo-value");
|
||||
await expect(meterModal.getByLabel("Other value")).toHaveValue("");
|
||||
await expect(meterModal.getByLabel("Country")).toHaveValue("");
|
||||
await expect(meterModal.getByLabel("City")).toHaveValue("");
|
||||
});
|
||||
|
||||
test("clear button", async ({ page }) => {
|
||||
const meterModal = await openMeterModal(page);
|
||||
const valueField = meterModal.getByLabel("Important value");
|
||||
await expect(valueField).toHaveValue("demo-value");
|
||||
|
||||
const clearButton = valueField.locator("..").getByLabel("Clear");
|
||||
await expect(clearButton).toBeVisible();
|
||||
|
||||
// click clear button and verify it disappears and field is cleared
|
||||
await clearButton.click();
|
||||
await expect(clearButton).not.toBeVisible();
|
||||
await expect(valueField).toHaveValue("");
|
||||
});
|
||||
});
|
||||
|
|
@ -146,3 +146,12 @@ export async function dragElement(
|
|||
await page.mouse.up();
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDatalistOptions(input: Locator): Promise<string[]> {
|
||||
return input.evaluate((element: HTMLInputElement) => {
|
||||
const datalistId = element.getAttribute("list");
|
||||
if (!datalistId) return [];
|
||||
const datalist = document.getElementById(datalistId);
|
||||
return Array.from(datalist?.querySelectorAll("option") || []).map((opt) => opt.value);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
39
util/demo/service.go
Normal file
39
util/demo/service.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package demo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/evcc-io/evcc/server/service"
|
||||
)
|
||||
|
||||
func init() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /single", getSingle)
|
||||
mux.HandleFunc("GET /country", getCountry)
|
||||
mux.HandleFunc("GET /{country}/city", getCity)
|
||||
|
||||
service.Register("demo", mux)
|
||||
}
|
||||
|
||||
func getSingle(w http.ResponseWriter, req *http.Request) {
|
||||
json.NewEncoder(w).Encode([]string{"demo-value"})
|
||||
}
|
||||
|
||||
func getCountry(w http.ResponseWriter, req *http.Request) {
|
||||
json.NewEncoder(w).Encode([]string{"germany", "france", "spain"})
|
||||
}
|
||||
|
||||
func getCity(w http.ResponseWriter, req *http.Request) {
|
||||
country := req.PathValue("country")
|
||||
var cities []string
|
||||
switch country {
|
||||
case "germany":
|
||||
cities = []string{"berlin", "munich", "hamburg"}
|
||||
case "france":
|
||||
cities = []string{"paris", "lyon", "marseille"}
|
||||
case "spain":
|
||||
cities = []string{"madrid", "barcelona", "valencia"}
|
||||
}
|
||||
json.NewEncoder(w).Encode(cities)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue