Add setting modals to top navigation (#11405)
This commit is contained in:
parent
0b906727a0
commit
41a5dd7355
13 changed files with 207 additions and 46 deletions
|
|
@ -78,6 +78,7 @@
|
|||
--evcc-accent2: var(--evcc-dark-green);
|
||||
--evcc-accent3: var(--evcc-darker-green);
|
||||
--bs-primary: var(--evcc-dark-green);
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.175);
|
||||
}
|
||||
|
||||
html {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<div
|
||||
class="energyflow cursor-pointer position-relative"
|
||||
:class="{ 'energyflow--open': detailsOpen }"
|
||||
data-testid="energyflow"
|
||||
@click="toggleDetails"
|
||||
>
|
||||
<div class="row">
|
||||
|
|
@ -78,6 +79,7 @@
|
|||
:details="batterySoc"
|
||||
:detailsFmt="batteryFmt"
|
||||
detailsClickable
|
||||
data-testid="energyflow-entry-batterydischarge"
|
||||
@details-clicked="openBatterySettingsModal"
|
||||
/>
|
||||
<EnergyflowEntry
|
||||
|
|
@ -87,8 +89,9 @@
|
|||
:powerInKw="powerInKw"
|
||||
:details="detailsValue(tariffGrid, tariffCo2)"
|
||||
:detailsFmt="detailsFmt"
|
||||
:detailsClickable="smartCostAvailable"
|
||||
:detailsClickable="gridModalAvailable"
|
||||
:detailsTooltip="detailsTooltip(tariffGrid, tariffCo2)"
|
||||
data-testid="energyflow-entry-gridimport"
|
||||
@details-clicked="openGridSettingsModal"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -157,8 +160,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<GridSettingsModal v-bind="gridSettings" />
|
||||
<BatterySettingsModal v-bind="batterySettings" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -167,13 +168,12 @@ import "@h2d2/shopicons/es/filled/square";
|
|||
import Modal from "bootstrap/js/dist/modal";
|
||||
import Visualization from "./Visualization.vue";
|
||||
import EnergyflowEntry from "./EnergyflowEntry.vue";
|
||||
import GridSettingsModal from "../GridSettingsModal.vue";
|
||||
import formatter from "../../mixins/formatter";
|
||||
import AnimatedNumber from "../AnimatedNumber.vue";
|
||||
import settings from "../../settings";
|
||||
import { CO2_TYPE, PRICE_DYNAMIC_TYPE, PRICE_FORECAST_TYPE } from "../../units";
|
||||
import { CO2_TYPE } from "../../units";
|
||||
import collector from "../../mixins/collector";
|
||||
import BatterySettingsModal from "../BatterySettingsModal.vue";
|
||||
import gridModalAvailable from "../../utils/gridModalAvailable";
|
||||
|
||||
export default {
|
||||
name: "Energyflow",
|
||||
|
|
@ -181,8 +181,6 @@ export default {
|
|||
Visualization,
|
||||
EnergyflowEntry,
|
||||
AnimatedNumber,
|
||||
GridSettingsModal,
|
||||
BatterySettingsModal,
|
||||
},
|
||||
mixins: [formatter, collector],
|
||||
props: {
|
||||
|
|
@ -217,8 +215,8 @@ export default {
|
|||
return { detailsOpen: false, detailsCompleteHeight: null, gridSettingsModal: null };
|
||||
},
|
||||
computed: {
|
||||
smartCostAvailable: function () {
|
||||
return [CO2_TYPE, PRICE_DYNAMIC_TYPE, PRICE_FORECAST_TYPE].includes(this.smartCostType);
|
||||
gridModalAvailable: function () {
|
||||
return gridModalAvailable(this.smartCostType);
|
||||
},
|
||||
gridImport: function () {
|
||||
return Math.max(0, this.gridPower);
|
||||
|
|
@ -288,27 +286,15 @@ export default {
|
|||
batteryFmt() {
|
||||
return (soc) => `${Math.round(soc)}%`;
|
||||
},
|
||||
gridSettings() {
|
||||
return this.collectProps(GridSettingsModal);
|
||||
},
|
||||
batterySettings() {
|
||||
return this.collectProps(BatterySettingsModal);
|
||||
},
|
||||
co2Available() {
|
||||
return this.smartCostType === CO2_TYPE;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.gridSettingsModal = Modal.getOrCreateInstance(
|
||||
document.querySelector("#gridSettingsModal")
|
||||
);
|
||||
this.batterySettingsModal = Modal.getOrCreateInstance(
|
||||
document.querySelector("#batterySettingsModal")
|
||||
);
|
||||
window.addEventListener("resize", this.updateHeight);
|
||||
// height must be calculated in case of initially open details
|
||||
if (settings.energyflowDetails) {
|
||||
setTimeout(this.toggleDetails, 50);
|
||||
setTimeout(this.toggleDetails, 100);
|
||||
}
|
||||
},
|
||||
unmounted() {
|
||||
|
|
@ -349,10 +335,14 @@ export default {
|
|||
this.detailsCompleteHeight = this.$refs.detailsInner.offsetHeight;
|
||||
},
|
||||
openGridSettingsModal() {
|
||||
this.gridSettingsModal.show();
|
||||
const modal = Modal.getOrCreateInstance(document.getElementById("gridSettingsModal"));
|
||||
modal.show();
|
||||
},
|
||||
openBatterySettingsModal() {
|
||||
this.batterySettingsModal.show();
|
||||
const modal = Modal.getOrCreateInstance(
|
||||
document.getElementById("batterySettingsModal")
|
||||
);
|
||||
modal.show();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
ref="details"
|
||||
class="fw-normal"
|
||||
:class="{ 'text-decoration-underline': detailsClickable }"
|
||||
data-testid="energyflow-entry-details"
|
||||
data-bs-toggle="tooltip"
|
||||
:tabindex="detailsClickable ? 0 : undefined"
|
||||
@click="detailsClicked"
|
||||
|
|
|
|||
|
|
@ -26,18 +26,32 @@
|
|||
{{ $t("header.sessions") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item" @click="openSettingsModal">
|
||||
<span
|
||||
v-if="sponsorTokenExpires"
|
||||
class="d-inline-block p-1 rounded-circle bg-danger border border-light rounded-circle"
|
||||
></span>
|
||||
{{ $t("header.settings") }}
|
||||
{{ $t("settings.title") }}
|
||||
</button>
|
||||
</li>
|
||||
<li v-if="batteryModalAvailable">
|
||||
<button type="button" class="dropdown-item" @click="openBatterySettingsModal">
|
||||
{{ $t("batterySettings.modalTitle") }}
|
||||
</button>
|
||||
</li>
|
||||
<li v-if="gridModalAvailable">
|
||||
<button type="button" class="dropdown-item" @click="openGridSettingsModal">
|
||||
{{ $t("gridSettings.modalTitle") }}
|
||||
</button>
|
||||
</li>
|
||||
<li v-if="$hiddenFeatures()">
|
||||
<router-link class="dropdown-item" to="/config"> Configuration 🧪 </router-link>
|
||||
<router-link class="dropdown-item" to="/config">
|
||||
Device Configuration 🧪
|
||||
</router-link>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
<template v-if="providerLogins.length > 0">
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
<li>
|
||||
|
|
@ -73,8 +87,6 @@
|
|||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<GlobalSettingsModal v-bind="globalSettingsModalProps" />
|
||||
<HelpModal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -85,15 +97,13 @@ import "@h2d2/shopicons/es/regular/gift";
|
|||
import "@h2d2/shopicons/es/regular/moonstars";
|
||||
import "@h2d2/shopicons/es/regular/menu";
|
||||
import "@h2d2/shopicons/es/regular/newtab";
|
||||
import GlobalSettingsModal from "./GlobalSettingsModal.vue";
|
||||
import HelpModal from "./HelpModal.vue";
|
||||
import collector from "../mixins/collector";
|
||||
import gridModalAvailable from "../utils/gridModalAvailable";
|
||||
|
||||
import baseAPI from "../baseapi";
|
||||
|
||||
export default {
|
||||
name: "TopNavigation",
|
||||
components: { GlobalSettingsModal, HelpModal },
|
||||
mixins: [collector],
|
||||
props: {
|
||||
vehicleLogins: {
|
||||
|
|
@ -104,11 +114,10 @@ export default {
|
|||
},
|
||||
sponsor: String,
|
||||
sponsorTokenExpires: Number,
|
||||
batteryConfigured: Boolean,
|
||||
smartCostType: String,
|
||||
},
|
||||
computed: {
|
||||
globalSettingsModalProps: function () {
|
||||
return this.collectProps(GlobalSettingsModal);
|
||||
},
|
||||
logoutCount() {
|
||||
return this.providerLogins.filter((login) => !login.loggedIn).length;
|
||||
},
|
||||
|
|
@ -126,6 +135,12 @@ export default {
|
|||
showBadge() {
|
||||
return this.loginRequired || this.sponsorTokenExpires;
|
||||
},
|
||||
batteryModalAvailable() {
|
||||
return this.batteryConfigured;
|
||||
},
|
||||
gridModalAvailable: function () {
|
||||
return gridModalAvailable(this.smartCostType);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const $el = document.getElementById("topNavigatonDropdown");
|
||||
|
|
@ -155,6 +170,16 @@ export default {
|
|||
const modal = Modal.getOrCreateInstance(document.getElementById("helpModal"));
|
||||
modal.show();
|
||||
},
|
||||
openBatterySettingsModal() {
|
||||
const modal = Modal.getOrCreateInstance(
|
||||
document.getElementById("batterySettingsModal")
|
||||
);
|
||||
modal.show();
|
||||
},
|
||||
openGridSettingsModal() {
|
||||
const modal = Modal.getOrCreateInstance(document.getElementById("gridSettingsModal"));
|
||||
modal.show();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
export default {
|
||||
methods: {
|
||||
// collect all target component properties from current instance
|
||||
collectProps: function (component) {
|
||||
collectProps: function (component, state) {
|
||||
let data = {};
|
||||
for (var p in component.props) {
|
||||
// check in optional state
|
||||
if (state && p in state) {
|
||||
data[p] = state[p];
|
||||
}
|
||||
// check in current instance
|
||||
if (p in this) {
|
||||
data[p] = this[p];
|
||||
}
|
||||
|
|
|
|||
5
assets/js/utils/gridModalAvailable.js
Normal file
5
assets/js/utils/gridModalAvailable.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { CO2_TYPE, PRICE_DYNAMIC_TYPE, PRICE_FORECAST_TYPE } from "../units";
|
||||
|
||||
export default function (smartCostType) {
|
||||
return [CO2_TYPE, PRICE_DYNAMIC_TYPE, PRICE_FORECAST_TYPE].includes(smartCostType);
|
||||
}
|
||||
|
|
@ -1,14 +1,27 @@
|
|||
<template>
|
||||
<div class="app">
|
||||
<router-view :notifications="notifications" :offline="offline"></router-view>
|
||||
|
||||
<GlobalSettingsModal v-bind="globalSettingsProps" />
|
||||
<BatterySettingsModal v-if="batteryModalAvailabe" v-bind="batterySettingsProps" />
|
||||
<GridSettingsModal v-if="gridModalAvailable" v-bind="gridSettingsProps" />
|
||||
<HelpModal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from "../store";
|
||||
import GlobalSettingsModal from "../components/GlobalSettingsModal.vue";
|
||||
import BatterySettingsModal from "../components/BatterySettingsModal.vue";
|
||||
import GridSettingsModal from "../components/GridSettingsModal.vue";
|
||||
import HelpModal from "../components/HelpModal.vue";
|
||||
import collector from "../mixins/collector";
|
||||
import gridModalAvailable from "../utils/gridModalAvailable";
|
||||
|
||||
export default {
|
||||
name: "App",
|
||||
components: { GlobalSettingsModal, HelpModal, BatterySettingsModal, GridSettingsModal },
|
||||
mixins: [collector],
|
||||
props: {
|
||||
notifications: Array,
|
||||
offline: Boolean,
|
||||
|
|
@ -20,6 +33,23 @@ export default {
|
|||
const siteTitle = store.state.siteTitle;
|
||||
return { title: siteTitle ? `${siteTitle} | evcc` : "evcc" };
|
||||
},
|
||||
computed: {
|
||||
gridModalAvailable: function () {
|
||||
return gridModalAvailable(store.state.smartCostType);
|
||||
},
|
||||
batteryModalAvailabe: function () {
|
||||
return store.state.batteryConfigured;
|
||||
},
|
||||
globalSettingsProps: function () {
|
||||
return this.collectProps(GlobalSettingsModal, store.state);
|
||||
},
|
||||
batterySettingsProps() {
|
||||
return this.collectProps(BatterySettingsModal, store.state);
|
||||
},
|
||||
gridSettingsProps() {
|
||||
return this.collectProps(GridSettingsModal, store.state);
|
||||
},
|
||||
},
|
||||
mounted: function () {
|
||||
this.connect();
|
||||
document.addEventListener("visibilitychange", this.pageVisibilityChanged, false);
|
||||
|
|
|
|||
|
|
@ -140,7 +140,6 @@ github = "GitHub"
|
|||
login = "Fahrzeug-Logins"
|
||||
needHelp = "Hilfe benötigt?"
|
||||
sessions = "Ladevorgänge"
|
||||
settings = "Einstellungen"
|
||||
|
||||
[help]
|
||||
discussionsButton = "GitHub Discussions"
|
||||
|
|
@ -384,7 +383,7 @@ allVehicles = "Alle Fahrzeuge"
|
|||
filter = "Filtern"
|
||||
|
||||
[settings]
|
||||
title = "Einstellungen"
|
||||
title = "Allgemeine Einstellungen"
|
||||
|
||||
[settings.hiddenFeatures]
|
||||
label = "Experimentell"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ legendTitle = "How should solar energy be used?"
|
|||
legendTopAutostart = "starts automatically"
|
||||
legendTopName = "battery supported charging"
|
||||
legendTopSubline = "without interruptions"
|
||||
modalTitle = "Battery settings"
|
||||
modalTitle = "Battery Settings"
|
||||
|
||||
[batterySettings.bufferStart]
|
||||
full = "when nearly full"
|
||||
|
|
@ -134,10 +134,9 @@ about = "About"
|
|||
blog = "Blog"
|
||||
docs = "Documentation"
|
||||
github = "GitHub"
|
||||
login = "Vehicle logins"
|
||||
login = "Vehicle Logins"
|
||||
needHelp = "Need help?"
|
||||
sessions = "Charging sessions"
|
||||
settings = "Settings"
|
||||
sessions = "Charging Sessions"
|
||||
|
||||
[help]
|
||||
discussionsButton = "GitHub discussions"
|
||||
|
|
@ -381,7 +380,7 @@ allVehicles = "all vehicles"
|
|||
filter = "Filter"
|
||||
|
||||
[settings]
|
||||
title = "Settings"
|
||||
title = "General Settings"
|
||||
|
||||
[settings.hiddenFeatures]
|
||||
label = "Experimental"
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ func NewFixedFromConfig(other map[string]interface{}) (api.Tariff, error) {
|
|||
|
||||
t := &Fixed{
|
||||
clock: clock.New(),
|
||||
dynamic: len(cc.Zones) > 1,
|
||||
dynamic: len(cc.Zones) >= 1,
|
||||
}
|
||||
|
||||
for _, z := range cc.Zones {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const { test, expect } = require("@playwright/test");
|
||||
const { start, stop, restart } = require("./evcc");
|
||||
const { start, stop } = require("./evcc");
|
||||
const { startSimulator, stopSimulator, SIMULATOR_URL } = require("./simulator");
|
||||
|
||||
const CONFIG = "simulator.evcc.yaml";
|
||||
|
|
|
|||
103
tests/modals.spec.js
Normal file
103
tests/modals.spec.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
const { test, expect } = require("@playwright/test");
|
||||
const { start, stop } = require("./evcc");
|
||||
const { startSimulator, stopSimulator } = require("./simulator");
|
||||
|
||||
const BASICS_CONFIG = "basics.evcc.yaml";
|
||||
const SIMULATOR_CONFIG = "simulator.evcc.yaml";
|
||||
|
||||
test.describe("Basics", async () => {
|
||||
test.beforeAll(async () => {
|
||||
await start(BASICS_CONFIG);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
test("Menu options. No battery and grid.", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByTestId("topnavigation-button").click();
|
||||
await expect(page.getByRole("button", { name: "General Settings" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Battery Settings" })).not.toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Smart Grid Charging" })).not.toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Need help?" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("Need help?", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByTestId("topnavigation-button").click();
|
||||
await page.getByRole("button", { name: "Need help?" }).click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Need help?" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("General Settings", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("topnavigation-button").click();
|
||||
await page.getByRole("button", { name: "General Settings" }).click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "General Settings" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Advanced", async () => {
|
||||
test.beforeAll(async () => {
|
||||
await start(SIMULATOR_CONFIG);
|
||||
await startSimulator();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await stopSimulator();
|
||||
await stop();
|
||||
});
|
||||
|
||||
test("Menu options. All available.", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await page.getByTestId("topnavigation-button").click();
|
||||
await expect(page.getByRole("button", { name: "General Settings" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Battery Settings" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Smart Grid Charging" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Need help?" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("Battery Settings from top navigation", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("topnavigation-button").click();
|
||||
await page.getByRole("button", { name: "Battery Settings" }).click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Battery Settings" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("Battery Settings from energyflow", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("energyflow").click();
|
||||
await page
|
||||
.getByTestId("energyflow-entry-batterydischarge")
|
||||
.getByTestId("energyflow-entry-details")
|
||||
.click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Battery Settings" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("Smart Grid Charging from top navigation", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("topnavigation-button").click();
|
||||
await page.getByRole("button", { name: "Smart Grid Charging" }).click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Smart Grid Charging" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("Smart Grid Charging from energyflow", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("energyflow").click();
|
||||
await page
|
||||
.getByTestId("energyflow-entry-gridimport")
|
||||
.getByTestId("energyflow-entry-details")
|
||||
.click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Smart Grid Charging" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
@ -95,4 +95,7 @@ tariffs:
|
|||
currency: EUR
|
||||
grid:
|
||||
type: fixed
|
||||
price: 0.40
|
||||
price: 0.4 # EUR/kWh
|
||||
zones:
|
||||
- hours: 1-6
|
||||
price: 0.2
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue