apps/chat/src/components/Folder/Folder.tsx (1,221 lines of code) (raw):

import { useDismiss, useFloating, useInteractions } from '@floating-ui/react'; import { IconCheck, IconFolder, IconMinus, IconX } from '@tabler/icons-react'; import { ChangeEvent, DragEvent, FC, Fragment, KeyboardEvent, MouseEvent, MouseEventHandler, createElement, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { useTranslation } from 'next-i18next'; import classNames from 'classnames'; import { doesHaveDotsInTheEnd, hasInvalidNameInPath, isEntityNameInvalid, isEntityNameOnSameLevelUnique, prepareEntityName, } from '@/src/utils/app/common'; import { getEntityNameError } from '@/src/utils/app/errors'; import { notAllowedSymbolsRegex } from '@/src/utils/app/file'; import { getChildAndCurrentFoldersIdsById, getFoldersDepth, getParentFolderIdsFromFolderId, sortByName, } from '@/src/utils/app/folders'; import { getIdWithoutRootPathSegments, isEntityIdExternal, isRootId, } from '@/src/utils/app/id'; import { hasParentWithAttribute, hasParentWithFloatingOverlay, } from '@/src/utils/app/modals'; import { getDragImage, getEntityMoveType, getFolderMoveType, hasDragEventAnyData, } from '@/src/utils/app/move'; import { doesEntityContainSearchItem } from '@/src/utils/app/search'; import { getPublicItemIdWithoutVersion } from '@/src/utils/server/api'; import { Conversation } from '@/src/types/chat'; import { AdditionalItemData, FeatureType } from '@/src/types/common'; import { DialFile } from '@/src/types/files'; import { FolderInterface } from '@/src/types/folder'; import { PromptInfo } from '@/src/types/prompt'; import { SharingType } from '@/src/types/share'; import { Translation } from '@/src/types/translation'; import { ConversationsActions } from '@/src/store/conversations/conversations.reducers'; import { FilesActions } from '@/src/store/files/files.reducers'; import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; import { PublicationSelectors } from '@/src/store/publication/publication.reducers'; import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; import { ShareActions } from '@/src/store/share/share.reducers'; import { UIActions } from '@/src/store/ui/ui.reducers'; import SidebarActionButton from '@/src/components/Buttons/SidebarActionButton'; import CaretIconComponent from '@/src/components/Common/CaretIconComponent'; import { PublishModal } from '../Chat/Publish/PublishWizard'; import { ReviewDot } from '../Chat/Publish/ReviewDot'; import { ConfirmDialog } from '../Common/ConfirmDialog'; import { FolderContextMenu } from '../Common/FolderContextMenu'; import ShareIcon from '../Common/ShareIcon'; import { Spinner } from '../Common/Spinner'; import Tooltip from '../Common/Tooltip'; import { ConversationInfo, PublishActions, ShareEntity, UploadStatus, } from '@epam/ai-dial-shared'; export interface FolderProps<T, P = unknown> { currentFolder: FolderInterface; itemComponent?: FC<{ item: T; level: number; readonly?: boolean; additionalItemData?: AdditionalItemData; onEvent?: (eventId: string, data: P) => void; }>; allItems?: T[]; allFolders: FolderInterface[]; level?: number; highlightedFolders?: string[]; searchTerm: string; openedFoldersIds: string[]; isInitialRenameEnabled?: boolean; newAddedFolderId?: string; loadingFolderIds?: string[]; displayCaretAlways?: boolean; additionalItemData?: AdditionalItemData; handleDrop?: (e: DragEvent, folder: FolderInterface) => void; onRenameFolder?: (newName: string, folderId: string) => void; onDeleteFolder?: (folderId: string) => void; onSelectFolder?: (folderId: string, isSelected: boolean) => void; onAddFolder?: (parentFolderId: string) => void; onClickFolder?: (folderId: string) => void; featureType: FeatureType; onItemEvent?: (eventId: string, data: unknown) => void; readonly?: boolean; onFileUpload?: (parentFolderId: string) => void; maxDepth?: number; highlightTemporaryFolders?: boolean; withBorderHighlight?: boolean; allFoldersWithoutFilters?: FolderInterface[]; allItemsWithoutFilters?: T[]; folderClassName?: string; skipFolderRenameValidation?: boolean; noCaretIcon?: boolean; canSelectFolders?: boolean; isSelectAlwaysVisible?: boolean; showTooltip?: boolean; } const Folder = <T extends ConversationInfo | PromptInfo | DialFile>({ currentFolder, searchTerm, itemComponent, allItems, allItemsWithoutFilters = undefined, allFolders, allFoldersWithoutFilters = [], highlightedFolders, openedFoldersIds, level = 0, isInitialRenameEnabled = false, newAddedFolderId, loadingFolderIds = [], displayCaretAlways = false, additionalItemData, handleDrop, onRenameFolder, onDeleteFolder, onSelectFolder, onClickFolder, onAddFolder, onFileUpload, onItemEvent, featureType, readonly = false, maxDepth, highlightTemporaryFolders, withBorderHighlight = true, folderClassName, skipFolderRenameValidation = false, noCaretIcon = false, canSelectFolders = false, isSelectAlwaysVisible = false, showTooltip, }: FolderProps<T>) => { const { t } = useTranslation(Translation.Chat); const dispatch = useAppDispatch(); const checkboxRef = useRef<HTMLInputElement>(null); const renameInputRef = useRef<HTMLInputElement>(null); const [isDeletingConfirmDialog, setIsDeletingConfirmDialog] = useState(false); const [search, setSearch] = useState(searchTerm); const [isRenaming, setIsRenaming] = useState( isInitialRenameEnabled && newAddedFolderId === currentFolder.id && !currentFolder.serverSynced, ); const [renameValue, setRenameValue] = useState(currentFolder.name); const [isDraggingOver, setIsDraggingOver] = useState(false); const [isContextMenu, setIsContextMenu] = useState(false); const [isConfirmRenaming, setIsConfirmRenaming] = useState(false); const dragDropElement = useRef<HTMLDivElement>(null); const [isPublishing, setIsPublishing] = useState(false); const [isUnpublishing, setIsUnpublishing] = useState(false); const [isUnshareConfirmOpened, setIsUnshareConfirmOpened] = useState(false); const [isSelected, setIsSelected] = useState(false); const [isPartialSelected, setIsPartialSelected] = useState(false); const isPublishingEnabled = useAppSelector((state) => SettingsSelectors.selectIsPublishingEnabled(state, featureType), ); const hasResourcesToReview = useAppSelector((state) => PublicationSelectors.selectIsFolderContainsResourcesToReview( state, currentFolder.id, additionalItemData?.publicationUrl, ), ); const selectedPublicationUrl = useAppSelector( PublicationSelectors.selectSelectedPublicationUrl, ); const publicVersionGroups = useAppSelector( PublicationSelectors.selectPublicVersionGroups, ); const isNameInvalid = isEntityNameInvalid(currentFolder.name); const isInvalidPath = hasInvalidNameInPath(currentFolder.folderId); const isNameOrPathInvalid = isNameInvalid || isInvalidPath; const isExternal = isEntityIdExternal(currentFolder); const handleToggleFolder = useCallback( (e: ChangeEvent<HTMLInputElement>) => { e.stopPropagation(); onSelectFolder?.(`${currentFolder.id}/`, isSelected); setIsSelected((value) => !value); }, [currentFolder.id, isSelected, onSelectFolder], ); useEffect(() => { const parentFolderIds = getParentFolderIdsFromFolderId(currentFolder.id); const isParentSelected = parentFolderIds.some((id) => (additionalItemData?.selectedFolderIds ?? []).includes(`${id}/`), ); setIsSelected(isParentSelected); }, [additionalItemData?.selectedFolderIds, currentFolder.id, dispatch]); useEffect(() => { const currentId = `${currentFolder.id}/`; setIsPartialSelected( !isSelected && (additionalItemData?.partialSelectedFolderIds ?? []).includes( currentId, ), ); }, [ additionalItemData?.partialSelectedFolderIds, currentFolder.id, isSelected, ]); useEffect(() => { if (checkboxRef.current) { checkboxRef.current.indeterminate = isPartialSelected && !isSelected; } }, [isPartialSelected, isSelected]); useEffect(() => { // only if search term was changed after first render // to allow `isInitialRenameEnabled` be used if (search !== searchTerm) { setIsRenaming(false); } setSearch(searchTerm); }, [search, searchTerm]); useEffect(() => { if (isRenaming) { // focus manually because `autoFocus` doesn't work well with several items and rerender renameInputRef.current?.focus(); renameInputRef.current?.select(); } }, [isRenaming]); const handleShare: MouseEventHandler = useCallback( (e) => { e.stopPropagation(); dispatch( ShareActions.share({ resourceId: currentFolder.id, featureType, isFolder: true, }), ); }, [currentFolder.id, dispatch, featureType], ); const handleUnshare: MouseEventHandler = useCallback((e) => { e.stopPropagation(); setIsUnshareConfirmOpened(true); }, []); const allChildItems = useMemo(() => { const folderPath = `${currentFolder.id}/`; const sortedItems = sortByName( allItemsWithoutFilters?.filter((item) => item.id.startsWith(folderPath), ) || [], ); if (isUnpublishing) { return sortedItems.filter((item) => { const currentVersionGroupId = item.publicationInfo?.version ? getPublicItemIdWithoutVersion(item.publicationInfo.version, item.id) : null; if (currentVersionGroupId) { const selectedVersion = publicVersionGroups[currentVersionGroupId]?.selectedVersion; return selectedVersion && selectedVersion.id === item.id; } return false; }); } if (featureType !== FeatureType.Chat) { return sortedItems; } return (sortedItems as (ConversationInfo & Partial<Conversation>)[]).filter( (item) => item.isPlayback || (!item.isReplay && (item.messages?.length || !item.messages)), ); }, [ allItemsWithoutFilters, currentFolder.id, featureType, isUnpublishing, publicVersionGroups, ]); const handleOpenPublishing: MouseEventHandler = useCallback( (e) => { e.stopPropagation(); if ( featureType === FeatureType.Chat && (!allChildItems.length || !allChildItems.every((item) => (item as Conversation).messages)) ) { dispatch( ConversationsActions.uploadConversationsWithContentRecursive({ path: currentFolder.id, }), ); } setIsPublishing(true); }, [allChildItems, currentFolder.id, dispatch, featureType], ); const handleClosePublishModal = useCallback(() => { setIsPublishing(false); setIsUnpublishing(false); }, []); const handleOpenUnpublishing: MouseEventHandler = useCallback( (e) => { e.stopPropagation(); if (featureType === FeatureType.Chat) { dispatch( ConversationsActions.uploadConversationsWithContentRecursive({ path: currentFolder.id, }), ); } setIsUnpublishing(true); }, [currentFolder.id, dispatch, featureType], ); const isFolderOpened = useMemo(() => { return openedFoldersIds.includes(currentFolder.id); }, [openedFoldersIds, currentFolder.id]); const filteredChildFolders = useMemo(() => { return sortByName( allFolders.filter((folder) => folder.folderId === currentFolder.id), ); }, [currentFolder, allFolders]); const filteredChildItems = useMemo(() => { return sortByName( allItems?.filter( (item) => item.folderId === currentFolder.id && (!searchTerm || doesEntityContainSearchItem(item, searchTerm)), ) || [], ); }, [allItems, currentFolder.id, searchTerm]); const hasChildElements = useMemo(() => { return filteredChildFolders.length > 0 || filteredChildItems.length > 0; }, [filteredChildFolders.length, filteredChildItems.length]); const hasChildItemOnAnyLevel = useMemo(() => { const prefix = `${currentFolder.id}/`; return !!allItemsWithoutFilters?.some( (entity) => entity.folderId === currentFolder.id || entity.folderId.startsWith(prefix), ); }, [allItemsWithoutFilters, currentFolder.id]); const { refs, context } = useFloating({ open: isContextMenu, onOpenChange: setIsContextMenu, }); const dismiss = useDismiss(context); const { getFloatingProps } = useInteractions([dismiss]); const handleNewFolderRename = useCallback(() => { if (newAddedFolderId === currentFolder.id) { dispatch(FilesActions.resetNewFolderId()); } }, [newAddedFolderId, dispatch, currentFolder]); const handleRename = useCallback(() => { if (!onRenameFolder) { return; } const newName = prepareEntityName(renameValue, { forRenaming: true }); setRenameValue(newName); if (!skipFolderRenameValidation) { if ( !isEntityNameOnSameLevelUnique( newName, currentFolder, allFoldersWithoutFilters, ) ) { dispatch( UIActions.showErrorToast( t( 'Folder with name "{{folderName}}" already exists in this folder.', { ns: 'folder', folderName: newName, }, ), ), ); return; } if (doesHaveDotsInTheEnd(newName)) { dispatch( UIActions.showErrorToast( t('Using a dot at the end of a name is not permitted.'), ), ); return; } } if (currentFolder.isShared && newName !== currentFolder.name) { setIsConfirmRenaming(true); setIsRenaming(false); setIsContextMenu(false); return; } if (newName && newName !== currentFolder.name) { onRenameFolder(newName, currentFolder.id); } handleNewFolderRename(); setRenameValue(''); setIsRenaming(false); setIsContextMenu(false); }, [ onRenameFolder, renameValue, skipFolderRenameValidation, currentFolder, allFoldersWithoutFilters, handleNewFolderRename, dispatch, t, ]); const handleEnterDown = useCallback( (e: KeyboardEvent<HTMLDivElement>) => { e.stopPropagation(); if (e.key === 'Enter') { e.preventDefault(); handleRename(); } }, [handleRename], ); const dropHandler = useCallback( (e: DragEvent) => { if (!handleDrop || isExternal || canSelectFolders) { return; } if (e.dataTransfer) { e.preventDefault(); e.stopPropagation(); dispatch(UIActions.openFolder({ id: currentFolder.id, featureType })); setIsDraggingOver(false); const folderData = e.dataTransfer.getData( getFolderMoveType(featureType), ); if (folderData) { const draggedFolder = JSON.parse(folderData); if (draggedFolder.id === currentFolder.id) { return; } const childIds = new Set( getChildAndCurrentFoldersIdsById(draggedFolder.id, allFolders), ); if (childIds.has(currentFolder.id)) { dispatch( UIActions.showErrorToast( t("It's not allowed to move parent folder in child folder"), ), ); return; } const foldersDepth = getFoldersDepth(draggedFolder, allFolders); if (maxDepth && level + foldersDepth > maxDepth) { dispatch( UIActions.showErrorToast( t("It's not allowed to have more nested folders"), ), ); return; } if ( !isEntityNameOnSameLevelUnique( draggedFolder.name, { ...draggedFolder, folderId: currentFolder.id }, allFoldersWithoutFilters, ) ) { dispatch( UIActions.showErrorToast( t( 'Folder with name "{{folderName}}" already exists in this folder.', { ns: 'folder', folderName: draggedFolder.name, }, ), ), ); return; } } const entityData = e.dataTransfer.getData( getEntityMoveType(featureType), ); if (entityData) { const draggedEntity = JSON.parse(entityData); if ( !isEntityNameOnSameLevelUnique( draggedEntity.name, { ...draggedEntity, folderId: currentFolder.id }, allItemsWithoutFilters || [], ) ) { dispatch( UIActions.showErrorToast( t( '{{entityType}} with name "{{entityName}}" already exists in this folder.', { ns: 'common', entityType: featureType === FeatureType.Chat ? 'Conversation' : 'Prompt', entityName: draggedEntity.name, }, ), ), ); return; } } handleDrop(e, currentFolder); } }, [ allFolders, allFoldersWithoutFilters, allItemsWithoutFilters, canSelectFolders, currentFolder, dispatch, featureType, handleDrop, isExternal, level, maxDepth, t, ], ); const allowDrop = useCallback( (e: DragEvent) => { if ( !canSelectFolders && !isExternal && hasDragEventAnyData(e, featureType) ) { e.preventDefault(); } }, [canSelectFolders, featureType, isExternal], ); const isParentFolder = useCallback( (currentFolder: Element, checkedElement: Element) => { let isParentFolder = true; let parent = checkedElement.parentElement; while (parent) { if (parent.id === 'folder' && parent !== currentFolder) { isParentFolder = false; break; } if (currentFolder === parent) { break; } parent = parent.parentElement; } return isParentFolder; }, [], ); const highlightDrop = useCallback( (evt: DragEvent) => { if (isExternal || !hasDragEventAnyData(evt, featureType)) { return; } if (dragDropElement.current === evt.target) { setIsDraggingOver(true); return; } if ( dragDropElement.current?.contains(evt.target as Node) && isParentFolder(dragDropElement.current, evt.target as Element) ) { dispatch(UIActions.openFolder({ id: currentFolder.id, featureType })); setIsDraggingOver(true); } }, [currentFolder, dispatch, featureType, isExternal, isParentFolder], ); const deleteHighlight = useCallback( (evt: DragEvent) => { if (!dragDropElement.current?.contains(evt.relatedTarget as Node)) { setIsDraggingOver(false); return; } if ( !isParentFolder(dragDropElement.current, evt.relatedTarget as Element) ) { setIsDraggingOver(false); } }, [isParentFolder], ); const onRename: MouseEventHandler = useCallback( (e) => { if (!onRenameFolder) { return; } e.stopPropagation(); setIsRenaming(true); setRenameValue(currentFolder.name); // `setTimeout` because isRenaming should be applied to render input and only after that it can be focused setTimeout(() => renameInputRef.current?.focus()); }, [currentFolder.name, onRenameFolder], ); const onDelete: MouseEventHandler = useCallback( (e) => { if (!onDeleteFolder) { return; } e.stopPropagation(); setIsDeletingConfirmDialog(true); }, [onDeleteFolder], ); const onSelect: MouseEventHandler = useCallback( (e) => { if (!onSelectFolder) { return; } e.stopPropagation(); onSelectFolder(`${currentFolder.id}/`, isSelected); }, [currentFolder.id, isSelected, onSelectFolder], ); const onAdd: MouseEventHandler = useCallback( (e) => { if (!onAddFolder) { return; } e.stopPropagation(); if (maxDepth && level + 1 > maxDepth) { dispatch( UIActions.showErrorToast( t("It's not allowed to have more nested folders"), ), ); return; } onAddFolder(currentFolder.id); }, [currentFolder, dispatch, level, maxDepth, onAddFolder, t], ); const onUpload: MouseEventHandler = useCallback( (e) => { if (!onFileUpload) { return; } e.stopPropagation(); onFileUpload(currentFolder.id); }, [currentFolder.id, onFileUpload], ); const handleDragStart = useCallback( (e: DragEvent<HTMLDivElement>, folder: FolderInterface) => { if (e.dataTransfer && !isExternal) { e.dataTransfer.setDragImage(getDragImage(), 0, 0); e.dataTransfer.setData( getFolderMoveType(featureType), JSON.stringify(folder), ); dispatch(UIActions.closeFolder({ id: currentFolder.id, featureType })); } }, [currentFolder, dispatch, featureType, isExternal], ); const handleContextMenuOpen = (e: MouseEvent) => { if (hasParentWithFloatingOverlay(e.target as Element)) { return; } e.preventDefault(); e.stopPropagation(); setIsContextMenu(true); }; useEffect(() => { if (isRenaming) { setIsDeletingConfirmDialog(false); } else if (isDeletingConfirmDialog) { setIsRenaming(false); } }, [isRenaming, isDeletingConfirmDialog]); useEffect(() => { if (searchTerm) { dispatch(UIActions.openFolder({ id: currentFolder.id, featureType })); } }, [currentFolder, dispatch, featureType, searchTerm]); const isPartOfSelectedPublication = !additionalItemData?.publicationUrl || selectedPublicationUrl === additionalItemData?.publicationUrl; const isHighlighted = isRenaming || isContextMenu || ((additionalItemData?.selectedFolderIds ?? []).includes( `${currentFolder.id}/`, ) && !isSelectAlwaysVisible) || (allItems === undefined && highlightedFolders?.includes(currentFolder.id) && isPartOfSelectedPublication); const hideContextMenu = (canSelectFolders && featureType !== FeatureType.File) || readonly || isRenaming; const iconSize = additionalItemData?.isSidePanelItem ? 24 : 18; const folderIconStrokeWidth = additionalItemData?.isSidePanelItem ? 1.5 : 2; const isSidePanelItem = additionalItemData?.isSidePanelItem; return ( <div id="folder" className={classNames( isDraggingOver && 'bg-accent-primary-alpha', currentFolder.temporary && 'text-primary', )} onDrop={dropHandler} onDragOver={allowDrop} onDragEnter={highlightDrop} onDragLeave={deleteHighlight} onContextMenu={handleContextMenuOpen} ref={dragDropElement} > <div className={classNames( 'group/button group/folder-item group relative flex cursor-pointer items-center rounded border-l-2 hover:bg-accent-primary-alpha', (canSelectFolders || !withBorderHighlight) && 'border-transparent', isHighlighted ? 'bg-accent-primary-alpha' : 'border-transparent', !canSelectFolders && isHighlighted && withBorderHighlight && 'border-accent-primary', folderClassName, additionalItemData?.isSidePanelItem ? 'h-[34px]' : 'h-[30px]', )} data-qa="folder" aria-selected={isHighlighted} onClick={(e) => { if ( onClickFolder && !hasParentWithAttribute( e.target as HTMLDivElement, 'data-item-checkbox', ) ) { onClickFolder(currentFolder.id); } }} property={isRootId(currentFolder.folderId) ? 'root' : 'child'} draggable={ !!handleDrop && !isExternal && !isNameOrPathInvalid && !canSelectFolders } onDragStart={(e) => handleDragStart(e, currentFolder)} onDragOver={(e) => { if (!isExternal && hasDragEventAnyData(e, featureType)) { e.preventDefault(); } }} > {isRenaming ? ( <div className="flex w-full items-center gap-1 py-2 pr-3" style={{ paddingLeft: `${level * 1.5}rem`, }} data-qa="edit-container" > <CaretIconComponent isOpen={isFolderOpened} hidden={!hasChildElements && !displayCaretAlways} /> {loadingFolderIds.includes(currentFolder.id) && !hasChildElements ? ( <Spinner /> ) : ( <> {!isSelected && ( <ShareIcon {...currentFolder} isHighlighted featureType={featureType} containerClassName={classNames( (!isExternal || !additionalItemData?.isSidePanelItem) && canSelectFolders && 'group-hover:hidden', )} > {hasResourcesToReview && additionalItemData?.isSidePanelItem && additionalItemData?.publicationUrl && ( <ReviewDot className="group-hover:bg-accent-primary-alpha" /> )} <IconFolder strokeWidth={folderIconStrokeWidth} size={iconSize} className={classNames( 'mr-1 text-secondary', (!isExternal || !additionalItemData?.isSidePanelItem) && canSelectFolders && 'group-hover:hidden', )} /> </ShareIcon> )} {canSelectFolders && ((!isExternal && !loadingFolderIds.includes(currentFolder.id)) || !additionalItemData?.isSidePanelItem) && ( <div className={classNames( 'relative mr-1 group-hover/folder-item:flex', additionalItemData?.isSidePanelItem ? 'size-[24px] items-center justify-center' : 'size-[18px]', isSelected ? 'flex' : 'hidden', )} data-item-checkbox > <input className={classNames( 'checkbox peer size-[18px] bg-layer-3', additionalItemData?.isSidePanelItem && 'mr-0', )} type="checkbox" checked={isSelected} onChange={handleToggleFolder} /> <IconCheck size={18} className="pointer-events-none invisible absolute text-accent-primary peer-checked:visible" /> </div> )} </> )} <input className="mr-12 w-full flex-1 overflow-hidden text-ellipsis bg-transparent text-left outline-none" type="text" value={renameValue} onChange={(e) => setRenameValue( e.target.value.replaceAll(notAllowedSymbolsRegex, ''), ) } onKeyDown={handleEnterDown} ref={renameInputRef} name="edit-input" autoComplete="off" /> </div> ) : ( <div className="group/folder-item flex max-w-full items-center gap-1 py-2 pr-3" style={{ paddingLeft: `${level * (isSidePanelItem ? 30 : 24)}px`, }} > <CaretIconComponent isOpen={isFolderOpened} hidden={ (!hasChildElements && currentFolder.status === UploadStatus.LOADED && !displayCaretAlways) || noCaretIcon } /> {loadingFolderIds.includes(currentFolder.id) && !hasChildElements ? ( <Spinner className="mr-1" /> ) : ( <> {canSelectFolders && ((!isExternal && !loadingFolderIds.includes(currentFolder.id)) || !additionalItemData?.isSidePanelItem || isSelectAlwaysVisible) && ( <div className={classNames( 'relative mr-1 group-hover/folder-item:flex', additionalItemData?.isSidePanelItem ? 'size-[24px] items-center justify-center' : 'size-[18px]', isSelected || isPartialSelected || isSelectAlwaysVisible ? 'flex' : 'hidden', )} data-item-checkbox > <input className={classNames( 'checkbox peer size-[18px] bg-layer-3', additionalItemData?.isSidePanelItem && 'mr-0', )} type="checkbox" checked={isSelected} onChange={handleToggleFolder} ref={checkboxRef} data-qa={ isSelected ? 'checked' : isPartialSelected ? 'partiallyChecked' : 'unchecked' } /> {isSelected && ( <IconCheck size={18} className="pointer-events-none absolute text-accent-primary" /> )} {isPartialSelected && ( <IconMinus size={18} className="pointer-events-none absolute text-accent-primary" /> )} </div> )} {(isSelectAlwaysVisible || (!isSelected && !isPartialSelected)) && ( <ShareIcon {...currentFolder} isHighlighted={isContextMenu} featureType={featureType} containerClassName={ (!isExternal || !additionalItemData?.isSidePanelItem) && canSelectFolders && !isSelectAlwaysVisible ? 'group-hover/folder-item:hidden' : '' } > {hasResourcesToReview && additionalItemData?.isSidePanelItem && additionalItemData?.publicationUrl && ( <ReviewDot className="group-hover/folder-item:bg-accent-primary-alpha" /> )} <IconFolder strokeWidth={folderIconStrokeWidth} size={iconSize} className="mr-1 text-secondary" /> </ShareIcon> )} </> )} <div className={classNames( 'relative max-h-5 flex-1 truncate text-left', isNameOrPathInvalid && 'text-secondary', !hideContextMenu && 'group-hover/button:pr-5', )} data-qa="folder-name" > <Tooltip tooltip={ showTooltip && !isNameOrPathInvalid ? currentFolder.name : t( getEntityNameError( isNameInvalid, isInvalidPath, isExternal, ), ) } contentClassName="sm:max-w-[400px] max-w-[250px] break-all" triggerClassName={classNames( 'block max-h-5 flex-1 truncate whitespace-pre break-all text-left', highlightTemporaryFolders && (currentFolder.temporary ? 'text-primary' : 'text-secondary'), isNameOrPathInvalid ? 'text-secondary' : highlightedFolders?.includes(currentFolder.id) && isPartOfSelectedPublication && featureType && !canSelectFolders ? 'text-accent-primary' : 'text-primary', )} > {currentFolder.name} </Tooltip> </div> {(onDeleteFolder || onRenameFolder || onAddFolder || onSelectFolder) && !hideContextMenu && ( <div ref={refs.setFloating} {...getFloatingProps()} className={classNames( 'invisible absolute right-3 z-50 flex justify-end group-hover/button:visible', isContextMenu && 'max-md:visible', )} > <FolderContextMenu folder={currentFolder} featureType={featureType} onRename={ (onRenameFolder && !currentFolder.serverSynced && onRename) || undefined } onDelete={onDeleteFolder && onDelete} onAddFolder={onAddFolder && onAdd} onShare={handleShare} onUnshare={handleUnshare} onPublish={ featureType !== FeatureType.Chat || !allChildItems.every( (item) => (item as ConversationInfo).isReplay, ) ? handleOpenPublishing : undefined } onUnpublish={handleOpenUnpublishing} onPublishUpdate={handleOpenPublishing} onOpenChange={setIsContextMenu} onUpload={onFileUpload && onUpload} isOpen={isContextMenu} isEmpty={!hasChildItemOnAnyLevel} onSelect={onSelectFolder && onSelect} additionalItemData={additionalItemData} /> </div> )} </div> )} {isRenaming && ( <div className="absolute right-1 z-10 flex" data-qa="actions"> <SidebarActionButton handleClick={(e) => { e.stopPropagation(); if (isRenaming) { handleRename(); } }} dataQA="confirm-edit" > <IconCheck size={18} className="hover:text-accent-primary" /> </SidebarActionButton> <SidebarActionButton handleClick={(e) => { e.stopPropagation(); setIsRenaming(false); handleNewFolderRename(); }} dataQA="cancel-edit" > <IconX size={18} className="hover:text-accent-primary" /> </SidebarActionButton> </div> )} </div> {isFolderOpened ? ( <div className="flex flex-col gap-1"> <div className="flex flex-col"> {filteredChildFolders.map((item) => { return ( <Fragment key={item.id}> <div className="h-1"></div> <Folder folderClassName={folderClassName} noCaretIcon={noCaretIcon} readonly={readonly} level={level + 1} searchTerm={searchTerm} currentFolder={item} itemComponent={itemComponent} allItems={allItems} allItemsWithoutFilters={allItemsWithoutFilters} allFolders={allFolders} allFoldersWithoutFilters={allFoldersWithoutFilters} highlightedFolders={highlightedFolders} openedFoldersIds={openedFoldersIds} loadingFolderIds={loadingFolderIds} displayCaretAlways={displayCaretAlways} additionalItemData={additionalItemData} isInitialRenameEnabled={isInitialRenameEnabled} newAddedFolderId={newAddedFolderId} handleDrop={handleDrop} onRenameFolder={onRenameFolder} onFileUpload={onFileUpload} onDeleteFolder={onDeleteFolder} onAddFolder={onAddFolder} onClickFolder={onClickFolder} onItemEvent={onItemEvent} featureType={featureType} maxDepth={maxDepth} highlightTemporaryFolders={highlightTemporaryFolders} withBorderHighlight={withBorderHighlight} canSelectFolders={canSelectFolders} isSelectAlwaysVisible={isSelectAlwaysVisible} showTooltip={showTooltip} onSelectFolder={onSelectFolder} /> </Fragment> ); })} </div> {itemComponent && filteredChildItems.map((item) => ( <div key={item.id}> {createElement(itemComponent, { item, level: level + 1, readonly, additionalItemData, ...(!!onItemEvent && { onEvent: onItemEvent }), })} </div> ))} </div> ) : null} {onDeleteFolder && ( <ConfirmDialog isOpen={isDeletingConfirmDialog} heading={t('Confirm deleting folder')} description={`${t('Are you sure that you want to delete a folder with all nested elements?')}${t( currentFolder.isShared ? '\nDeleting will stop sharing and other users will no longer see this folder.' : '', )}`} confirmLabel={t('Delete')} cancelLabel={t('Cancel')} onClose={(result) => { setIsDeletingConfirmDialog(false); if (result) { onDeleteFolder(currentFolder.id); } }} /> )} {(isPublishing || isUnpublishing) && isPublishingEnabled && ( <PublishModal entity={currentFolder} entities={allChildItems as ShareEntity[]} type={ featureType === FeatureType.Prompt ? SharingType.PromptFolder : SharingType.ConversationFolder } isOpen={isPublishing || isUnpublishing} onClose={handleClosePublishModal} depth={getFoldersDepth(currentFolder, allFolders)} publishAction={ isPublishing ? PublishActions.ADD : PublishActions.DELETE } defaultPath={ isUnpublishing && !isRootId(currentFolder.folderId) ? getIdWithoutRootPathSegments(currentFolder.folderId) : undefined } /> )} {isUnshareConfirmOpened && ( <ConfirmDialog isOpen={isUnshareConfirmOpened} heading={t('Confirm unsharing: {{folderName}}', { folderName: currentFolder.name, })} description={ t('Are you sure that you want to unshare this folder?') || '' } confirmLabel={t('Unshare')} cancelLabel={t('Cancel')} onClose={(result) => { setIsUnshareConfirmOpened(false); if (result) { dispatch( ShareActions.revokeAccess({ resourceId: currentFolder.id, isFolder: true, featureType, }), ); } }} /> )} <ConfirmDialog isOpen={isConfirmRenaming} heading={t('Confirm renaming folder')} confirmLabel={t('Rename')} cancelLabel={t('Cancel')} description={ t( 'Renaming will stop sharing and other users will no longer see this folder.', ) || '' } onClose={(result) => { setIsConfirmRenaming(false); if (result) { const newName = prepareEntityName(renameValue); if (newName) { onRenameFolder!(newName, currentFolder.id); } setRenameValue(''); } }} /> </div> ); }; export default Folder;