This commit is contained in:
abhinav7x94 2026-08-26 04:01:27 +05:30 committed by GitHub
commit c35a32a8f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 223 additions and 43 deletions

View file

@ -5,7 +5,7 @@ import { LinkIcon } from "lucide-react"
import { cn } from "@lib/utils"
import { Button } from "@ui/components/button"
import { dmSansClassName } from "@/lib/fonts"
import { TextEditor } from "../text-editor"
import { resolveSubmittedContent, TextEditor } from "../text-editor"
import { extractUrls } from "@/lib/url-helpers"
interface NoteContentProps {
@ -31,11 +31,10 @@ export function NoteContent({
const { urls: detectedUrls } = useMemo(() => extractUrls(content), [content])
const showBulkOffer = detectedUrls.length >= 2 && !dismissed
const canSubmit = content.trim().length > 0 && !isSubmitting
const handleSubmit = () => {
if (canSubmit && onSubmit) {
onSubmit(content)
const handleSubmit = (submittedContent?: string) => {
const contentToSubmit = resolveSubmittedContent(submittedContent, content)
if (contentToSubmit.trim() && !isSubmitting && onSubmit) {
onSubmit(contentToSubmit)
}
}

View file

@ -14,7 +14,7 @@ export interface TextEditorProps {
hasUnsavedChanges: boolean
isSaving: boolean
onContentChange: (content: string) => void
onSave: () => void
onSave: (content?: string) => void
onReset: () => void
}
@ -60,7 +60,7 @@ export function TextEditorContent({
<Button
variant="insideOut"
size="sm"
onClick={onSave}
onClick={() => onSave()}
disabled={isSaving}
className="hover:text-white rounded-full px-4"
>

View file

@ -28,6 +28,7 @@ import { toast } from "sonner"
import { useIsMobile } from "@hooks/use-mobile"
import { parsePluginDocument } from "@/lib/plugin-document"
import { useFullDocumentContent } from "@/hooks/use-full-document"
import { resolveSubmittedContent } from "../text-editor"
type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
type DocumentWithMemories = DocumentsResponse["documents"][0]
@ -247,15 +248,23 @@ export function DocumentModal({
draftContentString !== initialEditorString &&
draftContentString !== lastSavedContent
const handleSave = useCallback(() => {
if (!_document?.id) return
updateMutation.mutate(
{ documentId: _document.id, content: draftContentString },
{
onSuccess: (_data, variables) => setLastSavedContent(variables.content),
},
)
}, [_document?.id, draftContentString, updateMutation])
const handleSave = useCallback(
(submittedContent?: string) => {
if (!_document?.id) return
const content = resolveSubmittedContent(
submittedContent,
draftContentString,
)
updateMutation.mutate(
{ documentId: _document.id, content },
{
onSuccess: (_data, variables) =>
setLastSavedContent(variables.content),
},
)
},
[_document?.id, draftContentString, updateMutation],
)
const textEditorProps = useMemo(
() => ({

View file

@ -7,7 +7,7 @@ import { dmSansClassName } from "@/lib/fonts"
import { Logo } from "@ui/assets/Logo"
import { Minimize2, Plus, Loader2 } from "lucide-react"
import { useAuth } from "@lib/auth-context"
import { TextEditor } from "./text-editor"
import { resolveSubmittedContent, TextEditor } from "./text-editor"
import { useProject } from "@/stores"
import { useQuickNoteDraft } from "@/stores/quick-note-draft"
import { useLocalStorageUsername } from "@hooks/use-local-storage-username"
@ -52,12 +52,18 @@ export function FullscreenNoteModal({
""
const userName = displayName ? `${displayName.split(" ")[0]}'s` : "My"
const handleSave = useCallback(() => {
const currentContent = contentRef.current
if (currentContent.trim() && !isSaving) {
onSave(currentContent)
}
}, [isSaving, onSave])
const handleSave = useCallback(
(submittedContent?: string) => {
const currentContent = resolveSubmittedContent(
submittedContent,
contentRef.current,
)
if (currentContent.trim() && !isSaving) {
onSave(currentContent)
}
},
[isSaving, onSave],
)
const handleContentChange = useCallback(
(newContent: string) => {
@ -164,7 +170,7 @@ export function FullscreenNoteModal({
>
<button
type="button"
onClick={handleSave}
onClick={() => handleSave()}
disabled={!canSave}
className={cn(
"bg-[#1B1F24] rounded-[8px] px-4 py-2.5 flex items-center justify-center gap-1.5 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",

View file

@ -8,7 +8,7 @@ import { dmSansClassName } from "@/lib/fonts"
import { Maximize2, Plus, Loader2, X } from "lucide-react"
import { useProject } from "@/stores"
import { useQuickNoteDraft } from "@/stores/quick-note-draft"
import { TextEditor } from "./text-editor"
import { resolveSubmittedContent, TextEditor } from "./text-editor"
interface QuickNoteCardProps {
onSave: (content: string) => void
@ -85,11 +85,15 @@ export function QuickNoteCard({
setIsExpanded(false)
}, [])
const handleSaveClick = useCallback(() => {
if (draft.trim() && !isSaving) {
onSave(draft)
}
}, [draft, isSaving, onSave])
const handleSave = useCallback(
(submittedContent?: string) => {
const content = resolveSubmittedContent(submittedContent, draft)
if (content.trim() && !isSaving) {
onSave(content)
}
},
[draft, isSaving, onSave],
)
const handleMaximizeClick = useCallback(() => {
setIsExpanded(false)
@ -199,7 +203,7 @@ export function QuickNoteCard({
>
<button
type="button"
onClick={handleSaveClick}
onClick={() => handleSave()}
disabled={!canSave}
className={cn(
"flex items-center gap-1.5 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",
@ -322,7 +326,7 @@ export function QuickNoteCard({
<TextEditor
content={expandedInitialContent}
onContentChange={handleChange}
onSubmit={handleSaveClick}
onSubmit={handleSave}
debounceMs={0}
autoFocus
placeholder="Start writing..."
@ -332,7 +336,7 @@ export function QuickNoteCard({
<footer className="flex shrink-0 justify-center border-t border-[#202A36]/70 px-4 py-4">
<button
type="button"
onClick={handleSaveClick}
onClick={() => handleSave()}
disabled={!canSave}
className={cn(
"bg-[#1B1F24] rounded-[8px] px-4 py-2.5 flex items-center justify-center gap-1.5 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50",

View file

@ -0,0 +1,116 @@
import { describe, expect, it } from "bun:test"
import {
handleEditorSubmitShortcut,
resolveSubmittedContent,
submitEditorContent,
} from "."
type EditorSnapshot = NonNullable<Parameters<typeof submitEditorContent>[0]>
function createEditor(readMarkdown: () => string): EditorSnapshot {
return {
getJSON: () => ({ type: "doc" }),
storage: {
markdown: {
manager: {
serialize: readMarkdown,
},
},
},
} as unknown as EditorSnapshot
}
describe("submitEditorContent", () => {
it("submits the editor snapshot captured before pending updates flush", () => {
let markdown = "latest editor content"
const events: string[] = []
const editor = createEditor(() => {
events.push("serialize")
return markdown
})
submitEditorContent(
editor,
() => {
events.push("flush")
markdown = "content changed during flush"
},
(content) => events.push(`submit:${content}`),
)
expect(events).toEqual([
"serialize",
"flush",
"submit:latest editor content",
])
})
it("does not submit when the editor is unavailable", () => {
const events: string[] = []
submitEditorContent(
null,
() => events.push("flush"),
(content) => events.push(`submit:${content}`),
)
expect(events).toEqual([])
})
})
describe("handleEditorSubmitShortcut", () => {
it("handles both Command+Enter and Control+Enter", () => {
for (const modifier of ["metaKey", "ctrlKey"] as const) {
const events: string[] = []
const editor = createEditor(() => "latest editor content")
const event = {
metaKey: false,
ctrlKey: false,
key: "Enter",
preventDefault: () => events.push("preventDefault"),
[modifier]: true,
}
const handled = handleEditorSubmitShortcut(
event,
editor,
() => events.push("flush"),
(content) => events.push(`submit:${content}`),
)
expect(handled).toBe(true)
expect(events).toEqual([
"preventDefault",
"flush",
"submit:latest editor content",
])
}
})
it("ignores unrelated key presses", () => {
const events: string[] = []
const handled = handleEditorSubmitShortcut(
{
metaKey: true,
ctrlKey: false,
key: "Escape",
preventDefault: () => events.push("preventDefault"),
},
createEditor(() => "latest editor content"),
() => events.push("flush"),
(content) => events.push(`submit:${content}`),
)
expect(handled).toBe(false)
expect(events).toEqual([])
})
})
describe("resolveSubmittedContent", () => {
it("prefers the editor snapshot and falls back for button submissions", () => {
expect(resolveSubmittedContent("latest", "stale")).toBe("latest")
expect(resolveSubmittedContent(undefined, "button state")).toBe(
"button state",
)
})
})

View file

@ -11,6 +11,50 @@ import { Bold, Italic, Code } from "lucide-react"
import { useDebouncedCallback } from "use-debounce"
import { cn } from "@lib/utils"
type EditorSnapshot = Pick<Editor, "getJSON" | "storage">
type SubmitShortcutEvent = Pick<
KeyboardEvent,
"ctrlKey" | "key" | "metaKey" | "preventDefault"
>
function getEditorMarkdown(editor: EditorSnapshot): string {
const json = editor.getJSON()
return editor.storage.markdown?.manager?.serialize(json) ?? ""
}
export function resolveSubmittedContent(
submittedContent: string | undefined,
fallbackContent: string,
): string {
return submittedContent ?? fallbackContent
}
export function submitEditorContent(
editor: EditorSnapshot | null,
flushPendingUpdate: () => void,
onSubmit: (content: string) => void,
): void {
if (!editor) return
// Parent state updates flushed below are not visible to this event's submit closure.
const markdown = getEditorMarkdown(editor)
flushPendingUpdate()
onSubmit(markdown)
}
export function handleEditorSubmitShortcut(
event: SubmitShortcutEvent,
editor: EditorSnapshot | null,
flushPendingUpdate: () => void,
onSubmit: (content: string) => void,
): boolean {
if (!(event.metaKey || event.ctrlKey) || event.key !== "Enter") return false
event.preventDefault()
submitEditorContent(editor, flushPendingUpdate, onSubmit)
return true
}
export function TextEditor({
content: initialContent,
onContentChange,
@ -21,7 +65,7 @@ export function TextEditor({
}: {
content: string | undefined
onContentChange: (content: string) => void
onSubmit: () => void
onSubmit: (content: string) => void
debounceMs?: number
autoFocus?: boolean
placeholder?: string
@ -41,8 +85,7 @@ export function TextEditor({
const debouncedUpdates = useDebouncedCallback((editor: Editor) => {
if (!hasUserEditedRef.current) return
const json = editor.getJSON()
const markdown = editor.storage.markdown?.manager?.serialize(json) ?? ""
const markdown = getEditorMarkdown(editor)
onContentChange?.(markdown)
}, debounceMs)
@ -58,8 +101,7 @@ export function TextEditor({
editorRef.current = editor
if (!hasUserEditedRef.current) return
if (debounceMs === 0) {
const json = editor.getJSON()
const markdown = editor.storage.markdown?.manager?.serialize(json) ?? ""
const markdown = getEditorMarkdown(editor)
onContentChange?.(markdown)
return
}
@ -67,10 +109,14 @@ export function TextEditor({
},
editorProps: {
handleKeyDown: (_view, event) => {
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
event.preventDefault()
debouncedUpdates.flush()
onSubmitRef.current?.()
if (
handleEditorSubmitShortcut(
event,
editorRef.current,
() => debouncedUpdates.flush(),
onSubmitRef.current,
)
) {
return true
}
hasUserEditedRef.current = true