package beads import ( "strconv" "strings" ) // --- view model -------------------------------------------------------------- // BeadsData is the opaque .Data value handed to beads.html. Mode discriminates // the renderings: "board" (all lanes), "detail" (one issue), or "epic" (a // detail whose issue is an epic, which also carries its subtask rollup). type BeadsData struct { Mode string // "board" | "detail" | "epic" // board mode Lanes []BeadsLane Counts BeadsCounts Total int // issues placed on the board (after filtering) Truncated bool // an input table exceeded Max and was clipped ShownOf int // when Truncated: the reported table total Filter BeadsFilter // active board filters (sticky form state) FilterOpts BeadsFilterOptions // distinct values for the filter dropdowns // detail / epic modes Issue *BeadIssue DependsOn []BeadEdge // this issue depends on … (outgoing, direct) DependedOnBy []BeadEdge // … is depended on by this issue (incoming, direct) Comments []BeadComment // the comment thread (Comments tab) History []BeadActivity // comments + audit events, time-sorted (History tab) // Transitive dependency trees (flattened, pre-order with Depth), shown only // when they reach past the direct edges. DependsTree is the full prerequisite // chain; DependentTree is everything this issue transitively unblocks. DependsTree []BeadTreeNode DependentTree []BeadTreeNode // epic mode: the issue's parent-child children and their rollup. Subtasks []BeadSubtask SubtaskDone int // # of subtasks in the closed category SubtaskTotal int // len(Subtasks); the progress denominator } // BeadsFilter holds the active board filters, parsed from the query string and // echoed back into the form so selections stick across submits. Empty fields // mean "no constraint". type BeadsFilter struct { Query string // substring match over id + title (case-insensitive) Type string // exact issue_type Priority string // exact priority ("0".."3") Assignee string // exact assignee Label string // issue must carry this label Ready bool // only actionable-now issues (bd's `ready` set) } // Active reports whether any filter is set (drives the "Clear" link and the // empty-board wording). func (f BeadsFilter) Active() bool { return f.Query != "" || f.Type != "" || f.Priority != "" || f.Assignee != "" || f.Label != "" || f.Ready } // matches reports whether one issue row passes every set filter. func (f BeadsFilter) matches(id string, row []string, cols map[string]int, labels []string) bool { if f.Type != "" && cell(cols, row, "issue_type") != f.Type { return false } if f.Priority != "" && cell(cols, row, "priority") != f.Priority { return false } if f.Assignee != "" && cell(cols, row, "assignee") != f.Assignee { return false } if f.Label != "" && !containsString(labels, f.Label) { return false } if f.Query != "" { hay := strings.ToLower(id + " " + cell(cols, row, "title")) if !strings.Contains(hay, strings.ToLower(f.Query)) { return false } } return true } // BeadsFilterOptions lists the distinct values present across all issues, so the // filter dropdowns offer only real choices. Collected from the unfiltered set so // the options don't shrink as a filter narrows the board. type BeadsFilterOptions struct { Types []string Priorities []string // "0".."3" Assignees []string Labels []string } // BeadsLane is one parade lane and the cards in it. type BeadsLane struct { Name string // human label, e.g. "Rolling" Slug string // css-safe identifier, e.g. "rolling" Accent string // hex accent color for the lane header/border Issues []BeadCard } // BeadsCounts is the marquee: per-lane totals plus the grand total. type BeadsCounts struct { Rolling int LinedUp int Stalled int PastStand int Total int } // BeadCard is one issue as it appears on the board. type BeadCard struct { ID string Title string Type string Priority string // as stored ("0".."3", ""); PriorityLabel derives the pill Assignee string Labels []string BlockedBy int // # of deps this issue has (things it waits on) Blocks int // # of deps pointing at this issue (things waiting on it) Ready bool // actionable now: open, unblocked, not deferred/template (bd's `ready` set) Category string // open | in_progress | closed (used by the milestones view) } // PriorityLabel renders the numeric priority as a P-pill label ("P0".."P3"), // or "" when unset/unparseable so the template can omit the marker. func (c BeadCard) PriorityLabel() string { if c.Priority == "" { return "" } if _, err := strconv.Atoi(c.Priority); err != nil { return "" } return "P" + c.Priority } // BeadEdge is one dependency edge to another issue, linked in the detail pane. type BeadEdge struct { IssueID string Title string Type string Status string Closed bool } // BeadTreeNode is one node in a flattened transitive dependency tree. Depth is // the indentation level (0 = a direct edge of the root issue); Type is the // dependency type of the edge that reached this node. type BeadTreeNode struct { ID string Title string Type string Status string Closed bool Depth int } // BeadComment is one row of the comments thread. type BeadComment struct { Author string Text string CreatedAt string } // BeadActivity is one entry in the merged history timeline: either a comment or // an audit event from the events table. Summary is a human-readable one-liner // ("changed status to in_progress"); Text carries the comment body or an event's // free-text note. Kind drives the icon/label in the template. type BeadActivity struct { Kind string // "comment" | "event" Event string // events only: the event_type (created/status_changed/updated/closed/…) Actor string Summary string Text string CreatedAt string } // BeadSubtask is one child of an epic — the "from" side of a parent-child // dependency that points at the epic. Category (open/in_progress/closed) drives // the status accent and feeds the epic's progress rollup. type BeadSubtask struct { ID string Title string Status string Category string Priority string Assignee string Blocked bool } // PriorityLabel renders a subtask's numeric priority as a P-pill ("P0".."P3"), // or "" when unset/unparseable. func (s BeadSubtask) PriorityLabel() string { if s.Priority == "" { return "" } if _, err := strconv.Atoi(s.Priority); err != nil { return "" } return "P" + s.Priority } // SubtaskPct is the epic's completion percentage (0..100), for the progress bar // width. Zero subtasks reads as 0%. func (d *BeadsData) SubtaskPct() int { if d.SubtaskTotal == 0 { return 0 } return d.SubtaskDone * 100 / d.SubtaskTotal } // BeadIssue is the full issue shown in the detail pane. The field set mirrors // the user-facing columns bd surfaces for an issue (see `bd show`): identity and // status, the four long-text bodies, effort/reference metadata, the full // timestamp trail, and the close reason recorded when an issue is resolved. type BeadIssue struct { ID string Title string Status string Lane string Accent string Priority string IssueType string Assignee string CreatedBy string Owner string EstimatedMinutes string ExternalRef string SpecID string Description string Design string AcceptanceCriteria string Notes string CreatedAt string StartedAt string UpdatedAt string ClosedAt string CloseReason string Labels []string }