mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-07 22:18:33 +00:00
430 lines
13 KiB
JavaScript
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;
|