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.
147 lines
6.6 KiB
Go
147 lines
6.6 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
|
|
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
|
|
|
|
package server
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"git.coopcloud.tech/wiki-cafe/member-console/internal/forms"
|
|
)
|
|
|
|
// Record creation gets its own page (design D20, round 4; maintainer,
|
|
// 2026-09-03: "agree!"). Products, entitlement sets, and plan ladders each
|
|
// have a create page under their list, reached from the list section
|
|
// header's one filled "New ..." link:
|
|
//
|
|
// GET /operator/products/new
|
|
// GET /operator/entitlement-sets/new
|
|
// GET /operator/plan-ladders/new
|
|
//
|
|
// The POST endpoints these forms submit to are unchanged
|
|
// (/partials/operator/{products,entitlement-sets,plan-ladders}): success is
|
|
// still a full navigation to the new record with ?flash=created, and a
|
|
// validation failure now re-renders THIS page with the field errors and the
|
|
// values the operator typed, at 422, instead of the list page.
|
|
//
|
|
// Sub-record creation (a price, a tier, a rule, Issue grant, Extend) stays
|
|
// inline behind a tertiary opener on its section header: those records have
|
|
// no identity apart from the record they hang off, so a page of their own
|
|
// would leave the parent behind for one field.
|
|
|
|
// ProductNewData is the body data for GET /operator/products/new. The
|
|
// page is the product declaration rendered unbound (form-library): no
|
|
// values, no errors, and no gate, because the entitlement-set select's
|
|
// first option creates the set with the product, so a deployment with no
|
|
// set yet is no longer a blocked state (design D14).
|
|
type ProductNewData struct {
|
|
Form forms.FormView
|
|
}
|
|
|
|
// Header titles the page and roots its trail at the products list
|
|
// ("Operator / Products / New product"; design D18).
|
|
func (ProductNewData) Header() PageHeader {
|
|
return PageHeader{Title: "New product", Crumbs: []Link{{Label: "Products", URL: "/operator/products"}}}
|
|
}
|
|
|
|
// EntitlementSetNewData is the body data for GET
|
|
// /operator/entitlement-sets/new. The page is the set declaration rendered
|
|
// unbound (form-library): no values, no errors, and no Active control,
|
|
// because Active is the declaration's edit-only field (design D21).
|
|
type EntitlementSetNewData struct {
|
|
Form forms.FormView
|
|
}
|
|
|
|
// Header titles the page below the sets list ("Operator / Products /
|
|
// Entitlement sets / New entitlement set"), the trail the sets list itself
|
|
// carries plus this page.
|
|
func (EntitlementSetNewData) Header() PageHeader {
|
|
return PageHeader{Title: "New entitlement set", Crumbs: []Link{
|
|
{Label: "Products", URL: "/operator/products"},
|
|
{Label: "Entitlement sets", URL: "/operator/entitlement-sets"},
|
|
}}
|
|
}
|
|
|
|
// PlanLadderNewData is the body data for GET /operator/plan-ladders/new.
|
|
// The page is the ladder declaration rendered unbound (form-library).
|
|
type PlanLadderNewData struct {
|
|
Form forms.FormView
|
|
}
|
|
|
|
// Header titles the page below the ladders list ("Operator / Products /
|
|
// Plan ladders / New plan ladder").
|
|
func (PlanLadderNewData) Header() PageHeader {
|
|
return PageHeader{Title: "New plan ladder", Crumbs: []Link{
|
|
{Label: "Products", URL: "/operator/products"},
|
|
{Label: "Plan ladders", URL: "/operator/plan-ladders"},
|
|
}}
|
|
}
|
|
|
|
// GetProductNewPage handles GET /operator/products/new.
|
|
func (h *OperatorPartialsHandler) GetProductNewPage(w http.ResponseWriter, r *http.Request) {
|
|
page := h.buildOperatorPageData(r)
|
|
page.IAPosition = "catalog:products:new"
|
|
page.ActiveCapability = "products"
|
|
page.BodyTemplate = "operator_product_new.html"
|
|
page.BodyData = h.loadProductNewData(r, forms.NewValues(), nil)
|
|
|
|
h.Templates.Render(w, "operator.html", page)
|
|
}
|
|
|
|
// GetEntitlementSetNewPage handles GET /operator/entitlement-sets/new.
|
|
func (h *OperatorPartialsHandler) GetEntitlementSetNewPage(w http.ResponseWriter, r *http.Request) {
|
|
page := h.buildOperatorPageData(r)
|
|
page.IAPosition = "catalog:entitlement-sets:new"
|
|
page.ActiveCapability = "entitlement-sets"
|
|
page.BodyTemplate = "operator_entitlement_set_new.html"
|
|
page.BodyData = EntitlementSetNewData{Form: entitlementSetCreateForm(forms.NewValues(), nil)}
|
|
|
|
h.Templates.Render(w, "operator.html", page)
|
|
}
|
|
|
|
// GetPlanLadderNewPage handles GET /operator/plan-ladders/new.
|
|
func (h *OperatorPartialsHandler) GetPlanLadderNewPage(w http.ResponseWriter, r *http.Request) {
|
|
page := h.buildOperatorPageData(r)
|
|
page.IAPosition = "catalog:plan-ladders:new"
|
|
page.ActiveCapability = "plan-ladders"
|
|
page.BodyTemplate = "operator_plan_ladder_new.html"
|
|
page.BodyData = PlanLadderNewData{Form: planLadderCreateForm(forms.NewValues(), nil)}
|
|
|
|
h.Templates.Render(w, "operator.html", page)
|
|
}
|
|
|
|
// loadProductNewData hydrates the product create page: the declaration
|
|
// rendered on its create side, with the picker's active entitlement sets
|
|
// behind the side's own first option.
|
|
func (h *OperatorPartialsHandler) loadProductNewData(r *http.Request, values forms.Values, errs *forms.Errors) ProductNewData {
|
|
sets := h.loadEntitlementSetOptions(r)
|
|
return ProductNewData{Form: productCreateForm(sets, values, errs)}
|
|
}
|
|
|
|
// renderProductNewRefusal re-renders the product create page at 422 with
|
|
// the declaration in submission mode: every field's error under its
|
|
// control, the form-level error in the slot the part always renders, and
|
|
// every value that was typed still there (design D9). One function for
|
|
// both kinds of refusal, because a refusal that belongs to no field lands
|
|
// in the form's own slot rather than in a page banner (finding FA-26). The
|
|
// form posts with htmx targeting #operator-body, so the body partial alone
|
|
// is the response and the address bar stays on /operator/products/new.
|
|
func (h *OperatorPartialsHandler) renderProductNewRefusal(w http.ResponseWriter, r *http.Request, values forms.Values, errs *forms.Errors) {
|
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
h.Templates.Render(w, "operator_product_new.html", h.loadProductNewData(r, values, errs))
|
|
}
|
|
|
|
// renderEntitlementSetNewRefusal re-renders the set create page at 422
|
|
// with the declaration in submission mode (design D9).
|
|
func (h *OperatorPartialsHandler) renderEntitlementSetNewRefusal(w http.ResponseWriter, r *http.Request, values forms.Values, errs *forms.Errors) {
|
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
h.Templates.Render(w, "operator_entitlement_set_new.html", EntitlementSetNewData{Form: entitlementSetCreateForm(values, errs)})
|
|
}
|
|
|
|
// renderPlanLadderNewRefusal re-renders the ladder create page at 422
|
|
// with the declaration in submission mode (design D9).
|
|
func (h *OperatorPartialsHandler) renderPlanLadderNewRefusal(w http.ResponseWriter, r *http.Request, values forms.Values, errs *forms.Errors) {
|
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
h.Templates.Render(w, "operator_plan_ladder_new.html", PlanLadderNewData{Form: planLadderCreateForm(values, errs)})
|
|
}
|