diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 3940fe9..4b22e51 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -20,7 +20,7 @@ import { import { cn } from "@/lib/utils"; import { useAppSelector } from "@/hooks/redux-hooks"; -import { useTenantTheme } from "@/hooks/useTenantTheme"; +import { useAppTheme } from "@/hooks/useAppTheme"; import { AuthenticatedImage } from "@/components/shared"; interface MenuItem { @@ -109,6 +109,11 @@ const tenantAdminPlatformServiceMenu: MenuItem[] = [ path: "/tenant/files", requiredPermission: { resource: "files" }, }, + { + label: "Storage Dashboard", + path: "/tenant/files/storage-dashboard", + requiredPermission: { resource: "files" }, + }, ], requiredPermission: { resource: "files" }, }, @@ -185,15 +190,15 @@ const GroupMenuItem = ({ item, childrenItems, location, - isSuperAdmin, - theme, + primaryColor, + secondaryColor, onClose, }: { item: MenuItem; childrenItems: any[]; location: any; - isSuperAdmin: boolean; - theme: any; + primaryColor: string; + secondaryColor: string; onClose: () => void; }) => { const isChildActive = (path: string) => { @@ -205,6 +210,14 @@ const GroupMenuItem = ({ ); if (isSubActionActive) return false; } + + // Special handling for Files List to NOT show as active when Storage Dashboard is active + if (path === "/tenant/files") { + if (location.pathname.startsWith("/tenant/files/storage-dashboard")) { + return false; + } + } + return ( location.pathname === path || location.pathname.startsWith(`${path}/`) ); @@ -233,14 +246,8 @@ const GroupMenuItem = ({ style={ isAnyChildActive ? { - backgroundColor: - !isSuperAdmin && theme?.primary_color - ? theme.primary_color - : "#112868", - color: - !isSuperAdmin && theme?.secondary_color - ? theme.secondary_color - : "#23dce1", + backgroundColor: primaryColor, + color: secondaryColor, } : undefined } @@ -283,10 +290,7 @@ const GroupMenuItem = ({ style={ isActive ? { - color: - !isSuperAdmin && theme?.primary_color - ? theme.primary_color - : "#112868", + color: primaryColor, } : undefined } @@ -304,9 +308,9 @@ const GroupMenuItem = ({ }; export const Sidebar = ({ isOpen, onClose }: SidebarProps) => { + const { primaryColor, secondaryColor, accentColor, logoUrl } = useAppTheme(); const location = useLocation(); const { roles, permissions } = useAppSelector((state) => state.auth); - const { theme, logoUrl } = useAppSelector((state) => state.theme); // Fetch theme for tenant admin const isSuperAdminCheck = () => { @@ -357,9 +361,9 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => { const roleName = getRoleName(); // Fetch theme if tenant admin - if (!isSuperAdmin) { - useTenantTheme(); - } + // if (!isSuperAdmin) { + // useTenantTheme(); + // } // Helper function to check if user has permission for a resource const hasPermission = ( @@ -461,8 +465,8 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => { item={item} childrenItems={children} location={location} - isSuperAdmin={isSuperAdmin} - theme={theme} + primaryColor={primaryColor} + secondaryColor={secondaryColor} onClose={onClose} /> ); @@ -494,14 +498,8 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => { style={ isActive ? { - backgroundColor: - !isSuperAdmin && theme?.primary_color - ? theme.primary_color - : "#112868", - color: - !isSuperAdmin && theme?.secondary_color - ? theme.secondary_color - : "#23dce1", + backgroundColor: primaryColor, + color: secondaryColor, } : undefined } @@ -545,10 +543,7 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => { className="w-9 h-9 rounded-[10px] flex items-center justify-center shadow-[0px_4px_12px_0px_rgba(15,23,42,0.1)] shrink-0" style={{ display: !isSuperAdmin && logoUrl ? "none" : "flex", - backgroundColor: - !isSuperAdmin && theme?.primary_color - ? theme.primary_color - : "#112868", + backgroundColor: primaryColor, }} > @@ -561,10 +556,7 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
{roleName} @@ -625,7 +617,10 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => { alt="Logo" className="h-9 w-auto max-w-[180px] object-contain" fallback={ -
+
} @@ -635,10 +630,7 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => { className="w-9 h-9 rounded-[10px] flex items-center justify-center shadow-[0px_4px_12px_0px_rgba(15,23,42,0.1)] shrink-0" style={{ display: !isSuperAdmin && logoUrl ? "none" : "flex", - backgroundColor: - !isSuperAdmin && theme?.primary_color - ? theme.primary_color - : "#112868", + backgroundColor: primaryColor, }} > @@ -651,10 +643,7 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
{roleName} diff --git a/src/components/shared/ActionDropdown.tsx b/src/components/shared/ActionDropdown.tsx index edf56f5..c32bd0a 100644 --- a/src/components/shared/ActionDropdown.tsx +++ b/src/components/shared/ActionDropdown.tsx @@ -1,8 +1,9 @@ import React, { useState, useRef, useEffect } from 'react'; import { createPortal } from 'react-dom'; import type { ReactElement } from 'react'; -import { MoreVertical, Eye, Edit, Trash2, Users, BarChart3 } from 'lucide-react'; +import { MoreVertical, Eye, Edit, Trash2, Users, BarChart3, Download } from 'lucide-react'; import { cn } from '@/lib/utils'; +import { useAppTheme } from '@/hooks/useAppTheme'; export interface ActionItem { label: string; @@ -17,6 +18,7 @@ interface ActionDropdownProps { onDelete?: () => void; onContacts?: () => void; onScorecards?: () => void; + onDownload?: () => void; actions?: ActionItem[]; trigger?: React.ReactNode; className?: string; @@ -28,10 +30,12 @@ export const ActionDropdown = ({ onDelete, onContacts, onScorecards, + onDownload, actions, trigger, className, }: ActionDropdownProps): ReactElement => { + const { primaryColor } = useAppTheme(); const [isOpen, setIsOpen] = useState(false); const [dropdownStyle, setDropdownStyle] = useState<{ top?: string; bottom?: string; right: string; width: string }>({ right: '0', width: '0' }); const dropdownRef = useRef(null); @@ -67,14 +71,22 @@ export const ActionDropdown = ({ const rect = buttonRef.current.getBoundingClientRect(); const spaceBelow = window.innerHeight - rect.bottom; const spaceAbove = rect.top; - const dropdownHeight = actions ? actions.length * 32 + 16 : 120; // Approximate height based on actions + const dropdownHeight = ( + (onView ? 1 : 0) + + (onEdit ? 1 : 0) + + (onDelete ? 1 : 0) + + (onContacts ? 1 : 0) + + (onScorecards ? 1 : 0) + + (onDownload ? 1 : 0) + + (actions ? actions.length : 0) + ) * 36 + 12; // Determine if should open upward or downward const shouldOpenUp = spaceBelow < dropdownHeight && spaceAbove > spaceBelow; // Calculate dropdown position const right = window.innerWidth - rect.right; - const width = actions ? 140 : 76; // Wider for custom actions + const width = (actions || onScorecards || onDownload || onContacts) ? 140 : 100; // Wider for longer labels if (shouldOpenUp) { // Position above the button @@ -125,12 +137,25 @@ export const ActionDropdown = ({ ref={buttonRef} type="button" onClick={() => setIsOpen(!isOpen)} - className={cn( - 'flex items-center justify-center w-7 h-7 rounded-full border border-[rgba(0,0,0,0.08)] transition-colors cursor-pointer', - isOpen - ? 'bg-[#084cc8] text-white' - : 'bg-white text-[#0f1724] hover:bg-[#084cc8] hover:text-white' - )} + className="flex items-center justify-center w-7 h-7 rounded-full border border-[rgba(0,0,0,0.08)] transition-colors cursor-pointer" + style={isOpen + ? { backgroundColor: primaryColor, color: 'white', borderColor: primaryColor } + : { backgroundColor: 'white', color: '#0f1724' } + } + onMouseEnter={(e) => { + if (!isOpen) { + e.currentTarget.style.backgroundColor = primaryColor; + e.currentTarget.style.color = 'white'; + e.currentTarget.style.borderColor = primaryColor; + } + }} + onMouseLeave={(e) => { + if (!isOpen) { + e.currentTarget.style.backgroundColor = 'white'; + e.currentTarget.style.color = '#0f1724'; + e.currentTarget.style.borderColor = 'rgba(0,0,0,0.08)'; + } + }} aria-label="Actions" aria-expanded={isOpen} > @@ -169,9 +194,9 @@ export const ActionDropdown = ({ )} @@ -179,9 +204,9 @@ export const ActionDropdown = ({ )} @@ -189,9 +214,9 @@ export const ActionDropdown = ({ )} @@ -199,9 +224,9 @@ export const ActionDropdown = ({ )} @@ -209,12 +234,22 @@ export const ActionDropdown = ({ )} + {onDownload && ( + + )} )}
diff --git a/src/components/shared/DeleteConfirmationModal.tsx b/src/components/shared/DeleteConfirmationModal.tsx index d0bffb0..1a72e61 100644 --- a/src/components/shared/DeleteConfirmationModal.tsx +++ b/src/components/shared/DeleteConfirmationModal.tsx @@ -12,6 +12,7 @@ interface DeleteConfirmationModalProps { message: string; itemName?: string; isLoading?: boolean; + children?: React.ReactNode; } export const DeleteConfirmationModal = ({ @@ -22,6 +23,7 @@ export const DeleteConfirmationModal = ({ message, itemName, isLoading = false, + children, }: DeleteConfirmationModalProps): ReactElement | null => { const modalRef = useRef(null); @@ -65,10 +67,10 @@ export const DeleteConfirmationModal = ({ if (!isOpen) return null; const modalContent = ( -
+
{/* Modal Header */}
@@ -100,6 +102,7 @@ export const DeleteConfirmationModal = ({ )} ? This action cannot be undone.

+ {children &&
{children}
}
{/* Modal Footer */} diff --git a/src/components/shared/FileShareModal.tsx b/src/components/shared/FileShareModal.tsx index fc34e59..b40b517 100644 --- a/src/components/shared/FileShareModal.tsx +++ b/src/components/shared/FileShareModal.tsx @@ -13,6 +13,7 @@ import { import { cn } from "@/lib/utils"; import { Modal } from "./Modal"; import fileAttachmentService, { type FileAttachment } from "@/services/file-attachment-service"; +import { DeleteConfirmationModal } from "./DeleteConfirmationModal"; interface FileShareModalProps { isOpen: boolean; @@ -30,7 +31,9 @@ export const FileShareModal: React.FC = ({ const [permissions, setPermissions] = useState<"view" | "download">("download"); const [isSharing, setIsSharing] = useState(false); - const [shareData, setShareData] = useState<{ url: string; token: string } | null>(null); + const [shareData, setShareData] = useState<{ url: string; token: string; id: string } | null>(null); + const [isRevoking, setIsRevoking] = useState(false); + const [showRevokeConfirm, setShowRevokeConfirm] = useState(false); const [copied, setCopied] = useState(false); const handleCreateShare = async () => { @@ -45,7 +48,7 @@ export const FileShareModal: React.FC = ({ }); const fullUrl = `${baseUrl}/files/shared/${res.data.share_token}`; - setShareData({ url: fullUrl, token: res.data.share_token }); + setShareData({ url: fullUrl, token: res.data.share_token, id: res.data.id }); } catch (error) { console.error("Failed to share:", error); } finally { @@ -53,6 +56,22 @@ export const FileShareModal: React.FC = ({ } }; + const handleRevokeShare = async () => { + if (!shareData) return; + + setIsRevoking(true); + try { + await fileAttachmentService.revokeShare(shareData.id); + setShareData(null); + setShowRevokeConfirm(false); + } catch (error) { + console.error("Failed to revoke share:", error); + alert("Failed to revoke share link. Please try again."); + } finally { + setIsRevoking(false); + } + }; + const copyToClipboard = async () => { if (!shareData) return; try { @@ -90,6 +109,7 @@ export const FileShareModal: React.FC = ({ title="Share File" description={file.original_name} maxWidth="md" + preventCloseOnClickOutside={showRevokeConfirm} >
{!shareData ? ( @@ -204,19 +224,33 @@ export const FileShareModal: React.FC = ({
-
+
+
+ + +
+ -
@@ -229,6 +263,15 @@ export const FileShareModal: React.FC = ({

+ + setShowRevokeConfirm(false)} + onConfirm={handleRevokeShare} + title="Revoke Share Link" + message="Are you sure you want to revoke this share link? It will stop working immediately." + isLoading={isRevoking} + /> ); }; diff --git a/src/components/shared/FileUploadModal.tsx b/src/components/shared/FileUploadModal.tsx index 952a616..7d08fba 100644 --- a/src/components/shared/FileUploadModal.tsx +++ b/src/components/shared/FileUploadModal.tsx @@ -14,7 +14,6 @@ import { type ReactElement, useEffect, } from "react"; -import { createPortal } from "react-dom"; import { X, Upload, @@ -29,6 +28,14 @@ import { ChevronDown as ChevronDownIcon, } from "lucide-react"; import { cn } from "@/lib/utils"; +import { + Modal, + PrimaryButton, + SecondaryButton, + FormField, + FormSelect, + FormTextArea, +} from "@/components/shared"; import fileAttachmentService, { type CategoriesFilterOptions, } from "@/services/file-attachment-service"; @@ -328,348 +335,324 @@ export const FileUploadModal = ({ const validCount = files.filter((f) => f.status !== "blocked").length; const doneCount = files.filter((f) => f.status === "done").length; - const content = ( -
-
- {/* Header */} -
-
-

Upload New File

-

Attach files via File Attachment Service

-
- -
+ const footer = ( + <> + + Cancel + + + {isUploading ? ( + <> + + Uploading ({doneCount}/{validCount}) + + ) : ( + <> + + Upload {validCount > 0 ? `(${validCount})` : ""} + + )} + + + ); - {/* Scrollable body */} -
- - {/* Drop Zone */} -
-

Attach Files

- {files.length === 0 ? ( -
{ e.preventDefault(); setIsDragging(true); }} - onDragLeave={() => setIsDragging(false)} - onDrop={onDrop} - onClick={() => inputRef.current?.click()} - className={cn( - "border-2 border-dashed rounded-xl flex flex-col items-center justify-center gap-3 py-8 cursor-pointer transition-all", - isDragging - ? "border-[#084cc8] bg-[#084cc8]/5" - : "border-[rgba(0,0,0,0.12)] hover:border-[#084cc8]/50 hover:bg-gray-50" - )} - > -
- -
-
-

Click to upload or drag and drop

-

- Attach supporting source files via File Attachment Service -

-

PDF, DOCX, XLSX up to {MAX_SIZE_MB}MB

-
+ return ( + +
+ {/* Drop Zone */} +
+

Attach Files

+ {files.length === 0 ? ( +
{ e.preventDefault(); setIsDragging(true); }} + onDragLeave={() => setIsDragging(false)} + onDrop={onDrop} + onClick={() => inputRef.current?.click()} + className={cn( + "border-2 border-dashed rounded-xl flex flex-col items-center justify-center gap-3 py-8 cursor-pointer transition-all", + isDragging + ? "border-[#084cc8] bg-[#084cc8]/5" + : "border-[rgba(0,0,0,0.12)] hover:border-[#084cc8]/50 hover:bg-gray-50" + )} + > +
+
- ) : ( +
+

Click to upload or drag and drop

+

+ Attach supporting source files via File Attachment Service +

+

PDF, DOCX, XLSX up to {MAX_SIZE_MB}MB

+
+
+ ) : ( +
{ e.preventDefault(); setIsDragging(true); }} + onDragLeave={() => setIsDragging(false)} + onDrop={onDrop} + className={cn( + "border-2 border-dashed rounded-xl transition-all", + isDragging ? "border-[#084cc8] bg-[#084cc8]/5" : "border-[rgba(0,0,0,0.08)]" + )} + > + {/* Add files button */}
{ e.preventDefault(); setIsDragging(true); }} - onDragLeave={() => setIsDragging(false)} - onDrop={onDrop} - className={cn( - "border-2 border-dashed rounded-xl transition-all", - isDragging ? "border-[#084cc8] bg-[#084cc8]/5" : "border-[rgba(0,0,0,0.08)]" - )} + onClick={() => inputRef.current?.click()} + className="flex items-center justify-center gap-2 py-3 cursor-pointer border-b border-[rgba(0,0,0,0.06)] hover:bg-gray-50 transition-all rounded-t-xl" > - {/* Add files button */} -
inputRef.current?.click()} - className="flex items-center justify-center gap-2 py-3 cursor-pointer border-b border-[rgba(0,0,0,0.06)] hover:bg-gray-50 transition-all rounded-t-xl" - > -
- -
- Add Files +
+
+ Add Files +
- {/* File list */} -
- {files.map((entry) => ( -
-
{getFileIcon(entry.file.type, entry.file.name)}
-
-
- - {entry.file.name} - - - {formatBytes(entry.file.size)} - - {entry.status === "uploading" && ( - - {entry.progress}% - - )} - {entry.status === "done" && ( - - Complete - - )} - {(entry.status === "blocked" || entry.status === "error") && ( - - {entry.error} - - )} -
+ {/* File list */} +
+ {files.map((entry) => ( +
+
{getFileIcon(entry.file.type, entry.file.name)}
+
+
+ + {entry.file.name} + + + {formatBytes(entry.file.size)} + {entry.status === "uploading" && ( -
-
-
+ + {entry.progress}% + )} {entry.status === "done" && ( -
-
-
+ + Complete + )} {(entry.status === "blocked" || entry.status === "error") && ( -
+ + {entry.error} + )}
- {entry.status !== "uploading" && entry.status !== "done" && ( - + {entry.status === "uploading" && ( +
+
+
+ )} + {entry.status === "done" && ( +
+
+
+ )} + {(entry.status === "blocked" || entry.status === "error") && ( +
)}
- ))} -
-
- )} -

Up to {MAX_FILES} files allowed

- - {uploadSuccess && ( -
- - Files upload successfully. -
- )} -
- - - - {/* Fields: Entity Type + Entity ID */} -
-
- - -
-
- -
- setEntityId(e.target.value)} - disabled={!!defaultEntityId || isUploading} - placeholder="e.g. PRJ-1204 (UUID)" - className="w-full h-9 border border-[rgba(0,0,0,0.12)] rounded-lg pl-3 pr-9 text-sm text-[#0e1b2a] placeholder:text-[#c4cbd6] focus:outline-none focus:ring-2 focus:ring-[#084cc8]/20 focus:border-[#084cc8] disabled:bg-gray-50 disabled:text-[#9aa6b2]" - /> - {!defaultEntityId && !isUploading && isTenantAdmin && ( - - )} -
-
-
- - {/* Category Name (Editable Combobox) */} -
- -
- { - setCategoryInput(e.target.value); - setCategorySearch(e.target.value); - setShowCategoryDropdown(true); - }} - onFocus={() => setShowCategoryDropdown(true)} - disabled={isUploading} - placeholder="Type or select a category..." - className="w-full h-9 border border-[rgba(0,0,0,0.12)] rounded-lg pl-3 pr-9 text-sm text-[#0e1b2a] bg-white focus:outline-none focus:ring-2 focus:ring-[#084cc8]/20 focus:border-[#084cc8]" - /> -
- {categoryInput && ( - - )} - -
-
- - {showCategoryDropdown && !isUploading && ( -
- {categories.filter(c => c.category.toLowerCase().includes(categorySearch.toLowerCase())).length > 0 ? ( - categories - .filter(c => c.category.toLowerCase().includes(categorySearch.toLowerCase())) - .map((cat) => ( + {entry.status !== "uploading" && entry.status !== "done" && ( - )) - ) : ( -
- {categorySearch ? `Press enter to use "${categorySearch}"` : "No categories found"} + )}
- )} + ))}
- )} -
- - {/* Tags */} -
- -
- {tags.map((tag) => ( - - {tag} - - - ))} - setTagInput(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === ",") { - e.preventDefault(); - addTag(); - } - }} - placeholder={tags.length === 0 ? "Add a tag..." : ""} - disabled={isUploading} - className="flex-1 min-w-[80px] text-sm outline-none bg-transparent placeholder:text-[#c4cbd6]" - />
-
+ )} +

Up to {MAX_FILES} files allowed

- {/* Description */} -
- -