UI: make loadpoints sortable and hideable (#23621)
Co-authored-by: Michael Geers <michael@geers.tv>
This commit is contained in:
parent
87d66ed98c
commit
5617c19bb9
35 changed files with 908 additions and 224 deletions
|
|
@ -198,7 +198,9 @@ This file provides guidance to AI coding agents when working with code in this r
|
||||||
- **Examples**:
|
- **Examples**:
|
||||||
- `page.getByRole("button", { name: "Add charger" })`
|
- `page.getByRole("button", { name: "Add charger" })`
|
||||||
- `page.getByLabel("Manufacturer").selectOption("Demo charger")`
|
- `page.getByLabel("Manufacturer").selectOption("Demo charger")`
|
||||||
|
- `page.getByRole("listitem", { name: "Draggable: First Loadpoint" })` (using aria-label)
|
||||||
- `page.getByTestId("loadpoint")` (fallback only)
|
- `page.getByTestId("loadpoint")` (fallback only)
|
||||||
|
- never use `.locator()` or `class` and `id`-based selectors
|
||||||
|
|
||||||
### Test Patterns
|
### Test Patterns
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -296,6 +296,11 @@ a:hover {
|
||||||
outline-width: var(--bs-focus-ring-width);
|
outline-width: var(--bs-focus-ring-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-link:disabled {
|
||||||
|
border-color: transparent !important;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
.dark .btn-outline-secondary {
|
.dark .btn-outline-secondary {
|
||||||
--bs-btn-color: var(--bs-gray-bright);
|
--bs-btn-color: var(--bs-gray-bright);
|
||||||
--bs-btn-border-color: var(--bs-gray-bright);
|
--bs-btn-border-color: var(--bs-gray-bright);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
test-id="generalconfig-telemetry"
|
test-id="generalconfig-telemetry"
|
||||||
:label="$t('config.general.telemetry')"
|
:label="$t('config.general.telemetry')"
|
||||||
:text="$t(`config.general.${telemetryEnabled ? 'on' : 'off'}`)"
|
:text="$t(`config.general.${telemetryEnabled ? 'on' : 'off'}`)"
|
||||||
modal-id="globalSettingsModal"
|
modal-id="telemetryModal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<GeneralConfigEntry
|
<GeneralConfigEntry
|
||||||
|
|
|
||||||
27
assets/js/components/Config/TelemetryModal.vue
Normal file
27
assets/js/components/Config/TelemetryModal.vue
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<template>
|
||||||
|
<GenericModal
|
||||||
|
id="telemetryModal"
|
||||||
|
:title="$t('config.telemetry.title')"
|
||||||
|
size="lg"
|
||||||
|
data-testid="telemetry-modal"
|
||||||
|
>
|
||||||
|
<p>{{ $t("config.telemetry.description") }}</p>
|
||||||
|
<TelemetrySettings :sponsorActive="sponsor && !!sponsor.name" :telemetry="telemetry" />
|
||||||
|
</GenericModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
import GenericModal from "../Helper/GenericModal.vue";
|
||||||
|
import TelemetrySettings from "../TelemetrySettings.vue";
|
||||||
|
import type { Sponsor } from "../../types/evcc";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "TelemetryModal",
|
||||||
|
components: { GenericModal, TelemetrySettings },
|
||||||
|
props: {
|
||||||
|
sponsor: { type: Object as PropType<Sponsor> },
|
||||||
|
telemetry: { type: Boolean },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -7,7 +7,7 @@ export default {
|
||||||
component: Energyflow,
|
component: Energyflow,
|
||||||
} as Meta<typeof Energyflow>;
|
} as Meta<typeof Energyflow>;
|
||||||
|
|
||||||
const Template: StoryFn<typeof Energyflow> = (args) => ({
|
const Template: StoryFn<typeof Energyflow> = (args: any) => ({
|
||||||
components: { Energyflow },
|
components: { Energyflow },
|
||||||
setup() {
|
setup() {
|
||||||
return { args };
|
return { args };
|
||||||
|
|
@ -22,39 +22,33 @@ GridAndPV.args = {
|
||||||
pvPower: 7300,
|
pvPower: 7300,
|
||||||
gridPower: -2300,
|
gridPower: -2300,
|
||||||
homePower: 800,
|
homePower: 800,
|
||||||
loadpointsCompact: [
|
loadpoints: [
|
||||||
{
|
{
|
||||||
power: 1000,
|
|
||||||
icon: "car",
|
icon: "car",
|
||||||
charging: true,
|
charging: true,
|
||||||
title: "Garage",
|
title: "Garage",
|
||||||
chargePower: 1000,
|
chargePower: 1000,
|
||||||
connected: true,
|
connected: true,
|
||||||
index: 0,
|
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicleSoc: 50,
|
vehicleSoc: 50,
|
||||||
chargerFeatureHeating: false,
|
chargerFeatureHeating: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
power: 1000,
|
|
||||||
icon: "bike",
|
icon: "bike",
|
||||||
charging: true,
|
charging: true,
|
||||||
title: "Garage",
|
title: "Garage",
|
||||||
chargePower: 1000,
|
chargePower: 1000,
|
||||||
connected: true,
|
connected: true,
|
||||||
index: 1,
|
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicleSoc: 50,
|
vehicleSoc: 50,
|
||||||
chargerFeatureHeating: false,
|
chargerFeatureHeating: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
power: 2200,
|
|
||||||
icon: "car",
|
icon: "car",
|
||||||
charging: true,
|
charging: true,
|
||||||
title: "Garage",
|
title: "Garage",
|
||||||
chargePower: 2200,
|
chargePower: 2200,
|
||||||
connected: true,
|
connected: true,
|
||||||
index: 2,
|
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicleSoc: 50,
|
vehicleSoc: 50,
|
||||||
chargerFeatureHeating: false,
|
chargerFeatureHeating: false,
|
||||||
|
|
@ -72,7 +66,7 @@ GridAndPV.args = {
|
||||||
dayAfterTomorrow: { energy: 1000, complete: false },
|
dayAfterTomorrow: { energy: 1000, complete: false },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
export const BatteryAndGrid = Template.bind({});
|
export const BatteryAndGrid = Template.bind({});
|
||||||
BatteryAndGrid.args = {
|
BatteryAndGrid.args = {
|
||||||
|
|
@ -91,7 +85,7 @@ BatteryAndGrid.args = {
|
||||||
{ soc: 44.999, capacity: 13.3, power: 350, controllable: true },
|
{ soc: 44.999, capacity: 13.3, power: 350, controllable: true },
|
||||||
{ soc: 82.3331, capacity: 21, power: 450, controllable: false },
|
{ soc: 82.3331, capacity: 21, power: 450, controllable: false },
|
||||||
],
|
],
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
export const BatteryCharging = Template.bind({});
|
export const BatteryCharging = Template.bind({});
|
||||||
BatteryCharging.args = {
|
BatteryCharging.args = {
|
||||||
|
|
@ -101,15 +95,13 @@ BatteryCharging.args = {
|
||||||
pvPower: 5000,
|
pvPower: 5000,
|
||||||
gridPower: -1300,
|
gridPower: -1300,
|
||||||
homePower: 800,
|
homePower: 800,
|
||||||
loadpointsCompact: [
|
loadpoints: [
|
||||||
{
|
{
|
||||||
power: 1400,
|
|
||||||
icon: "car",
|
icon: "car",
|
||||||
charging: true,
|
charging: true,
|
||||||
title: "Garage",
|
title: "Garage",
|
||||||
chargePower: 1400,
|
chargePower: 1400,
|
||||||
connected: true,
|
connected: true,
|
||||||
index: 0,
|
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicleSoc: 50,
|
vehicleSoc: 50,
|
||||||
chargerFeatureHeating: false,
|
chargerFeatureHeating: false,
|
||||||
|
|
@ -117,7 +109,7 @@ BatteryCharging.args = {
|
||||||
],
|
],
|
||||||
batteryPower: -1500,
|
batteryPower: -1500,
|
||||||
batterySoc: 75,
|
batterySoc: 75,
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
export const GridPVAndBattery = Template.bind({});
|
export const GridPVAndBattery = Template.bind({});
|
||||||
GridPVAndBattery.args = {
|
GridPVAndBattery.args = {
|
||||||
|
|
@ -129,7 +121,7 @@ GridPVAndBattery.args = {
|
||||||
homePower: 3300,
|
homePower: 3300,
|
||||||
batteryPower: 1500,
|
batteryPower: 1500,
|
||||||
batterySoc: 30,
|
batterySoc: 30,
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
export const BatteryThresholds = Template.bind({});
|
export const BatteryThresholds = Template.bind({});
|
||||||
BatteryThresholds.args = {
|
BatteryThresholds.args = {
|
||||||
|
|
@ -138,27 +130,23 @@ BatteryThresholds.args = {
|
||||||
batteryConfigured: true,
|
batteryConfigured: true,
|
||||||
pvPower: 8700,
|
pvPower: 8700,
|
||||||
gridPower: -500,
|
gridPower: -500,
|
||||||
loadpointsCompact: [
|
loadpoints: [
|
||||||
{
|
{
|
||||||
power: 5000,
|
|
||||||
icon: "car",
|
icon: "car",
|
||||||
charging: true,
|
charging: true,
|
||||||
title: "Garage",
|
title: "Garage",
|
||||||
chargePower: 5000,
|
chargePower: 5000,
|
||||||
connected: true,
|
connected: true,
|
||||||
index: 0,
|
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicleSoc: 50,
|
vehicleSoc: 50,
|
||||||
chargerFeatureHeating: false,
|
chargerFeatureHeating: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
power: 2500,
|
|
||||||
icon: "bus",
|
icon: "bus",
|
||||||
charging: true,
|
charging: true,
|
||||||
title: "Garage",
|
title: "Garage",
|
||||||
chargePower: 2500,
|
chargePower: 2500,
|
||||||
connected: true,
|
connected: true,
|
||||||
index: 1,
|
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicleSoc: 50,
|
vehicleSoc: 50,
|
||||||
chargerFeatureHeating: false,
|
chargerFeatureHeating: false,
|
||||||
|
|
@ -166,7 +154,7 @@ BatteryThresholds.args = {
|
||||||
],
|
],
|
||||||
batteryPower: -700,
|
batteryPower: -700,
|
||||||
batterySoc: 95,
|
batterySoc: 95,
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
export const PVThresholds = Template.bind({});
|
export const PVThresholds = Template.bind({});
|
||||||
PVThresholds.args = {
|
PVThresholds.args = {
|
||||||
|
|
@ -176,27 +164,23 @@ PVThresholds.args = {
|
||||||
pvPower: 300,
|
pvPower: 300,
|
||||||
gridPower: 6500,
|
gridPower: 6500,
|
||||||
homePower: 1000,
|
homePower: 1000,
|
||||||
loadpointsCompact: [
|
loadpoints: [
|
||||||
{
|
{
|
||||||
power: 5000,
|
|
||||||
icon: "car",
|
icon: "car",
|
||||||
charging: true,
|
charging: true,
|
||||||
title: "Garage",
|
title: "Garage",
|
||||||
chargePower: 5000,
|
chargePower: 5000,
|
||||||
connected: true,
|
connected: true,
|
||||||
index: 0,
|
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicleSoc: 50,
|
vehicleSoc: 50,
|
||||||
chargerFeatureHeating: false,
|
chargerFeatureHeating: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
power: 1600,
|
|
||||||
icon: "car",
|
icon: "car",
|
||||||
charging: true,
|
charging: true,
|
||||||
title: "Garage",
|
title: "Garage",
|
||||||
chargePower: 1600,
|
chargePower: 1600,
|
||||||
connected: true,
|
connected: true,
|
||||||
index: 1,
|
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicleSoc: 50,
|
vehicleSoc: 50,
|
||||||
chargerFeatureHeating: false,
|
chargerFeatureHeating: false,
|
||||||
|
|
@ -204,7 +188,7 @@ PVThresholds.args = {
|
||||||
],
|
],
|
||||||
batteryPower: 800,
|
batteryPower: 800,
|
||||||
batterySoc: 76,
|
batterySoc: 76,
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
export const GridOnly = Template.bind({});
|
export const GridOnly = Template.bind({});
|
||||||
GridOnly.args = {
|
GridOnly.args = {
|
||||||
|
|
@ -214,51 +198,43 @@ GridOnly.args = {
|
||||||
pvPower: 0,
|
pvPower: 0,
|
||||||
gridPower: 6500,
|
gridPower: 6500,
|
||||||
homePower: 1000,
|
homePower: 1000,
|
||||||
loadpointsCompact: [
|
loadpoints: [
|
||||||
{
|
{
|
||||||
power: 5500,
|
|
||||||
icon: "car",
|
icon: "car",
|
||||||
charging: true,
|
charging: true,
|
||||||
title: "Garage",
|
title: "Garage",
|
||||||
chargePower: 5500,
|
chargePower: 5500,
|
||||||
connected: true,
|
connected: true,
|
||||||
index: 0,
|
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicleSoc: 50,
|
vehicleSoc: 50,
|
||||||
chargerFeatureHeating: false,
|
chargerFeatureHeating: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
power: 0,
|
|
||||||
icon: "car",
|
icon: "car",
|
||||||
charging: false,
|
charging: false,
|
||||||
title: "Garage",
|
title: "Garage",
|
||||||
chargePower: 0,
|
chargePower: 0,
|
||||||
connected: false,
|
connected: false,
|
||||||
index: 1,
|
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicleSoc: 50,
|
vehicleSoc: 50,
|
||||||
chargerFeatureHeating: false,
|
chargerFeatureHeating: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
power: 0,
|
|
||||||
icon: "car",
|
icon: "car",
|
||||||
charging: false,
|
charging: false,
|
||||||
title: "Garage",
|
title: "Garage",
|
||||||
chargePower: 0,
|
chargePower: 0,
|
||||||
connected: false,
|
connected: false,
|
||||||
index: 2,
|
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicleSoc: 50,
|
vehicleSoc: 50,
|
||||||
chargerFeatureHeating: false,
|
chargerFeatureHeating: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
power: 0,
|
|
||||||
icon: "car",
|
icon: "car",
|
||||||
charging: false,
|
charging: false,
|
||||||
title: "Garage",
|
title: "Garage",
|
||||||
chargePower: 0,
|
chargePower: 0,
|
||||||
connected: false,
|
connected: false,
|
||||||
index: 3,
|
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicleSoc: 50,
|
vehicleSoc: 50,
|
||||||
chargerFeatureHeating: false,
|
chargerFeatureHeating: false,
|
||||||
|
|
@ -266,7 +242,7 @@ GridOnly.args = {
|
||||||
],
|
],
|
||||||
batteryPower: 0,
|
batteryPower: 0,
|
||||||
batterySoc: 0,
|
batterySoc: 0,
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
export const LowPower = Template.bind({});
|
export const LowPower = Template.bind({});
|
||||||
LowPower.args = {
|
LowPower.args = {
|
||||||
|
|
@ -281,7 +257,7 @@ LowPower.args = {
|
||||||
tariffGrid: 0.25,
|
tariffGrid: 0.25,
|
||||||
tariffFeedIn: 0.08,
|
tariffFeedIn: 0.08,
|
||||||
currency: CURRENCY.EUR,
|
currency: CURRENCY.EUR,
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
export const CO2 = Template.bind({});
|
export const CO2 = Template.bind({});
|
||||||
CO2.args = {
|
CO2.args = {
|
||||||
|
|
@ -290,39 +266,33 @@ CO2.args = {
|
||||||
pvPower: 7300,
|
pvPower: 7300,
|
||||||
gridPower: -2300,
|
gridPower: -2300,
|
||||||
homePower: 800,
|
homePower: 800,
|
||||||
loadpointsCompact: [
|
loadpoints: [
|
||||||
{
|
{
|
||||||
power: 1000,
|
|
||||||
icon: "car",
|
icon: "car",
|
||||||
charging: true,
|
charging: true,
|
||||||
title: "Garage",
|
title: "Garage",
|
||||||
chargePower: 1000,
|
chargePower: 1000,
|
||||||
connected: true,
|
connected: true,
|
||||||
index: 0,
|
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicleSoc: 50,
|
vehicleSoc: 50,
|
||||||
chargerFeatureHeating: false,
|
chargerFeatureHeating: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
power: 1000,
|
|
||||||
icon: "car",
|
icon: "car",
|
||||||
charging: true,
|
charging: true,
|
||||||
title: "Garage",
|
title: "Garage",
|
||||||
chargePower: 1000,
|
chargePower: 1000,
|
||||||
connected: true,
|
connected: true,
|
||||||
index: 1,
|
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicleSoc: 50,
|
vehicleSoc: 50,
|
||||||
chargerFeatureHeating: false,
|
chargerFeatureHeating: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
power: 2200,
|
|
||||||
icon: "car",
|
icon: "car",
|
||||||
charging: true,
|
charging: true,
|
||||||
title: "Garage",
|
title: "Garage",
|
||||||
chargePower: 2200,
|
chargePower: 2200,
|
||||||
connected: true,
|
connected: true,
|
||||||
index: 2,
|
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicleSoc: 50,
|
vehicleSoc: 50,
|
||||||
chargerFeatureHeating: false,
|
chargerFeatureHeating: false,
|
||||||
|
|
@ -334,7 +304,7 @@ CO2.args = {
|
||||||
smartCostType: "co2",
|
smartCostType: "co2",
|
||||||
currency: CURRENCY.EUR,
|
currency: CURRENCY.EUR,
|
||||||
pv: [{ power: 5000 }, { power: 2300 }],
|
pv: [{ power: 5000 }, { power: 2300 }],
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
export const UnknownInput = Template.bind({});
|
export const UnknownInput = Template.bind({});
|
||||||
UnknownInput.args = {
|
UnknownInput.args = {
|
||||||
|
|
@ -342,21 +312,19 @@ UnknownInput.args = {
|
||||||
pvConfigured: true,
|
pvConfigured: true,
|
||||||
pvPower: 2000,
|
pvPower: 2000,
|
||||||
gridPower: -2000,
|
gridPower: -2000,
|
||||||
loadpointsCompact: [
|
loadpoints: [
|
||||||
{
|
{
|
||||||
power: 1000,
|
|
||||||
icon: "car",
|
icon: "car",
|
||||||
charging: true,
|
charging: true,
|
||||||
title: "Garage",
|
title: "Garage",
|
||||||
chargePower: 1000,
|
chargePower: 1000,
|
||||||
connected: true,
|
connected: true,
|
||||||
index: 0,
|
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicleSoc: 50,
|
vehicleSoc: 50,
|
||||||
chargerFeatureHeating: false,
|
chargerFeatureHeating: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
export const UnknownInputFill = Template.bind({});
|
export const UnknownInputFill = Template.bind({});
|
||||||
UnknownInputFill.args = {
|
UnknownInputFill.args = {
|
||||||
|
|
@ -366,8 +334,8 @@ UnknownInputFill.args = {
|
||||||
pvPower: 500,
|
pvPower: 500,
|
||||||
gridPower: 0,
|
gridPower: 0,
|
||||||
batteryPower: -1000,
|
batteryPower: -1000,
|
||||||
loadpointsCompact: [],
|
loadpoints: [],
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
export const UnknownOutput = Template.bind({});
|
export const UnknownOutput = Template.bind({});
|
||||||
UnknownOutput.args = {
|
UnknownOutput.args = {
|
||||||
|
|
@ -375,21 +343,19 @@ UnknownOutput.args = {
|
||||||
pvConfigured: true,
|
pvConfigured: true,
|
||||||
pvPower: 3000,
|
pvPower: 3000,
|
||||||
gridPower: -1000,
|
gridPower: -1000,
|
||||||
loadpointsCompact: [
|
loadpoints: [
|
||||||
{
|
{
|
||||||
power: 1700,
|
chargePower: 1700,
|
||||||
icon: "car",
|
icon: "car",
|
||||||
charging: true,
|
charging: true,
|
||||||
title: "Garage",
|
title: "Garage",
|
||||||
chargePower: 1700,
|
|
||||||
connected: true,
|
connected: true,
|
||||||
index: 0,
|
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicleSoc: 50,
|
vehicleSoc: 50,
|
||||||
chargerFeatureHeating: false,
|
chargerFeatureHeating: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
} as any;
|
||||||
|
|
||||||
export const UnknownOutputLessThan10Percent = Template.bind({});
|
export const UnknownOutputLessThan10Percent = Template.bind({});
|
||||||
UnknownOutputLessThan10Percent.args = {
|
UnknownOutputLessThan10Percent.args = {
|
||||||
|
|
@ -397,18 +363,16 @@ UnknownOutputLessThan10Percent.args = {
|
||||||
pvConfigured: true,
|
pvConfigured: true,
|
||||||
pvPower: 3000,
|
pvPower: 3000,
|
||||||
gridPower: -1000,
|
gridPower: -1000,
|
||||||
loadpointsCompact: [
|
loadpoints: [
|
||||||
{
|
{
|
||||||
power: 1800,
|
chargePower: 1800,
|
||||||
icon: "car",
|
icon: "car",
|
||||||
charging: true,
|
charging: true,
|
||||||
title: "Garage",
|
title: "Garage",
|
||||||
chargePower: 1800,
|
|
||||||
connected: true,
|
connected: true,
|
||||||
index: 0,
|
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicleSoc: 50,
|
vehicleSoc: 50,
|
||||||
chargerFeatureHeating: false,
|
chargerFeatureHeating: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
} as any;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
:gridImport="gridImport"
|
:gridImport="gridImport"
|
||||||
:selfPv="selfPv"
|
:selfPv="selfPv"
|
||||||
:selfBattery="selfBattery"
|
:selfBattery="selfBattery"
|
||||||
:loadpoints="loadpointsCompact"
|
:loadpoints="loadpoints"
|
||||||
:pvExport="pvExport"
|
:pvExport="pvExport"
|
||||||
:batteryCharge="batteryCharge"
|
:batteryCharge="batteryCharge"
|
||||||
:batteryDischarge="batteryDischarge"
|
:batteryDischarge="batteryDischarge"
|
||||||
|
|
@ -194,14 +194,18 @@
|
||||||
<template v-if="activeLoadpointsCount > 0" #expanded>
|
<template v-if="activeLoadpointsCount > 0" #expanded>
|
||||||
<EnergyflowEntry
|
<EnergyflowEntry
|
||||||
v-for="lp in activeLoadpoints"
|
v-for="lp in activeLoadpoints"
|
||||||
:key="lp.index"
|
:key="lp.id"
|
||||||
:name="lp.title"
|
:name="lp.displayTitle"
|
||||||
:power="lp.power"
|
:power="lp.chargePower"
|
||||||
:powerUnit="powerUnit"
|
:powerUnit="powerUnit"
|
||||||
icon="vehicle"
|
icon="vehicle"
|
||||||
:iconProps="{ names: [lp.icon] }"
|
:iconProps="{ names: [lp.icon] }"
|
||||||
:details="lp.soc || undefined"
|
:details="lp.vehicleSoc || undefined"
|
||||||
:detailsFmt="lp.heating ? fmtLoadpointTemp : fmtLoadpointSoc"
|
:detailsFmt="
|
||||||
|
lp.chargerFeatureHeating
|
||||||
|
? fmtLoadpointTemp
|
||||||
|
: fmtLoadpointSoc
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</EnergyflowEntry>
|
</EnergyflowEntry>
|
||||||
|
|
@ -289,7 +293,7 @@ import {
|
||||||
type Battery,
|
type Battery,
|
||||||
type CURRENCY,
|
type CURRENCY,
|
||||||
type Forecast,
|
type Forecast,
|
||||||
type LoadpointCompact,
|
type UiLoadpoint,
|
||||||
} from "@/types/evcc";
|
} from "@/types/evcc";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
|
@ -307,7 +311,7 @@ export default defineComponent({
|
||||||
pvConfigured: Boolean,
|
pvConfigured: Boolean,
|
||||||
pv: { type: Array as PropType<Pv[]> },
|
pv: { type: Array as PropType<Pv[]> },
|
||||||
pvPower: { type: Number, default: 0 },
|
pvPower: { type: Number, default: 0 },
|
||||||
loadpointsCompact: { type: Array as PropType<LoadpointCompact[]>, default: () => [] },
|
loadpoints: { type: Array as PropType<UiLoadpoint[]>, default: () => [] },
|
||||||
batteryConfigured: { type: Boolean },
|
batteryConfigured: { type: Boolean },
|
||||||
battery: { type: Array as PropType<Battery[]> },
|
battery: { type: Array as PropType<Battery[]> },
|
||||||
batteryPower: { type: Number, default: 0 },
|
batteryPower: { type: Number, default: 0 },
|
||||||
|
|
@ -365,7 +369,7 @@ export default defineComponent({
|
||||||
return Math.min(this.batteryDischarge, this.consumption - this.selfPv);
|
return Math.min(this.batteryDischarge, this.consumption - this.selfPv);
|
||||||
},
|
},
|
||||||
activeLoadpoints() {
|
activeLoadpoints() {
|
||||||
return this.loadpointsCompact.filter((lp) => lp.charging);
|
return this.loadpoints.filter((lp) => lp.charging);
|
||||||
},
|
},
|
||||||
activeLoadpointsCount() {
|
activeLoadpointsCount() {
|
||||||
return this.activeLoadpoints.length;
|
return this.activeLoadpoints.length;
|
||||||
|
|
@ -377,8 +381,8 @@ export default defineComponent({
|
||||||
return ["car"];
|
return ["car"];
|
||||||
},
|
},
|
||||||
loadpointsPower() {
|
loadpointsPower() {
|
||||||
return this.loadpointsCompact.reduce((sum, lp) => {
|
return this.loadpoints.reduce((sum, lp) => {
|
||||||
return sum + (lp.power || 0);
|
return sum + (lp.chargePower || 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
},
|
},
|
||||||
pvExport() {
|
pvExport() {
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@
|
||||||
<LabelBar
|
<LabelBar
|
||||||
v-for="(lp, index) in loadpoints"
|
v-for="(lp, index) in loadpoints"
|
||||||
:key="index"
|
:key="index"
|
||||||
v-bind="labelBarProps('bottom', 'loadpoints', lp.power)"
|
v-bind="labelBarProps('bottom', 'loadpoints', lp.chargePower)"
|
||||||
>
|
>
|
||||||
<VehicleIcon :names="[lp.icon]" />
|
<VehicleIcon :names="[lp.icon]" />
|
||||||
</LabelBar>
|
</LabelBar>
|
||||||
|
|
@ -116,7 +116,7 @@ import QuestionIcon from "../MaterialIcon/Question.vue";
|
||||||
import "@h2d2/shopicons/es/regular/sun";
|
import "@h2d2/shopicons/es/regular/sun";
|
||||||
import "@h2d2/shopicons/es/regular/home";
|
import "@h2d2/shopicons/es/regular/home";
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
import type { LoadpointCompact } from "@/types/evcc";
|
import type { UiLoadpoint } from "@/types/evcc";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Visualization",
|
name: "Visualization",
|
||||||
|
|
@ -127,7 +127,7 @@ export default defineComponent({
|
||||||
selfPv: { type: Number, default: 0 },
|
selfPv: { type: Number, default: 0 },
|
||||||
selfBattery: { type: Number, default: 0 },
|
selfBattery: { type: Number, default: 0 },
|
||||||
pvExport: { type: Number, default: 0 },
|
pvExport: { type: Number, default: 0 },
|
||||||
loadpoints: { type: Array as PropType<LoadpointCompact[]>, default: () => [] },
|
loadpoints: { type: Array as PropType<UiLoadpoint[]>, default: () => [] },
|
||||||
batteryCharge: { type: Number, default: 0 },
|
batteryCharge: { type: Number, default: 0 },
|
||||||
batteryDischarge: { type: Number, default: 0 },
|
batteryDischarge: { type: Number, default: 0 },
|
||||||
batteryHold: { type: Boolean, default: false },
|
batteryHold: { type: Boolean, default: false },
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
:title="$t('settings.title')"
|
:title="$t('settings.title')"
|
||||||
data-testid="global-settings-modal"
|
data-testid="global-settings-modal"
|
||||||
>
|
>
|
||||||
<UserInterfaceSettings :sponsor="sponsor" :telemetry="telemetry" />
|
<UserInterfaceSettings :loadpoints="uiLoadpoints" />
|
||||||
</GenericModal>
|
</GenericModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -12,14 +12,13 @@
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
import GenericModal from "../Helper/GenericModal.vue";
|
import GenericModal from "../Helper/GenericModal.vue";
|
||||||
import UserInterfaceSettings from "./UserInterfaceSettings.vue";
|
import UserInterfaceSettings from "./UserInterfaceSettings.vue";
|
||||||
import type { Sponsor } from "@/types/evcc";
|
import type { UiLoadpoint } from "@/types/evcc";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "GlobalSettingsModal",
|
name: "GlobalSettingsModal",
|
||||||
components: { GenericModal, UserInterfaceSettings },
|
components: { GenericModal, UserInterfaceSettings },
|
||||||
props: {
|
props: {
|
||||||
sponsor: { type: Object as PropType<Sponsor>, default: () => ({}) },
|
uiLoadpoints: { type: Array as PropType<UiLoadpoint[]>, default: () => [] },
|
||||||
telemetry: Boolean,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
101
assets/js/components/GlobalSettings/LoadpointOrderSettings.vue
Normal file
101
assets/js/components/GlobalSettings/LoadpointOrderSettings.vue
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="small text-muted mb-2 col-form-label">
|
||||||
|
{{ $t("settings.loadpoints.help") }}
|
||||||
|
</div>
|
||||||
|
<DragDropList :values="loadpoints.map((item) => item.id)" @reorder="handleReorder">
|
||||||
|
<DragDropItem
|
||||||
|
v-for="item in loadpoints"
|
||||||
|
:key="item.id"
|
||||||
|
:title="item.title"
|
||||||
|
:visible="item.visible"
|
||||||
|
>
|
||||||
|
<template #actions>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input
|
||||||
|
:id="`loadpoint-visible-${item.id}`"
|
||||||
|
v-model="item.visible"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
role="switch"
|
||||||
|
:aria-label="getVisibilityLabel(item)"
|
||||||
|
:disabled="isLastVisible(item)"
|
||||||
|
@change="updateVisibility(item.id, item.visible)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</DragDropItem>
|
||||||
|
</DragDropList>
|
||||||
|
<div class="mt-2 text-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-link btn-sm text-muted"
|
||||||
|
:disabled="resetDisabled"
|
||||||
|
@click="resetOrder"
|
||||||
|
>
|
||||||
|
{{ $t("config.general.reset") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
import type { UiLoadpoint } from "@/types/evcc";
|
||||||
|
import {
|
||||||
|
setLoadpointOrder,
|
||||||
|
setLoadpointVisibility,
|
||||||
|
resetLoadpointsOrder,
|
||||||
|
resetLoadpointsVisible,
|
||||||
|
} from "@/uiLoadpoints";
|
||||||
|
import DragDropList from "@/components/Helper/DragDropList.vue";
|
||||||
|
import DragDropItem from "@/components/Helper/DragDropItem.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "LoadpointOrderSettings",
|
||||||
|
components: { DragDropList, DragDropItem },
|
||||||
|
props: {
|
||||||
|
loadpoints: { type: Array as PropType<UiLoadpoint[]>, default: () => [] },
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
resetDisabled() {
|
||||||
|
const allVisible = this.loadpoints.every((lp) => lp.visible);
|
||||||
|
const noOrder = this.loadpoints.every((lp) => lp.order === null);
|
||||||
|
return allVisible && noOrder;
|
||||||
|
},
|
||||||
|
visibleCount() {
|
||||||
|
return this.loadpoints.filter((lp) => lp.visible).length;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isLastVisible(item: UiLoadpoint) {
|
||||||
|
return item.visible && this.visibleCount <= 1;
|
||||||
|
},
|
||||||
|
getVisibilityLabel(item: UiLoadpoint) {
|
||||||
|
const action = item.visible ? "hide" : "show";
|
||||||
|
return this.$t(`settings.loadpoints.${action}`, { title: item.title });
|
||||||
|
},
|
||||||
|
updateVisibility(loadpointId: string, visible: boolean) {
|
||||||
|
setLoadpointVisibility(loadpointId, visible);
|
||||||
|
},
|
||||||
|
handleReorder(newOrder: string[]) {
|
||||||
|
setLoadpointOrder(newOrder);
|
||||||
|
},
|
||||||
|
resetOrder() {
|
||||||
|
resetLoadpointsOrder();
|
||||||
|
resetLoadpointsVisible();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-check-input {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-check {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -59,12 +59,20 @@
|
||||||
equal-width
|
equal-width
|
||||||
/>
|
/>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow id="telemetryEnabled" :label="$t('settings.telemetry.label')">
|
<FormRow v-if="loadpoints.length" :label="$t('settings.loadpoints.label')">
|
||||||
<TelemetrySettings
|
<LoadpointOrderSettings :loadpoints="loadpoints" />
|
||||||
:sponsorActive="sponsor && !!sponsor.name"
|
</FormRow>
|
||||||
:telemetry="telemetry"
|
<FormRow v-if="fullscreenAvailable" :label="$t('settings.fullscreen.label')">
|
||||||
class="mt-1 mb-0"
|
<button
|
||||||
/>
|
v-if="fullscreenActive"
|
||||||
|
class="btn btn-sm btn-outline-secondary"
|
||||||
|
@click="exitFullscreen"
|
||||||
|
>
|
||||||
|
{{ $t("settings.fullscreen.exit") }}
|
||||||
|
</button>
|
||||||
|
<button v-else class="btn btn-sm btn-outline-secondary" @click="enterFullscreen">
|
||||||
|
{{ $t("settings.fullscreen.enter") }}
|
||||||
|
</button>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow id="hiddenFeaturesEnabled" :label="`${$t('settings.hiddenFeatures.label')} 🧪`">
|
<FormRow id="hiddenFeaturesEnabled" :label="`${$t('settings.hiddenFeatures.label')} 🧪`">
|
||||||
<div class="form-check form-switch my-1">
|
<div class="form-check form-switch my-1">
|
||||||
|
|
@ -82,25 +90,16 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow v-if="fullscreenAvailable" :label="$t('settings.fullscreen.label')">
|
<div class="small text-muted mb-3">
|
||||||
<button
|
{{ $t("settings.deviceInfo") }}
|
||||||
v-if="fullscreenActive"
|
</div>
|
||||||
class="btn btn-sm btn-outline-secondary"
|
|
||||||
@click="exitFullscreen"
|
|
||||||
>
|
|
||||||
{{ $t("settings.fullscreen.exit") }}
|
|
||||||
</button>
|
|
||||||
<button v-else class="btn btn-sm btn-outline-secondary" @click="enterFullscreen">
|
|
||||||
{{ $t("settings.fullscreen.enter") }}
|
|
||||||
</button>
|
|
||||||
</FormRow>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TelemetrySettings from "../TelemetrySettings.vue";
|
|
||||||
import FormRow from "../Helper/FormRow.vue";
|
import FormRow from "../Helper/FormRow.vue";
|
||||||
import SelectGroup from "../Helper/SelectGroup.vue";
|
import SelectGroup from "../Helper/SelectGroup.vue";
|
||||||
|
import LoadpointOrderSettings from "./LoadpointOrderSettings.vue";
|
||||||
import {
|
import {
|
||||||
getLocalePreference,
|
getLocalePreference,
|
||||||
setLocalePreference,
|
setLocalePreference,
|
||||||
|
|
@ -112,17 +111,16 @@ import { getUnits, setUnits, is12hFormat, set12hFormat } from "@/units";
|
||||||
import { getHiddenFeatures, setHiddenFeatures } from "@/featureflags.ts";
|
import { getHiddenFeatures, setHiddenFeatures } from "@/featureflags.ts";
|
||||||
import { isApp } from "@/utils/native";
|
import { isApp } from "@/utils/native";
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
import { LENGTH_UNIT, THEME, type Sponsor } from "@/types/evcc";
|
import { LENGTH_UNIT, THEME, type UiLoadpoint } from "@/types/evcc";
|
||||||
|
|
||||||
const TIME_12H = "12";
|
const TIME_12H = "12";
|
||||||
const TIME_24H = "24";
|
const TIME_24H = "24";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "UserInterfaceSettings",
|
name: "UserInterfaceSettings",
|
||||||
components: { TelemetrySettings, FormRow, SelectGroup },
|
components: { FormRow, SelectGroup, LoadpointOrderSettings },
|
||||||
props: {
|
props: {
|
||||||
sponsor: Object as PropType<Sponsor>,
|
loadpoints: { type: Array as PropType<UiLoadpoint[]>, default: () => [] },
|
||||||
telemetry: Boolean,
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -184,6 +182,7 @@ export default defineComponent({
|
||||||
document.removeEventListener("fullscreenchange", this.fullscreenChange);
|
document.removeEventListener("fullscreenchange", this.fullscreenChange);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
isApp,
|
||||||
enterFullscreen() {
|
enterFullscreen() {
|
||||||
document.documentElement.requestFullscreen();
|
document.documentElement.requestFullscreen();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
76
assets/js/components/Helper/DragDropItem.vue
Normal file
76
assets/js/components/Helper/DragDropItem.vue
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="drag-drop-item d-flex align-items-center p-2 mb-2 border rounded"
|
||||||
|
:class="{
|
||||||
|
'drag-drop-item--hidden': !visible,
|
||||||
|
}"
|
||||||
|
role="listitem"
|
||||||
|
:aria-label="$t('config.general.dragItem', { title })"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="drag-handle me-2"
|
||||||
|
:aria-label="$t('config.general.dragHandle')"
|
||||||
|
role="button"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<shopicon-regular-menu></shopicon-regular-menu>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<slot>
|
||||||
|
{{ title }}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div v-if="$slots['actions']" class="drag-drop-item__actions">
|
||||||
|
<slot name="actions"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import "@h2d2/shopicons/es/regular/menu";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "DragDropItem",
|
||||||
|
props: {
|
||||||
|
title: { type: String, default: "" },
|
||||||
|
visible: { type: Boolean, default: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.drag-drop-item {
|
||||||
|
cursor: move;
|
||||||
|
user-select: none;
|
||||||
|
background-color: var(--evcc-box);
|
||||||
|
border-color: var(--bs-border-color-translucent) !important;
|
||||||
|
transition: all var(--evcc-transition-fast) ease-in-out;
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-drop-item--hidden {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
color: var(--bs-secondary);
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: opacity var(--evcc-transition-fast) ease-in-out;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-drop-item:hover .drag-handle {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-drop-item__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
37
assets/js/components/Helper/DragDropList.vue
Normal file
37
assets/js/components/Helper/DragDropList.vue
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<template>
|
||||||
|
<div ref="container" role="list" :aria-label="$t('config.general.dragList')">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { dragAndDrop } from "@formkit/drag-and-drop";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "DragDropList",
|
||||||
|
props: {
|
||||||
|
values: { type: Array, required: true },
|
||||||
|
},
|
||||||
|
emits: ["reorder"],
|
||||||
|
mounted() {
|
||||||
|
const container = this.$refs["container"] as HTMLElement;
|
||||||
|
if (container) {
|
||||||
|
dragAndDrop({
|
||||||
|
parent: container,
|
||||||
|
getValues: () => this.values,
|
||||||
|
setValues: (newOrder: unknown[]) => this.$emit("reorder", newOrder),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Overwrites Animation for Drag-Elements */
|
||||||
|
.drag-drop-item[style*="position: fixed"],
|
||||||
|
.drag-drop-item[style*="z-index: 9999"] {
|
||||||
|
transition: none !important;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
import Loadpoint from "./Loadpoint.vue";
|
import Loadpoint from "./Loadpoint.vue";
|
||||||
import type { Meta, StoryFn } from "@storybook/vue3";
|
import type { Meta, StoryFn } from "@storybook/vue3";
|
||||||
import { SMART_COST_TYPE } from "@/types/evcc";
|
import {
|
||||||
|
SMART_COST_TYPE,
|
||||||
|
CHARGE_MODE,
|
||||||
|
CHARGER_STATUS_REASON,
|
||||||
|
PHASE_ACTION,
|
||||||
|
PV_ACTION,
|
||||||
|
} from "@/types/evcc";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Loadpoints/Loadpoint",
|
title: "Loadpoints/Loadpoint",
|
||||||
|
|
@ -12,9 +18,9 @@ export default {
|
||||||
|
|
||||||
// Based on actual API state structure from demo.evcc.io
|
// Based on actual API state structure from demo.evcc.io
|
||||||
const baseState = {
|
const baseState = {
|
||||||
id: 1,
|
id: "1",
|
||||||
title: "Carport",
|
title: "Carport",
|
||||||
mode: "pv",
|
mode: CHARGE_MODE.PV,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
connected: true,
|
connected: true,
|
||||||
socBasedCharging: true,
|
socBasedCharging: true,
|
||||||
|
|
@ -50,9 +56,9 @@ const baseState = {
|
||||||
planProjectedStart: undefined,
|
planProjectedStart: undefined,
|
||||||
planTime: undefined,
|
planTime: undefined,
|
||||||
priority: 0,
|
priority: 0,
|
||||||
phaseAction: "inactive",
|
phaseAction: PHASE_ACTION.INACTIVE,
|
||||||
phaseRemaining: 0,
|
phaseRemaining: 0,
|
||||||
pvAction: "inactive",
|
pvAction: PV_ACTION.INACTIVE,
|
||||||
pvRemaining: 0,
|
pvRemaining: 0,
|
||||||
smartCostActive: false,
|
smartCostActive: false,
|
||||||
smartCostNextStart: undefined,
|
smartCostNextStart: undefined,
|
||||||
|
|
@ -65,7 +71,7 @@ const baseState = {
|
||||||
chargerFeatureIntegratedDevice: false,
|
chargerFeatureIntegratedDevice: false,
|
||||||
chargerIcon: "",
|
chargerIcon: "",
|
||||||
chargerSinglePhase: false,
|
chargerSinglePhase: false,
|
||||||
chargerStatusReason: "unknown",
|
chargerStatusReason: CHARGER_STATUS_REASON.UNKNOWN,
|
||||||
connectedDuration: 0,
|
connectedDuration: 0,
|
||||||
// Global props that would typically come from parent
|
// Global props that would typically come from parent
|
||||||
vehicles: [
|
vehicles: [
|
||||||
|
|
@ -125,7 +131,7 @@ Idle.args = {
|
||||||
connected: false,
|
connected: false,
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicles: [],
|
vehicles: [],
|
||||||
mode: "off",
|
mode: CHARGE_MODE.OFF,
|
||||||
charging: false,
|
charging: false,
|
||||||
chargePower: 0,
|
chargePower: 0,
|
||||||
offeredCurrent: 0,
|
offeredCurrent: 0,
|
||||||
|
|
@ -139,7 +145,7 @@ DisabledLongTitle.args = {
|
||||||
title: "Charging point with a very very very long title!!!1!",
|
title: "Charging point with a very very very long title!!!1!",
|
||||||
remoteDisabled: "soft",
|
remoteDisabled: "soft",
|
||||||
remoteDisabledSource: "Sunny Home Manager",
|
remoteDisabledSource: "Sunny Home Manager",
|
||||||
mode: "now",
|
mode: CHARGE_MODE.NOW,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
charging: false,
|
charging: false,
|
||||||
chargePower: 0,
|
chargePower: 0,
|
||||||
|
|
@ -150,7 +156,7 @@ ChargerIconNoVehicle.args = {
|
||||||
...baseState,
|
...baseState,
|
||||||
chargerIcon: "heater",
|
chargerIcon: "heater",
|
||||||
title: "Heating device with long name",
|
title: "Heating device with long name",
|
||||||
mode: "now",
|
mode: CHARGE_MODE.NOW,
|
||||||
chargerFeatureIntegratedDevice: true,
|
chargerFeatureIntegratedDevice: true,
|
||||||
vehicleName: "",
|
vehicleName: "",
|
||||||
vehicles: [],
|
vehicles: [],
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,9 @@ import { defineComponent, type PropType } from "vue";
|
||||||
import type {
|
import type {
|
||||||
CHARGE_MODE,
|
CHARGE_MODE,
|
||||||
PHASES,
|
PHASES,
|
||||||
|
PHASE_ACTION,
|
||||||
|
PV_ACTION,
|
||||||
|
CHARGER_STATUS_REASON,
|
||||||
Timeout,
|
Timeout,
|
||||||
Vehicle,
|
Vehicle,
|
||||||
Forecast,
|
Forecast,
|
||||||
|
|
@ -130,12 +133,12 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
mixins: [formatter, collector],
|
mixins: [formatter, collector],
|
||||||
props: {
|
props: {
|
||||||
id: Number,
|
id: String,
|
||||||
single: Boolean,
|
single: Boolean,
|
||||||
|
|
||||||
// main
|
// main
|
||||||
title: String,
|
title: String,
|
||||||
mode: String,
|
mode: String as PropType<CHARGE_MODE>,
|
||||||
effectiveLimitSoc: Number,
|
effectiveLimitSoc: Number,
|
||||||
limitEnergy: Number,
|
limitEnergy: Number,
|
||||||
remoteDisabled: String,
|
remoteDisabled: String,
|
||||||
|
|
@ -147,16 +150,16 @@ export default defineComponent({
|
||||||
|
|
||||||
// session
|
// session
|
||||||
sessionEnergy: Number,
|
sessionEnergy: Number,
|
||||||
sessionCo2PerKWh: Number,
|
sessionCo2PerKWh: Number as PropType<number | null>,
|
||||||
sessionPricePerKWh: Number,
|
sessionPricePerKWh: Number as PropType<number | null>,
|
||||||
sessionPrice: Number,
|
sessionPrice: Number as PropType<number | null>,
|
||||||
sessionSolarPercentage: Number,
|
sessionSolarPercentage: Number,
|
||||||
|
|
||||||
// charger
|
// charger
|
||||||
chargerStatusReason: String,
|
chargerStatusReason: String as PropType<CHARGER_STATUS_REASON | null>,
|
||||||
chargerFeatureIntegratedDevice: Boolean,
|
chargerFeatureIntegratedDevice: Boolean,
|
||||||
chargerFeatureHeating: Boolean,
|
chargerFeatureHeating: Boolean,
|
||||||
chargerIcon: String,
|
chargerIcon: String as PropType<string | null>,
|
||||||
|
|
||||||
// vehicle
|
// vehicle
|
||||||
connected: Boolean,
|
connected: Boolean,
|
||||||
|
|
@ -170,20 +173,20 @@ export default defineComponent({
|
||||||
vehicleLimitSoc: Number,
|
vehicleLimitSoc: Number,
|
||||||
vehicles: Array as PropType<Vehicle[]>,
|
vehicles: Array as PropType<Vehicle[]>,
|
||||||
planActive: Boolean,
|
planActive: Boolean,
|
||||||
planProjectedStart: String,
|
planProjectedStart: String as PropType<string | null>,
|
||||||
planProjectedEnd: String,
|
planProjectedEnd: String as PropType<string | null>,
|
||||||
planOverrun: { type: Number, default: 0 },
|
planOverrun: { type: Number, default: 0 },
|
||||||
planEnergy: Number,
|
planEnergy: Number,
|
||||||
planPrecondition: Number,
|
planPrecondition: Number,
|
||||||
planTime: String,
|
planTime: String as PropType<string | null>,
|
||||||
effectivePlanTime: String,
|
effectivePlanTime: String as PropType<string | null>,
|
||||||
effectivePlanSoc: Number,
|
effectivePlanSoc: Number,
|
||||||
vehicleProviderLoggedIn: Boolean,
|
vehicleProviderLoggedIn: Boolean,
|
||||||
vehicleProviderLoginPath: String,
|
vehicleProviderLoginPath: String,
|
||||||
vehicleProviderLogoutPath: String,
|
vehicleProviderLogoutPath: String,
|
||||||
|
|
||||||
// details
|
// details
|
||||||
vehicleClimaterActive: Boolean,
|
vehicleClimaterActive: Boolean as PropType<boolean | null>,
|
||||||
vehicleWelcomeActive: Boolean,
|
vehicleWelcomeActive: Boolean,
|
||||||
chargePower: { type: Number, default: 0 },
|
chargePower: { type: Number, default: 0 },
|
||||||
chargedEnergy: { type: Number, default: 0 },
|
chargedEnergy: { type: Number, default: 0 },
|
||||||
|
|
@ -200,19 +203,19 @@ export default defineComponent({
|
||||||
connectedDuration: Number,
|
connectedDuration: Number,
|
||||||
chargeCurrents: Array,
|
chargeCurrents: Array,
|
||||||
chargeRemainingEnergy: Number,
|
chargeRemainingEnergy: Number,
|
||||||
phaseAction: String,
|
phaseAction: String as PropType<PHASE_ACTION>,
|
||||||
phaseRemaining: { type: Number, default: 0 },
|
phaseRemaining: { type: Number, default: 0 },
|
||||||
pvRemaining: { type: Number, default: 0 },
|
pvRemaining: { type: Number, default: 0 },
|
||||||
pvAction: String,
|
pvAction: String as PropType<PV_ACTION>,
|
||||||
smartCostLimit: { type: Number, default: null },
|
smartCostLimit: { type: Number as PropType<number | null>, default: null },
|
||||||
smartCostType: String as PropType<SMART_COST_TYPE>,
|
smartCostType: String as PropType<SMART_COST_TYPE>,
|
||||||
smartCostAvailable: Boolean,
|
smartCostAvailable: Boolean,
|
||||||
smartCostActive: Boolean,
|
smartCostActive: Boolean,
|
||||||
smartCostNextStart: String,
|
smartCostNextStart: String as PropType<string | null>,
|
||||||
smartFeedInPriorityLimit: { type: Number, default: null },
|
smartFeedInPriorityLimit: { type: Number as PropType<number | null>, default: null },
|
||||||
smartFeedInPriorityAvailable: Boolean,
|
smartFeedInPriorityAvailable: Boolean,
|
||||||
smartFeedInPriorityActive: Boolean,
|
smartFeedInPriorityActive: Boolean,
|
||||||
smartFeedInPriorityNextStart: String,
|
smartFeedInPriorityNextStart: String as PropType<string | null>,
|
||||||
tariffGrid: Number,
|
tariffGrid: Number,
|
||||||
tariffFeedIn: Number,
|
tariffFeedIn: Number,
|
||||||
tariffCo2: Number,
|
tariffCo2: Number,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import type { Meta, StoryFn } from "@storybook/vue3";
|
||||||
import { SMART_COST_TYPE } from "@/types/evcc";
|
import { SMART_COST_TYPE } from "@/types/evcc";
|
||||||
|
|
||||||
// Create LoadpointCompact structure for the Loadpoints component
|
// Create LoadpointCompact structure for the Loadpoints component
|
||||||
const createLoadpoint = (opts = {}) => {
|
const createLoadpoint = (opts: any = {}) => {
|
||||||
const base = {
|
const base = {
|
||||||
icon: "car",
|
icon: "car",
|
||||||
title: "Carport",
|
title: "Carport",
|
||||||
|
|
@ -18,7 +18,9 @@ const createLoadpoint = (opts = {}) => {
|
||||||
vehicleSoc: 0,
|
vehicleSoc: 0,
|
||||||
chargerFeatureHeating: false,
|
chargerFeatureHeating: false,
|
||||||
};
|
};
|
||||||
return { ...base, ...opts };
|
const result = { ...base, ...opts };
|
||||||
|
(result as any).id = result.index + 1;
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,12 @@
|
||||||
:class="`carousel--${loadpoints.length}`"
|
:class="`carousel--${loadpoints.length}`"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(loadpoint, index) in loadpoints"
|
v-for="loadpoint in loadpoints"
|
||||||
:key="index"
|
:key="loadpoint.id"
|
||||||
class="flex-grow-1 mb-3 m-lg-0 p-lg-0"
|
class="flex-grow-1 mb-3 m-lg-0 p-lg-0"
|
||||||
>
|
>
|
||||||
<Loadpoint
|
<Loadpoint
|
||||||
v-bind="loadpoint"
|
v-bind="loadpoint"
|
||||||
:id="index + 1"
|
|
||||||
data-testid="loadpoint"
|
data-testid="loadpoint"
|
||||||
:vehicles="vehicles"
|
:vehicles="vehicles"
|
||||||
:smartCostType="smartCostType"
|
:smartCostType="smartCostType"
|
||||||
|
|
@ -30,18 +29,18 @@
|
||||||
:batteryConfigured="batteryConfigured"
|
:batteryConfigured="batteryConfigured"
|
||||||
:forecast="forecast"
|
:forecast="forecast"
|
||||||
class="h-100"
|
class="h-100"
|
||||||
:class="{ 'loadpoint-unselected': !selected(index) }"
|
:class="{ 'loadpoint-unselected': !selected(loadpoint.id) }"
|
||||||
@click="goTo(index)"
|
@click="goTo(loadpoint.id)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="loadpoints.length > 1" class="d-flex d-lg-none justify-content-center">
|
<div v-if="loadpoints.length > 1" class="d-flex d-lg-none justify-content-center">
|
||||||
<button
|
<button
|
||||||
v-for="(loadpoint, index) in loadpoints"
|
v-for="loadpoint in loadpoints"
|
||||||
:key="index"
|
:key="loadpoint.id"
|
||||||
class="btn btn-sm btn-link p-0 mx-1 indicator d-flex justify-content-center align-items-center evcc-default-text"
|
class="btn btn-sm btn-link p-0 mx-1 indicator d-flex justify-content-center align-items-center evcc-default-text"
|
||||||
:class="{ 'indicator--selected': selected(index) }"
|
:class="{ 'indicator--selected': selected(loadpoint.id) }"
|
||||||
@click="goTo(index)"
|
@click="goTo(loadpoint.id)"
|
||||||
>
|
>
|
||||||
<shopicon-filled-lightning
|
<shopicon-filled-lightning
|
||||||
v-if="isCharging(loadpoint)"
|
v-if="isCharging(loadpoint)"
|
||||||
|
|
@ -64,13 +63,13 @@ import "@h2d2/shopicons/es/filled/lightning";
|
||||||
|
|
||||||
import Loadpoint from "./Loadpoint.vue";
|
import Loadpoint from "./Loadpoint.vue";
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
import type { LoadpointCompact, SMART_COST_TYPE, Timeout, Vehicle } from "@/types/evcc";
|
import type { UiLoadpoint, SMART_COST_TYPE, Timeout, Vehicle } from "@/types/evcc";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: "Loadpoints",
|
name: "Loadpoints",
|
||||||
components: { Loadpoint },
|
components: { Loadpoint },
|
||||||
props: {
|
props: {
|
||||||
loadpoints: { type: Array as PropType<LoadpointCompact[]>, default: () => [] },
|
loadpoints: { type: Array as PropType<UiLoadpoint[]>, default: () => [] },
|
||||||
vehicles: { type: Array as PropType<Vehicle[]> },
|
vehicles: { type: Array as PropType<Vehicle[]> },
|
||||||
smartCostType: String as PropType<SMART_COST_TYPE>,
|
smartCostType: String as PropType<SMART_COST_TYPE>,
|
||||||
smartCostAvailable: Boolean,
|
smartCostAvailable: Boolean,
|
||||||
|
|
@ -79,13 +78,13 @@ export default defineComponent({
|
||||||
tariffCo2: Number,
|
tariffCo2: Number,
|
||||||
tariffFeedIn: Number,
|
tariffFeedIn: Number,
|
||||||
currency: String,
|
currency: String,
|
||||||
selectedIndex: { type: Number, default: 0 },
|
selectedId: String,
|
||||||
gridConfigured: Boolean,
|
gridConfigured: Boolean,
|
||||||
pvConfigured: Boolean,
|
pvConfigured: Boolean,
|
||||||
batteryConfigured: Boolean,
|
batteryConfigured: Boolean,
|
||||||
forecast: Object, // as PropType<Forecast>,
|
forecast: Object, // as PropType<Forecast>,
|
||||||
},
|
},
|
||||||
emits: ["index-changed"],
|
emits: ["id-changed"],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
snapTimeout: null as Timeout,
|
snapTimeout: null as Timeout,
|
||||||
|
|
@ -93,6 +92,11 @@ export default defineComponent({
|
||||||
highlightedIndex: 0,
|
highlightedIndex: 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
selectedIndex() {
|
||||||
|
return this.indexById(this.selectedId);
|
||||||
|
},
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
selectedIndex(newIndex) {
|
selectedIndex(newIndex) {
|
||||||
this.scrollTo(newIndex);
|
this.scrollTo(newIndex);
|
||||||
|
|
@ -108,6 +112,12 @@ export default defineComponent({
|
||||||
this.$refs["carousel"]?.removeEventListener("scroll", this.handleCarouselScroll);
|
this.$refs["carousel"]?.removeEventListener("scroll", this.handleCarouselScroll);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
indexById(id: string | undefined) {
|
||||||
|
return this.loadpoints.findIndex((lp) => lp.id === id) || 0;
|
||||||
|
},
|
||||||
|
idByIndex(index: number) {
|
||||||
|
return this.loadpoints[index]?.id;
|
||||||
|
},
|
||||||
handleCarouselScroll() {
|
handleCarouselScroll() {
|
||||||
const { scrollLeft } = this.$refs["carousel"] as HTMLElement;
|
const { scrollLeft } = this.$refs["carousel"] as HTMLElement;
|
||||||
const { offsetWidth } = this.$refs["carousel"]?.children[0] as HTMLElement;
|
const { offsetWidth } = this.$refs["carousel"]?.children[0] as HTMLElement;
|
||||||
|
|
@ -119,18 +129,18 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
this.scrollTimeout = setTimeout(() => {
|
this.scrollTimeout = setTimeout(() => {
|
||||||
if (this.highlightedIndex !== this.selectedIndex) {
|
if (this.highlightedIndex !== this.selectedIndex) {
|
||||||
this.$emit("index-changed", this.highlightedIndex);
|
this.$emit("id-changed", this.idByIndex(this.highlightedIndex));
|
||||||
}
|
}
|
||||||
}, 2000);
|
}, 2000);
|
||||||
},
|
},
|
||||||
goTo(index: number) {
|
goTo(id: string) {
|
||||||
this.$emit("index-changed", index);
|
this.$emit("id-changed", id);
|
||||||
},
|
},
|
||||||
isCharging(lp: LoadpointCompact) {
|
isCharging(lp: UiLoadpoint) {
|
||||||
return lp.charging && lp.chargePower > 0;
|
return lp.charging && lp.chargePower > 0;
|
||||||
},
|
},
|
||||||
selected(index: number) {
|
selected(id: string) {
|
||||||
return this.highlightedIndex === index;
|
return this.highlightedIndex === this.indexById(id);
|
||||||
},
|
},
|
||||||
left(index: number) {
|
left(index: number) {
|
||||||
return (this.$refs["carousel"]?.children[0] as HTMLElement).offsetWidth * index;
|
return (this.$refs["carousel"]?.children[0] as HTMLElement).offsetWidth * index;
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ import { defineComponent, type PropType } from "vue";
|
||||||
import LabelAndValue from "../Helper/LabelAndValue.vue";
|
import LabelAndValue from "../Helper/LabelAndValue.vue";
|
||||||
import CustomSelect from "../Helper/CustomSelect.vue";
|
import CustomSelect from "../Helper/CustomSelect.vue";
|
||||||
import formatter from "@/mixins/formatter";
|
import formatter from "@/mixins/formatter";
|
||||||
import { getSessionInfo, setSessionInfo } from "./session";
|
import { getLoadpointSessionInfo, setLoadpointSessionInfo } from "@/uiLoadpoints";
|
||||||
import type { CURRENCY, SelectOption, SessionInfoKey } from "@/types/evcc";
|
import type { CURRENCY, SelectOption, SessionInfoKey } from "@/types/evcc";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
|
@ -42,7 +42,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
mixins: [formatter],
|
mixins: [formatter],
|
||||||
props: {
|
props: {
|
||||||
id: { type: Number, default: 0 },
|
id: String,
|
||||||
sessionCo2PerKWh: { type: Number, default: 0 },
|
sessionCo2PerKWh: { type: Number, default: 0 },
|
||||||
sessionPricePerKWh: { type: Number, default: 0 },
|
sessionPricePerKWh: { type: Number, default: 0 },
|
||||||
sessionPrice: { type: Number, default: 0 },
|
sessionPrice: { type: Number, default: 0 },
|
||||||
|
|
@ -55,7 +55,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedKey: getSessionInfo(this.id),
|
selectedKey: this.id ? getLoadpointSessionInfo(this.id) : undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -165,8 +165,8 @@ export default defineComponent({
|
||||||
this.presist();
|
this.presist();
|
||||||
},
|
},
|
||||||
presist() {
|
presist() {
|
||||||
if (this.selectedKey) {
|
if (this.selectedKey && this.id) {
|
||||||
setSessionInfo(this.id, this.selectedKey);
|
setLoadpointSessionInfo(this.id, this.selectedKey);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import settings from "@/settings";
|
|
||||||
import type { SessionInfoKey } from "@/types/evcc";
|
|
||||||
|
|
||||||
export function getSessionInfo(index: number): SessionInfoKey | undefined {
|
|
||||||
return settings.sessionInfo[index - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setSessionInfo(index: number, value: SessionInfoKey) {
|
|
||||||
const clone = [...settings.sessionInfo];
|
|
||||||
clone[index - 1] = value;
|
|
||||||
clone.map((v) => v || "");
|
|
||||||
settings.sessionInfo = clone;
|
|
||||||
}
|
|
||||||
|
|
@ -51,7 +51,7 @@
|
||||||
<Loadpoints
|
<Loadpoints
|
||||||
v-else-if="loadpoints.length > 0"
|
v-else-if="loadpoints.length > 0"
|
||||||
class="mt-1 mt-sm-2 flex-grow-1"
|
class="mt-1 mt-sm-2 flex-grow-1"
|
||||||
:loadpoints="loadpoints"
|
:loadpoints="orderedVisibleLoadpoints"
|
||||||
:vehicles="vehicleList"
|
:vehicles="vehicleList"
|
||||||
:smartCostType="smartCostType"
|
:smartCostType="smartCostType"
|
||||||
:smartCostAvailable="smartCostAvailable"
|
:smartCostAvailable="smartCostAvailable"
|
||||||
|
|
@ -65,8 +65,8 @@
|
||||||
:batteryConfigured="batteryConfigured"
|
:batteryConfigured="batteryConfigured"
|
||||||
:batterySoc="batterySoc"
|
:batterySoc="batterySoc"
|
||||||
:forecast="forecast"
|
:forecast="forecast"
|
||||||
:selectedIndex="selectedLoadpointIndex"
|
:selectedId="selectedLoadpointId"
|
||||||
@index-changed="selectedLoadpointChanged"
|
@id-changed="selectedLoadpointChanged"
|
||||||
/>
|
/>
|
||||||
<Footer v-bind="footer"></Footer>
|
<Footer v-bind="footer"></Footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -90,7 +90,6 @@ import type {
|
||||||
Battery,
|
Battery,
|
||||||
CURRENCY,
|
CURRENCY,
|
||||||
Forecast,
|
Forecast,
|
||||||
LoadpointCompact,
|
|
||||||
Notification,
|
Notification,
|
||||||
Circuit,
|
Circuit,
|
||||||
SMART_COST_TYPE,
|
SMART_COST_TYPE,
|
||||||
|
|
@ -98,6 +97,7 @@ import type {
|
||||||
FatalError,
|
FatalError,
|
||||||
EvOpt,
|
EvOpt,
|
||||||
} from "@/types/evcc";
|
} from "@/types/evcc";
|
||||||
|
import store from "@/store";
|
||||||
import type { Grid } from "./types";
|
import type { Grid } from "./types";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
|
@ -113,8 +113,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
mixins: [formatter, collector],
|
mixins: [formatter, collector],
|
||||||
props: {
|
props: {
|
||||||
loadpoints: { type: Array as PropType<LoadpointCompact[]>, default: () => [] },
|
selectedLoadpointId: String,
|
||||||
selectedLoadpointIndex: Number,
|
|
||||||
|
|
||||||
notifications: { type: Array as PropType<Notification[]>, default: () => [] },
|
notifications: { type: Array as PropType<Notification[]>, default: () => [] },
|
||||||
offline: Boolean,
|
offline: Boolean,
|
||||||
|
|
@ -165,6 +164,12 @@ export default defineComponent({
|
||||||
evopt: { type: Object as PropType<EvOpt> },
|
evopt: { type: Object as PropType<EvOpt> },
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
loadpoints() {
|
||||||
|
return store.uiLoadpoints.value;
|
||||||
|
},
|
||||||
|
orderedVisibleLoadpoints() {
|
||||||
|
return this.loadpoints.filter((lp) => lp.visible);
|
||||||
|
},
|
||||||
batteryConfigured() {
|
batteryConfigured() {
|
||||||
return this.battery?.length > 0;
|
return this.battery?.length > 0;
|
||||||
},
|
},
|
||||||
|
|
@ -178,22 +183,7 @@ export default defineComponent({
|
||||||
return this.collectProps(Energyflow);
|
return this.collectProps(Energyflow);
|
||||||
},
|
},
|
||||||
loadpointTitles() {
|
loadpointTitles() {
|
||||||
return this.loadpoints.map((lp) => lp.title);
|
return this.orderedVisibleLoadpoints.map((lp) => lp.displayTitle);
|
||||||
},
|
|
||||||
loadpointsCompact() {
|
|
||||||
return this.loadpoints.map((lp, index) => {
|
|
||||||
const vehicleIcon = this.vehicles?.[lp.vehicleName]?.icon;
|
|
||||||
const icon = lp.chargerIcon || vehicleIcon || "car";
|
|
||||||
const title =
|
|
||||||
this.vehicleTitle(lp.vehicleName) ||
|
|
||||||
lp.title ||
|
|
||||||
this.$t("main.loadpoint.fallbackName");
|
|
||||||
const charging = lp.charging;
|
|
||||||
const soc = lp.vehicleSoc;
|
|
||||||
const power = lp.chargePower || 0;
|
|
||||||
const heating = lp.chargerFeatureHeating;
|
|
||||||
return { icon, title, charging, power, soc, heating, index };
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
vehicleList() {
|
vehicleList() {
|
||||||
const vehicles = this.vehicles || {};
|
const vehicles = this.vehicles || {};
|
||||||
|
|
@ -235,11 +225,8 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
selectedLoadpointChanged(index: number) {
|
selectedLoadpointChanged(id: string | undefined) {
|
||||||
this.$router.push({ query: { lp: index + 1 } });
|
this.$router.push({ query: { lp: id } });
|
||||||
},
|
|
||||||
vehicleTitle(vehicleName: string) {
|
|
||||||
return this.vehicles?.[vehicleName]?.title;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ export default function setupRouter(i18n: VueI18nInstance) {
|
||||||
props: (route) => {
|
props: (route) => {
|
||||||
const { lp } = route.query;
|
const { lp } = route.query;
|
||||||
return {
|
return {
|
||||||
selectedLoadpointIndex: lp ? parseInt(lp as string, 10) - 1 : undefined,
|
selectedLoadpointId: lp as string | undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -106,8 +106,11 @@ export default function setupRouter(i18n: VueI18nInstance) {
|
||||||
await ensureCurrentLocaleMessages(i18n);
|
await ensureCurrentLocaleMessages(i18n);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
router.afterEach(() => {
|
router.afterEach((to, from) => {
|
||||||
hideAllModals();
|
// Only hide modals when the actual route path changes, not query parameters
|
||||||
|
if (to.path !== from.path) {
|
||||||
|
hideAllModals();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,14 @@ const SETTINGS_ENERGYFLOW_DETAILS = "settings_energyflow_details";
|
||||||
const SETTINGS_ENERGYFLOW_PV = "settings_energyflow_pv";
|
const SETTINGS_ENERGYFLOW_PV = "settings_energyflow_pv";
|
||||||
const SETTINGS_ENERGYFLOW_BATTERY = "settings_energyflow_battery";
|
const SETTINGS_ENERGYFLOW_BATTERY = "settings_energyflow_battery";
|
||||||
const SETTINGS_ENERGYFLOW_LOADPOINTS = "settings_energyflow_loadpoints";
|
const SETTINGS_ENERGYFLOW_LOADPOINTS = "settings_energyflow_loadpoints";
|
||||||
const SESSION_INFO = "session_info";
|
const LOADPOINTS = "loadpoints";
|
||||||
const SESSION_COLUMNS = "session_columns";
|
const SESSION_COLUMNS = "session_columns";
|
||||||
const SAVINGS_PERIOD = "savings_period";
|
const SAVINGS_PERIOD = "savings_period";
|
||||||
const SAVINGS_REGION = "savings_region";
|
const SAVINGS_REGION = "savings_region";
|
||||||
const SESSIONS_GROUP = "sessions_group";
|
const SESSIONS_GROUP = "sessions_group";
|
||||||
const SESSIONS_TYPE = "sessions_type";
|
const SESSIONS_TYPE = "sessions_type";
|
||||||
const SETTINGS_SOLAR_ADJUSTED = "settings_solar_adjusted";
|
const SETTINGS_SOLAR_ADJUSTED = "settings_solar_adjusted";
|
||||||
|
|
||||||
function read(key: string) {
|
function read(key: string) {
|
||||||
return window.localStorage[key];
|
return window.localStorage[key];
|
||||||
}
|
}
|
||||||
|
|
@ -57,6 +58,32 @@ function saveArray(key: string) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readJSON(key: string) {
|
||||||
|
const value = read(key);
|
||||||
|
try {
|
||||||
|
return value ? JSON.parse(value) : {};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("unable to parse JSON from localStorage", { key, value, e });
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveJSON(key: string) {
|
||||||
|
return (value: object) => {
|
||||||
|
try {
|
||||||
|
save(key)(JSON.stringify(value));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("unable to stringify JSON for localStorage", { key, value, e });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadpointSettings {
|
||||||
|
order?: number;
|
||||||
|
visible?: boolean;
|
||||||
|
info?: SessionInfoKey;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
locale: keyof typeof LOCALES | null;
|
locale: keyof typeof LOCALES | null;
|
||||||
theme: THEME | null;
|
theme: THEME | null;
|
||||||
|
|
@ -67,13 +94,13 @@ export interface Settings {
|
||||||
energyflowPv: boolean;
|
energyflowPv: boolean;
|
||||||
energyflowBattery: boolean;
|
energyflowBattery: boolean;
|
||||||
energyflowLoadpoints: boolean;
|
energyflowLoadpoints: boolean;
|
||||||
sessionInfo: SessionInfoKey[];
|
|
||||||
sessionColumns: string[];
|
sessionColumns: string[];
|
||||||
savingsPeriod: string;
|
savingsPeriod: string;
|
||||||
savingsRegion: string;
|
savingsRegion: string;
|
||||||
sessionsGroup: string;
|
sessionsGroup: string;
|
||||||
sessionsType: string;
|
sessionsType: string;
|
||||||
solarAdjusted: boolean;
|
solarAdjusted: boolean;
|
||||||
|
loadpoints: Record<string, LoadpointSettings>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const settings: Settings = reactive({
|
const settings: Settings = reactive({
|
||||||
|
|
@ -86,13 +113,13 @@ const settings: Settings = reactive({
|
||||||
energyflowPv: readBool(SETTINGS_ENERGYFLOW_PV),
|
energyflowPv: readBool(SETTINGS_ENERGYFLOW_PV),
|
||||||
energyflowBattery: readBool(SETTINGS_ENERGYFLOW_BATTERY),
|
energyflowBattery: readBool(SETTINGS_ENERGYFLOW_BATTERY),
|
||||||
energyflowLoadpoints: readBool(SETTINGS_ENERGYFLOW_LOADPOINTS),
|
energyflowLoadpoints: readBool(SETTINGS_ENERGYFLOW_LOADPOINTS),
|
||||||
sessionInfo: readArray(SESSION_INFO) as SessionInfoKey[],
|
|
||||||
sessionColumns: readArray(SESSION_COLUMNS),
|
sessionColumns: readArray(SESSION_COLUMNS),
|
||||||
savingsPeriod: read(SAVINGS_PERIOD),
|
savingsPeriod: read(SAVINGS_PERIOD),
|
||||||
savingsRegion: read(SAVINGS_REGION),
|
savingsRegion: read(SAVINGS_REGION),
|
||||||
sessionsGroup: read(SESSIONS_GROUP),
|
sessionsGroup: read(SESSIONS_GROUP),
|
||||||
sessionsType: read(SESSIONS_TYPE),
|
sessionsType: read(SESSIONS_TYPE),
|
||||||
solarAdjusted: readBool(SETTINGS_SOLAR_ADJUSTED),
|
solarAdjusted: readBool(SETTINGS_SOLAR_ADJUSTED),
|
||||||
|
loadpoints: readJSON(LOADPOINTS),
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(() => settings.locale, save(SETTINGS_LOCALE));
|
watch(() => settings.locale, save(SETTINGS_LOCALE));
|
||||||
|
|
@ -104,11 +131,33 @@ watch(() => settings.energyflowDetails, saveBool(SETTINGS_ENERGYFLOW_DETAILS));
|
||||||
watch(() => settings.energyflowPv, saveBool(SETTINGS_ENERGYFLOW_PV));
|
watch(() => settings.energyflowPv, saveBool(SETTINGS_ENERGYFLOW_PV));
|
||||||
watch(() => settings.energyflowBattery, saveBool(SETTINGS_ENERGYFLOW_BATTERY));
|
watch(() => settings.energyflowBattery, saveBool(SETTINGS_ENERGYFLOW_BATTERY));
|
||||||
watch(() => settings.energyflowLoadpoints, saveBool(SETTINGS_ENERGYFLOW_LOADPOINTS));
|
watch(() => settings.energyflowLoadpoints, saveBool(SETTINGS_ENERGYFLOW_LOADPOINTS));
|
||||||
watch(() => settings.sessionInfo, saveArray(SESSION_INFO));
|
|
||||||
watch(() => settings.sessionColumns as string[], saveArray(SESSION_COLUMNS));
|
watch(() => settings.sessionColumns as string[], saveArray(SESSION_COLUMNS));
|
||||||
watch(() => settings.savingsPeriod, save(SAVINGS_PERIOD));
|
watch(() => settings.savingsPeriod, save(SAVINGS_PERIOD));
|
||||||
watch(() => settings.savingsRegion, save(SAVINGS_REGION));
|
watch(() => settings.savingsRegion, save(SAVINGS_REGION));
|
||||||
watch(() => settings.sessionsGroup, save(SESSIONS_GROUP));
|
watch(() => settings.sessionsGroup, save(SESSIONS_GROUP));
|
||||||
watch(() => settings.sessionsType, save(SESSIONS_TYPE));
|
watch(() => settings.sessionsType, save(SESSIONS_TYPE));
|
||||||
watch(() => settings.solarAdjusted, saveBool(SETTINGS_SOLAR_ADJUSTED));
|
watch(() => settings.solarAdjusted, saveBool(SETTINGS_SOLAR_ADJUSTED));
|
||||||
|
watch(() => settings.loadpoints, saveJSON(LOADPOINTS), { deep: true });
|
||||||
|
|
||||||
export default settings;
|
export default settings;
|
||||||
|
|
||||||
|
// MIGRATIONS
|
||||||
|
|
||||||
|
// Convert old comma-separated session_info to new loadpoints structure
|
||||||
|
// TODO: remove in later release
|
||||||
|
const SESSION_INFO = "session_info";
|
||||||
|
const oldSessionInfo = read(SESSION_INFO);
|
||||||
|
if (oldSessionInfo && Object.keys(settings.loadpoints).length === 0) {
|
||||||
|
const sessionInfoArray = oldSessionInfo.split(",");
|
||||||
|
sessionInfoArray.forEach((info: string, index: number) => {
|
||||||
|
if (info.trim()) {
|
||||||
|
const loadpointId = `${index + 1}`;
|
||||||
|
if (!settings.loadpoints[loadpointId]) {
|
||||||
|
settings.loadpoints[loadpointId] = {};
|
||||||
|
}
|
||||||
|
settings.loadpoints[loadpointId].info = info.trim() as SessionInfoKey;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Remove the old session_info key
|
||||||
|
delete window.localStorage[SESSION_INFO];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import { reactive } from "vue";
|
import { reactive } from "vue";
|
||||||
import type { State } from "./types/evcc";
|
import type { State } from "./types/evcc";
|
||||||
|
import { convertToUiLoadpoints } from "./uiLoadpoints";
|
||||||
|
import { useDebouncedComputed } from "./utils/useDebouncedComputed";
|
||||||
|
import settings from "./settings";
|
||||||
|
|
||||||
function setProperty(obj: object, props: string[], value: any) {
|
function setProperty(obj: object, props: string[], value: any) {
|
||||||
const prop = props.shift();
|
const prop = props.shift();
|
||||||
|
|
@ -32,12 +35,27 @@ const initialState: State = {
|
||||||
|
|
||||||
const state = reactive(initialState);
|
const state = reactive(initialState);
|
||||||
|
|
||||||
const store = {
|
// create derived loadpoints array with ui specific fields (defaults, browser settings, ...); debounce for better performance
|
||||||
|
const uiLoadpoints = useDebouncedComputed(
|
||||||
|
() => convertToUiLoadpoints(state.loadpoints, state.vehicles),
|
||||||
|
() => [state.loadpoints, state.vehicles, settings.loadpoints],
|
||||||
|
50
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface Store {
|
||||||
|
state: State; // raw state from websocket
|
||||||
|
uiLoadpoints: typeof uiLoadpoints;
|
||||||
|
offline(value: boolean): void;
|
||||||
|
update(msg: any): void;
|
||||||
|
reset(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const store: Store = {
|
||||||
state,
|
state,
|
||||||
|
uiLoadpoints,
|
||||||
offline(value: boolean) {
|
offline(value: boolean) {
|
||||||
state.offline = value;
|
state.offline = value;
|
||||||
},
|
},
|
||||||
// @ts-expect-error no-explicit-any
|
|
||||||
update(msg) {
|
update(msg) {
|
||||||
Object.keys(msg).forEach(function (k) {
|
Object.keys(msg).forEach(function (k) {
|
||||||
if (k === "log") {
|
if (k === "log") {
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ export interface FatalError {
|
||||||
export interface State {
|
export interface State {
|
||||||
offline: boolean;
|
offline: boolean;
|
||||||
startup?: boolean;
|
startup?: boolean;
|
||||||
loadpoints: [];
|
loadpoints: Loadpoint[];
|
||||||
forecast?: Forecast;
|
forecast?: Forecast;
|
||||||
currency?: CURRENCY;
|
currency?: CURRENCY;
|
||||||
fatal?: FatalError[];
|
fatal?: FatalError[];
|
||||||
|
|
@ -177,20 +177,87 @@ export enum LENGTH_UNIT {
|
||||||
MILES = "mi",
|
MILES = "mi",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoadpointCompact {
|
export interface Loadpoint {
|
||||||
icon: string;
|
batteryBoost: boolean;
|
||||||
title: string;
|
chargeCurrents?: number[];
|
||||||
charging: boolean;
|
chargeDuration: number;
|
||||||
soc?: number;
|
|
||||||
power: number;
|
|
||||||
heating?: boolean;
|
|
||||||
chargePower: number;
|
chargePower: number;
|
||||||
connected: boolean;
|
chargeRemainingDuration?: number;
|
||||||
index: number;
|
chargeRemainingEnergy?: number;
|
||||||
vehicleName: string;
|
chargeTotalImport?: number;
|
||||||
chargerIcon?: string;
|
chargeVoltages?: number[];
|
||||||
vehicleSoc: number;
|
chargedEnergy: number;
|
||||||
chargerFeatureHeating: boolean;
|
chargerFeatureHeating: boolean;
|
||||||
|
chargerFeatureIntegratedDevice: boolean;
|
||||||
|
chargerIcon: string | null;
|
||||||
|
chargerPhases1p3p: boolean;
|
||||||
|
chargerSinglePhase: boolean;
|
||||||
|
chargerStatusReason: CHARGER_STATUS_REASON | null;
|
||||||
|
charging: boolean;
|
||||||
|
connected: boolean;
|
||||||
|
connectedDuration: number;
|
||||||
|
disableDelay: number;
|
||||||
|
disableThreshold: number;
|
||||||
|
effectiveLimitSoc: number;
|
||||||
|
effectiveMaxCurrent: number;
|
||||||
|
effectiveMinCurrent: number;
|
||||||
|
effectivePlanId: number;
|
||||||
|
effectivePlanSoc: number;
|
||||||
|
effectivePlanTime: string | null;
|
||||||
|
effectivePriority: number;
|
||||||
|
enableDelay: number;
|
||||||
|
enableThreshold: number;
|
||||||
|
enabled: boolean;
|
||||||
|
limitEnergy: number;
|
||||||
|
limitSoc: number;
|
||||||
|
maxCurrent: number;
|
||||||
|
minCurrent: number;
|
||||||
|
mode: CHARGE_MODE;
|
||||||
|
offeredCurrent: number;
|
||||||
|
phaseAction: PHASE_ACTION;
|
||||||
|
phaseRemaining: number;
|
||||||
|
phasesActive: number;
|
||||||
|
phasesConfigured: number;
|
||||||
|
planActive: boolean;
|
||||||
|
planEnergy: number;
|
||||||
|
planOverrun: number;
|
||||||
|
planPrecondition: number;
|
||||||
|
planProjectedEnd: string | null;
|
||||||
|
planProjectedStart: string | null;
|
||||||
|
planTime: string | null;
|
||||||
|
priority: number;
|
||||||
|
pvAction: PV_ACTION;
|
||||||
|
pvRemaining: number;
|
||||||
|
sessionCo2PerKWh: number | null;
|
||||||
|
sessionEnergy: number;
|
||||||
|
sessionPrice: number | null;
|
||||||
|
sessionPricePerKWh: number | null;
|
||||||
|
sessionSolarPercentage: number;
|
||||||
|
smartCostActive: boolean;
|
||||||
|
smartCostLimit: number | null;
|
||||||
|
smartCostNextStart: string | null;
|
||||||
|
smartFeedInPriorityActive: boolean;
|
||||||
|
smartFeedInPriorityLimit: number | null;
|
||||||
|
smartFeedInPriorityNextStart: string | null;
|
||||||
|
title: string;
|
||||||
|
vehicleClimaterActive: boolean | null;
|
||||||
|
vehicleDetectionActive: boolean;
|
||||||
|
vehicleLimitSoc: number;
|
||||||
|
vehicleName: string;
|
||||||
|
vehicleOdometer: number;
|
||||||
|
vehicleRange: number;
|
||||||
|
vehicleSoc: number;
|
||||||
|
vehicleTitle: string;
|
||||||
|
vehicleWelcomeActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UiLoadpoint extends Loadpoint {
|
||||||
|
// Derived/computed fields for UI display
|
||||||
|
id: string;
|
||||||
|
displayTitle: string;
|
||||||
|
icon: string;
|
||||||
|
order: number | null;
|
||||||
|
visible: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum THEME {
|
export enum THEME {
|
||||||
|
|
@ -238,6 +305,24 @@ export enum PHASES {
|
||||||
THREE_PHASES = 3,
|
THREE_PHASES = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PHASE_ACTION {
|
||||||
|
INACTIVE = "inactive",
|
||||||
|
SCALE_1P = "scale1p",
|
||||||
|
SCALE_3P = "scale3p",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PV_ACTION {
|
||||||
|
INACTIVE = "inactive",
|
||||||
|
ENABLE = "enable",
|
||||||
|
DISABLE = "disable",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CHARGER_STATUS_REASON {
|
||||||
|
UNKNOWN = "unknown",
|
||||||
|
WAITING_FOR_AUTHORIZATION = "waitingforauthorization",
|
||||||
|
DISCONNECT_REQUIRED = "disconnectrequired",
|
||||||
|
}
|
||||||
|
|
||||||
export enum LOADPOINT_TYPE {
|
export enum LOADPOINT_TYPE {
|
||||||
CHARGING = "charging",
|
CHARGING = "charging",
|
||||||
HEATING = "heating",
|
HEATING = "heating",
|
||||||
|
|
|
||||||
77
assets/js/uiLoadpoints.ts
Normal file
77
assets/js/uiLoadpoints.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import settings from "./settings";
|
||||||
|
import type { UiLoadpoint, SessionInfoKey, Loadpoint, Vehicle } from "./types/evcc";
|
||||||
|
|
||||||
|
const get = (id: string) => {
|
||||||
|
if (!settings.loadpoints[id]) {
|
||||||
|
settings.loadpoints[id] = {};
|
||||||
|
}
|
||||||
|
return settings.loadpoints[id];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertToUiLoadpoints = (
|
||||||
|
loadpoints: Loadpoint[],
|
||||||
|
vehicles: Record<string, Vehicle>
|
||||||
|
): UiLoadpoint[] => {
|
||||||
|
if (loadpoints.length === 0) return [];
|
||||||
|
|
||||||
|
const mappedLoadpoints = loadpoints.map((lp, originalIndex) => {
|
||||||
|
const vehicle = vehicles[lp.vehicleName];
|
||||||
|
const id = `${originalIndex + 1}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...lp,
|
||||||
|
id,
|
||||||
|
displayTitle: vehicle?.title || lp.title || "Charging point",
|
||||||
|
icon: lp.chargerIcon || vehicle?.icon || "car",
|
||||||
|
order: getLoadpointOrder(id),
|
||||||
|
visible: isLoadpointVisible(id),
|
||||||
|
sessionInfo: getLoadpointSessionInfo(id),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by order (loadpoints with no order go to the end)
|
||||||
|
return mappedLoadpoints.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLoadpointOrder = (id: string): number | null => {
|
||||||
|
return get(id).order ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setLoadpointOrder = (orderedIds: string[]) => {
|
||||||
|
// Update order for all loadpoints in the ordered list
|
||||||
|
orderedIds.forEach((id, index) => {
|
||||||
|
get(id).order = index;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isLoadpointVisible = (id: string): boolean => {
|
||||||
|
return get(id).visible ?? true; // Default to visible
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setLoadpointVisibility = (id: string, visible: boolean) => {
|
||||||
|
get(id).visible = visible;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLoadpointSessionInfo = (id: string): SessionInfoKey | undefined => {
|
||||||
|
return get(id).info;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setLoadpointSessionInfo = (id: string, value: SessionInfoKey) => {
|
||||||
|
get(id).info = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetLoadpointsOrder = () => {
|
||||||
|
Object.keys(settings.loadpoints).forEach((id) => {
|
||||||
|
if (settings.loadpoints[id]) {
|
||||||
|
settings.loadpoints[id].order = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetLoadpointsVisible = () => {
|
||||||
|
Object.keys(settings.loadpoints).forEach((id) => {
|
||||||
|
if (settings.loadpoints[id]) {
|
||||||
|
settings.loadpoints[id].visible = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
9
assets/js/utils/debounce.ts
Normal file
9
assets/js/utils/debounce.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import type { Timeout } from "@/types/evcc";
|
||||||
|
|
||||||
|
export function debounce<T extends (...args: any[]) => any>(fn: T, delay: number): T {
|
||||||
|
let timer: Timeout;
|
||||||
|
return ((...args: any[]) => {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => fn(...args), delay);
|
||||||
|
}) as T;
|
||||||
|
}
|
||||||
17
assets/js/utils/useDebouncedComputed.ts
Normal file
17
assets/js/utils/useDebouncedComputed.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { ref, watch, type Ref } from "vue";
|
||||||
|
import { debounce } from "./debounce";
|
||||||
|
|
||||||
|
export function useDebouncedComputed<T>(
|
||||||
|
getter: () => T,
|
||||||
|
deps: () => any,
|
||||||
|
delay: number = 100
|
||||||
|
): Ref<T> {
|
||||||
|
const result = ref<T>();
|
||||||
|
const debouncedUpdate = debounce(() => {
|
||||||
|
result.value = getter();
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
watch(deps, debouncedUpdate, { immediate: true, deep: true });
|
||||||
|
|
||||||
|
return result as Ref<T>;
|
||||||
|
}
|
||||||
|
|
@ -67,20 +67,24 @@ export default defineComponent({
|
||||||
batteryModalAvailabe() {
|
batteryModalAvailabe() {
|
||||||
return store.state.battery?.length;
|
return store.state.battery?.length;
|
||||||
},
|
},
|
||||||
|
state() {
|
||||||
|
const { state, uiLoadpoints } = store;
|
||||||
|
return { ...state, uiLoadpoints: uiLoadpoints.value };
|
||||||
|
},
|
||||||
globalSettingsProps() {
|
globalSettingsProps() {
|
||||||
return this.collectProps(GlobalSettingsModal, store.state);
|
return this.collectProps(GlobalSettingsModal, this.state);
|
||||||
},
|
},
|
||||||
batterySettingsProps() {
|
batterySettingsProps() {
|
||||||
return this.collectProps(BatterySettingsModal, store.state);
|
return this.collectProps(BatterySettingsModal, this.state);
|
||||||
},
|
},
|
||||||
offlineIndicatorProps() {
|
offlineIndicatorProps() {
|
||||||
return this.collectProps(OfflineIndicator, store.state);
|
return this.collectProps(OfflineIndicator, this.state);
|
||||||
},
|
},
|
||||||
forecastModalProps() {
|
forecastModalProps() {
|
||||||
return this.collectProps(ForecastModal, store.state);
|
return this.collectProps(ForecastModal, this.state);
|
||||||
},
|
},
|
||||||
loginModalProps() {
|
loginModalProps() {
|
||||||
return this.collectProps(LoginModal, store.state);
|
return this.collectProps(LoginModal, this.state);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
|
||||||
|
|
@ -390,6 +390,7 @@
|
||||||
<ShmModal @changed="loadDirty" />
|
<ShmModal @changed="loadDirty" />
|
||||||
<MessagingModal @changed="yamlChanged" />
|
<MessagingModal @changed="yamlChanged" />
|
||||||
<TariffsModal @changed="yamlChanged" />
|
<TariffsModal @changed="yamlChanged" />
|
||||||
|
<TelemetryModal :sponsor="sponsor" :telemetry="telemetry" />
|
||||||
<ModbusProxyModal @changed="yamlChanged" />
|
<ModbusProxyModal @changed="yamlChanged" />
|
||||||
<CircuitsModal
|
<CircuitsModal
|
||||||
:gridMeter="gridMeter"
|
:gridMeter="gridMeter"
|
||||||
|
|
@ -443,6 +444,7 @@ import restart, { performRestart } from "../restart";
|
||||||
import SponsorModal from "../components/Config/SponsorModal.vue";
|
import SponsorModal from "../components/Config/SponsorModal.vue";
|
||||||
import store from "../store";
|
import store from "../store";
|
||||||
import TariffsModal from "../components/Config/TariffsModal.vue";
|
import TariffsModal from "../components/Config/TariffsModal.vue";
|
||||||
|
import TelemetryModal from "../components/Config/TelemetryModal.vue";
|
||||||
import Header from "../components/Top/Header.vue";
|
import Header from "../components/Top/Header.vue";
|
||||||
import VehicleIcon from "../components/VehicleIcon";
|
import VehicleIcon from "../components/VehicleIcon";
|
||||||
import VehicleModal from "../components/Config/VehicleModal.vue";
|
import VehicleModal from "../components/Config/VehicleModal.vue";
|
||||||
|
|
@ -500,6 +502,7 @@ export default defineComponent({
|
||||||
NotificationIcon,
|
NotificationIcon,
|
||||||
SponsorModal,
|
SponsorModal,
|
||||||
TariffsModal,
|
TariffsModal,
|
||||||
|
TelemetryModal,
|
||||||
TopHeader: Header,
|
TopHeader: Header,
|
||||||
VehicleIcon,
|
VehicleIcon,
|
||||||
VehicleModal,
|
VehicleModal,
|
||||||
|
|
@ -662,6 +665,13 @@ export default defineComponent({
|
||||||
const { name } = store.state?.sponsor || {};
|
const { name } = store.state?.sponsor || {};
|
||||||
return !!name;
|
return !!name;
|
||||||
},
|
},
|
||||||
|
sponsor() {
|
||||||
|
return store.state?.sponsor;
|
||||||
|
},
|
||||||
|
telemetry() {
|
||||||
|
// @ts-expect-error: telemetry property exists but not in TypeScript definitions
|
||||||
|
return store.state?.telemetry === true;
|
||||||
|
},
|
||||||
eebusTags() {
|
eebusTags() {
|
||||||
return { configured: { value: store.state?.eebus || false } };
|
return { configured: { value: store.state?.eebus || false } };
|
||||||
},
|
},
|
||||||
|
|
|
||||||
15
i18n/de.json
15
i18n/de.json
|
|
@ -162,6 +162,9 @@
|
||||||
"customOption": "Benutzerdefiniertes Gerät",
|
"customOption": "Benutzerdefiniertes Gerät",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"docsLink": "Siehe Dokumentation.",
|
"docsLink": "Siehe Dokumentation.",
|
||||||
|
"dragHandle": "Verschieben",
|
||||||
|
"dragItem": "Verschiebbar: {title}",
|
||||||
|
"dragList": "Sortierbare Liste",
|
||||||
"experimental": "Experimentell",
|
"experimental": "Experimentell",
|
||||||
"hideAdvancedSettings": "Erweiterte Einstellungen ausblenden",
|
"hideAdvancedSettings": "Erweiterte Einstellungen ausblenden",
|
||||||
"invalidFileSelected": "Ungültige Datei ausgewählt",
|
"invalidFileSelected": "Ungültige Datei ausgewählt",
|
||||||
|
|
@ -172,6 +175,7 @@
|
||||||
"readFromFile": "Aus Datei lesen",
|
"readFromFile": "Aus Datei lesen",
|
||||||
"remove": "Entfernen",
|
"remove": "Entfernen",
|
||||||
"required": "erforderlich",
|
"required": "erforderlich",
|
||||||
|
"reset": "Zurücksetzen",
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"selectFile": "Durchsuchen",
|
"selectFile": "Durchsuchen",
|
||||||
"showAdvancedSettings": "Erweiterte Einstellungen anzeigen",
|
"showAdvancedSettings": "Erweiterte Einstellungen anzeigen",
|
||||||
|
|
@ -510,6 +514,10 @@
|
||||||
"description": "Hinterlege deine Stromtarife, um die Kosten zu berechnen.",
|
"description": "Hinterlege deine Stromtarife, um die Kosten zu berechnen.",
|
||||||
"title": "Tarife"
|
"title": "Tarife"
|
||||||
},
|
},
|
||||||
|
"telemetry": {
|
||||||
|
"description": "Konfiguriere die Datenfreigabe, um evcc zu verbessern. Deine Privatsphäre ist uns wichtig und die Teilnahme ist völlig freiwillig.",
|
||||||
|
"title": "Telemetrie"
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"description": "Wird in der Hauptansicht und im Browser-Tab angezeigt.",
|
"description": "Wird in der Hauptansicht und im Browser-Tab angezeigt.",
|
||||||
"label": "Titel",
|
"label": "Titel",
|
||||||
|
|
@ -1088,6 +1096,7 @@
|
||||||
"vehicle": "Fahrzeug"
|
"vehicle": "Fahrzeug"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"deviceInfo": "Einstellungen, die du in diesem Dialog vornimmst, gelten nur für dieses Gerät.",
|
||||||
"fullscreen": {
|
"fullscreen": {
|
||||||
"enter": "Vollbild starten",
|
"enter": "Vollbild starten",
|
||||||
"exit": "Vollbild beenden",
|
"exit": "Vollbild beenden",
|
||||||
|
|
@ -1101,6 +1110,12 @@
|
||||||
"auto": "Automatisch",
|
"auto": "Automatisch",
|
||||||
"label": "Sprache"
|
"label": "Sprache"
|
||||||
},
|
},
|
||||||
|
"loadpoints": {
|
||||||
|
"help": "Reihenfolge und Sichtbarkeit für die Benutzeroberfläche ändern.",
|
||||||
|
"hide": "{title} ausblenden",
|
||||||
|
"label": "Ladepunkte",
|
||||||
|
"show": "{title} anzeigen"
|
||||||
|
},
|
||||||
"sponsorToken": {
|
"sponsorToken": {
|
||||||
"expires": "Dein Sponsortoken läuft {inXDays} ab. {getNewToken} und aktualisiere es hier.",
|
"expires": "Dein Sponsortoken läuft {inXDays} ab. {getNewToken} und aktualisiere es hier.",
|
||||||
"getNew": "Hol dir ein Neues",
|
"getNew": "Hol dir ein Neues",
|
||||||
|
|
|
||||||
15
i18n/en.json
15
i18n/en.json
|
|
@ -162,6 +162,9 @@
|
||||||
"customOption": "User-defined device",
|
"customOption": "User-defined device",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"docsLink": "See documentation.",
|
"docsLink": "See documentation.",
|
||||||
|
"dragHandle": "Drag handle",
|
||||||
|
"dragItem": "Draggable: {title}",
|
||||||
|
"dragList": "Reorderable list",
|
||||||
"experimental": "Experimental",
|
"experimental": "Experimental",
|
||||||
"hideAdvancedSettings": "Hide advanced settings",
|
"hideAdvancedSettings": "Hide advanced settings",
|
||||||
"invalidFileSelected": "Invalid file selected",
|
"invalidFileSelected": "Invalid file selected",
|
||||||
|
|
@ -172,6 +175,7 @@
|
||||||
"readFromFile": "Read from file",
|
"readFromFile": "Read from file",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"required": "required",
|
"required": "required",
|
||||||
|
"reset": "Reset",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"selectFile": "Browse",
|
"selectFile": "Browse",
|
||||||
"showAdvancedSettings": "Show advanced settings",
|
"showAdvancedSettings": "Show advanced settings",
|
||||||
|
|
@ -510,6 +514,10 @@
|
||||||
"description": "Define your energy tariffs to calculate the costs of your charging sessions.",
|
"description": "Define your energy tariffs to calculate the costs of your charging sessions.",
|
||||||
"title": "Tariffs"
|
"title": "Tariffs"
|
||||||
},
|
},
|
||||||
|
"telemetry": {
|
||||||
|
"description": "Configure data sharing to help improve evcc. Your privacy is important to us and participation is completely optional.",
|
||||||
|
"title": "Telemetry"
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"description": "Displayed on main screen and browser tab.",
|
"description": "Displayed on main screen and browser tab.",
|
||||||
"label": "Title",
|
"label": "Title",
|
||||||
|
|
@ -1088,6 +1096,7 @@
|
||||||
"vehicle": "Vehicle"
|
"vehicle": "Vehicle"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
"deviceInfo": "Settings you make in this dialog only affect this device.",
|
||||||
"fullscreen": {
|
"fullscreen": {
|
||||||
"enter": "Enter fullscreen",
|
"enter": "Enter fullscreen",
|
||||||
"exit": "Exit fullscreen",
|
"exit": "Exit fullscreen",
|
||||||
|
|
@ -1101,6 +1110,12 @@
|
||||||
"auto": "Automatic",
|
"auto": "Automatic",
|
||||||
"label": "Language"
|
"label": "Language"
|
||||||
},
|
},
|
||||||
|
"loadpoints": {
|
||||||
|
"help": "Change order and visibility for the UI.",
|
||||||
|
"hide": "Hide {title}",
|
||||||
|
"label": "Charging points",
|
||||||
|
"show": "Show {title}"
|
||||||
|
},
|
||||||
"sponsorToken": {
|
"sponsorToken": {
|
||||||
"expires": "You sponsor token expires {inXDays}. {getNewToken} and update it here.",
|
"expires": "You sponsor token expires {inXDays}. {getNewToken} and update it here.",
|
||||||
"getNew": "Grab a fresh one",
|
"getNew": "Grab a fresh one",
|
||||||
|
|
|
||||||
7
package-lock.json
generated
7
package-lock.json
generated
|
|
@ -7,6 +7,7 @@
|
||||||
"name": "evcc",
|
"name": "evcc",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@formkit/drag-and-drop": "^0.5.3",
|
||||||
"@guolao/vue-monaco-editor": "^1.5.5",
|
"@guolao/vue-monaco-editor": "^1.5.5",
|
||||||
"@h2d2/shopicons": "^1.9.0",
|
"@h2d2/shopicons": "^1.9.0",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
|
|
@ -2285,6 +2286,12 @@
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@formkit/drag-and-drop": {
|
||||||
|
"version": "0.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formkit/drag-and-drop/-/drag-and-drop-0.5.3.tgz",
|
||||||
|
"integrity": "sha512-VGQsKYc350OOMbz7wYt69YqDbn49x7RJ32eqiJieNke54nGZYmx18XEqTw0lBiJmH3RaiH6V45gB1u+tvI/TNw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@guolao/vue-monaco-editor": {
|
"node_modules/@guolao/vue-monaco-editor": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@guolao/vue-monaco-editor/-/vue-monaco-editor-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@guolao/vue-monaco-editor/-/vue-monaco-editor-1.5.5.tgz",
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@formkit/drag-and-drop": "^0.5.3",
|
||||||
"@guolao/vue-monaco-editor": "^1.5.5",
|
"@guolao/vue-monaco-editor": "^1.5.5",
|
||||||
"@h2d2/shopicons": "^1.9.0",
|
"@h2d2/shopicons": "^1.9.0",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
|
|
|
||||||
30
tests/loadpoint-sort.evcc.yaml
Normal file
30
tests/loadpoint-sort.evcc.yaml
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
site:
|
||||||
|
title: Loadpoint Sort Test
|
||||||
|
|
||||||
|
meters:
|
||||||
|
- name: grid
|
||||||
|
type: template
|
||||||
|
template: demo-meter
|
||||||
|
power: 200
|
||||||
|
|
||||||
|
loadpoints:
|
||||||
|
- title: First Loadpoint
|
||||||
|
charger: charger_1
|
||||||
|
mode: now
|
||||||
|
- title: Second Loadpoint
|
||||||
|
charger: charger_2
|
||||||
|
mode: now
|
||||||
|
- title: Third Loadpoint
|
||||||
|
charger: charger_3
|
||||||
|
mode: now
|
||||||
|
|
||||||
|
chargers:
|
||||||
|
- name: charger_1
|
||||||
|
type: template
|
||||||
|
template: demo-charger
|
||||||
|
- name: charger_2
|
||||||
|
type: template
|
||||||
|
template: demo-charger
|
||||||
|
- name: charger_3
|
||||||
|
type: template
|
||||||
|
template: demo-charger
|
||||||
112
tests/loadpoint-sort.spec.ts
Normal file
112
tests/loadpoint-sort.spec.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import { test, expect, type Page, type Locator } from "@playwright/test";
|
||||||
|
import { start, stop, baseUrl } from "./evcc";
|
||||||
|
import { expectModalVisible, expectModalHidden, openTopNavigation, dragElement } from "./utils";
|
||||||
|
|
||||||
|
const CONFIG_LOADPOINT_SORT = "loadpoint-sort.evcc.yaml";
|
||||||
|
|
||||||
|
test.use({ baseURL: baseUrl() });
|
||||||
|
test.describe.configure({ mode: "parallel" });
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
await start(CONFIG_LOADPOINT_SORT);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
});
|
||||||
|
|
||||||
|
async function openModal(page: Page) {
|
||||||
|
await openTopNavigation(page);
|
||||||
|
await page.getByTestId("topnavigation-settings").click();
|
||||||
|
const modal = page.getByTestId("global-settings-modal");
|
||||||
|
await expectModalVisible(modal);
|
||||||
|
return modal;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeModal(modal: Locator) {
|
||||||
|
await modal.getByRole("button", { name: "Close" }).click();
|
||||||
|
await expectModalHidden(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("loadpoint ordering and hiding", async () => {
|
||||||
|
test("initial loadpoint order", async ({ page }) => {
|
||||||
|
await expect(page.getByTestId("loadpoint")).toHaveCount(3);
|
||||||
|
|
||||||
|
const loadpoints = page.getByTestId("loadpoint");
|
||||||
|
await expect(loadpoints.nth(0)).toContainText("First Loadpoint");
|
||||||
|
await expect(loadpoints.nth(1)).toContainText("Second Loadpoint");
|
||||||
|
await expect(loadpoints.nth(2)).toContainText("Third Loadpoint");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("hide a loadpoint", async ({ page }) => {
|
||||||
|
const modal = await openModal(page);
|
||||||
|
|
||||||
|
await modal.getByRole("switch", { name: "Hide Second Loadpoint" }).click();
|
||||||
|
|
||||||
|
await closeModal(modal);
|
||||||
|
|
||||||
|
await expect(page.getByTestId("loadpoint")).toHaveCount(2);
|
||||||
|
await expect(page.getByRole("heading", { name: "Second Loadpoint" })).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reorder loadpoints", async ({ page }) => {
|
||||||
|
const modal = await openModal(page);
|
||||||
|
|
||||||
|
const firstLoadpointDragItem = modal.getByRole("listitem", {
|
||||||
|
name: "Draggable: First Loadpoint",
|
||||||
|
});
|
||||||
|
const thirdLoadpointDragItem = modal.getByRole("listitem", {
|
||||||
|
name: "Draggable: Third Loadpoint",
|
||||||
|
});
|
||||||
|
|
||||||
|
await dragElement(page, firstLoadpointDragItem, thirdLoadpointDragItem);
|
||||||
|
|
||||||
|
await closeModal(modal);
|
||||||
|
|
||||||
|
await expect(page.getByTestId("loadpoint")).toHaveCount(3);
|
||||||
|
const reorderedLoadpoints = page.getByTestId("loadpoint");
|
||||||
|
await expect(reorderedLoadpoints.nth(0)).toContainText("Second Loadpoint");
|
||||||
|
await expect(reorderedLoadpoints.nth(1)).toContainText("Third Loadpoint");
|
||||||
|
await expect(reorderedLoadpoints.nth(2)).toContainText("First Loadpoint");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reset to initial state", async ({ page }) => {
|
||||||
|
const modal = await openModal(page);
|
||||||
|
|
||||||
|
await modal.getByRole("switch", { name: "Hide Second Loadpoint" }).click();
|
||||||
|
|
||||||
|
const firstLoadpointDragItem = modal.getByRole("listitem", {
|
||||||
|
name: "Draggable: First Loadpoint",
|
||||||
|
});
|
||||||
|
const thirdLoadpointDragItem = modal.getByRole("listitem", {
|
||||||
|
name: "Draggable: Third Loadpoint",
|
||||||
|
});
|
||||||
|
|
||||||
|
await dragElement(page, firstLoadpointDragItem, thirdLoadpointDragItem);
|
||||||
|
|
||||||
|
await modal.getByRole("button", { name: "Reset" }).click();
|
||||||
|
|
||||||
|
await closeModal(modal);
|
||||||
|
|
||||||
|
await expect(page.getByTestId("loadpoint")).toHaveCount(3);
|
||||||
|
const loadpoints = page.getByTestId("loadpoint");
|
||||||
|
await expect(loadpoints.nth(0)).toContainText("First Loadpoint");
|
||||||
|
await expect(loadpoints.nth(1)).toContainText("Second Loadpoint");
|
||||||
|
await expect(loadpoints.nth(2)).toContainText("Third Loadpoint");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("persist changes after page reload", async ({ page }) => {
|
||||||
|
const modal = await openModal(page);
|
||||||
|
|
||||||
|
await modal.getByRole("switch", { name: "Hide Second Loadpoint" }).click();
|
||||||
|
await closeModal(modal);
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
await expect(page.getByTestId("loadpoint")).toHaveCount(2);
|
||||||
|
await expect(page.getByRole("heading", { name: "Second Loadpoint" })).not.toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -123,3 +123,26 @@ export async function newLoadpoint(
|
||||||
.click();
|
.click();
|
||||||
await lpModal.getByLabel("Title").fill(title);
|
await lpModal.getByLabel("Title").fill(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function dragElement(
|
||||||
|
page: Page,
|
||||||
|
sourceElement: Locator,
|
||||||
|
targetElement: Locator
|
||||||
|
): Promise<void> {
|
||||||
|
// Get bounding boxes to calculate actual positions
|
||||||
|
const sourceBox = await sourceElement.boundingBox();
|
||||||
|
const targetBox = await targetElement.boundingBox();
|
||||||
|
|
||||||
|
if (sourceBox && targetBox) {
|
||||||
|
// Move from center of source item to center of target item
|
||||||
|
const startX = sourceBox.x + sourceBox.width / 2;
|
||||||
|
const startY = sourceBox.y + sourceBox.height / 2;
|
||||||
|
const endX = targetBox.x + targetBox.width / 2;
|
||||||
|
const endY = targetBox.y + targetBox.height / 2;
|
||||||
|
|
||||||
|
await page.mouse.move(startX, startY);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(endX, endY, { steps: 10 });
|
||||||
|
await page.mouse.up();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue