feat(tables): rollup Add Row routes via the party column
The project-level MDL/RSK rollup specs lose `addable: false` and gain
a sibling form schema (default-project-{mdl,rsk}.form.yaml) that
makes `party` a required field. + Add row on the rollup view is now
live: the user types the party name in the Package column, the
server reads `party` from the body, validates that
<project>/archive/<party>/ exists on disk, strips the field, and
writes the row into archive/<party>/<slot>/<date>-<email>.yaml. The
response Location is the synthetic <project>/<slot>/<party>__<file>.yaml
URL so the rollup table client swaps the draft URL cleanly.
Wrong party = 422 with a clear error pointing at the SSR view as the
place to create the folder first. No auto-creation here — the rollup
is for filing deliverables/risks against existing packages, not for
spinning up new ones.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cef7188a77
commit
f3d334a221
9 changed files with 383 additions and 19 deletions
91
zddc/internal/handler/default-project-mdl.form.yaml
Normal file
91
zddc/internal/handler/default-project-mdl.form.yaml
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
# Default project-rollup MDL row schema, served by zddc-server when
|
||||||
|
# no operator-supplied form.yaml exists at <project>/mdl/.
|
||||||
|
#
|
||||||
|
# Identical to the per-party MDL schema (default-mdl.form.yaml)
|
||||||
|
# except for one extra required field: `party`. That field is the
|
||||||
|
# routing key — the server reads it on POST <project>/mdl/form.html,
|
||||||
|
# finds the matching <project>/archive/<party>/ folder, and writes
|
||||||
|
# the row inside its mdl/ subfolder. The `party` value is stripped
|
||||||
|
# from the YAML on write (folder name IS the identity); on read the
|
||||||
|
# dispatcher injects it back so the rollup table can show the
|
||||||
|
# Package column.
|
||||||
|
#
|
||||||
|
# To customize: drop your own form.yaml at <project>/mdl/form.yaml.
|
||||||
|
# Keep the `party` field shape unless you also customize the rollup
|
||||||
|
# create handler — the server's routing depends on it.
|
||||||
|
|
||||||
|
title: Deliverable (project rollup)
|
||||||
|
description: One deliverable across all parties. The first field (Package) routes the row to the matching archive/<party>/mdl/ folder; the rest mirrors the per-party MDL schema.
|
||||||
|
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [party, originator, project, discipline, type, sequence, title]
|
||||||
|
additionalProperties: false
|
||||||
|
properties:
|
||||||
|
party:
|
||||||
|
type: string
|
||||||
|
title: Package (party folder)
|
||||||
|
description: Routing key — must match an existing <project>/archive/<party>/ folder. Typical naming = MasterFormat 4-digit code + C|P + sequence digit (e.g. 0330C1).
|
||||||
|
pattern: "^[A-Za-z0-9][A-Za-z0-9.-]*$"
|
||||||
|
minLength: 1
|
||||||
|
originator:
|
||||||
|
type: string
|
||||||
|
title: Originator
|
||||||
|
description: Organizational unit responsible for this deliverable (e.g. ACME).
|
||||||
|
minLength: 1
|
||||||
|
phase:
|
||||||
|
type: string
|
||||||
|
title: Phase
|
||||||
|
description: Optional project phase code (e.g. ECI, EPC).
|
||||||
|
project:
|
||||||
|
type: string
|
||||||
|
title: Project
|
||||||
|
description: Project identifier, or your corporate placeholder for non-project deliverables.
|
||||||
|
minLength: 1
|
||||||
|
area:
|
||||||
|
type: string
|
||||||
|
title: Area
|
||||||
|
description: Optional area / budget code (e.g. B02).
|
||||||
|
discipline:
|
||||||
|
type: string
|
||||||
|
title: Discipline
|
||||||
|
description: Engineering or functional group code (EL, ME, CV, PM, ...).
|
||||||
|
minLength: 1
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
title: Document type
|
||||||
|
description: Document category code within the discipline (SPC, DWG, RPT, ...).
|
||||||
|
minLength: 1
|
||||||
|
sequence:
|
||||||
|
type: string
|
||||||
|
title: Sequence
|
||||||
|
description: Zero-padded integer (0001, 0042, 2623). Stored as a string so leading zeros survive YAML.
|
||||||
|
minLength: 1
|
||||||
|
suffix:
|
||||||
|
type: string
|
||||||
|
title: Suffix
|
||||||
|
description: Optional structural-part suffix.
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
title: Deliverable title
|
||||||
|
minLength: 1
|
||||||
|
plannedRevision:
|
||||||
|
type: string
|
||||||
|
title: Planned revision
|
||||||
|
plannedDate:
|
||||||
|
type: string
|
||||||
|
title: Planned date
|
||||||
|
format: date
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
title: Current status
|
||||||
|
enum: [DFT, IFR, IFA, IFC, AFC, AB]
|
||||||
|
owner:
|
||||||
|
type: string
|
||||||
|
title: Owner
|
||||||
|
notes:
|
||||||
|
type: string
|
||||||
|
title: Notes
|
||||||
|
ui:
|
||||||
|
notes:
|
||||||
|
ui:widget: textarea
|
||||||
|
|
@ -8,15 +8,14 @@
|
||||||
# column is derived from the row's source folder (path-injected by
|
# column is derived from the row's source folder (path-injected by
|
||||||
# the server, not stored in the YAML).
|
# the server, not stored in the YAML).
|
||||||
#
|
#
|
||||||
# + Add row is suppressed in this view because the party affiliation
|
# + Add row IS enabled here: the `party` column doubles as the
|
||||||
# would be ambiguous — add deliverables at the per-party path
|
# routing key — the server reads the submitted `party` field, finds
|
||||||
# (<project>/archive/<party>/mdl/) and they'll appear here on next
|
# the matching <project>/archive/<party>/ folder, and writes the row
|
||||||
# load.
|
# inside its mdl/ subfolder. The party folder must already exist
|
||||||
|
# (create it via the SSR view).
|
||||||
|
|
||||||
title: Project Deliverables (all parties)
|
title: Project Deliverables (all parties)
|
||||||
description: Every deliverable across all parties under archive/. Click a row to edit; add rows at the per-party MDL view.
|
description: Every deliverable across all parties under archive/. Click a row to edit; + Add row uses the Package column to route the new row to the matching archive/<party>/mdl/ folder.
|
||||||
|
|
||||||
addable: false
|
|
||||||
|
|
||||||
columns:
|
columns:
|
||||||
- field: party
|
- field: party
|
||||||
|
|
|
||||||
82
zddc/internal/handler/default-project-rsk.form.yaml
Normal file
82
zddc/internal/handler/default-project-rsk.form.yaml
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
# Default project-rollup RSK row schema, served by zddc-server when
|
||||||
|
# no operator-supplied form.yaml exists at <project>/rsk/.
|
||||||
|
#
|
||||||
|
# Identical to the per-party RSK schema (default-rsk.form.yaml)
|
||||||
|
# except for one extra required field: `party`. The server reads it
|
||||||
|
# on POST <project>/rsk/form.html and routes the row to the matching
|
||||||
|
# <project>/archive/<party>/rsk/ folder. The `party` value is
|
||||||
|
# stripped from the YAML on write (folder name IS the identity); on
|
||||||
|
# read the dispatcher injects it back.
|
||||||
|
#
|
||||||
|
# To customize: drop your own form.yaml at <project>/rsk/form.yaml.
|
||||||
|
# Keep the `party` field shape unless you also customize the rollup
|
||||||
|
# create handler.
|
||||||
|
|
||||||
|
title: Risk (project rollup)
|
||||||
|
description: One risk across all parties. The first field (Package) routes the row to the matching archive/<party>/rsk/ folder; the rest mirrors the per-party RSK schema.
|
||||||
|
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [party, id, title]
|
||||||
|
additionalProperties: false
|
||||||
|
properties:
|
||||||
|
party:
|
||||||
|
type: string
|
||||||
|
title: Package (party folder)
|
||||||
|
description: Routing key — must match an existing <project>/archive/<party>/ folder.
|
||||||
|
pattern: "^[A-Za-z0-9][A-Za-z0-9.-]*$"
|
||||||
|
minLength: 1
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
title: ID
|
||||||
|
description: Stable identifier, e.g. R-001.
|
||||||
|
minLength: 1
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
title: Risk
|
||||||
|
minLength: 1
|
||||||
|
category:
|
||||||
|
type: string
|
||||||
|
title: Category
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
title: Description
|
||||||
|
likelihood:
|
||||||
|
type: integer
|
||||||
|
title: Likelihood
|
||||||
|
minimum: 1
|
||||||
|
maximum: 5
|
||||||
|
impact:
|
||||||
|
type: integer
|
||||||
|
title: Impact
|
||||||
|
minimum: 1
|
||||||
|
maximum: 5
|
||||||
|
severity:
|
||||||
|
type: integer
|
||||||
|
title: Severity
|
||||||
|
minimum: 1
|
||||||
|
maximum: 25
|
||||||
|
mitigation:
|
||||||
|
type: string
|
||||||
|
title: Mitigation
|
||||||
|
owner:
|
||||||
|
type: string
|
||||||
|
title: Owner
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
title: Status
|
||||||
|
enum: [open, mitigated, accepted, closed]
|
||||||
|
dueDate:
|
||||||
|
type: string
|
||||||
|
title: Due date
|
||||||
|
format: date
|
||||||
|
notes:
|
||||||
|
type: string
|
||||||
|
title: Notes
|
||||||
|
ui:
|
||||||
|
description:
|
||||||
|
ui:widget: textarea
|
||||||
|
mitigation:
|
||||||
|
ui:widget: textarea
|
||||||
|
notes:
|
||||||
|
ui:widget: textarea
|
||||||
|
|
@ -7,15 +7,14 @@
|
||||||
# column is derived from the row's source folder (path-injected by
|
# column is derived from the row's source folder (path-injected by
|
||||||
# the server, not stored in the YAML).
|
# the server, not stored in the YAML).
|
||||||
#
|
#
|
||||||
# + Add row is suppressed in this view because the party affiliation
|
# + Add row IS enabled here: the `party` column doubles as the
|
||||||
# would be ambiguous — add risks at the per-party path
|
# routing key — the server reads the submitted `party` field, finds
|
||||||
# (<project>/archive/<party>/rsk/) and they'll appear here on next
|
# the matching <project>/archive/<party>/ folder, and writes the row
|
||||||
# load.
|
# inside its rsk/ subfolder. The party folder must already exist
|
||||||
|
# (create it via the SSR view).
|
||||||
|
|
||||||
title: Project Risk Register (all parties)
|
title: Project Risk Register (all parties)
|
||||||
description: Every risk across all parties under archive/. Click a row to edit; add rows at the per-party RSK view.
|
description: Every risk across all parties under archive/. Click a row to edit; + Add row uses the Package column to route the new row to the matching archive/<party>/rsk/ folder.
|
||||||
|
|
||||||
addable: false
|
|
||||||
|
|
||||||
columns:
|
columns:
|
||||||
- field: party
|
- field: party
|
||||||
|
|
|
||||||
|
|
@ -89,9 +89,12 @@ type FormRequest struct {
|
||||||
// SubmitURL is the URL the form should POST back to (the server-injected
|
// SubmitURL is the URL the form should POST back to (the server-injected
|
||||||
// "submit to my own URL" value).
|
// "submit to my own URL" value).
|
||||||
SubmitURL string
|
SubmitURL string
|
||||||
// Project carries the project name for create-via-ssr requests. Empty
|
// Project carries the project name for create-via-ssr /
|
||||||
// for all other kinds.
|
// create-via-rollup requests. Empty for all other kinds.
|
||||||
Project string
|
Project string
|
||||||
|
// Slot carries the slot name ("mdl" or "rsk") for create-via-rollup
|
||||||
|
// requests. Empty for all other kinds.
|
||||||
|
Slot string
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecognizeFormRequest classifies r as a form-system request, or returns nil
|
// RecognizeFormRequest classifies r as a form-system request, or returns nil
|
||||||
|
|
@ -130,6 +133,26 @@ func RecognizeFormRequest(fsRoot, method, urlPath string) *FormRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Project-rollup MDL / RSK create: /<project>/(mdl|rsk)/form.html
|
||||||
|
// reads a `party` field from the body and routes the new row to
|
||||||
|
// <project>/archive/<party>/<slot>/. Recognized before the generic
|
||||||
|
// /<dir>/form.html branch so a virtual rollup URL doesn't get
|
||||||
|
// misrouted as an in-dir create.
|
||||||
|
if project, slot, ok := zddc.IsRollupCreateURL(urlPath); ok {
|
||||||
|
kind := "render-empty"
|
||||||
|
if method == http.MethodPost {
|
||||||
|
kind = "create-via-rollup"
|
||||||
|
}
|
||||||
|
specAbs := filepath.Join(fsRoot, project, slot, "form.yaml")
|
||||||
|
return &FormRequest{
|
||||||
|
Kind: kind,
|
||||||
|
SpecPath: specAbs,
|
||||||
|
SubmitURL: urlPath,
|
||||||
|
Project: project,
|
||||||
|
Slot: slot,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
underlying := strings.TrimSuffix(urlPath, ".html")
|
underlying := strings.TrimSuffix(urlPath, ".html")
|
||||||
|
|
||||||
// specEligible accepts a spec path that exists on disk OR matches
|
// specEligible accepts a spec path that exists on disk OR matches
|
||||||
|
|
@ -249,6 +272,8 @@ func ServeForm(cfg config.Config, req *FormRequest, w http.ResponseWriter, r *ht
|
||||||
serveFormUpdate(cfg, req, w, r)
|
serveFormUpdate(cfg, req, w, r)
|
||||||
case "create-via-ssr":
|
case "create-via-ssr":
|
||||||
serveFormCreateSSR(cfg, req, w, r)
|
serveFormCreateSSR(cfg, req, w, r)
|
||||||
|
case "create-via-rollup":
|
||||||
|
serveFormCreateRollup(cfg, req, w, r)
|
||||||
default:
|
default:
|
||||||
http.Error(w, "unknown form request kind", http.StatusInternalServerError)
|
http.Error(w, "unknown form request kind", http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
|
"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
|
||||||
"codeberg.org/VARASYS/ZDDC/zddc/internal/jsonschema"
|
"codeberg.org/VARASYS/ZDDC/zddc/internal/jsonschema"
|
||||||
|
|
@ -179,6 +180,133 @@ func serveFormCreateSSR(cfg config.Config, req *FormRequest, w http.ResponseWrit
|
||||||
auditFile(r, "ssr-create", rowURL, http.StatusCreated, len(yamlBytes), nil)
|
auditFile(r, "ssr-create", rowURL, http.StatusCreated, len(yamlBytes), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serveFormCreateRollup adds a row to a project-level MDL or RSK
|
||||||
|
// rollup view by writing it inside the per-party folder named by the
|
||||||
|
// submitted `party` field.
|
||||||
|
//
|
||||||
|
// Wire-form: POST /<project>/(mdl|rsk)/form.html
|
||||||
|
//
|
||||||
|
// Content-Type: application/yaml | application/json
|
||||||
|
// body: { party: "<name>", ...row fields... }
|
||||||
|
//
|
||||||
|
// The rollup form schema (default-project-{mdl,rsk}.form.yaml) makes
|
||||||
|
// `party` a required field; the rollup view's `Package` column maps
|
||||||
|
// to it. The party folder must already exist — create it via the
|
||||||
|
// SSR view first. ACL gate: ActionCreate at
|
||||||
|
// <project>/archive/<party>/<slot>/, same chain the generic
|
||||||
|
// serveFormCreate would gate against if the user were on the
|
||||||
|
// per-party path directly.
|
||||||
|
//
|
||||||
|
// On success: 201 + Location: /<project>/<slot>/<party>__<filename>.yaml,
|
||||||
|
// the virtual row URL that the rollup table client uses to address
|
||||||
|
// the new row.
|
||||||
|
func serveFormCreateRollup(cfg config.Config, req *FormRequest, w http.ResponseWriter, r *http.Request) {
|
||||||
|
email := EmailFromContext(r)
|
||||||
|
if email == "" {
|
||||||
|
http.Error(w, "authentication required", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Project == "" || (req.Slot != "mdl" && req.Slot != "rsk") {
|
||||||
|
http.Error(w, "internal: rollup create missing project/slot", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := decodeRequestData(r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "request body: "+err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
spec, err := loadFormSpec(cfg.Root, req.SpecPath)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "form spec error: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if errs := jsonschema.Validate(spec.Schema, data); len(errs) > 0 {
|
||||||
|
writeValidationErrors(w, errs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dataMap, ok := data.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "request body must be a YAML/JSON object", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
partyRaw, _ := dataMap["party"].(string)
|
||||||
|
party := strings.TrimSpace(partyRaw)
|
||||||
|
if !zddc.ValidPartyName(party) {
|
||||||
|
writeValidationErrors(w, []jsonschema.Error{{
|
||||||
|
Path: "/party",
|
||||||
|
Message: "must match " + `^[A-Za-z0-9][A-Za-z0-9.-]*$`,
|
||||||
|
}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
partyAbs := filepath.Join(cfg.Root, req.Project, "archive", party)
|
||||||
|
if !strings.HasPrefix(partyAbs, cfg.Root+string(filepath.Separator)) {
|
||||||
|
http.Error(w, "Not Found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if info, err := os.Stat(partyAbs); err != nil || !info.IsDir() {
|
||||||
|
writeValidationErrors(w, []jsonschema.Error{{
|
||||||
|
Path: "/party",
|
||||||
|
Message: "party folder does not exist — create it via the SSR view first",
|
||||||
|
}})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slotAbs := filepath.Join(partyAbs, req.Slot)
|
||||||
|
slotURL := "/" + req.Project + "/archive/" + party + "/" + req.Slot + "/"
|
||||||
|
rowDirURL := slotURL // The slot folder where the new row lands.
|
||||||
|
_ = rowDirURL // kept for clarity; ACL chain is gated below.
|
||||||
|
|
||||||
|
// ACL gate: create at <project>/archive/<party>/<slot>/. authorizeAction
|
||||||
|
// walks up to the closest existing ancestor (typically <party>/), where
|
||||||
|
// the auto-own .zddc grants the party owner rwcd.
|
||||||
|
if !authorizeAction(cfg, w, r, slotAbs, slotURL, policy.ActionCreate) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip the routing key from the data before write — the folder
|
||||||
|
// name IS the identity and the per-party MDL/RSK schemas forbid
|
||||||
|
// `additionalProperties` other than the listed ones.
|
||||||
|
delete(dataMap, "party")
|
||||||
|
|
||||||
|
if err := os.MkdirAll(slotAbs, 0o755); err != nil {
|
||||||
|
auditFile(r, "rollup-create", req.SubmitURL, http.StatusInternalServerError, 0, err)
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dateStr := time.Now().UTC().Format("2006-01-02")
|
||||||
|
emailSan := sanitizeEmail(email)
|
||||||
|
base := dateStr + "-" + emailSan
|
||||||
|
target, fname, ok := pickAvailableFilename(slotAbs, base, ".yaml")
|
||||||
|
if !ok {
|
||||||
|
http.Error(w, "could not pick a free filename (>100 collisions)", http.StatusConflict)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
yamlBytes, err := yaml.Marshal(dataMap)
|
||||||
|
if err != nil {
|
||||||
|
auditFile(r, "rollup-create", req.SubmitURL, http.StatusInternalServerError, 0, err)
|
||||||
|
http.Error(w, "marshal yaml: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := zddc.WriteAtomic(target, yamlBytes); err != nil {
|
||||||
|
auditFile(r, "rollup-create", req.SubmitURL, http.StatusInternalServerError, len(yamlBytes), err)
|
||||||
|
http.Error(w, "write: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rowURL := "/" + req.Project + "/" + req.Slot + "/" + party + "__" + fname
|
||||||
|
w.Header().Set("Location", rowURL)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("X-ZDDC-Source", "rollup-create")
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
_, _ = w.Write([]byte(fmt.Sprintf(`{"ok":true,"location":%q}`, rowURL)))
|
||||||
|
auditFile(r, "rollup-create", rowURL, http.StatusCreated, len(yamlBytes), nil)
|
||||||
|
}
|
||||||
|
|
||||||
// serveSSRRename renames a party folder by rewriting an SSR row URL.
|
// serveSSRRename renames a party folder by rewriting an SSR row URL.
|
||||||
//
|
//
|
||||||
// Wire-form: POST /<project>/ssr/<old>.yaml
|
// Wire-form: POST /<project>/ssr/<old>.yaml
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,12 @@ var embeddedDefaultProjectMdlTable []byte
|
||||||
//go:embed default-project-rsk.table.yaml
|
//go:embed default-project-rsk.table.yaml
|
||||||
var embeddedDefaultProjectRskTable []byte
|
var embeddedDefaultProjectRskTable []byte
|
||||||
|
|
||||||
|
//go:embed default-project-mdl.form.yaml
|
||||||
|
var embeddedDefaultProjectMdlForm []byte
|
||||||
|
|
||||||
|
//go:embed default-project-rsk.form.yaml
|
||||||
|
var embeddedDefaultProjectRskForm []byte
|
||||||
|
|
||||||
// DefaultMdlTableYAML returns the embedded default mdl.table.yaml bytes.
|
// DefaultMdlTableYAML returns the embedded default mdl.table.yaml bytes.
|
||||||
// Used by callers that need the canonical spec without going through
|
// Used by callers that need the canonical spec without going through
|
||||||
// the URL-recognition path.
|
// the URL-recognition path.
|
||||||
|
|
@ -93,6 +99,16 @@ func DefaultProjectMdlTableYAML() []byte { return embeddedDefaultProjectMdlTable
|
||||||
// rsk.table.yaml bytes.
|
// rsk.table.yaml bytes.
|
||||||
func DefaultProjectRskTableYAML() []byte { return embeddedDefaultProjectRskTable }
|
func DefaultProjectRskTableYAML() []byte { return embeddedDefaultProjectRskTable }
|
||||||
|
|
||||||
|
// DefaultProjectMdlFormYAML returns the embedded project-rollup
|
||||||
|
// mdl.form.yaml bytes. Differs from the per-party MDL form by an
|
||||||
|
// additional required `party` field — the routing key for the
|
||||||
|
// rollup create handler.
|
||||||
|
func DefaultProjectMdlFormYAML() []byte { return embeddedDefaultProjectMdlForm }
|
||||||
|
|
||||||
|
// DefaultProjectRskFormYAML returns the embedded project-rollup
|
||||||
|
// rsk.form.yaml bytes.
|
||||||
|
func DefaultProjectRskFormYAML() []byte { return embeddedDefaultProjectRskForm }
|
||||||
|
|
||||||
// IsDefaultSpec reports whether urlPath is one of the embedded
|
// IsDefaultSpec reports whether urlPath is one of the embedded
|
||||||
// default-spec virtual files served when no operator file exists on
|
// default-spec virtual files served when no operator file exists on
|
||||||
// disk. Recognized URL shapes:
|
// disk. Recognized URL shapes:
|
||||||
|
|
@ -195,14 +211,14 @@ func classifyDefaultSpec(rel string) []byte {
|
||||||
case "table.yaml":
|
case "table.yaml":
|
||||||
return embeddedDefaultProjectMdlTable
|
return embeddedDefaultProjectMdlTable
|
||||||
case "form.yaml":
|
case "form.yaml":
|
||||||
return embeddedDefaultMdlForm
|
return embeddedDefaultProjectMdlForm
|
||||||
}
|
}
|
||||||
case "rsk":
|
case "rsk":
|
||||||
switch file {
|
switch file {
|
||||||
case "table.yaml":
|
case "table.yaml":
|
||||||
return embeddedDefaultProjectRskTable
|
return embeddedDefaultProjectRskTable
|
||||||
case "form.yaml":
|
case "form.yaml":
|
||||||
return embeddedDefaultRskForm
|
return embeddedDefaultProjectRskForm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1511,7 +1511,7 @@ body.is-elevated::after {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
||||||
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-19 12:37:53 · 847e082-dirty</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-19 13:13:20 · cef7188-dirty</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,30 @@ func IsSSRCreateURL(urlPath string) (string, bool) {
|
||||||
return project, true
|
return project, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsRollupCreateURL reports whether urlPath is
|
||||||
|
// /<project>/(mdl|rsk)/form.html — the "+ Add row" target on a
|
||||||
|
// project-level MDL or RSK rollup view. Returns the project name +
|
||||||
|
// slot ("mdl" or "rsk") when matched. The rollup-create handler
|
||||||
|
// reads a `party` field from the body and routes the new row into
|
||||||
|
// <project>/archive/<party>/<slot>/.
|
||||||
|
func IsRollupCreateURL(urlPath string) (project, slot string, ok bool) {
|
||||||
|
if urlPath == "" || urlPath[0] != '/' {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
parts := strings.Split(strings.TrimPrefix(urlPath, "/"), "/")
|
||||||
|
if len(parts) != 3 || parts[2] != "form.html" {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
if parts[1] != "mdl" && parts[1] != "rsk" {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
project = parts[0]
|
||||||
|
if project == "" || strings.HasPrefix(project, ".") || strings.HasPrefix(project, "_") {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
return project, parts[1], true
|
||||||
|
}
|
||||||
|
|
||||||
// StripYAMLHTML returns urlPath with a trailing ".html" stripped iff
|
// StripYAMLHTML returns urlPath with a trailing ".html" stripped iff
|
||||||
// the URL has the form-edit shape ".../<name>.yaml.html". Otherwise
|
// the URL has the form-edit shape ".../<name>.yaml.html". Otherwise
|
||||||
// returns urlPath unchanged + false. The form recognizer calls this
|
// returns urlPath unchanged + false. The form recognizer calls this
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue