import React, { useState, useMemo, useEffect, useRef } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import Modal from 'components/Modal'; import SearchInput from 'components/SearchInput'; import Button from 'ui/Button'; import { IconFolder, IconChevronRight, IconCheck, IconX, IconEye, IconEyeOff } from '@tabler/icons'; import filter from 'lodash/filter'; import toast from 'react-hot-toast'; import StyledWrapper from './StyledWrapper'; import useCollectionFolderTree from 'hooks/useCollectionFolderTree'; import { removeSaveTransientRequestModal, deleteRequestDraft } from 'providers/ReduxStore/slices/collections'; import { insertTaskIntoQueue } from 'providers/ReduxStore/slices/app'; import { newFolder, closeTabs } from 'providers/ReduxStore/slices/collections/actions'; import { sanitizeName, validateName, validateNameError } from 'utils/common/regex'; import { resolveRequestFilename } from 'utils/common/platform'; import path from 'utils/common/path'; import { transformRequestToSaveToFilesystem, findCollectionByUid, findItemInCollection } from 'utils/collections'; import { DEFAULT_COLLECTION_FORMAT } from 'utils/common/constants'; import { itemSchema } from '@usebruno/schema'; import { uuid } from 'utils/common'; import { formatIpcError } from 'utils/common/error'; const SaveTransientRequest = ({ item: itemProp, collection: collectionProp, isOpen = false, onClose }) => { const dispatch = useDispatch(); const latestCollection = useSelector((state) => collectionProp ? findCollectionByUid(state.collections.collections, collectionProp.uid) : null ); const latestItem = latestCollection && itemProp ? findItemInCollection(latestCollection, itemProp.uid) : itemProp; const item = itemProp; const collection = collectionProp; const handleClose = () => { if (onClose) { onClose(); return; } // Remove from Redux array dispatch(removeSaveTransientRequestModal({ itemUid: item.uid })); }; const [requestName, setRequestName] = useState(item?.name || ''); const [searchText, setSearchText] = useState(''); const [showNewFolderInput, setShowNewFolderInput] = useState(false); const [newFolderName, setNewFolderName] = useState(''); const [newFolderDirectoryName, setNewFolderDirectoryName] = useState(''); const [showFilesystemName, setShowFilesystemName] = useState(false); const [isEditingFolderFilename, setIsEditingFolderFilename] = useState(false); const [pendingFolderNavigation, setPendingFolderNavigation] = useState(null); const newFolderInputRef = useRef(null); const { currentFolders, breadcrumbs, selectedFolderUid, navigateIntoFolder, navigateToRoot, navigateToBreadcrumb, getCurrentParentFolder, getCurrentSelectedFolder, reset, isAtRoot } = useCollectionFolderTree(collection?.uid); const resetForm = () => { setRequestName(item.name || ''); setSearchText(''); reset(); setShowNewFolderInput(false); setNewFolderName(''); setNewFolderDirectoryName(''); setShowFilesystemName(false); setIsEditingFolderFilename(false); setPendingFolderNavigation(null); }; useEffect(() => { isOpen && item && resetForm(); }, [isOpen, item]); useEffect(() => { if (showNewFolderInput && newFolderInputRef.current) { newFolderInputRef.current.focus(); } }, [showNewFolderInput]); // Auto-navigate into newly created folder when it appears in currentFolders useEffect(() => { if (pendingFolderNavigation) { const newFolder = currentFolders.find((f) => f.filename === pendingFolderNavigation); if (newFolder) { navigateIntoFolder(newFolder.uid); setPendingFolderNavigation(null); } } }, [currentFolders, pendingFolderNavigation, navigateIntoFolder]); const filteredFolders = useMemo(() => { if (!searchText.trim()) { return currentFolders; } const searchLower = searchText.toLowerCase(); return filter(currentFolders, (folder) => folder.name.toLowerCase().includes(searchLower)); }, [currentFolders, searchText]); const handleCancel = () => { resetForm(); handleClose(); }; const handleConfirm = async () => { if (!item || !collection || !latestItem) { return; } try { const { ipcRenderer } = window; const selectedFolder = getCurrentSelectedFolder(); const targetDirname = selectedFolder ? selectedFolder.pathname : collection.pathname; const trimmedName = requestName.trim(); if (!trimmedName || trimmedName.length === 0) { toast.error('Request name is required'); return; } if (!validateName(trimmedName)) { toast.error(validateNameError(trimmedName)); return; } const sanitizedFilename = sanitizeName(trimmedName); const itemToSave = latestItem.draft ? { ...latestItem, ...latestItem.draft } : { ...latestItem }; itemToSave.name = sanitizedFilename; delete itemToSave.draft; const transformedItem = transformRequestToSaveToFilesystem(itemToSave); await itemSchema.validate(transformedItem); const format = collection.format || DEFAULT_COLLECTION_FORMAT; const targetFilename = resolveRequestFilename(sanitizedFilename, format); const targetPathname = path.join(targetDirname, targetFilename); await ipcRenderer.invoke('renderer:save-transient-request', { sourcePathname: item.pathname, targetDirname, targetFilename, request: transformedItem, format }); // Add task to open the newly saved request when file watcher detects it dispatch( insertTaskIntoQueue({ uid: uuid(), type: 'OPEN_REQUEST', collectionUid: collection.uid, itemPathname: targetPathname, preview: false }) ); dispatch(closeTabs({ tabUids: [item.uid] })); dispatch({ type: 'collections/deleteItem', payload: { itemUid: item.uid, collectionUid: collection.uid } }); toast.success('Request saved successfully'); handleClose(); } catch (err) { toast.error(formatIpcError(err) || 'Failed to save request'); console.error('Error saving request:', err); } }; const handleShowNewFolder = () => { setShowNewFolderInput(true); setNewFolderName(''); setNewFolderDirectoryName(''); setShowFilesystemName(false); setIsEditingFolderFilename(false); }; const handleCancelNewFolder = () => { setShowNewFolderInput(false); setNewFolderName(''); setNewFolderDirectoryName(''); setShowFilesystemName(false); setIsEditingFolderFilename(false); }; const handleNewFolderNameChange = (value) => { setNewFolderName(value); if (!isEditingFolderFilename) { setNewFolderDirectoryName(sanitizeName(value)); } }; const handleDirectoryNameChange = (value) => { setNewFolderDirectoryName(value); setIsEditingFolderFilename(true); }; const handleCreateNewFolder = async () => { const trimmedFolderName = newFolderName.trim(); if (!trimmedFolderName) { toast.error('Folder name is required'); return; } if (!validateName(trimmedFolderName)) { toast.error(validateNameError(trimmedFolderName)); return; } const directoryName = newFolderDirectoryName.trim() || sanitizeName(trimmedFolderName); const parentFolder = getCurrentParentFolder(); try { await dispatch(newFolder(trimmedFolderName, directoryName, collection?.uid, parentFolder?.uid)); toast.success('New folder created!'); // Set pending navigation - useEffect will navigate when folder appears in state setPendingFolderNavigation(directoryName); handleCancelNewFolder(); } catch (err) { const errorMessage = err?.message || 'An error occurred while adding the folder'; toast.error(errorMessage); } }; const handleFolderClick = (folderUid) => { navigateIntoFolder(folderUid); setSearchText(''); }; if (!isOpen) { return null; } return (
setRequestName(e.target.value)} autoFocus={true} onFocus={(e) => e.target.select()} />
Save to Collections
{collection && (
{collection.name} {breadcrumbs.length > 0 && ( <> {breadcrumbs.map((breadcrumb, index) => ( { e.stopPropagation(); navigateToBreadcrumb(index); setSearchText(''); }} > {breadcrumb.name} ))} )} {isAtRoot && }
)}
{filteredFolders.length > 0 || showNewFolderInput ? (
    {filteredFolders.map((folder) => (
  • handleFolderClick(folder.uid)} >
    {folder.name}
  • ))} {showNewFolderInput && (
  • handleNewFolderNameChange(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); handleCreateNewFolder(); } else if (e.key === 'Escape') { e.stopPropagation(); handleCancelNewFolder(); } }} />
    {showFilesystemName && (
    handleDirectoryNameChange(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); handleCreateNewFolder(); } else if (e.key === 'Escape') { e.stopPropagation(); handleCancelNewFolder(); } }} />
    )}
  • )}
) : (
{searchText.trim() ? 'No folders found' : 'No folders available'}
)}
{!showNewFolderInput && ( )}
); }; export default SaveTransientRequest;