About modal: powered by footer for branded installations, update status badge (#33233)

This commit is contained in:
Michael Geers 2026-08-27 20:45:50 +02:00 • committed by GitHub
parent 27bbdce298
commit 8905c2777b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 114 additions and 49 deletions

View file

@ -1,4 +1,11 @@
import { StorybookConfig } from "@storybook/vue3-vite"; import { StorybookConfig } from "@storybook/vue3-vite";
import { createReadStream } from "node:fs";
// serve test logos so the custom branding stories can render them
const customLogos = {
"/custom-logo-light": "tests/custom-logo-light.svg",
"/custom-logo-dark": "tests/custom-logo-dark.svg",
};
export default { export default {
stories: ["../assets/js/**/*.stories.@(js|ts)"], stories: ["../assets/js/**/*.stories.@(js|ts)"],
@ -10,4 +17,21 @@ export default {
core: { core: {
disableTelemetry: true, disableTelemetry: true,
}, },
viteFinal: (config) => ({
...config,
plugins: [
...(config.plugins ?? []),
{
name: "evcc-custom-logos",
configureServer(server) {
server.middlewares.use((req, res, next) => {
const file = customLogos[req.url?.split("?")[0] as keyof typeof customLogos];
if (!file) return next();
res.setHeader("Content-Type", "image/svg+xml");
createReadStream(file).pipe(res);
});
},
},
],
}),
} satisfies StorybookConfig; } satisfies StorybookConfig;

View file

@ -52,6 +52,11 @@ export default {
hasUpdater: { control: "boolean" }, hasUpdater: { control: "boolean" },
uploadMessage: { control: "text" }, uploadMessage: { control: "text" },
uploadProgress: { control: "number" }, uploadProgress: { control: "number" },
customLogo: { control: "boolean" },
customBrand: { control: "text" },
customWebsite: { control: "text" },
customEmail: { control: "text" },
customPhone: { control: "text" },
}, },
} as Meta<typeof AboutModal>; } as Meta<typeof AboutModal>;
@ -102,3 +107,14 @@ export const DevBuild = Template.bind({});
DevBuild.args = { DevBuild.args = {
installed: "0.0.0", installed: "0.0.0",
}; };
export const CustomBranding = Template.bind({});
CustomBranding.args = {
installed: "0.303.1",
availableVersion: "0.303.1",
customLogo: true,
customBrand: "G1GA HEMS",
customWebsite: "https://example.com/hems",
customEmail: "support@example.com",
customPhone: "+49 123 456789",
};

View file

@ -1,5 +1,5 @@
<template> <template>
<GenericModal id="aboutModal" :size="modalSize" @opened="acknowledge"> <GenericModal id="aboutModal" @opened="acknowledge">
<template #title> <template #title>
<a :href="websiteUrl" target="_blank" rel="noopener noreferrer"> <a :href="websiteUrl" target="_blank" rel="noopener noreferrer">
<template v-if="customLogo"> <template v-if="customLogo">
@ -44,14 +44,12 @@
> >
v{{ installed }} v{{ installed }}
</a> </a>
<span <Badge v-if="newVersionAvailable">
v-if="!nightly && !newVersionAvailable" {{ $t("footer.version.availableLong") }}
class="text-muted text-nowrap" </Badge>
>{{ $t("footer.version.latestVersion") }}</span <Badge v-else-if="!nightly" variant="muted">
> {{ $t("footer.version.latestVersion") }}
<span v-if="newVersionAvailable" class="text-nowrap">{{ </Badge>
$t("footer.version.availableLong")
}}</span>
</div> </div>
</td> </td>
</tr> </tr>
@ -115,8 +113,22 @@
<!-- open source --> <!-- open source -->
<hr /> <hr />
<p class="mb-0 small d-flex flex-wrap column-gap-1"> <p
<i18n-t keypath="footer.version.madeByCommunity" tag="span"> class="mb-0 d-flex justify-content-between align-items-center flex-wrap column-gap-2 row-gap-1"
>
<span v-if="customLogo" class="d-flex align-items-center gap-2">
<span>{{ $t("footer.version.poweredBy") }}</span>
<a
:href="evccWebsiteUrl"
target="_blank"
rel="noopener noreferrer"
class="d-flex"
aria-label="evcc"
>
<Logo class="footer-logo" />
</a>
</span>
<i18n-t v-else keypath="footer.version.madeByCommunity" tag="span">
<a <a
:href="githubRepoUrl" :href="githubRepoUrl"
target="_blank" target="_blank"
@ -125,15 +137,13 @@
>{{ $t("footer.version.community") }}</a >{{ $t("footer.version.community") }}</a
> >
</i18n-t> </i18n-t>
<i18n-t keypath="footer.version.poweredByOpenSource" tag="span" class="d-inline">
<a <a
class="text-muted" class="evcc-gray"
:href="githubDependenciesUrl" :href="githubDependenciesUrl"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
>{{ $t("footer.version.openSource") }}</a >{{ $t("footer.version.openSource") }}</a
> >
</i18n-t>
</p> </p>
</div> </div>
</GenericModal> </GenericModal>
@ -144,6 +154,7 @@ import GenericModal from "./Helper/GenericModal.vue";
import "@h2d2/shopicons/es/regular/gift"; import "@h2d2/shopicons/es/regular/gift";
import "@h2d2/shopicons/es/filled/heart"; import "@h2d2/shopicons/es/filled/heart";
import Logo from "./Footer/Logo.vue"; import Logo from "./Footer/Logo.vue";
import Badge from "./Helper/Badge.vue";
import api from "@/api"; import api from "@/api";
import settings from "@/settings"; import settings from "@/settings";
import { extractDomain } from "@/utils/extractDomain"; import { extractDomain } from "@/utils/extractDomain";
@ -155,7 +166,7 @@ const EVCC_WEBSITE = "https://evcc.io/";
export default defineComponent({ export default defineComponent({
name: "AboutModal", name: "AboutModal",
components: { GenericModal, Logo }, components: { GenericModal, Logo, Badge },
props: { props: {
installed: { type: String, default: "" }, installed: { type: String, default: "" },
commit: { type: String, default: "" }, commit: { type: String, default: "" },
@ -199,15 +210,15 @@ export default defineComponent({
githubRepoUrl() { githubRepoUrl() {
return GITHUB_REPO; return GITHUB_REPO;
}, },
evccWebsiteUrl() {
return EVCC_WEBSITE;
},
githubDependenciesUrl() { githubDependenciesUrl() {
return `${GITHUB_REPO}/network/dependencies`; return `${GITHUB_REPO}/network/dependencies`;
}, },
githubCommitUrl() { githubCommitUrl() {
return `${GITHUB_REPO}/commit/${this.commit}`; return `${GITHUB_REPO}/commit/${this.commit}`;
}, },
modalSize() {
return this.newVersionAvailable ? undefined : "sm";
},
cleanedReleaseNotes() { cleanedReleaseNotes() {
if (!this.releaseNotes) return ""; if (!this.releaseNotes) return "";
return this.releaseNotes.replaceAll("<h2>Changelog</h2>", ""); return this.releaseNotes.replaceAll("<h2>Changelog</h2>", "");
@ -241,6 +252,9 @@ export default defineComponent({
.about-logo { .about-logo {
height: 2.5rem; height: 2.5rem;
} }
.footer-logo {
height: 1rem;
}
.custom-logo { .custom-logo {
height: 4rem; height: 4rem;
width: auto; width: auto;

View file

@ -44,8 +44,9 @@ describe("getShortVersion", () => {
}); });
describe("isNewVersionAvailable", () => { describe("isNewVersionAvailable", () => {
test("never for dev builds", () => { test("never for dev or nightly builds", () => {
expect(isNewVersionAvailable(DEV, "0.303.1")).toBe(false); expect(isNewVersionAvailable(DEV, "0.303.1")).toBe(false);
expect(isNewVersionAvailable(NIGHTLY, "0.304.0")).toBe(false);
}); });
test("only when available differs", () => { test("only when available differs", () => {
expect(isNewVersionAvailable(STABLE, "0.303.1")).toBe(false); expect(isNewVersionAvailable(STABLE, "0.303.1")).toBe(false);

View file

@ -19,7 +19,8 @@ export function getShortVersion(version: string): string {
} }
export function isNewVersionAvailable(installed?: string, available?: string): boolean { export function isNewVersionAvailable(installed?: string, available?: string): boolean {
return !!available && !isDevelopment(installed || "") && available !== installed; const v = installed || "";
return !!available && !isDevelopment(v) && !isNightly(v) && available !== installed;
} }
export function isNewVersionUnacknowledged( export function isNewVersionUnacknowledged(

View file

@ -1141,7 +1141,7 @@
"modalUpdateStatusStart": "Instal·lació iniciada:", "modalUpdateStatusStart": "Instal·lació iniciada:",
"modalViewOnGitHub": "Vegeu a GitHub", "modalViewOnGitHub": "Vegeu a GitHub",
"openSource": "codi obert", "openSource": "codi obert",
"poweredByOpenSource": "Impulsat per {0}." "poweredBy": "impulsat per"
} }
}, },
"forecast": { "forecast": {

View file

@ -1141,7 +1141,7 @@
"modalUpdateStatusStart": "Instalace spuštěna:", "modalUpdateStatusStart": "Instalace spuštěna:",
"modalViewOnGitHub": "Zobrazit na GitHubu", "modalViewOnGitHub": "Zobrazit na GitHubu",
"openSource": "open source", "openSource": "open source",
"poweredByOpenSource": "Běží na {0}." "poweredBy": "běží na"
} }
}, },
"forecast": { "forecast": {

View file

@ -1126,7 +1126,7 @@
"modalUpdateStatusStart": "Installation startede:", "modalUpdateStatusStart": "Installation startede:",
"modalViewOnGitHub": "Se på GitHub", "modalViewOnGitHub": "Se på GitHub",
"openSource": "open source", "openSource": "open source",
"poweredByOpenSource": "Drevet af {0}." "poweredBy": "drevet af"
} }
}, },
"forecast": { "forecast": {

View file

@ -1119,14 +1119,14 @@
"optInSponsorship": "Sponsoring erforderlich." "optInSponsorship": "Sponsoring erforderlich."
}, },
"version": { "version": {
"availableLong": "neue Version verfügbar", "availableLong": "Update verfügbar",
"community": "evcc Community", "community": "evcc Community",
"labelEmail": "E-Mail", "labelEmail": "E-Mail",
"labelPhone": "Telefon", "labelPhone": "Telefon",
"labelRelease": "Release", "labelRelease": "Release",
"labelVersion": "Version", "labelVersion": "Version",
"labelWebsite": "Website", "labelWebsite": "Website",
"latestVersion": "aktuelle Version", "latestVersion": "aktuell",
"madeByCommunity": "Entwickelt von der {0}.", "madeByCommunity": "Entwickelt von der {0}.",
"modalCancel": "Abbrechen", "modalCancel": "Abbrechen",
"modalDownload": "Download", "modalDownload": "Download",
@ -1141,7 +1141,7 @@
"modalUpdateStatusStart": "Installation gestartet:", "modalUpdateStatusStart": "Installation gestartet:",
"modalViewOnGitHub": "Auf GitHub ansehen", "modalViewOnGitHub": "Auf GitHub ansehen",
"openSource": "Open Source", "openSource": "Open Source",
"poweredByOpenSource": "Unterstützt von {0}." "poweredBy": "powered by"
} }
}, },
"forecast": { "forecast": {

View file

@ -1119,14 +1119,14 @@
"optInSponsorship": "Sponsoring required." "optInSponsorship": "Sponsoring required."
}, },
"version": { "version": {
"availableLong": "new version available", "availableLong": "update available",
"community": "evcc community", "community": "evcc community",
"labelEmail": "Email", "labelEmail": "Email",
"labelPhone": "Phone", "labelPhone": "Phone",
"labelRelease": "Release", "labelRelease": "Release",
"labelVersion": "Version", "labelVersion": "Version",
"labelWebsite": "Website", "labelWebsite": "Website",
"latestVersion": "latest version", "latestVersion": "up to date",
"madeByCommunity": "Made by the {0}.", "madeByCommunity": "Made by the {0}.",
"modalCancel": "Cancel", "modalCancel": "Cancel",
"modalDownload": "Download", "modalDownload": "Download",
@ -1141,7 +1141,7 @@
"modalUpdateStatusStart": "Installation started:", "modalUpdateStatusStart": "Installation started:",
"modalViewOnGitHub": "View on GitHub", "modalViewOnGitHub": "View on GitHub",
"openSource": "open source", "openSource": "open source",
"poweredByOpenSource": "Powered by {0}." "poweredBy": "powered by"
} }
}, },
"forecast": { "forecast": {

View file

@ -1126,7 +1126,7 @@
"modalUpdateStatusStart": "Asennus aloitettu:", "modalUpdateStatusStart": "Asennus aloitettu:",
"modalViewOnGitHub": "Näytä GitHubista", "modalViewOnGitHub": "Näytä GitHubista",
"openSource": "avoimen lähdekoodin", "openSource": "avoimen lähdekoodin",
"poweredByOpenSource": "{0} voimalla." "poweredBy": "powered by"
} }
}, },
"forecast": { "forecast": {

View file

@ -1121,7 +1121,7 @@
"modalUpdateStatusStart": "L'installation a commencé :", "modalUpdateStatusStart": "L'installation a commencé :",
"modalViewOnGitHub": "Voir sur GitHub", "modalViewOnGitHub": "Voir sur GitHub",
"openSource": "open source", "openSource": "open source",
"poweredByOpenSource": "Propulsé par {0}." "poweredBy": "propulsé par"
} }
}, },
"forecast": { "forecast": {

View file

@ -1047,7 +1047,7 @@
"modalUpdateStatusStart": "Installazione avviata:", "modalUpdateStatusStart": "Installazione avviata:",
"modalViewOnGitHub": "Vedi su GitHub", "modalViewOnGitHub": "Vedi su GitHub",
"openSource": "open source", "openSource": "open source",
"poweredByOpenSource": "Powered by {0}." "poweredBy": "powered by"
} }
}, },
"forecast": { "forecast": {

View file

@ -934,7 +934,7 @@
"modalUpdateStatusStart": "インストール開始:", "modalUpdateStatusStart": "インストール開始:",
"modalViewOnGitHub": "GitHub で見る", "modalViewOnGitHub": "GitHub で見る",
"openSource": "open source", "openSource": "open source",
"poweredByOpenSource": "Powered by {0}." "poweredBy": "powered by"
} }
}, },
"forecast": { "forecast": {

View file

@ -1121,7 +1121,7 @@
"modalUpdateStatusStart": "Installatioun ugefaang:", "modalUpdateStatusStart": "Installatioun ugefaang:",
"modalViewOnGitHub": "Op GitHub kucken", "modalViewOnGitHub": "Op GitHub kucken",
"openSource": "Open Source", "openSource": "Open Source",
"poweredByOpenSource": "Bedriwwen vun {0}." "poweredBy": "bedriwwen vun"
} }
}, },
"forecast": { "forecast": {

View file

@ -1141,7 +1141,7 @@
"modalUpdateStatusStart": "Instaliavimas pradėtas:", "modalUpdateStatusStart": "Instaliavimas pradėtas:",
"modalViewOnGitHub": "Peržiūrėti GitHub’e", "modalViewOnGitHub": "Peržiūrėti GitHub’e",
"openSource": "atvirąjį kodą", "openSource": "atvirąjį kodą",
"poweredByOpenSource": "Sukurta naudojant {0}." "poweredBy": "sukurta naudojant"
} }
}, },
"forecast": { "forecast": {

View file

@ -1141,7 +1141,7 @@
"modalUpdateStatusStart": "Instalācija sākta:", "modalUpdateStatusStart": "Instalācija sākta:",
"modalViewOnGitHub": "Skatīt GitHub", "modalViewOnGitHub": "Skatīt GitHub",
"openSource": "atvērtais kods", "openSource": "atvērtais kods",
"poweredByOpenSource": "Darbina {0}." "poweredBy": "darbina"
} }
}, },
"forecast": { "forecast": {

View file

@ -1129,7 +1129,7 @@
"modalUpdateStatusStart": "Installatie gestart:", "modalUpdateStatusStart": "Installatie gestart:",
"modalViewOnGitHub": "Op GitHub bekijken", "modalViewOnGitHub": "Op GitHub bekijken",
"openSource": "open source", "openSource": "open source",
"poweredByOpenSource": "Mogelijk gemaakt door {0}." "poweredBy": "mogelijk gemaakt door"
} }
}, },
"forecast": { "forecast": {

View file

@ -1121,7 +1121,7 @@
"modalUpdateStatusStart": "Instalação iniciada:", "modalUpdateStatusStart": "Instalação iniciada:",
"modalViewOnGitHub": "Ver no GitHub", "modalViewOnGitHub": "Ver no GitHub",
"openSource": "código aberto", "openSource": "código aberto",
"poweredByOpenSource": "Desenvolvido por {0}." "poweredBy": "desenvolvido por"
} }
}, },
"forecast": { "forecast": {

View file

@ -1126,7 +1126,7 @@
"modalUpdateStatusStart": "Установка начата:", "modalUpdateStatusStart": "Установка начата:",
"modalViewOnGitHub": "Посмотреть на GitHub", "modalViewOnGitHub": "Посмотреть на GitHub",
"openSource": "с открытым исходным кодом", "openSource": "с открытым исходным кодом",
"poweredByOpenSource": "Работает на {0}." "poweredBy": "работает на"
} }
}, },
"forecast": { "forecast": {

View file

@ -1126,7 +1126,7 @@
"modalUpdateStatusStart": "Inštalácia sa začala:", "modalUpdateStatusStart": "Inštalácia sa začala:",
"modalViewOnGitHub": "Zobraziť na GitHub", "modalViewOnGitHub": "Zobraziť na GitHub",
"openSource": "otvorený zdrojový kód", "openSource": "otvorený zdrojový kód",
"poweredByOpenSource": "Využíva technológiu {0}." "poweredBy": "beží na"
} }
}, },
"forecast": { "forecast": {

View file

@ -1140,7 +1140,7 @@
"modalUpdateStatusStart": "Installationen har börjat:", "modalUpdateStatusStart": "Installationen har börjat:",
"modalViewOnGitHub": "Se på GitHub", "modalViewOnGitHub": "Se på GitHub",
"openSource": "open source", "openSource": "open source",
"poweredByOpenSource": "Powered by {0}." "poweredBy": "drivs av"
} }
}, },
"forecast": { "forecast": {

View file

@ -1020,7 +1020,7 @@
"modalUpdateStatusStart": "நிறுவல் தொடங்கியது:", "modalUpdateStatusStart": "நிறுவல் தொடங்கியது:",
"modalViewOnGitHub": "GitHub இல் காண்க", "modalViewOnGitHub": "GitHub இல் காண்க",
"openSource": "திறந்த மூல", "openSource": "திறந்த மூல",
"poweredByOpenSource": "{0} ஆல் இயக்கப்படுகிறது." "poweredBy": "powered by"
} }
}, },
"forecast": { "forecast": {

View file

@ -987,7 +987,7 @@
"modalUpdateStatusStart": "Kurulum başladı:", "modalUpdateStatusStart": "Kurulum başladı:",
"modalViewOnGitHub": "GitHub'da görüntüle", "modalViewOnGitHub": "GitHub'da görüntüle",
"openSource": "açık kaynak", "openSource": "açık kaynak",
"poweredByOpenSource": "{0} tarafından desteklenmekte." "poweredBy": "powered by"
} }
}, },
"forecast": { "forecast": {

View file

@ -54,6 +54,15 @@ test.describe("customization", async () => {
const email = page.getByRole("link", { name: "support@example.com" }); const email = page.getByRole("link", { name: "support@example.com" });
await expect(email).toBeVisible(); await expect(email).toBeVisible();
await expect(email).toHaveAttribute("href", "mailto:support@example.com"); await expect(email).toHaveAttribute("href", "mailto:support@example.com");
// footer: powered by evcc instead of community credit
await expect(page.getByText("powered by")).toBeVisible();
await expect(page.getByRole("link", { name: "evcc", exact: true })).toHaveAttribute(
"href",
"https://evcc.io/"
);
await expect(page.getByRole("link", { name: "open source" })).toBeVisible();
await expect(page.getByText("evcc community")).not.toBeVisible();
}); });
test("issue page uses email flow", async ({ page }) => { test("issue page uses email flow", async ({ page }) => {