TASK BACKLOG REFINEMENT ISSUE

This commit is contained in:
Ine Maria Nilssen Aanonsen
2026-03-06 12:52:13 +01:00
parent f371f9a8e9
commit 68054f7409
11 changed files with 377 additions and 103 deletions

View File

@@ -2,6 +2,7 @@ package tasks
import (
"context"
"strings"
"github.com/LazyBachelor/LazyPM/internal/models"
"github.com/LazyBachelor/LazyPM/internal/utils/check"
@@ -12,12 +13,12 @@ const backlogRefinementDescription = `You are tasked with backlog refinement.
The product backlog has become cluttered with old and unclear issues. You need to groom the backlog:
1. Review all issues in the backlog
2. Identify stale or obsolete issues (older items that are no longer relevant)
3. Update issue descriptions for clarity where needed
4. Close issues that are duplicates or no longer applicable
5. Reprioritize issues based on current business value
6. Ensure remaining issues are well-defined and actionable
1. Go to the backlog.
2. Find two issues that got the same name or describe the same problem.
3. Open one of these issues.
4. Select "Close issue"
5. Choose "Duplicate issue" as closing reason.
6. Save/close issue.
Focus on making the backlog a reliable source of upcoming work.`
@@ -47,60 +48,64 @@ func (t *BacklogRefinementTask) Questions(interfaceType InterfaceType) Questions
return BaseQuestions(interfaceType).With(
huh.NewGroup(
huh.NewSelect[int]().
Title("How many issues did you close or update during refinement?").
Title("How many duplicate issues did you close during refinement?").
Options(
huh.NewOption("1-2", 1),
huh.NewOption("3-4", 2),
huh.NewOption("5+", 3),
huh.NewOption("1", 1),
huh.NewOption("2", 2),
huh.NewOption("3+", 3),
),
),
)
}
func (t *BacklogRefinementTask) QuestionnaireKeys(_ InterfaceType) []string {
return []string{"task_completed", "task_difficulty"}
}
func (t *BacklogRefinementTask) Setup(ctx context.Context) error {
if err := ClearIssues(t.app); err != nil {
return err
}
refinementIssues := []*models.Issue{
NewIssueBuilder().
WithTitle("Old feature request: Fax integration").
WithDescription("Allow sending reports via fax. DEPRECATED - nobody uses fax anymore").
WithPriority(3).
WithStatus(models.StatusOpen).
WithIssueType(models.TypeTask).
Build(),
NewIssueBuilder().
WithTitle("User profile page").
WithDescription("Create page for users to view profile. DUPLICATE of user-management epic").
WithDescription("Create page for users to view and edit their profile").
WithPriority(2).
WithStatus(models.StatusOpen).
WithIssueType(models.TypeTask).
Build(),
NewIssueBuilder().
WithTitle("Mobile app redesign").
WithDescription("Redesign mobile interface with modern UI patterns. Still relevant, needs clarity").
WithTitle("User profile page").
WithDescription("Allow users to view their profile information").
WithPriority(2).
WithStatus(models.StatusOpen).
WithIssueType(models.TypeTask).
Build(),
NewIssueBuilder().
WithTitle("Fix login timeout").
WithDescription("Login sometimes times out after 30 seconds").
WithPriority(1).
WithStatus(models.StatusOpen).
WithIssueType(models.TypeTask).
WithIssueType(models.TypeBug).
Build(),
NewIssueBuilder().
WithTitle("Legacy data export tool").
WithDescription("Tool for exporting data in old format. OBSOLETE - format no longer supported").
WithPriority(3).
WithTitle("Fix login timeout").
WithDescription("Users report login requests timing out").
WithPriority(1).
WithStatus(models.StatusOpen).
WithIssueType(models.TypeTask).
WithIssueType(models.TypeBug).
Build(),
NewIssueBuilder().
WithTitle("API v1 documentation").
WithDescription("Document old API version. DEPRECATED - migrating to v2").
WithPriority(3).
WithTitle("Mobile app redesign").
WithDescription("Redesign mobile interface with modern UI patterns").
WithPriority(2).
WithStatus(models.StatusOpen).
WithIssueType(models.TypeTask).
Build(),
NewIssueBuilder().
WithTitle("Customer feedback system").
WithDescription("Build system for collecting user feedback. HIGH VALUE - prioritize").
WithDescription("Build system for collecting user feedback").
WithPriority(2).
WithStatus(models.StatusOpen).
WithIssueType(models.TypeTask).
@@ -122,5 +127,22 @@ func (t *BacklogRefinementTask) Setup(ctx context.Context) error {
func (t *BacklogRefinementTask) Validate(ctx context.Context) ValidationFeedback {
expect := check.NewExpector()
issues, err := FetchIssues(ctx, t.app, t.setupIssue)
if err != nil {
return expect.ValidationFeedback
}
var closedDuplicate *models.Issue
for _, issue := range issues {
if issue.Status == models.StatusClosed &&
strings.Contains(strings.ToLower(issue.CloseReason), "duplicate") {
closedDuplicate = issue
break
}
}
expect.Assert(closedDuplicate != nil,
"Expected one duplicate issue to be closed with 'Duplicate issue' as closing reason")
return expect.Complete()
}

View File

@@ -7,8 +7,6 @@ import (
"github.com/spf13/cobra"
)
// CloseCmd represents the close command,
// which allows users to close an existing issue by its ID.
var CloseCmd = &cobra.Command{
Use: "close [id]",
Short: "Close an existing issue",
@@ -21,8 +19,6 @@ var CloseCmd = &cobra.Command{
ValidArgsFunction: completeIssues,
}
// runCloseCmd executes the close command logic,
// which closes an issue by its ID after confirming with the user.
func runCloseCmd(cmd *cobra.Command, args []string) error {
closeID := args[0]
@@ -42,14 +38,27 @@ func runCloseCmd(cmd *cobra.Command, args []string) error {
return fmt.Errorf("issue with ID %s not found", closeID)
}
// Ask for closing reason
if err = huh.NewInput().Value(&issue.CloseReason).
Title("Reason for closing the issue?").WithTheme(huh.ThemeBase()).Run(); err != nil {
closeReason := ""
if err = huh.NewSelect[string]().Value(&closeReason).
Title("Reason for closing the issue?").
Options(
huh.NewOption("Done", "Done"),
huh.NewOption("Duplicate issue", "Duplicate issue"),
huh.NewOption("Won't fix", "Won't fix"),
huh.NewOption("Obsolete", "Obsolete"),
huh.NewOption("Other", "Other"),
).WithTheme(huh.ThemeBase()).Run(); err != nil {
return fmt.Errorf("error getting close reason: %w", err)
}
// Close the issue.
err = app.Issues.CloseIssue(cmd.Context(), closeID, issue.CloseReason, "", "")
if closeReason == "Other" {
if err = huh.NewInput().Value(&closeReason).
Title("Enter closing reason:").WithTheme(huh.ThemeBase()).Run(); err != nil {
return fmt.Errorf("error getting close reason: %w", err)
}
}
err = app.Issues.CloseIssue(cmd.Context(), closeID, closeReason, "", "")
if err != nil {
return fmt.Errorf("error closing issue: %w", err)
}

View File

@@ -0,0 +1,33 @@
package components
import "github.com/LazyBachelor/LazyPM/pkg/web/components/base"
type CloseIssueFormProps struct {
PostAction string
}
templ CloseIssueForm(props CloseIssueFormProps) {
<form
class="form space-y-4"
hx-post={ props.PostAction }
hx-target="body"
hx-swap="none"
>
@base.Select(base.SelectProps{
Name: "close_reason",
Label: "Reason for closing the issue",
Required: true,
Options: []base.SelectOption{
{Label: "Done", Value: "Done"},
{Label: "Duplicate issue", Value: "Duplicate issue"},
{Label: "Won't fix", Value: "Won't fix"},
{Label: "Obsolete", Value: "Obsolete"},
{Label: "Other", Value: "Other"},
},
Size: "md",
})
<button class="btn btn-primary w-full" type="submit">
Close issue
</button>
</form>
}

View File

@@ -0,0 +1,79 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.977
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "github.com/LazyBachelor/LazyPM/pkg/web/components/base"
type CloseIssueFormProps struct {
PostAction string
}
func CloseIssueForm(props CloseIssueFormProps) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<form class=\"form space-y-4\" hx-post=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(props.PostAction)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/close_issue_form.templ`, Line: 12, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" hx-target=\"body\" hx-swap=\"none\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = base.Select(base.SelectProps{
Name: "close_reason",
Label: "Reason for closing the issue",
Required: true,
Options: []base.SelectOption{
{Label: "Done", Value: "Done"},
{Label: "Duplicate issue", Value: "Duplicate issue"},
{Label: "Won't fix", Value: "Won't fix"},
{Label: "Obsolete", Value: "Obsolete"},
{Label: "Other", Value: "Other"},
},
Size: "md",
}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<button class=\"btn btn-primary w-full\" type=\"submit\">Close issue</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View File

@@ -213,6 +213,41 @@ func DeleteIssue(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
func CloseIssue(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
issue := r.Context().Value(issueKey).(*models.Issue)
closeReason := r.FormValue("close_reason")
if closeReason == "" {
if HTMX(r).IsHxRequest() {
HTMX(r).WriteString("<div class=\"alert alert-error\">Closing reason is required</div>")
} else {
http.Error(w, "Closing reason is required", http.StatusBadRequest)
}
return
}
if err := App(r).Issues.CloseIssue(r.Context(), issue.ID, closeReason, "web", ""); err != nil {
if HTMX(r).IsHxRequest() {
HTMX(r).WriteString("<div class=\"alert alert-error\">Failed to close issue: " + err.Error() + "</div>")
} else {
http.Error(w, "Failed to close issue: "+err.Error(), http.StatusInternalServerError)
}
return
}
if HTMX(r).IsHxRequest() {
w.Header().Set("HX-Refresh", "true")
return
}
w.WriteHeader(http.StatusNoContent)
}
func (f *IssueForm) toIssue() *models.Issue {
return &models.Issue{
Title: f.Title,

View File

@@ -72,3 +72,29 @@ func AssigneeFormModal(w http.ResponseWriter, r *http.Request) {
})
modal.Render(r.Context(), w)
}
func CloseIssueFormModal(w http.ResponseWriter, r *http.Request) {
issue := r.Context().Value(issueKey).(*models.Issue)
if issue == nil {
http.Error(w, "Issue not found in context", http.StatusInternalServerError)
return
}
if issue.Status == models.StatusClosed {
http.Error(w, "Issue is already closed", http.StatusBadRequest)
return
}
modalContent := components.CloseIssueForm(components.CloseIssueFormProps{
PostAction: "/issues/" + issue.ID + "/close",
})
modal := components.Modal(components.ModalProps{
ID: "close-issue-modal",
Title: "Close Issue",
Content: modalContent,
Open: true,
})
modal.Render(r.Context(), w)
}

View File

@@ -55,6 +55,17 @@ templ DashboardContent(props DashboardProps) {
>
Edit
</button>
if props.SelectedIssue.Status != "closed" {
<button
type="button"
class="btn btn-warning btn-sm btn-outline"
hx-get={ "/issues/" + props.SelectedIssue.ID + "/close" }
hx-target="#modal-container"
hx-swap="innerHTML"
>
Close issue
</button>
}
<button
type="button"
class="btn btn-error btn-sm btn-outline"

View File

@@ -128,33 +128,56 @@ func DashboardContent(props DashboardProps) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\">Edit</button> <button type=\"button\" class=\"btn btn-error btn-sm btn-outline\" hx-delete=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\">Edit</button> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs("/issues/" + props.SelectedIssue.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 61, Col: 52}
if props.SelectedIssue.Status != "closed" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<button type=\"button\" class=\"btn btn-warning btn-sm btn-outline\" hx-get=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs("/issues/" + props.SelectedIssue.ID + "/close")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 62, Col: 61}
}
_, 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, 7, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\">Close issue</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, 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, 6, "\" hx-confirm=\"Are you sure you want to delete this issue? This action cannot be undone.\" hx-target=\"body\">Delete issue</button> <button class=\"btn btn-sm fixed right-3\" hx-get=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " <button type=\"button\" class=\"btn btn-error btn-sm btn-outline\" hx-delete=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("/issues/" + props.SelectedIssue.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 69, Col: 49}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 72, Col: 52}
}
_, 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, 7, "\" hx-target=\"main\" hx-swap=\"innerHTML\" hx-push-url=\"true\">Details</button><div id=\"issue-detail-container\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" hx-confirm=\"Are you sure you want to delete this issue? This action cannot be undone.\" hx-target=\"body\">Delete issue</button> <button class=\"btn btn-sm fixed right-3\" hx-get=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs("/issues/" + props.SelectedIssue.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 80, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" hx-target=\"main\" hx-swap=\"innerHTML\" hx-push-url=\"true\">Details</button><div id=\"issue-detail-container\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -164,12 +187,12 @@ func DashboardContent(props DashboardProps) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -193,12 +216,12 @@ func DashboardIssueList(issues []*models.Issue, selectedID string) templ.Compone
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
if templ_7745c5c3_Var7 == nil {
templ_7745c5c3_Var7 = templ.NopComponent
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
if templ_7745c5c3_Var8 == nil {
templ_7745c5c3_Var8 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<section class=\"flex\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<section class=\"flex\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -218,7 +241,7 @@ func DashboardIssueList(issues []*models.Issue, selectedID string) templ.Compone
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</section>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</section>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -242,13 +265,13 @@ func DashboardIssueRows(issues []*models.Issue, selectedID string) templ.Compone
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
if templ_7745c5c3_Var8 == nil {
templ_7745c5c3_Var8 = templ.NopComponent
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
if templ_7745c5c3_Var9 == nil {
templ_7745c5c3_Var9 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
if len(issues) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<tr><td colspan=\"5\" class=\"text-center py-4\">No issues</td></tr>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<tr><td colspan=\"5\" class=\"text-center py-4\">No issues</td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -258,64 +281,64 @@ func DashboardIssueRows(issues []*models.Issue, selectedID string) templ.Compone
if selectedID == issue.ID {
active = "bg-primary/10"
}
var templ_7745c5c3_Var9 = []any{"hover cursor-pointer min-h-full w-full", active}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...)
var templ_7745c5c3_Var10 = []any{"hover cursor-pointer min-h-full w-full", active}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var10...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<tr hx-get=\"/\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var9).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" hx-target=\"main\" hx-vals=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<tr hx-get=\"/\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(`{"selected-issue": "` + issue.ID + `"}`)
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var10).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 117, Col: 53}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 1, Col: 0}
}
_, 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, 15, "\" hx-swap=\"innerHTML\"><td class=\"min-w-20 truncate\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" hx-target=\"main\" hx-vals=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(issue.ID)
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(`{"selected-issue": "` + issue.ID + `"}`)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 120, Col: 43}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 128, Col: 53}
}
_, 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, 16, "</td><td class=\"truncate max-w-60\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" hx-swap=\"innerHTML\"><td class=\"min-w-20 truncate\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title)
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(issue.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 121, Col: 46}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 131, Col: 43}
}
_, 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, 17, "</td><td>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</td><td class=\"truncate max-w-60\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 132, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -323,7 +346,7 @@ func DashboardIssueRows(issues []*models.Issue, selectedID string) templ.Compone
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</td><td>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -331,7 +354,7 @@ func DashboardIssueRows(issues []*models.Issue, selectedID string) templ.Compone
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</td><td>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -339,7 +362,7 @@ func DashboardIssueRows(issues []*models.Issue, selectedID string) templ.Compone
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</td></tr>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View File

@@ -32,6 +32,17 @@ templ IssueDetailContent(props IssueDetailProps) {
>
Edit
</button>
if props.Issue.Status != "closed" {
<button
type="button"
class="btn btn-warning btn-sm btn-outline"
hx-get={ "/issues/" + props.Issue.ID + "/close" }
hx-target="#modal-container"
hx-swap="innerHTML"
>
Close issue
</button>
}
<button
type="button"
class="btn btn-error btn-sm btn-outline"

View File

@@ -60,20 +60,43 @@ func IssueDetailContent(props IssueDetailProps) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\">Edit</button> <button type=\"button\" class=\"btn btn-error btn-sm btn-outline\" hx-delete=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\">Edit</button> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs("/issues/" + props.Issue.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 38, Col: 43}
if props.Issue.Status != "closed" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<button type=\"button\" class=\"btn btn-warning btn-sm btn-outline\" hx-get=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs("/issues/" + props.Issue.ID + "/close")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 39, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\">Close issue</button> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<button type=\"button\" class=\"btn btn-error btn-sm btn-outline\" hx-delete=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" hx-confirm=\"Are you sure you want to delete this issue? This action cannot be undone.\" hx-target=\"body\">Delete issue</button></div><div id=\"issue-detail-container\" class=\"mb-6\">")
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("/issues/" + props.Issue.ID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/issue_detail.templ`, Line: 49, Col: 43}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" hx-confirm=\"Are you sure you want to delete this issue? This action cannot be undone.\" hx-target=\"body\">Delete issue</button></div><div id=\"issue-detail-container\" class=\"mb-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -81,7 +104,7 @@ func IssueDetailContent(props IssueDetailProps) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -92,7 +115,7 @@ func IssueDetailContent(props IssueDetailProps) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -116,12 +139,12 @@ func IssueDetail(props IssueDetailProps) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil {
templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var5 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_Var6 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
@@ -139,7 +162,7 @@ func IssueDetail(props IssueDetailProps) templ.Component {
}
return nil
})
templ_7745c5c3_Err = BaseLayout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var5), templ_7745c5c3_Buffer)
templ_7745c5c3_Err = BaseLayout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var6), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View File

@@ -47,6 +47,8 @@ func (s *Server) RegisterRoutes(assets embed.FS) http.Handler {
r.Get("/", handler.GetIssue)
r.Patch("/", handler.UpdateIssue)
r.Get("/edit", handler.EditIssueFormModal)
r.Get("/close", handler.CloseIssueFormModal)
r.Post("/close", handler.CloseIssue)
r.Get("/assignee", handler.AssigneeFormModal)
r.Patch("/assignee", handler.UpdateAssignee)
r.Delete("/", handler.DeleteIssue)