diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx index 4bbaaca5074..f24593d53ce 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx @@ -1,5 +1,6 @@ import { useParams } from "@tanstack/react-router"; import { electronTrpc } from "renderer/lib/electron-trpc"; +import { FeedbackButton } from "./components/FeedbackButton"; import { OpenInMenuButton } from "./components/OpenInMenuButton"; import { OrganizationDropdown } from "./components/OrganizationDropdown"; import { SidebarToggle } from "./components/SidebarToggle"; @@ -36,6 +37,7 @@ export function TopBar() { /> )} + {!isMac && } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/FeedbackButton/FeedbackButton.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/FeedbackButton/FeedbackButton.tsx new file mode 100644 index 00000000000..f73cd801251 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/FeedbackButton/FeedbackButton.tsx @@ -0,0 +1,255 @@ +import { Button } from "@superset/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@superset/ui/dialog"; +import { toast } from "@superset/ui/sonner"; +import { Textarea } from "@superset/ui/textarea"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { useRef, useState } from "react"; +import { FaDiscord } from "react-icons/fa6"; +import { + LuBookOpen, + LuCircleHelp, + LuGithub, + LuImagePlus, + LuLoader, + LuTrash2, + LuX, +} from "react-icons/lu"; +import { apiTrpcClient } from "renderer/lib/api-trpc-client"; +import { + useAddFeedbackImage, + useClearFeedbackForm, + useCloseFeedbackModal, + useFeedbackImages, + useFeedbackMessage, + useFeedbackModalOpen, + useOpenFeedbackModal, + useRemoveFeedbackImage, + useSetFeedbackMessage, +} from "renderer/stores/feedback-modal"; + +export function FeedbackButton() { + const isOpen = useFeedbackModalOpen(); + const openModal = useOpenFeedbackModal(); + const closeModal = useCloseFeedbackModal(); + const message = useFeedbackMessage(); + const images = useFeedbackImages(); + const setMessage = useSetFeedbackMessage(); + const addImage = useAddFeedbackImage(); + const removeImage = useRemoveFeedbackImage(); + const clearForm = useClearFeedbackForm(); + const fileInputRef = useRef(null); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleFileSelect = (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files) return; + + for (const file of files) { + if (!file.type.startsWith("image/")) continue; + + const reader = new FileReader(); + reader.onload = (event) => { + const dataUrl = event.target?.result as string; + addImage({ + id: crypto.randomUUID(), + dataUrl, + name: file.name, + }); + }; + reader.readAsDataURL(file); + } + + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + const handleSend = async () => { + if (!message.trim()) { + toast.error("Please enter a message"); + return; + } + + setIsSubmitting(true); + try { + await apiTrpcClient.feedback.create.mutate({ + message: message.trim(), + images: images.map((img) => img.dataUrl), + }); + + toast.success("Feedback sent", { + description: "Thank you for your feedback!", + }); + clearForm(); + closeModal(); + } catch (error) { + console.error("[feedback] Failed to submit:", error); + toast.error("Failed to send feedback", { + description: "Please try again later", + }); + } finally { + setIsSubmitting(false); + } + }; + + const handleClear = () => { + clearForm(); + }; + + const hasContent = message.trim() || images.length > 0; + + return ( + <> + + + + + Feedback + + + !open && closeModal()} + > + + + Send Feedback + + +
+