mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 22:17:48 +02:00
The create-workspace and onboarding UI hardcoded `multica.ai/` as the workspace slug URL prefix, so self-hosted deployments showed the wrong domain. Add a `workspaceUrlHost` helper that derives the host from the deployment's app URL (`daemon_app_url` from `/api/config`, via the config store) and falls back to the brand host when none is configured, then use it in both views. Fixes #4263. Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
138 lines
4.6 KiB
TypeScript
138 lines
4.6 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";
|
|
import { useConfigStore } from "@multica/core/config";
|
|
import { workspaceUrlHost } from "@multica/core/workspace/workspace-url";
|
|
|
|
export interface CreateWorkspaceFormProps {
|
|
onSuccess: (workspace: Workspace) => void | Promise<void>;
|
|
}
|
|
|
|
export function CreateWorkspaceForm({ onSuccess }: CreateWorkspaceFormProps) {
|
|
const { t } = useT("workspace");
|
|
const createWorkspace = useCreateWorkspace();
|
|
const urlHost = workspaceUrlHost(useConfigStore((s) => s.daemonAppUrl));
|
|
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(
|
|
error instanceof Error && error.message
|
|
? error.message
|
|
: 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">
|
|
<span className="pl-3 text-sm text-muted-foreground select-none">
|
|
{`${urlHost}/`}
|
|
</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>
|
|
);
|
|
}
|