mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
* fix: handle transient requests during app quit flow in SaveRequestsModal * test: non serial * chore: fix theme * fix: ui polish * chore: import * chore: cr
805 lines
33 KiB
JavaScript
805 lines
33 KiB
JavaScript
import React, { useState, useMemo, useEffect, useCallback } 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, IconEdit, IconArrowBackUp } from '@tabler/icons';
|
|
import PathDisplay from 'components/PathDisplay/index';
|
|
import Help from 'components/Help';
|
|
import filter from 'lodash/filter';
|
|
import toast from 'react-hot-toast';
|
|
import StyledWrapper from './StyledWrapper';
|
|
import CollectionListItem from './CollectionListItem';
|
|
import FolderBreadcrumbs from './FolderBreadcrumbs';
|
|
import useCollectionFolderTree from 'hooks/useCollectionFolderTree';
|
|
import { removeSaveTransientRequestModal } from 'providers/ReduxStore/slices/collections';
|
|
import { insertTaskIntoQueue } from 'providers/ReduxStore/slices/app';
|
|
import { newFolder, closeTabs, mountCollection, createCollection, browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
|
|
import { sanitizeName, validateName, validateNameError } from 'utils/common/regex';
|
|
import { resolveRequestFilename } from 'utils/common/platform';
|
|
import path, { normalizePath } from 'utils/common/path';
|
|
import { transformRequestToSaveToFilesystem, findCollectionByUid, findItemInCollection, areItemsLoading } 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';
|
|
import get from 'lodash/get';
|
|
|
|
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 { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
|
|
const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);
|
|
const allCollections = useSelector((state) => state.collections.collections);
|
|
const isScratchCollection = activeWorkspace?.scratchCollectionUid === collection?.uid;
|
|
const preferences = useSelector((state) => state.app.preferences);
|
|
const isDefaultWorkspace = activeWorkspace?.type === 'default';
|
|
const defaultCollectionLocation = isDefaultWorkspace
|
|
? get(preferences, 'general.defaultLocation', '')
|
|
: (activeWorkspace?.pathname ? path.join(activeWorkspace.pathname, 'collections') : '');
|
|
|
|
const availableCollections = useMemo(() => {
|
|
if (!isScratchCollection || !activeWorkspace) return [];
|
|
|
|
return (activeWorkspace.collections || []).map((wc) => {
|
|
const fullCollection = allCollections.find((c) => normalizePath(c.pathname) === normalizePath(wc.path));
|
|
// Use stable deterministic UID based on path to avoid duplicate Redux entries
|
|
const stableUid = wc.path ? `pending-${wc.path.replace(/[^a-zA-Z0-9]/g, '-')}` : uuid();
|
|
return fullCollection || { ...wc, uid: stableUid, mountStatus: 'unmounted' };
|
|
}).filter((c) => !workspaces.some((w) => w.scratchCollectionUid === c.uid));
|
|
}, [isScratchCollection, activeWorkspace, allCollections, workspaces]);
|
|
|
|
const handleClose = () => {
|
|
if (onClose) {
|
|
onClose();
|
|
return;
|
|
}
|
|
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);
|
|
|
|
// State for new collection creation
|
|
const [newCollection, setNewCollection] = useState({ show: false, name: '', location: '', format: DEFAULT_COLLECTION_FORMAT });
|
|
|
|
const [selectedTargetCollectionPath, setSelectedTargetCollectionPath] = useState(null);
|
|
const [isSelectingCollection, setIsSelectingCollection] = useState(isScratchCollection);
|
|
const folderTreeCollectionUid = selectedTargetCollectionPath
|
|
? availableCollections.find((c) => (c.path || c.pathname) === selectedTargetCollectionPath)?.uid
|
|
: collection?.uid;
|
|
|
|
const selectedTargetCollection = selectedTargetCollectionPath
|
|
? availableCollections.find((c) => (c.path || c.pathname) === selectedTargetCollectionPath)
|
|
: null;
|
|
|
|
useEffect(() => {
|
|
const isMounted = selectedTargetCollection?.mountStatus === 'mounted';
|
|
const isFullyLoaded = isMounted && !areItemsLoading(selectedTargetCollection);
|
|
if (selectedTargetCollectionPath && isFullyLoaded) {
|
|
setIsSelectingCollection(false);
|
|
}
|
|
}, [selectedTargetCollectionPath, selectedTargetCollection]);
|
|
|
|
const {
|
|
currentFolders,
|
|
breadcrumbs,
|
|
selectedFolderUid,
|
|
navigateIntoFolder,
|
|
navigateToRoot,
|
|
navigateToBreadcrumb,
|
|
getCurrentParentFolder,
|
|
getCurrentSelectedFolder,
|
|
reset,
|
|
isAtRoot
|
|
} = useCollectionFolderTree(folderTreeCollectionUid);
|
|
|
|
const resetForm = useCallback(() => {
|
|
setRequestName(item?.name || '');
|
|
setSearchText('');
|
|
reset();
|
|
setShowNewFolderInput(false);
|
|
setNewFolderName('');
|
|
setNewFolderDirectoryName('');
|
|
setShowFilesystemName(false);
|
|
setIsEditingFolderFilename(false);
|
|
setPendingFolderNavigation(null);
|
|
setSelectedTargetCollectionPath(null);
|
|
setIsSelectingCollection(isScratchCollection);
|
|
// Reset new collection state
|
|
setNewCollection({ show: false, name: '', location: '', format: DEFAULT_COLLECTION_FORMAT });
|
|
}, [item?.name, isScratchCollection, reset]);
|
|
|
|
useEffect(() => {
|
|
if (isOpen && item) {
|
|
resetForm();
|
|
}
|
|
}, [isOpen, item, resetForm]);
|
|
|
|
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 handleSelectCollection = useCallback((selectedCollection) => {
|
|
const collectionPath = selectedCollection.path || selectedCollection.pathname;
|
|
const isMounted = selectedCollection.mountStatus === 'mounted';
|
|
const isFullyLoaded = isMounted && !areItemsLoading(selectedCollection);
|
|
|
|
setSelectedTargetCollectionPath(collectionPath);
|
|
|
|
if (isFullyLoaded) {
|
|
setIsSelectingCollection(false);
|
|
return;
|
|
}
|
|
|
|
if (!isMounted && selectedCollection.mountStatus !== 'mounting') {
|
|
dispatch(
|
|
mountCollection({
|
|
collectionUid: selectedCollection.uid || uuid(),
|
|
collectionPathname: collectionPath,
|
|
brunoConfig: selectedCollection.brunoConfig
|
|
})
|
|
);
|
|
}
|
|
}, [dispatch]);
|
|
|
|
const handleConfirm = async () => {
|
|
if (!item || !collection || !latestItem) {
|
|
return;
|
|
}
|
|
|
|
const targetCollection = selectedTargetCollection || collection;
|
|
|
|
try {
|
|
const { ipcRenderer } = window;
|
|
|
|
const selectedFolder = getCurrentSelectedFolder();
|
|
const targetDirname = selectedFolder ? selectedFolder.pathname : targetCollection.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 targetFormat = targetCollection.format || DEFAULT_COLLECTION_FORMAT;
|
|
const sourceFormat = collection.format || DEFAULT_COLLECTION_FORMAT;
|
|
const targetFilename = resolveRequestFilename(sanitizedFilename, targetFormat);
|
|
const targetPathname = path.join(targetDirname, targetFilename);
|
|
|
|
await ipcRenderer.invoke('renderer:save-transient-request', {
|
|
sourcePathname: item.pathname,
|
|
targetDirname,
|
|
targetFilename,
|
|
request: transformedItem,
|
|
format: targetFormat,
|
|
sourceFormat
|
|
});
|
|
|
|
dispatch(
|
|
insertTaskIntoQueue({
|
|
uid: uuid(),
|
|
type: 'OPEN_REQUEST',
|
|
collectionUid: targetCollection.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 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();
|
|
const targetCollectionUid = selectedTargetCollection?.uid || collection?.uid;
|
|
|
|
try {
|
|
await dispatch(newFolder(trimmedFolderName, directoryName, targetCollectionUid, parentFolder?.uid));
|
|
toast.success('New folder created!');
|
|
|
|
setPendingFolderNavigation(directoryName);
|
|
handleCancelNewFolder();
|
|
} catch (err) {
|
|
const errorMessage = err?.message || 'An error occurred while adding the folder';
|
|
toast.error(errorMessage);
|
|
}
|
|
};
|
|
|
|
// New Collection handlers
|
|
const handleShowNewCollection = () => {
|
|
setNewCollection({ show: true, name: '', location: defaultCollectionLocation, format: DEFAULT_COLLECTION_FORMAT });
|
|
};
|
|
|
|
const handleCancelNewCollection = () => {
|
|
setNewCollection({ show: false, name: '', location: '', format: DEFAULT_COLLECTION_FORMAT });
|
|
};
|
|
|
|
const handleBrowseCollectionLocation = () => {
|
|
dispatch(browseDirectory())
|
|
.then((dirPath) => {
|
|
if (typeof dirPath === 'string') {
|
|
setNewCollection((prev) => ({ ...prev, location: dirPath }));
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
};
|
|
|
|
const handleCreateNewCollection = async () => {
|
|
const trimmedName = newCollection.name.trim();
|
|
if (!trimmedName) {
|
|
toast.error('Collection name is required');
|
|
return;
|
|
}
|
|
if (!validateName(trimmedName)) {
|
|
toast.error(validateNameError(trimmedName));
|
|
return;
|
|
}
|
|
if (!newCollection.location) {
|
|
toast.error('Location is required');
|
|
return;
|
|
}
|
|
try {
|
|
await dispatch(createCollection(trimmedName, sanitizeName(trimmedName), newCollection.location, { format: newCollection.format }));
|
|
toast.success('Collection created!');
|
|
handleCancelNewCollection();
|
|
} catch (err) {
|
|
toast.error(err?.message || 'An error occurred while creating the collection');
|
|
}
|
|
};
|
|
|
|
const handleFolderClick = (folderUid) => {
|
|
navigateIntoFolder(folderUid);
|
|
setSearchText('');
|
|
};
|
|
|
|
const handleBreadcrumbNavigate = useCallback((index) => {
|
|
navigateToBreadcrumb(index);
|
|
setSearchText('');
|
|
}, [navigateToBreadcrumb]);
|
|
|
|
if (!isOpen) {
|
|
return null;
|
|
}
|
|
|
|
const showNewFolderFooterButton = !showNewFolderInput && !isSelectingCollection && (filteredFolders.length > 0 && !searchText.trim());
|
|
|
|
return (
|
|
<StyledWrapper>
|
|
<Modal
|
|
size="sm"
|
|
title={isSelectingCollection ? 'Select Collection' : 'Save Request'}
|
|
handleCancel={handleCancel}
|
|
handleConfirm={handleConfirm}
|
|
confirmText="Save"
|
|
cancelText="Cancel"
|
|
hideFooter={true}
|
|
>
|
|
<div className="save-request-form">
|
|
<div className="form-section">
|
|
<label htmlFor="request-name" className="form-label">
|
|
Request name
|
|
</label>
|
|
<input
|
|
id="request-name"
|
|
type="text"
|
|
className="form-input textbox"
|
|
autoComplete="off"
|
|
autoCorrect="off"
|
|
autoCapitalize="off"
|
|
spellCheck="false"
|
|
value={requestName}
|
|
onChange={(e) => setRequestName(e.target.value)}
|
|
autoFocus={!isSelectingCollection}
|
|
onFocus={(e) => e.target.select()}
|
|
/>
|
|
</div>
|
|
|
|
<div className="collections-section">
|
|
<div className="collections-label">
|
|
{isSelectingCollection ? 'Select a collection to save to' : 'Save to Collections'}
|
|
</div>
|
|
|
|
{isScratchCollection && (
|
|
<div className="collection-name">
|
|
<span
|
|
className={isSelectingCollection ? '' : 'collection-name-breadcrumb'}
|
|
onClick={!isSelectingCollection ? () => {
|
|
setIsSelectingCollection(true);
|
|
setSelectedTargetCollectionPath(null);
|
|
reset();
|
|
} : undefined}
|
|
>
|
|
Collections
|
|
</span>
|
|
{!isSelectingCollection && (
|
|
<>
|
|
<IconChevronRight size={16} strokeWidth={1.5} className="collection-name-chevron" />
|
|
<FolderBreadcrumbs
|
|
collectionName={(selectedTargetCollection || collection).name}
|
|
breadcrumbs={breadcrumbs}
|
|
isAtRoot={isAtRoot}
|
|
onNavigateToRoot={navigateToRoot}
|
|
onNavigateToBreadcrumb={handleBreadcrumbNavigate}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{isSelectingCollection ? (
|
|
<div className="collection-list">
|
|
{availableCollections.length > 0 || newCollection.show ? (
|
|
<ul className="collection-list-items">
|
|
{availableCollections.map((coll) => {
|
|
const collPath = coll.path || coll.pathname;
|
|
return (
|
|
<CollectionListItem
|
|
key={collPath}
|
|
collectionUid={coll.uid}
|
|
collectionPath={collPath}
|
|
collectionName={coll.name}
|
|
isSelected={selectedTargetCollectionPath === collPath}
|
|
onSelect={() => handleSelectCollection(coll)}
|
|
/>
|
|
);
|
|
})}
|
|
{newCollection.show && (
|
|
<li className="new-collection-item">
|
|
<div className="new-collection-field">
|
|
<label className="new-collection-label">
|
|
Collection name
|
|
</label>
|
|
<input
|
|
ref={(node) => node?.focus()}
|
|
type="text"
|
|
className="new-collection-input"
|
|
placeholder="Enter collection name"
|
|
value={newCollection.name}
|
|
onChange={(e) => setNewCollection((prev) => ({ ...prev, name: e.target.value }))}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
handleCreateNewCollection();
|
|
} else if (e.key === 'Escape') {
|
|
e.stopPropagation();
|
|
handleCancelNewCollection();
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div className="new-collection-field">
|
|
<label className="new-collection-label flex items-center">
|
|
Location
|
|
<Help width={250} placement="top">
|
|
<p>
|
|
Bruno stores your collections on your computer's filesystem.
|
|
</p>
|
|
<p className="mt-2">
|
|
Choose the location where you want to store this collection.
|
|
</p>
|
|
</Help>
|
|
</label>
|
|
<div className="new-collection-location-row">
|
|
<input
|
|
type="text"
|
|
className="new-collection-input cursor-pointer"
|
|
placeholder="Select location"
|
|
value={newCollection.location}
|
|
readOnly
|
|
onClick={handleBrowseCollectionLocation}
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
color="secondary"
|
|
size="sm"
|
|
rounded="sm"
|
|
onClick={handleBrowseCollectionLocation}
|
|
>
|
|
Browse
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="new-collection-field">
|
|
<label className="new-collection-label flex items-center">
|
|
File Format
|
|
<Help width={300} placement="top">
|
|
<p>
|
|
Choose the file format for storing requests in this collection.
|
|
</p>
|
|
<p className="mt-2">
|
|
<strong>OpenCollection (YAML):</strong> Industry-standard YAML format (.yml files)
|
|
</p>
|
|
<p className="mt-1">
|
|
<strong>BRU:</strong> Bruno's native file format (.bru files)
|
|
</p>
|
|
</Help>
|
|
</label>
|
|
<select
|
|
className="new-collection-select"
|
|
value={newCollection.format}
|
|
onChange={(e) => setNewCollection((prev) => ({ ...prev, format: e.target.value }))}
|
|
>
|
|
<option value="yml">OpenCollection (YAML)</option>
|
|
<option value="bru">BRU Format (.bru)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="new-collection-actions-footer">
|
|
<Button
|
|
type="button"
|
|
color="secondary"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleCancelNewCollection}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
color="primary"
|
|
size="sm"
|
|
onClick={handleCreateNewCollection}
|
|
>
|
|
Create
|
|
</Button>
|
|
</div>
|
|
</li>
|
|
)}
|
|
</ul>
|
|
) : (
|
|
<div className="collection-empty-state">
|
|
<p>No Collections Yet</p>
|
|
<p className="collection-empty-state-subtitle">Collections help you organize your requests. Create your first one to save this request.</p>
|
|
<Button
|
|
type="button"
|
|
color="primary"
|
|
variant="outline"
|
|
icon={<IconFolder size={16} strokeWidth={1.5} />}
|
|
onClick={handleShowNewCollection}
|
|
className="mt-4"
|
|
>
|
|
New collection
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<>
|
|
{!isScratchCollection && (selectedTargetCollection || collection) && (
|
|
<div className="collection-name">
|
|
<FolderBreadcrumbs
|
|
collectionName={(selectedTargetCollection || collection).name}
|
|
breadcrumbs={breadcrumbs}
|
|
isAtRoot={isAtRoot}
|
|
onNavigateToRoot={navigateToRoot}
|
|
onNavigateToBreadcrumb={handleBreadcrumbNavigate}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="search-container">
|
|
<SearchInput
|
|
searchText={searchText}
|
|
setSearchText={setSearchText}
|
|
placeholder="Search for folder"
|
|
autoFocus={false}
|
|
/>
|
|
</div>
|
|
|
|
<div className="folder-list">
|
|
{filteredFolders.length > 0 || showNewFolderInput ? (
|
|
<ul className="folder-list-items">
|
|
{filteredFolders.map((folder) => (
|
|
<li
|
|
key={folder.uid}
|
|
className={`folder-item ${selectedFolderUid === folder.uid ? 'selected' : ''}`}
|
|
onClick={() => handleFolderClick(folder.uid)}
|
|
>
|
|
<div className="folder-item-content">
|
|
<IconFolder size={16} strokeWidth={1.5} />
|
|
<span className="folder-item-name">{folder.name}</span>
|
|
</div>
|
|
<IconChevronRight size={16} strokeWidth={1.5} />
|
|
</li>
|
|
))}
|
|
{showNewFolderInput && (
|
|
<li className="new-folder-item">
|
|
<div className="new-folder-header">
|
|
<IconFolder size={16} strokeWidth={1.5} />
|
|
<label className="new-folder-header-label">
|
|
{showFilesystemName ? 'New Folder name (in bruno)' : 'New Folder name'}
|
|
</label>
|
|
</div>
|
|
<div className="new-folder-input-row">
|
|
<input
|
|
ref={(node) => node?.focus()}
|
|
type="text"
|
|
className="new-folder-input"
|
|
placeholder="Untitled new folder"
|
|
value={newFolderName}
|
|
onChange={(e) => handleNewFolderNameChange(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
handleCreateNewFolder();
|
|
} else if (e.key === 'Escape') {
|
|
e.stopPropagation();
|
|
handleCancelNewFolder();
|
|
}
|
|
}}
|
|
/>
|
|
<div className="new-folder-actions">
|
|
<button
|
|
type="button"
|
|
className="new-folder-action-btn"
|
|
onClick={handleCancelNewFolder}
|
|
title="Cancel"
|
|
>
|
|
<IconX size={16} strokeWidth={1.5} />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="new-folder-action-btn"
|
|
onClick={handleCreateNewFolder}
|
|
title="Create folder"
|
|
>
|
|
<IconCheck size={16} strokeWidth={1.5} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{showFilesystemName && (
|
|
<div className="new-folder-filesystem-wrapper">
|
|
<div className="flex items-center justify-between">
|
|
<label className="new-folder-filesystem-label flex items-center font-medium">
|
|
Folder Name <small className="font-normal text-muted ml-1">(on filesystem)</small>
|
|
<Help width={300} placement="top">
|
|
<p>
|
|
You can choose to save the folder as a different name on your file system versus what is displayed in the app.
|
|
</p>
|
|
</Help>
|
|
</label>
|
|
{isEditingFolderFilename ? (
|
|
<IconArrowBackUp
|
|
className="cursor-pointer opacity-50 hover:opacity-80"
|
|
size={16}
|
|
strokeWidth={1.5}
|
|
onClick={() => setIsEditingFolderFilename(false)}
|
|
/>
|
|
) : (
|
|
<IconEdit
|
|
className="cursor-pointer opacity-50 hover:opacity-80"
|
|
size={16}
|
|
strokeWidth={1.5}
|
|
onClick={() => setIsEditingFolderFilename(true)}
|
|
/>
|
|
)}
|
|
</div>
|
|
{isEditingFolderFilename ? (
|
|
<div className="relative flex flex-row gap-1 items-center justify-between">
|
|
<input
|
|
type="text"
|
|
className="block textbox mt-2 w-full"
|
|
placeholder="Folder Name"
|
|
value={newFolderDirectoryName}
|
|
autoComplete="off"
|
|
autoCorrect="off"
|
|
autoCapitalize="off"
|
|
spellCheck="false"
|
|
onChange={(e) => setNewFolderDirectoryName(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
handleCreateNewFolder();
|
|
} else if (e.key === 'Escape') {
|
|
e.stopPropagation();
|
|
handleCancelNewFolder();
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="relative flex flex-row gap-1 items-center justify-between">
|
|
<PathDisplay
|
|
iconType="folder"
|
|
baseName={newFolderDirectoryName}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
type="button"
|
|
className="new-folder-toggle-filesystem-btn"
|
|
onClick={() => {
|
|
setShowFilesystemName(!showFilesystemName);
|
|
setNewFolderDirectoryName(sanitizeName(newFolderName));
|
|
setIsEditingFolderFilename(false);
|
|
}}
|
|
>
|
|
{showFilesystemName ? (
|
|
<>
|
|
<IconEyeOff size={16} strokeWidth={1.5} />
|
|
<span>Hide filesystem name</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<IconEye size={16} strokeWidth={1.5} />
|
|
<span>Show filesystem name</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</li>
|
|
)}
|
|
</ul>
|
|
) : (
|
|
<div className="folder-empty-state">
|
|
<div className="flex flex-col items-center">
|
|
<span>
|
|
{searchText.trim() ? 'No folders found' : 'No folders available' }
|
|
</span>
|
|
<Button
|
|
type="button"
|
|
color="primary"
|
|
variant="ghost"
|
|
icon={<IconFolder size={16} strokeWidth={1.5} />}
|
|
onClick={handleShowNewFolder}
|
|
>
|
|
New Folder
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="custom-modal-footer">
|
|
<div className="footer-left">
|
|
{showNewFolderFooterButton && (
|
|
<Button
|
|
type="button"
|
|
color="primary"
|
|
variant="ghost"
|
|
icon={<IconFolder size={16} strokeWidth={1.5} />}
|
|
onClick={handleShowNewFolder}
|
|
>
|
|
New Folder
|
|
</Button>
|
|
)}
|
|
{isSelectingCollection && !newCollection.show && availableCollections.length > 0 && (
|
|
<Button
|
|
type="button"
|
|
color="primary"
|
|
variant="ghost"
|
|
icon={<IconFolder size={16} strokeWidth={1.5} />}
|
|
onClick={handleShowNewCollection}
|
|
>
|
|
New collection
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<div className="footer-right">
|
|
<Button type="button" color="secondary" variant="ghost" onClick={handleCancel}>
|
|
Cancel
|
|
</Button>
|
|
{!isSelectingCollection && (
|
|
<Button type="button" color="primary" onClick={handleConfirm}>
|
|
Save
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
</StyledWrapper>
|
|
);
|
|
};
|
|
|
|
export default SaveTransientRequest;
|