mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-01 01:16:17 +02:00
The slug_reserved error introduced in #2228 was hardcoded English, and the older inline format/conflict errors in step-workspace.tsx had the same problem. Move all of them to the workspace + onboarding locale namespaces (en + zh-Hans) and drop the now-unused string constants from slug.ts. Co-authored-by: multica-agent <github@multica.ai>
132 lines
4.4 KiB
TypeScript
132 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
import { useRef, useState } from "react";
|
|
import { toast } from "sonner";
|
|
import { Input } from "@multica/ui/components/ui/input";
|
|
import { Label } from "@multica/ui/components/ui/label";
|
|
import { Button } from "@multica/ui/components/ui/button";
|
|
import { Card, CardContent } from "@multica/ui/components/ui/card";
|
|
import { useCreateWorkspace } from "@multica/core/workspace/mutations";
|
|
import type { Workspace } from "@multica/core/types";
|
|
import { isImeComposing } from "@multica/core/utils";
|
|
import {
|
|
WORKSPACE_SLUG_REGEX,
|
|
isWorkspaceSlugConflict,
|
|
nameToWorkspaceSlug,
|
|
} from "./slug";
|
|
import { useT } from "../i18n";
|
|
import { isReservedSlug } from "@multica/core/paths";
|
|
|
|
export interface CreateWorkspaceFormProps {
|
|
onSuccess: (workspace: Workspace) => void | Promise<void>;
|
|
}
|
|
|
|
export function CreateWorkspaceForm({ onSuccess }: CreateWorkspaceFormProps) {
|
|
const { t } = useT("workspace");
|
|
const createWorkspace = useCreateWorkspace();
|
|
const [name, setName] = useState("");
|
|
const [slug, setSlug] = useState("");
|
|
const [slugServerError, setSlugServerError] = useState<string | null>(null);
|
|
const slugTouched = useRef(false);
|
|
|
|
const slugValidationError =
|
|
slug.length > 0 && !WORKSPACE_SLUG_REGEX.test(slug)
|
|
? t(($) => $.create_form.errors.slug_format)
|
|
: null;
|
|
const slugReservedError =
|
|
slug.length > 0 && isReservedSlug(slug)
|
|
? t(($) => $.create_form.errors.slug_reserved)
|
|
: null;
|
|
const slugError = slugValidationError ?? slugReservedError ?? slugServerError;
|
|
const canSubmit =
|
|
name.trim().length > 0 && slug.trim().length > 0 && !slugError;
|
|
|
|
const handleNameChange = (value: string) => {
|
|
setName(value);
|
|
if (!slugTouched.current) {
|
|
setSlug(nameToWorkspaceSlug(value));
|
|
setSlugServerError(null);
|
|
}
|
|
};
|
|
|
|
const handleSlugChange = (value: string) => {
|
|
slugTouched.current = true;
|
|
setSlug(value);
|
|
setSlugServerError(null);
|
|
};
|
|
|
|
const handleCreate = () => {
|
|
if (!canSubmit) return;
|
|
createWorkspace.mutate(
|
|
{ name: name.trim(), slug: slug.trim() },
|
|
{
|
|
onSuccess,
|
|
onError: (error) => {
|
|
if (isWorkspaceSlugConflict(error)) {
|
|
setSlugServerError(t(($) => $.create_form.errors.slug_taken));
|
|
toast.error(t(($) => $.create_form.errors.slug_conflict_toast));
|
|
return;
|
|
}
|
|
toast.error(t(($) => $.create_form.errors.create_failed));
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Card className="w-full">
|
|
<CardContent className="space-y-4 pt-6">
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="ws-name">{t(($) => $.create_form.name_label)}</Label>
|
|
<Input
|
|
id="ws-name"
|
|
autoFocus
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => handleNameChange(e.target.value)}
|
|
placeholder={t(($) => $.create_form.name_placeholder)}
|
|
onKeyDown={(e) => {
|
|
if (isImeComposing(e)) return;
|
|
if (e.key === "Enter") handleCreate();
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor="ws-slug">{t(($) => $.create_form.url_label)}</Label>
|
|
<div className="flex items-center gap-0 rounded-md border bg-background focus-within:ring-2 focus-within:ring-ring">
|
|
{/* eslint-disable-next-line i18next/no-literal-string -- brand URL prefix, not translatable */}
|
|
<span className="pl-3 text-sm text-muted-foreground select-none">
|
|
multica.ai/
|
|
</span>
|
|
<Input
|
|
id="ws-slug"
|
|
type="text"
|
|
value={slug}
|
|
onChange={(e) => handleSlugChange(e.target.value)}
|
|
placeholder={t(($) => $.create_form.url_placeholder)}
|
|
className="border-0 shadow-none focus-visible:ring-0"
|
|
onKeyDown={(e) => {
|
|
if (isImeComposing(e)) return;
|
|
if (e.key === "Enter") handleCreate();
|
|
}}
|
|
/>
|
|
</div>
|
|
{slugError && (
|
|
<p className="text-xs text-destructive">{slugError}</p>
|
|
)}
|
|
</div>
|
|
<Button
|
|
className="w-full"
|
|
size="lg"
|
|
onClick={handleCreate}
|
|
disabled={createWorkspace.isPending || !canSubmit}
|
|
>
|
|
{createWorkspace.isPending
|
|
? t(($) => $.create_form.submitting)
|
|
: t(($) => $.create_form.submit)}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|