diff --git a/cmd/pm/tasks/backlogRefinement.go b/cmd/pm/tasks/backlogRefinement.go
index be536b5..f1ed46b 100644
--- a/cmd/pm/tasks/backlogRefinement.go
+++ b/cmd/pm/tasks/backlogRefinement.go
@@ -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,65 @@ 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?").
+ Key("how-many-duplicate-issues").
+ 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", "how-many-duplicate-issues"}
+}
+
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 +128,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()
}
diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go
index 5df0ec3..754f94c 100644
--- a/cmd/pm/tasks/codingTask.go
+++ b/cmd/pm/tasks/codingTask.go
@@ -2,7 +2,6 @@ package tasks
import (
"context"
- "fmt"
"os"
"strings"
@@ -18,15 +17,14 @@ The MongoDB Driver dependency in the file is outdated and needs to be updated to
This is a common task for developers, and it requires attention to detail and the ability to follow instructions carefully.
Your task:
-1. Assign this Issue to yourself as "Me" and mark it as "In Progress"
-2. Create a New Issue and give it these details:
+1. Create a New Issue and give it these details:
- Title: "Upgrade MongoDB Driver Dependency"
- Description: "We need to upgrade the MongoDB Driver dependency to the latest version."
- Status: "In Progress"
- Issue Type: "Chore"
-3. A file will appear in the current directory named "code.txt".
+2. A file will appear in the current directory named "code.txt".
Open it and follow the instructions inside. And save the file after you are done.
-4. When you are done, mark this and the issue you made as "Closed".`
+3. When you are done, mark this and the issue you made as "Closed".`
var textFileDescription = `
Please upgrade the MongoDB Driver dependency in the go.mod file to the latest version.
@@ -122,7 +120,7 @@ func (t *CodingTask) Setup(ctx context.Context) error {
return nil
}
-var codeTaskInProgress = false
+var codingTaskInProgress = false
func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback {
expect := check.NewExpector()
@@ -132,47 +130,31 @@ func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback {
return expect.ValidationFeedback
}
- expect.Assert(t.setupIssue.Assignee == "Me",
- "The original issue should be assigned to 'Me'.")
-
- expect.Assert(t.setupIssue.Status == models.StatusInProgress,
- "The original issue should be marked as In Progress before starting work.")
-
- expect.Assert(len(issues) > 0,
- "No new issues created. Please create an issue with the specified details.")
-
if len(issues) == 0 {
expect.Fail("No new issues created")
return expect.ValidationFeedback
+ } else {
+ expect.Pass("An issue was created")
}
issue := issues[0]
- expect.Assert(len(issues) < 2,
- "Multiple issues were created instead of one. Delete the extra issues and try again.")
+ expect.Assert(len(issues) < 2, "Multiple issues were created instead of one")
- expect.NotEmptyString(issue.Title,
- "Issue title should not be empty")
+ expect.NotEmptyAndEqual(issue.Title, "Upgrade MongoDB Driver Dependency", "Issue title")
- expect.Assert(issue.Title == "Upgrade MongoDB Driver Dependency",
- fmt.Sprintf("Issue title does not match the expected value 'Upgrade MongoDB Driver Dependency', but was '%s'", issue.Title))
+ expect.NotEmptyAndEqual(issue.Description,
+ "We need to upgrade the MongoDB Driver dependency to the latest version.", "Issue description")
- expect.NotEmptyString(issue.Description,
- "Issue description should not be empty")
+ expect.NotEmptyAndEqual(issue.Assignee, "Me", "Issue Assignee")
- expect.Assert(issue.Description == "We need to upgrade the MongoDB Driver dependency to the latest version.",
- fmt.Sprintf("Issue description does not match 'We need to upgrade the MongoDB Driver dependency to the latest version.', but was '%s'", issue.Description))
+ expect.Equal(issue.IssueType, models.TypeChore, "Issue type")
- expect.Assert(issue.IssueType == models.TypeChore,
- fmt.Sprintf("Issue type should be 'Chore', but was '%s'", issue.IssueType))
-
- expect.Assert(issue.Assignee == "Me",
- fmt.Sprintf("Issue should be assigned to 'Me', but was assigned to '%s'", issue.Assignee))
-
- if issue.Status == models.StatusInProgress || codeTaskInProgress {
- codeTaskInProgress = true
+ if issue.Status == models.StatusInProgress || codingTaskInProgress {
+ codingTaskInProgress = true
} else {
- expect.Fail("Issue should be marked as In Progress when work starts")
+ expect.Fail("The issue should be marked as In Progress while working on the task.")
+ return expect.ValidationFeedback
}
if _, err := os.Stat("./code.txt"); os.IsNotExist(err) {
@@ -195,14 +177,8 @@ func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback {
expect.Assert(strings.Contains(code, "go.mongodb.org/mongo-driver v1.17.9"),
"The MongoDB Driver dependency should be updated to version v1.17.9 in the file.")
- if !codeTaskInProgress {
- return expect.ValidationFeedback
- } else if issue.Status != models.StatusClosed {
- expect.Fail("Issue should be set to Closed once the work is completed")
- } else {
- expect.Assert(t.setupIssue.Status == models.StatusClosed,
- "The original setup issue should be set to Closed once the work is completed")
- }
+ expect.Assert(codingTaskInProgress && issue.Status == models.StatusClosed,
+ "The issue should be marked as Closed after completing the task.")
return expect.Complete()
}
diff --git a/cmd/pm/tasks/createIssue.go b/cmd/pm/tasks/createIssue.go
index 4dde38b..a600c47 100644
--- a/cmd/pm/tasks/createIssue.go
+++ b/cmd/pm/tasks/createIssue.go
@@ -16,8 +16,7 @@ Your task:
1. Create a new issue with the title "My first Issue"
2. Add this detailed description "I need to do some coding"
3. Assign the issue to yourself as "Me"
-4. Mark the issue as In Progress when you start working on it
-5. Close the issue once you've completed the work
+4. Mark the issue as In Progress when you are done.
Make sure to fill out all the necessary details to help others understand the work item.`
@@ -43,8 +42,7 @@ func (t *CreateIssueTask) Questions(interfaceType InterfaceType) Questions {
return BaseQuestions(interfaceType)
}
-func (t *CreateIssueTask) QuestionnaireKeys(interfaceType InterfaceType) []string {
- _ = interfaceType
+func (t *CreateIssueTask) QuestionnaireKeys(_ InterfaceType) []string {
return []string{"task_completed", "task_difficulty"}
}
@@ -61,8 +59,6 @@ func (t *CreateIssueTask) Setup(ctx context.Context) error {
return t.app.Issues.CreateIssue(ctx, t.setupIssue, "")
}
-var isInProgress = false
-
func (t *CreateIssueTask) Validate(ctx context.Context) ValidationFeedback {
expect := check.NewExpector()
@@ -78,30 +74,14 @@ func (t *CreateIssueTask) Validate(ctx context.Context) ValidationFeedback {
issue := issues[0]
- expect.Assert(len(issues) < 2, "Multiple issues were created instead of one. Delete the extra issues and try again.")
+ expect.Assert(len(issues) < 2, "Multiple issues were created instead of one")
- expect.NotEmptyString(issue.Title, "Issue title should not be empty")
- expect.Assert(issue.Title == "My first Issue",
- fmt.Sprintf("Issue title does not match the expected value 'My first Issue', but was '%s'", issue.Title))
+ expect.NotEmptyAndEqual(issue.Title, "My first Issue", "Issue title")
+ expect.NotEmptyAndEqual(issue.Description, "I need to do some coding", "Issue description")
+ expect.NotEmptyAndEqual(issue.Assignee, "Me", "Issue assignee")
- expect.NotEmptyString(issue.Description, "Issue description should not be empty")
- expect.Assert(issue.Description == "I need to do some coding",
- fmt.Sprintf("Issue description does not match the expected value 'I need to do some coding', but was '%s'", issue.Description))
-
- expect.Assert(issue.Assignee == "Me",
- fmt.Sprintf("Issue should be assigned to 'Me', but was assigned to '%s'", issue.Assignee))
-
- if issue.Status == models.StatusInProgress || isInProgress {
- isInProgress = true
- } else {
- expect.Fail("Issue should be marked as in-progress when work starts")
- }
-
- if !isInProgress {
- return expect.ValidationFeedback
- } else if issue.Status != models.StatusClosed {
- expect.Fail("Issue should be set to Closed once the work is completed")
- }
+ expect.Assert(issue.Status == models.StatusInProgress,
+ fmt.Sprintf("Issue status should be 'In Progress', but was '%s'", issue.Status))
return expect.Complete()
}
diff --git a/go.mod b/go.mod
index ff1e7b2..a9ecc2f 100644
--- a/go.mod
+++ b/go.mod
@@ -25,7 +25,7 @@ require (
// Web dependencies
require (
github.com/NYTimes/gziphandler v1.1.1
- github.com/a-h/templ v0.3.977
+ github.com/a-h/templ v0.3.1001
github.com/donseba/go-htmx v1.12.1
github.com/go-chi/chi/v5 v5.2.5
github.com/go-playground/form/v4 v4.3.0
@@ -69,7 +69,7 @@ require (
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/snappy v0.0.4 // indirect
- github.com/haatos/goshipit v0.0.0-20260206030541-056850f43320 // indirect
+ github.com/haatos/goshipit v0.0.0-20260305043009-36e5c9a2e5c6 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kevinburke/ssh_config v1.5.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
diff --git a/go.sum b/go.sum
index 9e37a12..d159960 100644
--- a/go.sum
+++ b/go.sum
@@ -10,8 +10,8 @@ github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBi
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
-github.com/a-h/templ v0.3.977 h1:kiKAPXTZE2Iaf8JbtM21r54A8bCNsncrfnokZZSrSDg=
-github.com/a-h/templ v0.3.977/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
+github.com/a-h/templ v0.3.1001 h1:yHDTgexACdJttyiyamcTHXr2QkIeVF1MukLy44EAhMY=
+github.com/a-h/templ v0.3.1001/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
@@ -128,8 +128,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/haatos/goshipit v0.0.0-20260206030541-056850f43320 h1:sb7SfxZfN+U9OHC61tcS98Ge0zY9uEkW5CP6KB4YVHg=
-github.com/haatos/goshipit v0.0.0-20260206030541-056850f43320/go.mod h1:LFP8N8y5ORkifb+LZuOVNZYlJuV3WqdXCjxX5pGUaNI=
+github.com/haatos/goshipit v0.0.0-20260305043009-36e5c9a2e5c6 h1:Yakj3J1ZxYCpFtBHUVkSBNA80LK/8bNrl/LA8fSvvFw=
+github.com/haatos/goshipit v0.0.0-20260305043009-36e5c9a2e5c6/go.mod h1:2A31H3xgQHTgNp8OlX7aliXaY3QFwtI7XHuaP8G1ZSw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kevinburke/ssh_config v1.5.0 h1:3cPZmE54xb5j3G5xQCjSvokqNwU2uW+3ry1+PRLSPpA=
diff --git a/internal/commands/issues/close.go b/internal/commands/issues/close.go
index 3c89b10..ce4bfcb 100644
--- a/internal/commands/issues/close.go
+++ b/internal/commands/issues/close.go
@@ -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,30 @@ 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)
+ }
+ if closeReason == "" {
+ return fmt.Errorf("closing reason cannot be empty when selecting 'Other'")
+ }
+ }
+
+ err = app.Issues.CloseIssue(cmd.Context(), closeID, closeReason, "", "")
if err != nil {
return fmt.Errorf("error closing issue: %w", err)
}
diff --git a/internal/utils/check/expect.go b/internal/utils/check/expect.go
index 670fcdb..8850d83 100644
--- a/internal/utils/check/expect.go
+++ b/internal/utils/check/expect.go
@@ -43,11 +43,6 @@ func (e *Expector) CompleteWithMessage(message string) ValidationFeedback {
return e.ValidationFeedback
}
-func (e *Expector) Fail(message string) ValidationFeedback {
- e.Checks = append(e.Checks, NewCheck(message, false))
- return e.ValidationFeedback
-}
-
func (e *Expector) Errors() []error {
var errors []error
for _, check := range e.Checks {
@@ -58,6 +53,32 @@ func (e *Expector) Errors() []error {
return errors
}
+func (e *Expector) Pass(message string) *Expector {
+ e.Checks = append(e.Checks, NewCheck(message, true))
+ return e
+}
+
+func (e *Expector) Fail(message string) *Expector {
+ e.Checks = append(e.Checks, NewCheck(message, false))
+ return e
+}
+
+func (e *Expector) NotEmptyAndEqual(val, expected string, message string) *Expector {
+ if val == "" {
+ return e.Fail(fmt.Sprintf("%s is empty", message))
+ } else if val != expected {
+ return e.Fail(fmt.Sprintf(`%s expected "%v", got "%v"`, message, expected, val))
+ }
+ return e.Pass(message + " is correct")
+}
+
+func (e *Expector) Equal(val, expected any, message string) *Expector {
+ if val != expected {
+ return e.Fail(fmt.Sprintf(`%s expected "%v", got "%v"`, message, expected, val))
+ }
+ return e.Pass(message + " is correct")
+}
+
func (e *Expector) Assert(condition bool, message string) *Expector {
check := NewCheck(message, condition)
e.Checks = append(e.Checks, check)
diff --git a/pkg/web/assets/js/board-drag-drop.js b/pkg/web/assets/js/board-drag-drop.js
new file mode 100644
index 0000000..fba3bad
--- /dev/null
+++ b/pkg/web/assets/js/board-drag-drop.js
@@ -0,0 +1,68 @@
+/**
+ * Drag and drop for board view - allows moving issue cards between status columns
+ */
+(function () {
+ document.addEventListener("dragstart", function (e) {
+ if (e.target.closest("button") || e.target.closest("a")) return;
+ const card = e.target.closest(".board-card");
+ if (!card) return;
+ e.dataTransfer.setData("text/plain", card.dataset.issueId);
+ e.dataTransfer.effectAllowed = "move";
+ card.classList.add("opacity-50");
+ });
+
+ document.addEventListener("dragend", function (e) {
+ const card = e.target.closest(".board-card");
+ if (card) card.classList.remove("opacity-50");
+ });
+
+ document.addEventListener("dragover", function (e) {
+ const zone = e.target.closest(".column-drop-zone");
+ if (!zone) return;
+ e.preventDefault();
+ e.dataTransfer.dropEffect = "move";
+ zone.classList.add("ring-2", "ring-primary", "ring-inset");
+ });
+
+ document.addEventListener("dragleave", function (e) {
+ const zone = e.target.closest(".column-drop-zone");
+ if (!zone || zone.contains(e.relatedTarget)) return;
+ zone.classList.remove("ring-2", "ring-primary", "ring-inset");
+ });
+
+ document.addEventListener("drop", function (e) {
+ const zone = e.target.closest(".column-drop-zone");
+ if (!zone) return;
+ e.preventDefault();
+ zone.classList.remove("ring-2", "ring-primary", "ring-inset");
+ const issueId = e.dataTransfer.getData("text/plain");
+ const newStatus = zone.dataset.status;
+ if (!issueId || !newStatus) return;
+
+ const formData = new URLSearchParams();
+ formData.append("status", newStatus);
+
+ fetch("/issues/" + issueId + "?from=board", {
+ method: "PATCH",
+ body: formData,
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ "HX-Request": "true",
+ },
+ }).then(function (r) {
+ if (r.ok) {
+ var redirect = r.headers.get("HX-Redirect");
+ if (redirect) {
+ window.location.href = redirect;
+ } else if (typeof htmx !== "undefined") {
+ htmx.ajax("GET", "/?board=true", {
+ target: "main",
+ swap: "innerHTML",
+ });
+ } else {
+ window.location.href = "/?board=true";
+ }
+ }
+ });
+ });
+})();
diff --git a/pkg/web/components/assignee_form_templ.go b/pkg/web/components/assignee_form_templ.go
index 06f3ea1..ae68a4f 100644
--- a/pkg/web/components/assignee_form_templ.go
+++ b/pkg/web/components/assignee_form_templ.go
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
-// templ: version: v0.3.977
+// templ: version: v0.3.1001
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
diff --git a/pkg/web/components/base/input_templ.go b/pkg/web/components/base/input_templ.go
index 9d787ce..1b68a0c 100644
--- a/pkg/web/components/base/input_templ.go
+++ b/pkg/web/components/base/input_templ.go
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
-// templ: version: v0.3.977
+// templ: version: v0.3.1001
package base
//lint:file-ignore SA4006 This context is only used if a nested component is present.
diff --git a/pkg/web/components/base/range_templ.go b/pkg/web/components/base/range_templ.go
index 4ae6395..1a57ebe 100644
--- a/pkg/web/components/base/range_templ.go
+++ b/pkg/web/components/base/range_templ.go
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
-// templ: version: v0.3.977
+// templ: version: v0.3.1001
package base
//lint:file-ignore SA4006 This context is only used if a nested component is present.
diff --git a/pkg/web/components/base/select_templ.go b/pkg/web/components/base/select_templ.go
index aab85ee..a7b5040 100644
--- a/pkg/web/components/base/select_templ.go
+++ b/pkg/web/components/base/select_templ.go
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
-// templ: version: v0.3.977
+// templ: version: v0.3.1001
package base
//lint:file-ignore SA4006 This context is only used if a nested component is present.
diff --git a/pkg/web/components/base/table_templ.go b/pkg/web/components/base/table_templ.go
index b7544a0..7259528 100644
--- a/pkg/web/components/base/table_templ.go
+++ b/pkg/web/components/base/table_templ.go
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
-// templ: version: v0.3.977
+// templ: version: v0.3.1001
package base
//lint:file-ignore SA4006 This context is only used if a nested component is present.
diff --git a/pkg/web/components/base/textarea_templ.go b/pkg/web/components/base/textarea_templ.go
index 32f9357..0d1bf07 100644
--- a/pkg/web/components/base/textarea_templ.go
+++ b/pkg/web/components/base/textarea_templ.go
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
-// templ: version: v0.3.977
+// templ: version: v0.3.1001
package base
//lint:file-ignore SA4006 This context is only used if a nested component is present.
diff --git a/pkg/web/components/close_issue_form.templ b/pkg/web/components/close_issue_form.templ
new file mode 100644
index 0000000..7c6a1c2
--- /dev/null
+++ b/pkg/web/components/close_issue_form.templ
@@ -0,0 +1,34 @@
+package components
+
+import "github.com/LazyBachelor/LazyPM/pkg/web/components/base"
+
+type CloseIssueFormProps struct {
+ PostAction string
+}
+
+templ CloseIssueForm(props CloseIssueFormProps) {
+
+
+}
diff --git a/pkg/web/components/close_issue_form_templ.go b/pkg/web/components/close_issue_form_templ.go
new file mode 100644
index 0000000..1e2b421
--- /dev/null
+++ b/pkg/web/components/close_issue_form_templ.go
@@ -0,0 +1,79 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.3.1001
+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, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/pkg/web/components/comment_templ.go b/pkg/web/components/comment_templ.go
index e97390e..2542d51 100644
--- a/pkg/web/components/comment_templ.go
+++ b/pkg/web/components/comment_templ.go
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
-// templ: version: v0.3.977
+// templ: version: v0.3.1001
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
diff --git a/pkg/web/components/header_templ.go b/pkg/web/components/header_templ.go
index 4a6a7fa..a4c3217 100644
--- a/pkg/web/components/header_templ.go
+++ b/pkg/web/components/header_templ.go
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
-// templ: version: v0.3.977
+// templ: version: v0.3.1001
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
diff --git a/pkg/web/components/icons_templ.go b/pkg/web/components/icons_templ.go
index c8c421c..0df923b 100644
--- a/pkg/web/components/icons_templ.go
+++ b/pkg/web/components/icons_templ.go
@@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT.
-// templ: version: v0.3.977
+// templ: version: v0.3.1001
package components
//lint:file-ignore SA4006 This context is only used if a nested component is present.
diff --git a/pkg/web/components/issue.templ b/pkg/web/components/issue.templ
index 9ce4408..894bdbd 100644
--- a/pkg/web/components/issue.templ
+++ b/pkg/web/components/issue.templ
@@ -7,20 +7,22 @@ import (
)
type IssueFormProps struct {
- PostAction string
- PatchAction string
- Title string
- Description string
- Status string
- Priority int
- IssueType string
- Class string
- Attrs templ.Attributes
- Target string // Optional: if empty, server controls via HX-Target header
+ PostAction string
+ PatchAction string
+ DeleteAction string // Optional: URL for delete confirmation modal
+ Title string
+ Description string
+ Status string
+ CloseReason string // Set when status is "closed"
+ Priority int
+ IssueType string
+ Class string
+ Attrs templ.Attributes
+ Target string // Optional: if empty, server controls via HX-Target header
}
templ IssueForm(props IssueFormProps) {
-