feat(docforge): slice 5 — docforge-editor pkg + variable catalogue SSOT (t-paliad-349)
Establish the shared frontend editor package and make the Go resolvers the
single source of truth for variable labels.
Go — catalogue SSOT:
- VariableResolver gains Keys() []VariableKey; ResolverSet gains
Catalogue(). The 7 submission resolvers implement Keys() with the
bilingual labels ported from the TS VARIABLE_LABELS table (incl. the
legacy rule.* aliases). Keys() is entity-independent, so
SubmissionVariableCatalogue() builds a metadata-only ResolverSet.
- GET /api/docforge/variables serves the catalogue (auth-gated, static).
- Tests: docforge ResolverSet (BuildBag merge + Catalogue order) and the
submission catalogue integrity (no dupes, labels present, spot-checks).
Frontend — frontend/src/lib/docforge-editor/ (new shared package):
- dom.ts: escapeHtml + cssEscape (pure), with bun tests. Dedupes the two
identical escapeHtml/escapeHTML copies + the cssEscape copy that lived
in the submission editor.
- catalogue.ts: fetchVariableCatalogue() + labelMap() — the client for
the Go catalogue.
- submission-draft.ts now imports escapeHtml/cssEscape from the lib and
fetches the catalogue on boot into state.varLabels (labelFor reads it,
falling back to the raw key if the fetch fails — graceful degrade). The
hardcoded VARIABLE_LABELS table is removed; VARIABLE_GROUPS stays
(presentation: which keys to show + how to section them, legitimately
frontend).
Scope note: the DOM-coupled editor plumbing (wireDraftVars/focus
preservation/autosave debounce) is extracted in slice 6 alongside its first
reuse — the authoring page — rather than speculatively now (extract with the
consumer; same principle as slices 2-3). Slice 5 lands the pure utilities +
the catalogue, which the slice-6 authoring palette consumes.
Verification: go build/vet/test green (Go files gofmt-clean; handlers.go
pre-existing drift, added region clean); bun run build.ts clean;
bun test 274/274 (incl. 5 new docforge-editor tests).
m/paliad#157
This commit is contained in:
@@ -18,14 +18,34 @@ package docforge
|
||||
// engine.
|
||||
type VariableResolver interface {
|
||||
// Namespace returns the dotted prefix this resolver owns, e.g.
|
||||
// "project". Informational — used for diagnostics and (later) the
|
||||
// authoring variable palette's grouping.
|
||||
// "project". Informational — used for diagnostics and as the default
|
||||
// group for this resolver's catalogue entries.
|
||||
Namespace() string
|
||||
|
||||
// Populate writes this resolver's keys into bag. Resolvers own
|
||||
// disjoint namespaces, so population order across resolvers does not
|
||||
// affect the final bag.
|
||||
Populate(bag PlaceholderMap)
|
||||
|
||||
// Keys returns the user-facing catalogue entries for this resolver —
|
||||
// the variables an authoring palette can offer and a sidebar form can
|
||||
// render, each with its bilingual label. This is the curated, static
|
||||
// surface (e.g. the flat parties.claimant.name form), not the full
|
||||
// possibly-dynamic key set Populate emits (e.g. the indexed
|
||||
// parties.claimant.0.name). Go owns these labels so the frontend form
|
||||
// and the authoring palette read one source of truth instead of a
|
||||
// duplicated TS table.
|
||||
Keys() []VariableKey
|
||||
}
|
||||
|
||||
// VariableKey is one catalogue entry: the placeholder key plus its
|
||||
// bilingual label and a group (the owning namespace by default). The
|
||||
// frontend maps groups onto its own lawyer-facing presentation sections.
|
||||
type VariableKey struct {
|
||||
Key string `json:"key"`
|
||||
LabelDE string `json:"label_de"`
|
||||
LabelEN string `json:"label_en"`
|
||||
Group string `json:"group"`
|
||||
}
|
||||
|
||||
// ResolverSet composes an ordered list of VariableResolvers into a single
|
||||
@@ -54,3 +74,16 @@ func (s *ResolverSet) BuildBag() PlaceholderMap {
|
||||
}
|
||||
return bag
|
||||
}
|
||||
|
||||
// Catalogue concatenates every resolver's Keys() in resolver order — the
|
||||
// full set of user-facing variables for a palette or form, with bilingual
|
||||
// labels. It does not require any per-call entity state, so a consumer can
|
||||
// build a metadata-only ResolverSet (resolvers constructed with nil
|
||||
// entities) purely to serve the catalogue.
|
||||
func (s *ResolverSet) Catalogue() []VariableKey {
|
||||
var out []VariableKey
|
||||
for _, r := range s.resolvers {
|
||||
out = append(out, r.Keys()...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
60
pkg/docforge/vars_test.go
Normal file
60
pkg/docforge/vars_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package docforge
|
||||
|
||||
import "testing"
|
||||
|
||||
// fakeResolver is a test double: it owns a namespace, populates a fixed
|
||||
// set of key/value pairs, and advertises a fixed catalogue.
|
||||
type fakeResolver struct {
|
||||
ns string
|
||||
values map[string]string
|
||||
catalog []VariableKey
|
||||
}
|
||||
|
||||
func (f fakeResolver) Namespace() string { return f.ns }
|
||||
func (f fakeResolver) Keys() []VariableKey { return f.catalog }
|
||||
func (f fakeResolver) Populate(bag PlaceholderMap) {
|
||||
for k, v := range f.values {
|
||||
bag[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolverSet_BuildBagMergesDisjointNamespaces(t *testing.T) {
|
||||
set := NewResolverSet(
|
||||
fakeResolver{ns: "a", values: map[string]string{"a.x": "1", "a.y": "2"}},
|
||||
fakeResolver{ns: "b", values: map[string]string{"b.z": "3"}},
|
||||
)
|
||||
bag := set.BuildBag()
|
||||
if len(bag) != 3 {
|
||||
t.Fatalf("bag size = %d; want 3", len(bag))
|
||||
}
|
||||
for k, want := range map[string]string{"a.x": "1", "a.y": "2", "b.z": "3"} {
|
||||
if bag[k] != want {
|
||||
t.Errorf("bag[%q] = %q; want %q", k, bag[k], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolverSet_AddAndCatalogueOrder(t *testing.T) {
|
||||
set := NewResolverSet(
|
||||
fakeResolver{ns: "a", catalog: []VariableKey{{Key: "a.x", Group: "a"}}},
|
||||
)
|
||||
set.Add(fakeResolver{ns: "b", catalog: []VariableKey{
|
||||
{Key: "b.y", Group: "b"},
|
||||
{Key: "b.z", Group: "b"},
|
||||
}})
|
||||
|
||||
cat := set.Catalogue()
|
||||
gotOrder := make([]string, len(cat))
|
||||
for i, e := range cat {
|
||||
gotOrder[i] = e.Key
|
||||
}
|
||||
want := []string{"a.x", "b.y", "b.z"} // resolver order, then Keys() order
|
||||
if len(gotOrder) != len(want) {
|
||||
t.Fatalf("catalogue len = %d; want %d", len(gotOrder), len(want))
|
||||
}
|
||||
for i := range want {
|
||||
if gotOrder[i] != want[i] {
|
||||
t.Errorf("catalogue[%d] = %q; want %q", i, gotOrder[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user