-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement hard delete for workspaces & refactor workspaces tabl…
…e 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
Showing
32 changed files
with
1,157 additions
and
273 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
|
||
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 ?? " "} | ||
</Button> | ||
<Button | ||
isDestructive={activeQuestion?.config.isDestructive} | ||
variant="primary" | ||
onPress={() => handleAnswer(true)} | ||
> | ||
{activeQuestion?.config.buttons.yes ?? " "} | ||
</Button> | ||
</div> | ||
</DialogFooter> | ||
</Dialog> | ||
</DialogModal> | ||
</DialogModalOverlay> | ||
</ConfirmContext.Provider> | ||
); | ||
} |
15 changes: 0 additions & 15 deletions
15
src/features/workspace-system-prompt/hooks/use-archive-workspace.ts
This file was deleted.
Oops, something went wrong.
11 changes: 0 additions & 11 deletions
11
src/features/workspace-system-prompt/hooks/use-set-system-prompt.tsx
This file was deleted.
Oops, something went wrong.
71 changes: 52 additions & 19 deletions
71
src/features/workspace/components/__tests__/archive-workspace.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
301 changes: 301 additions & 0 deletions
301
src/features/workspace/components/__tests__/table-actions-workspaces.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,301 @@ | ||
import { hrefs } from "@/lib/hrefs"; | ||
|
||
import { waitFor } from "@testing-library/dom"; | ||
import userEvent from "@testing-library/user-event"; | ||
|
||
import { TableActionsWorkspaces } from "../table-actions-workspaces"; | ||
import { render } from "@/lib/test-utils"; | ||
|
||
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, | ||
}; | ||
}); | ||
|
||
it("has correct actions for default workspace when not active", async () => { | ||
const { getByRole } = render( | ||
<TableActionsWorkspaces | ||
workspace={{ is_active: false, isArchived: false, name: "default" }} | ||
activeWorkspaceName="foo-bar" | ||
/>, | ||
); | ||
|
||
await userEvent.click(getByRole("button", { name: /actions/i })); | ||
|
||
await waitFor(() => { | ||
expect(getByRole("menu")).toBeVisible(); | ||
}); | ||
|
||
const activate = getByRole("menuitem", { name: /activate/i }); | ||
expect(activate).not.toHaveAttribute("aria-disabled", "true"); | ||
|
||
const edit = getByRole("menuitem", { name: /edit/i }); | ||
expect(edit).toHaveAttribute("href", hrefs.workspaces.edit("default")); | ||
|
||
const archive = getByRole("menuitem", { name: /archive/i }); | ||
expect(archive).toHaveAttribute("aria-disabled", "true"); | ||
}); | ||
|
||
it("has correct actions for default workspace when active", async () => { | ||
const { getByRole } = render( | ||
<TableActionsWorkspaces | ||
workspace={{ is_active: true, isArchived: false, name: "default" }} | ||
activeWorkspaceName="default" | ||
/>, | ||
); | ||
|
||
await userEvent.click(getByRole("button", { name: /actions/i })); | ||
|
||
await waitFor(() => { | ||
expect(getByRole("menu")).toBeVisible(); | ||
}); | ||
|
||
const activate = getByRole("menuitem", { name: /activate/i }); | ||
expect(activate).toHaveAttribute("aria-disabled", "true"); | ||
|
||
const edit = getByRole("menuitem", { name: /edit/i }); | ||
expect(edit).toHaveAttribute("href", hrefs.workspaces.edit("default")); | ||
|
||
const archive = getByRole("menuitem", { name: /archive/i }); | ||
expect(archive).toHaveAttribute("aria-disabled", "true"); | ||
}); | ||
|
||
it("has correct actions for normal workspace when not active", async () => { | ||
const { getByRole } = render( | ||
<TableActionsWorkspaces | ||
workspace={{ is_active: false, isArchived: false, name: "foo-bar" }} | ||
activeWorkspaceName="default" | ||
/>, | ||
); | ||
|
||
await userEvent.click(getByRole("button", { name: /actions/i })); | ||
|
||
await waitFor(() => { | ||
expect(getByRole("menu")).toBeVisible(); | ||
}); | ||
|
||
const activate = getByRole("menuitem", { name: /activate/i }); | ||
expect(activate).not.toHaveAttribute("aria-disabled", "true"); | ||
|
||
const edit = getByRole("menuitem", { name: /edit/i }); | ||
expect(edit).toHaveAttribute("href", hrefs.workspaces.edit("foo-bar")); | ||
|
||
const archive = getByRole("menuitem", { name: /archive/i }); | ||
expect(archive).not.toHaveAttribute("aria-disabled", "true"); | ||
}); | ||
|
||
it("has correct actions for normal workspace when active", async () => { | ||
const { getByRole } = render( | ||
<TableActionsWorkspaces | ||
workspace={{ is_active: true, isArchived: false, name: "foo-bar" }} | ||
activeWorkspaceName="foo-bar" | ||
/>, | ||
); | ||
|
||
await userEvent.click(getByRole("button", { name: /actions/i })); | ||
|
||
await waitFor(() => { | ||
expect(getByRole("menu")).toBeVisible(); | ||
}); | ||
|
||
const activate = getByRole("menuitem", { name: /activate/i }); | ||
expect(activate).toHaveAttribute("aria-disabled", "true"); | ||
|
||
const edit = getByRole("menuitem", { name: /edit/i }); | ||
expect(edit).toHaveAttribute("href", hrefs.workspaces.edit("foo-bar")); | ||
|
||
const archive = getByRole("menuitem", { name: /archive/i }); | ||
expect(archive).toHaveAttribute("aria-disabled", "true"); | ||
}); | ||
|
||
it("has correct actions for archived workspace", async () => { | ||
const { getByRole } = render( | ||
<TableActionsWorkspaces | ||
workspace={{ is_active: true, isArchived: true, name: "foo-bar" }} | ||
activeWorkspaceName="default" | ||
/>, | ||
); | ||
|
||
await userEvent.click(getByRole("button", { name: /actions/i })); | ||
|
||
await waitFor(() => { | ||
expect(getByRole("menu")).toBeVisible(); | ||
}); | ||
|
||
const restore = getByRole("menuitem", { name: /restore/i }); | ||
expect(restore).not.toHaveAttribute("aria-disabled", "true"); | ||
|
||
const hardDelete = getByRole("menuitem", { | ||
name: /permanently delete/i, | ||
}); | ||
expect(hardDelete).not.toHaveAttribute("aria-disabled", "true"); | ||
}); | ||
|
||
it("can activate default workspace", async () => { | ||
const { getByRole, getByText } = render( | ||
<TableActionsWorkspaces | ||
workspace={{ is_active: false, isArchived: false, name: "default" }} | ||
activeWorkspaceName="foo-bar" | ||
/>, | ||
); | ||
|
||
await userEvent.click(getByRole("button", { name: /actions/i })); | ||
|
||
await waitFor(() => { | ||
expect(getByRole("menu")).toBeVisible(); | ||
}); | ||
|
||
const activate = getByRole("menuitem", { name: /activate/i }); | ||
await userEvent.click(activate); | ||
|
||
await waitFor(() => { | ||
expect(getByText(/activated "default" workspace/i)).toBeVisible(); | ||
}); | ||
}); | ||
|
||
it("can edit default workspace", async () => { | ||
const { getByRole } = render( | ||
<TableActionsWorkspaces | ||
workspace={{ is_active: false, isArchived: false, name: "default" }} | ||
activeWorkspaceName="foo-bar" | ||
/>, | ||
); | ||
|
||
await userEvent.click(getByRole("button", { name: /actions/i })); | ||
|
||
await waitFor(() => { | ||
expect(getByRole("menu")).toBeVisible(); | ||
}); | ||
|
||
const edit = getByRole("menuitem", { name: /edit/i }); | ||
await userEvent.click(edit); | ||
|
||
await waitFor(() => { | ||
expect(mockNavigate).toHaveBeenCalledWith( | ||
hrefs.workspaces.edit("default"), | ||
undefined, | ||
); | ||
}); | ||
}); | ||
|
||
it("can activate normal workspace", async () => { | ||
const { getByRole, getByText } = render( | ||
<TableActionsWorkspaces | ||
workspace={{ is_active: false, isArchived: false, name: "foo-bar" }} | ||
activeWorkspaceName="default" | ||
/>, | ||
); | ||
|
||
await userEvent.click(getByRole("button", { name: /actions/i })); | ||
|
||
await waitFor(() => { | ||
expect(getByRole("menu")).toBeVisible(); | ||
}); | ||
|
||
const activate = getByRole("menuitem", { name: /activate/i }); | ||
await userEvent.click(activate); | ||
|
||
await waitFor(() => { | ||
expect(getByText(/activated "foo-bar" workspace/i)).toBeVisible(); | ||
}); | ||
}); | ||
|
||
it("can edit normal workspace", async () => { | ||
const { getByRole } = render( | ||
<TableActionsWorkspaces | ||
workspace={{ is_active: false, isArchived: false, name: "foo-bar" }} | ||
activeWorkspaceName="default" | ||
/>, | ||
); | ||
|
||
await userEvent.click(getByRole("button", { name: /actions/i })); | ||
|
||
await waitFor(() => { | ||
expect(getByRole("menu")).toBeVisible(); | ||
}); | ||
|
||
const edit = getByRole("menuitem", { name: /edit/i }); | ||
await userEvent.click(edit); | ||
|
||
await waitFor(() => { | ||
expect(mockNavigate).toHaveBeenCalledWith( | ||
hrefs.workspaces.edit("foo-bar"), | ||
undefined, | ||
); | ||
}); | ||
}); | ||
|
||
it("can archive normal workspace", async () => { | ||
const { getByRole, getByText } = render( | ||
<TableActionsWorkspaces | ||
workspace={{ is_active: false, isArchived: false, name: "foo-bar" }} | ||
activeWorkspaceName="default" | ||
/>, | ||
); | ||
|
||
await userEvent.click(getByRole("button", { name: /actions/i })); | ||
|
||
await waitFor(() => { | ||
expect(getByRole("menu")).toBeVisible(); | ||
}); | ||
|
||
await userEvent.click(getByRole("menuitem", { name: /archive/i })); | ||
|
||
await waitFor(() => { | ||
expect(getByText(/archived "foo-bar" workspace/i)).toBeVisible(); | ||
}); | ||
}); | ||
|
||
it("can restore archived workspace", async () => { | ||
const { getByRole, getByText } = render( | ||
<TableActionsWorkspaces | ||
workspace={{ is_active: false, isArchived: true, name: "foo-bar" }} | ||
activeWorkspaceName="default" | ||
/>, | ||
); | ||
|
||
await userEvent.click(getByRole("button", { name: /actions/i })); | ||
|
||
await waitFor(() => { | ||
expect(getByRole("menu")).toBeVisible(); | ||
}); | ||
|
||
await userEvent.click(getByRole("menuitem", { name: /restore/i })); | ||
|
||
await waitFor(() => { | ||
expect(getByText(/restored "foo-bar" workspace/i)).toBeVisible(); | ||
}); | ||
}); | ||
|
||
it("can permanently delete archived workspace", async () => { | ||
const { getByRole, getByText } = render( | ||
<TableActionsWorkspaces | ||
workspace={{ is_active: false, isArchived: true, name: "foo-bar" }} | ||
activeWorkspaceName="default" | ||
/>, | ||
); | ||
|
||
await userEvent.click(getByRole("button", { name: /actions/i })); | ||
|
||
await waitFor(() => { | ||
expect(getByRole("menu")).toBeVisible(); | ||
}); | ||
|
||
await userEvent.click(getByRole("menuitem", { name: /permanently/i })); | ||
|
||
await waitFor(() => { | ||
expect(getByRole("dialog", { name: /permanently delete/i })).toBeVisible(); | ||
}); | ||
|
||
await userEvent.click(getByRole("button", { name: /delete/i })); | ||
|
||
await waitFor(() => { | ||
expect(getByText(/permanently deleted "foo-bar" workspace/i)).toBeVisible(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
src/features/workspace/components/__tests__/workspace-name.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { test, expect } from "vitest"; | ||
import { WorkspaceName } from "../workspace-name"; | ||
import { render, waitFor } from "@/lib/test-utils"; | ||
import userEvent from "@testing-library/user-event"; | ||
|
||
test("can rename workspace", async () => { | ||
const { getByRole, getByText } = render( | ||
<WorkspaceName workspaceName="foo-bar" isArchived={false} />, | ||
); | ||
|
||
const input = getByRole("textbox", { name: /workspace name/i }); | ||
await userEvent.clear(input); | ||
|
||
await userEvent.type(input, "baz-qux"); | ||
expect(input).toHaveValue("baz-qux"); | ||
|
||
await userEvent.click(getByRole("button", { name: /save/i })); | ||
|
||
await waitFor(() => { | ||
expect(getByText(/renamed workspace to "baz-qux"/i)).toBeVisible(); | ||
}); | ||
}); | ||
|
||
test("can't rename archived workspace", async () => { | ||
const { getByRole } = render( | ||
<WorkspaceName workspaceName="foo" isArchived={true} />, | ||
); | ||
|
||
expect(getByRole("textbox", { name: /workspace name/i })).toBeDisabled(); | ||
expect(getByRole("button", { name: /save/i })).toBeDisabled(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
128 changes: 128 additions & 0 deletions
128
src/features/workspace/components/table-actions-workspaces.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import { Workspace } from "@/api/generated"; | ||
import { | ||
Button, | ||
Menu, | ||
MenuTrigger, | ||
OptionsSchema, | ||
Popover, | ||
} from "@stacklok/ui-kit"; | ||
|
||
import { Undo2, X, SlidersHorizontal, Check, Ellipsis } from "lucide-react"; | ||
import { useMutationArchiveWorkspace } from "@/features/workspace/hooks/use-mutation-archive-workspace"; | ||
import { useMutationRestoreWorkspace } from "../hooks/use-mutation-restore-workspace"; | ||
import { useMutationHardDeleteWorkspace } from "../hooks/use-mutation-hard-delete-workspace"; | ||
import { useMutationActivateWorkspace } from "../hooks/use-mutation-activate-workspace"; | ||
import { useConfirmHardDeleteWorkspace } from "../hooks/use-confirm-hard-delete-workspace"; | ||
import { hrefs } from "@/lib/hrefs"; | ||
|
||
const getWorkspaceActions = ({ | ||
archiveWorkspace, | ||
workspace, | ||
activateWorkspace, | ||
activeWorkspaceName, | ||
}: { | ||
workspace: Workspace & { | ||
isArchived?: boolean; | ||
}; | ||
archiveWorkspace: ReturnType< | ||
typeof useMutationArchiveWorkspace | ||
>["mutateAsync"]; | ||
activateWorkspace: ReturnType< | ||
typeof useMutationActivateWorkspace | ||
>["mutateAsync"]; | ||
activeWorkspaceName: string | null | undefined; | ||
}): OptionsSchema<"menu">[] => [ | ||
{ | ||
textValue: "Activate", | ||
icon: <Check />, | ||
id: "activate", | ||
isDisabled: workspace.name === activeWorkspaceName, | ||
onAction: () => activateWorkspace({ body: { name: workspace.name } }), | ||
}, | ||
{ | ||
textValue: "Edit", | ||
icon: <SlidersHorizontal />, | ||
id: "edit", | ||
href: hrefs.workspaces.edit(workspace.name), | ||
}, | ||
{ | ||
textValue: "Archive", | ||
icon: <X />, | ||
id: "archive", | ||
isDisabled: | ||
workspace.name === activeWorkspaceName || workspace.name === "default", | ||
onAction: () => | ||
void archiveWorkspace({ path: { workspace_name: workspace.name } }), | ||
}, | ||
]; | ||
|
||
const getArchivedWorkspaceActions = ({ | ||
workspace, | ||
restoreWorkspace, | ||
hardDeleteWorkspace, | ||
}: { | ||
workspace: Workspace & { | ||
isArchived?: boolean; | ||
}; | ||
restoreWorkspace: ReturnType< | ||
typeof useMutationArchiveWorkspace | ||
>["mutateAsync"]; | ||
hardDeleteWorkspace: ReturnType< | ||
typeof useMutationHardDeleteWorkspace | ||
>["mutateAsync"]; | ||
}): OptionsSchema<"menu">[] => [ | ||
{ | ||
textValue: "Restore", | ||
icon: <Undo2 />, | ||
id: "restore", | ||
onAction: () => | ||
restoreWorkspace({ path: { workspace_name: workspace.name } }), | ||
}, | ||
{ | ||
textValue: "Permanently delete", | ||
isDestructive: true, | ||
icon: <X />, | ||
id: "permanently-delete", | ||
onAction: () => | ||
hardDeleteWorkspace({ path: { workspace_name: workspace.name } }), | ||
}, | ||
]; | ||
|
||
export function TableActionsWorkspaces({ | ||
workspace, | ||
activeWorkspaceName, | ||
}: { | ||
activeWorkspaceName: string | null | undefined; | ||
workspace: Workspace & { isArchived: boolean }; | ||
}) { | ||
const { mutateAsync: archiveWorkspace } = useMutationArchiveWorkspace(); | ||
const { mutateAsync: restoreWorkspace } = useMutationRestoreWorkspace(); | ||
const { mutateAsync: activateWorkspace } = useMutationActivateWorkspace(); | ||
const hardDeleteWorkspace = useConfirmHardDeleteWorkspace(); | ||
|
||
return ( | ||
<MenuTrigger> | ||
<Button aria-label="Actions" isIcon variant="tertiary"> | ||
<Ellipsis /> | ||
</Button> | ||
<Popover placement="bottom end"> | ||
<Menu | ||
items={ | ||
workspace.isArchived | ||
? getArchivedWorkspaceActions({ | ||
workspace, | ||
restoreWorkspace, | ||
hardDeleteWorkspace, | ||
}) | ||
: getWorkspaceActions({ | ||
workspace, | ||
archiveWorkspace, | ||
activateWorkspace, | ||
activeWorkspaceName, | ||
}) | ||
} | ||
/> | ||
</Popover> | ||
</MenuTrigger> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { | ||
Badge, | ||
Cell, | ||
Column, | ||
Row, | ||
Table, | ||
TableBody, | ||
TableHeader, | ||
} from "@stacklok/ui-kit"; | ||
|
||
import { useListAllWorkspaces } from "../hooks/use-query-list-all-workspaces"; | ||
import { useActiveWorkspaceName } from "../hooks/use-active-workspace-name"; | ||
import { TableActionsWorkspaces } from "./table-actions-workspaces"; | ||
import { hrefs } from "@/lib/hrefs"; | ||
|
||
function CellName({ | ||
name, | ||
isArchived = false, | ||
isActive = false, | ||
}: { | ||
name: string; | ||
isArchived: boolean; | ||
isActive: boolean; | ||
}) { | ||
if (isArchived) | ||
return ( | ||
<Cell className="text-disabled"> | ||
<span>{name}</span> | ||
| ||
<Badge size="sm" className="text-tertiary"> | ||
Archived | ||
</Badge> | ||
</Cell> | ||
); | ||
|
||
if (isActive) | ||
return ( | ||
<Cell> | ||
<span>{name}</span> | ||
| ||
<Badge size="sm" variant="inverted"> | ||
Active | ||
</Badge> | ||
</Cell> | ||
); | ||
|
||
return <Cell>{name}</Cell>; | ||
} | ||
|
||
export function TableWorkspaces() { | ||
const { data: workspaces } = useListAllWorkspaces(); | ||
const { data: activeWorkspaceName } = useActiveWorkspaceName(); | ||
|
||
return ( | ||
<Table aria-label="List of workspaces"> | ||
<Row> | ||
<TableHeader> | ||
<Column id="name" isRowHeader> | ||
Name | ||
</Column> | ||
<Column id="configuration"></Column> | ||
</TableHeader> | ||
</Row> | ||
<TableBody> | ||
{workspaces.map((workspace) => ( | ||
<Row key={workspace.id} href={hrefs.workspaces.edit(workspace.name)}> | ||
<CellName | ||
name={workspace.name} | ||
isActive={workspace.is_active} | ||
isArchived={workspace.isArchived} | ||
/> | ||
<Cell alignment="end"> | ||
<TableActionsWorkspaces | ||
activeWorkspaceName={activeWorkspaceName} | ||
workspace={workspace} | ||
/> | ||
</Cell> | ||
</Row> | ||
))} | ||
</TableBody> | ||
</Table> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
10 changes: 1 addition & 9 deletions
10
src/features/workspace/hooks/use-archive-workspace-button.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
src/features/workspace/hooks/use-confirm-hard-delete-workspace.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { useConfirm } from "@/hooks/use-confirm"; | ||
import { useCallback } from "react"; | ||
import { useMutationHardDeleteWorkspace } from "./use-mutation-hard-delete-workspace"; | ||
|
||
export function useConfirmHardDeleteWorkspace() { | ||
const { mutateAsync: hardDeleteWorkspace } = useMutationHardDeleteWorkspace(); | ||
|
||
const { confirm } = useConfirm(); | ||
|
||
return useCallback( | ||
async (...params: Parameters<typeof hardDeleteWorkspace>) => { | ||
const answer = await confirm( | ||
<> | ||
<p>Are you sure you want to delete this workspace?</p> | ||
<p> | ||
You will lose any custom instructions, or other configuration.{" "} | ||
<b>This action cannot be undone.</b> | ||
</p> | ||
</>, | ||
{ | ||
buttons: { | ||
yes: "Delete", | ||
no: "Cancel", | ||
}, | ||
title: "Permanently delete workspace", | ||
isDestructive: true, | ||
}, | ||
); | ||
if (answer) { | ||
return hardDeleteWorkspace(...params); | ||
} | ||
}, | ||
[confirm, hardDeleteWorkspace], | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 3 additions & 3 deletions
6
src/features/workspace/hooks/use-mutation-activate-workspace.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +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"; | ||
import { useQueryClient } from "@tanstack/react-query"; | ||
|
||
export function useMutationActivateWorkspace() { | ||
const invalidate = useInvalidateWorkspaceQueries(); | ||
const queryClient = useQueryClient(); | ||
|
||
return useToastMutation({ | ||
...v1ActivateWorkspaceMutation(), | ||
onSuccess: () => invalidate(), | ||
onSuccess: () => queryClient.invalidateQueries({ refetchType: "all" }), // Global setting, refetch **everything** | ||
successMsg: (variables) => `Activated "${variables.body.name}" workspace`, | ||
}); | ||
} |
78 changes: 76 additions & 2 deletions
78
src/features/workspace/hooks/use-mutation-archive-workspace.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
72 changes: 70 additions & 2 deletions
72
src/features/workspace/hooks/use-mutation-restore-workspace.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
src/features/workspace/hooks/use-mutation-set-workspace-custom-instructions.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { | ||
v1GetWorkspaceCustomInstructionsQueryKey, | ||
v1SetWorkspaceCustomInstructionsMutation, | ||
} from "@/api/generated/@tanstack/react-query.gen"; | ||
import { V1GetWorkspaceCustomInstructionsData } from "@/api/generated"; | ||
import { useToastMutation } from "@/hooks/use-toast-mutation"; | ||
import { useQueryClient } from "@tanstack/react-query"; | ||
|
||
export function useMutationSetWorkspaceCustomInstructions( | ||
options: V1GetWorkspaceCustomInstructionsData, | ||
) { | ||
const queryClient = useQueryClient(); | ||
|
||
return useToastMutation({ | ||
...v1SetWorkspaceCustomInstructionsMutation(options), | ||
onSuccess: () => | ||
queryClient.invalidateQueries({ | ||
queryKey: v1GetWorkspaceCustomInstructionsQueryKey(options), | ||
refetchType: "all", | ||
}), | ||
successMsg: "Successfully updated custom instructions", | ||
}); | ||
} |
2 changes: 1 addition & 1 deletion
2
...tem-prompt/hooks/use-get-system-prompt.ts → ...uery-get-workspace-custom-instructions.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
src/features/workspace/hooks/use-query-list-all-workspaces.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { | ||
DefinedUseQueryResult, | ||
QueryObserverLoadingErrorResult, | ||
QueryObserverLoadingResult, | ||
QueryObserverPendingResult, | ||
QueryObserverRefetchErrorResult, | ||
useQueries, | ||
} from "@tanstack/react-query"; | ||
import { | ||
v1ListArchivedWorkspacesOptions, | ||
v1ListWorkspacesOptions, | ||
} from "@/api/generated/@tanstack/react-query.gen"; | ||
import { | ||
V1ListArchivedWorkspacesResponse, | ||
V1ListWorkspacesResponse, | ||
} from "@/api/generated"; | ||
|
||
type QueryResult<T> = | ||
| DefinedUseQueryResult<T, Error> | ||
| QueryObserverLoadingErrorResult<T, Error> | ||
| QueryObserverLoadingResult<T, Error> | ||
| QueryObserverPendingResult<T, Error> | ||
| QueryObserverRefetchErrorResult<T, Error>; | ||
|
||
type UseQueryDataReturn = [ | ||
QueryResult<V1ListWorkspacesResponse>, | ||
QueryResult<V1ListArchivedWorkspacesResponse>, | ||
]; | ||
|
||
const combine = (results: UseQueryDataReturn) => { | ||
const [workspaces, archivedWorkspaces] = results; | ||
|
||
const active = workspaces.data?.workspaces | ||
? workspaces.data?.workspaces.map( | ||
(i) => ({ ...i, id: `workspace-${i.name}`, isArchived: false }), | ||
[], | ||
) | ||
: []; | ||
|
||
const archived = archivedWorkspaces.data?.workspaces | ||
? archivedWorkspaces.data?.workspaces.map( | ||
(i) => ({ ...i, id: `archived-workspace-${i.name}`, isArchived: true }), | ||
[], | ||
) | ||
: []; | ||
|
||
return { | ||
data: [...active, ...archived], | ||
isPending: results.some((r) => r.isPending), | ||
isFetching: results.some((r) => r.isFetching), | ||
isRefetching: results.some((r) => r.isRefetching), | ||
}; | ||
}; | ||
|
||
export const useListAllWorkspaces = () => { | ||
return useQueries({ | ||
combine, | ||
queries: [ | ||
{ | ||
...v1ListWorkspacesOptions(), | ||
}, | ||
{ | ||
...v1ListArchivedWorkspacesOptions(), | ||
}, | ||
], | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
"use client"; | ||
|
||
import { ConfirmContext } from "@/context/confirm-context"; | ||
import type { ReactNode } from "react"; | ||
import { useContext } from "react"; | ||
|
||
type Buttons = { | ||
yes: ReactNode; | ||
no: ReactNode; | ||
}; | ||
|
||
type Config = { | ||
buttons: Buttons; | ||
title?: ReactNode; | ||
isDestructive?: boolean; | ||
}; | ||
|
||
export type ConfirmFunction = ( | ||
message: ReactNode, | ||
config: Config, | ||
) => Promise<boolean>; | ||
|
||
export const useConfirm = () => { | ||
const context = useContext(ConfirmContext); | ||
if (!context) { | ||
throw new Error("useConfirmContext must be used within a ConfirmProvider"); | ||
} | ||
return context; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
export const hrefs = { | ||
workspaces: { | ||
all: "/workspaces", | ||
create: "/workspace/create", | ||
edit: (name: string) => `/workspace/${name}`, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters