Skip to content

Commit a98492a

Browse files
feat: implement hard delete for workspaces & refactor workspaces table to allow multiple actions (#185)
* feat: useKbdShortcuts hook & example implementation * chore: tidy up remnant * feat: useToastMutation hook * chore: remove junk comment * feat: implement `useToastMutation` for workspaces * refactor: `useQueries` to fetch workspaces data * feat: implement "hard delete" from workspaces table * chore: tidy ups * feat: add keyboard tooltip to create workspace * fix(workspaces): add badge to active workspace * test: test workspace actions & hard delete * chore: tidy up test * fix(workspace name): correct message on rename + test * chore: move code around * fix(custom instructions): failing test & rename to match API changes
1 parent 646ed5a commit a98492a

32 files changed

+1157
-273
lines changed

src/context/confirm-context.tsx

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"use client";
2+
3+
import {
4+
Button,
5+
Dialog,
6+
DialogContent,
7+
DialogFooter,
8+
DialogHeader,
9+
DialogModal,
10+
DialogModalOverlay,
11+
DialogTitle,
12+
} from "@stacklok/ui-kit";
13+
import type { ReactNode } from "react";
14+
import { createContext, useState } from "react";
15+
16+
type Buttons = {
17+
yes: ReactNode;
18+
no: ReactNode;
19+
};
20+
21+
type Config = {
22+
buttons: Buttons;
23+
title?: ReactNode;
24+
isDestructive?: boolean;
25+
};
26+
27+
type Question = {
28+
message: ReactNode;
29+
config: Config;
30+
resolve: (value: boolean) => void;
31+
};
32+
33+
type ConfirmContextType = {
34+
confirm: (message: ReactNode, config: Config) => Promise<boolean>;
35+
};
36+
37+
export const ConfirmContext = createContext<ConfirmContextType | null>(null);
38+
39+
export function ConfirmProvider({ children }: { children: ReactNode }) {
40+
const [activeQuestion, setActiveQuestion] = useState<Question | null>(null);
41+
const [isOpen, setIsOpen] = useState<boolean>(false);
42+
43+
const handleAnswer = (answer: boolean) => {
44+
if (activeQuestion === null) return;
45+
activeQuestion.resolve(answer);
46+
setIsOpen(false);
47+
};
48+
49+
const confirm = (message: ReactNode, config: Config) => {
50+
return new Promise<boolean>((resolve) => {
51+
setActiveQuestion({ message, config, resolve });
52+
setIsOpen(true);
53+
});
54+
};
55+
56+
return (
57+
<ConfirmContext.Provider value={{ confirm }}>
58+
{children}
59+
60+
<DialogModalOverlay isDismissable={false} isOpen={isOpen}>
61+
<DialogModal>
62+
<Dialog>
63+
<DialogHeader>
64+
<DialogTitle>{activeQuestion?.config.title}</DialogTitle>
65+
</DialogHeader>
66+
<DialogContent>{activeQuestion?.message}</DialogContent>
67+
<DialogFooter>
68+
<div className="flex grow justify-end gap-2">
69+
<Button variant="secondary" onPress={() => handleAnswer(false)}>
70+
{activeQuestion?.config.buttons.no ?? "&nbsp;"}
71+
</Button>
72+
<Button
73+
isDestructive={activeQuestion?.config.isDestructive}
74+
variant="primary"
75+
onPress={() => handleAnswer(true)}
76+
>
77+
{activeQuestion?.config.buttons.yes ?? "&nbsp;"}
78+
</Button>
79+
</div>
80+
</DialogFooter>
81+
</Dialog>
82+
</DialogModal>
83+
</DialogModalOverlay>
84+
</ConfirmContext.Provider>
85+
);
86+
}

src/features/workspace-system-prompt/hooks/use-archive-workspace.ts

-15
This file was deleted.

src/features/workspace-system-prompt/hooks/use-set-system-prompt.tsx

-11
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,62 @@
11
import { render } from "@/lib/test-utils";
22
import { ArchiveWorkspace } from "../archive-workspace";
33
import userEvent from "@testing-library/user-event";
4-
import { screen, waitFor } from "@testing-library/react";
5-
6-
const mockNavigate = vi.fn();
7-
8-
vi.mock("react-router-dom", async () => {
9-
const original =
10-
await vi.importActual<typeof import("react-router-dom")>(
11-
"react-router-dom",
12-
);
13-
return {
14-
...original,
15-
useNavigate: () => mockNavigate,
16-
};
4+
import { waitFor } from "@testing-library/react";
5+
6+
test("has correct buttons when not archived", async () => {
7+
const { getByRole } = render(
8+
<ArchiveWorkspace isArchived={false} workspaceName="foo-bar" />,
9+
);
10+
11+
expect(getByRole("button", { name: /archive/i })).toBeVisible();
12+
});
13+
14+
test("has correct buttons when archived", async () => {
15+
const { getByRole } = render(
16+
<ArchiveWorkspace isArchived={true} workspaceName="foo-bar" />,
17+
);
18+
expect(getByRole("button", { name: /restore/i })).toBeVisible();
19+
expect(getByRole("button", { name: /permanently delete/i })).toBeVisible();
20+
});
21+
22+
test("can archive workspace", async () => {
23+
const { getByText, getByRole } = render(
24+
<ArchiveWorkspace isArchived={false} workspaceName="foo-bar" />,
25+
);
26+
27+
await userEvent.click(getByRole("button", { name: /archive/i }));
28+
29+
await waitFor(() => {
30+
expect(getByText(/archived "foo-bar" workspace/i)).toBeVisible();
31+
});
1732
});
1833

19-
test("archive workspace", async () => {
20-
render(<ArchiveWorkspace isArchived={false} workspaceName="foo" />);
34+
test("can restore archived workspace", async () => {
35+
const { getByText, getByRole } = render(
36+
<ArchiveWorkspace isArchived={true} workspaceName="foo-bar" />,
37+
);
38+
39+
await userEvent.click(getByRole("button", { name: /restore/i }));
40+
41+
await waitFor(() => {
42+
expect(getByText(/restored "foo-bar" workspace/i)).toBeVisible();
43+
});
44+
});
45+
46+
test("can permanently delete archived workspace", async () => {
47+
const { getByText, getByRole } = render(
48+
<ArchiveWorkspace isArchived={true} workspaceName="foo-bar" />,
49+
);
50+
51+
await userEvent.click(getByRole("button", { name: /permanently delete/i }));
52+
53+
await waitFor(() => {
54+
expect(getByRole("dialog", { name: /permanently delete/i })).toBeVisible();
55+
});
2156

22-
await userEvent.click(screen.getByRole("button", { name: /archive/i }));
23-
await waitFor(() => expect(mockNavigate).toHaveBeenCalledTimes(1));
24-
expect(mockNavigate).toHaveBeenCalledWith("/workspaces");
57+
await userEvent.click(getByRole("button", { name: /delete/i }));
2558

2659
await waitFor(() => {
27-
expect(screen.getByText(/archived "(.*)" workspace/i)).toBeVisible();
60+
expect(getByText(/permanently deleted "foo-bar" workspace/i)).toBeVisible();
2861
});
2962
});

0 commit comments

Comments
 (0)