Add shared file storage with folder tree, permissions, and file manager UI

New document hub for centralized file storage — replaces scattered
drives and USB sticks for non-technical SMBs. Three new tables:
storage_folder (nested hierarchy), storage_folder_permission (role
and user-level access control), storage_file.

Backend: folder CRUD with nested paths, file upload/download via
signed URLs, permission checks (view/edit/admin with inheritance
from parent folders), public/private toggle, breadcrumb navigation,
file search.

Frontend: two-panel file manager — collapsible folder tree on left,
icon grid view on right. Folder icons by type, file size display,
upload button, context menu for download/delete. Breadcrumb nav.
Files sidebar link added.
This commit is contained in:
Ryan Moon
2026-03-29 15:31:20 -05:00
parent d36c6f7135
commit 0f6cc104d2
13 changed files with 1093 additions and 1 deletions

View File

@@ -0,0 +1,73 @@
import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client'
import type { StorageFolder, StorageFolderPermission, StorageFile } from '@/types/storage'
import type { PaginatedResponse, PaginationInput } from '@forte/shared/schemas'
// --- Folders ---
export const storageFolderKeys = {
all: ['storage-folders'] as const,
tree: ['storage-folders', 'tree'] as const,
children: (parentId: string | null) => ['storage-folders', 'children', parentId] as const,
detail: (id: string) => ['storage-folders', 'detail', id] as const,
permissions: (id: string) => ['storage-folders', id, 'permissions'] as const,
}
export function storageFolderTreeOptions() {
return queryOptions({
queryKey: storageFolderKeys.tree,
queryFn: () => api.get<{ data: StorageFolder[] }>('/v1/storage/folders/tree'),
})
}
export function storageFolderChildrenOptions(parentId: string | null) {
return queryOptions({
queryKey: storageFolderKeys.children(parentId),
queryFn: () => api.get<{ data: StorageFolder[] }>('/v1/storage/folders', parentId ? { parentId } : {}),
})
}
export function storageFolderDetailOptions(id: string) {
return queryOptions({
queryKey: storageFolderKeys.detail(id),
queryFn: () => api.get<StorageFolder & { breadcrumbs: { id: string; name: string }[] }>(`/v1/storage/folders/${id}`),
enabled: !!id,
})
}
export function storageFolderPermissionsOptions(id: string) {
return queryOptions({
queryKey: storageFolderKeys.permissions(id),
queryFn: () => api.get<{ data: StorageFolderPermission[] }>(`/v1/storage/folders/${id}/permissions`),
enabled: !!id,
})
}
export const storageFolderMutations = {
create: (data: Record<string, unknown>) => api.post<StorageFolder>('/v1/storage/folders', data),
update: (id: string, data: Record<string, unknown>) => api.patch<StorageFolder>(`/v1/storage/folders/${id}`, data),
delete: (id: string) => api.del<StorageFolder>(`/v1/storage/folders/${id}`),
addPermission: (folderId: string, data: Record<string, unknown>) => api.post<StorageFolderPermission>(`/v1/storage/folders/${folderId}/permissions`, data),
removePermission: (permId: string) => api.del<StorageFolderPermission>(`/v1/storage/folder-permissions/${permId}`),
}
// --- Files ---
export const storageFileKeys = {
all: (folderId: string) => ['storage-files', folderId] as const,
list: (folderId: string, params: PaginationInput) => ['storage-files', folderId, params] as const,
search: (q: string) => ['storage-files', 'search', q] as const,
}
export function storageFileListOptions(folderId: string, params: PaginationInput) {
return queryOptions({
queryKey: storageFileKeys.list(folderId, params),
queryFn: () => api.get<PaginatedResponse<StorageFile>>(`/v1/storage/folders/${folderId}/files`, params),
enabled: !!folderId,
})
}
export const storageFileMutations = {
delete: (id: string) => api.del<StorageFile>(`/v1/storage/files/${id}`),
getSignedUrl: (id: string) => api.get<{ url: string }>(`/v1/storage/files/${id}/signed-url`),
}