diff --git a/src/__tests__/unit/lib/constants.test.ts b/src/__tests__/unit/lib/constants.test.ts index 83fd489420..407512caaa 100644 --- a/src/__tests__/unit/lib/constants.test.ts +++ b/src/__tests__/unit/lib/constants.test.ts @@ -93,8 +93,8 @@ describe("constants", () => { expect(WORKSPACE_SLUG_PATTERNS.VALID.test("invalid_")).toBe(false); }); - it("should have min length of 2", () => { - expect(WORKSPACE_SLUG_PATTERNS.MIN_LENGTH).toBe(2); + it("should have min length of 3", () => { + expect(WORKSPACE_SLUG_PATTERNS.MIN_LENGTH).toBe(3); }); it("should have max length of 50", () => { diff --git a/src/__tests__/unit/lib/schemas/workspace.test.ts b/src/__tests__/unit/lib/schemas/workspace.test.ts index 597b55b716..ced11d7a91 100644 --- a/src/__tests__/unit/lib/schemas/workspace.test.ts +++ b/src/__tests__/unit/lib/schemas/workspace.test.ts @@ -67,7 +67,7 @@ describe("updateWorkspaceSchema", () => { const validSlugs = [ "my-workspace", "test123", - "a1", + "a1b", "workspace-with-many-hyphens", "123numbers", "work_space", diff --git a/src/__tests__/unit/services/validateWorkspaceSlug.test.ts b/src/__tests__/unit/services/validateWorkspaceSlug.test.ts index f5d302259c..7e608f499f 100644 --- a/src/__tests__/unit/services/validateWorkspaceSlug.test.ts +++ b/src/__tests__/unit/services/validateWorkspaceSlug.test.ts @@ -34,7 +34,7 @@ describe("validateWorkspaceSlug", () => { }); test("should accept minimum length slug", () => { - const result = validateWorkspaceSlug("ab"); + const result = validateWorkspaceSlug("abc"); expect(result).toEqual({ isValid: true }); }); @@ -193,7 +193,7 @@ describe("validateWorkspaceSlug", () => { }); test("should reject slug with only hyphens", () => { - const result = validateWorkspaceSlug("--"); + const result = validateWorkspaceSlug("---"); expect(result).toEqual({ isValid: false, error: WORKSPACE_ERRORS.SLUG_INVALID_FORMAT @@ -340,7 +340,7 @@ describe("validateWorkspaceSlug", () => { }); test("should handle slug at exact min length boundary", () => { - const minLengthSlug = "ab"; // exactly MIN_LENGTH characters + const minLengthSlug = "abc"; // exactly MIN_LENGTH characters const result = validateWorkspaceSlug(minLengthSlug); expect(result).toEqual({ isValid: true }); }); diff --git a/src/__tests__/unit/services/workspace-create.test.ts b/src/__tests__/unit/services/workspace-create.test.ts index 0e5fda1de1..cf6affe1ab 100644 --- a/src/__tests__/unit/services/workspace-create.test.ts +++ b/src/__tests__/unit/services/workspace-create.test.ts @@ -263,7 +263,7 @@ describe("createWorkspace - Unit Tests", () => { "my-workspace", "test-123", "workspace-with-hyphens", - "a1", + "a1b", ]; for (const slug of validSlugs) { @@ -290,7 +290,7 @@ describe("createWorkspace - Unit Tests", () => { await expect( createWorkspace({ name: "Test", - slug: "a", // 1 character (min is 2) + slug: "a", // 1 character (min is 3) ownerId: mockUserId, }) ).rejects.toThrow(WORKSPACE_ERRORS.SLUG_INVALID_LENGTH); @@ -312,7 +312,7 @@ describe("createWorkspace - Unit Tests", () => { (db.workspace.count as Mock).mockResolvedValue(0); (db.workspace.findUnique as Mock).mockResolvedValue(null); - const minSlug = "ab"; // 2 characters + const minSlug = "abc"; // 3 characters const mockWorkspace = createMockWorkspace({ slug: minSlug }); (db.workspace.create as Mock).mockResolvedValue(mockWorkspace); diff --git a/src/__tests__/unit/services/workspace.slug.test.ts b/src/__tests__/unit/services/workspace.slug.test.ts index 9bc8dad102..6c8ac2c673 100644 --- a/src/__tests__/unit/services/workspace.slug.test.ts +++ b/src/__tests__/unit/services/workspace.slug.test.ts @@ -7,7 +7,7 @@ describe("Workspace Slug Validation", () => { test("should accept valid slugs", () => { expect(validateWorkspaceSlug("my-workspace")).toEqual({ isValid: true }); expect(validateWorkspaceSlug("workspace123")).toEqual({ isValid: true }); - expect(validateWorkspaceSlug("a1")).toEqual({ isValid: true }); + expect(validateWorkspaceSlug("a1b")).toEqual({ isValid: true }); expect(validateWorkspaceSlug("test-workspace-123")).toEqual({ isValid: true }); }); diff --git a/src/components/CreateWorkspaceDialog.tsx b/src/components/CreateWorkspaceDialog.tsx index 9b04717456..f455ced15a 100644 --- a/src/components/CreateWorkspaceDialog.tsx +++ b/src/components/CreateWorkspaceDialog.tsx @@ -45,7 +45,11 @@ export function CreateWorkspaceDialog({ // Validation const newErrors: Record = {}; if (!formData.name.trim()) newErrors.name = "Name is required"; - if (!formData.slug.trim()) newErrors.slug = "Slug is required"; + if (!formData.slug.trim()) { + newErrors.slug = "Slug is required"; + } else if (formData.slug.trim().length < 3) { + newErrors.slug = "Workspace name must be between 3 and 50 characters."; + } // The session callback in nextauth.ts ensures user.id is present const userId = (session?.user as { id?: string })?.id; if (!userId) newErrors.ownerId = "User not authenticated"; @@ -124,12 +128,14 @@ export function CreateWorkspaceDialog({ id="slug" placeholder="e.g., my-team" value={formData.slug} - onChange={(e) => - setFormData({ - ...formData, - slug: e.target.value, - }) - } + onChange={(e) => { + const val = e.target.value; + setFormData({ ...formData, slug: val }); + if (val.trim().length > 0 && val.trim().length < 3) { + // Clear length error while still typing — defer until 3+ chars + setErrors((prev) => ({ ...prev, slug: "" })); + } + }} className={errors.slug ? "border-destructive" : ""} disabled={loading} /> diff --git a/src/components/ui/error-display.tsx b/src/components/ui/error-display.tsx index 756985d8ca..a06d5baacb 100644 --- a/src/components/ui/error-display.tsx +++ b/src/components/ui/error-display.tsx @@ -37,7 +37,7 @@ const ERROR_CONFIGS: Record = { [WORKSPACE_ERRORS.SLUG_INVALID_LENGTH]: { type: 'error', title: 'Invalid Name Length', - description: 'Workspace name must be between 2 and 50 characters.', + description: 'Workspace name must be between 3 and 50 characters.', }, [WORKSPACE_ERRORS.SLUG_RESERVED]: { type: 'error', diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 9f3c00dfad..3535d026c9 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -100,7 +100,7 @@ export const RESERVED_WORKSPACE_SLUGS = [ // Workspace slug validation patterns export const WORKSPACE_SLUG_PATTERNS = { VALID: /^[a-z0-9]([a-z0-9_-])*[a-z0-9]$|^[a-z0-9]$/, - MIN_LENGTH: 2, + MIN_LENGTH: 3, MAX_LENGTH: 50, } as const; @@ -129,7 +129,7 @@ export const WORKSPACE_ERRORS = { "This workspace name is reserved. Please choose a different name.", SLUG_INVALID_FORMAT: "Workspace name must start and end with letters or numbers, and can only contain letters, numbers, hyphens, and underscores.", - SLUG_INVALID_LENGTH: "Workspace name must be between 2 and 50 characters.", + SLUG_INVALID_LENGTH: "Workspace name must be between 3 and 50 characters.", SLUG_ALREADY_EXISTS: "A workspace with this name already exists. Please choose a different name.", WORKSPACE_LIMIT_EXCEEDED: