Skip to content

Commit a7ada19

Browse files
committed
feat(rich-editor): Cmd/Ctrl-click a mention to its resource in the file viewer
Threads a `navigable` flag through the mention storage: the file viewer opts in so a chip routes to its file/table/workflow/etc., while modal fields stay inert so a click can't navigate away from an unsaved edit. Styling is identical either way.
1 parent f922f2d commit a7ada19

4 files changed

Lines changed: 46 additions & 9 deletions

File tree

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

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import type { MouseEvent } from 'react'
12
import type { JSONContent, MarkdownToken } from '@tiptap/core'
23
import { Node } from '@tiptap/core'
34
import type { ReactNodeViewProps } from '@tiptap/react'
45
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react'
6+
import { useParams, useRouter } from 'next/navigation'
7+
import { cn } from '@/lib/core/utils/cn'
58
import { getBareIconStyle, type StyleableIcon } from '@/blocks/icon-color'
69
import { mentionIcon } from './mention-icon'
7-
import { toSimHref } from './sim-link'
10+
import { simLinkPath, toSimHref } from './sim-link'
811
import type { MentionKind } from './types'
912

1013
interface MentionAttrs {
@@ -119,14 +122,35 @@ export const MarkdownMention = Node.create({
119122
const CHIP_CLASS =
120123
'mention-chip mx-px inline-flex items-center gap-1 align-middle text-[var(--text-primary)] leading-[1.5] [&>svg]:size-[12px] [&>svg]:shrink-0 [&>svg]:text-[var(--text-icon)]'
121124

122-
/** Live chip: a display-only entity icon + label, matching the chat input's mention rendering. */
123-
function MentionChipView({ node }: ReactNodeViewProps) {
125+
/**
126+
* Live chip: an entity icon + label matching the chat input's mention rendering. Where the host opted
127+
* into navigation (the file viewer), Cmd/Ctrl-click routes to the resource; in a modal field it stays
128+
* inert so a click can't navigate away from an unsaved edit.
129+
*/
130+
function MentionChipView({ node, editor }: ReactNodeViewProps) {
131+
const router = useRouter()
132+
const params = useParams()
124133
const { kind, id, label } = node.attrs as MentionAttrs
125134
const Icon = mentionIcon(kind, id) as StyleableIcon | undefined
126135
const iconStyle = Icon ? getBareIconStyle(Icon) : undefined
136+
const navigable = editor.storage.mention?.navigable === true
137+
138+
const handleClick = (event: MouseEvent) => {
139+
if (!(event.metaKey || event.ctrlKey)) return
140+
const workspaceId = typeof params.workspaceId === 'string' ? params.workspaceId : undefined
141+
const path = workspaceId && simLinkPath(workspaceId, kind, id)
142+
if (!path) return
143+
event.preventDefault()
144+
router.push(path)
145+
}
127146

128147
return (
129-
<NodeViewWrapper as='span' className={CHIP_CLASS} title={label}>
148+
<NodeViewWrapper
149+
as='span'
150+
className={cn(CHIP_CLASS, navigable && 'cursor-pointer')}
151+
onClick={navigable ? handleClick : undefined}
152+
title={label}
153+
>
130154
{Icon && <Icon style={iconStyle} />}
131155
<span>{label}</span>
132156
</NodeViewWrapper>

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ export const MENTION_PLUGIN_KEY = new PluginKey('mention')
1313
* Per-editor storage for the `@` mention extension. The host component populates {@link store} with
1414
* the current workspace mention data and may set {@link onOpen} to lazily start fetching that data the
1515
* first time the menu is triggered. {@link enabled} gates the menu off entirely (e.g. a field with no
16-
* workspace scope) so `@` stays literal text.
16+
* workspace scope) so `@` stays literal text. {@link navigable} lets a chip Cmd/Ctrl-click to its
17+
* resource — on for the file viewer, off inside a modal field so it can't route away from an edit.
1718
*/
1819
export interface MentionStorage {
1920
store: MentionStore
2021
onOpen: (() => void) | null
2122
enabled: boolean
23+
navigable: boolean
2224
}
2325

2426
declare module '@tiptap/core' {
@@ -39,7 +41,7 @@ export const Mention = Extension.create<Record<string, never>, MentionStorage>({
3941
name: 'mention',
4042

4143
addStorage() {
42-
return { store: createMentionStore(), onOpen: null, enabled: true }
44+
return { store: createMentionStore(), onOpen: null, enabled: true, navigable: false }
4345
},
4446

4547
addProseMirrorPlugins() {

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,35 @@ import { useEffect, useState } from 'react'
22
import type { Editor } from '@tiptap/react'
33
import { useMarkdownMentions } from './use-markdown-mentions'
44

5+
interface UseEditorMentionsOptions {
6+
/** Whether a chip can Cmd/Ctrl-click to its resource. On for the file viewer, off in modal fields. */
7+
navigable?: boolean
8+
}
9+
510
/**
611
* Wires an editor's `@` mention menu to its workspace data: gates the menu on a workspace scope,
712
* lazily fetches the data on the first open, and feeds it into the menu's reactive store. Shared by
813
* every editor surface that mounts the mention extension (the file editor and the modal field).
914
*/
10-
export function useEditorMentions(editor: Editor | null, workspaceId: string | undefined): void {
15+
export function useEditorMentions(
16+
editor: Editor | null,
17+
workspaceId: string | undefined,
18+
options?: UseEditorMentionsOptions
19+
): void {
1120
const [active, setActive] = useState(false)
1221
const items = useMarkdownMentions(workspaceId, { enabled: active })
22+
const navigable = options?.navigable ?? false
1323

1424
useEffect(() => {
1525
if (!editor) return
1626
const hasWorkspace = Boolean(workspaceId)
1727
editor.storage.mention.enabled = hasWorkspace
28+
editor.storage.mention.navigable = navigable
1829
editor.storage.mention.onOpen = hasWorkspace ? () => setActive(true) : null
1930
return () => {
2031
editor.storage.mention.onOpen = null
2132
}
22-
}, [editor, workspaceId])
33+
}, [editor, workspaceId, navigable])
2334

2435
useEffect(() => {
2536
editor?.storage.mention.store.set(items)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ export function LoadedRichMarkdownEditor({
333333
}
334334
}, [editor])
335335

336-
useEditorMentions(editor, workspaceId)
336+
useEditorMentions(editor, workspaceId, { navigable: true })
337337

338338
const wasStreamingRef = useRef(streamingAtMountRef.current)
339339

0 commit comments

Comments
 (0)