feat(server): cascade-resolved display: labels for the canonical project peers
A directory's display: map (on-disk child name → friendly label) was read only from the immediate on-disk .zddc, so the baked-in defaults could never supply labels. Resolve it through the cascade instead (new zddc.DisplayAt: embedded baseline + ancestor + on-disk overrides, deepest wins per key) and declare the labels in the embedded project-level default (defaults/_any_/.zddc): archive→Archive, incoming→Incoming, working→Working, staging→Staging, reviewing→Reviewing, mdl→"Master Deliverables List", rsk→"Risk Register", ssr→"Supplier/Subcontractor Status Report". On-disk names stay simple/lowercase; clients render display_name in their place (browse already does). An operator's on-disk display: still wins per key. Drops the now-unused readDisplayMap (folded into DisplayAt). Verified in a containerized browser: /Proj/ shows all eight friendly labels, with mdl/rsk/ssr still rendered as click-to-table leaves. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
18d3aaebf0
commit
7f5a54f845
3 changed files with 55 additions and 22 deletions
|
|
@ -80,9 +80,13 @@ func ListDirectory(ctx context.Context, decider policy.Decider, fsRoot, dirPath,
|
|||
result := make([]listing.FileInfo, 0, len(entries))
|
||||
|
||||
// Display overrides for this directory's children, sourced from
|
||||
// THIS directory's .zddc `display:` map. Built once and looked up
|
||||
// case-insensitively per entry. Empty map = no overrides.
|
||||
displayMap := readDisplayMap(absDir)
|
||||
// THIS directory's cascade-resolved `display:` map (embedded defaults +
|
||||
// ancestor + on-disk overrides). Built once and looked up case-
|
||||
// insensitively per entry. Empty/nil = no overrides. Cascade-resolved
|
||||
// (not just the on-disk file) so the baked-in default labels — archive →
|
||||
// "Archive", mdl → "Master Deliverables List", … — render with no
|
||||
// per-project config, while an operator's on-disk display: still wins.
|
||||
displayMap := zddc.DisplayAt(fsRoot, absDir)
|
||||
|
||||
// Set of cascade-declared child names (lowercase) for this dir.
|
||||
// Entries with a matching name get Declared=true so clients can
|
||||
|
|
@ -396,25 +400,6 @@ func virtualCanonicalFolders(ctx context.Context, decider policy.Decider, fsRoot
|
|||
return synth
|
||||
}
|
||||
|
||||
// readDisplayMap parses dirAbs/.zddc and returns its Display map (or
|
||||
// nil when the file doesn't exist or has no display block). All keys
|
||||
// are case-folded to lowercase so lookupDisplay's case-insensitive
|
||||
// match is a simple map read.
|
||||
func readDisplayMap(dirAbs string) map[string]string {
|
||||
zf, err := zddc.ParseFile(filepath.Join(dirAbs, ".zddc"))
|
||||
if err != nil || len(zf.Display) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]string, len(zf.Display))
|
||||
for k, v := range zf.Display {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
out[strings.ToLower(strings.TrimSpace(k))] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// lookupDisplay returns the custom display label for name (matched
|
||||
// case-insensitively against displayMap's keys), or "" when no
|
||||
// override applies.
|
||||
|
|
|
|||
|
|
@ -5,3 +5,17 @@ acl:
|
|||
project_team: r
|
||||
observer: r
|
||||
document_controller: rw
|
||||
|
||||
# Friendly labels for the canonical project peers. On-disk names stay
|
||||
# simple/lowercase; clients render these (listing display_name) in their
|
||||
# place. Cascade-merged + per-key overridable, so an operator can rename
|
||||
# any label in an on-disk project .zddc without renaming the folder.
|
||||
display:
|
||||
archive: Archive
|
||||
incoming: Incoming
|
||||
working: Working
|
||||
staging: Staging
|
||||
reviewing: Reviewing
|
||||
mdl: Master Deliverables List
|
||||
rsk: Risk Register
|
||||
ssr: "Supplier/Subcontractor Status Report"
|
||||
|
|
|
|||
|
|
@ -31,6 +31,40 @@ func DefaultToolAt(fsRoot, dirPath string) string {
|
|||
return chain.Embedded.DefaultTool
|
||||
}
|
||||
|
||||
// DisplayAt returns the cascade-resolved `display:` map for a directory —
|
||||
// the human-friendly labels a client renders in place of on-disk child
|
||||
// names (e.g. mdl → "Master Deliverables List"). Unlike a single tool
|
||||
// name, display: is a MAP that merges across the cascade: the embedded
|
||||
// baseline is the floor, then each on-disk level overrides per key
|
||||
// (shallow→deep, so the deepest .zddc wins). Keys are lower-cased so the
|
||||
// caller's lookup is case-insensitive on the on-disk basename. Returns nil
|
||||
// when nothing declares a label. Mirrors how walker.go merges Display, but
|
||||
// resolved on demand for one path (the listing reads it per directory).
|
||||
func DisplayAt(fsRoot, dirPath string) map[string]string {
|
||||
chain, err := EffectivePolicy(fsRoot, dirPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
merged := map[string]string{}
|
||||
add := func(m map[string]string) {
|
||||
for k, v := range m {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
merged[strings.ToLower(strings.TrimSpace(k))] = v
|
||||
}
|
||||
}
|
||||
add(chain.Embedded.Display) // embedded baseline (the defaults tree)
|
||||
for i := 0; i < len(chain.Levels); i++ {
|
||||
add(chain.Levels[i].Display) // on-disk overrides, shallow→deep
|
||||
}
|
||||
if len(merged) == 0 {
|
||||
return nil
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
// DirToolAt returns the cascade-resolved tool name served at the
|
||||
// directory's TRAILING-SLASH URL form. Walks chain.Levels leaf→root
|
||||
// (then the embedded defaults), returning the first non-empty
|
||||
|
|
|
|||
Loading…
Reference in a new issue