Skip to content

Commit 49d9c2b

Browse files
jracenetclaude
andcommitted
✨ feat(frontend): delete a package from the Context surface
Deleting a package existed in two places, and the plugin-first navigation lists neither: the packages index, which has no sidebar entry any more, and the package's own page, reachable from here only through "Open package". So the mode could create packages and never remove one, and a space accumulated the experiments made in it. The action sits in a menu on the pane header rather than beside the two buttons already there. This screen is made for reading, and a destructive control one stray click from the one that creates things is a trap; behind a menu it is still two clicks from anywhere in the surface, which is one less than the route it replaces. The confirmation says what the packages index says, plus the thing a container makes people doubt: a package is a set of memberships, so deleting one does not delete the standards, commands and skills it holds. Afterwards the surface stops naming the package it was showing. The parameter is dropped rather than pointed at another package: left in place, a shared link would ask for a package the space no longer has while the screen showed the first one instead, and dropping it falls back to that first package on its own, or to the blank state when the deleted one was the last. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent e7f69ba commit 49d9c2b

2 files changed

Lines changed: 122 additions & 0 deletions

File tree

apps/frontend/src/domain/deployments/components/context/ContextPackagePane.tsx

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
import { useCallback, useMemo, useState } from 'react';
22
import { Link, useSearchParams } from 'react-router';
33
import {
4+
PMAlertDialog,
45
PMBadge,
56
PMBox,
67
PMButton,
78
PMHStack,
89
PMHeading,
10+
PMIcon,
11+
PMIconButton,
12+
PMMenu,
13+
PMPortal,
914
PMTabsCompound,
1015
PMText,
1116
PMTooltip,
1217
PMVStack,
18+
pmToaster,
1319
} from '@packmind/ui';
20+
import { LuEllipsisVertical, LuTrash2 } from 'react-icons/lu';
1421
import type {
1522
OrganizationId,
1623
PackageResponse,
@@ -36,6 +43,8 @@ import { ContextCreateMenu } from './ContextCreateMenu';
3643
import { ContextPackageDistribution } from './ContextPackageDistribution';
3744
import { MoveComponentDialog } from './MoveComponentDialog';
3845
import { usePackageDrift } from './usePackageDrift';
46+
import { useDeletePackagesBatchMutation } from '../../api/queries/DeploymentsQueries';
47+
import { PACKAGE_MESSAGES } from '../../constants/messages';
3948

4049
const CONTENT_TAB = 'content';
4150
const DISTRIBUTION_TAB = 'distribution';
@@ -79,6 +88,7 @@ export function ContextPackagePane({
7988
packageHref,
8089
packageEditHref,
8190
distributionHistoryHref,
91+
onDeleted,
8292
}: Readonly<{
8393
pkg: PackageResponse;
8494
/** The whole space, so a component can be moved without a second query. */
@@ -101,6 +111,12 @@ export function ContextPackagePane({
101111
packageEditHref: string;
102112
/** Where the distribution events of this package are listed. */
103113
distributionHistoryHref: string;
114+
/**
115+
* The package is gone, so the surface has to stop asking for it. Deleting is
116+
* the one action here that outlives the pane: everything else changes what
117+
* the pane shows, this removes what it was showing.
118+
*/
119+
onDeleted: () => void;
104120
}>) {
105121
const [searchParams, setSearchParams] = useSearchParams();
106122
/*
@@ -129,6 +145,9 @@ export function ContextPackagePane({
129145
const [selectedKeys, setSelectedKeys] = useState<ReadonlySet<string>>(
130146
() => new Set(),
131147
);
148+
const [confirmingDelete, setConfirmingDelete] = useState(false);
149+
const { mutateAsync: deletePackages, isPending: isDeleting } =
150+
useDeletePackagesBatchMutation();
132151
/*
133152
* The picked components, resolved against what the package still holds. That
134153
* is also what repairs the selection after a move: the components that left
@@ -155,6 +174,36 @@ export function ContextPackagePane({
155174

156175
const clearSelection = useCallback(() => setSelectedKeys(new Set()), []);
157176

177+
/*
178+
* The batch mutation with one id in it, which is what the package page does
179+
* too: there is no single-package endpoint, and reaching for one here would
180+
* be a second way of deleting a package that could start behaving
181+
* differently.
182+
*/
183+
const deleteThisPackage = async () => {
184+
try {
185+
await deletePackages({
186+
packageIds: [pkg.id],
187+
spaceId,
188+
organizationId,
189+
});
190+
pmToaster.create({
191+
type: 'success',
192+
title: `Deleted ${pkg.name}`,
193+
description:
194+
'The standards, commands and skills it held stay in the space.',
195+
});
196+
setConfirmingDelete(false);
197+
onDeleted();
198+
} catch {
199+
pmToaster.create({
200+
type: 'error',
201+
title: `Couldn't delete ${pkg.name}`,
202+
description: 'Try again, or check your space access.',
203+
});
204+
}
205+
};
206+
158207
const tab =
159208
searchParams.get(TAB_PARAM) === DISTRIBUTION_TAB
160209
? DISTRIBUTION_TAB
@@ -282,6 +331,42 @@ export function ContextPackagePane({
282331
spaceSlug={spaceSlug}
283332
packageId={pkg.id}
284333
/>
334+
{/*
335+
Deleting the package, behind a menu rather than beside the two
336+
buttons: the plugin-first navigation has no packages list, so this
337+
is the only place the action exists, and a destructive control on
338+
a screen made for reading should not be one stray click from the
339+
one that creates things.
340+
*/}
341+
<PMMenu.Root>
342+
<PMMenu.Trigger asChild>
343+
<PMIconButton
344+
aria-label={`More actions for ${pkg.name}`}
345+
variant="tertiary"
346+
size="sm"
347+
>
348+
<LuEllipsisVertical />
349+
</PMIconButton>
350+
</PMMenu.Trigger>
351+
<PMPortal>
352+
<PMMenu.Positioner>
353+
<PMMenu.Content>
354+
<PMMenu.Item
355+
value="delete-package"
356+
color="text.error"
357+
onClick={() => setConfirmingDelete(true)}
358+
>
359+
<PMHStack gap={2}>
360+
<PMIcon>
361+
<LuTrash2 />
362+
</PMIcon>
363+
Delete package
364+
</PMHStack>
365+
</PMMenu.Item>
366+
</PMMenu.Content>
367+
</PMMenu.Positioner>
368+
</PMPortal>
369+
</PMMenu.Root>
285370
</PMHStack>
286371
</PMHStack>
287372

@@ -400,6 +485,23 @@ export function ContextPackagePane({
400485
</PMTabsCompound.Content>
401486

402487
{moveDialog}
488+
{/*
489+
What the message adds to the confirmation the packages list uses: a
490+
package is a set of memberships, so deleting one is not deleting what it
491+
holds, and that is the question the dialog is answering.
492+
*/}
493+
<PMAlertDialog
494+
title="Delete package"
495+
message={`${PACKAGE_MESSAGES.confirmation.deletePackage(
496+
pkg.name,
497+
)} The standards, commands and skills it holds stay in the space.`}
498+
confirmText="Delete"
499+
cancelText="Cancel"
500+
onConfirm={() => void deleteThisPackage()}
501+
open={confirmingDelete}
502+
onOpenChange={({ open }) => setConfirmingDelete(open)}
503+
isLoading={isDeleting}
504+
/>
403505
</PMTabsCompound.Root>
404506
);
405507
}

apps/frontend/src/domain/deployments/components/context/SpaceContextSurface.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,25 @@ export function SpaceContextSurface() {
208208
[setSearchParams],
209209
);
210210

211+
/*
212+
* The open package was deleted, so the address stops naming it: left in
213+
* place, a shared link would ask for a package the space no longer has, and
214+
* the surface would answer with the first one under a URL that says
215+
* otherwise. Dropping the parameter falls back to the first package, or to
216+
* the blank state when that was the last one.
217+
*/
218+
const forgetPackage = useCallback(() => {
219+
setSearchParams(
220+
(previous) => {
221+
previous.delete(PACKAGE_PARAM);
222+
previous.delete(COMPONENT_PARAM);
223+
previous.delete(FILE_PARAM);
224+
return previous;
225+
},
226+
{ replace: true },
227+
);
228+
}, [setSearchParams]);
229+
211230
const selectPackage = useCallback(
212231
(packageId: PackageId) => show(packageId),
213232
[show],
@@ -326,6 +345,7 @@ export function SpaceContextSurface() {
326345
spaceSlug,
327346
selectedPackage.id,
328347
)}?tab=distributions`}
348+
onDeleted={forgetPackage}
329349
/>
330350
)
331351
)}

0 commit comments

Comments
 (0)