diff --git a/packages/bruno-app/src/components/Modal/index.js b/packages/bruno-app/src/components/Modal/index.js index ca1513e48..36ca7a4fc 100644 --- a/packages/bruno-app/src/components/Modal/index.js +++ b/packages/bruno-app/src/components/Modal/index.js @@ -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) { diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/DeleteCollection/StyledWrapper.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/DeleteCollection/StyledWrapper.js new file mode 100644 index 000000000..6944fd9a2 --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/DeleteCollection/StyledWrapper.js @@ -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; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/DeleteCollection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/DeleteCollection/index.js new file mode 100644 index 000000000..8e40e439a --- /dev/null +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/DeleteCollection/index.js @@ -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 = ( +
+ + Delete Collection +
+ ); + + return ( + + +

+ Are you sure you want to permanently delete "{collection.name}"? +

+
+
{collection.name}
+
{collection.pathname}
+
+

+ This action cannot be undone. The collection files will be permanently deleted from disk. +

+
+ + setConfirmText(e.target.value)} + placeholder="delete" + autoComplete="off" + autoFocus + /> +
+
+
+ ); +}; + +export default DeleteCollection; diff --git a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceOverview/CollectionsList/index.js b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceOverview/CollectionsList/index.js index a7481a220..421eb5803 100644 --- a/packages/bruno-app/src/components/WorkspaceHome/WorkspaceOverview/CollectionsList/index.js +++ b/packages/bruno-app/src/components/WorkspaceHome/WorkspaceOverview/CollectionsList/index.js @@ -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 ( - setCollectionToRemove(null)} - handleConfirm={confirmRemoveCollection} - confirmText={isDelete ? 'Delete' : 'Remove'} - cancelText="Cancel" - confirmButtonColor={isDelete ? 'warning' : 'primary'} - style="new" - > -

- Are you sure you want to {isDelete ? 'delete' : 'remove'}{' '} - "{collectionToRemove.name}"? -

-

- {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.'} -

-
- ); + 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 && ( + { + setRemoveCollectionModalOpen(false); + setSelectedCollectionUid(null); + }} + /> + )} + + {deleteCollectionModalOpen && selectedCollectionUid && ( + { + setDeleteCollectionModalOpen(false); + setSelectedCollectionUid(null); + }} + /> + )} + {shareCollectionModalOpen && selectedCollectionUid && ( { /> )} - {renderRemoveModal()} -
{workspaceCollections.length === 0 ? (
@@ -289,15 +243,25 @@ const CollectionsList = ({ workspace }) => { Share
{ e.stopPropagation(); handleRemoveCollection(collection); }} > - + Remove
+
{ + e.stopPropagation(); + handleDeleteCollection(collection); + }} + > + + Delete +
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js index e56051dd0..f1f35298a 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js @@ -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( diff --git a/packages/bruno-electron/src/ipc/workspace.js b/packages/bruno-electron/src/ipc/workspace.js index 21badfcc8..7926c69ef 100644 --- a/packages/bruno-electron/src/ipc/workspace.js +++ b/packages/bruno-electron/src/ipc/workspace.js @@ -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); } diff --git a/packages/bruno-electron/src/utils/workspace-config.js b/packages/bruno-electron/src/utils/workspace-config.js index 002942ae8..ede88d43a 100644 --- a/packages/bruno-electron/src/utils/workspace-config.js +++ b/packages/bruno-electron/src/utils/workspace-config.js @@ -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 }; });