mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(feedback): always-on nav button with GitHub/email handoff when analytics is off (#428)
Keep the top-nav feedback button always visible (icon plus label on desktop, icon-only on mobile) instead of hiding it when an instance opts out of analytics. When analytics is off, the dialog keeps the typed message and hands off to a prefilled GitHub issue plus a contact@snapotter.com email, with no fake Thanks. Adds a feedback.yml issue template, URL builders, and feedback strings across all 21 locales. Claude-Session: https://claude.ai/code/session_01XVrHKXwzZDWBWgkGQdPZ3A
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
name: General feedback
|
||||
description: Share general feedback, ideas, or reactions about SnapOtter.
|
||||
title: "[Feedback]: "
|
||||
labels: ["feedback"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Thanks for taking a moment to share feedback.
|
||||
- type: textarea
|
||||
id: details
|
||||
attributes:
|
||||
label: Your feedback
|
||||
description: What is on your mind?
|
||||
placeholder: Tell us what you think...
|
||||
validations:
|
||||
required: true
|
||||
@@ -9,6 +9,8 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "@/contexts/i18n-context";
|
||||
import { useFocusTrap } from "@/hooks/use-focus-trap";
|
||||
import {
|
||||
buildFeedbackGithubUrl,
|
||||
buildFeedbackMailtoUrl,
|
||||
type FeedbackErrorCategory,
|
||||
type FeedbackFrictionArea,
|
||||
type FeedbackImportantArea,
|
||||
@@ -115,6 +117,7 @@ export function FeedbackDialog({
|
||||
|
||||
const isAdminInstall = source === "admin_installer";
|
||||
const isSearchMiss = source === "search_miss";
|
||||
const isGlobal = source === "global";
|
||||
const canSubmit = Boolean(
|
||||
message.trim() || sentiment || feedbackType !== "other" || isAdminInstall,
|
||||
);
|
||||
@@ -203,7 +206,28 @@ export function FeedbackDialog({
|
||||
|
||||
{submitted ? (
|
||||
<div className="p-6 space-y-4">
|
||||
{isSearchMiss && !accepted ? (
|
||||
{isGlobal && !accepted ? (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-foreground">{t.feedback.offlineDescription}</p>
|
||||
<p className="text-xs text-muted-foreground">{t.feedback.offlinePublicNote}</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
<a
|
||||
href={buildFeedbackGithubUrl(message)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-full text-center py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90"
|
||||
>
|
||||
{t.feedback.offlineGithubButton}
|
||||
</a>
|
||||
<a
|
||||
href={buildFeedbackMailtoUrl(message)}
|
||||
className="w-full text-center py-2 rounded-lg border border-border text-sm text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
{t.feedback.offlineEmailButton}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : isSearchMiss && !accepted ? (
|
||||
<p className="text-sm text-foreground">
|
||||
<a
|
||||
href={buildToolRequestDiscussionUrl(searchQuery ?? "")}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAnalyticsStore } from "@/stores/analytics-store";
|
||||
import { useConnectionStore } from "@/stores/connection-store";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
import { FeedbackDialog } from "../feedback/feedback-dialog";
|
||||
@@ -22,10 +21,8 @@ export function AppLayout({ children, breadcrumb, navVariant }: AppLayoutProps)
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
const [feedbackOpen, setFeedbackOpen] = useState(false);
|
||||
const isMobile = useMobile();
|
||||
const analyticsConfig = useAnalyticsStore((s) => s.config);
|
||||
const connectionStatus = useConnectionStore((s) => s.status);
|
||||
const bannerVisible = connectionStatus !== "connected";
|
||||
const feedbackEnabled = Boolean(analyticsConfig?.enabled);
|
||||
|
||||
// Load global settings (disabled tools, experimental flag, default theme) on
|
||||
// every authenticated page, not just the home grid. Without this, navigating
|
||||
@@ -51,7 +48,6 @@ export function AppLayout({ children, breadcrumb, navVariant }: AppLayoutProps)
|
||||
onHelpClick={() => setHelpOpen(true)}
|
||||
onFeedbackClick={() => setFeedbackOpen(true)}
|
||||
onSettingsClick={() => setSettingsOpen(true)}
|
||||
feedbackEnabled={feedbackEnabled}
|
||||
/>
|
||||
|
||||
{/* Main content area */}
|
||||
|
||||
@@ -28,7 +28,6 @@ interface TopNavProps {
|
||||
onHelpClick: () => void;
|
||||
onFeedbackClick?: () => void;
|
||||
onSettingsClick: () => void;
|
||||
feedbackEnabled?: boolean;
|
||||
}
|
||||
|
||||
interface NavLinkItem {
|
||||
@@ -59,7 +58,6 @@ export function TopNav({
|
||||
onHelpClick,
|
||||
onFeedbackClick,
|
||||
onSettingsClick,
|
||||
feedbackEnabled = false,
|
||||
}: TopNavProps) {
|
||||
const location = useLocation();
|
||||
const isMobile = useMobile();
|
||||
@@ -114,7 +112,7 @@ export function TopNav({
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
{feedbackEnabled && onFeedbackClick && (
|
||||
{onFeedbackClick && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onFeedbackClick}
|
||||
@@ -254,12 +252,12 @@ export function TopNav({
|
||||
{!isMobile && <ThemeToggle isDark={isDark} />}
|
||||
{!isMobile && <LanguageSelector isDark={isDark} />}
|
||||
|
||||
{feedbackEnabled && onFeedbackClick && (
|
||||
{onFeedbackClick && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onFeedbackClick}
|
||||
className={cn(
|
||||
"p-1.5 rounded-md transition-colors",
|
||||
"flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-sm font-medium transition-colors",
|
||||
isDark
|
||||
? "text-[#aaa] hover:text-[#e0e0e0] hover:bg-[#333]"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted",
|
||||
@@ -267,6 +265,7 @@ export function TopNav({
|
||||
aria-label={t.feedback.navLabel}
|
||||
>
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
{t.feedback.navButtonLabel}
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
||||
@@ -155,3 +155,33 @@ export function shouldShowUsageSurvey({
|
||||
export async function submitFeedback(payload: FeedbackPayload): Promise<FeedbackResponse> {
|
||||
return apiPost<FeedbackResponse>("/v1/feedback", payload);
|
||||
}
|
||||
|
||||
const FEEDBACK_ISSUE_NEW_URL = "https://github.com/snapotter-hq/snapotter/issues/new";
|
||||
const MAX_FEEDBACK_LEN = 2000;
|
||||
|
||||
export const SNAPOTTER_FEEDBACK_EMAIL = "contact@snapotter.com";
|
||||
|
||||
/** Normalize newlines, trim, and clamp so the message is safe to put in a URL. */
|
||||
function sanitizeFeedbackMessage(message: string): string {
|
||||
return message.replace(/\r\n/g, "\n").trim().slice(0, MAX_FEEDBACK_LEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefilled GitHub issue URL for general feedback. Blank issues are disabled on
|
||||
* the repo, so we must target a template by file name; `details` matches the
|
||||
* `id` of the textarea in `.github/ISSUE_TEMPLATE/feedback.yml`.
|
||||
*/
|
||||
export function buildFeedbackGithubUrl(message: string): string {
|
||||
const params = new URLSearchParams({
|
||||
template: "feedback.yml",
|
||||
details: sanitizeFeedbackMessage(message),
|
||||
});
|
||||
return `${FEEDBACK_ISSUE_NEW_URL}?${params.toString()}`;
|
||||
}
|
||||
|
||||
/** Prefilled mailto to the project address, for users who prefer a private channel. */
|
||||
export function buildFeedbackMailtoUrl(message: string): string {
|
||||
const subject = encodeURIComponent("SnapOtter feedback");
|
||||
const body = encodeURIComponent(sanitizeFeedbackMessage(message));
|
||||
return `mailto:${SNAPOTTER_FEEDBACK_EMAIL}?subject=${subject}&body=${body}`;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ export const ar: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "ملاحظات",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -65,6 +66,11 @@ export const ar: TranslationKeys = {
|
||||
searchMissTitle: "طلب أداة",
|
||||
searchMissContext: "لقد بحثت عن: {query}",
|
||||
searchMissDiscussionsFallback: "تعذّر تسجيل ذلك. افتح طلبًا في Discussions.",
|
||||
offlineDescription:
|
||||
"التحليلات معطّلة في هذا المثيل، لذلك لا تُسجَّل الملاحظات هنا. لا يزال بإمكانك التواصل مباشرةً مع فريق SnapOtter.",
|
||||
offlinePublicNote: "مشكلات GitHub علنية.",
|
||||
offlineGithubButton: "افتح مشكلة على GitHub",
|
||||
offlineEmailButton: "راسلنا",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const de: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "Feedback",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -66,6 +67,11 @@ export const de: TranslationKeys = {
|
||||
searchMissContext: "Du hast gesucht nach: {query}",
|
||||
searchMissDiscussionsFallback:
|
||||
"Das konnten wir nicht speichern. Stelle eine Anfrage in Discussions.",
|
||||
offlineDescription:
|
||||
"Für diese Instanz ist die Analyse deaktiviert, daher wird Feedback hier nicht erfasst. Du kannst das SnapOtter-Team weiterhin direkt erreichen.",
|
||||
offlinePublicNote: "GitHub-Issues sind öffentlich.",
|
||||
offlineGithubButton: "Ein GitHub-Issue öffnen",
|
||||
offlineEmailButton: "Schreib uns",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -40,6 +40,7 @@ export const en = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "Feedback",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -63,6 +64,11 @@ export const en = {
|
||||
searchMissTitle: "Request a tool",
|
||||
searchMissContext: "You searched for: {query}",
|
||||
searchMissDiscussionsFallback: "We couldn't record that. Open a request in Discussions.",
|
||||
offlineDescription:
|
||||
"This instance has analytics turned off, so feedback isn't recorded here. You can still reach the SnapOtter team directly.",
|
||||
offlinePublicNote: "GitHub issues are public.",
|
||||
offlineGithubButton: "Open a GitHub issue",
|
||||
offlineEmailButton: "Email us",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const es: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "Comentarios",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -65,6 +66,11 @@ export const es: TranslationKeys = {
|
||||
searchMissTitle: "Solicitar una herramienta",
|
||||
searchMissContext: "Buscaste: {query}",
|
||||
searchMissDiscussionsFallback: "No pudimos registrarlo. Abre una solicitud en Discussions.",
|
||||
offlineDescription:
|
||||
"Esta instancia tiene la analítica desactivada, así que los comentarios no se registran aquí. Aún puedes contactar directamente con el equipo de SnapOtter.",
|
||||
offlinePublicNote: "Las incidencias de GitHub son públicas.",
|
||||
offlineGithubButton: "Abrir una incidencia en GitHub",
|
||||
offlineEmailButton: "Escríbenos",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const fr: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "Commentaires",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -66,6 +67,11 @@ export const fr: TranslationKeys = {
|
||||
searchMissContext: "Vous avez recherché : {query}",
|
||||
searchMissDiscussionsFallback:
|
||||
"Nous n'avons pas pu l'enregistrer. Ouvrez une demande dans Discussions.",
|
||||
offlineDescription:
|
||||
"Cette instance a l'analytique désactivée, donc les commentaires ne sont pas enregistrés ici. Vous pouvez tout de même contacter directement l'équipe SnapOtter.",
|
||||
offlinePublicNote: "Les tickets GitHub sont publics.",
|
||||
offlineGithubButton: "Ouvrir un ticket GitHub",
|
||||
offlineEmailButton: "Écrivez-nous",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const hi: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "प्रतिक्रिया",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -65,6 +66,11 @@ export const hi: TranslationKeys = {
|
||||
searchMissTitle: "टूल का अनुरोध करें",
|
||||
searchMissContext: "आपने खोजा: {query}",
|
||||
searchMissDiscussionsFallback: "हम इसे रिकॉर्ड नहीं कर सके. Discussions में अनुरोध खोलें.",
|
||||
offlineDescription:
|
||||
"इस इंस्टेंस पर एनालिटिक्स बंद है, इसलिए प्रतिक्रिया यहाँ रिकॉर्ड नहीं होती. फिर भी आप SnapOtter टीम से सीधे संपर्क कर सकते हैं.",
|
||||
offlinePublicNote: "GitHub issue सार्वजनिक होते हैं.",
|
||||
offlineGithubButton: "GitHub issue खोलें",
|
||||
offlineEmailButton: "हमें ईमेल करें",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const id: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "Masukan",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -65,6 +66,11 @@ export const id: TranslationKeys = {
|
||||
searchMissTitle: "Minta alat",
|
||||
searchMissContext: "Anda mencari: {query}",
|
||||
searchMissDiscussionsFallback: "Kami tidak dapat mencatatnya. Buka permintaan di Discussions.",
|
||||
offlineDescription:
|
||||
"Instance ini menonaktifkan analitik, jadi masukan tidak dicatat di sini. Anda tetap bisa menghubungi tim SnapOtter secara langsung.",
|
||||
offlinePublicNote: "Issue GitHub bersifat publik.",
|
||||
offlineGithubButton: "Buka issue GitHub",
|
||||
offlineEmailButton: "Kirim email ke kami",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const it: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Condividi feedback",
|
||||
navButtonLabel: "Feedback",
|
||||
dialogTitle: "Condividi feedback",
|
||||
toolDialogTitle: "Come ha funzionato questo strumento?",
|
||||
failedDialogTitle: "Segnala un problema dello strumento",
|
||||
@@ -67,6 +68,11 @@ export const it: TranslationKeys = {
|
||||
searchMissContext: "Hai cercato: {query}",
|
||||
searchMissDiscussionsFallback:
|
||||
"Non siamo riusciti a registrarlo. Apri una richiesta in Discussions.",
|
||||
offlineDescription:
|
||||
"Questa istanza ha l'analisi disattivata, quindi il feedback non viene registrato qui. Puoi comunque contattare direttamente il team di SnapOtter.",
|
||||
offlinePublicNote: "Le issue di GitHub sono pubbliche.",
|
||||
offlineGithubButton: "Apri una issue su GitHub",
|
||||
offlineEmailButton: "Scrivici",
|
||||
thanksTitle: "Grazie per il feedback.",
|
||||
thanksDescription:
|
||||
"Ci aiuta a migliorare SnapOtter senza raccogliere file o contenuti privati.",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const ja: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "フィードバック",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -66,6 +67,11 @@ export const ja: TranslationKeys = {
|
||||
searchMissContext: "検索した内容: {query}",
|
||||
searchMissDiscussionsFallback:
|
||||
"記録できませんでした。Discussions でリクエストを開いてください。",
|
||||
offlineDescription:
|
||||
"このインスタンスは分析が無効になっているため、フィードバックはここには記録されません。SnapOtter チームに直接連絡することもできます。",
|
||||
offlinePublicNote: "GitHub の Issue は公開されます。",
|
||||
offlineGithubButton: "GitHub で Issue を作成",
|
||||
offlineEmailButton: "メールで連絡",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const ko: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "피드백",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -65,6 +66,11 @@ export const ko: TranslationKeys = {
|
||||
searchMissTitle: "도구 요청",
|
||||
searchMissContext: "검색한 내용: {query}",
|
||||
searchMissDiscussionsFallback: "기록하지 못했습니다. Discussions에서 요청을 열어 주세요.",
|
||||
offlineDescription:
|
||||
"이 인스턴스는 분석이 꺼져 있어 피드백이 여기에 기록되지 않습니다. 그래도 SnapOtter 팀에 직접 연락할 수 있습니다.",
|
||||
offlinePublicNote: "GitHub 이슈는 공개됩니다.",
|
||||
offlineGithubButton: "GitHub 이슈 열기",
|
||||
offlineEmailButton: "이메일 보내기",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const nl: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "Feedback",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -66,6 +67,11 @@ export const nl: TranslationKeys = {
|
||||
searchMissContext: "Je zocht op: {query}",
|
||||
searchMissDiscussionsFallback:
|
||||
"We konden dit niet vastleggen. Open een verzoek in Discussions.",
|
||||
offlineDescription:
|
||||
"Deze instantie heeft analyse uitgeschakeld, dus feedback wordt hier niet vastgelegd. Je kunt het SnapOtter-team nog steeds rechtstreeks bereiken.",
|
||||
offlinePublicNote: "GitHub-issues zijn openbaar.",
|
||||
offlineGithubButton: "Een GitHub-issue openen",
|
||||
offlineEmailButton: "Mail ons",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const pl: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "Opinie",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -65,6 +66,11 @@ export const pl: TranslationKeys = {
|
||||
searchMissTitle: "Poproś o narzędzie",
|
||||
searchMissContext: "Szukano: {query}",
|
||||
searchMissDiscussionsFallback: "Nie udało się tego zapisać. Otwórz prośbę w Discussions.",
|
||||
offlineDescription:
|
||||
"Ta instancja ma wyłączoną analitykę, więc opinie nie są tu zapisywane. Nadal możesz skontaktować się bezpośrednio z zespołem SnapOtter.",
|
||||
offlinePublicNote: "Zgłoszenia w GitHub są publiczne.",
|
||||
offlineGithubButton: "Otwórz zgłoszenie w GitHub",
|
||||
offlineEmailButton: "Napisz do nas",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const ptBR: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "Feedback",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -66,6 +67,11 @@ export const ptBR: TranslationKeys = {
|
||||
searchMissContext: "Você pesquisou: {query}",
|
||||
searchMissDiscussionsFallback:
|
||||
"Não conseguimos registrar. Abra uma solicitação no Discussions.",
|
||||
offlineDescription:
|
||||
"Esta instância está com a análise desativada, então o feedback não é registrado aqui. Mesmo assim, você pode falar diretamente com a equipe do SnapOtter.",
|
||||
offlinePublicNote: "As issues do GitHub são públicas.",
|
||||
offlineGithubButton: "Abrir uma issue no GitHub",
|
||||
offlineEmailButton: "Envie um e-mail",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const ru: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "Отзыв",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -65,6 +66,11 @@ export const ru: TranslationKeys = {
|
||||
searchMissTitle: "Запросить инструмент",
|
||||
searchMissContext: "Вы искали: {query}",
|
||||
searchMissDiscussionsFallback: "Не удалось записать. Откройте запрос в Discussions.",
|
||||
offlineDescription:
|
||||
"В этом экземпляре аналитика отключена, поэтому отзывы здесь не записываются. Вы всё равно можете напрямую связаться с командой SnapOtter.",
|
||||
offlinePublicNote: "Issue на GitHub публичны.",
|
||||
offlineGithubButton: "Открыть issue на GitHub",
|
||||
offlineEmailButton: "Напишите нам",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const sv: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "Feedback",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -66,6 +67,11 @@ export const sv: TranslationKeys = {
|
||||
searchMissContext: "Du sökte efter: {query}",
|
||||
searchMissDiscussionsFallback:
|
||||
"Vi kunde inte registrera det. Öppna en förfrågan i Discussions.",
|
||||
offlineDescription:
|
||||
"Den här instansen har stängt av analysen, så feedback registreras inte här. Du kan ändå nå SnapOtter-teamet direkt.",
|
||||
offlinePublicNote: "GitHub-ärenden är offentliga.",
|
||||
offlineGithubButton: "Öppna ett GitHub-ärende",
|
||||
offlineEmailButton: "Mejla oss",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const th: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "ความคิดเห็น",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -65,6 +66,11 @@ export const th: TranslationKeys = {
|
||||
searchMissTitle: "ขอเครื่องมือ",
|
||||
searchMissContext: "คุณค้นหา: {query}",
|
||||
searchMissDiscussionsFallback: "เราบันทึกไม่ได้ เปิดคำขอใน Discussions",
|
||||
offlineDescription:
|
||||
"อินสแตนซ์นี้ปิดการวิเคราะห์ไว้ ความคิดเห็นจึงไม่ถูกบันทึกที่นี่ คุณยังสามารถติดต่อทีม SnapOtter ได้โดยตรง",
|
||||
offlinePublicNote: "Issue บน GitHub เป็นสาธารณะ",
|
||||
offlineGithubButton: "เปิด Issue บน GitHub",
|
||||
offlineEmailButton: "ส่งอีเมลถึงเรา",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const tr: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "Geri bildirim",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -65,6 +66,11 @@ export const tr: TranslationKeys = {
|
||||
searchMissTitle: "Araç iste",
|
||||
searchMissContext: "Şunu aradın: {query}",
|
||||
searchMissDiscussionsFallback: "Bunu kaydedemedik. Discussions'ta bir istek aç.",
|
||||
offlineDescription:
|
||||
"Bu örnekte analiz kapalı, bu yüzden geri bildirim burada kaydedilmez. Yine de SnapOtter ekibine doğrudan ulaşabilirsin.",
|
||||
offlinePublicNote: "GitHub issue'ları herkese açıktır.",
|
||||
offlineGithubButton: "GitHub'da issue aç",
|
||||
offlineEmailButton: "Bize e-posta gönder",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const uk: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "Відгук",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -65,6 +66,11 @@ export const uk: TranslationKeys = {
|
||||
searchMissTitle: "Запитати інструмент",
|
||||
searchMissContext: "Ви шукали: {query}",
|
||||
searchMissDiscussionsFallback: "Не вдалося записати. Відкрийте запит у Discussions.",
|
||||
offlineDescription:
|
||||
"У цьому екземплярі аналітику вимкнено, тому відгуки тут не записуються. Ви все одно можете напряму звернутися до команди SnapOtter.",
|
||||
offlinePublicNote: "Issue на GitHub є публічними.",
|
||||
offlineGithubButton: "Відкрити issue на GitHub",
|
||||
offlineEmailButton: "Напишіть нам",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const vi: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "Phản hồi",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -66,6 +67,11 @@ export const vi: TranslationKeys = {
|
||||
searchMissContext: "Bạn đã tìm: {query}",
|
||||
searchMissDiscussionsFallback:
|
||||
"Chúng tôi không ghi lại được. Mở một yêu cầu trong Discussions.",
|
||||
offlineDescription:
|
||||
"Bản cài đặt này đã tắt phân tích, nên phản hồi không được ghi lại ở đây. Bạn vẫn có thể liên hệ trực tiếp với nhóm SnapOtter.",
|
||||
offlinePublicNote: "Issue trên GitHub là công khai.",
|
||||
offlineGithubButton: "Mở một issue trên GitHub",
|
||||
offlineEmailButton: "Gửi email cho chúng tôi",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const zhCN: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "反馈",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -65,6 +66,11 @@ export const zhCN: TranslationKeys = {
|
||||
searchMissTitle: "请求工具",
|
||||
searchMissContext: "你搜索了:{query}",
|
||||
searchMissDiscussionsFallback: "我们无法记录。请在 Discussions 中发起请求。",
|
||||
offlineDescription:
|
||||
"此实例已关闭分析功能,因此反馈不会记录在这里。你仍然可以直接联系 SnapOtter 团队。",
|
||||
offlinePublicNote: "GitHub issue 是公开的。",
|
||||
offlineGithubButton: "创建 GitHub issue",
|
||||
offlineEmailButton: "给我们发邮件",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -42,6 +42,7 @@ export const zhTW: TranslationKeys = {
|
||||
},
|
||||
feedback: {
|
||||
navLabel: "Share feedback",
|
||||
navButtonLabel: "意見回饋",
|
||||
dialogTitle: "Share feedback",
|
||||
toolDialogTitle: "How did this tool work?",
|
||||
failedDialogTitle: "Report a tool issue",
|
||||
@@ -65,6 +66,11 @@ export const zhTW: TranslationKeys = {
|
||||
searchMissTitle: "請求工具",
|
||||
searchMissContext: "你搜尋了:{query}",
|
||||
searchMissDiscussionsFallback: "我們無法記錄。請在 Discussions 中發起請求。",
|
||||
offlineDescription:
|
||||
"此執行個體已關閉分析功能,因此意見回饋不會記錄在這裡。你仍然可以直接聯絡 SnapOtter 團隊。",
|
||||
offlinePublicNote: "GitHub issue 是公開的。",
|
||||
offlineGithubButton: "建立 GitHub issue",
|
||||
offlineEmailButton: "寄電子郵件給我們",
|
||||
thanksTitle: "Thanks for the feedback.",
|
||||
thanksDescription: "It helps us improve SnapOtter without collecting files or private content.",
|
||||
quickThanks: "Thanks for the signal.",
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { FeedbackDialog } from "@/components/feedback/feedback-dialog";
|
||||
|
||||
const submitFeedback = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("@/lib/feedback", async (importOriginal) => {
|
||||
const actual: Record<string, unknown> = await importOriginal();
|
||||
return { ...actual, submitFeedback };
|
||||
});
|
||||
|
||||
const MESSAGE_PLACEHOLDER = "Tell us what worked, what broke, or what would make SnapOtter better.";
|
||||
|
||||
beforeEach(() => {
|
||||
submitFeedback.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("FeedbackDialog global off-state handoff", () => {
|
||||
it("reveals GitHub and email handoff when the server does not record the feedback", async () => {
|
||||
submitFeedback.mockResolvedValue({ ok: true, accepted: false });
|
||||
render(<FeedbackDialog open source="global" onClose={vi.fn()} />);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText(MESSAGE_PLACEHOLDER), {
|
||||
target: { value: "The queue stalls on large PDFs" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Send feedback" }));
|
||||
|
||||
const githubLink = await screen.findByRole("link", { name: "Open a GitHub issue" });
|
||||
expect(githubLink.getAttribute("href")).toContain("template=feedback.yml");
|
||||
expect(githubLink.getAttribute("href")).toContain("issues/new");
|
||||
|
||||
const emailLink = screen.getByRole("link", { name: "Email us" });
|
||||
expect(emailLink.getAttribute("href")).toContain("mailto:contact@snapotter.com");
|
||||
|
||||
// The typed message must actually reach both handoff targets, not just the static parts.
|
||||
const githubDetails = new URL(githubLink.getAttribute("href") ?? "").searchParams.get(
|
||||
"details",
|
||||
);
|
||||
expect(githubDetails).toBe("The queue stalls on large PDFs");
|
||||
const emailBody = new URL(emailLink.getAttribute("href") ?? "").searchParams.get("body");
|
||||
expect(emailBody).toBe("The queue stalls on large PDFs");
|
||||
|
||||
expect(screen.queryByText("Thanks for the feedback.")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows the normal thanks when the feedback is recorded", async () => {
|
||||
submitFeedback.mockResolvedValue({ ok: true, accepted: true });
|
||||
render(<FeedbackDialog open source="global" onClose={vi.fn()} />);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText(MESSAGE_PLACEHOLDER), {
|
||||
target: { value: "Great tool" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Send feedback" }));
|
||||
|
||||
expect(await screen.findByText("Thanks for the feedback.")).toBeDefined();
|
||||
expect(screen.queryByRole("link", { name: "Open a GitHub issue" })).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildFeedbackGithubUrl,
|
||||
buildFeedbackMailtoUrl,
|
||||
SNAPOTTER_FEEDBACK_EMAIL,
|
||||
} from "@/lib/feedback";
|
||||
|
||||
describe("buildFeedbackGithubUrl", () => {
|
||||
it("targets the feedback issue template and prefills the message", () => {
|
||||
const url = buildFeedbackGithubUrl("The export button is slow");
|
||||
expect(url).toContain("https://github.com/snapotter-hq/snapotter/issues/new");
|
||||
expect(url).toContain("template=feedback.yml");
|
||||
// URLSearchParams encodes spaces as "+"
|
||||
expect(url).toContain("details=The+export+button+is+slow");
|
||||
});
|
||||
|
||||
it("trims and clamps the message to 2000 characters", () => {
|
||||
const url = buildFeedbackGithubUrl(` hi ${"x".repeat(2100)}`);
|
||||
const details = new URL(url).searchParams.get("details") ?? "";
|
||||
expect(details.startsWith("hi")).toBe(true);
|
||||
expect(details.length).toBeLessThanOrEqual(2000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildFeedbackMailtoUrl", () => {
|
||||
it("builds a mailto to the project address with an encoded body", () => {
|
||||
const url = buildFeedbackMailtoUrl("Love the app, one bug");
|
||||
expect(url.startsWith(`mailto:${SNAPOTTER_FEEDBACK_EMAIL}`)).toBe(true);
|
||||
expect(url).toContain("subject=SnapOtter%20feedback");
|
||||
expect(url).toContain("body=Love%20the%20app%2C%20one%20bug");
|
||||
});
|
||||
|
||||
it("uses contact@snapotter.com", () => {
|
||||
expect(SNAPOTTER_FEEDBACK_EMAIL).toBe("contact@snapotter.com");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user