feat(editor): add mode toggle functionality and improve editor state management

This commit is contained in:
Anish Sarkar 2026-04-23 19:52:55 +05:30
parent 9317b3f9fc
commit 06b509213c
3 changed files with 116 additions and 36 deletions

View file

@ -88,7 +88,7 @@ export function EditorPanelContent({
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [downloading, setDownloading] = useState(false); const [downloading, setDownloading] = useState(false);
const [isSourceEditing, setIsSourceEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [editedMarkdown, setEditedMarkdown] = useState<string | null>(null); const [editedMarkdown, setEditedMarkdown] = useState<string | null>(null);
const [localFileContent, setLocalFileContent] = useState(""); const [localFileContent, setLocalFileContent] = useState("");
@ -111,7 +111,7 @@ export function EditorPanelContent({
setEditedMarkdown(null); setEditedMarkdown(null);
setLocalFileContent(""); setLocalFileContent("");
setHasCopied(false); setHasCopied(false);
setIsSourceEditing(false); setIsEditing(false);
initialLoadDone.current = false; initialLoadDone.current = false;
changeCountRef.current = 0; changeCountRef.current = 0;
@ -295,10 +295,18 @@ export function EditorPanelContent({
: false; : false;
const hasUnsavedChanges = editedMarkdown !== null; const hasUnsavedChanges = editedMarkdown !== null;
const showDesktopHeader = !!onClose; const showDesktopHeader = !!onClose;
const isSourceCodeMode = editorRenderMode === "source_code"; const showEditingActions = isEditableType && isEditing;
const showEditingActions = isSourceCodeMode && isSourceEditing;
const localFileLanguage = inferMonacoLanguageFromPath(localFilePath); const localFileLanguage = inferMonacoLanguageFromPath(localFilePath);
const handleCancelEditing = useCallback(() => {
const savedContent = editorDoc?.source_markdown ?? "";
markdownRef.current = savedContent;
setLocalFileContent(savedContent);
setEditedMarkdown(null);
changeCountRef.current = 0;
setIsEditing(false);
}, [editorDoc?.source_markdown]);
return ( return (
<> <>
{showDesktopHeader ? ( {showDesktopHeader ? (
@ -323,13 +331,7 @@ export function EditorPanelContent({
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-6 px-2 text-xs" className="h-6 px-2 text-xs"
onClick={() => { onClick={handleCancelEditing}
const savedContent = editorDoc?.source_markdown ?? "";
markdownRef.current = savedContent;
setLocalFileContent(savedContent);
setEditedMarkdown(null);
setIsSourceEditing(false);
}}
disabled={saving} disabled={saving}
> >
Cancel Cancel
@ -340,7 +342,7 @@ export function EditorPanelContent({
className="relative h-6 w-[56px] px-0 text-xs" className="relative h-6 w-[56px] px-0 text-xs"
onClick={async () => { onClick={async () => {
const saveSucceeded = await handleSave({ silent: true }); const saveSucceeded = await handleSave({ silent: true });
if (saveSucceeded) setIsSourceEditing(false); if (saveSucceeded) setIsEditing(false);
}} }}
disabled={saving || !hasUnsavedChanges} disabled={saving || !hasUnsavedChanges}
> >
@ -364,15 +366,19 @@ export function EditorPanelContent({
{hasCopied ? "Copied file contents" : "Copy file contents"} {hasCopied ? "Copied file contents" : "Copy file contents"}
</span> </span>
</Button> </Button>
{isSourceCodeMode && ( {isEditableType && (
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="size-6" className="size-6"
onClick={() => setIsSourceEditing(true)} onClick={() => {
changeCountRef.current = 0;
setEditedMarkdown(null);
setIsEditing(true);
}}
> >
<Pencil className="size-3.5" /> <Pencil className="size-3.5" />
<span className="sr-only">Edit file</span> <span className="sr-only">Edit document</span>
</Button> </Button>
)} )}
</> </>
@ -389,11 +395,69 @@ export function EditorPanelContent({
<h2 className="text-sm font-semibold truncate">{displayTitle}</h2> <h2 className="text-sm font-semibold truncate">{displayTitle}</h2>
</div> </div>
<div className="flex items-center gap-1 shrink-0"> <div className="flex items-center gap-1 shrink-0">
{!isLocalFileMode && editorDoc?.document_type && documentId && ( {showEditingActions ? (
<VersionHistoryButton <>
documentId={documentId} <Button
documentType={editorDoc.document_type} 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({ silent: true });
if (saveSucceeded) setIsEditing(false);
}}
disabled={saving || !hasUnsavedChanges}
>
<span className={saving ? "invisible" : ""}>Save</span>
{saving && <Loader2 className="absolute size-3 animate-spin" />}
</Button>
</>
) : (
<>
<Button
variant="ghost"
size="icon"
className="size-6"
onClick={() => {
void handleCopy();
}}
disabled={isLoading || !editorDoc}
>
{hasCopied ? <Check className="size-3.5" /> : <Copy className="size-3.5" />}
<span className="sr-only">
{hasCopied ? "Copied file contents" : "Copy file contents"}
</span>
</Button>
{isEditableType && (
<Button
variant="ghost"
size="icon"
className="size-6"
onClick={() => {
changeCountRef.current = 0;
setEditedMarkdown(null);
setIsEditing(true);
}}
>
<Pencil className="size-3.5" />
<span className="sr-only">Edit document</span>
</Button>
)}
{!isLocalFileMode && editorDoc?.document_type && documentId && (
<VersionHistoryButton
documentId={documentId}
documentType={editorDoc.document_type}
/>
)}
</>
)} )}
</div> </div>
</div> </div>
@ -489,7 +553,7 @@ export function EditorPanelContent({
onSave={() => { onSave={() => {
void handleSave({ silent: true }); void handleSave({ silent: true });
}} }}
readOnly={!isSourceEditing} readOnly={!isEditing}
onChange={(next) => { onChange={(next) => {
markdownRef.current = next; markdownRef.current = next;
setLocalFileContent(next); setLocalFileContent(next);
@ -500,19 +564,15 @@ export function EditorPanelContent({
</div> </div>
) : isEditableType ? ( ) : isEditableType ? (
<PlateEditor <PlateEditor
key={isLocalFileMode ? localFilePath ?? "local-file" : documentId} key={`${isLocalFileMode ? localFilePath ?? "local-file" : documentId}-${isEditing ? "editing" : "viewing"}`}
preset="full" preset="full"
markdown={editorDoc.source_markdown} markdown={editorDoc.source_markdown}
onMarkdownChange={handleMarkdownChange} onMarkdownChange={handleMarkdownChange}
readOnly={false} readOnly={!isEditing}
placeholder="Start writing..." placeholder="Start writing..."
editorVariant="default" editorVariant="default"
onSave={() => { allowModeToggle={false}
void handleSave(); defaultEditing={isEditing}
}}
hasUnsavedChanges={editedMarkdown !== null}
isSaving={saving}
defaultEditing={true}
className="[&_[role=toolbar]]:!bg-sidebar" className="[&_[role=toolbar]]:!bg-sidebar"
/> />
) : ( ) : (
@ -561,6 +621,8 @@ function MobileEditorDrawer() {
const panelState = useAtomValue(editorPanelAtom); const panelState = useAtomValue(editorPanelAtom);
const closePanel = useSetAtom(closeEditorPanelAtom); const closePanel = useSetAtom(closeEditorPanelAtom);
if (panelState.kind === "local_file") return null;
const hasTarget = const hasTarget =
panelState.kind === "document" panelState.kind === "document"
? !!panelState.documentId && !!panelState.searchSpaceId ? !!panelState.documentId && !!panelState.searchSpaceId
@ -604,6 +666,7 @@ export function EditorPanel() {
: !!panelState.localFilePath; : !!panelState.localFilePath;
if (!panelState.isOpen || !hasTarget) return null; if (!panelState.isOpen || !hasTarget) return null;
if (!isDesktop && panelState.kind === "local_file") return null;
if (isDesktop) { if (isDesktop) {
return <DesktopEditorPanel />; return <DesktopEditorPanel />;
@ -620,7 +683,7 @@ export function MobileEditorPanel() {
? !!panelState.documentId && !!panelState.searchSpaceId ? !!panelState.documentId && !!panelState.searchSpaceId
: !!panelState.localFilePath; : !!panelState.localFilePath;
if (isDesktop || !panelState.isOpen || !hasTarget) return null; if (isDesktop || !panelState.isOpen || !hasTarget || panelState.kind === "local_file") return null;
return <MobileEditorDrawer />; return <MobileEditorDrawer />;
} }

View file

@ -42,6 +42,8 @@ export interface PlateEditorProps {
hasUnsavedChanges?: boolean; hasUnsavedChanges?: boolean;
/** Whether a save is in progress */ /** Whether a save is in progress */
isSaving?: boolean; isSaving?: boolean;
/** Whether edit/view mode toggle UI should be available in toolbars. */
allowModeToggle?: boolean;
/** Start the editor in editing mode instead of viewing mode. Ignored when readOnly is true. */ /** Start the editor in editing mode instead of viewing mode. Ignored when readOnly is true. */
defaultEditing?: boolean; defaultEditing?: boolean;
/** /**
@ -91,6 +93,7 @@ export function PlateEditor({
onSave, onSave,
hasUnsavedChanges = false, hasUnsavedChanges = false,
isSaving = false, isSaving = false,
allowModeToggle = true,
defaultEditing = false, defaultEditing = false,
preset = "full", preset = "full",
extraPlugins = [], extraPlugins = [],
@ -174,7 +177,7 @@ export function PlateEditor({
}, [html, markdown, editor]); }, [html, markdown, editor]);
// When not forced read-only, the user can toggle between editing/viewing. // When not forced read-only, the user can toggle between editing/viewing.
const canToggleMode = !readOnly; const canToggleMode = !readOnly && allowModeToggle;
const contextProviderValue = useMemo( const contextProviderValue = useMemo(
() => ({ () => ({

View file

@ -1,19 +1,33 @@
"use client"; "use client";
import { createPlatePlugin } from "platejs/react"; import { createPlatePlugin } from "platejs/react";
import { useEditorReadOnly } from "platejs/react";
import { useEditorSave } from "@/components/editor/editor-save-context";
import { FixedToolbar } from "@/components/ui/fixed-toolbar"; import { FixedToolbar } from "@/components/ui/fixed-toolbar";
import { FixedToolbarButtons } from "@/components/ui/fixed-toolbar-buttons"; import { FixedToolbarButtons } from "@/components/ui/fixed-toolbar-buttons";
function ConditionalFixedToolbar() {
const readOnly = useEditorReadOnly();
const { onSave, hasUnsavedChanges, canToggleMode } = useEditorSave();
const hasVisibleControls =
!readOnly || canToggleMode || (!!onSave && hasUnsavedChanges && !readOnly);
if (!hasVisibleControls) return null;
return (
<FixedToolbar>
<FixedToolbarButtons />
</FixedToolbar>
);
}
export const FixedToolbarKit = [ export const FixedToolbarKit = [
createPlatePlugin({ createPlatePlugin({
key: "fixed-toolbar", key: "fixed-toolbar",
render: { render: {
beforeEditable: () => ( beforeEditable: () => <ConditionalFixedToolbar />,
<FixedToolbar>
<FixedToolbarButtons />
</FixedToolbar>
),
}, },
}), }),
]; ];