UI Customize: email-based problem report flow for custom support email (#32584)
This commit is contained in:
parent
84cf093d3d
commit
ffb91cc335
6 changed files with 175 additions and 9 deletions
|
|
@ -1,5 +1,11 @@
|
|||
import { describe, it, expect } from "vite-plus/test";
|
||||
import { generateGitHubContent } from "./template";
|
||||
import {
|
||||
generateGitHubContent,
|
||||
generateMailtoUrl,
|
||||
generateDebugFile,
|
||||
MAX_MAIL_TITLE_LENGTH,
|
||||
MAX_MAIL_DESCRIPTION_LENGTH,
|
||||
} from "./template";
|
||||
import type { IssueData, Sections } from "./types";
|
||||
|
||||
describe("Issue Utils", () => {
|
||||
|
|
@ -123,4 +129,46 @@ Linux/amd64, MST -07:00`);
|
|||
expect(result.body).not.toContain("## Logs");
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateMailtoUrl", () => {
|
||||
it("generates plaintext mailto url without steps or diagnostics", () => {
|
||||
const url = generateMailtoUrl("support@example.com", mockIssueData);
|
||||
|
||||
expect(url).toContain("mailto:support@example.com?subject=Test%20Issue&body=");
|
||||
const body = decodeURIComponent(url.split("&body=")[1]);
|
||||
expect(body).toBe(`This is a test description
|
||||
|
||||
Version: v1.0.0
|
||||
|
||||
System: Linux/amd64, MST -07:00`);
|
||||
});
|
||||
|
||||
it("stays below the windows mailto limit at maximum input length", () => {
|
||||
// umlaut-dense german text, each umlaut costs 6 chars percent-encoded
|
||||
const text = "Fehlermeldung: Ladepunkt überprüfen, Zählerstände größer als üblich. ";
|
||||
const fill = (length: number) =>
|
||||
text.repeat(Math.ceil(length / text.length)).slice(0, length);
|
||||
|
||||
const url = generateMailtoUrl("support@installer-example.com", {
|
||||
...mockIssueData,
|
||||
title: fill(MAX_MAIL_TITLE_LENGTH),
|
||||
description: fill(MAX_MAIL_DESCRIPTION_LENGTH),
|
||||
});
|
||||
|
||||
expect(url.length).toBeLessThan(2000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateDebugFile", () => {
|
||||
it("contains version, system and enabled sections", () => {
|
||||
const content = generateDebugFile(mockIssueData, mockSections);
|
||||
|
||||
expect(content).toContain("# evcc debug information");
|
||||
expect(content).toContain("Version: v1.0.0");
|
||||
expect(content).toContain("System: Linux/amd64, MST -07:00");
|
||||
expect(content).toContain("## Configuration (YAML)");
|
||||
expect(content).toContain("## Logs");
|
||||
expect(content).not.toContain("## System State");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ import type { IssueData, Sections, GitHubContent, Template, HelpType } from "./t
|
|||
// Constants
|
||||
const PLACEHOLDER = "⚠️ RETURN TO EVCC TAB → COPY STEP 2 → PASTE HERE";
|
||||
const MAX_BODY_LENGTH = 8000;
|
||||
// windows shell silently truncates mailto urls at ~2000 chars; input limits keep
|
||||
// the percent-encoded url below that even for umlaut-heavy text
|
||||
export const MAX_MAIL_TITLE_LENGTH = 100;
|
||||
export const MAX_MAIL_DESCRIPTION_LENGTH = 600;
|
||||
|
||||
function toString(sections: Template): string {
|
||||
return sections
|
||||
|
|
@ -68,6 +72,26 @@ function generateAdditional(sections: Sections): string {
|
|||
return toString(result);
|
||||
}
|
||||
|
||||
// Generates mailto url with plaintext body; diagnostics travel as file attachment instead
|
||||
export function generateMailtoUrl(email: string, issue: IssueData): string {
|
||||
const body = toString([
|
||||
issue.description,
|
||||
`Version: ${issue.version}`,
|
||||
`System: ${issue.system}, ${issue.timezone}`,
|
||||
]);
|
||||
|
||||
return `mailto:${email}?subject=${encodeURIComponent(issue.title)}&body=${encodeURIComponent(body)}`;
|
||||
}
|
||||
|
||||
// Generates text file content with selected diagnostics for manual mail attachment
|
||||
export function generateDebugFile(issue: IssueData, sections: Sections): string {
|
||||
return toString([
|
||||
"# evcc debug information",
|
||||
[`Version: ${issue.version}`, `System: ${issue.system}, ${issue.timezone}`],
|
||||
generateAdditional(sections),
|
||||
]);
|
||||
}
|
||||
|
||||
// Generates GitHub URL for issues or discussions
|
||||
export function generateGitHubUrl(type: HelpType, title: string, body: string): string {
|
||||
const baseUrl =
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Help Type Selection -->
|
||||
<div class="mb-5">
|
||||
<div v-if="!emailMode" class="mb-5">
|
||||
<h5 class="mb-3">{{ $t("issue.helpType.title") }}</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
|
|
@ -60,14 +60,14 @@
|
|||
</h4>
|
||||
</div>
|
||||
|
||||
<p class="text-muted mb-4">
|
||||
<p v-if="!emailMode" class="text-muted mb-4">
|
||||
🇬🇧 Please write your issue in English so everyone can participate.
|
||||
</p>
|
||||
|
||||
<!-- Two Column Layout -->
|
||||
<div class="row mb-5 g-5">
|
||||
<!-- Left Column: Form Fields -->
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="col-12 col-lg-6 pe-lg-5">
|
||||
<div class="mb-4">
|
||||
<label for="issueTitle" class="form-label">
|
||||
{{ $t("issue.issueTitle") }} *
|
||||
|
|
@ -78,6 +78,7 @@
|
|||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Brief description of the problem"
|
||||
:maxlength="titleMaxLength"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -89,12 +90,18 @@
|
|||
id="issueDescription"
|
||||
v-model="issue.description"
|
||||
class="form-control"
|
||||
rows="6"
|
||||
:rows="emailMode ? 12 : 6"
|
||||
placeholder="Describe what you expected to happen and what actually happened..."
|
||||
:maxlength="descriptionMaxLength"
|
||||
required
|
||||
></textarea>
|
||||
<div v-if="descriptionMaxLength" class="text-end">
|
||||
<small class="text-muted">
|
||||
{{ issue.description.length }} / {{ descriptionMaxLength }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div v-if="!emailMode" class="mb-4">
|
||||
<label for="stepsToReproduce" class="form-label">
|
||||
{{ $t("issue.stepsToReproduce") }} *
|
||||
</label>
|
||||
|
|
@ -149,10 +156,16 @@
|
|||
<div class="text-end">
|
||||
<small class="text-muted">* required</small>
|
||||
</div>
|
||||
<div v-if="emailMode" class="d-flex justify-content-end mt-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Toggleable Sections -->
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="col-12 col-lg-6 ps-lg-5">
|
||||
<hr class="d-lg-none mt-0 mb-5" />
|
||||
<div class="mb-4">
|
||||
<h5>{{ $t("issue.additional.title") }}</h5>
|
||||
<p class="text-muted small">
|
||||
|
|
@ -289,11 +302,24 @@
|
|||
</p>
|
||||
</template>
|
||||
</IssueAdditionalItem>
|
||||
|
||||
<div v-if="emailMode" class="mt-4">
|
||||
<p class="text-muted small">{{ $t("issue.downloadHint") }}</p>
|
||||
<div class="d-flex justify-content-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary"
|
||||
@click="downloadDebugFile"
|
||||
>
|
||||
{{ $t("issue.downloadButton") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Essential Section Actions -->
|
||||
<div class="d-flex justify-content-end gap-3 mb-5">
|
||||
<div v-if="!emailMode" class="d-flex justify-content-end gap-3 mb-5">
|
||||
<button type="submit" class="btn" :class="buttonClass">
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
|
|
@ -304,6 +330,7 @@
|
|||
|
||||
<!-- Issue Summary Modal -->
|
||||
<SummaryModal
|
||||
v-if="!emailMode"
|
||||
:help-type="helpType"
|
||||
:button-class="buttonClass"
|
||||
:issue-data="issueData"
|
||||
|
|
@ -324,6 +351,12 @@ import api from "@/api";
|
|||
import store from "@/store";
|
||||
import { LOG_LEVELS, DEFAULT_LOG_LEVEL } from "@/utils/log";
|
||||
import { formatJson } from "@/components/Issue/format";
|
||||
import {
|
||||
generateMailtoUrl,
|
||||
generateDebugFile,
|
||||
MAX_MAIL_TITLE_LENGTH,
|
||||
MAX_MAIL_DESCRIPTION_LENGTH,
|
||||
} from "@/components/Issue/template";
|
||||
import type { HelpType, IssueData } from "@/components/Issue/types";
|
||||
import type { State } from "@/types/evcc";
|
||||
|
||||
|
|
@ -387,6 +420,18 @@ export default defineComponent({
|
|||
return { title: this.$t("issue.title") };
|
||||
},
|
||||
computed: {
|
||||
customEmail(): string {
|
||||
return window.evcc?.customEmail ?? "";
|
||||
},
|
||||
emailMode(): boolean {
|
||||
return !!this.customEmail;
|
||||
},
|
||||
titleMaxLength(): number | undefined {
|
||||
return this.emailMode ? MAX_MAIL_TITLE_LENGTH : undefined;
|
||||
},
|
||||
descriptionMaxLength(): number | undefined {
|
||||
return this.emailMode ? MAX_MAIL_DESCRIPTION_LENGTH : undefined;
|
||||
},
|
||||
versionString(): string {
|
||||
return `v${store.state.version || ""}`;
|
||||
},
|
||||
|
|
@ -463,7 +508,11 @@ export default defineComponent({
|
|||
methods: {
|
||||
// Type-dependent translation helper
|
||||
$tt(key: string): string {
|
||||
const suffix = this.helpType === "discussion" ? "Discussion" : "Issue";
|
||||
const suffix = this.emailMode
|
||||
? "Email"
|
||||
: this.helpType === "discussion"
|
||||
? "Discussion"
|
||||
: "Issue";
|
||||
return this.$t(`${key}${suffix}`);
|
||||
},
|
||||
|
||||
|
|
@ -603,12 +652,28 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
handleFormSubmit() {
|
||||
if (this.emailMode) {
|
||||
window.location.href = generateMailtoUrl(this.customEmail, this.issueData);
|
||||
return;
|
||||
}
|
||||
const modalElement = document.getElementById("issueSummaryModal") as HTMLElement;
|
||||
if (modalElement) {
|
||||
Modal.getOrCreateInstance(modalElement).show();
|
||||
}
|
||||
},
|
||||
|
||||
downloadDebugFile() {
|
||||
const blob = new Blob([generateDebugFile(this.issueData, this.sections)], {
|
||||
type: "text/plain",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `evcc-debug-${this.versionString}.txt`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
|
||||
clearSessionStorage() {
|
||||
sessionStorage.removeItem("issue.title");
|
||||
sessionStorage.removeItem("issue.description");
|
||||
|
|
|
|||
|
|
@ -1223,7 +1223,10 @@
|
|||
"additionalContext": "Zusätzlicher Kontext",
|
||||
"additionalContextPlaceholder": "Alle zusätzlichen Informationen, die hilfreich sein könnten...\n- Konfigurationsdetails\n- Was du versucht hast\n- Umgebungsdetails",
|
||||
"createButtonDiscussion": "GitHub Diskussion starten...",
|
||||
"createButtonEmail": "Anfrage per E-Mail senden",
|
||||
"createButtonIssue": "GitHub Issue erstellen...",
|
||||
"downloadButton": "Debug-Informationen herunterladen",
|
||||
"downloadHint": "Hänge diese Datei an deine E-Mail an, falls weitere Informationen benötigt werden.",
|
||||
"description": "Deine Installation funktioniert nicht wie erwartet? Nutze diese Seite um Hilfe zu bekommen oder Probleme zu melden. Gib genügend Details an, damit wir das Problem verstehen und reproduzieren können. Halte deine Beschreibung prägnant, klar und leicht verständlich.",
|
||||
"helpType": {
|
||||
"discussion": "Brauche Hilfe bei meiner Einrichtung",
|
||||
|
|
@ -1236,6 +1239,7 @@
|
|||
"issueTitle": "Titel",
|
||||
"stepsToReproduce": "Schritte zur Reproduktion",
|
||||
"subTitleDiscussion": "Beschreibe dein Problem",
|
||||
"subTitleEmail": "Beschreibe dein Problem",
|
||||
"subTitleIssue": "Beschreibe das Problem",
|
||||
"summary": {
|
||||
"confirmationButtonDiscussion": "GitHub Diskussion starten",
|
||||
|
|
|
|||
|
|
@ -1223,7 +1223,10 @@
|
|||
"additionalContext": "Additional context",
|
||||
"additionalContextPlaceholder": "Any additional information that might be helpful...\n- Configuration details\n- What you tried\n- Environment details",
|
||||
"createButtonDiscussion": "Start GitHub Discussion...",
|
||||
"createButtonEmail": "Send request by email",
|
||||
"createButtonIssue": "Create GitHub Issue...",
|
||||
"downloadButton": "Download debug information",
|
||||
"downloadHint": "Attach this file to your email if more information is needed.",
|
||||
"description": "Your installation is not working as expected? Use this page to get help or report issues. Provide enough detail to help us understand and reproduce the problem, while keeping your description concise, clear and easy to follow.",
|
||||
"helpType": {
|
||||
"discussion": "Need help with my setup",
|
||||
|
|
@ -1236,6 +1239,7 @@
|
|||
"issueTitle": "Title",
|
||||
"stepsToReproduce": "Steps to reproduce",
|
||||
"subTitleDiscussion": "Describe your problem",
|
||||
"subTitleEmail": "Describe your problem",
|
||||
"subTitleIssue": "Describe the issue",
|
||||
"summary": {
|
||||
"confirmationButtonDiscussion": "Start GitHub Discussion",
|
||||
|
|
|
|||
|
|
@ -56,6 +56,27 @@ test.describe("customization", async () => {
|
|||
await expect(email).toHaveAttribute("href", "mailto:support@example.com");
|
||||
});
|
||||
|
||||
test("issue page uses email flow", async ({ page }) => {
|
||||
await page.goto("/#/issue");
|
||||
|
||||
// github-only elements gone
|
||||
await expect(page.getByRole("radio")).toHaveCount(0);
|
||||
await expect(page.getByLabel("Steps to Reproduce")).toHaveCount(0);
|
||||
await expect(page.getByText("Please write your issue in English")).toHaveCount(0);
|
||||
|
||||
await expect(page.getByRole("button", { name: "Send request by email" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Attach this file to your email if more information is needed.")
|
||||
).toBeVisible();
|
||||
|
||||
const downloadButton = page.getByRole("button", { name: "Download debug information" });
|
||||
await expect(downloadButton).toBeVisible();
|
||||
const downloadPromise = page.waitForEvent("download");
|
||||
await downloadButton.click();
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toMatch(/^evcc-debug-v.+\.txt$/);
|
||||
});
|
||||
|
||||
test("help modal shows contact button instead of github discussions", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
const menu = await openMoreMenu(page);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue