mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-29 19:06:24 +02:00
feat(editor): add mode toggle functionality and improve editor state management
This commit is contained in:
parent
9317b3f9fc
commit
06b509213c
3 changed files with 116 additions and 36 deletions
|
|
@ -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 />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue