package web
import (
"context"
"net/url"
"sourcecraft.dev/bigbes/sr-ht-dolt/browse"
"sourcecraft.dev/bigbes/sr-ht-dolt/core"
)
// View is a specialized, read-only rendering of a repository whose table shape
// it recognizes. Views are registered at init time via RegisterView and chosen
// per-repo by Applies. The generic table browser is always available too, so a
// View never has to be exhaustive.
//
// A View pulls all of its data through the BrowseSession surface only (Tables,
// Rows, Branches, Log, ...); there is no SQL engine behind a bare store. Build
// returns an opaque value handed to the view's Template as its .Data field.
type View interface {
Name() string // URL slug, e.g. "beads"; must be a valid path segment, unique
Label() string // human tab label, e.g. "Beads"
Template() string // page template filename in templates/, e.g. "beads.html"
Applies(tables []browse.TableInfo) bool // fingerprint by table names/columns
// Build produces the opaque .Data value for the view's Template. query is the
// request URL's query string, so a view can offer sub-modes (e.g. beads'
// ?issue=<id> detail pane) without a new route.
Build(ctx context.Context, sess BrowseSession, repo *core.Repo, ref string, query url.Values) (any, error)
}
// registeredViews is the package-global registry populated at init time by
// RegisterView. It is read once per process: Register snapshots it into
// app.views so handlers (and tests, which set app.views directly) never touch
// the global at request time.
//
// Nothing here parses a view's Template() any more: pages discovers every page
// in templates/, so a view's page is registered by its file existing. A view
// whose Template() names no such file renders as a 500 with a log line, which
// is what it is.
var registeredViews []View
// init registers every view this service ships, in the order their tabs appear.
//
// The registration lives here, in one list, rather than in an init() beside each
// concrete view — which is where it used to be, and which made the tab order a
// consequence of file names. A package's init functions run in lexical file
// order, so "memory.go" registering itself would have put Memory between Beads
// and Milestones, and the only way to restore the intended order from inside
// memory.go was to register a view belonging to another file first. Tab order is
// a decision, and a decision that reads as a list of three lines cannot be
// changed by renaming a file.
func init() {
RegisterView(&beadsView{})
RegisterView(&milestonesView{})
RegisterView(&memoryView{})
}
// RegisterView adds v to the global registry, appending it after everything
// registered before it: registration order is tab order. If a view with the same
// Name() is already registered it is replaced in place, so re-registering (a
// test swapping an implementation, say) never reorders the bar.
func RegisterView(v View) {
for i, existing := range registeredViews {
if existing.Name() == v.Name() {
registeredViews[i] = v
return
}
}
registeredViews = append(registeredViews, v)
}
// applicableViews returns the subset of views whose Applies reports true for
// tables, preserving registration order. It is pure: handlers pass their own
// snapshot (app.views) so the result is deterministic and testable.
func applicableViews(views []View, tables []browse.TableInfo) []View {
out := make([]View, 0, len(views))
for _, v := range views {
if v.Applies(tables) {
out = append(out, v)
}
}
return out
}