diff --git a/internal/models/app.go b/internal/models/app.go index e1e5603..014918a 100644 --- a/internal/models/app.go +++ b/internal/models/app.go @@ -53,6 +53,12 @@ type IssueService interface { AddIssueComment(ctx context.Context, issueID, author, text string) (*Comment, error) GetIssueComments(ctx context.Context, issueID string) ([]*Comment, error) GetCommentCounts(ctx context.Context, issueIDs []string) (map[string]int, error) + + // Dependency management (thin wrappers over beads dependency API) + AddDependency(ctx context.Context, issueID, dependsOnID string, depType DependencyType, actor string) error + RemoveDependency(ctx context.Context, issueID, dependsOnID string, actor string) error + GetDependencies(ctx context.Context, issueID string) ([]*Issue, error) + GetDependents(ctx context.Context, issueID string) ([]*Issue, error) } type StatsService interface { diff --git a/internal/storage/beads.go b/internal/storage/beads.go index 294da1f..99b9a34 100644 --- a/internal/storage/beads.go +++ b/internal/storage/beads.go @@ -52,3 +52,65 @@ func (s *BeadsService) DeleteIssues() error { } return nil } + + +///////////////////////////////////////////////////////////////////////////////////// +// Dependency Management API wrappers over the underlying beads storage API +///////////////////////////////////////////////////////////////////////////////////// +// AddDependency creates a dependency edge between two issues. +// It is a thin wrapper over the underlying beads storage AddDependency API. +func (s *BeadsService) AddDependency(ctx context.Context, issueID, dependsOnID string, depType models.DependencyType, actor string) error { + dep := &beads.Dependency{ + IssueID: issueID, + DependsOnID: dependsOnID, + Type: beads.DependencyType(depType), + } + return s.Storage.AddDependency(ctx, dep, actor) +} + +// RemoveDependency removes a dependency edge between two issues. +func (s *BeadsService) RemoveDependency(ctx context.Context, issueID, dependsOnID string, actor string) error { + return s.Storage.RemoveDependency(ctx, issueID, dependsOnID, actor) +} + +// GetDependencies returns issues that the given issue depends on. +func (s *BeadsService) GetDependencies(ctx context.Context, issueID string) ([]*models.Issue, error) { + issues, err := s.Storage.GetDependencies(ctx, issueID) + if err != nil { + return nil, err + } + if len(issues) == 0 { + return []*models.Issue{}, nil + } + // types.Issue is layout-compatible with models.Issue (alias to beads.Issue), + // so we can return the slice directly as []*models.Issue. + result := make([]*models.Issue, 0, len(issues)) + for _, iss := range issues { + if iss == nil { + continue + } + casted := models.Issue(*iss) + result = append(result, &casted) + } + return result, nil +} + +// GetDependents returns issues that depend on the given issue. +func (s *BeadsService) GetDependents(ctx context.Context, issueID string) ([]*models.Issue, error) { + issues, err := s.Storage.GetDependents(ctx, issueID) + if err != nil { + return nil, err + } + if len(issues) == 0 { + return []*models.Issue{}, nil + } + result := make([]*models.Issue, 0, len(issues)) + for _, iss := range issues { + if iss == nil { + continue + } + casted := models.Issue(*iss) + result = append(result, &casted) + } + return result, nil +} diff --git a/pkg/web/components/issue.templ b/pkg/web/components/issue.templ index 714f5c9..bf22e4f 100644 --- a/pkg/web/components/issue.templ +++ b/pkg/web/components/issue.templ @@ -10,6 +10,7 @@ type IssueFormProps struct { PostAction string PatchAction string DeleteAction string // Optional: URL for delete confirmation modal + IssueID string // Optional: when editing an existing issue Title string Description string Status string @@ -98,6 +99,44 @@ templ IssueForm(props IssueFormProps) { Value: props.Priority, Step: 1, }) + if props.IssueID != "" { +
+

Dependencies

+
+
+ + +
+ +
+
+
+ } diff --git a/pkg/web/components/issue_detail.templ b/pkg/web/components/issue_detail.templ index 04f101a..6fef8d8 100644 --- a/pkg/web/components/issue_detail.templ +++ b/pkg/web/components/issue_detail.templ @@ -25,6 +25,14 @@ templ IssueDetail(props IssueDetailProps) {

{ props.Issue.Description }

} +
+

Dependencies

+
+
diff --git a/pkg/web/components/issue_detail_templ.go b/pkg/web/components/issue_detail_templ.go index c405ee5..462f0ec 100644 --- a/pkg/web/components/issue_detail_templ.go +++ b/pkg/web/components/issue_detail_templ.go @@ -91,87 +91,100 @@ func IssueDetail(props IssueDetailProps) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
Created

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

Dependencies

Updated

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" hx-trigger=\"load\">

Created

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var5 string - templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(props.Issue.UpdatedAt.Format("Jan 2, 2006")) + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(props.Issue.CreatedAt.Format("Jan 2, 2006")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 36, Col: 51} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 40, Col: 51} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

Created by

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "

Updated

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var6 string - templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(props.Issue.CreatedBy) + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(props.Issue.UpdatedAt.Format("Jan 2, 2006")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 40, Col: 56} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 44, Col: 51} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if props.Issue.Assignee == "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "Unassigned (click to assign) ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "Unassigned (click to assign) ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "Assignee: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "Assignee: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var8 string - templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(props.Issue.Assignee) + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(props.Issue.Assignee) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 51, Col: 42} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 59, Col: 42} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -195,57 +208,57 @@ func StatusBadge(status models.Status) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var9 := templ.GetChildren(ctx) - if templ_7745c5c3_Var9 == nil { - templ_7745c5c3_Var9 = templ.NopComponent + templ_7745c5c3_Var10 := templ.GetChildren(ctx) + if templ_7745c5c3_Var10 == nil { + templ_7745c5c3_Var10 = templ.NopComponent } ctx = templ.ClearChildren(ctx) switch status { case "open": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "Open") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "Open") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "in_progress": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "In Progress") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "In Progress") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "ready_to_sprint": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "Ready to sprint") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "Ready to sprint") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "closed": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "Closed") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "Closed") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "blocked": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "Blocked") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "Blocked") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "deferred": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "Deferred") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "Deferred") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } default: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var10 string - templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(string(status)) + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(string(status)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 72, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 80, Col: 60} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -270,52 +283,52 @@ func TypeBadge(issueType models.IssueType) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var11 := templ.GetChildren(ctx) - if templ_7745c5c3_Var11 == nil { - templ_7745c5c3_Var11 = templ.NopComponent + templ_7745c5c3_Var12 := templ.GetChildren(ctx) + if templ_7745c5c3_Var12 == nil { + templ_7745c5c3_Var12 = templ.NopComponent } ctx = templ.ClearChildren(ctx) switch issueType { case "bug": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "Bug") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Bug") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "feature": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Feature") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "Feature") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "task": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "Task") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "Task") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "chore": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "Chore") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "Chore") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "epic": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "Epic") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "Epic") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } default: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(string(issueType)) + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(string(issueType)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 89, Col: 65} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 97, Col: 65} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -340,52 +353,52 @@ func PriorityBadge(priority int) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var13 := templ.GetChildren(ctx) - if templ_7745c5c3_Var13 == nil { - templ_7745c5c3_Var13 = templ.NopComponent + templ_7745c5c3_Var14 := templ.GetChildren(ctx) + if templ_7745c5c3_Var14 == nil { + templ_7745c5c3_Var14 = templ.NopComponent } ctx = templ.ClearChildren(ctx) switch priority { case 0: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Irrelevant") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "Irrelevant") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case 1: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "Low") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "Low") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case 2: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "Normal") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "Normal") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case 3: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "High") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "High") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case 4: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "Critical") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "Critical") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } default: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var14 string - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("P%d", priority)) + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("P%d", priority)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 106, Col: 74} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 114, Col: 74} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pkg/web/components/issue_templ.go b/pkg/web/components/issue_templ.go index bf4c931..9d66552 100644 --- a/pkg/web/components/issue_templ.go +++ b/pkg/web/components/issue_templ.go @@ -18,6 +18,7 @@ type IssueFormProps struct { PostAction string PatchAction string DeleteAction string // Optional: URL for delete confirmation modal + IssueID string // Optional: when editing an existing issue Title string Description string Status string @@ -75,7 +76,7 @@ func IssueForm(props IssueFormProps) templ.Component { var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(props.Status) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 25, Col: 122} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 26, Col: 122} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { @@ -93,7 +94,7 @@ func IssueForm(props IssueFormProps) templ.Component { var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(props.PatchAction) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 29, Col: 32} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 30, Col: 32} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -112,7 +113,7 @@ func IssueForm(props IssueFormProps) templ.Component { var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(props.PostAction) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 32, Col: 30} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 33, Col: 30} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -131,7 +132,7 @@ func IssueForm(props IssueFormProps) templ.Component { var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(props.Target) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 35, Col: 28} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 36, Col: 28} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -238,36 +239,81 @@ func IssueForm(props IssueFormProps) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if props.DeleteAction != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if props.DeleteAction != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if props.Target == "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -295,9 +341,9 @@ func IssueTable(props IssueTableProps) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var9 := templ.GetChildren(ctx) - if templ_7745c5c3_Var9 == nil { - templ_7745c5c3_Var9 = templ.NopComponent + templ_7745c5c3_Var12 := templ.GetChildren(ctx) + if templ_7745c5c3_Var12 == nil { + templ_7745c5c3_Var12 = templ.NopComponent } ctx = templ.ClearChildren(ctx) templ_7745c5c3_Err = base.Table( @@ -337,65 +383,65 @@ func IssueRows(issues []*models.Issue) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var10 := templ.GetChildren(ctx) - if templ_7745c5c3_Var10 == nil { - templ_7745c5c3_Var10 = templ.NopComponent + templ_7745c5c3_Var13 := templ.GetChildren(ctx) + if templ_7745c5c3_Var13 == nil { + templ_7745c5c3_Var13 = templ.NopComponent } ctx = templ.ClearChildren(ctx) for _, issue := range issues { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(issue.ID) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 153, Col: 15} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" hx-target=\"main\" hx-swap=\"innerHTML\" hx-push-url=\"true\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(issue.ID) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 192, Col: 15} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 201, Col: 18} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -403,46 +449,46 @@ func IssueRows(issues []*models.Issue) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var15 string - templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(string(issue.IssueType)) + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(string(issue.IssueType)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 167, Col: 57} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 206, Col: 57} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("P%d", issue.Priority)) + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("P%d", issue.Priority)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 168, Col: 68} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 207, Col: 68} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\" hx-confirm=\"Are you sure you want to delete this issue? This action cannot be undone.\" hx-target=\"body\" title=\"Delete issue\">Delete") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pkg/web/handler/issues.go b/pkg/web/handler/issues.go index 3f48bc1..84b3199 100644 --- a/pkg/web/handler/issues.go +++ b/pkg/web/handler/issues.go @@ -3,6 +3,7 @@ package handler import ( "context" "html" + "net/url" "net/http" "strings" @@ -226,6 +227,178 @@ func UpdateAssignee(w http.ResponseWriter, r *http.Request) { HTMX(r).WriteJSON(issue) } +// Returns a simple HTML fragment listing dependencies for an issue +func ListDependencies(w http.ResponseWriter, r *http.Request) { + issue := r.Context().Value(issueKey).(*models.Issue) + app := App(r) + + deps, err := app.Issues.GetDependencies(r.Context(), issue.ID) + if err != nil { + http.Error(w, "Failed to load dependencies", http.StatusInternalServerError) + return + } + + containerID := r.URL.Query().Get("container_id") + if containerID == "" { + containerID = "dependencies-list" + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + + if len(deps) == 0 { + _, _ = w.Write([]byte(`
No dependencies.
`)) + return + } + + builder := strings.Builder{} + builder.WriteString(`
`) + for _, d := range deps { + if d == nil { + continue + } + builder.WriteString(`
`) + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(html.EscapeString(d.ID)) + builder.WriteString(``) + if d.Title != "" { + builder.WriteString(` — `) + builder.WriteString(html.EscapeString(d.Title)) + builder.WriteString(``) + } + builder.WriteString(``) + builder.WriteString(``) + builder.WriteString(`
`) + } + builder.WriteString(`
`) + + _, _ = w.Write([]byte(builder.String())) +} + +// Adding a new dependency (issue depends on depends_on_id) +func AddDependencyHandler(w http.ResponseWriter, r *http.Request) { + issue := r.Context().Value(issueKey).(*models.Issue) + app := App(r) + + if err := r.ParseForm(); err != nil { + http.Error(w, "Invalid form data", http.StatusBadRequest) + return + } + + dependsOnID := r.FormValue("depends_on_id") + if dependsOnID == "" { + http.Error(w, "Dependency ID is required", http.StatusBadRequest) + return + } + + depType := models.DepBlocks + + if err := app.Issues.AddDependency(r.Context(), issue.ID, dependsOnID, depType, "web"); err != nil { + http.Error(w, "Failed to add dependency: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("HX-Trigger", "dependencies-updated") + + // Re-render the list so HTMX can swap it in + ListDependencies(w, r) +} + +// Removing an existing dependency +func RemoveDependencyHandler(w http.ResponseWriter, r *http.Request) { + issue := r.Context().Value(issueKey).(*models.Issue) + app := App(r) + + dependsOnID := r.FormValue("depends_on_id") + if dependsOnID == "" { + dependsOnID = r.URL.Query().Get("depends_on_id") + } + if dependsOnID == "" { + http.Error(w, "Dependency ID is required", http.StatusBadRequest) + return + } + + if err := app.Issues.RemoveDependency(r.Context(), issue.ID, dependsOnID, "web"); err != nil { + http.Error(w, "Failed to remove dependency: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("HX-Trigger", "dependencies-updated") + + // Re-render the list so HTMX can swap it in + ListDependencies(w, r) +} + +// DependencyOptions renders `) + for _, iss := range all { + if iss == nil { + continue + } + if _, skip := exclude[iss.ID]; skip { + continue + } + // Show "ID — Title" for quick scanning + b.WriteString(``) + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _, _ = w.Write([]byte(b.String())) +} + func DeleteIssue(w http.ResponseWriter, r *http.Request) { issue := r.Context().Value(issueKey).(*models.Issue) diff --git a/pkg/web/handler/modals.go b/pkg/web/handler/modals.go index b9bb2fa..64b91b8 100644 --- a/pkg/web/handler/modals.go +++ b/pkg/web/handler/modals.go @@ -58,6 +58,7 @@ func EditIssueFormModal(w http.ResponseWriter, r *http.Request) { modalContent := components.IssueForm(components.IssueFormProps{ PatchAction: patchAction, DeleteAction: deleteAction, + IssueID: issue.ID, Title: issue.Title, Description: issue.Description, Status: string(issue.Status), diff --git a/pkg/web/server/routes.go b/pkg/web/server/routes.go index 2c4b916..a71fd97 100644 --- a/pkg/web/server/routes.go +++ b/pkg/web/server/routes.go @@ -47,6 +47,11 @@ func (s *Server) RegisterRoutes(assets embed.FS) http.Handler { r.Get("/", handler.GetIssue) r.Patch("/", handler.UpdateIssue) r.Get("/edit", handler.EditIssueFormModal) + // Dependency management endpoints + r.Get("/dependencies", handler.ListDependencies) + r.Get("/dependencies/options", handler.DependencyOptions) + r.Post("/dependencies", handler.AddDependencyHandler) + r.Delete("/dependencies", handler.RemoveDependencyHandler) r.Get("/close", handler.CloseIssueFormModal) r.Post("/close", handler.CloseIssue) r.Get("/delete", handler.DeleteIssueConfirmModal)