fix(wizard): fork 409 — auto-unique slug + editable name/slug in fork step
Bug: forking a template POSTed /v1/servers with slug = trySlug(template.title),
a fixed value. If the user had any server with that slug already (e.g. forking
the same template twice, or a name collision), the create returned 409
slug_taken — and the fork wizard skips Step 1, so there was no slug field to
fix it with. The user was stuck (8 repeated 409s in the report).
Fix:
- On fork setup, after the fork call, GET /v1/servers and auto-unique the
default slug: echo-demo-template -> echo-demo-template-2 -> -3 ... against the
user's existing slugs. Lookup failure is non-fatal (slug field is editable).
- Fork step 2 now renders editable Name + Slug fields in the fork banner
('must be unique in your workspace' hint) — the normal wizard has these in
Step 1, which the fork flow skips, so they belong here.
- slug_taken build error now reads 'The slug "x" is already used by one of
your servers — change the Slug field above' instead of the raw code.
Note: the SES lockdown-install.js and content.js 'query' errors in the report
are browser extensions, not the app.
Verified: forked echo-demo-template (whose base slug was already taken) — slug
auto-filled echo-demo-template-2, build succeeded, container live on :4111,
template fork_count incremented to 2.
This commit is contained in:
parent
414903f16d
commit
c78420e0be
@ -130,8 +130,27 @@ export default function NewServerPage() {
|
||||
};
|
||||
}>(`/v1/templates/${templateSlug}/fork`, { method: 'POST', body: '{}' });
|
||||
if (cancelled) return;
|
||||
|
||||
// Auto-unique the slug against the user's existing servers so the
|
||||
// default fork doesn't 409 (e.g. forking the same template twice).
|
||||
let uniqueSlug = trySlug(res.template.title);
|
||||
try {
|
||||
const own = await apiFetch<{ servers: { slug: string }[] }>('/v1/servers');
|
||||
const taken = new Set(own.servers.map((s) => s.slug));
|
||||
if (taken.has(uniqueSlug)) {
|
||||
const base = uniqueSlug;
|
||||
let n = 2;
|
||||
while (taken.has(`${base}-${n}`)) n++;
|
||||
uniqueSlug = `${base}-${n}`;
|
||||
}
|
||||
} catch {
|
||||
// If the lookup fails we still proceed; the slug field is editable
|
||||
// and the build surfaces a clear slug_taken error.
|
||||
}
|
||||
if (cancelled) return;
|
||||
|
||||
setName(res.template.title);
|
||||
setSlug(trySlug(res.template.title));
|
||||
setSlug(uniqueSlug);
|
||||
setPrompt(`Fork of "${res.template.title}" template.`);
|
||||
setForkedTemplateId(res.templateId);
|
||||
setForkedTemplateTitle(res.template.title);
|
||||
@ -325,7 +344,12 @@ export default function NewServerPage() {
|
||||
setStep('building');
|
||||
} catch (e) {
|
||||
const detail = (e as { detail?: { error?: string; detail?: unknown } }).detail;
|
||||
setError(detail?.error ?? (e as Error).message);
|
||||
const code = detail?.error;
|
||||
setError(
|
||||
code === 'slug_taken'
|
||||
? `The slug "${slug}" is already used by one of your servers — change the Slug field above.`
|
||||
: (code ?? (e as Error).message),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -424,20 +448,45 @@ export default function NewServerPage() {
|
||||
{step === 'confirm' && preview && editable && (
|
||||
<div className="mt-7 space-y-6">
|
||||
{forkedTemplateTitle && (
|
||||
<div className="panel-subtle p-3 flex items-center justify-between">
|
||||
<div className="panel-subtle p-3 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-[12.5px]">
|
||||
Forking <span className="mono font-semibold">{forkedTemplateTitle}</span> — fill in
|
||||
your own credentials below. The template author never sees them.
|
||||
Forking <span className="mono font-semibold">{forkedTemplateTitle}</span> — name
|
||||
your copy and fill in your own credentials. The template author never sees them.
|
||||
</div>
|
||||
<a
|
||||
href={`/templates/${templateSlug}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-[11.5px] text-[--color-fg-muted] underline hover:text-[--color-fg]"
|
||||
className="shrink-0 text-[11.5px] text-[--color-fg-muted] underline hover:text-[--color-fg]"
|
||||
>
|
||||
Template ↗
|
||||
</a>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="fork-name">Name</Label>
|
||||
<Input
|
||||
id="fork-name"
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
if (!slug || slug === trySlug(name)) setSlug(trySlug(e.target.value));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="fork-slug" hint="must be unique in your workspace">
|
||||
Slug
|
||||
</Label>
|
||||
<Input
|
||||
id="fork-slug"
|
||||
value={slug}
|
||||
onChange={(e) => setSlug(trySlug(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="panel p-4">
|
||||
<div className="flex items-baseline justify-between">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user