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 ( -
- - - updateFormValues({ workspaceName })} - > - - - - - - - - -
- ) -} 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} -