Skip to content

feat: implement hard delete for workspaces & refactor workspaces table to allow multiple actions #185

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f2cf621
feat: useKbdShortcuts hook & example implementation
alex-mcgovern Jan 23, 2025
9973939
chore: tidy up remnant
alex-mcgovern Jan 23, 2025
567767f
Merge branch 'main' of github.com:stacklok/codegate-ui into feat/use-…
alex-mcgovern Jan 23, 2025
13249e9
feat: useToastMutation hook
alex-mcgovern Jan 23, 2025
343c342
chore: remove junk comment
alex-mcgovern Jan 23, 2025
3796ddd
feat: implement `useToastMutation` for workspaces
alex-mcgovern Jan 23, 2025
0229a15
Merge branch 'main' of github.com:stacklok/codegate-ui into feat/impl…
alex-mcgovern Jan 23, 2025
41da9bc
Merge branch 'main' of github.com:stacklok/codegate-ui into feat/impl…
alex-mcgovern Jan 23, 2025
e5ffc24
refactor: `useQueries` to fetch workspaces data
alex-mcgovern Jan 23, 2025
66a9157
feat: implement "hard delete" from workspaces table
alex-mcgovern Jan 23, 2025
dc4cd3c
chore: tidy ups
alex-mcgovern Jan 23, 2025
c906a19
feat: add keyboard tooltip to create workspace
alex-mcgovern Jan 23, 2025
0f5df0a
fix(workspaces): add badge to active workspace
alex-mcgovern Jan 24, 2025
9218878
test: test workspace actions & hard delete
alex-mcgovern Jan 24, 2025
67d0d9c
Merge branch 'main' of github.com:stacklok/codegate-ui into feat/impl…
alex-mcgovern Jan 24, 2025
ca05c01
chore: tidy up test
alex-mcgovern Jan 24, 2025
4a9db8b
fix(workspace name): correct message on rename + test
alex-mcgovern Jan 24, 2025
8b58309
chore: move code around
alex-mcgovern Jan 24, 2025
a5765f5
fix(custom instructions): failing test & rename to match API changes
alex-mcgovern Jan 24, 2025
ce98c6c
Merge branch 'main' into feat/implement-hard-delete
alex-mcgovern Jan 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions src/context/confirm-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"use client";

import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogModal,
DialogModalOverlay,
DialogTitle,
} from "@stacklok/ui-kit";
import type { ReactNode } from "react";
import { createContext, useState } from "react";

type Buttons = {
yes: ReactNode;
no: ReactNode;
};

type Config = {
buttons: Buttons;
title?: ReactNode;
isDestructive?: boolean;
};

type Question = {
message: ReactNode;
config: Config;
resolve: (value: boolean) => void;
};

type ConfirmContextType = {
confirm: (message: ReactNode, config: Config) => Promise<boolean>;
};

export const ConfirmContext = createContext<ConfirmContextType | null>(null);

Check warning on line 37 in src/context/confirm-context.tsx

View workflow job for this annotation

GitHub Actions / Static Checks / ESLint Check

Fast refresh only works when a file only exports components. Move your React context(s) to a separate file

export function ConfirmProvider({ children }: { children: ReactNode }) {
const [activeQuestion, setActiveQuestion] = useState<Question | null>(null);
const [isOpen, setIsOpen] = useState<boolean>(false);

const handleAnswer = (answer: boolean) => {
if (activeQuestion === null) return;
activeQuestion.resolve(answer);
setIsOpen(false);
};

const confirm = (message: ReactNode, config: Config) => {
return new Promise<boolean>((resolve) => {
setActiveQuestion({ message, config, resolve });
setIsOpen(true);
});
};

return (
<ConfirmContext.Provider value={{ confirm }}>
{children}

<DialogModalOverlay isDismissable={false} isOpen={isOpen}>
<DialogModal>
<Dialog>
<DialogHeader>
<DialogTitle>{activeQuestion?.config.title}</DialogTitle>
</DialogHeader>
<DialogContent>{activeQuestion?.message}</DialogContent>
<DialogFooter>
<div className="flex grow justify-end gap-2">
<Button variant="secondary" onPress={() => handleAnswer(false)}>
{activeQuestion?.config.buttons.no ?? "&nbsp;"}
</Button>
<Button
isDestructive={activeQuestion?.config.isDestructive}
variant="primary"
onPress={() => handleAnswer(true)}
>
{activeQuestion?.config.buttons.yes ?? "&nbsp;"}
</Button>
</div>
</DialogFooter>
</Dialog>
</DialogModal>
</DialogModalOverlay>
</ConfirmContext.Provider>
);
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,29 +1,62 @@
import { render } from "@/lib/test-utils";
import { ArchiveWorkspace } from "../archive-workspace";
import userEvent from "@testing-library/user-event";
import { screen, waitFor } from "@testing-library/react";

const mockNavigate = vi.fn();

vi.mock("react-router-dom", async () => {
const original =
await vi.importActual<typeof import("react-router-dom")>(
"react-router-dom",
);
return {
...original,
useNavigate: () => mockNavigate,
};
import { waitFor } from "@testing-library/react";

test("has correct buttons when not archived", async () => {
const { getByRole } = render(
<ArchiveWorkspace isArchived={false} workspaceName="foo-bar" />,
);

expect(getByRole("button", { name: /archive/i })).toBeVisible();
});

test("has correct buttons when archived", async () => {
const { getByRole } = render(
<ArchiveWorkspace isArchived={true} workspaceName="foo-bar" />,
);
expect(getByRole("button", { name: /restore/i })).toBeVisible();
expect(getByRole("button", { name: /permanently delete/i })).toBeVisible();
});

test("can archive workspace", async () => {
const { getByText, getByRole } = render(
<ArchiveWorkspace isArchived={false} workspaceName="foo-bar" />,
);

await userEvent.click(getByRole("button", { name: /archive/i }));

await waitFor(() => {
expect(getByText(/archived "foo-bar" workspace/i)).toBeVisible();
});
});

test("archive workspace", async () => {
render(<ArchiveWorkspace isArchived={false} workspaceName="foo" />);
test("can restore archived workspace", async () => {
const { getByText, getByRole } = render(
<ArchiveWorkspace isArchived={true} workspaceName="foo-bar" />,
);

await userEvent.click(getByRole("button", { name: /restore/i }));

await waitFor(() => {
expect(getByText(/restored "foo-bar" workspace/i)).toBeVisible();
});
});

test("can permanently delete archived workspace", async () => {
const { getByText, getByRole } = render(
<ArchiveWorkspace isArchived={true} workspaceName="foo-bar" />,
);

await userEvent.click(getByRole("button", { name: /permanently delete/i }));

await waitFor(() => {
expect(getByRole("dialog", { name: /permanently delete/i })).toBeVisible();
});

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

await waitFor(() => {
expect(screen.getByText(/archived "(.*)" workspace/i)).toBeVisible();
expect(getByText(/permanently deleted "foo-bar" workspace/i)).toBeVisible();
});
});
Loading
Loading