mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-28 07:04:10 +00:00
Improve delete collection in workspace overview (#6587)
* Improve delete collection in workspace overview * fixes
This commit is contained in:
@@ -96,9 +96,8 @@ const Modal = ({
|
||||
return closeModal({ type: 'esc' });
|
||||
}
|
||||
case ENTER_KEY_CODE: {
|
||||
// Skip if a submit button is focused - let native button click handle it to avoid double-fire
|
||||
const isSubmitButton = event.target?.type === 'submit';
|
||||
if (!shiftKey && !ctrlKey && !altKey && !metaKey && handleConfirm && !isSubmitButton) {
|
||||
if (!shiftKey && !ctrlKey && !altKey && !metaKey && handleConfirm && !isSubmitButton && !confirmDisabled) {
|
||||
return handleConfirm();
|
||||
}
|
||||
}
|
||||
@@ -117,7 +116,7 @@ const Modal = ({
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
};
|
||||
}, [disableEscapeKey, document, handleConfirm]);
|
||||
}, [disableEscapeKey, document, handleConfirm, confirmDisabled]);
|
||||
|
||||
let classes = 'bruno-modal';
|
||||
if (isClosing) {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import styled from 'styled-components';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
.modal-description {
|
||||
color: ${(props) => props.theme.text};
|
||||
margin-bottom: 12px;
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.collection-info-card {
|
||||
background-color: ${(props) => props.theme.modal.title.bg};
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.collection-name {
|
||||
font-weight: 500;
|
||||
color: ${(props) => props.theme.text};
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.collection-path {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.delete-confirmation {
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid ${(props) => props.theme.border.border0};
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
color: ${(props) => props.theme.text};
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.delete-keyword {
|
||||
font-weight: 600;
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
font-family: monospace;
|
||||
background-color: ${(props) => rgba(props.theme.colors.text.danger, 0.1)};
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: ${(props) => props.theme.font.size.sm};
|
||||
border: 1px solid ${(props) => props.theme.input.border};
|
||||
border-radius: 6px;
|
||||
background-color: ${(props) => props.theme.input.bg};
|
||||
color: ${(props) => props.theme.text};
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
|
||||
&::placeholder {
|
||||
color: ${(props) => props.theme.colors.text.muted};
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: ${(props) => props.theme.colors.text.danger};
|
||||
box-shadow: 0 0 0 2px ${(props) => rgba(props.theme.colors.text.danger, 0.15)};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,88 @@
|
||||
import React, { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import Modal from 'components/Modal';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { IconAlertTriangle } from '@tabler/icons';
|
||||
import { removeCollectionFromWorkspaceAction } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { findCollectionByUid } from 'utils/collections/index';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const DeleteCollection = ({ onClose, collectionUid, workspaceUid }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [confirmText, setConfirmText] = useState('');
|
||||
const collection = useSelector((state) => findCollectionByUid(state.collections.collections, collectionUid));
|
||||
const workspace = useSelector((state) => state.workspaces.workspaces.find((w) => w.uid === workspaceUid));
|
||||
|
||||
const isConfirmed = confirmText.toLowerCase() === 'delete';
|
||||
|
||||
const onConfirm = async () => {
|
||||
if (!collection || !workspace) {
|
||||
toast.error('Collection or workspace not found');
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await dispatch(removeCollectionFromWorkspaceAction(workspace.uid, collection.pathname, { deleteFiles: true }));
|
||||
toast.success(`Deleted "${collection.name}" collection`);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Error deleting collection:', error);
|
||||
toast.error(error.message || 'An error occurred while deleting the collection');
|
||||
}
|
||||
};
|
||||
|
||||
if (!collection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const customHeader = (
|
||||
<div className="flex items-center gap-2">
|
||||
<IconAlertTriangle size={18} strokeWidth={1.5} className="text-red-500" />
|
||||
<span>Delete Collection</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Modal
|
||||
size="sm"
|
||||
title="Delete Collection"
|
||||
customHeader={customHeader}
|
||||
confirmText="Delete"
|
||||
cancelText="Cancel"
|
||||
confirmButtonColor="danger"
|
||||
confirmDisabled={!isConfirmed}
|
||||
handleConfirm={onConfirm}
|
||||
handleCancel={onClose}
|
||||
>
|
||||
<p className="modal-description">
|
||||
Are you sure you want to permanently delete <strong>"{collection.name}"</strong>?
|
||||
</p>
|
||||
<div className="collection-info-card">
|
||||
<div className="collection-name">{collection.name}</div>
|
||||
<div className="collection-path">{collection.pathname}</div>
|
||||
</div>
|
||||
<p className="warning-text">
|
||||
This action cannot be undone. The collection files will be permanently deleted from disk.
|
||||
</p>
|
||||
<div className="delete-confirmation">
|
||||
<label htmlFor="delete-confirm-input">
|
||||
Type <span className="delete-keyword">delete</span> to confirm
|
||||
</label>
|
||||
<input
|
||||
id="delete-confirm-input"
|
||||
type="text"
|
||||
value={confirmText}
|
||||
onChange={(e) => setConfirmText(e.target.value)}
|
||||
placeholder="delete"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteCollection;
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useState, useMemo, useRef } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { IconBox, IconTrash, IconEdit, IconShare, IconDots } from '@tabler/icons';
|
||||
import { removeCollectionFromWorkspaceAction } from 'providers/ReduxStore/slices/workspaces/actions';
|
||||
import { IconBox, IconTrash, IconEdit, IconShare, IconDots, IconX } from '@tabler/icons';
|
||||
import { addTab } from 'providers/ReduxStore/slices/tabs';
|
||||
import { mountCollection } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { normalizePath } from 'utils/common/path';
|
||||
import toast from 'react-hot-toast';
|
||||
import Modal from 'components/Modal';
|
||||
import RenameCollection from 'components/Sidebar/Collections/Collection/RenameCollection';
|
||||
import RemoveCollection from 'components/Sidebar/Collections/Collection/RemoveCollection';
|
||||
import DeleteCollection from 'components/Sidebar/Collections/Collection/DeleteCollection';
|
||||
import ShareCollection from 'components/ShareCollection';
|
||||
import Dropdown from 'components/Dropdown';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -17,8 +17,9 @@ const CollectionsList = ({ workspace }) => {
|
||||
const { collections } = useSelector((state) => state.collections);
|
||||
const dropdownRefs = useRef({});
|
||||
|
||||
const [collectionToRemove, setCollectionToRemove] = useState(null);
|
||||
const [renameCollectionModalOpen, setRenameCollectionModalOpen] = useState(false);
|
||||
const [removeCollectionModalOpen, setRemoveCollectionModalOpen] = useState(false);
|
||||
const [deleteCollectionModalOpen, setDeleteCollectionModalOpen] = useState(false);
|
||||
const [shareCollectionModalOpen, setShareCollectionModalOpen] = useState(false);
|
||||
const [selectedCollectionUid, setSelectedCollectionUid] = useState(null);
|
||||
|
||||
@@ -65,35 +66,6 @@ const CollectionsList = ({ workspace }) => {
|
||||
});
|
||||
}, [workspace.collections, collections]);
|
||||
|
||||
const isInternalCollection = (collection) => {
|
||||
if (!workspace.pathname || !collection.pathname) return false;
|
||||
const workspaceCollectionsFolder = normalizePath(`${workspace.pathname}/collections`);
|
||||
const collectionPath = normalizePath(collection.pathname);
|
||||
return collectionPath.startsWith(workspaceCollectionsFolder);
|
||||
};
|
||||
|
||||
const getCollectionWorkspaceInfo = (collection) => {
|
||||
if (Object.prototype.hasOwnProperty.call(collection, 'isGitBacked')) {
|
||||
return {
|
||||
isGitBacked: collection.isGitBacked,
|
||||
gitRemoteUrl: collection.gitRemoteUrl,
|
||||
isLoaded: collection.isLoaded !== false,
|
||||
isInternal: isInternalCollection(collection)
|
||||
};
|
||||
}
|
||||
|
||||
const workspaceCollection = workspace.collections?.find(
|
||||
(wc) => normalizePath(collection.pathname) === normalizePath(wc.path)
|
||||
);
|
||||
|
||||
return {
|
||||
isGitBacked: !!workspaceCollection?.remote,
|
||||
gitRemoteUrl: workspaceCollection?.remote,
|
||||
isLoaded: true,
|
||||
isInternal: isInternalCollection(collection)
|
||||
};
|
||||
};
|
||||
|
||||
const handleOpenCollectionClick = (collection, event) => {
|
||||
if (event.target.closest('.collection-menu')) {
|
||||
return;
|
||||
@@ -156,59 +128,22 @@ const CollectionsList = ({ workspace }) => {
|
||||
|
||||
const handleRemoveCollection = (collection) => {
|
||||
dropdownRefs.current[collection.uid]?.hide();
|
||||
setCollectionToRemove(collection);
|
||||
};
|
||||
|
||||
const confirmRemoveCollection = async () => {
|
||||
if (!collectionToRemove) return;
|
||||
|
||||
try {
|
||||
const collectionInfo = getCollectionWorkspaceInfo(collectionToRemove);
|
||||
const isDelete = collectionInfo.isInternal && !collectionInfo.isGitBacked;
|
||||
|
||||
await dispatch(removeCollectionFromWorkspaceAction(workspace.uid, collectionToRemove.pathname));
|
||||
|
||||
if (isDelete) {
|
||||
toast.success(`Deleted "${collectionToRemove.name}" collection`);
|
||||
} else {
|
||||
toast.success(`Removed "${collectionToRemove.name}" from workspace`);
|
||||
}
|
||||
|
||||
setCollectionToRemove(null);
|
||||
} catch (error) {
|
||||
console.error('Error removing collection:', error);
|
||||
toast.error(error.message || 'Failed to remove collection from workspace');
|
||||
if (collection.isLoaded === false) {
|
||||
toast.error('Cannot remove collections that are not loaded');
|
||||
return;
|
||||
}
|
||||
setSelectedCollectionUid(collection.uid);
|
||||
setRemoveCollectionModalOpen(true);
|
||||
};
|
||||
|
||||
const renderRemoveModal = () => {
|
||||
if (!collectionToRemove) return null;
|
||||
|
||||
const collectionInfo = getCollectionWorkspaceInfo(collectionToRemove);
|
||||
const isDelete = collectionInfo.isInternal && !collectionInfo.isGitBacked;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="sm"
|
||||
title={isDelete ? 'Delete Collection' : 'Remove Collection'}
|
||||
handleCancel={() => setCollectionToRemove(null)}
|
||||
handleConfirm={confirmRemoveCollection}
|
||||
confirmText={isDelete ? 'Delete' : 'Remove'}
|
||||
cancelText="Cancel"
|
||||
confirmButtonColor={isDelete ? 'warning' : 'primary'}
|
||||
style="new"
|
||||
>
|
||||
<p className="text-gray-600 dark:text-gray-300">
|
||||
Are you sure you want to {isDelete ? 'delete' : 'remove'}{' '}
|
||||
<strong>"{collectionToRemove.name}"</strong>?
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-3">
|
||||
{isDelete
|
||||
? 'This will permanently delete the collection files from the workspace collections folder.'
|
||||
: 'This will remove the collection from the workspace. The collection files will not be deleted.'}
|
||||
</p>
|
||||
</Modal>
|
||||
);
|
||||
const handleDeleteCollection = (collection) => {
|
||||
dropdownRefs.current[collection.uid]?.hide();
|
||||
if (collection.isLoaded === false) {
|
||||
toast.error('Cannot delete collections that are not loaded');
|
||||
return;
|
||||
}
|
||||
setSelectedCollectionUid(collection.uid);
|
||||
setDeleteCollectionModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -223,6 +158,27 @@ const CollectionsList = ({ workspace }) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{removeCollectionModalOpen && selectedCollectionUid && (
|
||||
<RemoveCollection
|
||||
collectionUid={selectedCollectionUid}
|
||||
onClose={() => {
|
||||
setRemoveCollectionModalOpen(false);
|
||||
setSelectedCollectionUid(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{deleteCollectionModalOpen && selectedCollectionUid && (
|
||||
<DeleteCollection
|
||||
collectionUid={selectedCollectionUid}
|
||||
workspaceUid={workspace.uid}
|
||||
onClose={() => {
|
||||
setDeleteCollectionModalOpen(false);
|
||||
setSelectedCollectionUid(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shareCollectionModalOpen && selectedCollectionUid && (
|
||||
<ShareCollection
|
||||
collectionUid={selectedCollectionUid}
|
||||
@@ -233,8 +189,6 @@ const CollectionsList = ({ workspace }) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{renderRemoveModal()}
|
||||
|
||||
<div className="collections-list">
|
||||
{workspaceCollections.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
@@ -289,15 +243,25 @@ const CollectionsList = ({ workspace }) => {
|
||||
<span>Share</span>
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item delete-item"
|
||||
className="dropdown-item"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRemoveCollection(collection);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={16} strokeWidth={1.5} />
|
||||
<IconX size={16} strokeWidth={1.5} />
|
||||
<span>Remove</span>
|
||||
</div>
|
||||
<div
|
||||
className="dropdown-item delete-item"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteCollection(collection);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={16} strokeWidth={1.5} />
|
||||
<span>Delete</span>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
@@ -122,9 +122,10 @@ export const openWorkspaceDialog = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const removeCollectionFromWorkspaceAction = (workspaceUid, collectionPath) => {
|
||||
export const removeCollectionFromWorkspaceAction = (workspaceUid, collectionPath, options = {}) => {
|
||||
return async (dispatch, getState) => {
|
||||
try {
|
||||
const { deleteFiles = false } = options;
|
||||
const workspacesState = getState().workspaces;
|
||||
const collectionsState = getState().collections;
|
||||
const workspace = workspacesState.workspaces.find((w) => w.uid === workspaceUid);
|
||||
@@ -142,7 +143,8 @@ export const removeCollectionFromWorkspaceAction = (workspaceUid, collectionPath
|
||||
await ipcRenderer.invoke('renderer:remove-collection-from-workspace',
|
||||
workspaceUid,
|
||||
workspace.pathname,
|
||||
collectionPath);
|
||||
collectionPath,
|
||||
{ deleteFiles });
|
||||
|
||||
if (collection) {
|
||||
const workspaceCollection = workspace.collections?.find(
|
||||
|
||||
@@ -552,11 +552,12 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('renderer:remove-collection-from-workspace', async (event, workspaceUid, workspacePath, collectionPath) => {
|
||||
ipcMain.handle('renderer:remove-collection-from-workspace', async (event, workspaceUid, workspacePath, collectionPath, options = {}) => {
|
||||
try {
|
||||
const { deleteFiles = false } = options;
|
||||
const result = await removeCollectionFromWorkspace(workspacePath, collectionPath);
|
||||
|
||||
if (result.shouldDeleteFiles && result.removedCollection && fs.existsSync(collectionPath)) {
|
||||
if (deleteFiles && result.removedCollection && fs.existsSync(collectionPath)) {
|
||||
await fsExtra.remove(collectionPath);
|
||||
}
|
||||
|
||||
|
||||
@@ -358,7 +358,6 @@ const removeCollectionFromWorkspace = async (workspacePath, collectionPath) => {
|
||||
const config = readWorkspaceConfig(workspacePath);
|
||||
|
||||
let removedCollection = null;
|
||||
let shouldDeleteFiles = false;
|
||||
|
||||
config.collections = (config.collections || []).filter((c) => {
|
||||
const collectionPathFromYml = c.path;
|
||||
@@ -373,12 +372,6 @@ const removeCollectionFromWorkspace = async (workspacePath, collectionPath) => {
|
||||
|
||||
if (path.normalize(absoluteCollectionPath) === path.normalize(collectionPath)) {
|
||||
removedCollection = c;
|
||||
|
||||
const hasRemote = c.remote;
|
||||
const isExternalPath = path.isAbsolute(collectionPathFromYml);
|
||||
|
||||
shouldDeleteFiles = !hasRemote && !isExternalPath;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -390,7 +383,6 @@ const removeCollectionFromWorkspace = async (workspacePath, collectionPath) => {
|
||||
|
||||
return {
|
||||
removedCollection,
|
||||
shouldDeleteFiles,
|
||||
updatedConfig: config
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user