From 052ee513f9dc01fed45c12ed25c024b578e338d0 Mon Sep 17 00:00:00 2001 From: Rohan Chakraborty Date: Fri, 3 Jul 2026 14:55:59 +0530 Subject: [PATCH] feat: admin new workspace cleanup --- web/sdk/admin/utils/helper.ts | 19 +++ .../admin/views/organizations/list/create.tsx | 150 ++++++------------ .../create-organization-view.tsx | 2 +- 3 files changed, 70 insertions(+), 101 deletions(-) diff --git a/web/sdk/admin/utils/helper.ts b/web/sdk/admin/utils/helper.ts index 1dc147cc0..c8b97df75 100644 --- a/web/sdk/admin/utils/helper.ts +++ b/web/sdk/admin/utils/helper.ts @@ -22,6 +22,25 @@ export function capitalizeFirstLetter(str: string) { return str.charAt(0).toUpperCase() + str.slice(1); } +// Generate a URL-safe slug from a title. +export function generateSlug(title: string): string { + return title + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric chars with hyphens + .replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens + .substring(0, 30); // Limit length +} + +// Short random suffix to keep auto-generated slugs unique. +export function randomSuffix(length = 4): string { + const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; + let out = ''; + for (let i = 0; i < length; i++) { + out += chars[Math.floor(Math.random() * chars.length)]; + } + return out; +} + export function convertBillingAddressToString(address?: { line1?: string; line2?: string; diff --git a/web/sdk/admin/views/organizations/list/create.tsx b/web/sdk/admin/views/organizations/list/create.tsx index f7af0f15e..ae7de9a84 100644 --- a/web/sdk/admin/views/organizations/list/create.tsx +++ b/web/sdk/admin/views/organizations/list/create.tsx @@ -1,6 +1,7 @@ -import { useEffect, useState } from "react"; +import { ChangeEvent, useCallback, useEffect, useMemo, useState } from "react"; import styles from "./list.module.css"; import { useTerminology } from "../../../hooks/useTerminology"; +import { generateSlug, randomSuffix } from "~/admin/utils/helper"; import { Button, Field, @@ -14,6 +15,7 @@ import { } from "@raystack/apsara"; import { Cross1Icon } from "@radix-ui/react-icons"; import { ImageUpload } from "~/client/components/image-upload"; +import { debounce } from "lodash"; import { z } from "zod"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -21,42 +23,27 @@ import { useMutation } from "@connectrpc/connect-query"; import { Code } from "@connectrpc/connect"; import { AdminServiceQueries } from "@raystack/proton/frontier"; -const orgCreateSchema = z - .object({ - avatar: z.string().optional(), - title: z.string().min(1, "Title is required"), - name: z - .string() - .min(1, "URL is required") - .regex( - /^[a-z0-9-]+$/, - "Use only lowercase letters, numbers, and hyphens", - ), - orgOwnerEmail: z - .string() - .min(1, "Owner email is required") - .email("Enter a valid email address"), - size: z - .string() - .min(1, "Size is required") - .refine( - (value) => Number.isInteger(Number(value)) && Number(value) > 0, - "Enter a valid size greater than 0", - ) - .transform((value) => parseInt(value)), - type: z.string().min(1, "Industry is required"), - otherType: z.string().optional(), - country: z.string().min(1, "Country is required"), - }) - .refine((data) => data.type !== "other" || Boolean(data.otherType?.trim()), { - message: "Please specify the industry", - path: ["otherType"], - }); +const orgCreateSchema = z.object({ + avatar: z.string().optional(), + title: z.string().min(1, "Title is required"), + name: z + .string() + .min(1, "URL is required") + .min(3, "URL not valid, Min 3 characters allowed") + .max(50, "URL not valid, Max 50 characters allowed") + .regex( + /^[a-zA-Z0-9_-]{3,50}$/, + "Only numbers, letters, '-', and '_' are allowed. Spaces are not allowed.", + ), + orgOwnerEmail: z + .string() + .min(1, "Owner email is required") + .email("Enter a valid email address"), + country: z.string().min(1, "Country is required"), +}); type OrgCreateSchema = z.infer; -const otherTypePrefix = "Other - "; - export type CreateOrganizationPanelProps = { open?: boolean; onClose: () => void; @@ -69,14 +56,12 @@ export type CreateOrganizationPanelProps = { export function CreateOrganizationPanel({ open = false, onClose, - organizationTypes = [], appUrl = "", countries: countriesProp = [], onSuccess, }: CreateOrganizationPanelProps) { const t = useTerminology(); const [countries, setCountries] = useState(countriesProp); - const industries = organizationTypes; useEffect(() => { if (countriesProp.length > 0) { @@ -88,8 +73,8 @@ export function CreateOrganizationPanel({ handleSubmit, control, setError, - formState: { isSubmitting, errors }, - watch, + setValue, + formState: { isSubmitting, errors, dirtyFields }, register, } = useForm({ defaultValues: { @@ -97,13 +82,32 @@ export function CreateOrganizationPanel({ title: "", name: "", orgOwnerEmail: "", - type: "", - otherType: "", country: "", }, resolver: zodResolver(orgCreateSchema), }); + const uniqueSuffix = useMemo(() => randomSuffix(), []); + + const debouncedGenerateSlug = useMemo( + () => + debounce((title: string) => { + const baseSlug = generateSlug(title); + setValue("name", baseSlug ? `${baseSlug}-${uniqueSuffix}` : ""); + }, 300), + [setValue, uniqueSuffix], + ); + + useEffect(() => () => debouncedGenerateSlug.cancel(), [debouncedGenerateSlug]); + + const handleTitleChange = useCallback( + (e: ChangeEvent) => { + if (dirtyFields.name) return; + debouncedGenerateSlug(e.target.value); + }, + [debouncedGenerateSlug, dirtyFields.name], + ); + const { mutateAsync: createOrganization, isPending } = useMutation( AdminServiceQueries.adminCreateOrganization, { @@ -127,10 +131,6 @@ export function CreateOrganizationPanel({ title: data.title, orgOwnerEmail: data.orgOwnerEmail, metadata: { - size: data.size.toString(), - type: data.otherType - ? `${otherTypePrefix}${data.otherType}` - : data.type, country: data.country, }, }; @@ -145,8 +145,6 @@ export function CreateOrganizationPanel({ } } - const showOtherTypeField = watch("type") === "other"; - return ( !open && onClose()}> @@ -169,6 +167,7 @@ export function CreateOrganizationPanel({
- + - + - - - - { - return ( - - - - ); - }} - /> - {showOtherTypeField ? ( - - - - ) : null}