Files
cgalo5758 88db730fcc Add dual licensing and SPDX headers
Introduce a commercial license option alongside AGPL-3.0-only, require a
CLA for contributors, and document the terms in COMMERCIAL.md and
NOTICE. Add a script to stamp SPDX headers on Go files and apply it
across the tree.
2026-09-06 02:29:42 -05:00

118 lines
5.4 KiB
Go

// 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}
}