mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-26 14:15:52 +00:00
Improve "Close All Collections" community PR (#5994)
* refactor: move CollectionsBadge to a dedicaced folder Co-authored-by: Jérémy Munsch <github@jeremydev.ovh>
This commit is contained in:
24
packages/bruno-app/src/components/Icons/CloseAll/index.js
Normal file
24
packages/bruno-app/src/components/Icons/CloseAll/index.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
const CloseAllIcon = ({ size = 18, strokeWidth = 1.5, className = '', ...props }) => {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M7 7L7 5C7 4.46957 7.21072 3.96086 7.58579 3.58579C7.96086 3.21071 8.46957 3 9 3L19 3C19.5304 3 20.0391 3.21071 20.4142 3.58579C20.7893 3.96086 21 4.46957 21 5L21 15C21 15.5304 20.7893 16.0391 20.4142 16.4142C20.0391 16.7893 19.5304 17 19 17L17 17M17 19C17 19.5304 16.7893 20.0391 16.4142 20.4142C16.0391 20.7893 15.5304 21 15 21L5 21C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19L3 9C3 8.46957 3.21072 7.96086 3.58579 7.58579C3.96086 7.21071 4.46957 7 5 7L15 7C15.5304 7 16.0391 7.21071 16.4142 7.58579C16.7893 7.96086 17 8.46957 17 9L17 19Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M13 11L7 17M7 11L13 17"
|
||||
stroke="currentColor"
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default CloseAllIcon;
|
||||
@@ -0,0 +1,24 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.collections-badge {
|
||||
margin-inline: 0.5rem;
|
||||
background-color: ${(props) => props.theme.sidebar.badge.bg};
|
||||
border-radius: 5px;
|
||||
|
||||
.caret {
|
||||
margin-left: 0.25rem;
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140, 140, 140);
|
||||
}
|
||||
|
||||
.collections-header-actions {
|
||||
.collection-action-button {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { IconArrowsSort, IconFolders, IconSortAscendingLetters, IconSortDescendingLetters } from '@tabler/icons';
|
||||
import CloseAllIcon from 'components/Icons/CloseAll';
|
||||
import { sortCollections } from 'providers/ReduxStore/slices/collections/index';
|
||||
import RemoveCollectionsModal from '../RemoveCollectionsModal';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const CollectionsHeader = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { collections } = useSelector((state) => state.collections);
|
||||
const { collectionSortOrder } = useSelector((state) => state.collections);
|
||||
const [collectionsToClose, setCollectionsToClose] = useState([]);
|
||||
|
||||
const sortCollectionOrder = () => {
|
||||
let order;
|
||||
switch (collectionSortOrder) {
|
||||
case 'default':
|
||||
order = 'alphabetical';
|
||||
break;
|
||||
case 'alphabetical':
|
||||
order = 'reverseAlphabetical';
|
||||
break;
|
||||
case 'reverseAlphabetical':
|
||||
order = 'default';
|
||||
break;
|
||||
}
|
||||
dispatch(sortCollections({ order }));
|
||||
};
|
||||
|
||||
let sortIcon;
|
||||
if (collectionSortOrder === 'default') {
|
||||
sortIcon = <IconArrowsSort size={18} strokeWidth={1.5} />;
|
||||
} else if (collectionSortOrder === 'alphabetical') {
|
||||
sortIcon = <IconSortAscendingLetters size={18} strokeWidth={1.5} />;
|
||||
} else {
|
||||
sortIcon = <IconSortDescendingLetters size={18} strokeWidth={1.5} />;
|
||||
}
|
||||
|
||||
const selectAllCollectionsToClose = () => {
|
||||
setCollectionsToClose(collections.map((c) => c.uid));
|
||||
};
|
||||
|
||||
const clearCollectionsToClose = () => {
|
||||
setCollectionsToClose([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="collections-badge flex items-center justify-between px-2 mt-2 relative">
|
||||
<div className="flex items-center py-1 select-none">
|
||||
<span className="mr-2">
|
||||
<IconFolders size={18} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span>Collections</span>
|
||||
</div>
|
||||
{collections.length >= 1 && (
|
||||
<div className="flex items-center collections-header-actions">
|
||||
<button
|
||||
className="mr-1 collection-action-button"
|
||||
onClick={selectAllCollectionsToClose}
|
||||
aria-label="Close all collections"
|
||||
title="Close all collections"
|
||||
data-testid="close-all-collections-button"
|
||||
>
|
||||
<CloseAllIcon size={18} strokeWidth={1.5} className="cursor-pointer" />
|
||||
</button>
|
||||
<button
|
||||
className="collection-action-button"
|
||||
onClick={() => sortCollectionOrder()}
|
||||
aria-label="Sort collections"
|
||||
title="Sort collections"
|
||||
>
|
||||
{sortIcon}
|
||||
</button>
|
||||
{collectionsToClose.length > 0 && (
|
||||
<RemoveCollectionsModal collectionUids={collectionsToClose} onClose={clearCollectionsToClose} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionsHeader;
|
||||
@@ -0,0 +1,60 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
width: 600px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
|
||||
.collections-list-container {
|
||||
width: 100%;
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 4px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.collections-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.collection-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
background-color: ${(props) => props.theme.requestTabs.active.bg};
|
||||
border: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.text};
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.collection-tag-text {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.show-more-link,
|
||||
.show-less-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
span {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,273 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { filter, groupBy } from 'lodash';
|
||||
import Modal from 'components/Modal';
|
||||
import Portal from 'components/Portal';
|
||||
import {
|
||||
removeCollection,
|
||||
saveMultipleRequests,
|
||||
saveMultipleCollections,
|
||||
saveMultipleFolders
|
||||
} from 'providers/ReduxStore/slices/collections/actions';
|
||||
import {
|
||||
findCollectionByUid,
|
||||
flattenItems,
|
||||
isItemARequest,
|
||||
isItemAFolder,
|
||||
hasRequestChanges
|
||||
} from 'utils/collections/index';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const MAX_COLLECTIONS_WIDTH = 530;
|
||||
const CHARACTER_WIDTH = 8;
|
||||
const COLLECTION_PADDING = 24;
|
||||
const COLLECTION_GAP = 12;
|
||||
|
||||
const getDisplayItems = (items, maxWidth = MAX_COLLECTIONS_WIDTH) => {
|
||||
const visibleItems = [];
|
||||
let totalWidth = 0;
|
||||
|
||||
for (let i = 0; i < items.length; i += 1) {
|
||||
const currentItem = items[i];
|
||||
const name = typeof currentItem === 'string' ? currentItem : currentItem?.name || '';
|
||||
const width = name.length * CHARACTER_WIDTH + COLLECTION_PADDING + COLLECTION_GAP;
|
||||
|
||||
if (i === 0 || totalWidth + width <= maxWidth) {
|
||||
totalWidth += width;
|
||||
visibleItems.push(currentItem);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return visibleItems;
|
||||
};
|
||||
|
||||
const RemoveCollectionsModal = ({ collectionUids, onClose }) => {
|
||||
const dispatch = useDispatch();
|
||||
const allCollections = useSelector((state) => state.collections.collections || []);
|
||||
const [showAllCollections, setShowAllCollections] = useState(false);
|
||||
|
||||
const allDrafts = useMemo(() => {
|
||||
const requestDrafts = [];
|
||||
const collectionDrafts = [];
|
||||
const folderDrafts = [];
|
||||
|
||||
collectionUids.forEach((collectionUid) => {
|
||||
const collection = findCollectionByUid(allCollections, collectionUid);
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for collection draft
|
||||
if (collection.draft) {
|
||||
collectionDrafts.push({
|
||||
name: collection.name,
|
||||
collectionUid: collectionUid
|
||||
});
|
||||
}
|
||||
|
||||
// Check for request and folder drafts
|
||||
const items = flattenItems(collection.items);
|
||||
|
||||
// Request drafts
|
||||
const unsavedRequests = filter(items, (item) => isItemARequest(item) && hasRequestChanges(item));
|
||||
unsavedRequests.forEach((request) => {
|
||||
requestDrafts.push({
|
||||
...request,
|
||||
collectionUid: collectionUid
|
||||
});
|
||||
});
|
||||
|
||||
// Folder drafts
|
||||
const unsavedFolders = filter(items, (item) => isItemAFolder(item) && item.draft);
|
||||
unsavedFolders.forEach((folder) => {
|
||||
folderDrafts.push({
|
||||
name: folder.name,
|
||||
folderUid: folder.uid,
|
||||
collectionUid: collectionUid
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return { requestDrafts, collectionDrafts, folderDrafts };
|
||||
}, [collectionUids, allCollections]);
|
||||
|
||||
const collectionsWithUnsavedChanges = useMemo(() => {
|
||||
const allDraftTypes = [...allDrafts.collectionDrafts, ...allDrafts.folderDrafts, ...allDrafts.requestDrafts];
|
||||
const draftsByCollection = groupBy(allDraftTypes, 'collectionUid');
|
||||
return Object.keys(draftsByCollection)
|
||||
.map((collectionUid) => {
|
||||
const collection = findCollectionByUid(allCollections, collectionUid);
|
||||
return collection ? { uid: collectionUid, name: collection.name } : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
}, [allDrafts, allCollections]);
|
||||
|
||||
const hasUnsavedChanges
|
||||
= allDrafts.collectionDrafts.length > 0 || allDrafts.folderDrafts.length > 0 || allDrafts.requestDrafts.length > 0;
|
||||
|
||||
const handleCloseAllCollections = () => {
|
||||
const removalPromises = collectionUids.map((uid) => dispatch(removeCollection(uid)));
|
||||
|
||||
Promise.all(removalPromises)
|
||||
.then(() => {
|
||||
toast.success('Closed all collections');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error closing collections:', error);
|
||||
toast.error('An error occurred while closing collections');
|
||||
})
|
||||
.finally(() => {
|
||||
onClose();
|
||||
});
|
||||
};
|
||||
|
||||
const handleDiscard = () => {
|
||||
handleCloseAllCollections();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const savePromises = [];
|
||||
|
||||
// Save all collection drafts
|
||||
if (allDrafts.collectionDrafts.length > 0) {
|
||||
savePromises.push(dispatch(saveMultipleCollections(allDrafts.collectionDrafts)));
|
||||
}
|
||||
|
||||
// Save all folder drafts
|
||||
if (allDrafts.folderDrafts.length > 0) {
|
||||
savePromises.push(dispatch(saveMultipleFolders(allDrafts.folderDrafts)));
|
||||
}
|
||||
|
||||
// Save all request drafts
|
||||
if (allDrafts.requestDrafts.length > 0) {
|
||||
savePromises.push(dispatch(saveMultipleRequests(allDrafts.requestDrafts)));
|
||||
}
|
||||
|
||||
await Promise.all(savePromises);
|
||||
handleCloseAllCollections();
|
||||
} catch (error) {
|
||||
console.error('Error saving drafts:', error);
|
||||
toast.error('An error occurred while saving changes');
|
||||
handleCancel();
|
||||
}
|
||||
};
|
||||
|
||||
if (collectionUids.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasMultipleCollections = collectionUids.length > 1;
|
||||
const singleCollectionName = hasMultipleCollections
|
||||
? null
|
||||
: findCollectionByUid(allCollections, collectionUids[0])?.name;
|
||||
|
||||
const displayedCollections = useMemo(() => showAllCollections ? collectionsWithUnsavedChanges : getDisplayItems(collectionsWithUnsavedChanges),
|
||||
[collectionsWithUnsavedChanges, showAllCollections]);
|
||||
const hasMoreCollections = collectionsWithUnsavedChanges.length > displayedCollections.length;
|
||||
const hiddenCollectionsCount = collectionsWithUnsavedChanges.length - displayedCollections.length;
|
||||
|
||||
const toggleButton = hasMoreCollections || showAllCollections ? (
|
||||
<span
|
||||
className={`${showAllCollections ? 'show-less-link' : 'show-more-link'} w-fit flex items-center mt-2 cursor-pointer`}
|
||||
onClick={() => setShowAllCollections(!showAllCollections)}
|
||||
>
|
||||
<span className="text-sm text-link">
|
||||
{showAllCollections ? 'Show less' : `Show ${hiddenCollectionsCount} more`}
|
||||
</span>
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
size="md"
|
||||
title="Close all collections"
|
||||
disableEscapeKey={hasUnsavedChanges}
|
||||
disableCloseOnOutsideClick={hasUnsavedChanges}
|
||||
handleCancel={handleCancel}
|
||||
hideFooter={true}
|
||||
>
|
||||
<StyledWrapper>
|
||||
{hasUnsavedChanges ? (
|
||||
<>
|
||||
<div className="flex items-center font-normal">
|
||||
<IconAlertTriangle size={32} strokeWidth={1.5} className="text-yellow-600" />
|
||||
<h1 className="ml-2 text-lg font-semibold">Hold on..</h1>
|
||||
</div>
|
||||
<div className="font-normal mt-4">
|
||||
Do you want to save changes you made to the following{' '}
|
||||
{collectionsWithUnsavedChanges.length === 1 ? 'collection' : 'collections'}?
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
Collections will still be available in the file system and can be re-opened later.
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="collections-list-container">
|
||||
<div className="collections-list">
|
||||
{displayedCollections.map(({ uid, name }) => (
|
||||
<span key={uid} className="collection-tag">
|
||||
<span className="collection-tag-text">{name}</span>
|
||||
</span>
|
||||
))}
|
||||
{toggleButton}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<div>
|
||||
<button className="btn btn-sm btn-danger" onClick={handleDiscard}>
|
||||
Discard and Close
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button className="btn btn-close btn-sm mr-2" onClick={handleCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={handleSave}>
|
||||
Save and Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="mt-4">
|
||||
{hasMultipleCollections ? (
|
||||
`Are you sure you want to close all ${collectionUids.length} collections in Bruno?`
|
||||
) : (
|
||||
<>
|
||||
Are you sure you want to close the collection <strong>{singleCollectionName}</strong> in Bruno?
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 text-xs text-gray-500">
|
||||
Collections will still be available in the file system and can be re-opened later.
|
||||
</div>
|
||||
<div className="flex justify-end mt-6">
|
||||
<button className="btn btn-close btn-sm mr-2" onClick={handleCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-secondary btn-sm" onClick={handleCloseAllCollections}>
|
||||
{hasMultipleCollections ? 'Close All' : 'Close'}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemoveCollectionsModal;
|
||||
@@ -1,21 +1,13 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.collections-badge {
|
||||
margin-inline: 0.5rem;
|
||||
background-color: ${(props) => props.theme.sidebar.badge.bg};
|
||||
border-radius: 5px;
|
||||
|
||||
.caret {
|
||||
margin-left: 0.25rem;
|
||||
color: rgb(140, 140, 140);
|
||||
fill: rgb(140, 140, 140);
|
||||
}
|
||||
}
|
||||
|
||||
span.close-icon {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
}
|
||||
|
||||
&:hover .collections-badge .collections-header-actions .collection-action-button {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -1,64 +1,14 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
IconSearch,
|
||||
IconFolders,
|
||||
IconArrowsSort,
|
||||
IconSortAscendingLetters,
|
||||
IconSortDescendingLetters,
|
||||
IconX
|
||||
} from '@tabler/icons';
|
||||
import Collection from './Collection';
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import CreateCollection from '../CreateCollection';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Collection from './Collection';
|
||||
import CollectionsHeader from './CollectionsHeader';
|
||||
import CreateOrOpenCollection from './CreateOrOpenCollection';
|
||||
import { sortCollections } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
// todo: move this to a separate folder
|
||||
// the coding convention is to keep all the components in a folder named after the component
|
||||
const CollectionsBadge = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { collections } = useSelector((state) => state.collections);
|
||||
const { collectionSortOrder } = useSelector((state) => state.collections);
|
||||
const sortCollectionOrder = () => {
|
||||
let order;
|
||||
switch (collectionSortOrder) {
|
||||
case 'default':
|
||||
order = 'alphabetical';
|
||||
break;
|
||||
case 'alphabetical':
|
||||
order = 'reverseAlphabetical';
|
||||
break;
|
||||
case 'reverseAlphabetical':
|
||||
order = 'default';
|
||||
break;
|
||||
}
|
||||
dispatch(sortCollections({ order }));
|
||||
};
|
||||
return (
|
||||
<div className="items-center mt-2 relative">
|
||||
<div className="collections-badge flex items-center justify-between px-2">
|
||||
<div className="flex items-center py-1 select-none">
|
||||
<span className="mr-2">
|
||||
<IconFolders size={18} strokeWidth={1.5} />
|
||||
</span>
|
||||
<span>Collections</span>
|
||||
</div>
|
||||
{collections.length >= 1 && (
|
||||
<button onClick={() => sortCollectionOrder()}>
|
||||
{collectionSortOrder == 'default' ? (
|
||||
<IconArrowsSort size={18} strokeWidth={1.5} />
|
||||
) : collectionSortOrder == 'alphabetical' ? (
|
||||
<IconSortAscendingLetters size={18} strokeWidth={1.5} />
|
||||
) : (
|
||||
<IconSortDescendingLetters size={18} strokeWidth={1.5} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Collections = () => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
@@ -67,18 +17,18 @@ const Collections = () => {
|
||||
|
||||
if (!collections || !collections.length) {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<CollectionsBadge />
|
||||
<StyledWrapper data-testid="collections">
|
||||
<CollectionsHeader />
|
||||
<CreateOrOpenCollection />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<StyledWrapper data-testid="collections">
|
||||
{createCollectionModalOpen ? <CreateCollection onClose={() => setCreateCollectionModalOpen(false)} /> : null}
|
||||
|
||||
<CollectionsBadge />
|
||||
<CollectionsHeader />
|
||||
|
||||
<div className="mt-4 relative collection-filter px-2">
|
||||
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { test, expect } from '../../../playwright';
|
||||
import { Page, ElectronApplication } from '@playwright/test';
|
||||
import path from 'path';
|
||||
import { openCollectionAndAcceptSandbox } from '../../utils/page/actions';
|
||||
import { buildCommonLocators } from '../../utils/page/locators';
|
||||
|
||||
/**
|
||||
* Helper function to restart app and get fresh state with locators
|
||||
*/
|
||||
const restartAppAndGetLocators = async (restartApp: (options?: { initUserDataPath?: string }) => Promise<ElectronApplication>): Promise<{ app: ElectronApplication; page: Page; locators: ReturnType<typeof buildCommonLocators> }> => {
|
||||
const app = await restartApp();
|
||||
const page = await app.firstWindow();
|
||||
await page.locator('[data-app-state="loaded"]').waitFor();
|
||||
const locators = buildCommonLocators(page);
|
||||
return { app, page, locators };
|
||||
};
|
||||
|
||||
test.describe('Close All Collections', () => {
|
||||
test.afterAll(async () => {
|
||||
// Reset the request file to the original state after saving changes
|
||||
execSync(`git checkout -- "${path.join(__dirname, 'fixtures', 'collections', 'collection 1', 'test-request.bru')}"`);
|
||||
});
|
||||
|
||||
test('should show/hide close all icon based on hover state', async ({ pageWithUserData: page }) => {
|
||||
const locators = buildCommonLocators(page);
|
||||
|
||||
await test.step('Verify initial state', async () => {
|
||||
await expect(locators.sidebar.collection('collection 1')).toBeVisible();
|
||||
const closeAllButton = locators.sidebar.closeAllCollectionsButton();
|
||||
await expect(closeAllButton).toHaveCSS('opacity', '0');
|
||||
});
|
||||
|
||||
await test.step('Hover to show icon', async () => {
|
||||
const closeAllButton = locators.sidebar.closeAllCollectionsButton();
|
||||
await locators.sidebar.collectionsContainer().hover();
|
||||
await expect(closeAllButton).toHaveCSS('opacity', '1');
|
||||
});
|
||||
|
||||
await test.step('Move mouse away to hide icon', async () => {
|
||||
const closeAllButton = locators.sidebar.closeAllCollectionsButton();
|
||||
await page.mouse.move(0, 0);
|
||||
await expect(closeAllButton).toHaveCSS('opacity', '0');
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle closing all collections without unsaved changes', async ({ restartApp }) => {
|
||||
const { page, locators } = await restartAppAndGetLocators(restartApp);
|
||||
|
||||
await test.step('Verify collections are visible', async () => {
|
||||
await expect(locators.sidebar.collection('collection 1')).toBeVisible();
|
||||
await expect(locators.sidebar.collection('collection 2')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Cancel closing collections', async () => {
|
||||
// Hover and click close all icon
|
||||
await locators.sidebar.collectionsContainer().hover();
|
||||
await locators.sidebar.closeAllCollectionsButton().click();
|
||||
|
||||
// Verify confirmation modal appears
|
||||
const confirmModal = locators.modal.byTitle('Close all collections');
|
||||
await expect(confirmModal).toBeVisible();
|
||||
|
||||
// Click "Cancel" to dismiss the modal
|
||||
await locators.modal.closeButton().click();
|
||||
|
||||
// Verify collections are still visible
|
||||
await expect(locators.sidebar.collection('collection 1')).toBeVisible();
|
||||
await expect(locators.sidebar.collection('collection 2')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Confirm closing collections', async () => {
|
||||
// Hover and click close all icon again
|
||||
await locators.sidebar.collectionsContainer().hover();
|
||||
await locators.sidebar.closeAllCollectionsButton().click();
|
||||
|
||||
// Verify confirmation modal appears
|
||||
const confirmModal = locators.modal.byTitle('Close all collections');
|
||||
await expect(confirmModal).toBeVisible();
|
||||
|
||||
// Click "Close All" to confirm
|
||||
await locators.modal.button('Close All').click();
|
||||
|
||||
// Verify collections are closed
|
||||
await expect(locators.sidebar.collection('collection 1')).not.toBeVisible();
|
||||
await expect(locators.sidebar.collection('collection 2')).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test('should discard changes and close collections when Discard and Close is clicked', async ({ restartApp }) => {
|
||||
const { page, locators: newLocators } = await restartAppAndGetLocators(restartApp);
|
||||
|
||||
await test.step('Verify collections are visible', async () => {
|
||||
await expect(newLocators.sidebar.collection('collection 1')).toBeVisible();
|
||||
await expect(newLocators.sidebar.collection('collection 2')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Create unsaved changes', async () => {
|
||||
await openCollectionAndAcceptSandbox(page, 'collection 1');
|
||||
await newLocators.sidebar.request('test-request').click();
|
||||
|
||||
const urlContainer = page.locator('#request-url');
|
||||
await expect(urlContainer).toBeVisible();
|
||||
|
||||
const codeMirrorEditor = urlContainer.locator('.CodeMirror');
|
||||
await codeMirrorEditor.click();
|
||||
await page.keyboard.type('modified');
|
||||
});
|
||||
|
||||
await test.step('Trigger close all and discard changes', async () => {
|
||||
await newLocators.sidebar.collectionsContainer().hover();
|
||||
await newLocators.sidebar.closeAllCollectionsButton().click();
|
||||
|
||||
const unsavedChangesModal = newLocators.modal.byTitle('Close all collections');
|
||||
await expect(unsavedChangesModal).toBeVisible();
|
||||
await expect(unsavedChangesModal.getByText('Do you want to save')).toBeVisible();
|
||||
|
||||
await newLocators.modal.button('Discard and Close').click();
|
||||
|
||||
await expect(page.getByText('Closed all collections')).toBeVisible();
|
||||
await expect(newLocators.sidebar.collection('collection 1')).not.toBeVisible();
|
||||
await expect(newLocators.sidebar.collection('collection 2')).not.toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Restart app to verify changes were discarded', async () => {
|
||||
const { page: restartedPage, locators: restartedLocators } = await restartAppAndGetLocators(restartApp);
|
||||
|
||||
await expect(restartedLocators.sidebar.collection('collection 1')).toBeVisible();
|
||||
await openCollectionAndAcceptSandbox(restartedPage, 'collection 1');
|
||||
await restartedLocators.sidebar.request('test-request').click();
|
||||
|
||||
const urlContainerAfterReopen = restartedPage.locator('#request-url');
|
||||
await expect(urlContainerAfterReopen).toBeVisible();
|
||||
const urlAfterReopen = await urlContainerAfterReopen.locator('.CodeMirror').textContent();
|
||||
expect(urlAfterReopen).not.toContain('modified');
|
||||
});
|
||||
});
|
||||
|
||||
test('should save changes and close collections when Save and Close is clicked', async ({ restartApp }) => {
|
||||
const { page, locators: newLocators } = await restartAppAndGetLocators(restartApp);
|
||||
|
||||
await test.step('Verify collections are visible', async () => {
|
||||
await expect(newLocators.sidebar.collection('collection 1')).toBeVisible();
|
||||
await expect(newLocators.sidebar.collection('collection 2')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Create unsaved changes', async () => {
|
||||
await openCollectionAndAcceptSandbox(page, 'collection 1');
|
||||
await newLocators.sidebar.request('test-request').click();
|
||||
|
||||
const urlContainer = page.locator('#request-url');
|
||||
await expect(urlContainer).toBeVisible();
|
||||
|
||||
const codeMirrorEditor = urlContainer.locator('.CodeMirror');
|
||||
await codeMirrorEditor.click();
|
||||
await page.keyboard.type('modified');
|
||||
});
|
||||
|
||||
await test.step('Trigger close all and save changes', async () => {
|
||||
await newLocators.sidebar.collectionsContainer().hover();
|
||||
await newLocators.sidebar.closeAllCollectionsButton().click();
|
||||
|
||||
const unsavedChangesModal = newLocators.modal.byTitle('Close all collections');
|
||||
await expect(unsavedChangesModal).toBeVisible();
|
||||
await expect(unsavedChangesModal.getByText('Do you want to save')).toBeVisible();
|
||||
|
||||
await newLocators.modal.button('Save and Close').click();
|
||||
|
||||
await expect(newLocators.sidebar.collection('collection 1')).not.toBeVisible();
|
||||
await expect(newLocators.sidebar.collection('collection 2')).not.toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Restart app to verify changes were saved', async () => {
|
||||
const { page: restartedPage, locators: restartedLocators } = await restartAppAndGetLocators(restartApp);
|
||||
|
||||
await expect(restartedLocators.sidebar.collection('collection 1')).toBeVisible();
|
||||
await openCollectionAndAcceptSandbox(restartedPage, 'collection 1');
|
||||
await restartedLocators.sidebar.request('test-request').click();
|
||||
|
||||
const urlContainerAfterReopen = restartedPage.locator('#request-url');
|
||||
await expect(urlContainerAfterReopen).toBeVisible();
|
||||
const urlAfterReopen = await urlContainerAfterReopen.locator('.CodeMirror').textContent();
|
||||
expect(urlAfterReopen).toContain('modified');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "collection 1",
|
||||
"type": "collection"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
meta {
|
||||
name: test-request
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://jsonplaceholder.typicode.com/posts/1
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "collection 2",
|
||||
"type": "collection"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
meta {
|
||||
name: test-request
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://jsonplaceholder.typicode.com/users/1
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"lastOpenedCollections": [
|
||||
"{{projectRoot}}/tests/collection/close-all-collections/fixtures/collections/collection 1",
|
||||
"{{projectRoot}}/tests/collection/close-all-collections/fixtures/collections/collection 2"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ test.describe('Global Environment Import Tests', () => {
|
||||
.click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'Close' }).click();
|
||||
await page.locator('.dropdown-item').filter({ hasText: 'Close' }).waitFor({ state: 'detached' });
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
const closeModal = page.getByRole('dialog').filter({ has: page.getByText('Close Collection') });
|
||||
await closeModal.getByRole('button', { name: 'Close' }).click();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -101,7 +101,7 @@ test.describe('Onboarding', () => {
|
||||
await closeOption.click();
|
||||
|
||||
// Handle the confirmation dialog - click the 'Close' button to confirm
|
||||
const confirmCloseButton = page.getByRole('button', { name: 'Close' });
|
||||
const confirmCloseButton = page.locator('.bruno-modal').getByRole('button', { name: 'Close' });
|
||||
await expect(confirmCloseButton).toBeVisible();
|
||||
await confirmCloseButton.click();
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ export const buildCommonLocators = (page: Page) => ({
|
||||
.locator('.infotip')
|
||||
.filter({ hasText: /^Save/ }),
|
||||
sidebar: {
|
||||
collectionsContainer: () => page.getByTestId('collections'),
|
||||
collection: (name: string) => page.locator('#sidebar-collection-name').filter({ hasText: name }),
|
||||
folder: (name: string) => page.locator('.collection-item-name').filter({ hasText: name }),
|
||||
request: (name: string) => page.locator('.collection-item-name').filter({ hasText: name }),
|
||||
@@ -15,7 +16,8 @@ export const buildCommonLocators = (page: Page) => ({
|
||||
// Using .locator('..') gets the parent element of the folder's collection-item-name div.
|
||||
const folderWrapper = page.locator('.collection-item-name').filter({ hasText: folderName }).locator('..');
|
||||
return folderWrapper.locator('.collection-item-name').filter({ hasText: requestName });
|
||||
}
|
||||
},
|
||||
closeAllCollectionsButton: () => page.getByTestId('close-all-collections-button')
|
||||
},
|
||||
actions: {
|
||||
collectionActions: (collectionName: string) =>
|
||||
@@ -40,7 +42,9 @@ export const buildCommonLocators = (page: Page) => ({
|
||||
},
|
||||
modal: {
|
||||
title: (title: string) => page.locator('.bruno-modal-header-title').filter({ hasText: title }),
|
||||
button: (name: string) => page.getByRole('button', { name: name, exact: true })
|
||||
byTitle: (title: string) => page.locator('.bruno-modal').filter({ has: page.locator('.bruno-modal-header-title').filter({ hasText: title }) }),
|
||||
button: (name: string) => page.getByRole('button', { name: name, exact: true }),
|
||||
closeButton: () => page.locator('.bruno-modal').getByTestId('modal-close-button')
|
||||
},
|
||||
environment: {
|
||||
selector: () => page.getByTestId('environment-selector-trigger'),
|
||||
|
||||
Reference in New Issue
Block a user