Skip to content

Commit 8ccc309

Browse files
committed
fix(rich-editor): mention icon fallback + typed sim-link input rule
- mentionIcon never returns undefined: an empty/unrecognized kind (schema default '', or a future kind on a sim: link) falls back to a generic icon instead of crashing the chip's render. Adds tests. - Add a mention input rule so typing `[label](sim:kind/id)` becomes a chip on the closing paren — matching the paste/load path (the tokenizer), which previously left typed syntax as literal text. A plain InputRule (full-range replace) is used; nodeInputRule would keep the surrounding brackets.
1 parent d29e5e7 commit 8ccc309

3 files changed

Lines changed: 49 additions & 4 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/** @vitest-environment node */
2+
import { Box, File } from 'lucide-react'
3+
import { describe, expect, it } from 'vitest'
4+
import { mentionIcon } from './mention-icon'
5+
import type { MentionKind } from './types'
6+
7+
describe('mentionIcon', () => {
8+
it('returns the category icon for a known kind', () => {
9+
expect(mentionIcon('file', 'x')).toBe(File)
10+
})
11+
12+
it('falls back to a generic icon for an empty or unrecognized kind (never undefined)', () => {
13+
// The schema default is '' and a sim: link could carry a future kind — neither may crash render.
14+
expect(mentionIcon('' as unknown as MentionKind, 'x')).toBe(Box)
15+
expect(mentionIcon('dataset' as unknown as MentionKind, 'x')).toBe(Box)
16+
})
17+
})

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ const KIND_ICONS: Record<Exclude<MentionKind, 'integration'>, MentionIcon> = {
1717

1818
/**
1919
* Resolves the icon for a mention. Integrations use their brand icon from the block registry (keyed by
20-
* blockType, which is the mention `id`), falling back to a generic icon if the block was since removed
21-
* so the chip is never icon-less; every other kind uses a lucide category icon. Shared by the menu
20+
* blockType, which is the mention `id`), falling back to a generic icon if the block was since removed;
21+
* every other kind uses a lucide category icon, falling back to the same generic icon for an empty or
22+
* unrecognized kind (the schema default is `''`, and a `sim:` link could carry a kind a future version
23+
* adds) — so the result is always a real component and the chip is never icon-less. Shared by the menu
2224
* rows and the inserted chip so both render the same icon.
2325
*/
2426
export function mentionIcon(kind: MentionKind, id: string): MentionIcon {
2527
if (kind === 'integration') return (getBlock(id)?.icon as MentionIcon | undefined) ?? Box
26-
return KIND_ICONS[kind]
28+
return KIND_ICONS[kind] ?? Box
2729
}

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { JSONContent, MarkdownToken } from '@tiptap/core'
2-
import { Node } from '@tiptap/core'
2+
import { InputRule, Node } from '@tiptap/core'
33
import { toSimHref } from './sim-link'
44
import type { MentionKind } from './types'
55

@@ -103,4 +103,30 @@ export const MarkdownMention = Node.create({
103103
const { kind, id, label } = node.attrs as MentionAttrs
104104
return `[${escapeLabel(label)}](${toSimHref(kind, id)})`
105105
},
106+
107+
/**
108+
* Typing the portable `[label](sim:<kind>/<id>)` syntax inline turns it into a chip on the closing
109+
* paren — so live typing matches the paste/load path (which converts it via the tokenizer above). The
110+
* rule lives on this node, which sits before {@link MarkdownLinkInputRule} in the extension list, so it
111+
* claims the `sim:` form first; every other `[text](url)` falls through to the link rule untouched.
112+
*/
113+
addInputRules() {
114+
const type = this.type
115+
return [
116+
new InputRule({
117+
find: /\[((?:\\.|[^\]\\])+)\]\(sim:([a-z_]+)\/([^)\s]+)\)$/,
118+
handler: ({ state, range, match }) => {
119+
const [, rawLabel, kind, id] = match
120+
if (!kind || !id) return null
121+
// Replace the whole `[label](sim:…)` match with the chip (nodeInputRule would keep the
122+
// surrounding brackets, as it only swaps the first capture group).
123+
state.tr.replaceWith(
124+
range.from,
125+
range.to,
126+
type.create({ kind, id, label: unescapeLabel(rawLabel ?? '') })
127+
)
128+
},
129+
}),
130+
]
131+
},
106132
})

0 commit comments

Comments
 (0)