mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-18 23:11:12 +02:00
refactor(editor): replace Loader2 with Spinner component and enhance save button visibility
This commit is contained in:
parent
06b509213c
commit
0381632bc2
2 changed files with 76 additions and 12 deletions
|
|
@ -7,7 +7,6 @@ import {
|
||||||
Download,
|
Download,
|
||||||
FileQuestionMark,
|
FileQuestionMark,
|
||||||
FileText,
|
FileText,
|
||||||
Loader2,
|
|
||||||
Pencil,
|
Pencil,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
XIcon,
|
XIcon,
|
||||||
|
|
@ -22,6 +21,7 @@ import { MarkdownViewer } from "@/components/markdown-viewer";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
|
import { Drawer, DrawerContent, DrawerHandle, DrawerTitle } from "@/components/ui/drawer";
|
||||||
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { useMediaQuery } from "@/hooks/use-media-query";
|
import { useMediaQuery } from "@/hooks/use-media-query";
|
||||||
import { useElectronAPI } from "@/hooks/use-platform";
|
import { useElectronAPI } from "@/hooks/use-platform";
|
||||||
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
import { authenticatedFetch, getBearerToken, redirectToLogin } from "@/lib/auth-utils";
|
||||||
|
|
@ -346,8 +346,8 @@ export function EditorPanelContent({
|
||||||
}}
|
}}
|
||||||
disabled={saving || !hasUnsavedChanges}
|
disabled={saving || !hasUnsavedChanges}
|
||||||
>
|
>
|
||||||
<span className={saving ? "invisible" : ""}>Save</span>
|
<span className={saving ? "opacity-0" : ""}>Save</span>
|
||||||
{saving && <Loader2 className="absolute size-3 animate-spin" />}
|
{saving && <Spinner size="xs" className="absolute" />}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -416,8 +416,8 @@ export function EditorPanelContent({
|
||||||
}}
|
}}
|
||||||
disabled={saving || !hasUnsavedChanges}
|
disabled={saving || !hasUnsavedChanges}
|
||||||
>
|
>
|
||||||
<span className={saving ? "invisible" : ""}>Save</span>
|
<span className={saving ? "opacity-0" : ""}>Save</span>
|
||||||
{saving && <Loader2 className="absolute size-3 animate-spin" />}
|
{saving && <Spinner size="xs" className="absolute" />}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -534,7 +534,7 @@ export function EditorPanelContent({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{downloading ? (
|
{downloading ? (
|
||||||
<Loader2 className="size-3.5 animate-spin" />
|
<Spinner size="xs" />
|
||||||
) : (
|
) : (
|
||||||
<Download className="size-3.5" />
|
<Download className="size-3.5" />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
import { ChevronDownIcon, XIcon } from "lucide-react";
|
import { ChevronDownIcon, Pencil, XIcon } from "lucide-react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
@ -125,6 +125,7 @@ export function ReportPanelContent({
|
||||||
|
|
||||||
// Editor state — tracks the latest markdown from the Plate editor
|
// Editor state — tracks the latest markdown from the Plate editor
|
||||||
const [editedMarkdown, setEditedMarkdown] = useState<string | null>(null);
|
const [editedMarkdown, setEditedMarkdown] = useState<string | null>(null);
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
// Read-only when public (shareToken) OR shared (SEARCH_SPACE visibility)
|
// Read-only when public (shareToken) OR shared (SEARCH_SPACE visibility)
|
||||||
const currentThreadState = useAtomValue(currentThreadAtom);
|
const currentThreadState = useAtomValue(currentThreadAtom);
|
||||||
|
|
@ -188,6 +189,7 @@ export function ReportPanelContent({
|
||||||
// Reset edited markdown when switching versions or reports
|
// Reset edited markdown when switching versions or reports
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEditedMarkdown(null);
|
setEditedMarkdown(null);
|
||||||
|
setIsEditing(false);
|
||||||
}, [activeReportId]);
|
}, [activeReportId]);
|
||||||
|
|
||||||
// Copy markdown content (uses latest editor content)
|
// Copy markdown content (uses latest editor content)
|
||||||
|
|
@ -257,7 +259,7 @@ export function ReportPanelContent({
|
||||||
|
|
||||||
// Save edited report content
|
// Save edited report content
|
||||||
const handleSave = useCallback(async () => {
|
const handleSave = useCallback(async () => {
|
||||||
if (!currentMarkdown || !activeReportId) return;
|
if (!currentMarkdown || !activeReportId) return false;
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const response = await authenticatedFetch(
|
const response = await authenticatedFetch(
|
||||||
|
|
@ -278,9 +280,11 @@ export function ReportPanelContent({
|
||||||
setReportContent((prev) => (prev ? { ...prev, content: currentMarkdown } : prev));
|
setReportContent((prev) => (prev ? { ...prev, content: currentMarkdown } : prev));
|
||||||
setEditedMarkdown(null);
|
setEditedMarkdown(null);
|
||||||
toast.success("Report saved successfully");
|
toast.success("Report saved successfully");
|
||||||
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error saving report:", err);
|
console.error("Error saving report:", err);
|
||||||
toast.error(err instanceof Error ? err.message : "Failed to save report");
|
toast.error(err instanceof Error ? err.message : "Failed to save report");
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
|
|
@ -289,6 +293,14 @@ export function ReportPanelContent({
|
||||||
const activeVersionIndex = versions.findIndex((v) => v.id === activeReportId);
|
const activeVersionIndex = versions.findIndex((v) => v.id === activeReportId);
|
||||||
const isPublic = !!shareToken;
|
const isPublic = !!shareToken;
|
||||||
const btnBg = isPublic ? "bg-main-panel" : "bg-sidebar";
|
const btnBg = isPublic ? "bg-main-panel" : "bg-sidebar";
|
||||||
|
const isResume = reportContent?.content_type === "typst";
|
||||||
|
const showReportEditingTier = !isResume;
|
||||||
|
const hasUnsavedChanges = editedMarkdown !== null;
|
||||||
|
|
||||||
|
const handleCancelEditing = useCallback(() => {
|
||||||
|
setEditedMarkdown(null);
|
||||||
|
setIsEditing(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -383,6 +395,58 @@ export function ReportPanelContent({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showReportEditingTier && (
|
||||||
|
<div className="flex h-10 items-center justify-between gap-2 border-t border-b px-4 shrink-0">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="truncate text-sm text-muted-foreground">
|
||||||
|
{reportContent?.title || title}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
|
{!isReadOnly &&
|
||||||
|
(isEditing ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 px-2 text-xs"
|
||||||
|
onClick={handleCancelEditing}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
className="relative h-6 w-[56px] px-0 text-xs"
|
||||||
|
onClick={async () => {
|
||||||
|
const saveSucceeded = await handleSave();
|
||||||
|
if (saveSucceeded) setIsEditing(false);
|
||||||
|
}}
|
||||||
|
disabled={saving || !hasUnsavedChanges}
|
||||||
|
>
|
||||||
|
<span className={saving ? "opacity-0" : ""}>Save</span>
|
||||||
|
{saving && <Spinner size="xs" className="absolute" />}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-6"
|
||||||
|
onClick={() => {
|
||||||
|
setEditedMarkdown(null);
|
||||||
|
setIsEditing(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pencil className="size-3.5" />
|
||||||
|
<span className="sr-only">Edit report</span>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Report content — skeleton/error/viewer/editor shown only in this area */}
|
{/* Report content — skeleton/error/viewer/editor shown only in this area */}
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|
@ -406,15 +470,15 @@ export function ReportPanelContent({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<PlateEditor
|
<PlateEditor
|
||||||
|
key={`report-${activeReportId}-${isEditing ? "editing" : "viewing"}`}
|
||||||
preset="full"
|
preset="full"
|
||||||
markdown={reportContent.content}
|
markdown={reportContent.content}
|
||||||
onMarkdownChange={setEditedMarkdown}
|
onMarkdownChange={setEditedMarkdown}
|
||||||
readOnly={false}
|
readOnly={!isEditing}
|
||||||
placeholder="Report content..."
|
placeholder="Report content..."
|
||||||
editorVariant="default"
|
editorVariant="default"
|
||||||
onSave={handleSave}
|
allowModeToggle={false}
|
||||||
hasUnsavedChanges={editedMarkdown !== null}
|
defaultEditing={isEditing}
|
||||||
isSaving={saving}
|
|
||||||
className="[&_[role=toolbar]]:!bg-sidebar"
|
className="[&_[role=toolbar]]:!bg-sidebar"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue