Files
bruno/packages/bruno-app/src/components/Sidebar/Sections/CollectionsSection/index.js

430 lines
13 KiB
JavaScript

import { useState, useMemo } from 'react';
import toast from 'react-hot-toast';
import get from 'lodash/get';
import { useDispatch, useSelector } from 'react-redux';
import {
IconArrowsSort,
IconDotsVertical,
IconDownload,
IconFolder,
IconPlus,
IconSearch,
IconSortAscendingLetters,
IconSortDescendingLetters,
IconSquareX,
IconBox,
IconTerminal2
} from '@tabler/icons';
import { importCollection, openCollection, importCollectionFromZip, newHttpRequest } from 'providers/ReduxStore/slices/collections/actions';
import { sortCollections } from 'providers/ReduxStore/slices/collections/index';
import { savePreferences, setIsCreatingCollection, toggleSidebarSearch } from 'providers/ReduxStore/slices/app';
import { normalizePath } from 'utils/common/path';
import { isScratchCollection, flattenItems, isItemTransientRequest } from 'utils/collections';
import { sanitizeName } from 'utils/common/regex';
import filter from 'lodash/filter';
import MenuDropdown from 'ui/MenuDropdown';
import ActionIcon from 'ui/ActionIcon';
import ImportCollection from 'components/Sidebar/ImportCollection';
import ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';
import BulkImportCollectionLocation from 'components/Sidebar/BulkImportCollectionLocation';
import CloneGitRepository from 'components/Sidebar/CloneGitRespository';
import RemoveCollectionsModal from 'components/Sidebar/Collections/RemoveCollectionsModal/index';
import CreateCollection from 'components/Sidebar/CreateCollection';
import PostmanPackageReport from 'components/Sidebar/PostmanPackageReport';
import usePostmanPackagePrompt from 'hooks/usePostmanPackagePrompt';
import WelcomeModal from 'components/WelcomeModal';
import Collections from 'components/Sidebar/Collections';
import SidebarSection from 'components/Sidebar/SidebarSection';
import { openDevtoolsAndSwitchToTerminal } from 'utils/terminal';
import useKeybinding from 'hooks/useKeybinding';
const CollectionsSection = () => {
const dispatch = useDispatch();
const showSearch = useSelector((state) => state.app.showSidebarSearch);
const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);
const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);
const { collections } = useSelector((state) => state.collections);
const { collectionSortOrder } = useSelector((state) => state.collections);
const { isCreatingCollection } = useSelector((state) => state.app);
const preferences = useSelector((state) => state.app.preferences);
const [collectionsToClose, setCollectionsToClose] = useState([]);
const [importData, setImportData] = useState(null);
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
const [advancedCreateName, setAdvancedCreateName] = useState('');
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
const [showCloneGitModal, setShowCloneGitModal] = useState(false);
const [gitRepositoryUrl, setGitRepositoryUrl] = useState(null);
const { postmanPackagePrompt, clearPostmanPackagePrompt, handleImportResolved } = usePostmanPackagePrompt();
// Import collection shortcut
useKeybinding('importCollection', () => {
setImportCollectionModalOpen(true);
return false;
});
// Default to true (don't show modal) so that:
// 1. Existing users who upgrade (no hasSeenWelcomeModal in their prefs) don't see it
// 2. The modal doesn't flash before preferences are loaded from the electron process
// Only genuinely new users will have hasSeenWelcomeModal explicitly set to false by onboarding
const hasSeenWelcomeModal = get(preferences, 'onboarding.hasSeenWelcomeModal', true);
const showWelcomeModal = !hasSeenWelcomeModal;
const handleDismissWelcomeModal = () => {
const updatedPreferences = {
...preferences,
onboarding: {
...preferences.onboarding,
hasSeenWelcomeModal: true
}
};
dispatch(savePreferences(updatedPreferences)).catch(() => {
toast.error('Failed to save preferences');
});
};
const workspaceCollections = useMemo(() => {
if (!activeWorkspace) return [];
return collections.filter((c) => {
if (isScratchCollection(c, workspaces)) {
return false;
}
return activeWorkspace.collections?.some((wc) => normalizePath(wc.path) === normalizePath(c.pathname));
});
}, [activeWorkspace, collections, workspaces]);
const handleImportCollection = ({ rawData, type, repositoryUrl, ...rest }) => {
setImportCollectionModalOpen(false);
if (type === 'git-repository') {
setGitRepositoryUrl(repositoryUrl);
setShowCloneGitModal(true);
return;
}
setImportData({ rawData, type, ...rest });
setImportCollectionLocationModalOpen(true);
};
const handleImportCollectionLocation = (convertedCollection, collectionLocation, options = {}) => {
const importAction = options.isZipImport
? importCollectionFromZip(convertedCollection.zipFilePath, collectionLocation)
: importCollection(convertedCollection, collectionLocation, options);
dispatch(importAction)
.then((importedItem) => {
setImportCollectionLocationModalOpen(false);
setImportData(null);
handleImportResolved(convertedCollection, importedItem);
});
};
const handleCloseGitModal = () => {
setShowCloneGitModal(false);
setGitRepositoryUrl(null);
};
const handleToggleSearch = () => {
dispatch(toggleSidebarSearch());
};
const handleSortCollections = () => {
let order;
switch (collectionSortOrder) {
case 'default':
order = 'alphabetical';
break;
case 'alphabetical':
order = 'reverseAlphabetical';
break;
case 'reverseAlphabetical':
order = 'default';
break;
default:
order = 'default';
break;
}
dispatch(sortCollections({ order }));
};
const getSortIcon = () => {
switch (collectionSortOrder) {
case 'alphabetical':
return IconSortDescendingLetters;
case 'reverseAlphabetical':
return IconArrowsSort;
default:
return IconSortAscendingLetters;
}
};
const getSortLabel = () => {
switch (collectionSortOrder) {
case 'alphabetical':
return 'Sort Z-A';
case 'reverseAlphabetical':
return 'Clear sort';
default:
return 'Sort A-Z';
}
};
const selectAllCollectionsToClose = () => {
setCollectionsToClose(workspaceCollections.map((c) => c.uid));
};
const clearCollectionsToClose = () => {
setCollectionsToClose([]);
};
const handleOpenCollection = () => {
const options = {};
if (activeWorkspace?.pathname) {
options.workspaceId = activeWorkspace.pathname;
}
dispatch(openCollection(options)).catch((err) => {
toast.error('An error occurred while opening the collection');
});
};
const handleStartRequest = () => {
const scratchCollectionUid = activeWorkspace?.scratchCollectionUid;
if (!scratchCollectionUid) {
toast.error('Unable to create request');
return;
}
const scratchCollection = collections.find((c) => c.uid === scratchCollectionUid);
if (!scratchCollection) {
toast.error('Unable to create request');
return;
}
const allItems = flattenItems(scratchCollection.items || []);
const transientRequests = filter(allItems, (item) => isItemTransientRequest(item));
let maxNumber = 0;
transientRequests.forEach((item) => {
const match = item.name?.match(/^Untitled (\d+)$/);
if (match) {
const number = parseInt(match[1], 10);
if (number > maxNumber) {
maxNumber = number;
}
}
});
const requestName = `Untitled ${maxNumber + 1}`;
const filename = sanitizeName(requestName);
dispatch(
newHttpRequest({
requestName,
filename,
requestType: 'http-request',
requestUrl: '',
requestMethod: 'GET',
collectionUid: scratchCollectionUid,
itemUid: null,
isTransient: true
})
).catch((err) => {
toast.error('An error occurred while creating the request');
});
};
const handleOpenAdvancedCreate = (name) => {
dispatch(setIsCreatingCollection(false));
setAdvancedCreateName(name || '');
setCreateCollectionModalOpen(true);
};
const addDropdownItems = [
{
id: 'create',
leftSection: IconPlus,
label: 'Create collection',
onClick: () => {
dispatch(setIsCreatingCollection(true));
}
},
{
id: 'open',
leftSection: IconFolder,
label: 'Open collection',
onClick: () => {
handleOpenCollection();
}
},
{
id: 'import',
leftSection: IconDownload,
label: 'Import collection',
onClick: () => {
setImportCollectionModalOpen(true);
}
}
];
const actionsDropdownItems = [
{
id: 'sort',
leftSection: getSortIcon(),
label: getSortLabel(),
onClick: () => {
handleSortCollections();
}
},
{
id: 'close-all',
leftSection: IconSquareX,
label: 'Close all',
onClick: () => {
selectAllCollectionsToClose();
}
},
{
id: 'open-in-terminal',
leftSection: IconTerminal2,
label: 'Open in Terminal',
onClick: () => {
openDevtoolsAndSwitchToTerminal(dispatch, activeWorkspace?.pathname);
}
}
];
const sectionActions = (
<>
<ActionIcon
onClick={handleToggleSearch}
label="Search requests"
>
<IconSearch size={14} stroke={1.5} aria-hidden="true" />
</ActionIcon>
<MenuDropdown
data-testid="collections-header-add-menu"
items={addDropdownItems}
placement="bottom-end"
>
<ActionIcon
label="Add new collection"
>
<IconPlus size={14} stroke={1.5} aria-hidden="true" />
</ActionIcon>
</MenuDropdown>
<MenuDropdown
data-testid="collections-header-actions-menu"
items={actionsDropdownItems}
placement="bottom-end"
>
<ActionIcon
label="More actions"
>
<IconDotsVertical size={14} stroke={1.5} aria-hidden="true" />
</ActionIcon>
</MenuDropdown>
{collectionsToClose.length > 0 && (
<RemoveCollectionsModal collectionUids={collectionsToClose} onClose={clearCollectionsToClose} />
)}
</>
);
return (
<>
{showWelcomeModal && (
<WelcomeModal
onDismiss={handleDismissWelcomeModal}
onImportCollection={() => {
handleDismissWelcomeModal();
setImportCollectionModalOpen(true);
}}
onCreateCollection={() => {
handleDismissWelcomeModal();
setCreateCollectionModalOpen(true);
}}
onOpenCollection={() => {
handleDismissWelcomeModal();
handleOpenCollection();
}}
onStartRequest={() => {
handleDismissWelcomeModal();
handleStartRequest();
}}
/>
)}
{createCollectionModalOpen && (
<CreateCollection
onClose={() => {
setCreateCollectionModalOpen(false);
setAdvancedCreateName('');
}}
initialCollectionName={advancedCreateName}
/>
)}
{importCollectionModalOpen && (
<ImportCollection
onClose={() => setImportCollectionModalOpen(false)}
handleSubmit={handleImportCollection}
/>
)}
{importCollectionLocationModalOpen && importData && (importData.type !== 'multiple' && importData.type !== 'bulk') && (
<ImportCollectionLocation
rawData={importData.rawData}
format={importData.type}
sourceUrl={importData.sourceUrl}
filePath={importData.filePath}
rawContent={importData.rawContent}
onClose={() => setImportCollectionLocationModalOpen(false)}
handleSubmit={handleImportCollectionLocation}
/>
)}
{importCollectionLocationModalOpen && importData && (importData.type === 'multiple' || importData.type === 'bulk') && (
<BulkImportCollectionLocation
importData={importData}
onClose={() => setImportCollectionLocationModalOpen(false)}
handleSubmit={handleImportCollectionLocation}
/>
)}
{showCloneGitModal && (
<CloneGitRepository
onClose={handleCloseGitModal}
onFinish={handleCloseGitModal}
collectionRepositoryUrl={gitRepositoryUrl}
/>
)}
{postmanPackagePrompt && (
<PostmanPackageReport
key={postmanPackagePrompt.collectionPath}
report={postmanPackagePrompt.report}
collectionPath={postmanPackagePrompt.collectionPath}
onClose={clearPostmanPackagePrompt}
/>
)}
<SidebarSection
id="collections"
title="Collections"
icon={IconBox}
actions={sectionActions}
>
<Collections
showSearch={showSearch}
isCreatingCollection={isCreatingCollection}
onCreateClick={() => dispatch(setIsCreatingCollection(true))}
onDismissCreate={() => dispatch(setIsCreatingCollection(false))}
onOpenAdvancedCreate={handleOpenAdvancedCreate}
/>
</SidebarSection>
</>
);
};
export default CollectionsSection;