diff --git a/packages/insomnia/src/ui/components/modals/workspace-delete-modal.tsx b/packages/insomnia/src/ui/components/modals/workspace-delete-modal.tsx index bc1f1407ad..d99b0f0e78 100644 --- a/packages/insomnia/src/ui/components/modals/workspace-delete-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/workspace-delete-modal.tsx @@ -1,6 +1,5 @@ import type { Project, Workspace } from 'insomnia-data'; import { models } from 'insomnia-data'; -import { useEffect, useRef } from 'react'; import { Button, Dialog, Heading, Label, Modal, ModalOverlay, Radio, RadioGroup } from 'react-aria-components'; import { href } from 'react-router'; @@ -13,38 +12,25 @@ interface Props { workspace: Workspace; project: Project; organizationId: string; + // Driven by the host: it closes once the workspace is gone, so the overlay can play its exit animation + isOpen: boolean; onClose: () => void; - onDeleted?: () => void; } -export const WorkspaceDeleteModal = ({ workspace, project, organizationId, onClose, onDeleted }: Props) => { +export const WorkspaceDeleteModal = ({ workspace, project, organizationId, isOpen, onClose }: Props) => { const deleteWorkspaceFetcher = useWorkspaceDeleteActionFetcher(); const workspaceId = workspace._id; const workspaceName = workspace.name; - // Close once the deletion succeeds (the action redirects, so no error data is returned) - const wasSubmitting = useRef(false); - useEffect(() => { - if (deleteWorkspaceFetcher.state !== 'idle') { - wasSubmitting.current = true; - } else if (wasSubmitting.current) { - wasSubmitting.current = false; - if (!deleteWorkspaceFetcher.data?.error) { - onDeleted?.(); - onClose(); - } - } - }, [deleteWorkspaceFetcher.state, deleteWorkspaceFetcher.data, onClose, onDeleted]); - return ( onClose()} + isOpen={isOpen} + onOpenChange={open => !open && onClose()} isDismissable - className="fixed top-0 left-0 z-10 flex h-(--visual-viewport-height) w-full items-center justify-center bg-black/30" + className="fixed top-0 left-0 z-10 flex h-(--visual-viewport-height) w-full items-center justify-center bg-black/30 transition-opacity duration-150 data-exiting:opacity-0" > onClose()} + onOpenChange={open => !open && onClose()} className="max-h-full w-full max-w-2xl rounded-md border border-solid border-(--hl-sm) bg-(--color-bg) p-(--padding-lg) text-(--color-font)" > diff --git a/packages/insomnia/src/ui/components/sidebar/project-navigation-sidebar/project-navigation-sidebar.tsx b/packages/insomnia/src/ui/components/sidebar/project-navigation-sidebar/project-navigation-sidebar.tsx index 559835ec7b..8e4121767b 100644 --- a/packages/insomnia/src/ui/components/sidebar/project-navigation-sidebar/project-navigation-sidebar.tsx +++ b/packages/insomnia/src/ui/components/sidebar/project-navigation-sidebar/project-navigation-sidebar.tsx @@ -660,6 +660,7 @@ const ProjectNavigationSidebarInner = ( const [shortcutTargetItemId, setShortcutTargetItemId] = useState(null); const showRenameModal = useRenameModal(); const { showDeleteModal, deleteModal } = useDeleteModal({ + flatItems, onDeleteProject: projectId => deleteProjectFetcher.submit({ organizationId, projectId }), // Leave focus mode when its collection is deleted, otherwise the view is left empty onWorkspaceDeleted: workspaceId => { diff --git a/packages/insomnia/src/ui/components/sidebar/project-navigation-sidebar/use-delete-modal.tsx b/packages/insomnia/src/ui/components/sidebar/project-navigation-sidebar/use-delete-modal.tsx index 6640f6d258..060505bf17 100644 --- a/packages/insomnia/src/ui/components/sidebar/project-navigation-sidebar/use-delete-modal.tsx +++ b/packages/insomnia/src/ui/components/sidebar/project-navigation-sidebar/use-delete-modal.tsx @@ -1,5 +1,5 @@ import { models, services } from 'insomnia-data'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useRequestDeleteActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.delete'; import { useRequestGroupDeleteActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.delete'; @@ -8,21 +8,42 @@ import { AskModal } from '~/ui/components/modals/ask-modal'; import { WorkspaceDeleteModal } from '~/ui/components/modals/workspace-delete-modal'; import { getRequestNameOrFallback } from './request-node'; -import type { CollectionChildFlatItem, PinnedRequestFlatItem, ProjectFlatItem, WorkspaceFlatItem } from './types'; +import type { + CollectionChildFlatItem, + FlatItem, + PinnedRequestFlatItem, + ProjectFlatItem, + WorkspaceFlatItem, +} from './types'; export type DeleteTargetItem = ProjectFlatItem | WorkspaceFlatItem | CollectionChildFlatItem | PinnedRequestFlatItem; interface Options { + // Unfiltered sidebar items, used to notice when the workspace being deleted is gone + flatItems: FlatItem[]; onDeleteProject: (projectId: string) => void; onWorkspaceDeleted?: (workspaceId: string) => void; } // Returns a function to open the delete confirmation for projects, workspaces, request groups, and requests, // plus the element that hosts the workspace confirmation, which must be rendered once by the caller. -export const useDeleteModal = ({ onDeleteProject, onWorkspaceDeleted }: Options) => { +export const useDeleteModal = ({ flatItems, onDeleteProject, onWorkspaceDeleted }: Options) => { const deleteRequestFetcher = useRequestDeleteActionFetcher(); const deleteRequestGroupFetcher = useRequestGroupDeleteActionFetcher(); + // Kept after closing so the modal can play its exit animation const [workspaceToDelete, setWorkspaceToDelete] = useState(null); + const [isWorkspaceModalOpen, setIsWorkspaceModalOpen] = useState(false); + + // The workspace disappearing from the sidebar is the signal that the deletion succeeded. + // On failure (e.g. a remote error) it stays, so the modal remains open to show the error. + const isWorkspaceGone = + !!workspaceToDelete && !flatItems.some(i => i.kind === 'workspace' && i.doc._id === workspaceToDelete.doc._id); + useEffect(() => { + if (isWorkspaceModalOpen && isWorkspaceGone) { + setIsWorkspaceModalOpen(false); + onWorkspaceDeleted?.(workspaceToDelete.doc._id); + } + }, [isWorkspaceModalOpen, isWorkspaceGone, workspaceToDelete, onWorkspaceDeleted]); const showDeleteModal = (item: DeleteTargetItem) => { if (item.kind === 'project') { @@ -65,6 +86,7 @@ export const useDeleteModal = ({ onDeleteProject, onWorkspaceDeleted }: Options) return; } setWorkspaceToDelete(item); + setIsWorkspaceModalOpen(true); return; } @@ -113,11 +135,12 @@ export const useDeleteModal = ({ onDeleteProject, onWorkspaceDeleted }: Options) const deleteModal = workspaceToDelete && ( setWorkspaceToDelete(null)} - onDeleted={() => onWorkspaceDeleted?.(workspaceToDelete.doc._id)} + onClose={() => setIsWorkspaceModalOpen(false)} /> );