Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/src/components/FileUploadSkeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const FileUploadSkeleton: React.FC<FileUploadSkeletonProps> = ({
return (
<div className="w-full">
{/* Table header */}
<div className="grid grid-cols-12 gap-4 text-sm text-gray-500 dark:text-gray-400 pb-2 border-b border-gray-200 dark:border-gray-700">
<div className="grid grid-cols-12 gap-4 text-sm font-mono text-gray-500 dark:text-gray-400 pb-2 border-b border-gray-200 dark:border-gray-700">
<div className="col-span-5">FOLDER</div>
<div className="col-span-2"></div>
<div className="col-span-1 text-center">FILES</div>
Expand Down
198 changes: 118 additions & 80 deletions frontend/src/routes/_authenticated/knowledgeManagement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
ArrowLeft,
PanelLeftClose,
PanelLeftOpen,
ChevronRight,
ChevronDown,
} from "lucide-react"
import { Sidebar } from "@/components/Sidebar"
import { useState, useCallback, useEffect, memo } from "react"
Expand Down Expand Up @@ -401,7 +403,9 @@ function RouteComponent() {

// Check if the collection exists and has files
try {
const response = await api.cl.$get()
const response = await api.cl.$get({
query: { includeItems: "true" }
})
if (response.ok) {
const data = await response.json()
const existingCollection = data.find(
Expand Down Expand Up @@ -459,7 +463,9 @@ function RouteComponent() {

const checkUploadProgress = async () => {
try {
const response = await api.cl.$get()
const response = await api.cl.$get({
query: { includeItems: "true" }
})
if (response.ok) {
const data = await response.json()
const existingCollection = data.find(
Expand All @@ -484,29 +490,36 @@ function RouteComponent() {
clearUploadState()

// Refresh collections to show the new one
const updatedCollections = data.map(
(collection: CollectionType) => ({
id: collection.id,
name: collection.name,
description: collection.description,
files: collection.totalCount || 0,
items: [],
isOpen: false,
lastUpdated: new Date(collection.updatedAt).toLocaleString(
"en-GB",
{
day: "numeric",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
},
),
updatedBy: collection.lastUpdatedByEmail || "Unknown",
totalCount: collection.totalCount,
isPrivate: collection.isPrivate,

const updatedCollections = data.map((collection: CollectionType & { items?: CollectionItem[] }) => ({
id: collection.id,
name: collection.name,
description: collection.description,
files: collection.totalItems || 0,
items: buildFileTree(
(collection.items || []).map((item: CollectionItem) => ({
name: item.name,
type: item.type as "file" | "folder",
totalFileCount: item.totalFileCount,
updatedAt: item.updatedAt,
id: item.id,
updatedBy:
item.lastUpdatedByEmail || user?.email || "Unknown",
})),
),
isOpen: (collection.items || []).length > 0, // Open if has items
lastUpdated: new Date(collection.updatedAt).toLocaleString("en-GB", {
day: "numeric",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}),
)
updatedBy: collection.lastUpdatedByEmail || "Unknown",
totalCount: collection.totalItems,
isPrivate: collection.isPrivate,
}))

setCollections(updatedCollections)

showToast(
Expand All @@ -529,20 +542,32 @@ function RouteComponent() {
useEffect(() => {
const fetchCollections = async () => {
try {
const response = await api.cl.$get()
const response = await api.cl.$get({
query: { includeItems: "true" }
})
if (response.ok) {
const data = await response.json()
setCollections(
data.map((collection: CollectionType) => ({
id: collection.id,
name: collection.name,
description: collection.description,
files: collection.totalItems || 0,
items: [],
isOpen: false,
lastUpdated: new Date(collection.updatedAt).toLocaleString(
"en-GB",
{

setCollections(
data.map((collection: CollectionType & { items?: CollectionItem[] }) => ({
id: collection.id,
name: collection.name,
description: collection.description,
files: collection.totalItems || 0,
items: buildFileTree(
(collection.items || []).map((item: CollectionItem) => ({
name: item.name,
type: item.type as "file" | "folder",
totalFileCount: item.totalFileCount,
updatedAt: item.updatedAt,
id: item.id,
updatedBy:
item.lastUpdatedByEmail || user?.email || "Unknown",
})),
),
isOpen: (collection.items || []).length > 0, // Open if has items
lastUpdated: new Date(collection.updatedAt).toLocaleString("en-GB", {

day: "numeric",
month: "short",
year: "numeric",
Expand All @@ -568,7 +593,7 @@ function RouteComponent() {
}

fetchCollections()
}, [showToast])
}, [showToast, user?.email])

const handleCloseModal = () => {
setShowNewCollection(false)
Expand Down Expand Up @@ -1266,7 +1291,7 @@ function RouteComponent() {
<div className="fixed inset-0 bg-black bg-opacity-50 z-40 flex">
<div className="bg-gray-100 flex flex-col border-r border-gray-200 w-[30%] max-w-[400px] min-w-[250px] dark:bg-[#1E1E1E] dark:border-gray-700 lg:w-[300px] lg:min-w-[250px] lg:max-w-[400px] h-64 lg:h-full">
{/* Collection Header */}
<div className="px-4 py-4 h-12 bg-gray-50 dark:bg-[#1E1E1E] border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<div className="px-4 py-4 h-12 bg-gray-50 dark:bg-[#1E1E1E] flex items-center justify-between sticky top-0 z-20">
<h2 className="text-sm font-bold font-mono text-gray-400 dark:text-gray-500 uppercase tracking-wider truncate">
{selectedDocument.collection.name}
</h2>
Expand Down Expand Up @@ -1429,10 +1454,10 @@ function RouteComponent() {
</div>
)}

{collections.map((collection, index) => (
<div key={index} className="mb-8">
{collections.map((collection) => (
<div key={collection.id} className="mb-8">
<div
className="flex justify-between items-center mb-4 cursor-pointer"
className="sticky mb-2 cursor-pointer top-0 bg-white dark:bg-[#1E1E1E] py-1"
onClick={async () => {
const updatedCollections = [...collections]
const coll = updatedCollections.find(
Expand Down Expand Up @@ -1465,60 +1490,73 @@ function RouteComponent() {
}
}}
>
<h2 className="text-xl font-semibold text-gray-800 dark:text-gray-200">
{collection.name}
</h2>
<div className="flex items-center gap-4">
<div className="absolute left-[-24px] top-1/2 transform -translate-y-1/2">
{collection.isOpen ? (
<ChevronDown size={16} className="text-gray-600 dark:text-gray-400" />
) : (
<ChevronRight size={16} className="text-gray-600 dark:text-gray-400" />
)}
</div>

{/* Collection header aligned with table grid */}
<div className="grid grid-cols-12 gap-4 items-center">
<div className="col-span-5">
<h2 className="text-lg font-semibold text-gray-800 dark:text-gray-200">
{collection.name}
</h2>
</div>
<div className="col-span-7 flex justify-end items-center gap-4">
<Plus
size={16}
className={`cursor-pointer text-gray-600 dark:text-gray-400 ${isUploading ? "opacity-50 cursor-not-allowed" : ""}`}
className={`cursor-pointer text-gray-600 dark:text-gray-400 ${isUploading ? "opacity-50 cursor-not-allowed" : ""}`}
onClick={(e) => {
e.stopPropagation()
!isUploading && handleOpenAddFilesModal(collection)
}}
/>
<DropdownMenu
open={openDropdown === collection.id}
onOpenChange={(open) => setOpenDropdown(open ? collection.id : null)}
>
<DropdownMenuTrigger asChild>
<MoreHorizontal
size={16}
className={`cursor-pointer text-gray-600 dark:text-gray-400 ${isUploading ? "opacity-50 cursor-not-allowed" : ""}`}
!isUploading && handleOpenAddFilesModal(collection)
}}
/>
<DropdownMenu
open={openDropdown === collection.id}
onOpenChange={(open) => setOpenDropdown(open ? collection.id : null)}
>
<DropdownMenuTrigger asChild>
<MoreHorizontal
size={16}
className={`cursor-pointer text-gray-600 dark:text-gray-400 ${isUploading ? "opacity-50 cursor-not-allowed" : ""}`}
onClick={(e) => e.stopPropagation()}
/>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation()
!isUploading && handleEditCollection(collection)
}}
disabled={isUploading}
>
<Edit className="mr-2 h-4 w-4" />
<span>Edit</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation()
if (!isUploading) {
setDeletingCollection(collection)
setOpenDropdown(null)
}
}}
disabled={isUploading}
>
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
!isUploading && handleEditCollection(collection)
}}
disabled={isUploading}
>
<Edit className="mr-2 h-4 w-4" />
<span>Edit</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation()
if (!isUploading) {
setDeletingCollection(collection)
setOpenDropdown(null)
}
}}
disabled={isUploading}
>
<Trash2 className="mr-2 h-4 w-4" />
<span>Delete</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
{collection.isOpen && (
<>
<div className="grid grid-cols-12 gap-4 text-sm text-gray-500 dark:text-gray-400 pb-2 border-b border-gray-200 dark:border-gray-700">
<div className="grid grid-cols-12 gap-4 text-sm font-mono text-gray-500 dark:text-gray-400 pb-2 border-b border-gray-200 dark:border-gray-700">
<div className="col-span-5">FOLDER</div>
<div className="col-span-2"></div>
<div className="col-span-1 text-center">FILES</div>
Expand Down
36 changes: 36 additions & 0 deletions server/api/knowledgeBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ export const CreateCollectionApi = async (c: Context) => {
export const ListCollectionsApi = async (c: Context) => {
const { sub: userEmail } = c.get(JwtPayloadKey)
const showOnlyOwn = c.req.query("ownOnly") === "true"
const includeItems = c.req.query("includeItems") === "true"

// Get user from database
const users = await getUserByEmail(db, userEmail)
Expand All @@ -243,6 +244,41 @@ export const ListCollectionsApi = async (c: Context) => {
const collections = showOnlyOwn
? await getCollectionsByOwner(db, user.id)
: await getAccessibleCollections(db, user.id)

// If includeItems is requested, fetch items for each collection
if (includeItems) {
const collectionsWithItems = await Promise.all(
collections.map(async (collection) => {
try {
// Check access: owner can always access, others only if Collection is public
if (collection.ownerId !== user.id && collection.isPrivate) {
return {
...collection,
items: [], // Return empty items array for inaccessible collections
}
}

const items = await getCollectionItemsByParent(db, collection.id, null)
return {
...collection,
items,
}
} catch (error) {
loggerWithChild({ email: userEmail }).warn(
error,
`Failed to fetch items for collection ${collection.id}: ${getErrorMessage(error)}`,
)
return {
...collection,
items: [], // Return empty items array on error
}
}
})
)

return c.json(collectionsWithItems)
}

return c.json(collections)
} catch (error) {
const errMsg = getErrorMessage(error)
Expand Down