TASK BACKLOG REFINEMENT ISSUE
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
33
pkg/web/components/close_issue_form.templ
Normal file
33
pkg/web/components/close_issue_form.templ
Normal 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>
|
||||
}
|
||||
79
pkg/web/components/close_issue_form_templ.go
Normal file
79
pkg/web/components/close_issue_form_templ.go
Normal 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
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user