mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-21 22:57:59 -04:00
Fix: Deleting a remote project deletes it from the remote (#6782)
* add delete modal and update action * exclude git synced workspaces * update e2e tests
This commit is contained in:
@@ -118,8 +118,9 @@ test.describe('Dashboard', async () => {
|
||||
// Delete document
|
||||
await page.click('text=Documenttest123just now >> button');
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
await page.locator('text=Yes').click();
|
||||
await expect(workspaceCards).toHaveCount(1);
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
// @TODO: Re-enable - Requires mocking VCS operations
|
||||
// await expect(workspaceCards).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('Can create, rename and delete a collection', async ({ page }) => {
|
||||
@@ -154,8 +155,9 @@ test.describe('Dashboard', async () => {
|
||||
// Delete collection
|
||||
await page.click('text=Collectiontest123just now >> button');
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
await page.locator('text=Yes').click();
|
||||
await expect(workspaceCards).toHaveCount(1);
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
// @TODO: Re-enable - Requires mocking VCS operations
|
||||
// await expect(workspaceCards).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { FC, Fragment, useCallback, useState } from 'react';
|
||||
import { Button, Dialog, Heading, Modal, ModalOverlay } from 'react-aria-components';
|
||||
import { useFetcher, useParams } from 'react-router-dom';
|
||||
|
||||
import { parseApiSpec } from '../../../common/api-specs';
|
||||
@@ -8,7 +9,7 @@ import { RENDER_PURPOSE_NO_RENDER } from '../../../common/render';
|
||||
import type { ApiSpec } from '../../../models/api-spec';
|
||||
import { CaCertificate } from '../../../models/ca-certificate';
|
||||
import { ClientCertificate } from '../../../models/client-certificate';
|
||||
import { Project } from '../../../models/project';
|
||||
import { isRemoteProject, Project } from '../../../models/project';
|
||||
import type { Workspace } from '../../../models/workspace';
|
||||
import { WorkspaceScopeKeys } from '../../../models/workspace';
|
||||
import { WorkspaceMeta } from '../../../models/workspace-meta';
|
||||
@@ -17,8 +18,8 @@ import { getDocumentActions } from '../../../plugins';
|
||||
import * as pluginContexts from '../../../plugins/context';
|
||||
import { useLoadingRecord } from '../../hooks/use-loading-record';
|
||||
import { Dropdown, DropdownButton, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
|
||||
import { showError, showModal, showPrompt } from '../modals';
|
||||
import { AskModal } from '../modals/ask-modal';
|
||||
import { Icon } from '../icon';
|
||||
import { showError, showPrompt } from '../modals';
|
||||
import { ExportRequestsModal } from '../modals/export-requests-modal';
|
||||
import { ImportModal } from '../modals/import-modal';
|
||||
import { WorkspaceDuplicateModal } from '../modals/workspace-duplicate-modal';
|
||||
@@ -90,11 +91,14 @@ export const WorkspaceCardDropdown: FC<Props> = props => {
|
||||
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
|
||||
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
|
||||
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
|
||||
const [isDeleteRemoteWorkspaceModalOpen, setIsDeleteRemoteWorkspaceModalOpen] = useState(false);
|
||||
const {
|
||||
organizationId,
|
||||
projectId,
|
||||
} = useParams() as { organizationId: string; projectId: string };
|
||||
|
||||
const deleteWorkspaceFetcher = useFetcher();
|
||||
|
||||
const workspaceName = workspace.name;
|
||||
const projectName = project.name ?? getProductName();
|
||||
const { refresh, renderPluginDropdownItems } = useDocumentActionPlugins(props);
|
||||
@@ -172,26 +176,7 @@ export const WorkspaceCardDropdown: FC<Props> = props => {
|
||||
icon="trash-o"
|
||||
className="danger"
|
||||
onClick={() => {
|
||||
const label = getWorkspaceLabel(workspace);
|
||||
showModal(AskModal, {
|
||||
title: `Delete ${label.singular}`,
|
||||
message: `Do you really want to delete "${workspaceName}"?`,
|
||||
yesText: 'Yes',
|
||||
noText: 'Cancel',
|
||||
onDone: async (isYes: boolean) => {
|
||||
if (!isYes) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetcher.submit(
|
||||
{ workspaceId: workspace._id },
|
||||
{
|
||||
action: `/organization/${organizationId}/project/${workspace.parentId}/workspace/delete`,
|
||||
method: 'post',
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
setIsDeleteRemoteWorkspaceModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
</DropdownItem>
|
||||
@@ -230,6 +215,64 @@ export const WorkspaceCardDropdown: FC<Props> = props => {
|
||||
onHide={() => setIsSettingsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{isDeleteRemoteWorkspaceModalOpen && (
|
||||
<ModalOverlay
|
||||
isOpen
|
||||
onOpenChange={() => {
|
||||
setIsDeleteRemoteWorkspaceModalOpen(false);
|
||||
}}
|
||||
isDismissable
|
||||
className="w-full h-[--visual-viewport-height] fixed z-10 top-0 left-0 flex items-center justify-center bg-black/30"
|
||||
>
|
||||
<Modal className="max-w-2xl w-full rounded-md border border-solid border-[--hl-sm] p-[--padding-lg] max-h-full bg-[--color-bg] text-[--color-font]">
|
||||
<Dialog
|
||||
onClose={() => {
|
||||
setIsDeleteRemoteWorkspaceModalOpen(false);
|
||||
}}
|
||||
className="outline-none"
|
||||
>
|
||||
{({ close }) => (
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex gap-2 items-center justify-between'>
|
||||
<Heading className='text-2xl'>Delete {getWorkspaceLabel(workspace).singular}</Heading>
|
||||
<Button
|
||||
className="flex flex-shrink-0 items-center justify-center aspect-square h-6 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
onPress={close}
|
||||
>
|
||||
<Icon icon="x" />
|
||||
</Button>
|
||||
</div>
|
||||
<deleteWorkspaceFetcher.Form
|
||||
action={`/organization/${organizationId}/project/${workspace.parentId}/workspace/delete`}
|
||||
method="POST"
|
||||
className='flex flex-col gap-4'
|
||||
>
|
||||
<input type="hidden" name="workspaceId" value={workspace._id} />
|
||||
<p>
|
||||
This will permanently delete the {<strong style={{ whiteSpace: 'pre-wrap' }}>{workspace?.name}</strong>}{' '}
|
||||
{getWorkspaceLabel(workspace).singular} {isRemoteProject(project) ? 'remotely' : ''}.
|
||||
</p>
|
||||
{deleteWorkspaceFetcher.data && deleteWorkspaceFetcher.data.error && (
|
||||
<p className="notice error margin-bottom-sm no-margin-top">
|
||||
{deleteWorkspaceFetcher.data.error}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
className="hover:no-underline bg-[--color-danger] hover:bg-opacity-90 border border-solid border-[--hl-md] py-2 px-3 text-[--color-font-danger] transition-colors rounded-sm"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</deleteWorkspaceFetcher.Form>
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
)}
|
||||
{ }
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IconName } from '@fortawesome/fontawesome-svg-core';
|
||||
import React, { FC, ReactNode, useCallback, useState } from 'react';
|
||||
import { Button, Item, Menu, MenuTrigger, Popover } from 'react-aria-components';
|
||||
import { Button, Dialog, Heading, Item, Menu, MenuTrigger, Modal, ModalOverlay, Popover } from 'react-aria-components';
|
||||
import { useFetcher, useParams, useRouteLoaderData } from 'react-router-dom';
|
||||
|
||||
import { isLoggedIn } from '../../../account/session';
|
||||
@@ -8,6 +8,7 @@ import { getProductName } from '../../../common/constants';
|
||||
import { database as db } from '../../../common/database';
|
||||
import { getWorkspaceLabel } from '../../../common/get-workspace-label';
|
||||
import { RENDER_PURPOSE_NO_RENDER } from '../../../common/render';
|
||||
import { isRemoteProject } from '../../../models/project';
|
||||
import { isRequest } from '../../../models/request';
|
||||
import { isRequestGroup } from '../../../models/request-group';
|
||||
import { isDesign, isScratchpad, Workspace } from '../../../models/workspace';
|
||||
@@ -53,7 +54,8 @@ export const WorkspaceDropdown: FC = () => {
|
||||
const workspaceName = activeWorkspace.name;
|
||||
const projectName = activeProject.name ?? getProductName();
|
||||
const fetcher = useFetcher();
|
||||
|
||||
const [isDeleteRemoteWorkspaceModalOpen, setIsDeleteRemoteWorkspaceModalOpen] = useState(false);
|
||||
const deleteWorkspaceFetcher = useFetcher();
|
||||
const [actionPlugins, setActionPlugins] = useState<WorkspaceAction[]>([]);
|
||||
const [loadingActions, setLoadingActions] = useState<Record<string, boolean>>({});
|
||||
|
||||
@@ -141,6 +143,14 @@ export const WorkspaceDropdown: FC = () => {
|
||||
},
|
||||
}] : [],
|
||||
{
|
||||
id: 'delete',
|
||||
name: 'Delete',
|
||||
icon: <Icon icon='trash' />,
|
||||
action: () => {
|
||||
setIsDeleteRemoteWorkspaceModalOpen(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'import',
|
||||
name: 'Import',
|
||||
icon: <Icon icon='file-import' />,
|
||||
@@ -252,6 +262,63 @@ export const WorkspaceDropdown: FC = () => {
|
||||
onHide={() => setIsSettingsModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{isDeleteRemoteWorkspaceModalOpen && (
|
||||
<ModalOverlay
|
||||
isOpen
|
||||
onOpenChange={() => {
|
||||
setIsDeleteRemoteWorkspaceModalOpen(false);
|
||||
}}
|
||||
isDismissable
|
||||
className="w-full h-[--visual-viewport-height] fixed z-10 top-0 left-0 flex items-center justify-center bg-black/30"
|
||||
>
|
||||
<Modal className="max-w-2xl w-full rounded-md border border-solid border-[--hl-sm] p-[--padding-lg] max-h-full bg-[--color-bg] text-[--color-font]">
|
||||
<Dialog
|
||||
onClose={() => {
|
||||
setIsDeleteRemoteWorkspaceModalOpen(false);
|
||||
}}
|
||||
className="outline-none"
|
||||
>
|
||||
{({ close }) => (
|
||||
<div className='flex flex-col gap-4'>
|
||||
<div className='flex gap-2 items-center justify-between'>
|
||||
<Heading className='text-2xl'>Delete {getWorkspaceLabel(activeWorkspace).singular}</Heading>
|
||||
<Button
|
||||
className="flex flex-shrink-0 items-center justify-center aspect-square h-6 aria-pressed:bg-[--hl-sm] rounded-sm text-[--color-font] hover:bg-[--hl-xs] focus:ring-inset ring-1 ring-transparent focus:ring-[--hl-md] transition-all text-sm"
|
||||
onPress={close}
|
||||
>
|
||||
<Icon icon="x" />
|
||||
</Button>
|
||||
</div>
|
||||
<deleteWorkspaceFetcher.Form
|
||||
action={`/organization/${organizationId}/project/${activeWorkspace.parentId}/workspace/delete`}
|
||||
method="POST"
|
||||
className='flex flex-col gap-4'
|
||||
>
|
||||
<input type="hidden" name="workspaceId" value={activeWorkspace._id} />
|
||||
<p>
|
||||
This will permanently delete the {<strong style={{ whiteSpace: 'pre-wrap' }}>{activeWorkspace?.name}</strong>}{' '}
|
||||
{getWorkspaceLabel(activeWorkspace).singular} {isRemoteProject(activeProject) ? 'remotely' : ''}.
|
||||
</p>
|
||||
{deleteWorkspaceFetcher.data && deleteWorkspaceFetcher.data.error && (
|
||||
<p className="notice error margin-bottom-sm no-margin-top">
|
||||
{deleteWorkspaceFetcher.data.error}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
className="hover:no-underline bg-[--color-danger] hover:bg-opacity-90 border border-solid border-[--hl-md] py-2 px-3 text-[--color-font-danger] transition-colors rounded-sm"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</deleteWorkspaceFetcher.Form>
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -145,13 +145,6 @@ export const WorkspaceSettingsModal = ({ workspace, clientCertificates, caCertif
|
||||
});
|
||||
_handleToggleCertificateForm();
|
||||
};
|
||||
const _handleRemoveWorkspace = async () => {
|
||||
const workspaceId = workspace._id;
|
||||
workspaceFetcher.submit({ workspaceId }, {
|
||||
action: `/organization/${organizationId}/project/${projectId}/workspace/delete`,
|
||||
method: 'post',
|
||||
});
|
||||
};
|
||||
|
||||
const renderCertificate = (certificate: ClientCertificate) => {
|
||||
return (
|
||||
@@ -301,14 +294,6 @@ export const WorkspaceSettingsModal = ({ workspace, clientCertificates, caCertif
|
||||
</div>
|
||||
<h2>Actions</h2>
|
||||
<div className="form-control form-control--padded">
|
||||
{!isScratchpadWorkspace && (
|
||||
<PromptButton
|
||||
onClick={_handleRemoveWorkspace}
|
||||
className="width-auto btn btn--clicky inline-block"
|
||||
>
|
||||
<i className="fa fa-trash-o" /> Delete
|
||||
</PromptButton>
|
||||
)}
|
||||
<PromptButton
|
||||
onClick={_handleClearAllResponses}
|
||||
className="width-auto btn btn--clicky inline-block space-left"
|
||||
|
||||
@@ -12,6 +12,7 @@ import { importResourcesToWorkspace, scanResources } from '../../common/import';
|
||||
import { generateId } from '../../common/misc';
|
||||
import * as models from '../../models';
|
||||
import { getById, update } from '../../models/helpers/request-operations';
|
||||
import { isRemoteProject } from '../../models/project';
|
||||
import { isRequest, Request } from '../../models/request';
|
||||
import { isRequestGroup, isRequestGroupId } from '../../models/request-group';
|
||||
import { UnitTest } from '../../models/unit-test';
|
||||
@@ -334,19 +335,25 @@ export const deleteWorkspaceAction: ActionFunction = async ({
|
||||
invariant(typeof workspaceId === 'string', 'Workspace ID is required');
|
||||
|
||||
const workspace = await models.workspace.getById(workspaceId);
|
||||
const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspaceId);
|
||||
invariant(workspace, 'Workspace not found');
|
||||
if (isRemoteProject(project) && !workspaceMeta.gitRepositoryId) {
|
||||
try {
|
||||
const vcs = VCSInstance();
|
||||
await vcs.switchAndCreateBackendProjectIfNotExist(workspaceId, workspace.name);
|
||||
const backendProject = await vcs._getBackendProjectByRootDocument(workspace._id);
|
||||
await vcs._removeProject(backendProject);
|
||||
await vcs.archiveProject();
|
||||
} catch (err) {
|
||||
return {
|
||||
error: err instanceof Error ? err.message : `An unexpected error occurred while deleting the workspace. Please try again. ${err}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await models.stats.incrementDeletedRequestsForDescendents(workspace);
|
||||
await models.workspace.remove(workspace);
|
||||
|
||||
try {
|
||||
const vcs = VCSInstance();
|
||||
const backendProject = await vcs._getBackendProjectByRootDocument(workspace._id);
|
||||
await vcs._removeProject(backendProject);
|
||||
} catch (err) {
|
||||
console.warn('Failed to remove project from VCS', err);
|
||||
}
|
||||
console.log(`redirecting to /organization/${organizationId}/project/${projectId}`);
|
||||
return redirect(`/organization/${organizationId}/project/${projectId}`);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user