diff --git a/server/src/data/repository/knowledge-repo.ts b/server/src/data/repository/knowledge-repo.ts index afa0f320..beb69561 100644 --- a/server/src/data/repository/knowledge-repo.ts +++ b/server/src/data/repository/knowledge-repo.ts @@ -45,6 +45,30 @@ export class KnowledgeRepository { .orderBy(asc(knowledgeFolders.title)); } + /** + * Get a folder by id plus all its ancestors up to the root. + * Returns an array starting from the given folder up to the root. + */ + async getFolderWithAncestors(folderId: string) { + const chain: (typeof knowledgeFolders.$inferSelect)[] = []; + let cursor: string | null = folderId; + const seen = new Set(); + + while (cursor && !seen.has(cursor)) { + seen.add(cursor); + const [folder] = await db + .select() + .from(knowledgeFolders) + .where(eq(knowledgeFolders.folderId, cursor)) + .limit(1); + if (!folder) break; + chain.push(folder); + cursor = folder.parentFolderId; + } + + return chain; + } + /** * Get child folders for a parent folder. */ diff --git a/server/src/routers/knowledge.ts b/server/src/routers/knowledge.ts index 2c72e191..eb157fb0 100644 --- a/server/src/routers/knowledge.ts +++ b/server/src/routers/knowledge.ts @@ -10,6 +10,7 @@ import { createItemAttachmentInputSchema, createItemInputSchema, deleteItemAttachmentInputSchema, + getFolderAncestorsInputSchema, getFoldersInFolderInputSchema, getItemAttachmentInputSchema, getItemInputSchema, @@ -45,6 +46,23 @@ const getRootFolders = protectedProcedure }), ); +const getFolderAncestors = protectedProcedure + .input(getFolderAncestorsInputSchema) + .output(z.array(knowledgeFolderOutputSchema)) + .meta({ + openapi: { + method: "POST", + path: "/knowledge.getFolderAncestors", + summary: "Get a folder and all its ancestors up to the root", + tags: ["Knowledge"], + }, + }) + .query(({ input }) => + withErrorHandling("getFolderAncestors", async () => { + return await knowledgeService.getFolderWithAncestors(input.folderId); + }), + ); + const getFoldersInFolder = protectedProcedure .input(getFoldersInFolderInputSchema) .output(z.array(knowledgeFolderOutputSchema)) @@ -262,6 +280,7 @@ const deleteItemAttachment = protectedProcedure export const knowledgeRouter = router({ getRootFolders, + getFolderAncestors, getFoldersInFolder, getItemsInFolder, getItem, diff --git a/server/src/service/knowledge-service.ts b/server/src/service/knowledge-service.ts index ca4d7a0e..66bb1cc7 100644 --- a/server/src/service/knowledge-service.ts +++ b/server/src/service/knowledge-service.ts @@ -37,6 +37,10 @@ export class KnowledgeService { return await this.knowledgeRepo.getChildFolders(parentFolderId); } + async getFolderWithAncestors(folderId: string) { + return await this.knowledgeRepo.getFolderWithAncestors(folderId); + } + async getItemsByFolder(folderId: string) { return await this.knowledgeRepo.getItemsByFolder(folderId); } diff --git a/server/src/types/knowledge-types.ts b/server/src/types/knowledge-types.ts index 6dd2b858..53f2792c 100644 --- a/server/src/types/knowledge-types.ts +++ b/server/src/types/knowledge-types.ts @@ -36,6 +36,10 @@ export const knowledgeAttachmentWithFileOutputSchema = metadata: z.unknown().nullable(), }); +export const getFolderAncestorsInputSchema = z.object({ + folderId: uuidSchema, +}); + export const getFoldersInFolderInputSchema = z.object({ parentFolderId: uuidSchema, }); diff --git a/web/src/app/knowledge/page.tsx b/web/src/app/knowledge/page.tsx index 1f74ac80..7311d28b 100644 --- a/web/src/app/knowledge/page.tsx +++ b/web/src/app/knowledge/page.tsx @@ -117,6 +117,15 @@ function KnowledgePage() { }) as Promise, }); + const folderAncestorsQuery = useQuery({ + queryKey: ["knowledge", "folders", "ancestors", currentFolderId], + enabled: Boolean(currentFolderId), + queryFn: () => + trpcClient.knowledge.getFolderAncestors.query({ + folderId: currentFolderId ?? "", + }) as Promise, + }); + const openedItemQuery = useQuery({ queryKey: ["knowledge", "item", openedItemId], enabled: Boolean(openedItemId), @@ -170,6 +179,10 @@ function KnowledgePage() { registerFolders((childFoldersQuery.data ?? []) as FolderRecord[]); }, [childFoldersQuery.data, registerFolders]); + useEffect(() => { + registerFolders((folderAncestorsQuery.data ?? []) as FolderRecord[]); + }, [folderAncestorsQuery.data, registerFolders]); + const folders = currentFolderId ? ((childFoldersQuery.data ?? []) as FolderRecord[]) : ((rootFoldersQuery.data ?? []) as FolderRecord[]); @@ -552,10 +565,9 @@ function KnowledgePage() {
-
+
Name Date Modified - Size
{loading ? ( @@ -579,7 +591,7 @@ function KnowledgePage() { key={`${row.kind}-${row.id}`} type="button" className={cn( - "grid w-full grid-cols-[minmax(0,1fr)_16rem_6rem] cursor-pointer items-center px-4 py-2 text-left", + "grid w-full grid-cols-[minmax(0,1fr)_16rem] cursor-pointer items-center px-4 py-2 text-left", isSelected ? "border-l-2 border-primary bg-primary/20" : "border-l-2 border-transparent hover:bg-primary/5", @@ -614,9 +626,6 @@ function KnowledgePage() { {formatDate(row.updatedAt)} - - — - ); })