From 3d0aa55890c6c79efd0f5bebe9f2bd8f865fde33 Mon Sep 17 00:00:00 2001 From: MichaelUnkey Date: Thu, 23 Oct 2025 16:33:13 -0400 Subject: [PATCH 1/5] update slug as workspace name is entered --- .../app/new/hooks/use-workspace-step.tsx | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/apps/dashboard/app/new/hooks/use-workspace-step.tsx b/apps/dashboard/app/new/hooks/use-workspace-step.tsx index 645545ca02..4e4020b656 100644 --- a/apps/dashboard/app/new/hooks/use-workspace-step.tsx +++ b/apps/dashboard/app/new/hooks/use-workspace-step.tsx @@ -126,7 +126,6 @@ export const useWorkspaceStep = (props: Props): OnboardingStep => { }).length; const isLoading = createWorkspace.isLoading; - return { name: "Workspace", icon: , @@ -135,26 +134,38 @@ export const useWorkspaceStep = (props: Props): OnboardingStep => {
{ + // Re-validate on change to update validFieldCount + form.trigger("workspaceName"); + + // Only auto-generate if not manually edited + if (!slugManuallyEdited && evt.target.value.length > 3) { + form.setValue("slug", slugify(evt.target.value), { + shouldValidate: true, + }); + } + }, + })} placeholder="Enter workspace name" label="Workspace name" - onBlur={(evt) => { - const currentSlug = form.getValues("slug"); - const isSlugDirty = form.formState.dirtyFields.slug; - - // Only auto-generate if slug is empty, not dirty, and hasn't been manually edited - if (!currentSlug && !isSlugDirty && !slugManuallyEdited) { - form.setValue("slug", slugify(evt.currentTarget.value), { - shouldValidate: true, - }); - } - }} required error={form.formState.errors.workspaceName?.message} disabled={isLoading || workspaceCreated} /> { + // If we don't clear the manually set error, it will persist even if the user clears + // or changes the input + form.clearErrors("slug"); + const v = evt.currentTarget.value; + setSlugManuallyEdited(v.length > 0); + + // Re-validate on change to update validFieldCount + form.trigger("slug"); + }, + })} placeholder="enter-a-handle" label="Workspace URL handle" required @@ -167,6 +178,10 @@ export const useWorkspaceStep = (props: Props): OnboardingStep => { form.clearErrors("slug"); const v = evt.currentTarget.value; setSlugManuallyEdited(v.length > 0); + form.setValue("slug", slugify(v), { + shouldValidate: true, + }); + form.trigger("slug"); }} />
From f3bdde9a2d11654a1f7588a50f0d4aae27ff1e73 Mon Sep 17 00:00:00 2001 From: MichaelUnkey Date: Fri, 24 Oct 2025 15:37:48 -0400 Subject: [PATCH 2/5] remove slug if not edited and workspace name is deleted. --- .../app/new/hooks/use-workspace-step.tsx | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/apps/dashboard/app/new/hooks/use-workspace-step.tsx b/apps/dashboard/app/new/hooks/use-workspace-step.tsx index 4e4020b656..7a2dd17832 100644 --- a/apps/dashboard/app/new/hooks/use-workspace-step.tsx +++ b/apps/dashboard/app/new/hooks/use-workspace-step.tsx @@ -22,7 +22,7 @@ const workspaceSchema = z.object({ .max(64, "Workspace slug must be 64 characters or less") .regex( /^[a-z0-9]+(?:-[a-z0-9]+)*$/, - "Use lowercase letters, numbers, and single hyphens (no leading/trailing hyphens).", + "Use lowercase letters, numbers, and single hyphens (no leading/trailing hyphens)." ), }); @@ -100,7 +100,11 @@ export const useWorkspaceStep = (props: Props): OnboardingStep => { ), }); } else if (error.data?.code === "CONFLICT") { - form.setError("slug", { message: error.message }, { shouldFocus: true }); + form.setError( + "slug", + { message: error.message }, + { shouldFocus: true } + ); } else { toast.error(`Failed to create workspace: ${error.message}`); } @@ -140,10 +144,17 @@ export const useWorkspaceStep = (props: Props): OnboardingStep => { form.trigger("workspaceName"); // Only auto-generate if not manually edited - if (!slugManuallyEdited && evt.target.value.length > 3) { - form.setValue("slug", slugify(evt.target.value), { - shouldValidate: true, - }); + if (!slugManuallyEdited) { + if (evt.target.value.length >= 3) { + form.setValue("slug", slugify(evt.target.value), { + shouldValidate: true, + }); + } else { + // Clear slug when workspace name is too short + form.setValue("slug", "", { + shouldValidate: false, + }); + } } }, })} From f5d1c5cdaa3e8ce7bc5a303e455130aa352b9cfd Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 19:39:24 +0000 Subject: [PATCH 3/5] [autofix.ci] apply automated fixes --- apps/dashboard/app/new/hooks/use-workspace-step.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/dashboard/app/new/hooks/use-workspace-step.tsx b/apps/dashboard/app/new/hooks/use-workspace-step.tsx index 7a2dd17832..0c7f9352a9 100644 --- a/apps/dashboard/app/new/hooks/use-workspace-step.tsx +++ b/apps/dashboard/app/new/hooks/use-workspace-step.tsx @@ -22,7 +22,7 @@ const workspaceSchema = z.object({ .max(64, "Workspace slug must be 64 characters or less") .regex( /^[a-z0-9]+(?:-[a-z0-9]+)*$/, - "Use lowercase letters, numbers, and single hyphens (no leading/trailing hyphens)." + "Use lowercase letters, numbers, and single hyphens (no leading/trailing hyphens).", ), }); @@ -100,11 +100,7 @@ export const useWorkspaceStep = (props: Props): OnboardingStep => { ), }); } else if (error.data?.code === "CONFLICT") { - form.setError( - "slug", - { message: error.message }, - { shouldFocus: true } - ); + form.setError("slug", { message: error.message }, { shouldFocus: true }); } else { toast.error(`Failed to create workspace: ${error.message}`); } From 522ce7e2a783d86e705d1584f1d2128579a809aa Mon Sep 17 00:00:00 2001 From: Eeyoritron Date: Tue, 28 Oct 2025 16:09:06 -0400 Subject: [PATCH 4/5] cleanup --- .../app/new/hooks/use-workspace-step.tsx | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/apps/dashboard/app/new/hooks/use-workspace-step.tsx b/apps/dashboard/app/new/hooks/use-workspace-step.tsx index b67ff116b1..7e436f6cfd 100644 --- a/apps/dashboard/app/new/hooks/use-workspace-step.tsx +++ b/apps/dashboard/app/new/hooks/use-workspace-step.tsx @@ -168,8 +168,9 @@ export const useWorkspaceStep = (props: Props): OnboardingStep => { form.clearErrors("slug"); const v = evt.currentTarget.value; setSlugManuallyEdited(v.length > 0); - - // Re-validate on change to update validFieldCount + form.setValue("slug", slugify(v), { + shouldValidate: true, + }); form.trigger("slug"); }, })} @@ -179,17 +180,6 @@ export const useWorkspaceStep = (props: Props): OnboardingStep => { error={form.formState.errors.slug?.message} prefix="app.unkey.com/" maxLength={64} - onChange={(evt) => { - // If we don't clear the manually set error, it will persist even if the user clears - // or changes the input - form.clearErrors("slug"); - const v = evt.currentTarget.value; - setSlugManuallyEdited(v.length > 0); - form.setValue("slug", slugify(v), { - shouldValidate: true, - }); - form.trigger("slug"); - }} />
From 8b064ec72131c717c33f3209a238cf0a5da3c860 Mon Sep 17 00:00:00 2001 From: Eeyoritron Date: Wed, 29 Oct 2025 12:43:46 -0400 Subject: [PATCH 5/5] fix page refresh flicker --- .../app/new/hooks/use-workspace-step.tsx | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/apps/dashboard/app/new/hooks/use-workspace-step.tsx b/apps/dashboard/app/new/hooks/use-workspace-step.tsx index 7e436f6cfd..3aa41e51ea 100644 --- a/apps/dashboard/app/new/hooks/use-workspace-step.tsx +++ b/apps/dashboard/app/new/hooks/use-workspace-step.tsx @@ -4,7 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { StackPerspective2 } from "@unkey/icons"; import { Button, FormInput, toast } from "@unkey/ui"; import { useRouter } from "next/navigation"; -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import type { OnboardingStep } from "../components/onboarding-wizard"; @@ -36,13 +36,22 @@ type Props = { export const useWorkspaceStep = (props: Props): OnboardingStep => { const [slugManuallyEdited, setSlugManuallyEdited] = useState(false); const [workspaceCreated, setWorkspaceCreated] = useState(false); + const [isMounted, setIsMounted] = useState(false); const formRef = useRef(null); const router = useRouter(); const utils = trpc.useUtils(); + useEffect(() => { + setIsMounted(true); + }, []); + const form = useForm({ resolver: zodResolver(workspaceSchema), mode: "onChange", + defaultValues: { + workspaceName: "", + slug: "", + }, }); const switchOrgMutation = trpc.user.switchOrg.useMutation({ @@ -118,10 +127,14 @@ export const useWorkspaceStep = (props: Props): OnboardingStep => { }); }; - const validFieldCount = Object.keys(form.getValues()).filter((field) => { - const fieldName = field as keyof WorkspaceFormData; + // Watch form values to ensure consistent hydration + const workspaceName = form.watch("workspaceName"); + const slug = form.watch("slug"); + + const validFieldCount = (["workspaceName", "slug"] as const).filter((fieldName) => { const hasError = Boolean(form.formState.errors[fieldName]); - const hasValue = Boolean(form.getValues(fieldName)); + const value = fieldName === "workspaceName" ? workspaceName : slug; + const hasValue = Boolean(value); return !hasError && hasValue; }).length; @@ -174,7 +187,7 @@ export const useWorkspaceStep = (props: Props): OnboardingStep => { form.trigger("slug"); }, })} - placeholder="enter-a-handle" + placeholder={isMounted ? "enter-a-handle" : ""} label="Workspace URL handle" required error={form.formState.errors.slug?.message}