diff --git a/apps/docs/content/docs/en/files/editor.mdx b/apps/docs/content/docs/en/files/editor.mdx new file mode 100644 index 00000000000..672797fd98a --- /dev/null +++ b/apps/docs/content/docs/en/files/editor.mdx @@ -0,0 +1,56 @@ +--- +title: Editor +description: A rich markdown editor for your files — type markdown and watch it render, or edit visually. +pageType: concept +--- + +import { Image } from '@/components/ui/image' +import { Card, Cards } from 'fumadocs-ui/components/card' +import { Callout } from 'fumadocs-ui/components/callout' + +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. + +
+ A markdown file rendered in the editor: headings, bold, italic, and a link, a nested bullet list, and a table +
+ +## Formatting text + +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. + +## Structure + +Headings, blockquotes, and dividers keep long documents scannable. Type `# ` through `###### ` for headings, `> ` for a quote, and `---` for a divider. + +## Lists and checklists + +Bullet, ordered, and nested lists all work, plus task lists you can tick right in the document. + +## Tables + +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. + +## Code blocks + +Fenced code blocks are syntax-highlighted, with a language picker in the corner. Pick `mermaid` to render a live diagram instead of code. + +## Images + +Paste or drag an image straight into the document, then drag a corner to resize it. + +## Slash menu and shortcuts + +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. + +## Markdown fidelity + +The editor round-trips your markdown exactly — it saves what you wrote, with no reformatting churn. + + +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. + + + + + + diff --git a/apps/docs/content/docs/en/meta.json b/apps/docs/content/docs/en/meta.json index 5d11bb02c2a..69ddd2d5679 100644 --- a/apps/docs/content/docs/en/meta.json +++ b/apps/docs/content/docs/en/meta.json @@ -27,6 +27,7 @@ "./tables/workflow-columns", "---Files---", "./files/index", + "./files/editor", "./files/using-in-workflows", "./files/generating", "./files/passing-files", diff --git a/apps/docs/public/static/files/editor/overview.png b/apps/docs/public/static/files/editor/overview.png new file mode 100644 index 00000000000..0f736b2eeff Binary files /dev/null and b/apps/docs/public/static/files/editor/overview.png differ diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-paste.test.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-paste.test.ts index 3311dd5ea52..789c2d8a60c 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-paste.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-paste.test.ts @@ -16,8 +16,8 @@ afterEach(() => { editor = null }) -function mount(): Editor { - return new Editor({ extensions: [...createMarkdownContentExtensions(), MarkdownPaste] }) +function mount(editable = true): Editor { + return new Editor({ extensions: [...createMarkdownContentExtensions(), MarkdownPaste], editable }) } /** Run the plugin paste handlers the way ProseMirror would, with a mocked clipboard. */ @@ -93,4 +93,61 @@ describe('markdown paste', () => { expect(editor.isActive('codeBlock')).toBe(true) expect(paste(editor, '[link](https://example.com)')).toBe(false) }) + + it('rejects the paste entirely in a read-only editor', () => { + editor = mount(false) + expect(paste(editor, '# heading\n\n- one\n- two')).toBe(false) + expect(editor.getText()).toBe('') + }) + + it.each([ + ['empty string', ''], + ['whitespace only', ' \n\n '], + ['a bare thematic break (ambiguous — needs another markdown signal)', '---'], + ['inline-only italic (single asterisk would false-positive on e.g. *args)', 'an *italic* word'], + ['inline-only strikethrough', 'a ~~struck~~ word'], + ['inline-only code', 'some `code` here'], + ])('leaves %s to the default handler', (_label, text) => { + editor = mount() + expect(paste(editor, text)).toBe(false) + }) + + // Only structural / unambiguous constructs gate the markdown parse. Inline-only marks that + // `looksLikeMarkdown` deliberately omits to avoid false positives — single-asterisk italic + // (`*args`), `~~`, single-backtick code — are covered by the Markdown extension's own paste path, + // not MarkdownPaste, so they belong to a different test surface. + it.each([ + ['heading', '# Heading', 'heading'], + ['bold', 'a **bold** word', 'bold'], + ['bullet list', '- one\n- two', 'bulletList'], + ['ordered list', '1. one\n2. two', 'orderedList'], + ['task list', '- [x] done\n- [ ] todo', 'taskList'], + ['blockquote', '> a quote', 'blockquote'], + ['fenced code block', '```ts\nconst x = 1\n```', 'codeBlock'], + ['standalone image', '![alt](https://e.com/i.png)', 'image'], + ['thematic break within a document', '# Title\n\n---\n\nbody', 'horizontalRule'], + ])('renders pasted %s as rich content', (_label, md, nodeType) => { + editor = mount() + expect(paste(editor, md)).toBe(true) + expect(JSON.stringify(editor.getJSON())).toContain(`"type":"${nodeType}"`) + }) + + it('parses markdown-shaped plain text even when an HTML sibling is present', () => { + editor = mount() + const html = '

Title

' + expect(paste(editor, '# Title\n\n- a\n- b', html)).toBe(true) + const json = JSON.stringify(editor.getJSON()) + expect(json).toContain('"type":"heading"') + expect(json).toContain('"type":"bulletList"') + expect(json).not.toContain('# Title') + }) + + it('preserves the structural blocks of a multi-block document, in order, on paste', () => { + editor = mount() + expect(paste(editor, '# Title\n\nA paragraph.\n\n- a\n- b\n\n> quote')).toBe(true) + const structural = (editor.getJSON().content ?? []) + .map((node) => node.type) + .filter((type) => type !== 'paragraph') + expect(structural).toEqual(['heading', 'bulletList', 'blockquote']) + }) }) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css index 66a80d470f0..8c019c36f7c 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css @@ -380,6 +380,15 @@ pointer-events: none; } +/* + * prosemirror-tables' column-resizing plugin toggles the `resize-cursor` class on the editor root + * while the pointer is over a column boundary; without this rule the handle shows but the cursor + * never changes to the resize affordance. + */ +.rich-markdown-prose.resize-cursor { + cursor: col-resize; +} + .rich-markdown-prose p.is-editor-empty:first-child::before { content: attr(data-placeholder); color: var(--text-subtle);