Skip to content

Commit 9129a5b

Browse files
fix: workspace hard delete nits (#202)
1 parent 05ed2e2 commit 9129a5b

6 files changed

+135
-10
lines changed

src/features/workspace/components/__tests__/archive-workspace.test.tsx

+39-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ import { render } from "@/lib/test-utils";
22
import { ArchiveWorkspace } from "../archive-workspace";
33
import userEvent from "@testing-library/user-event";
44
import { waitFor } from "@testing-library/react";
5+
import { server } from "@/mocks/msw/node";
6+
import { http, HttpResponse } from "msw";
57

68
test("has correct buttons when not archived", async () => {
7-
const { getByRole } = render(
9+
const { getByRole, queryByRole } = render(
810
<ArchiveWorkspace isArchived={false} workspaceName="foo-bar" />,
911
);
1012

1113
expect(getByRole("button", { name: /archive/i })).toBeVisible();
14+
expect(queryByRole("button", { name: /contextual help/i })).toBe(null);
1215
});
1316

1417
test("has correct buttons when archived", async () => {
@@ -60,3 +63,38 @@ test("can permanently delete archived workspace", async () => {
6063
expect(getByText(/permanently deleted "foo-bar" workspace/i)).toBeVisible();
6164
});
6265
});
66+
67+
test("can't archive active workspace", async () => {
68+
server.use(
69+
http.get("*/api/v1/workspaces/active", () =>
70+
HttpResponse.json({
71+
workspaces: [
72+
{
73+
name: "foo",
74+
is_active: true,
75+
last_updated: new Date(Date.now()).toISOString(),
76+
},
77+
],
78+
}),
79+
),
80+
);
81+
const { getByRole } = render(
82+
<ArchiveWorkspace workspaceName="foo" isArchived={false} />,
83+
);
84+
85+
await waitFor(() => {
86+
expect(getByRole("button", { name: /archive/i })).toBeDisabled();
87+
expect(getByRole("button", { name: /contextual help/i })).toBeVisible();
88+
});
89+
});
90+
91+
test("can't archive default workspace", async () => {
92+
const { getByRole } = render(
93+
<ArchiveWorkspace workspaceName="default" isArchived={false} />,
94+
);
95+
96+
await waitFor(() => {
97+
expect(getByRole("button", { name: /archive/i })).toBeDisabled();
98+
expect(getByRole("button", { name: /contextual help/i })).toBeVisible();
99+
});
100+
});

src/features/workspace/components/__tests__/workspace-name.test.tsx

+33
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { test, expect } from "vitest";
22
import { WorkspaceName } from "../workspace-name";
33
import { render, waitFor } from "@/lib/test-utils";
44
import userEvent from "@testing-library/user-event";
5+
import { server } from "@/mocks/msw/node";
6+
import { http, HttpResponse } from "msw";
57

68
test("can rename workspace", async () => {
79
const { getByRole, getByText } = render(
@@ -29,3 +31,34 @@ test("can't rename archived workspace", async () => {
2931
expect(getByRole("textbox", { name: /workspace name/i })).toBeDisabled();
3032
expect(getByRole("button", { name: /save/i })).toBeDisabled();
3133
});
34+
35+
test("can't rename active workspace", async () => {
36+
server.use(
37+
http.get("*/api/v1/workspaces/active", () =>
38+
HttpResponse.json({
39+
workspaces: [
40+
{
41+
name: "foo",
42+
is_active: true,
43+
last_updated: new Date(Date.now()).toISOString(),
44+
},
45+
],
46+
}),
47+
),
48+
);
49+
const { getByRole } = render(
50+
<WorkspaceName workspaceName="foo" isArchived={true} />,
51+
);
52+
53+
expect(getByRole("textbox", { name: /workspace name/i })).toBeDisabled();
54+
expect(getByRole("button", { name: /save/i })).toBeDisabled();
55+
});
56+
57+
test("can't rename default workspace", async () => {
58+
const { getByRole } = render(
59+
<WorkspaceName workspaceName="foo" isArchived={true} />,
60+
);
61+
62+
expect(getByRole("textbox", { name: /workspace name/i })).toBeDisabled();
63+
expect(getByRole("button", { name: /save/i })).toBeDisabled();
64+
});

src/features/workspace/components/archive-workspace.tsx

+50-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,62 @@
1-
import { Card, CardBody, Button, Text } from "@stacklok/ui-kit";
1+
import {
2+
Card,
3+
CardBody,
4+
Button,
5+
Text,
6+
TooltipTrigger,
7+
Tooltip,
8+
TooltipInfoButton,
9+
} from "@stacklok/ui-kit";
210
import { twMerge } from "tailwind-merge";
311
import { useRestoreWorkspaceButton } from "../hooks/use-restore-workspace-button";
412
import { useArchiveWorkspaceButton } from "../hooks/use-archive-workspace-button";
513
import { useConfirmHardDeleteWorkspace } from "../hooks/use-confirm-hard-delete-workspace";
614
import { useNavigate } from "react-router-dom";
715
import { hrefs } from "@/lib/hrefs";
16+
import { useActiveWorkspaceName } from "../hooks/use-active-workspace-name";
17+
18+
function getContextualText({
19+
activeWorkspaceName,
20+
workspaceName,
21+
}: {
22+
workspaceName: string;
23+
activeWorkspaceName: string;
24+
}) {
25+
if (workspaceName === activeWorkspaceName) {
26+
return "Cannot archive the active workspace";
27+
}
28+
if (workspaceName === "default") {
29+
return "Cannot archive the default workspace";
30+
}
31+
return null;
32+
}
33+
34+
// NOTE: You can't show a tooltip on a disabled button
35+
// React Aria's recommended approach is https://spectrum.adobe.com/page/contextual-help/
36+
function ContextualHelp({ workspaceName }: { workspaceName: string }) {
37+
const { data: activeWorkspaceName } = useActiveWorkspaceName();
38+
if (!activeWorkspaceName) return null;
39+
40+
const text = getContextualText({ activeWorkspaceName, workspaceName });
41+
if (!text) return null;
42+
43+
return (
44+
<TooltipTrigger delay={0}>
45+
<TooltipInfoButton aria-label="Contextual help" />
46+
<Tooltip>{text}</Tooltip>
47+
</TooltipTrigger>
48+
);
49+
}
850

951
const ButtonsUnarchived = ({ workspaceName }: { workspaceName: string }) => {
1052
const archiveButtonProps = useArchiveWorkspaceButton({ workspaceName });
1153

12-
return <Button {...archiveButtonProps} />;
54+
return (
55+
<div className="flex gap-2 items-center">
56+
<Button {...archiveButtonProps} />
57+
<ContextualHelp workspaceName={workspaceName} />
58+
</div>
59+
);
1360
};
1461

1562
const ButtonsArchived = ({ workspaceName }: { workspaceName: string }) => {
@@ -51,7 +98,7 @@ export function ArchiveWorkspace({
5198
<CardBody className="flex justify-between items-center">
5299
<div>
53100
<Text className="text-primary">Archive Workspace</Text>
54-
<Text className="flex items-center text-secondary mb-0">
101+
<Text className="flex items-center text-secondary mb-0 text-balance">
55102
Archiving this workspace removes it from the main workspaces list,
56103
though it can be restored if needed.
57104
</Text>

src/features/workspace/components/workspace-custom-instructions.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@ export function WorkspaceCustomInstructions({
186186
<CardBody>
187187
<Text className="text-primary">Custom instructions</Text>
188188
<Text className="text-secondary mb-4">
189-
Pass custom instructions to your LLM to augment it's behavior, and
190-
save time & tokens.
189+
Pass custom instructions to your LLM to augment its behavior, and save
190+
time & tokens.
191191
</Text>
192192
<div className="border border-gray-200 rounded overflow-hidden">
193193
{isCustomInstructionsPending ? (

src/features/workspace/hooks/use-archive-workspace-button.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
import { Button } from "@stacklok/ui-kit";
22
import { ComponentProps } from "react";
33
import { useMutationArchiveWorkspace } from "@/features/workspace/hooks/use-mutation-archive-workspace";
4+
import { useActiveWorkspaceName } from "./use-active-workspace-name";
45

56
export function useArchiveWorkspaceButton({
67
workspaceName,
78
}: {
89
workspaceName: string;
910
}): ComponentProps<typeof Button> {
11+
const { data: activeWorkspaceName } = useActiveWorkspaceName();
1012
const { mutateAsync, isPending } = useMutationArchiveWorkspace();
1113

1214
return {
1315
isPending,
14-
isDisabled: isPending,
16+
isDisabled:
17+
isPending ||
18+
workspaceName === activeWorkspaceName ||
19+
workspaceName === "default",
1520
onPress: () => mutateAsync({ path: { workspace_name: workspaceName } }),
1621
isDestructive: true,
1722
children: "Archive",

src/features/workspace/hooks/use-confirm-hard-delete-workspace.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ export function useConfirmHardDeleteWorkspace() {
1111
async (...params: Parameters<typeof hardDeleteWorkspace>) => {
1212
const answer = await confirm(
1313
<>
14-
<p>Are you sure you want to delete this workspace?</p>
14+
<p className="mb-1">
15+
Are you sure you want to permanently delete this workspace?
16+
</p>
1517
<p>
16-
You will lose any custom instructions, or other configuration.{" "}
17-
<b>This action cannot be undone.</b>
18+
You will lose all configuration and data associated with this
19+
workspace, like prompt history or alerts.
1820
</p>
1921
</>,
2022
{

0 commit comments

Comments
 (0)