Skip to content

Commit 7191792

Browse files
authored
improvement(rich-markdown-editor): table column-resize cursor, exhaustive paste tests, editor docs (#5455)
* fix(rich-markdown-editor): show the col-resize cursor on table column borders prosemirror-tables toggles a `resize-cursor` class on the editor while the pointer is over a column boundary, but there was no rule to change the cursor — the blue resize handle showed with no cursor affordance. Add the scoped `col-resize` rule. * test(rich-markdown-editor): exhaustive markdown paste coverage Cover every rich construct (headings, marks, lists, task lists, blockquote, code block, image, thematic break, table), markdown parsed despite an HTML sibling, multi-block order, read-only rejection, and the defer/verbatim cases for non-markdown input. * docs(editor): add rich markdown editor page Document the inline rich markdown editor — formatting, structure, lists, tables, code blocks, images, the slash menu, and markdown fidelity — with a rendered overview screenshot. * test(rich-markdown-editor): scope paste tests to what MarkdownPaste actually gates Inline-only marks (single-asterisk italic, ~~, single-backtick code) are intentionally not detected by looksLikeMarkdown (single `*` would false-positive on e.g. `*args`); they route through the Markdown extension's own paste path, not MarkdownPaste. Move them from the rich-render cases to the defers-to-default cases so the suite tests the handler it names.
1 parent 03478cd commit 7191792

5 files changed

Lines changed: 125 additions & 2 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
title: Editor
3+
description: A rich markdown editor for your files — type markdown and watch it render, or edit visually.
4+
pageType: concept
5+
---
6+
7+
import { Image } from '@/components/ui/image'
8+
import { Card, Cards } from 'fumadocs-ui/components/card'
9+
import { Callout } from 'fumadocs-ui/components/callout'
10+
11+
Every markdown file in your workspace opens in a **rich editor**. Type markdown and it renders as you go, or format visually with the toolbar and slash menu. What you see is exactly what gets saved — plain markdown underneath, no lock-in.
12+
13+
<div className="mx-auto w-full overflow-hidden rounded-lg my-6">
14+
<Image src="/static/files/editor/overview.png" alt="A markdown file rendered in the editor: headings, bold, italic, and a link, a nested bullet list, and a table" width={900} height={410} />
15+
</div>
16+
17+
## Formatting text
18+
19+
Select any text to bring up the formatting toolbar — bold, italic, strikethrough, inline code, and links. The same marks appear instantly as you type the markdown for them, like `**bold**` or `*italic*`. Links show a hover card so you can open, copy, edit, or remove them without hunting through the source.
20+
21+
## Structure
22+
23+
Headings, blockquotes, and dividers keep long documents scannable. Type `# ` through `###### ` for headings, `> ` for a quote, and `---` for a divider.
24+
25+
## Lists and checklists
26+
27+
Bullet, ordered, and nested lists all work, plus task lists you can tick right in the document.
28+
29+
## Tables
30+
31+
Insert a table from the slash menu, then click any cell for the floating table toolbar — add or remove rows and columns, toggle the header row, or delete the table. Drag a column border to resize it.
32+
33+
## Code blocks
34+
35+
Fenced code blocks are syntax-highlighted, with a language picker in the corner. Pick `mermaid` to render a live diagram instead of code.
36+
37+
## Images
38+
39+
Paste or drag an image straight into the document, then drag a corner to resize it.
40+
41+
## Slash menu and shortcuts
42+
43+
Type `/` anywhere to insert any block — heading, list, table, code block, image, and more — without leaving the keyboard. Familiar shortcuts work too: **Cmd/Ctrl + B** for bold, **Cmd/Ctrl + I** for italic, and **Cmd/Ctrl + K** to add a link over selected text.
44+
45+
## Markdown fidelity
46+
47+
The editor round-trips your markdown exactly — it saves what you wrote, with no reformatting churn.
48+
49+
<Callout type="info">
50+
A few constructs can't be represented visually without losing information on save — footnotes, raw HTML, and HTML comments. When a file contains one of these, it opens **read-only** so the original source is preserved untouched. Everything is still rendered faithfully; you just can't edit that file inline.
51+
</Callout>
52+
53+
<Cards>
54+
<Card title="Generating files" href="/files/generating" description="Have a workflow produce a document from a run." />
55+
<Card title="Using files in workflows" href="/files/using-in-workflows" description="Read a file into a workflow and produce one from a run." />
56+
</Cards>

apps/docs/content/docs/en/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"./tables/workflow-columns",
2828
"---Files---",
2929
"./files/index",
30+
"./files/editor",
3031
"./files/using-in-workflows",
3132
"./files/generating",
3233
"./files/passing-files",
32.7 KB
Loading

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

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ afterEach(() => {
1616
editor = null
1717
})
1818

19-
function mount(): Editor {
20-
return new Editor({ extensions: [...createMarkdownContentExtensions(), MarkdownPaste] })
19+
function mount(editable = true): Editor {
20+
return new Editor({ extensions: [...createMarkdownContentExtensions(), MarkdownPaste], editable })
2121
}
2222

2323
/** Run the plugin paste handlers the way ProseMirror would, with a mocked clipboard. */
@@ -93,4 +93,61 @@ describe('markdown paste', () => {
9393
expect(editor.isActive('codeBlock')).toBe(true)
9494
expect(paste(editor, '[link](https://example.com)')).toBe(false)
9595
})
96+
97+
it('rejects the paste entirely in a read-only editor', () => {
98+
editor = mount(false)
99+
expect(paste(editor, '# heading\n\n- one\n- two')).toBe(false)
100+
expect(editor.getText()).toBe('')
101+
})
102+
103+
it.each([
104+
['empty string', ''],
105+
['whitespace only', ' \n\n '],
106+
['a bare thematic break (ambiguous — needs another markdown signal)', '---'],
107+
['inline-only italic (single asterisk would false-positive on e.g. *args)', 'an *italic* word'],
108+
['inline-only strikethrough', 'a ~~struck~~ word'],
109+
['inline-only code', 'some `code` here'],
110+
])('leaves %s to the default handler', (_label, text) => {
111+
editor = mount()
112+
expect(paste(editor, text)).toBe(false)
113+
})
114+
115+
// Only structural / unambiguous constructs gate the markdown parse. Inline-only marks that
116+
// `looksLikeMarkdown` deliberately omits to avoid false positives — single-asterisk italic
117+
// (`*args`), `~~`, single-backtick code — are covered by the Markdown extension's own paste path,
118+
// not MarkdownPaste, so they belong to a different test surface.
119+
it.each([
120+
['heading', '# Heading', 'heading'],
121+
['bold', 'a **bold** word', 'bold'],
122+
['bullet list', '- one\n- two', 'bulletList'],
123+
['ordered list', '1. one\n2. two', 'orderedList'],
124+
['task list', '- [x] done\n- [ ] todo', 'taskList'],
125+
['blockquote', '> a quote', 'blockquote'],
126+
['fenced code block', '```ts\nconst x = 1\n```', 'codeBlock'],
127+
['standalone image', '![alt](https://e.com/i.png)', 'image'],
128+
['thematic break within a document', '# Title\n\n---\n\nbody', 'horizontalRule'],
129+
])('renders pasted %s as rich content', (_label, md, nodeType) => {
130+
editor = mount()
131+
expect(paste(editor, md)).toBe(true)
132+
expect(JSON.stringify(editor.getJSON())).toContain(`"type":"${nodeType}"`)
133+
})
134+
135+
it('parses markdown-shaped plain text even when an HTML sibling is present', () => {
136+
editor = mount()
137+
const html = '<h1>Title</h1><ul><li>a</li><li>b</li></ul>'
138+
expect(paste(editor, '# Title\n\n- a\n- b', html)).toBe(true)
139+
const json = JSON.stringify(editor.getJSON())
140+
expect(json).toContain('"type":"heading"')
141+
expect(json).toContain('"type":"bulletList"')
142+
expect(json).not.toContain('# Title')
143+
})
144+
145+
it('preserves the structural blocks of a multi-block document, in order, on paste', () => {
146+
editor = mount()
147+
expect(paste(editor, '# Title\n\nA paragraph.\n\n- a\n- b\n\n> quote')).toBe(true)
148+
const structural = (editor.getJSON().content ?? [])
149+
.map((node) => node.type)
150+
.filter((type) => type !== 'paragraph')
151+
expect(structural).toEqual(['heading', 'bulletList', 'blockquote'])
152+
})
96153
})

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,15 @@
380380
pointer-events: none;
381381
}
382382

383+
/*
384+
* prosemirror-tables' column-resizing plugin toggles the `resize-cursor` class on the editor root
385+
* while the pointer is over a column boundary; without this rule the handle shows but the cursor
386+
* never changes to the resize affordance.
387+
*/
388+
.rich-markdown-prose.resize-cursor {
389+
cursor: col-resize;
390+
}
391+
383392
.rich-markdown-prose p.is-editor-empty:first-child::before {
384393
content: attr(data-placeholder);
385394
color: var(--text-subtle);

0 commit comments

Comments
 (0)