Skip to content

Commit 89a269e

Browse files
committed
improvement(file-viewer): reuse shared copy hook, lazy frontmatter split
- code-block: replace hand-rolled copy-with-timeout with shared useCopyToClipboard - rich-markdown-editor: compute frontmatter split once via lazy ref, drop redundant frontmatterRef - round-trip-safety: correct stale comments (read-only, not raw editor fallback)
1 parent f844a6a commit 89a269e

3 files changed

Lines changed: 18 additions & 26 deletions

File tree

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/code-block.tsx

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useState } from 'react'
1+
import { useState } from 'react'
22
import type { JSONContent } from '@tiptap/core'
33
import { CodeBlock } from '@tiptap/extension-code-block'
44
import type { ReactNodeViewProps } from '@tiptap/react'
@@ -12,6 +12,7 @@ import {
1212
DropdownMenuTrigger,
1313
} from '@/components/emcn'
1414
import { cn } from '@/lib/core/utils/cn'
15+
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
1516
import { detectLanguage } from './detect-language'
1617

1718
const PLAIN = 'plain'
@@ -35,29 +36,15 @@ const CONTROL_CLASS =
3536

3637
function CodeBlockView({ node, updateAttributes }: ReactNodeViewProps) {
3738
const [wrap, setWrap] = useState(false)
38-
const [copied, setCopied] = useState(false)
3939
const [menuOpen, setMenuOpen] = useState(false)
40+
const { copied, copy } = useCopyToClipboard({ resetMs: 1500 })
4041
const explicitLanguage = node.attrs.language as string | null
4142
const language = explicitLanguage ?? detectLanguage(node.textContent) ?? PLAIN
4243
const label =
4344
LANGUAGE_OPTIONS.find((option) => option.value === language)?.label ??
4445
explicitLanguage ??
4546
'Plain text'
4647

47-
useEffect(() => {
48-
if (!copied) return
49-
const timer = setTimeout(() => setCopied(false), 1500)
50-
return () => clearTimeout(timer)
51-
}, [copied])
52-
53-
const copy = async () => {
54-
const ok = await navigator.clipboard
55-
?.writeText(node.textContent)
56-
.then(() => true)
57-
.catch(() => false)
58-
if (ok) setCopied(true)
59-
}
60-
6148
return (
6249
<NodeViewWrapper className='group relative'>
6350
<div
@@ -111,7 +98,7 @@ function CodeBlockView({ node, updateAttributes }: ReactNodeViewProps) {
11198
type='button'
11299
aria-label='Copy code'
113100
onMouseDown={(event) => event.preventDefault()}
114-
onClick={copy}
101+
onClick={() => copy(node.textContent)}
115102
className={CONTROL_CLASS}
116103
>
117104
{copied ? <Check /> : <Copy />}

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { memo, useEffect, useMemo, useRef } from 'react'
3+
import { memo, useEffect, useRef } from 'react'
44
import type { Editor } from '@tiptap/react'
55
import { EditorContent, useEditor } from '@tiptap/react'
66
import { cn } from '@/lib/core/utils/cn'
@@ -163,9 +163,14 @@ function LoadedRichMarkdownEditor({
163163
}
164164
const isEditable = canEdit && roundTripSafeRef.current
165165

166-
const { frontmatter, body } = useMemo(() => splitFrontmatter(initialContent), [initialContent])
167-
const frontmatterRef = useRef(frontmatter)
168-
frontmatterRef.current = frontmatter
166+
// Split frontmatter off once, on the opened content (stable for the editor's lifetime, like the
167+
// verdict above): the body seeds the editor's initial document, and the frontmatter is re-attached
168+
// on every change so the editor only ever round-trips the body.
169+
const splitRef = useRef<{ frontmatter: string; body: string } | null>(null)
170+
if (splitRef.current === null) {
171+
splitRef.current = splitFrontmatter(initialContent)
172+
}
173+
const { frontmatter, body } = splitRef.current
169174
const onChangeRef = useRef(onChange)
170175
onChangeRef.current = onChange
171176
const onSaveShortcutRef = useRef(onSaveShortcut)
@@ -251,7 +256,7 @@ function LoadedRichMarkdownEditor({
251256
},
252257
onUpdate: ({ editor }) => {
253258
const md = postProcessSerializedMarkdown(editor.getMarkdown())
254-
onChangeRef.current(applyFrontmatter(frontmatterRef.current, md))
259+
onChangeRef.current(applyFrontmatter(frontmatter, md))
255260
},
256261
})
257262
editorInstanceRef.current = editor

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/round-trip-safety.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
/**
1010
* Above this size we don't run the (synchronous) round-trip probe — building two editors to
1111
* serialize a large document blocks the main thread for too long, and a very large markdown file
12-
* is heavier to edit richly anyway, so it opens in the raw editor.
12+
* is heavier to edit richly anyway, so it opens read-only.
1313
*/
1414
const PROBE_SIZE_LIMIT = 128 * 1024
1515

@@ -48,7 +48,7 @@ const STABLE_LOSS_PATTERNS: ReadonlyArray<RegExp> = [
4848
* or tilde, length-matched on the closer so nested fences strip as one unit) and inline code.
4949
* Indented (4-space) code is deliberately NOT stripped — list/paragraph continuation lines are
5050
* also indented, and over-stripping would risk missing a real unsafe construct (a false negative,
51-
* which is worse than the rare false positive of an indented code block opening in the raw editor).
51+
* which is worse than the rare false positive of an indented code block opening read-only).
5252
*/
5353
function stripCode(content: string): string {
5454
return content
@@ -70,8 +70,8 @@ function serialize(content: string): string {
7070

7171
/**
7272
* Whether `content` survives the editor's markdown round-trip without data loss or autosave
73-
* churn. Callers fall back to the raw text editor when this is false, so the gate is
74-
* deliberately conservative: it rejects on any doubt rather than risk silently corrupting a file.
73+
* churn. The editor opens the content read-only when this is false, so the probe is deliberately
74+
* conservative: it rejects on any doubt rather than risk an edit silently corrupting a file.
7575
*
7676
* Two complementary checks: known stable-loss constructs are matched directly (the idempotency
7777
* probe is blind to them), and everything else must reach a fixpoint — `serialize(x)` twice in a

0 commit comments

Comments
 (0)