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))
|
result := make([]listing.FileInfo, 0, len(entries))
|
||||||
|
|
||||||
// Display overrides for this directory's children, sourced from
|
// Display overrides for this directory's children, sourced from
|
||||||
// THIS directory's .zddc `display:` map. Built once and looked up
|
// THIS directory's cascade-resolved `display:` map (embedded defaults +
|
||||||
// case-insensitively per entry. Empty map = no overrides.
|
// ancestor + on-disk overrides). Built once and looked up case-
|
||||||
displayMap := readDisplayMap(absDir)
|
// 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.
|
// Set of cascade-declared child names (lowercase) for this dir.
|
||||||
// Entries with a matching name get Declared=true so clients can
|
// 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
|
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
|
// lookupDisplay returns the custom display label for name (matched
|
||||||
// case-insensitively against displayMap's keys), or "" when no
|
// case-insensitively against displayMap's keys), or "" when no
|
||||||
// override applies.
|
// override applies.
|
||||||
|
|
|
||||||
|
|
@ -5,3 +5,17 @@ acl:
|
||||||
project_team: r
|
project_team: r
|
||||||
observer: r
|
observer: r
|
||||||
document_controller: rw
|
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
|
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
|
// DirToolAt returns the cascade-resolved tool name served at the
|
||||||
// directory's TRAILING-SLASH URL form. Walks chain.Levels leaf→root
|
// directory's TRAILING-SLASH URL form. Walks chain.Levels leaf→root
|
||||||
// (then the embedded defaults), returning the first non-empty
|
// (then the embedded defaults), returning the first non-empty
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue