// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial // SPDX-FileCopyrightText: 2025-2026 Christian Galo package web import "git.coopcloud.tech/wiki-cafe/member-console/internal/forms" // The site-create form (spec fedwiki-sites "The site create form is one // declaration"; spec form-library; design D2, D12; findings FA-17, FA-22). // A create form rendered inside the create-site modal: the modal is a // pre-existing container this change does not restructure (form-conventions' // container-size rule is prospective; the site-create modal predates it), // so only the form's own content (its fields, its 422 handling, its // select's placeholder) moves onto the declaration. // // Field order departs from the template it replaces (domain, then a // domain-suffix picker, then the custom-domain checkbox below): here the // checkbox comes right after the site name, and the suffix picker (renamed // selectedDomain, a real Select field now) follows it, gated by ShowIf so // it is not on the page, and not read on submission, while a custom // domain is in play. That reordering is required, not cosmetic: the // picker's first option is disabled (below), so an unbound submit with an // unchosen picker is refused by design (FA-8's rule, applied here per // fedwiki-sites' "the domain select SHALL open with a disabled, selected // first option, so a site cannot be created on a domain the member did not // choose"); a custom-domain submission does not choose from this picker at // all, so the only way to keep that submission from being wrongly refused // is to keep the picker out of Values entirely while isCustomDomain is // checked (ShowIf's own doc: a field not shown is "neither validated nor // carried into Values"). ShowIf.Field must be declared earlier than the // field reading it, which forces this order. // // isCustomDomain itself declares ShowIf against a hidden field the render // sets from CustomDomainsEnabled, so the checkbox and its explanation exist // in the DOM only when the affordance is actually open, matching the // template this replaces; the field must therefore be declared before it // in Fields (design D2, ShowIf's trust model). // // This file is the form's home; the registry is only the index (design // D1). It sits beside partials.go, which parses through the declaration // and never through r.FormValue. const siteCreateFormName = "fedwiki.site.create" // domainHint is the site-name label grammar, shown under the field. It is // not a Pattern on the field: the same input also carries a full custom // domain FQDN when isCustomDomain is checked, and a label-only regex would // wrongly refuse that submission. The label grammar is instead enforced // handler-side, on the hosted path only, by dnsname.ValidateDNSLabel, the // same split the template this replaces already made. const domainHint = "Lowercase letters, numbers and hyphens only; must start and end with one." // siteCreateForm is the registered declaration. var siteCreateForm = forms.Register(forms.FormSpec{ Name: siteCreateFormName, Kind: forms.KindCreate, Family: forms.Stacked, Method: "POST", Path: "/partials/fedwiki/sites", Target: "#createSiteModalBody", Swap: "innerHTML", Fields: []forms.Field{ { Name: "domain", Label: "Site name", Control: forms.Text, Placeholder: "mysite", Hint: domainHint, Autocomplete: "off", }, // customDomainsEnabled carries CustomDomainsEnabled into the DOM so // isCustomDomain's ShowIf can read it back on a refused // resubmission; it writes nothing of its own (finding FA-10's // lesson: a hidden field is a convenience, never an authority). { Name: "customDomainsEnabled", Label: "Custom domains enabled", Control: forms.Hidden, Optional: true, }, { Name: "isCustomDomain", Label: "Use a domain I own", Control: forms.Checkbox, Value: "true", Hint: "Enter the full domain above (e.g., wiki.example.org) once checked.", Help: forms.Help("Use a domain I own", "A domain your workspace has already verified, or one it sits "+ "under, creates the site right away; otherwise verification starts first and the DNS records show here."), ShowIf: forms.ShowIf{Field: "customDomainsEnabled", Equals: []string{"true"}}, }, // selectedDomain is shown only while isCustomDomain is unchecked (its // raw value is "" both before any choice is made and when the // checkbox is off): a custom-domain submission never chooses from // this picker, so keeping it out of the DOM keeps it out of Parse's // requiredness check too (see the field-order comment above). { Name: "selectedDomain", Label: "Domain", Control: forms.Select, RuntimeOptions: true, ShowIf: forms.ShowIf{Field: "isCustomDomain", Equals: []string{""}}, }, }, Commit: "Create site", WayOut: forms.DismissModal("Cancel"), }) // siteCreateOptions is the option map both the render and the parse read // for the domain select, so what the control offered and what the handler // accepts are one list (spec fedwiki-sites, "the domain select SHALL open // with a disabled, selected first option"). func siteCreateOptions(allowed []string) map[string][]forms.Option { options := []forms.Option{forms.ChooseOption("a domain")} for _, d := range allowed { options = append(options, forms.Option{Value: d, Label: "." + d}) } return map[string][]forms.Option{"selectedDomain": options} }