Skip to content
This repository was archived by the owner on Feb 27, 2026. It is now read-only.
Draft
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
4 changes: 2 additions & 2 deletions apps/server/src/config/storage.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import { StorageConfig } from "../types/storage";
* Standard S3-compatible service domains that support virtual-hosted-style URLs.
* When using these services with forcePathStyle=false, the AWS SDK should
* construct the proper URL from the region, so we should NOT set an explicit endpoint.
*
*
* Criteria for adding domains to this list:
* - The service must be a well-known, publicly accessible S3-compatible service
* - The service must support AWS SDK's automatic URL construction from region
* - The service must use standard S3 virtual-hosted-style URL format
*
*
* Examples of services that should NOT be in this list:
* - Self-hosted MinIO, Garage, or other S3-compatible servers
* - Services that require custom endpoint configuration
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/modules/auth-providers/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export class AuthProvidersController {
// Handle multiple protocols in x-forwarded-proto (e.g., "https, https" from multiple proxies)
const forwardedProto = request.headers["x-forwarded-proto"] as string;
const protocol = forwardedProto ? forwardedProto.split(",")[0].trim() : request.protocol;

return {
protocol,
host: (request.headers["x-forwarded-host"] as string) || (request.headers.host as string),
Expand Down
7 changes: 7 additions & 0 deletions apps/web/messages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,13 @@
"viewMode": {
"table": "Table",
"grid": "Grid"
},
"sort": {
"name": "Name",
"date": "Date",
"size": "Size",
"ascending": "Sort Ascending",
"descending": "Sort Descending"
}
},
"filesTable": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,14 @@ export function usePublicShare() {

const shareFolderIds = new Set(allFolders.map((f) => f.id));

// Find root folders in the shared context
// Root is either folders with no parent OR folders whose parent is not in the share
const folders = allFolders.filter((folder: any) => {
if (folderId === null) {
// Show folders at the root of the share (no parent or parent not in share)
return !folder.parentId || !shareFolderIds.has(folder.parentId);
} else {
// Show folders that are direct children of the current folder
return folder.parentId === folderId;
}
});
Expand Down Expand Up @@ -542,7 +546,8 @@ export function usePublicShare() {
setCurrentFolderId(resolvedFolderId);
loadFolderContents(resolvedFolderId);
}
}, [share, loadFolderContents, urlFolderSlug, getFolderIdFromPathSlug]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [share, urlFolderSlug]);

return {
// Original functionality
Expand Down
41 changes: 39 additions & 2 deletions apps/web/src/app/files/components/files-view-manager.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { useEffect, useState } from "react";
import { IconLayoutGrid, IconSearch, IconTable } from "@tabler/icons-react";
import { IconLayoutGrid, IconSearch, IconSortAscending, IconSortDescending, IconTable } from "@tabler/icons-react";
import { useTranslations } from "next-intl";

import { FilesGridSkeleton, FilesTableSkeleton } from "@/components/skeletons";
import { FilesGrid } from "@/components/tables/files-grid";
import { FilesTable } from "@/components/tables/files-table";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";

interface File {
id: string;
Expand Down Expand Up @@ -71,6 +72,10 @@ interface FilesViewManagerProps {
setClearSelectionCallback?: (callback: () => void) => void;
onUpdateFolderName?: (folderId: string, newName: string) => void;
onUpdateFolderDescription?: (folderId: string, newDescription: string) => void;
sortBy?: "name" | "date" | "size";
sortOrder?: "asc" | "desc";
onSortByChange?: (sortBy: "name" | "date" | "size") => void;
onSortOrderChange?: (sortOrder: "asc" | "desc") => void;
}

export type ViewMode = "table" | "grid";
Expand Down Expand Up @@ -111,6 +116,10 @@ export function FilesViewManager({
setClearSelectionCallback,
onUpdateFolderName,
onUpdateFolderDescription,
sortBy = "name",
sortOrder = "asc",
onSortByChange,
onSortOrderChange,
}: FilesViewManagerProps) {
const t = useTranslations();
const [viewMode, setViewMode] = useState<ViewMode>(() => {
Expand Down Expand Up @@ -173,7 +182,7 @@ export function FilesViewManager({
<div className="flex items-center justify-between">
<div className="flex-1 min-w-0">{breadcrumbs}</div>

<div className="flex items-center gap-4">
<div className="flex items-center gap-3">
<div className="relative">
<IconSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
Expand All @@ -185,6 +194,34 @@ export function FilesViewManager({
/>
</div>

{/* Sort Controls */}
<div className="flex items-center gap-2">
<Select value={sortBy} onValueChange={onSortByChange}>
<SelectTrigger size="sm" className="w-[130px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="name">{t("files.sort.name")}</SelectItem>
<SelectItem value="date">{t("files.sort.date")}</SelectItem>
<SelectItem value="size">{t("files.sort.size")}</SelectItem>
</SelectContent>
</Select>

<Button
variant="outline"
size="sm"
className="h-8 px-3"
onClick={() => onSortOrderChange?.(sortOrder === "asc" ? "desc" : "asc")}
title={sortOrder === "asc" ? t("files.sort.ascending") : t("files.sort.descending")}
>
{sortOrder === "asc" ? (
<IconSortAscending className="h-4 w-4" />
) : (
<IconSortDescending className="h-4 w-4" />
)}
</Button>
</div>

<div className="flex items-center border rounded-lg p-1">
<Button
variant={viewMode === "table" ? "default" : "ghost"}
Expand Down
76 changes: 51 additions & 25 deletions apps/web/src/app/files/hooks/use-file-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ export function useFileBrowser() {
const [forceUpdate] = useState(0);
const isNavigatingRef = useRef(false);
const loadFilesRef = useRef<(() => Promise<void>) | null>(null);
const [sortBy, setSortBy] = useState<"name" | "date" | "size">("name");
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc");

const urlFolderSlug = searchParams.get("folder") || null;
const [currentFolderId, setCurrentFolderId] = useState<string | null>(null);
Expand Down Expand Up @@ -127,18 +129,44 @@ export function useFileBrowser() {
return pathParts.join(" / ");
}, []);

const sortItems = useCallback(
<T extends { name: string; createdAt: string; size?: number | string }>(items: T[]): T[] => {
return [...items].sort((a, b) => {
let comparison = 0;

switch (sortBy) {
case "name":
comparison = a.name.toLowerCase().localeCompare(b.name.toLowerCase());
break;
case "date":
comparison = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
break;
case "size": {
// Handle both number and string sizes (for files and folders)
// Files have numeric size, folders have string totalSize
// Folders are treated as size 0 for sorting (they don't have a meaningful comparable size)
const aSize = typeof a.size === "number" ? a.size : 0;
const bSize = typeof b.size === "number" ? b.size : 0;
comparison = aSize - bSize;
break;
}
default:
comparison = 0;
}

return sortOrder === "asc" ? comparison : -comparison;
});
},
[sortBy, sortOrder]
);

const navigateToFolderDirect = useCallback(
(targetFolderId: string | null) => {
const currentFiles = allFiles.filter((file: any) => (file.folderId || null) === targetFolderId);
const currentFolders = allFolders.filter((folder: any) => (folder.parentId || null) === targetFolderId);

const sortedFiles = [...currentFiles].sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);

const sortedFolders = [...currentFolders].sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
const sortedFiles = sortItems(currentFiles);
const sortedFolders = sortItems(currentFolders);

setFiles(sortedFiles);
setFolders(sortedFolders);
Expand All @@ -163,14 +191,14 @@ export function useFileBrowser() {
}
window.history.pushState({}, "", `/files?${params.toString()}`);
},
[allFiles, allFolders, buildBreadcrumbPath, searchParams, getFolderPathSlugFromId]
[allFiles, allFolders, buildBreadcrumbPath, searchParams, getFolderPathSlugFromId, sortItems]
);

const navigateToFolder = useCallback(
(folderId?: string) => {
const targetFolderId = folderId || null;

if (dataLoaded && allFiles.length > 0) {
if (dataLoaded) {
isNavigatingRef.current = true;
navigateToFolderDirect(targetFolderId);
// Refresh data when navigating to ensure we have latest state
Expand All @@ -195,7 +223,7 @@ export function useFileBrowser() {
router.push(`/files?${params.toString()}`);
}
},
[dataLoaded, allFiles.length, navigateToFolderDirect, searchParams, router, getFolderPathSlugFromId, allFolders]
[dataLoaded, navigateToFolderDirect, searchParams, router, getFolderPathSlugFromId, allFolders]
);

const navigateToRoot = useCallback(() => {
Expand All @@ -221,13 +249,8 @@ export function useFileBrowser() {
const currentFiles = fetchedFiles.filter((file: any) => (file.folderId || null) === resolvedFolderId);
const currentFolders = fetchedFolders.filter((folder: any) => (folder.parentId || null) === resolvedFolderId);

const sortedFiles = [...currentFiles].sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);

const sortedFolders = [...currentFolders].sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
const sortedFiles = sortItems(currentFiles);
const sortedFolders = sortItems(currentFolders);

setFiles(sortedFiles);
setFolders(sortedFolders);
Expand All @@ -243,7 +266,7 @@ export function useFileBrowser() {
} finally {
setIsLoading(false);
}
}, [urlFolderSlug, buildBreadcrumbPath, t, getFolderIdFromPathSlug]);
}, [urlFolderSlug, buildBreadcrumbPath, t, getFolderIdFromPathSlug, sortItems]);

const handleImmediateUpdate = useCallback(
(itemId: string, itemType: "file" | "folder", newParentId: string | null) => {
Expand Down Expand Up @@ -335,21 +358,19 @@ export function useFileBrowser() {
}
});

return allFolders
.filter((folder: any) => matchingItems.has(folder.id))
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
return allFolders.filter((folder: any) => matchingItems.has(folder.id));
}, [searchQuery, allFiles, allFolders, currentFolderId]);

const filteredFiles = searchQuery
? allFiles
.filter(
? sortItems(
allFiles.filter(
(file: any) =>
file.name.toLowerCase().includes(searchQuery.toLowerCase()) && (file.folderId || null) === currentFolderId
)
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
)
: files;

const filteredFolders = searchQuery ? getImmediateChildFoldersWithMatches() : folders;
const filteredFolders = searchQuery ? sortItems(getImmediateChildFoldersWithMatches()) : folders;

// Update loadFilesRef whenever loadFiles changes
useEffect(() => {
Expand Down Expand Up @@ -395,6 +416,11 @@ export function useFileBrowser() {
allFiles,
allFolders,
buildFolderPath,

sortBy,
sortOrder,
setSortBy,
setSortOrder,
};
}

Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/app/files/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ export default function FilesPage() {
modals,
allFiles,
allFolders,
sortBy,
sortOrder,
setSortBy,
setSortOrder,
} = useFileBrowser();

const handleMoveFile = (file: any) => {
Expand Down Expand Up @@ -208,6 +212,10 @@ export default function FilesPage() {
isLoading={isLoading}
onCreateFolder={() => fileManager.setCreateFolderModalOpen(true)}
onUpload={modals.onOpenUploadModal}
sortBy={sortBy}
sortOrder={sortOrder}
onSortByChange={setSortBy}
onSortOrderChange={setSortOrder}
breadcrumbs={
<Breadcrumb>
<BreadcrumbList>
Expand Down
Loading