Files
member-console/internal/server/list_scale_forms.go
T
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

122 lines
4.4 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
package server
import (
"strconv"
"git.coopcloud.tech/wiki-cafe/member-console/internal/forms"
)
// The governed list's search (spec operator-panel-navigation "The landing
// surface's lookup is a search form"; spec form-conventions "Every mutation
// form's no-JS behavior is a recorded decision"; design D4). One
// declaration, rendered once per governed list with its own field name
// (ListNav.QParam, prefix applied) and, for an embedded list sharing a page
// with siblings, its own hidden carry-along state (design D2's ShowIf-style
// per-request field list, the same shape operator.integration.settings
// uses: the field the person types into is always "the list's own search
// box"; which hidden state rides with it depends on which list this is).
//
// This file is the form's home; the registry is only the index (design D1).
// It sits beside list_scale.go, which owns ListNav.
const listControlsFormName = "operator.list.controls"
// listControlsForm is the registered declaration: the contract every
// governed list's search shares (kind, family, method, commit).
// listSearchForm builds the real per-request declaration, whose field name
// and hidden state depend on the ListNav the list handed it.
//
// The Bar family is the only one a Search form may declare (design D20):
// one input group with a leading magnifier, no box, and an outline button,
// so the list's own "New …" link stays the page's one filled control
// (form-conventions' button-weight rule). Round 3 forced this into Stacked
// and every list page rendered a narrow input with its button wrapped onto
// the next line (maintainer, 2026-09-04).
var listControlsForm = forms.Register(forms.FormSpec{
Name: listControlsFormName,
Kind: forms.KindSearch,
Family: forms.Bar,
Method: "GET",
Path: "/operator",
Fields: []forms.Field{
{Name: "q", Label: "Search this list", HideLabel: true, Control: forms.Text, Optional: true, Autocomplete: "off"},
},
Commit: "Search",
})
// listSearchForm builds and renders the search declaration for one list's
// current state. A standalone list (Target unset) renders Boosted: no
// explicit htmx attributes, relying on the page's own hx-boost inheritance
// for both the scripted behaviour and the native no-JS fallback, because
// its target is the page itself, not a selector this declaration could
// name. An embedded list (Target set) narrows the request to its own panel
// with HXSelect, and refreshes any sibling panels with HXSelectOOB.
func listSearchForm(n ListNav) forms.FormView {
spec := listControlsForm
spec.Path = n.BasePath
spec.Target = n.Target
spec.Fields = []forms.Field{
{
Name: n.QParam(),
Label: "Search this list",
HideLabel: true,
Control: forms.Text,
Optional: true,
Placeholder: n.SearchPlaceholder,
Autocomplete: "off",
},
}
for k, vs := range n.ExtraInputs() {
if len(vs) == 0 {
continue
}
spec.Fields = append(spec.Fields, forms.Field{Name: k, Label: k, Control: forms.Hidden, Optional: true})
}
if n.Facet != "" && n.FacetParam != "" {
spec.Fields = append(spec.Fields, forms.Field{Name: n.FacetParam, Label: "Filter", Control: forms.Hidden, Optional: true})
}
if n.PerPageOffDefault() {
spec.Fields = append(spec.Fields, forms.Field{Name: n.PerParam(), Label: "Page size", Control: forms.Hidden, Optional: true})
}
if n.Q != "" {
spec.WayOut = forms.ScopedLinkOut("Clear", n.ClearSearchURL(), n.Target)
if n.Target == "" {
// A standalone list's Clear is an ordinary boosted navigation,
// the same as the search itself.
spec.WayOut = forms.LinkOut("Clear", n.ClearSearchURL())
}
}
values := forms.NewValues()
values.Set(n.QParam(), n.Q)
for k, vs := range n.ExtraInputs() {
if len(vs) > 0 {
values.Set(k, vs[0])
}
}
if n.Facet != "" && n.FacetParam != "" {
values.Set(n.FacetParam, n.Facet)
}
if n.PerPageOffDefault() {
values.Set(n.PerParam(), strconv.Itoa(n.EffectivePerPage()))
}
b := forms.Binding{Mode: forms.ModeRecord, Values: values}
if n.Target != "" {
b.HXSelect = n.Target
if n.SyncSelect != "" {
b.HXSelectOOB = n.SyncSelect
}
} else {
b.Boosted = true
}
return forms.Render(spec, b)
}
// Form is listSearchForm(n), for the template call {{ template "form"
// .Form }} inside operator_list_controls.html's listSearch define.
func (n ListNav) Form() forms.FormView { return listSearchForm(n) }