1- import { useState , useRef , useEffect } from 'react' ;
1+ import { useState , useRef , useEffect , useCallback } from 'react' ;
22import * as Ariakit from '@ariakit/react' ;
33import { Dialog , DialogContent , DialogHeader , DialogTitle } from '@librechat/client' ;
44import { Button } from '@librechat/client' ;
55import { useToastContext , DropdownPopup } from '@librechat/client' ;
66import { saasApi } from '~/services/saasApi' ;
7- import { File as FileIcon , ChevronDown , X } from 'lucide-react' ;
7+ import { File as FileIcon , ChevronDown , Plus , X } from 'lucide-react' ;
88import { cn } from '~/utils' ;
9+ import { asset } from '~/utils/assetPath' ;
10+ import CreateFolderModal from '~/components/Resources/Modals/CreateFolderModal' ;
911import {
1012 isResearchAllowedUploadFile ,
1113 RESEARCH_ALLOWED_ACCEPT ,
@@ -57,11 +59,19 @@ interface FlatFolder {
5759
5860const EMPTY_FOLDER_LIST : any [ ] = [ ] ;
5961
62+ const createFolderButtonClassName = cn (
63+ 'flex h-[var(--Size-zero-button)] w-full items-center justify-center gap-0.5 rounded-[2px] border border-fig-Stroke-primary' ,
64+ 'bg-fig-Surface-two-primary px-[var(--Dimensions-Size-xs3)] py-px' ,
65+ 'font-inter text-xs font-normal leading-4 text-fig-Subject-two-primary' ,
66+ 'transition-opacity hover:opacity-90' ,
67+ 'hover:!border-fig-Stroke-primary hover:!bg-fig-Surface-two-primary hover:!text-fig-Subject-two-primary' ,
68+ ) ;
69+
6070export default function UploadFileModal ( {
6171 folderId,
6272 orgId,
6373 folders,
64- isSuperAdmin : _isSuperAdmin = false ,
74+ isSuperAdmin : isSuperAdminProp = false ,
6575 isOrgAdmin : isOrgAdminProp ,
6676 currentUserId : currentUserIdProp ,
6777 onClose,
@@ -79,10 +89,12 @@ export default function UploadFileModal({
7989 const [ loading , setLoading ] = useState ( false ) ;
8090 const [ error , setError ] = useState < string | null > ( null ) ;
8191 const [ isFolderMenuOpen , setIsFolderMenuOpen ] = useState ( false ) ;
92+ const [ showCreateFolderModal , setShowCreateFolderModal ] = useState ( false ) ;
8293 const fileInputRef = useRef < HTMLInputElement > ( null ) ;
8394
8495 const currentUserId = currentUserIdProp || ( userInfo ?. user_id || userInfo ?. id ) ?. toString ( ) ;
8596 const isOrgAdmin = isOrgAdminProp !== undefined ? isOrgAdminProp : userInfo ?. org_role === 'admin' ;
97+ const isSuperAdmin = isSuperAdminProp || Boolean ( userInfo ?. is_super_admin ) ;
8698
8799 const resolvedOrgId =
88100 orgId != null && String ( orgId ) . trim ( ) !== ''
@@ -94,6 +106,21 @@ export default function UploadFileModal({
94106 : null ;
95107 const hasFoldersFromProps = ( folders ?. length ?? 0 ) > 0 ;
96108
109+ const refreshFolderTree = useCallback ( async ( ) => {
110+ setFoldersLoading ( true ) ;
111+ setFoldersError ( null ) ;
112+ try {
113+ const data = await saasApi . getFolderTree ( resolvedOrgId ) ;
114+ setFolderTree ( data . folders ?? [ ] ) ;
115+ } catch ( err ) {
116+ const message = err instanceof Error ? err . message : 'Failed to load folders' ;
117+ setFoldersError ( message ) ;
118+ setFolderTree ( [ ] ) ;
119+ } finally {
120+ setFoldersLoading ( false ) ;
121+ }
122+ } , [ resolvedOrgId ] ) ;
123+
97124 useEffect ( ( ) => {
98125 if ( hasFoldersFromProps && folders ) {
99126 setFolderTree ( folders ) ;
@@ -139,23 +166,12 @@ export default function UploadFileModal({
139166 let cancelled = false ;
140167
141168 const loadFolders = async ( ) => {
142- setFoldersLoading ( true ) ;
143- setFoldersError ( null ) ;
144169 try {
145- const data = await saasApi . getFolderTree ( resolvedOrgId ) ;
146- if ( ! cancelled ) {
147- setFolderTree ( data . folders ?? [ ] ) ;
148- }
149- } catch ( err ) {
170+ await refreshFolderTree ( ) ;
171+ } catch {
150172 if ( ! cancelled ) {
151- const message = err instanceof Error ? err . message : 'Failed to load folders' ;
152- setFoldersError ( message ) ;
153173 setFolderTree ( [ ] ) ;
154174 }
155- } finally {
156- if ( ! cancelled ) {
157- setFoldersLoading ( false ) ;
158- }
159175 }
160176 } ;
161177
@@ -164,7 +180,7 @@ export default function UploadFileModal({
164180 return ( ) => {
165181 cancelled = true ;
166182 } ;
167- } , [ hasFoldersFromProps , resolvedOrgId , userInfoLoaded ] ) ;
183+ } , [ hasFoldersFromProps , refreshFolderTree , userInfoLoaded ] ) ;
168184
169185 // Flatten folder tree for dropdown
170186 const flattenFolders = ( folderNodes : any [ ] , level = 0 ) : FlatFolder [ ] => {
@@ -210,6 +226,9 @@ export default function UploadFileModal({
210226 return pa - pb ;
211227 } ) ;
212228
229+ const hasSelectableFolders = sortedFolders . length > 0 ;
230+ const showEmptyFolderBanner = ! foldersLoading && ! foldersError && ! hasSelectableFolders ;
231+
213232 useEffect ( ( ) => {
214233 const applyDefaultFolder = ( ) => {
215234 const primary = sortedFolders . find ( ( f ) => isResearchDefaultUploadFolder ( f ) ) ;
@@ -296,7 +315,14 @@ export default function UploadFileModal({
296315 }
297316 }
298317
299- if ( sortedFolders . length > 0 && ! selectedFolderId ) {
318+ if ( ! hasSelectableFolders ) {
319+ const msg = 'Create a folder before uploading documents.' ;
320+ setError ( msg ) ;
321+ showToast ( { message : msg , status : 'error' } ) ;
322+ return ;
323+ }
324+
325+ if ( ! selectedFolderId ) {
300326 const msg = 'Please select a folder.' ;
301327 setError ( msg ) ;
302328 showToast ( { message : msg , status : 'error' } ) ;
@@ -346,6 +372,7 @@ export default function UploadFileModal({
346372 } ;
347373
348374 return (
375+ < >
349376 < Dialog
350377 open
351378 onOpenChange = { ( open ) => {
@@ -487,54 +514,88 @@ export default function UploadFileModal({
487514 { foldersError ? (
488515 < p className = "fy-typography-body-tiny text-fig-Subject-danger" > { foldersError } </ p >
489516 ) : null }
490- { ! foldersLoading && sortedFolders . length === 0 ? (
491- < p className = "fy-typography-body-tiny text-fig-Subject-soft" >
492- No upload folders are available.
493- </ p >
494- ) : null }
495- < DropdownPopup
496- portal = { false }
497- sameWidth = { true }
498- anchor = { { x : 'start' , y : 'bottom' } }
499- menuId = "folder-selector-upload"
500- isOpen = { isFolderMenuOpen }
501- setIsOpen = { setIsFolderMenuOpen }
502- trigger = {
503- < Ariakit . MenuButton
504- className = { cn (
505- 'fy-typography-body flex h-[var(--Size-zero-button)] w-full items-center justify-between' ,
506- 'rounded-[var(--Corner-moderatelyRounded)] border border-fig-Stroke-soft bg-fig-Surface-standard' ,
507- 'px-[var(--Padding-zero-spacer)] text-fig-Subject-standard' ,
508- 'transition-colors hover:border-fig-Stroke-standard' ,
509- ) }
517+ { showEmptyFolderBanner ? (
518+ < div
519+ className = { cn (
520+ 'flex flex-col gap-[var(--Gap-parentChild)]' ,
521+ 'rounded-[var(--Corner-moderatelyRounded)] border border-fig-Stroke-soft' ,
522+ 'bg-fig-Surface-one-neutral p-[var(--Padding-spacer)]' ,
523+ ) }
524+ >
525+ < div className = "flex items-start gap-[var(--Gap-zero-parentChild)]" >
526+ < img
527+ src = { asset ( 'Folder.svg' ) }
528+ alt = ""
529+ className = "mt-0.5 h-8 w-8 shrink-0 opacity-70 dark:invert"
530+ aria-hidden
531+ />
532+ < div className = "flex min-w-0 flex-1 flex-col gap-[var(--Gap-zero-sibling)]" >
533+ < p className = "fy-typography-title-tiny text-fig-Subject-standard" >
534+ No folders available
535+ </ p >
536+ < p className = "fy-typography-body-small text-fig-Subject-soft" >
537+ Create a folder to upload and organize your documents.
538+ </ p >
539+ </ div >
540+ </ div >
541+ < button
542+ type = "button"
543+ onClick = { ( ) => setShowCreateFolderModal ( true ) }
544+ className = { createFolderButtonClassName }
510545 >
511- < span className = "min-w-0 flex-1 overflow-hidden text-ellipsis text-left" >
512- { selectedFolderName || 'Select Folder' }
513- </ span >
514- < ChevronDown
515- className = "h-[var(--Size-zero-icon)] w-[var(--Size-zero-icon)] shrink-0 text-fig-Subject-soft"
546+ < Plus
547+ className = "h-[var(--Size-zero-icon)] w-[var(--Size-zero-icon)] shrink-0"
516548 aria-hidden
517549 />
518- </ Ariakit . MenuButton >
519- }
520- items = { sortedFolders . map ( ( folder ) => ( {
521- label : `${ ' ' . repeat ( folder . level ) } ${ folder . level > 0 ? '└─ ' : '' } ${ folder . name } ` ,
522- onClick : ( ) => {
523- setSelectedFolderId ( folder . id ) ;
524- setSelectedFolderName ( folder . name ) ;
525- setIsFolderMenuOpen ( false ) ;
526- } ,
527- } ) ) }
528- className = { cn (
529- 'rounded-[var(--Corner-moderatelyRounded)] border border-fig-Stroke-soft' ,
530- 'bg-fig-Surface-standard shadow-sm' ,
531- ) }
532- itemClassName = { cn (
533- 'fy-typography-body px-[var(--Padding-zero-neighbor)] py-[var(--Padding-zero-buddy)]' ,
534- 'text-fig-Subject-standard hover:bg-fig-Surface-neutral' ,
535- 'cursor-pointer transition-colors' ,
536- ) }
537- />
550+ Create folder
551+ </ button >
552+ </ div >
553+ ) : null }
554+ { hasSelectableFolders ? (
555+ < DropdownPopup
556+ portal = { false }
557+ sameWidth = { true }
558+ anchor = { { x : 'start' , y : 'bottom' } }
559+ menuId = "folder-selector-upload"
560+ isOpen = { isFolderMenuOpen }
561+ setIsOpen = { setIsFolderMenuOpen }
562+ trigger = {
563+ < Ariakit . MenuButton
564+ className = { cn (
565+ 'fy-typography-body flex h-[var(--Size-zero-button)] w-full items-center justify-between' ,
566+ 'rounded-[var(--Corner-moderatelyRounded)] border border-fig-Stroke-soft bg-fig-Surface-standard' ,
567+ 'px-[var(--Padding-zero-spacer)] text-fig-Subject-standard' ,
568+ 'transition-colors hover:border-fig-Stroke-standard' ,
569+ ) }
570+ >
571+ < span className = "min-w-0 flex-1 overflow-hidden text-ellipsis text-left" >
572+ { selectedFolderName || 'Select Folder' }
573+ </ span >
574+ < ChevronDown
575+ className = "h-[var(--Size-zero-icon)] w-[var(--Size-zero-icon)] shrink-0 text-fig-Subject-soft"
576+ aria-hidden
577+ />
578+ </ Ariakit . MenuButton >
579+ }
580+ items = { sortedFolders . map ( ( folder ) => ( {
581+ label : `${ ' ' . repeat ( folder . level ) } ${ folder . level > 0 ? '└─ ' : '' } ${ folder . name } ` ,
582+ onClick : ( ) => {
583+ setSelectedFolderId ( folder . id ) ;
584+ setSelectedFolderName ( folder . name ) ;
585+ setIsFolderMenuOpen ( false ) ;
586+ } ,
587+ } ) ) }
588+ className = { cn (
589+ 'rounded-[var(--Corner-moderatelyRounded)] border border-fig-Stroke-soft' ,
590+ 'bg-fig-Surface-standard shadow-sm' ,
591+ ) }
592+ itemClassName = { cn (
593+ 'fy-typography-body px-[var(--Padding-zero-neighbor)] py-[var(--Padding-zero-buddy)]' ,
594+ 'text-fig-Subject-standard hover:bg-fig-Surface-neutral' ,
595+ 'cursor-pointer transition-colors' ,
596+ ) }
597+ />
598+ ) : null }
538599 </ div >
539600 </ div >
540601
@@ -546,7 +607,8 @@ export default function UploadFileModal({
546607 loading ||
547608 foldersLoading ||
548609 ! selectedFile ||
549- ( sortedFolders . length > 0 && ! selectedFolderId )
610+ ! hasSelectableFolders ||
611+ ! selectedFolderId
550612 }
551613 className = { cn (
552614 'fy-typography-label h-[var(--Size-button)] rounded-[2px]' ,
@@ -575,5 +637,19 @@ export default function UploadFileModal({
575637 </ div >
576638 </ DialogContent >
577639 </ Dialog >
640+
641+ { showCreateFolderModal ? (
642+ < CreateFolderModal
643+ orgId = { resolvedOrgId }
644+ isSuperAdmin = { isSuperAdmin }
645+ folders = { folderTree }
646+ onClose = { ( ) => setShowCreateFolderModal ( false ) }
647+ onSuccess = { async ( ) => {
648+ setShowCreateFolderModal ( false ) ;
649+ await refreshFolderTree ( ) ;
650+ } }
651+ />
652+ ) : null }
653+ </ >
578654 ) ;
579655}
0 commit comments