diff --git a/packages/frontend-web/components/chat-widget.tsx b/packages/frontend-web/components/chat-widget.tsx deleted file mode 100644 index 225acb59..00000000 --- a/packages/frontend-web/components/chat-widget.tsx +++ /dev/null @@ -1,86 +0,0 @@ -"use client"; - -import { useEffect } from 'react'; - -declare global { - interface Window { - Tawk_API?: { - onLoad?: () => void; - onChatMaximized?: () => void; - onChatMinimized?: () => void; - onChatHidden?: () => void; - onChatStarted?: () => void; - onChatEnded?: () => void; - onPrechatSubmit?: (data: any) => void; - onOfflineSubmit?: (data: any) => void; - maximize?: () => void; - minimize?: () => void; - toggle?: () => void; - popup?: () => void; - showWidget?: () => void; - hideWidget?: () => void; - toggleVisibility?: () => void; - endChat?: () => void; - }; - Tawk_LoadStart?: Date; - } -} - -export function ChatWidget() { - useEffect(() => { - // Initialize Tawk_API - window.Tawk_API = window.Tawk_API || {}; - - // Set up event handlers - window.Tawk_API.onLoad = function() { - console.log('Tawk.to chat widget loaded'); - }; - - window.Tawk_API.onChatMaximized = function() { - console.log('Chat maximized'); - }; - - window.Tawk_API.onChatMinimized = function() { - console.log('Chat minimized'); - }; - - window.Tawk_API.onChatStarted = function() { - console.log('Chat started'); - }; - - window.Tawk_API.onChatEnded = function() { - console.log('Chat ended'); - }; - - window.Tawk_API.onPrechatSubmit = function(data: any) { - console.log('Pre-chat form submitted:', data); - }; - - window.Tawk_API.onOfflineSubmit = function(data: any) { - console.log('Offline form submitted:', data); - }; - - // Load Tawk.to script - const Tawk_LoadStart = new Date(); - const s1 = document.createElement("script"); - const s0 = document.getElementsByTagName("script")[0]; - - s1.async = true; - s1.src = 'https://embed.tawk.to/684c069b4b5a53190afc6fb5/1itkfjkm5'; - s1.charset = 'UTF-8'; - s1.setAttribute('crossorigin', '*'); - - if (s0?.parentNode) { - s0.parentNode.insertBefore(s1, s0); - } - - // Cleanup function - return () => { - s1.remove(); - // Reset Tawk_API - window.Tawk_API = {}; - }; - }, []); - - return null; -} \ No newline at end of file diff --git a/packages/frontend-web/components/dashboard-layout.tsx b/packages/frontend-web/components/dashboard-layout.tsx index 6a071dfb..f7bce262 100644 --- a/packages/frontend-web/components/dashboard-layout.tsx +++ b/packages/frontend-web/components/dashboard-layout.tsx @@ -30,6 +30,8 @@ import { FileText, PanelLeftClose, PanelLeftOpen, + Folder, + HelpCircle, } from "lucide-react"; import { cn } from "@/libs/utils"; import { GeneratePasswordDialog } from "@/components/generate-password-dialog"; @@ -58,7 +60,7 @@ import { secureSetItem } from '@/libs/local-storage-utils'; import { SearchModal } from "@/components/search-modal"; import { logout } from "@/libs/utils"; import { SidebarNav } from "@/components/sidebar-nav"; -import { ChatWidget } from "@/components/chat-widget"; +import { SupportModal } from "@/components/support-modal"; interface DashboardLayoutProps { children: React.ReactNode; @@ -257,6 +259,7 @@ export function DashboardLayout({ children, locale = "en" }: DashboardLayoutProp const [showFavoritesDialog, setShowFavoritesDialog] = useState(false); const [favoriteTags, setFavoriteTags] = useState(["Personal", "Work", "Banking"]); const [showSearchModal, setShowSearchModal] = useState(false); + const [showSupportModal, setShowSupportModal] = useState(false); const [currentLocale, setCurrentLocale] = useState(locale); const languageLabels: Record = { @@ -504,6 +507,13 @@ export function DashboardLayout({ children, locale = "en" }: DashboardLayoutProp path: "/dashboard", icon: }); + + enabledModules.push({ + key: "drive", + labelKey: "drive", + path: "/dashboard/drive", + icon: + }); if (selectedProject.features.login?.enabled) { enabledModules.push({ @@ -599,7 +609,6 @@ export function DashboardLayout({ children, locale = "en" }: DashboardLayoutProp return (
- setShowSearchModal(false)} @@ -702,6 +711,23 @@ export function DashboardLayout({ children, locale = "en" }: DashboardLayoutProp Toggle theme + + + + + + + Support & Queries + +
@@ -711,6 +737,7 @@ export function DashboardLayout({ children, locale = "en" }: DashboardLayoutProp {showGeneratePassword && setShowGeneratePassword(false)} />} {showProjectDialog && setShowProjectDialog(false)} />} + {showSupportModal && setShowSupportModal(false)} />}
); } \ No newline at end of file diff --git a/packages/frontend-web/components/drive-content.tsx b/packages/frontend-web/components/drive-content.tsx index e5b68d10..0d523d84 100644 --- a/packages/frontend-web/components/drive-content.tsx +++ b/packages/frontend-web/components/drive-content.tsx @@ -3,6 +3,7 @@ import { useState, useCallback, useEffect } from "react"; import { useSelector } from "react-redux"; import { RootState } from "@/libs/Redux/store"; +import { useSearchParams, useRouter, usePathname } from "next/navigation"; import { Button } from "@/components/ui/button"; import { Folder as FolderIcon, @@ -69,6 +70,10 @@ interface DriveFile { export function DriveContent() { const { translate } = useTranslator(); + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const selectedWorkspaceId = useSelector( (state: RootState) => state.workspace.selectedWorkspaceId ); @@ -90,6 +95,7 @@ export function DriveContent() { getFolderPath, getSubfolders, uploadFile, + uploadFolder, renameFile, moveFile, deleteFiles, @@ -98,6 +104,7 @@ export function DriveContent() { previewFile, isUploading, uploadProgress, + uploadStatusMessage, isDownloading, downloadProgress, } = useDriveManagement({ @@ -124,6 +131,28 @@ export function DriveContent() { const [previewBlobUrl, setPreviewBlobUrl] = useState(null); const [previewingFile, setPreviewingFile] = useState(null); + // Sync currentFolder with URL parameter + useEffect(() => { + const folderId = searchParams.get('folder'); + + if (!folderId) { + // No folder parameter means we're at root + setCurrentFolder(null); + } else if (folders.length > 0) { + // Find the folder by ID + const folder = folders.find(f => f.folder_id === folderId); + + // Only update if the folder exists and is different from current + if (folder && folder.folder_id !== currentFolder?.folder_id) { + setCurrentFolder(folder); + } else if (!folder && currentFolder) { + // Folder doesn't exist, reset to root + setCurrentFolder(null); + router.replace(pathname); + } + } + }, [searchParams, folders, currentFolder, router, pathname, setCurrentFolder]); + // Get current subfolders to display const currentSubfolders = getSubfolders(currentFolder?.folder_id || null); @@ -134,22 +163,22 @@ export function DriveContent() { const breadcrumbPath = getFolderPath(currentFolder?.folder_id || null); const handleFolderClick = (folder: Folder) => { - setCurrentFolder(folder); + // Update URL with folder parameter to create browser history entry + const params = new URLSearchParams(searchParams.toString()); + params.set('folder', folder.folder_id); + router.push(`${pathname}?${params.toString()}`); }; const handleBackClick = () => { if (!currentFolder) return; - if (currentFolder.parent_id) { - const parentFolder = folders.find(f => f.folder_id === currentFolder.parent_id); - setCurrentFolder(parentFolder || null); - } else { - setCurrentFolder(null); - } + // Use browser back to navigate through history + router.back(); }; const handleHomeClick = () => { - setCurrentFolder(null); + // Navigate to root by removing folder parameter + router.push(pathname); }; const handleCreateFolder = () => { @@ -300,7 +329,11 @@ export function DriveContent() { @@ -322,7 +355,7 @@ export function DriveContent() { )} + + +
+

+ Contact the maintainers for support/queries +

+ +
+ {maintainerEmails.map((email) => ( + +
+ +
+
+

{email}

+

Click to send email

+
+
+ ))} +
+ +
+ +
+
+ + + ) +} + diff --git a/packages/frontend-web/components/upload-file-dialog.tsx b/packages/frontend-web/components/upload-file-dialog.tsx index 86f023f3..06791a14 100644 --- a/packages/frontend-web/components/upload-file-dialog.tsx +++ b/packages/frontend-web/components/upload-file-dialog.tsx @@ -11,7 +11,7 @@ import { } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Progress } from "@/components/ui/progress"; -import { Upload, X, FileIcon } from "lucide-react"; +import { Upload, X, FileIcon, FolderIcon } from "lucide-react"; import { useTranslator } from "@/hooks/use-translations"; import { validateFileSize, formatBytes } from "@/libs/file-encryption"; @@ -27,8 +27,10 @@ interface UploadFileDialogProps { onFileUploaded: () => void; currentFolder: Folder | null; uploadFile: (file: File, parentId?: string | null) => Promise; + uploadFolder: (files: FileList, parentId?: string | null) => Promise; isUploading: boolean; uploadProgress: number; + uploadStatusMessage: string; } export function UploadFileDialog({ @@ -37,32 +39,43 @@ export function UploadFileDialog({ onFileUploaded, currentFolder, uploadFile, + uploadFolder, isUploading, uploadProgress, + uploadStatusMessage, }: UploadFileDialogProps) { const { translate } = useTranslator(); - const [selectedFile, setSelectedFile] = useState(null); + const [selectedFiles, setSelectedFiles] = useState(null); + const [isFolder, setIsFolder] = useState(false); const [dragActive, setDragActive] = useState(false); const [error, setError] = useState(""); - const handleFileSelect = useCallback((file: File | null) => { + const handleFilesSelect = useCallback((files: FileList | null) => { setError(""); - if (!file) { - setSelectedFile(null); + if (!files || files.length === 0) { + setSelectedFiles(null); + setIsFolder(false); return; } - // Validate file size (50MB max) - if (!validateFileSize(file, 50)) { - setError(translate("file_too_large", "drive", { - default: "File size exceeds 50MB limit", - })); - setSelectedFile(null); - return; + // Check if it's a folder upload (files have webkitRelativePath) + const firstFile = files[0] as any; + const hasRelativePath = firstFile.webkitRelativePath && firstFile.webkitRelativePath.includes('/'); + setIsFolder(hasRelativePath); + + // For individual file uploads, validate size + if (!hasRelativePath && files.length === 1) { + if (!validateFileSize(files[0], 50)) { + setError(translate("file_too_large", "drive", { + default: "File size exceeds 50MB limit", + })); + setSelectedFiles(null); + return; + } } - setSelectedFile(file); + setSelectedFiles(files); }, [translate]); const handleDrag = useCallback((e: React.DragEvent) => { @@ -80,35 +93,60 @@ export function UploadFileDialog({ e.stopPropagation(); setDragActive(false); - if (e.dataTransfer.files && e.dataTransfer.files[0]) { - handleFileSelect(e.dataTransfer.files[0]); + if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { + handleFilesSelect(e.dataTransfer.files); } - }, [handleFileSelect]); + }, [handleFilesSelect]); const handleInputChange = useCallback((e: React.ChangeEvent) => { - if (e.target.files && e.target.files[0]) { - handleFileSelect(e.target.files[0]); + if (e.target.files && e.target.files.length > 0) { + handleFilesSelect(e.target.files); } - }, [handleFileSelect]); + }, [handleFilesSelect]); const handleUpload = async () => { - if (!selectedFile) return; + if (!selectedFiles) return; try { - await uploadFile(selectedFile, currentFolder?.folder_id || null); - setSelectedFile(null); + setError(""); // Clear any previous errors + if (isFolder || selectedFiles.length > 1) { + // Upload folder or multiple files + await uploadFolder(selectedFiles, currentFolder?.folder_id || null); + } else { + // Upload single file + await uploadFile(selectedFiles[0], currentFolder?.folder_id || null); + } + setSelectedFiles(null); + setIsFolder(false); setError(""); onFileUploaded(); onOpenChange(false); - } catch (error) { + } catch (error: any) { console.error("Upload error:", error); - // Error is handled in the hook with a toast + + // Extract error message from API response + const errorData = error?.response?.data; + const isFileExistsError = + (errorData?.status_code === 400 || error?.response?.status === 400) && + (errorData?.message?.toLowerCase().includes("file already exists") || + errorData?.message?.toLowerCase().includes("already exists")); + + const errorMessage = isFileExistsError + ? translate("file_already_exists", "drive", { + default: "A file with this name already exists in this location", + }) + : errorData?.message || translate("error_uploading_file", "drive", { + default: "Failed to upload file. Please try again.", + }); + + setError(errorMessage); } }; const handleCancel = () => { if (!isUploading) { - setSelectedFile(null); + setSelectedFiles(null); + setIsFolder(false); setError(""); onOpenChange(false); } @@ -119,7 +157,7 @@ export function UploadFileDialog({ - {translate("upload_file", "drive", { default: "Upload File" })} + {translate("upload_file", "drive", { default: "Upload Files or Folder" })} @@ -129,11 +167,11 @@ export function UploadFileDialog({ folderName: currentFolder.name, }) : translate("upload_to_root", "drive", { - default: "Upload a file to root", + default: "Upload files or folders to root", })} - {translate("max_file_size", "drive", { default: "Maximum file size: 50MB" })} + {translate("max_file_size", "drive", { default: "Maximum file size: 50MB per file" })} @@ -151,45 +189,92 @@ export function UploadFileDialog({ onDragOver={handleDrag} onDrop={handleDrop} > - - -
+
-
- - - {" "} - {translate("or_drag_drop", "drive", { default: "or drag and drop" })} - +
+ {translate("or_drag_drop", "drive", { default: "Drag and drop files or folders here" })} +
+ + {/* Two separate upload buttons */} +
+ {/* Upload Files Button */} +
+ + +
+ + {/* Upload Folder Button */} +
+ + +
- {/* Selected file display */} - {selectedFile && !isUploading && ( + {/* Selected files display */} + {selectedFiles && !isUploading && (
-

- {selectedFile.name} -

-

{formatBytes(selectedFile.size)}

+ {isFolder ? ( + <> +

+ Folder selected +

+

+ {selectedFiles.length} files +

+ + ) : selectedFiles.length === 1 ? ( + <> +

+ {selectedFiles[0].name} +

+

{formatBytes(selectedFiles[0].size)}

+ + ) : ( + <> +

+ {selectedFiles.length} files selected +

+ + )}