Skip to content

Commit c2f04e9

Browse files
committed
refactor(knowledge): sever the shell's route context, in place
Same ordering that made the table move safe: the behavioural edits land first, as a diff against files that have not moved. - `useParams` -> `knowledgeWorkspaceId(source)`. The shell took `workspaceId?` optionally and fell back to route params, which the page relied on — so the fallback was load-bearing, and it is what stopped this surface existing more than once per page. - `useRouter` -> `onNavigate`. Three pushes: the document detail route, the post-delete redirect, and the breadcrumb root. The latter two already went through `hrefFor({ to: 'list' })`; the first now goes through a new `knowledgeDocumentHref`, which lives in a per-kind module because a document is not a `ResourceKind` — it has no view, no grants and no share surface, so it cannot be spelled through `hrefFor({ to: 'resource' })`. - `useUserPermissionsContext` -> `grants`. 13 reads, including two of the form `canEdit || isLoading` that gate whether a breadcrumb dropdown renders at all. Those become `grants.write || !grants.settled` — the exact pattern `settled` was added for, and without it the caret would have popped in after load instead of rendering disabled. Three supporting relocations, all pure moves: - `knowledge/components/icons` deleted — byte-identical to `@/components/icons/document-icons`, confirmed with `diff`. - `use-knowledge-upload` -> `@/hooks/kb/`. - `useContextMenu` -> `components/anchored-context-menu/`, beside the component every one of its 13 consumers pairs it with. Also makes table-view's internal imports absolute. They arrived relative with the move and I did not rewrite them; log-view and knowledge-view have zero relative imports, so table-view was the outlier against the rule this PR documents. (file-view has 83 and is left alone — pre-existing, and not worth 83 files of churn here.)
1 parent f3ec1d4 commit c2f04e9

38 files changed

Lines changed: 181 additions & 382 deletions

File tree

apps/sim/app/workspace/[workspaceId]/files/files.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { getErrorMessage, toError } from '@sim/utils/errors'
2424
import { useParams, useRouter } from 'next/navigation'
2525
import { useQueryStates } from 'nuqs'
2626
import { usePostHog } from 'posthog-js/react'
27+
import { useContextMenu } from '@/components/anchored-context-menu'
2728
import { getDocumentIcon } from '@/components/icons/document-icons'
2829
import type { PreviewMode } from '@/components/resources/file-view'
2930
import {
@@ -106,7 +107,6 @@ import {
106107
filesUrlKeys,
107108
} from '@/app/workspace/[workspaceId]/files/search-params'
108109
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
109-
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
110110
import { usePinItem, usePinnedIds, useUnpinItem } from '@/hooks/queries/pinned-items'
111111
import { useWorkspaceMembersQuery, type WorkspaceMember } from '@/hooks/queries/workspace'
112112
import {

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/terminal-session/terminal-session.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { type IBufferRange, Terminal } from '@xterm/xterm'
3939
import { useTheme } from 'next-themes'
4040
import '@xterm/xterm/css/xterm.css'
4141
import type { TerminalTabState, TerminalTabsState } from '@sim/terminal-protocol'
42+
import { useContextMenu } from '@/components/anchored-context-menu'
4243
import { SIM_RESOURCE_DRAG_TYPE } from '@/lib/copilot/resource-types'
4344
import { TERMINAL_SESSION_RESOURCE_ID } from '@/lib/copilot/resources/types'
4445
import { getDesktopBridge } from '@/lib/desktop'
@@ -68,7 +69,6 @@ import {
6869
import { useMothershipResources } from '@/app/workspace/[workspaceId]/home/components/mothership-resources-context'
6970
import { TerminalContextMenu } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/terminal-session/terminal-context-menu'
7071
import { ContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu'
71-
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
7272
import { useDesktopPreferenceMutation } from '@/hooks/use-desktop-preference-mutation'
7373
import { useCopilotTerminalStore } from '@/stores/copilot-terminal/store'
7474
import type { ChatContext, TerminalTextSelection } from '@/stores/panel'

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,11 @@ export const ResourceContent = memo(function ResourceContent({
270270
error: logError,
271271
} = useLogDetail(resource.id, workspaceId, { enabled: isLogResource })
272272

273+
const knowledgeSource = useMemo(
274+
() => workspaceSource({ kind: 'knowledge' as const, workspaceId, resourceId: resource.id }),
275+
[workspaceId, resource.id]
276+
)
277+
273278
const tableSource = useMemo(
274279
() => workspaceSource({ kind: 'table' as const, workspaceId, resourceId: resource.id }),
275280
[workspaceId, resource.id]
@@ -380,7 +385,9 @@ export const ResourceContent = memo(function ResourceContent({
380385
key={resource.id}
381386
id={resource.id}
382387
knowledgeBaseName={resource.title}
383-
workspaceId={workspaceId}
388+
source={knowledgeSource}
389+
grants={grants}
390+
onNavigate={navigate}
384391
host='panel'
385392
/>
386393
)

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { createLogger } from '@sim/logger'
77
import { truncate } from '@sim/utils/string'
88
import { useParams, useRouter } from 'next/navigation'
99
import { useQueryStates } from 'nuqs'
10+
import { useContextMenu } from '@/components/anchored-context-menu'
11+
import { getDocumentIcon } from '@/components/icons/document-icons'
1012
import { SearchHighlight } from '@/components/resources/knowledge-view'
1113
import type { ChunkData } from '@/lib/knowledge/types'
1214
import { formatTokenCount } from '@/lib/tokenization'
@@ -34,9 +36,7 @@ import {
3436
documentUrlKeys,
3537
} from '@/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/search-params'
3638
import { ActionBar } from '@/app/workspace/[workspaceId]/knowledge/[id]/components'
37-
import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components'
3839
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
39-
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
4040
import { CONNECTOR_META_REGISTRY } from '@/connectors/registry'
4141
import { useDocument, useDocumentChunks, useKnowledgeBase } from '@/hooks/kb/use-knowledge'
4242
import {

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-documents-modal/add-documents-modal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { createLogger } from '@sim/logger'
1717
import { useParams } from 'next/navigation'
1818
import { formatFileSize, validateKnowledgeBaseFile } from '@/lib/uploads/utils/file-utils'
1919
import { ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
20-
import { useKnowledgeUpload } from '@/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload'
20+
import { useKnowledgeUpload } from '@/hooks/kb/use-knowledge-upload'
2121

2222
const logger = createLogger('AddDocumentsModal')
2323

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/base-tags-modal/base-tags-modal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ import {
1616
Trash,
1717
} from '@sim/emcn'
1818
import { createLogger } from '@sim/logger'
19+
import { getDocumentIcon } from '@/components/icons/document-icons'
1920
import type { TagUsageData } from '@/lib/api/contracts/knowledge'
2021
import { SUPPORTED_FIELD_TYPES, TAG_SLOT_CONFIG } from '@/lib/knowledge/constants'
21-
import { getDocumentIcon } from '@/app/workspace/[workspaceId]/knowledge/components'
2222
import {
2323
type TagDefinition,
2424
useKnowledgeBaseTagDefinitions,

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/knowledge-base.tsx

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

3-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
3+
import { useCallback, useEffect, useRef, useState } from 'react'
44
import {
55
ChipConfirmModal,
66
type ChipConfirmTextSegment,
@@ -11,8 +11,8 @@ import {
1111
} from '@sim/emcn'
1212
import { Database, Pencil, Plus, TagIcon } from '@sim/emcn/icons'
1313
import { createLogger } from '@sim/logger'
14-
import { useParams, useRouter } from 'next/navigation'
1514
import { usePostHog } from 'posthog-js/react'
15+
import { useContextMenu } from '@/components/anchored-context-menu'
1616
import { Resource } from '@/components/resource'
1717
import {
1818
type KnowledgeDocumentList,
@@ -35,8 +35,6 @@ import {
3535
RenameDocumentModal,
3636
} from '@/app/workspace/[workspaceId]/knowledge/[id]/components'
3737
import { useKnowledgeListState } from '@/app/workspace/[workspaceId]/knowledge/[id]/hooks/use-knowledge-list-state'
38-
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
39-
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
4038
import {
4139
useKnowledgeBase,
4240
useKnowledgeBaseDocuments,
@@ -53,7 +51,13 @@ import {
5351
} from '@/hooks/queries/kb/knowledge'
5452
import { useInlineRename } from '@/hooks/use-inline-rename'
5553
import { useOAuthReturnForKBConnectors } from '@/hooks/use-oauth-return'
56-
import { grantsFromPermissions, type ResourceHost, workspaceSource } from '@/resources'
54+
import {
55+
knowledgeDocumentHref,
56+
knowledgeWorkspaceId,
57+
type ResourceGrants,
58+
type ResourceHost,
59+
type ResourceSource,
60+
} from '@/resources'
5761

5862
const logger = createLogger('KnowledgeBase')
5963

@@ -62,7 +66,15 @@ const DOCUMENTS_PER_PAGE = 50
6266
interface KnowledgeBaseProps {
6367
id: string
6468
knowledgeBaseName?: string
65-
workspaceId?: string
69+
/**
70+
* Where the knowledge base comes from and by what address. Built by the host's
71+
* client shell; a source carries closures and cannot cross the RSC boundary.
72+
*/
73+
source: ResourceSource<'knowledge'>
74+
/** What this viewer may do. `write` is `canEdit`; `settled` says whether it is final. */
75+
grants: ResourceGrants
76+
/** The router half of `host`; targets come from `source.hrefFor`. */
77+
onNavigate?: (path: string) => void
6678
/**
6779
* Who owns the URL around this surface. The knowledge page owns it; the
6880
* mothership panel does not, and its document-list view-state stays local so
@@ -81,12 +93,12 @@ interface KnowledgeBaseProps {
8193
export function KnowledgeBase({
8294
id,
8395
knowledgeBaseName: passedKnowledgeBaseName,
84-
workspaceId: propWorkspaceId,
96+
source,
97+
grants,
98+
onNavigate,
8599
host,
86100
}: KnowledgeBaseProps) {
87-
const params = useParams()
88-
const workspaceId = propWorkspaceId || (params.workspaceId as string)
89-
const router = useRouter()
101+
const workspaceId = knowledgeWorkspaceId(source) ?? ''
90102
const posthog = usePostHog()
91103

92104
useEffect(() => {
@@ -98,7 +110,6 @@ export function KnowledgeBase({
98110

99111
useOAuthReturnForKBConnectors(id)
100112
const { removeKnowledgeBase } = useKnowledgeBasesList(workspaceId, { enabled: false })
101-
const userPermissions = useUserPermissionsContext()
102113

103114
const { mutate: updateDocumentMutation, mutateAsync: updateDocumentAsync } = useUpdateDocument()
104115
const { mutate: deleteDocumentMutation } = useDeleteDocument()
@@ -225,12 +236,6 @@ export function KnowledgeBase({
225236

226237
const totalPages = Math.ceil(pagination.total / pagination.limit)
227238

228-
const source = useMemo(
229-
() => workspaceSource({ kind: 'knowledge', workspaceId, resourceId: id }),
230-
[workspaceId, id]
231-
)
232-
const grants = useMemo(() => grantsFromPermissions(userPermissions), [userPermissions])
233-
234239
/**
235240
* Checks for documents with stale processing states and marks them as failed
236241
*/
@@ -408,7 +413,8 @@ export function KnowledgeBase({
408413
kbName: knowledgeBaseName,
409414
docName: document?.filename || 'Document',
410415
})
411-
router.push(`/workspace/${workspaceId}/knowledge/${id}/${docId}?${urlParams.toString()}`)
416+
const documentHref = knowledgeDocumentHref(source, docId)
417+
if (documentHref) onNavigate?.(`${documentHref}?${urlParams.toString()}`)
412418
}
413419

414420
/**
@@ -423,7 +429,7 @@ export function KnowledgeBase({
423429
onSuccess: () => {
424430
removeKnowledgeBase(id)
425431
const list = source.hrefFor({ to: 'list' })
426-
if (list) router.push(list)
432+
if (list) onNavigate?.(list)
427433
},
428434
}
429435
)
@@ -625,7 +631,7 @@ export function KnowledgeBase({
625631
icon: Database,
626632
onClick: () => {
627633
const list = source.hrefFor({ to: 'list' })
628-
if (list) router.push(list)
634+
if (list) onNavigate?.(list)
629635
},
630636
},
631637
{
@@ -642,24 +648,24 @@ export function KnowledgeBase({
642648
}
643649
: undefined,
644650
dropdownItems: [
645-
...(userPermissions.canEdit || userPermissions.isLoading
651+
...(grants.write || !grants.settled
646652
? [
647653
{
648654
label: 'Rename',
649655
icon: Pencil,
650-
disabled: !userPermissions.canEdit,
656+
disabled: !grants.write,
651657
onClick: () => kbRename.startRename(id, knowledgeBaseName),
652658
},
653659
{
654660
label: 'Tags',
655661
icon: TagIcon,
656-
disabled: !userPermissions.canEdit,
662+
disabled: !grants.write,
657663
onClick: () => setShowTagsModal(true),
658664
},
659665
{
660666
label: 'Delete',
661667
icon: Trash,
662-
disabled: !userPermissions.canEdit,
668+
disabled: !grants.write,
663669
onClick: () => setShowDeleteDialog(true),
664670
},
665671
]
@@ -669,12 +675,12 @@ export function KnowledgeBase({
669675
]
670676

671677
const headerActions: ResourceAction[] = [
672-
...(userPermissions.canEdit || userPermissions.isLoading
678+
...(grants.write || !grants.settled
673679
? [
674680
{
675681
text: 'New connector',
676682
icon: Plus,
677-
disabled: !userPermissions.canEdit,
683+
disabled: !grants.write,
678684
onSelect: () => setShowAddConnectorModal(true),
679685
},
680686
]
@@ -741,7 +747,7 @@ export function KnowledgeBase({
741747
text: 'New documents',
742748
icon: Plus,
743749
onSelect: handleAddDocuments,
744-
disabled: userPermissions.canEdit !== true,
750+
disabled: !grants.write,
745751
variant: 'primary',
746752
},
747753
]}
@@ -915,7 +921,7 @@ export function KnowledgeBase({
915921
knowledgeBaseId={id}
916922
connectors={connectors}
917923
isLoading={isLoadingConnectors}
918-
canEdit={userPermissions.canEdit}
924+
canEdit={grants.write}
919925
className='mt-0'
920926
/>
921927
</ChipModalBody>
@@ -964,7 +970,7 @@ export function KnowledgeBase({
964970
: undefined
965971
}
966972
onViewTags={
967-
contextMenuDocument && selectedDocuments.size === 1 && userPermissions.canEdit
973+
contextMenuDocument && selectedDocuments.size === 1 && grants.write
968974
? () => handleViewDocumentTags(contextMenuDocument)
969975
: undefined
970976
}
@@ -976,16 +982,14 @@ export function KnowledgeBase({
976982
: undefined
977983
}
978984
onAddDocument={handleAddDocuments}
979-
disableRename={!userPermissions.canEdit}
985+
disableRename={!grants.write}
980986
disableToggleEnabled={
981-
!userPermissions.canEdit ||
987+
!grants.write ||
982988
contextMenuDocument?.processingStatus === 'processing' ||
983989
contextMenuDocument?.processingStatus === 'pending'
984990
}
985-
disableDelete={
986-
!userPermissions.canEdit || contextMenuDocument?.processingStatus === 'processing'
987-
}
988-
disableAddDocument={!userPermissions.canEdit}
991+
disableDelete={!grants.write || contextMenuDocument?.processingStatus === 'processing'}
992+
disableAddDocument={!grants.write}
989993
/>
990994
</>
991995
)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use client'
2+
3+
import { useCallback, useMemo } from 'react'
4+
import { useParams, useRouter } from 'next/navigation'
5+
import { KnowledgeBase } from '@/app/workspace/[workspaceId]/knowledge/[id]/knowledge-base'
6+
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
7+
import { grantsFromPermissions, workspaceSource } from '@/resources'
8+
9+
interface KnowledgeRouteProps {
10+
id: string
11+
knowledgeBaseName?: string
12+
}
13+
14+
/**
15+
* The knowledge page's client shell: it resolves the axes the route can supply
16+
* and mounts the surface.
17+
*
18+
* Exists because `page.tsx` is a Server Component and both `grants` and a
19+
* `ResourceSource` are client-only — grants comes from a React context, and a
20+
* source carries closures that cannot cross the RSC boundary.
21+
*
22+
* Reading `useParams()` here is legitimate where it was not inside the surface
23+
* itself: a route shell exists exactly once per page by definition, whereas the
24+
* knowledge base is also mounted in the chat panel beside it. Mirrors
25+
* `tables/[tableId]/table-route.tsx` and `files/[fileId]/view/fullscreen-file-view.tsx`.
26+
*/
27+
export function KnowledgeRoute({ id, knowledgeBaseName }: KnowledgeRouteProps) {
28+
const params = useParams()
29+
const workspaceId = typeof params?.workspaceId === 'string' ? params.workspaceId : ''
30+
const permissions = useUserPermissionsContext()
31+
const router = useRouter()
32+
33+
const source = useMemo(
34+
() => workspaceSource({ kind: 'knowledge' as const, workspaceId, resourceId: id }),
35+
[workspaceId, id]
36+
)
37+
const grants = useMemo(() => grantsFromPermissions(permissions), [permissions])
38+
const navigate = useCallback((path: string) => router.push(path), [router])
39+
40+
return (
41+
<KnowledgeBase
42+
id={id}
43+
knowledgeBaseName={knowledgeBaseName}
44+
source={source}
45+
grants={grants}
46+
onNavigate={navigate}
47+
host='page'
48+
/>
49+
)
50+
}

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Suspense } from 'react'
22
import type { Metadata } from 'next'
3-
import { KnowledgeBase } from '@/app/workspace/[workspaceId]/knowledge/[id]/knowledge-base'
3+
import { KnowledgeRoute } from '@/app/workspace/[workspaceId]/knowledge/[id]/knowledge-route'
44

55
interface PageProps {
66
params: Promise<{
@@ -21,7 +21,7 @@ export default async function KnowledgeBasePage({ params, searchParams }: PagePr
2121

2222
return (
2323
<Suspense fallback={null}>
24-
<KnowledgeBase id={id} knowledgeBaseName={kbName || 'Knowledge Base'} host='page' />
24+
<KnowledgeRoute id={id} knowledgeBaseName={kbName || 'Knowledge Base'} />
2525
</Suspense>
2626
)
2727
}

apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import type { StrategyOptions } from '@/lib/chunkers/types'
2929
import { KNOWLEDGE_BASE_DESCRIPTION_MAX_LENGTH } from '@/lib/knowledge/constants'
3030
import { formatFileSize, validateKnowledgeBaseFile } from '@/lib/uploads/utils/file-utils'
3131
import { ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
32-
import { useKnowledgeUpload } from '@/app/workspace/[workspaceId]/knowledge/hooks/use-knowledge-upload'
32+
import { useKnowledgeUpload } from '@/hooks/kb/use-knowledge-upload'
3333
import { useCreateKnowledgeBase, useDeleteKnowledgeBase } from '@/hooks/queries/kb/knowledge'
3434

3535
const logger = createLogger('CreateBaseModal')

0 commit comments

Comments
 (0)