Skip to content

feat: implement use toast mutation for workspaces #184

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 7 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import userEvent from "@testing-library/user-event";
import { screen, waitFor } from "@testing-library/react";

const mockNavigate = vi.fn();
const mockToast = vi.fn();

vi.mock("react-router-dom", async () => {
const original =
await vi.importActual<typeof import("react-router-dom")>(
Expand All @@ -16,21 +16,14 @@ vi.mock("react-router-dom", async () => {
};
});

vi.mock("@stacklok/ui-kit", async () => {
const original =
await vi.importActual<typeof import("@stacklok/ui-kit")>(
"@stacklok/ui-kit",
);
return {
...original,
toast: { error: () => mockToast },
};
});

Comment on lines -19 to -29
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

test("archive workspace", async () => {
render(<ArchiveWorkspace isArchived={false} workspaceName="foo" />);

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

await waitFor(() => {
expect(screen.getByText(/archived "(.*)" workspace/i)).toBeVisible();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ test("create workspace", async () => {
await userEvent.type(screen.getByRole("textbox"), "workspaceA");
await userEvent.click(screen.getByRole("button", { name: /create/i }));
await waitFor(() => expect(mockNavigate).toBeCalled());

await waitFor(() => {
expect(screen.getByText(/created "(.*)" workspace/i)).toBeVisible();
});
});

test("create workspace with enter button", async () => {
Expand All @@ -32,4 +36,8 @@ test("create workspace with enter button", async () => {

await userEvent.type(screen.getByRole("textbox"), "workspaceA{enter}");
await waitFor(() => expect(mockNavigate).toBeCalled());

await waitFor(() => {
expect(screen.getByText(/created "(.*)" workspace/i)).toBeVisible();
});
});
7 changes: 4 additions & 3 deletions src/features/workspace/components/workspace-creation.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCreateWorkspace } from "@/features/workspace/hooks/use-create-workspace";
import { useMutationCreateWorkspace } from "@/features/workspace/hooks/use-mutation-create-workspace";
import {
Button,
Card,
Expand All @@ -16,12 +16,12 @@ import { useNavigate } from "react-router-dom";
export function WorkspaceCreation() {
const navigate = useNavigate();
const [workspaceName, setWorkspaceName] = useState("");
const { mutate, isPending, error } = useCreateWorkspace();
const { mutateAsync, isPending, error } = useMutationCreateWorkspace();
const errorMsg = error?.detail ? `${error?.detail}` : "";

const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
mutate(
mutateAsync(
{
body: { name: workspaceName },
},
Expand All @@ -36,6 +36,7 @@ export function WorkspaceCreation() {
<Card>
<CardBody className="w-full">
<TextField
autoFocus
aria-label="Workspace name"
name="Workspace name"
validationBehavior="aria"
Expand Down
7 changes: 4 additions & 3 deletions src/features/workspace/components/workspace-name.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
TextField,
} from "@stacklok/ui-kit";
import { twMerge } from "tailwind-merge";
import { useCreateWorkspace } from "../hooks/use-create-workspace";
import { useMutationCreateWorkspace } from "../hooks/use-mutation-create-workspace";
import { FormEvent, useState } from "react";
import { useNavigate } from "react-router-dom";

Expand All @@ -24,12 +24,12 @@ export function WorkspaceName({
}) {
const navigate = useNavigate();
const [name, setName] = useState(workspaceName);
const { mutate, isPending, error } = useCreateWorkspace();
const { mutateAsync, isPending, error } = useMutationCreateWorkspace();
const errorMsg = error?.detail ? `${error?.detail}` : "";

const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
mutate(
mutateAsync(
{ body: { name: workspaceName, rename_to: name } },
{
onSuccess: () => navigate(`/workspace/${name}`),
Expand Down Expand Up @@ -63,6 +63,7 @@ export function WorkspaceName({
isDisabled={isArchived || name === ""}
isPending={isPending}
type="submit"
variant="secondary"
>
Save
</Button>
Expand Down
4 changes: 2 additions & 2 deletions src/features/workspace/components/workspaces-selection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ import {
import { useQueryClient } from "@tanstack/react-query";
import { ChevronDown, Search, Settings } from "lucide-react";
import { useState } from "react";
import { useActivateWorkspace } from "../hooks/use-activate-workspace";
import { useMutationActivateWorkspace } from "../hooks/use-mutation-activate-workspace";
import clsx from "clsx";
import { useActiveWorkspaceName } from "../hooks/use-active-workspace-name";

export function WorkspacesSelection() {
const queryClient = useQueryClient();

const { data: workspacesResponse } = useListWorkspaces();
const { mutateAsync: activateWorkspace } = useActivateWorkspace();
const { mutateAsync: activateWorkspace } = useMutationActivateWorkspace();

const { data: activeWorkspaceName } = useActiveWorkspaceName();

Expand Down
8 changes: 0 additions & 8 deletions src/features/workspace/hooks/use-activate-workspace.ts

This file was deleted.

14 changes: 11 additions & 3 deletions src/features/workspace/hooks/use-archive-workspace-button.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import { Button } from "@stacklok/ui-kit";
import { ComponentProps } from "react";
import { useArchiveWorkspace } from "@/features/workspace-system-prompt/hooks/use-archive-workspace";
import { useMutationArchiveWorkspace } from "@/features/workspace/hooks/use-mutation-archive-workspace";
import { useNavigate } from "react-router-dom";

export function useArchiveWorkspaceButton({
workspaceName,
}: {
workspaceName: string;
}): ComponentProps<typeof Button> {
const { mutate, isPending } = useArchiveWorkspace();
const { mutateAsync, isPending } = useMutationArchiveWorkspace();
const navigate = useNavigate();

return {
isPending,
isDisabled: isPending,
onPress: () => mutate({ path: { workspace_name: workspaceName } }),
onPress: () =>
mutateAsync(
{ path: { workspace_name: workspaceName } },
{
onSuccess: () => navigate("/workspaces"),
},
),
isDestructive: true,
children: "Archive",
};
Expand Down
8 changes: 0 additions & 8 deletions src/features/workspace/hooks/use-create-workspace.ts

This file was deleted.

23 changes: 23 additions & 0 deletions src/features/workspace/hooks/use-invalidate-workspace-queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {
v1ListArchivedWorkspacesQueryKey,
v1ListWorkspacesOptions,
} from "@/api/generated/@tanstack/react-query.gen";
import { useQueryClient } from "@tanstack/react-query";
import { useCallback } from "react";

export function useInvalidateWorkspaceQueries() {
const queryClient = useQueryClient();

const invalidate = useCallback(() => {
queryClient.invalidateQueries({
queryKey: v1ListWorkspacesOptions(),
refetchType: "all",
});
queryClient.invalidateQueries({
queryKey: v1ListArchivedWorkspacesQueryKey(),
refetchType: "all",
});
}, [queryClient]);

return invalidate;
}
13 changes: 13 additions & 0 deletions src/features/workspace/hooks/use-mutation-activate-workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { v1ActivateWorkspaceMutation } from "@/api/generated/@tanstack/react-query.gen";
import { useToastMutation as useToastMutation } from "@/hooks/use-toast-mutation";
import { useInvalidateWorkspaceQueries } from "./use-invalidate-workspace-queries";

export function useMutationActivateWorkspace() {
const invalidate = useInvalidateWorkspaceQueries();

return useToastMutation({
...v1ActivateWorkspaceMutation(),
onSuccess: () => invalidate(),
successMsg: (variables) => `Activated "${variables.body.name}" workspace`,
});
}
14 changes: 14 additions & 0 deletions src/features/workspace/hooks/use-mutation-archive-workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { v1DeleteWorkspaceMutation } from "@/api/generated/@tanstack/react-query.gen";
import { useToastMutation } from "@/hooks/use-toast-mutation";
import { useInvalidateWorkspaceQueries } from "./use-invalidate-workspace-queries";

export function useMutationArchiveWorkspace() {
const invalidate = useInvalidateWorkspaceQueries();

return useToastMutation({
...v1DeleteWorkspaceMutation(),
onSuccess: () => invalidate(),
successMsg: (variables) =>
`Archived "${variables.path.workspace_name}" workspace`,
});
}
13 changes: 13 additions & 0 deletions src/features/workspace/hooks/use-mutation-create-workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { v1CreateWorkspaceMutation } from "@/api/generated/@tanstack/react-query.gen";
import { useInvalidateWorkspaceQueries } from "./use-invalidate-workspace-queries";
import { useToastMutation } from "@/hooks/use-toast-mutation";

export function useMutationCreateWorkspace() {
const invalidate = useInvalidateWorkspaceQueries();

return useToastMutation({
...v1CreateWorkspaceMutation(),
onSuccess: () => invalidate(),
successMsg: (variables) => `Created "${variables.body.name}" workspace`,
});
}
14 changes: 14 additions & 0 deletions src/features/workspace/hooks/use-mutation-hard-delete-workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { v1HardDeleteWorkspaceMutation } from "@/api/generated/@tanstack/react-query.gen";
import { useToastMutation } from "@/hooks/use-toast-mutation";
import { useInvalidateWorkspaceQueries } from "./use-invalidate-workspace-queries";

export function useMutationHardDeleteWorkspace() {
const invalidate = useInvalidateWorkspaceQueries();

return useToastMutation({
...v1HardDeleteWorkspaceMutation(),
onSuccess: () => invalidate(),
successMsg: (variables) =>
`Permanently deleted "${variables.path.name}" workspace`,
});
}
14 changes: 14 additions & 0 deletions src/features/workspace/hooks/use-mutation-restore-workspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { v1RecoverWorkspaceMutation } from "@/api/generated/@tanstack/react-query.gen";
import { useToastMutation } from "@/hooks/use-toast-mutation";
import { useInvalidateWorkspaceQueries } from "./use-invalidate-workspace-queries";

export function useMutationRestoreWorkspace() {
const invalidate = useInvalidateWorkspaceQueries();

return useToastMutation({
...v1RecoverWorkspaceMutation(),
onSuccess: () => invalidate(),
successMsg: (variables) =>
`Restored "${variables.path.workspace_name}" workspace`,
});
}
6 changes: 3 additions & 3 deletions src/features/workspace/hooks/use-restore-workspace-button.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { Button } from "@stacklok/ui-kit";
import { ComponentProps } from "react";
import { useRestoreWorkspace } from "./use-restore-workspace";
import { useMutationRestoreWorkspace } from "./use-mutation-restore-workspace";

export function useRestoreWorkspaceButton({
workspaceName,
}: {
workspaceName: string;
}): ComponentProps<typeof Button> {
const { mutate, isPending } = useRestoreWorkspace();
const { mutateAsync, isPending } = useMutationRestoreWorkspace();

return {
isPending,
isDisabled: isPending,
onPress: () => mutate({ path: { workspace_name: workspaceName } }),
onPress: () => mutateAsync({ path: { workspace_name: workspaceName } }),
children: "Restore",
};
}
28 changes: 0 additions & 28 deletions src/features/workspace/hooks/use-restore-workspace.ts

This file was deleted.

27 changes: 19 additions & 8 deletions src/hooks/use-toast-mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,40 @@ export function useToastMutation<
TError = DefaultError,
TVariables = void,
TContext = unknown,
>(options: UseMutationOptions<TData, TError, TVariables, TContext>) {
>({
successMsg,
errorMsg,
loadingMsg,
...options
}: UseMutationOptions<TData, TError, TVariables, TContext> & {
successMsg?: ((variables: TVariables) => string) | string;
loadingMsg?: string;
errorMsg?: string;
}) {
const {
mutateAsync: originalMutateAsync,
// NOTE: We are not allowing direct use of the `mutate` (sync) function.
// NOTE: We are not allowing direct use of the `mutate` (sync) function
// eslint-disable-next-line @typescript-eslint/no-unused-vars
mutate: _,
...rest
} = useMutation(options);

const mutateAsync = useCallback(
<TError extends { detail: string | undefined }>(
async <TError extends { detail: string | undefined }>(
variables: Parameters<typeof originalMutateAsync>[0],
options: Parameters<typeof originalMutateAsync>[1],
{ successMsg }: { successMsg: string },
options: Parameters<typeof originalMutateAsync>[1] = {},
) => {
const promise = originalMutateAsync(variables, options);

toast.promise(promise, {
success: successMsg,
error: (e: TError) => (e.detail ? e.detail : "An error occurred"),
success:
typeof successMsg === "function" ? successMsg(variables) : successMsg,
loading: loadingMsg ?? "Loading...",
error: (e: TError) =>
errorMsg ?? (e.detail ? e.detail : "An error occurred"),
});
},
[originalMutateAsync],
[errorMsg, loadingMsg, originalMutateAsync, successMsg],
);

return { mutateAsync, ...rest };
Expand Down
3 changes: 2 additions & 1 deletion src/lib/test-utils.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SidebarProvider } from "@/components/ui/sidebar";
import { DarkModeProvider } from "@stacklok/ui-kit";
import { DarkModeProvider, Toaster } from "@stacklok/ui-kit";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RenderOptions, render } from "@testing-library/react";
import React, { ReactNode } from "react";
Expand Down Expand Up @@ -45,6 +45,7 @@
render(
<TestQueryClientProvider>
<DarkModeProvider>
<Toaster />
<MemoryRouter {...options?.routeConfig}>
<Routes>
<Route
Expand All @@ -57,6 +58,6 @@
</TestQueryClientProvider>,
);

export * from "@testing-library/react";

Check warning on line 61 in src/lib/test-utils.tsx

View workflow job for this annotation

GitHub Actions / Static Checks / ESLint Check

This rule can't verify that `export *` only exports components

export { renderWithProviders as render };

Check warning on line 63 in src/lib/test-utils.tsx

View workflow job for this annotation

GitHub Actions / Static Checks / ESLint Check

Fast refresh only works when a file only exports components. Use a new file to share constants or functions between components
Loading