diff --git a/package-lock.json b/package-lock.json
index 2ad375ba..79726989 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@hey-api/client-fetch": "^0.7.1",
+ "@hookform/resolvers": "^4.1.0",
"@jsonforms/core": "^3.5.1",
"@jsonforms/react": "^3.5.1",
"@jsonforms/vanilla-renderers": "^3.5.1",
@@ -18,7 +19,7 @@
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
- "@stacklok/ui-kit": "^1.0.1-4",
+ "@stacklok/ui-kit": "^1.0.1-8",
"@tanstack/react-query": "^5.64.1",
"@tanstack/react-query-devtools": "^5.66.0",
"@types/lodash": "^4.17.15",
@@ -1183,6 +1184,18 @@
"node": ">=18"
}
},
+ "node_modules/@hookform/resolvers": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.0.tgz",
+ "integrity": "sha512-fX/uHKb+OOCpACLc6enuTQsf0ZpRrKbeBBPETg5PCPLCIYV6osP2Bw6ezuclM61lH+wBF9eXcuC0+BFh9XOEnQ==",
+ "license": "MIT",
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001698"
+ },
+ "peerDependencies": {
+ "react-hook-form": "^7.0.0"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -4074,18 +4087,20 @@
}
},
"node_modules/@stacklok/ui-kit": {
- "version": "1.0.1-4",
- "resolved": "https://registry.npmjs.org/@stacklok/ui-kit/-/ui-kit-1.0.1-4.tgz",
- "integrity": "sha512-Az5mQmb+0P7pMUKVyhJLEpDCzGGtSFi0H0C2Us32gM4Fsz78FL92MHvwdWMZgcepdwbdQoR/C6JASwSHteAbmA==",
+ "version": "1.0.1-8",
+ "resolved": "https://registry.npmjs.org/@stacklok/ui-kit/-/ui-kit-1.0.1-8.tgz",
+ "integrity": "sha512-PtrBGhbHDW9oh6zirQ+lJLaP0mNJT3FlFP2klW3ZVAFB94UXsVQ/S38t7ikLkq+zIzxteFkbVMZXoj/0WOwBbg==",
"license": "ISC",
"dependencies": {
"@fontsource-variable/figtree": "^5.1.1",
"@fontsource-variable/inter": "^5.1.0",
"@fontsource-variable/source-code-pro": "^5.1.0",
+ "@hookform/resolvers": "4.1.0",
"@untitled-ui/icons-react": "^0.1.4",
"postcss": "^8.4.47",
"react-aria": "3.36.0",
"react-aria-components": "1.5.0",
+ "react-hook-form": "7.47.0",
"react-stately": "3.34.0",
"sonner": "^1.7.1",
"tailwind-merge": "^2.5.2",
@@ -5844,10 +5859,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001690",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
- "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
- "dev": true,
+ "version": "1.0.30001700",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz",
+ "integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==",
"funding": [
{
"type": "opencollective",
@@ -11884,6 +11898,22 @@
"react": "^19.0.0"
}
},
+ "node_modules/react-hook-form": {
+ "version": "7.47.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.47.0.tgz",
+ "integrity": "sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.22.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18"
+ }
+ },
"node_modules/react-markdown": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.1.tgz",
diff --git a/package.json b/package.json
index 609b3e02..0c1303fa 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@hey-api/client-fetch": "^0.7.1",
+ "@hookform/resolvers": "^4.1.0",
"@jsonforms/core": "^3.5.1",
"@jsonforms/react": "^3.5.1",
"@jsonforms/vanilla-renderers": "^3.5.1",
@@ -31,7 +32,7 @@
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
- "@stacklok/ui-kit": "^1.0.1-4",
+ "@stacklok/ui-kit": "^1.0.1-8",
"@tanstack/react-query": "^5.64.1",
"@tanstack/react-query-devtools": "^5.66.0",
"@types/lodash": "^4.17.15",
diff --git a/src/components/Error.tsx b/src/components/Error.tsx
index 2de71a38..1d18b1c8 100644
--- a/src/components/Error.tsx
+++ b/src/components/Error.tsx
@@ -45,11 +45,9 @@ export function ErrorFallbackContent() {
export function Error() {
return (
-
+ >
)
}
diff --git a/src/features/workspace/components/__tests__/workspace-name.test.tsx b/src/features/workspace/components/__tests__/workspace-name.test.tsx
index 0b4a44af..846bd40a 100644
--- a/src/features/workspace/components/__tests__/workspace-name.test.tsx
+++ b/src/features/workspace/components/__tests__/workspace-name.test.tsx
@@ -1,5 +1,5 @@
import { test, expect } from 'vitest'
-import { WorkspaceName } from '../workspace-name'
+import { FormWorkspaceName } from '../form-workspace-name'
import { render, waitFor } from '@/lib/test-utils'
import userEvent from '@testing-library/user-event'
import { server } from '@/mocks/msw/node'
@@ -8,7 +8,7 @@ import { mswEndpoint } from '@/test/msw-endpoint'
test('can rename workspace', async () => {
const { getByRole, getByText } = render(
-
+
)
const input = getByRole('textbox', { name: /workspace name/i })
@@ -26,7 +26,7 @@ test('can rename workspace', async () => {
test("can't rename archived workspace", async () => {
const { getByRole } = render(
-
+
)
expect(getByRole('textbox', { name: /workspace name/i })).toBeDisabled()
@@ -48,7 +48,7 @@ test("can't rename active workspace", async () => {
)
)
const { getByRole } = render(
-
+
)
expect(getByRole('textbox', { name: /workspace name/i })).toBeDisabled()
@@ -57,7 +57,7 @@ test("can't rename active workspace", async () => {
test("can't rename archived workspace", async () => {
const { getByRole, queryByText } = render(
-
+
)
expect(getByRole('textbox', { name: /workspace name/i })).toBeDisabled()
@@ -69,7 +69,7 @@ test("can't rename archived workspace", async () => {
test("can't rename default workspace", async () => {
const { getByRole, getByText } = render(
-
+
)
expect(getByRole('textbox', { name: /workspace name/i })).toBeDisabled()
diff --git a/src/features/workspace/components/form-workspace-name.tsx b/src/features/workspace/components/form-workspace-name.tsx
new file mode 100644
index 00000000..ff2636b0
--- /dev/null
+++ b/src/features/workspace/components/form-workspace-name.tsx
@@ -0,0 +1,90 @@
+import {
+ Card,
+ CardBody,
+ CardFooter,
+ Description,
+ FormDiscardChangesButton,
+ FormSubmitButton,
+ FormTextField,
+ FormV2,
+ Input,
+ Label,
+} from '@stacklok/ui-kit'
+import { useMutationCreateWorkspace } from '../hooks/use-mutation-create-workspace'
+import { useNavigate } from 'react-router-dom'
+import { twMerge } from 'tailwind-merge'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { z } from 'zod'
+
+const schema = z.object({
+ rename_to: z.string().nonempty(),
+})
+type FieldValues = z.infer
+const FIELD_NAME = schema.keyof().Enum
+
+export function FormWorkspaceName({
+ className,
+ workspaceName,
+ isArchived,
+}: {
+ className?: string
+ workspaceName: string
+ isArchived: boolean | undefined
+}) {
+ const navigate = useNavigate()
+ const { mutateAsync, isPending } = useMutationCreateWorkspace()
+
+ const isDefault = workspaceName === 'default'
+ const isDisabled = isArchived || isPending || isDefault
+
+ const description: string | null = isDefault
+ ? 'Cannot rename the default workspace'
+ : isArchived
+ ? 'Cannot rename an archived workspace'
+ : null
+
+ const handleSubmit = ({ rename_to }: FieldValues) => {
+ mutateAsync(
+ { body: { name: workspaceName, rename_to } },
+ {
+ onSuccess: () => {
+ navigate(`/workspace/${rename_to}`)
+ },
+ }
+ )
+ }
+
+ return (
+
+ onSubmit={handleSubmit}
+ data-testid="workspace-name"
+ options={{
+ resolver: zodResolver(schema),
+ defaultValues: {
+ rename_to: workspaceName,
+ },
+ }}
+ >
+
+
+
+
+
+ {description ? (
+ {description}
+ ) : null}
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/features/workspace/components/workspace-name.tsx b/src/features/workspace/components/workspace-name.tsx
deleted file mode 100644
index 805e2171..00000000
--- a/src/features/workspace/components/workspace-name.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import {
- Card,
- CardBody,
- CardFooter,
- Form,
- Input,
- Label,
- TextField,
-} from "@stacklok/ui-kit";
-import { useMutationCreateWorkspace } from "../hooks/use-mutation-create-workspace";
-import { useNavigate } from "react-router-dom";
-import { twMerge } from "tailwind-merge";
-import { useFormState } from "@/hooks/useFormState";
-import { FormButtons } from "@/components/FormButtons";
-import { FormEvent, useEffect } from "react";
-
-export function WorkspaceName({
- className,
- workspaceName,
- isArchived,
-}: {
- className?: string
- workspaceName: string
- isArchived: boolean | undefined
-}) {
- const navigate = useNavigate()
- const { mutateAsync, isPending, error } = useMutationCreateWorkspace()
- const errorMsg = error?.detail ? `${error?.detail}` : ''
- const formState = useFormState({
- workspaceName,
- });
- const { values, updateFormValues, setInitialValues } = formState;
- const isDefault = workspaceName === "default";
- const isUneditable = isArchived || isPending || isDefault;
-
- useEffect(() => {
- setInitialValues({ workspaceName });
- }, [setInitialValues, workspaceName]);
-
- const handleSubmit = (event: FormEvent) => {
- event.preventDefault()
-
- mutateAsync(
- { body: { name: workspaceName, rename_to: values.workspaceName } },
- {
- onSuccess: () => {
- formState.setInitialValues({ workspaceName: values.workspaceName });
- navigate(`/workspace/${values.workspaceName}`);
- },
- },
- );
- };
-
- return (
-
- )
-}
diff --git a/src/features/workspace/hooks/use-mutation-create-workspace.ts b/src/features/workspace/hooks/use-mutation-create-workspace.ts
index 400d4f43..baae94ab 100644
--- a/src/features/workspace/hooks/use-mutation-create-workspace.ts
+++ b/src/features/workspace/hooks/use-mutation-create-workspace.ts
@@ -2,15 +2,18 @@ import {
v1CreateWorkspaceMutation,
v1GetWorkspaceCustomInstructionsQueryKey,
v1GetWorkspaceMuxesQueryKey,
-} from "@/api/generated/@tanstack/react-query.gen";
-import { useInvalidateWorkspaceQueries } from "./use-invalidate-workspace-queries";
-import { useToastMutation } from "@/hooks/use-toast-mutation";
-import { useQueryClient } from "@tanstack/react-query";
-import { removeQueriesByIds } from "@/lib/react-query-utils";
+ v1ListWorkspacesQueryKey,
+ v1ListArchivedWorkspacesQueryKey,
+ v1ListActiveWorkspacesQueryKey,
+} from '@/api/generated/@tanstack/react-query.gen'
+import { useInvalidateWorkspaceQueries } from './use-invalidate-workspace-queries'
+import { useToastMutation } from '@/hooks/use-toast-mutation'
+import { useQueryClient } from '@tanstack/react-query'
+import { removeQueriesByIds } from '@/lib/react-query-utils'
export function useMutationCreateWorkspace() {
- const queryClient = useQueryClient();
- const invalidate = useInvalidateWorkspaceQueries();
+ const queryClient = useQueryClient()
+ const invalidate = useInvalidateWorkspaceQueries()
return useToastMutation({
...v1CreateWorkspaceMutation(),
@@ -18,11 +21,14 @@ export function useMutationCreateWorkspace() {
removeQueriesByIds({
queryClient,
queryKeyFns: [
- v1GetWorkspaceMuxesQueryKey,
v1GetWorkspaceCustomInstructionsQueryKey,
+ v1GetWorkspaceMuxesQueryKey,
+ v1ListWorkspacesQueryKey,
+ v1ListArchivedWorkspacesQueryKey,
+ v1ListActiveWorkspacesQueryKey,
],
- });
- await invalidate();
+ })
+ await invalidate()
},
successMsg: (variables) =>
variables.body.rename_to
diff --git a/src/routes/route-workspace.tsx b/src/routes/route-workspace.tsx
index d5df90ed..073118db 100644
--- a/src/routes/route-workspace.tsx
+++ b/src/routes/route-workspace.tsx
@@ -1,7 +1,7 @@
import { BreadcrumbHome } from '@/components/BreadcrumbHome'
import { ArchiveWorkspace } from '@/features/workspace/components/archive-workspace'
import { PageHeading } from '@/components/heading'
-import { WorkspaceName } from '@/features/workspace/components/workspace-name'
+import { FormWorkspaceName } from '@/features/workspace/components/form-workspace-name'
import { Alert, Breadcrumb, Breadcrumbs } from '@stacklok/ui-kit'
import { useParams } from 'react-router-dom'
import { useArchivedWorkspaces } from '@/features/workspace/hooks/use-archived-workspaces'
@@ -51,20 +51,23 @@ export function RouteWorkspace() {
{isArchived ? : null}
-