Improve delete collection in workspace overview (#6587)

* Improve delete collection in workspace overview

* fixes
This commit is contained in:
naman-bruno
2026-01-02 15:11:39 +05:30
committed by GitHub
parent c03c5eb927
commit 058d2e0e61
7 changed files with 231 additions and 102 deletions

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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(

View File

@@ -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);
}

View File

@@ -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
};
});