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) { +
+
+ @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", + }) + +
+} 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 + } + 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, "
") + 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) { -
+
+ @base.Textarea(base.TextareaProps{ + Name: "close_reason", + Label: "Reason for closing issue", + Value: props.CloseReason, + Class: "w-full", + Placeholder: "Describe why this issue is being closed...", + Rows: 3, + }) +
+ } @base.Select(base.SelectProps{ Name: "issue_type", Label: "Issue Type", @@ -84,6 +100,17 @@ templ IssueForm(props IssueFormProps) { Submit Issue + if props.DeleteAction != "" { + + } if props.Target == "" {
} diff --git a/pkg/web/components/issue_detail.templ b/pkg/web/components/issue_detail.templ index 6638c26..f03a219 100644 --- a/pkg/web/components/issue_detail.templ +++ b/pkg/web/components/issue_detail.templ @@ -40,16 +40,15 @@ templ IssueDetail(props IssueDetailProps) {

{ props.Issue.CreatedBy }

if props.Issue.Assignee == "" { - Unassigned + Unassigned (click to assign) } else { - Assignee: -

{ props.Issue.Assignee }

+ Assignee: { props.Issue.Assignee } }
diff --git a/pkg/web/components/issue_detail_templ.go b/pkg/web/components/issue_detail_templ.go index 300533c..c3d4351 100644 --- a/pkg/web/components/issue_detail_templ.go +++ b/pkg/web/components/issue_detail_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. @@ -130,7 +130,7 @@ func IssueDetail(props IssueDetailProps) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "

Unassigned") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "Unassigned (click to assign) ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "Assignee:

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "Assignee: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var8 string templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(props.Issue.Assignee) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 52, Col: 56} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 51, Col: 42} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -234,7 +234,7 @@ func StatusBadge(status models.Status) templ.Component { var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(string(status)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 71, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 70, Col: 60} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { @@ -304,7 +304,7 @@ func TypeBadge(issueType models.IssueType) templ.Component { var templ_7745c5c3_Var12 string templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(string(issueType)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 88, Col: 65} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 87, Col: 65} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { @@ -374,7 +374,7 @@ func PriorityBadge(priority int) templ.Component { var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("P%d", priority)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 105, Col: 74} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 104, Col: 74} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { diff --git a/pkg/web/components/issue_templ.go b/pkg/web/components/issue_templ.go index 7715cb9..d055a8e 100644 --- a/pkg/web/components/issue_templ.go +++ b/pkg/web/components/issue_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. @@ -15,16 +15,18 @@ 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 } func IssueForm(props IssueFormProps) templ.Component { @@ -66,68 +68,81 @@ func IssueForm(props IssueFormProps) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, ">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -167,11 +182,33 @@ func IssueForm(props IssueFormProps) templ.Component { {Label: "In Progress", Value: "in_progress", Selected: props.Status == "in_progress"}, {Label: "Closed", Value: "closed", Selected: props.Status == "closed"}, }, - Size: "md", + Size: "md", + Attrs: templ.Attributes{"x-model": "status"}, }).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + if props.PatchAction != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = base.Textarea(base.TextareaProps{ + Name: "close_reason", + Label: "Reason for closing issue", + Value: props.CloseReason, + Class: "w-full", + Placeholder: "Describe why this issue is being closed...", + Rows: 3, + }).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } templ_7745c5c3_Err = base.Select(base.SelectProps{ Name: "issue_type", Label: "Issue Type", @@ -199,17 +236,36 @@ func IssueForm(props IssueFormProps) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if props.Target == "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") + if props.DeleteAction != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") + if props.Target == "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -237,9 +293,9 @@ func IssueTable(props IssueTableProps) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var7 := templ.GetChildren(ctx) - if templ_7745c5c3_Var7 == nil { - templ_7745c5c3_Var7 = templ.NopComponent + templ_7745c5c3_Var9 := templ.GetChildren(ctx) + if templ_7745c5c3_Var9 == nil { + templ_7745c5c3_Var9 = templ.NopComponent } ctx = templ.ClearChildren(ctx) templ_7745c5c3_Err = base.Table( @@ -279,117 +335,117 @@ func IssueRows(issues []*models.Issue) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var8 := templ.GetChildren(ctx) - if templ_7745c5c3_Var8 == nil { - templ_7745c5c3_Var8 = templ.NopComponent + templ_7745c5c3_Var10 := templ.GetChildren(ctx) + if templ_7745c5c3_Var10 == nil { + templ_7745c5c3_Var10 = templ.NopComponent } ctx = templ.ClearChildren(ctx) for _, issue := range issues { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var10 string - templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(issue.ID) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 124, Col: 15} - } - _, 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, 16, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" hx-target=\"main\" hx-swap=\"innerHTML\" hx-push-url=\"true\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title) + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(issue.ID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 133, Col: 18} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 151, Col: 15} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" hx-target=\"main\" hx-swap=\"innerHTML\" hx-push-url=\"true\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var14 string - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(string(issue.IssueType)) + 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/components/issue.templ`, Line: 136, Col: 57} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 160, Col: 18} } _, 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, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var15 string - templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("P%d", issue.Priority)) + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(string(issue.Status)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 137, Col: 68} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 162, Col: 54} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("P%d", issue.Priority)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 164, Col: 68} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pkg/web/components/layout_templ.go b/pkg/web/components/layout_templ.go index 28d4c95..a426fce 100644 --- a/pkg/web/components/layout_templ.go +++ b/pkg/web/components/layout_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/modal.templ b/pkg/web/components/modal.templ index 8cbe906..36c049b 100644 --- a/pkg/web/components/modal.templ +++ b/pkg/web/components/modal.templ @@ -3,15 +3,22 @@ package components import "fmt" type ModalProps struct { - ID string - Title string - Open bool - Content templ.Component + ID string + Title string + Open bool + Content templ.Component + MaxWidth string // Optional: custom max-width class (defaults to max-w-xl) } templ Modal(props ModalProps) { + {{ + maxWidth := props.MaxWidth + if maxWidth == "" { + maxWidth = "max-w-xl" + } + }}
} + +type ConfirmModalProps struct { + Title string + Message string + ConfirmText string + CancelText string + ConfirmAction string + ConfirmClass string + IssueID string +} + +templ ConfirmModal(props ConfirmModalProps) { + +} diff --git a/pkg/web/components/modal_templ.go b/pkg/web/components/modal_templ.go index ac4ec26..b6fbc34 100644 --- a/pkg/web/components/modal_templ.go +++ b/pkg/web/components/modal_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. @@ -11,10 +11,11 @@ import templruntime "github.com/a-h/templ/runtime" import "fmt" type ModalProps struct { - ID string - Title string - Open bool - Content templ.Component + ID string + Title string + Open bool + Content templ.Component + MaxWidth string // Optional: custom max-width class (defaults to max-w-xl) } func Modal(props ModalProps) templ.Component { @@ -38,68 +39,90 @@ func Modal(props ModalProps) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" id=\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var4 = []any{"relative bg-base-100 rounded-lg w-full pointer-events-auto"} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...) + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(props.ID) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 22, Col: 15} + } + _, 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, 4, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" x-show=\"open\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"modal-title\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var6 string - templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(props.Title) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 26, Col: 65} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + var templ_7745c5c3_Var6 = []any{"relative bg-base-100 rounded-lg w-full pointer-events-auto"} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

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

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(props.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 33, Col: 65} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -109,7 +132,7 @@ func Modal(props ModalProps) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -133,12 +156,152 @@ func ModalContainer() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var7 := templ.GetChildren(ctx) - if templ_7745c5c3_Var7 == nil { - templ_7745c5c3_Var7 = templ.NopComponent + templ_7745c5c3_Var9 := templ.GetChildren(ctx) + if templ_7745c5c3_Var9 == nil { + templ_7745c5c3_Var9 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +type ConfirmModalProps struct { + Title string + Message string + ConfirmText string + CancelText string + ConfirmAction string + ConfirmClass string + IssueID string +} + +func ConfirmModal(props ConfirmModalProps) 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_Var10 := templ.GetChildren(ctx) + if templ_7745c5c3_Var10 == nil { + templ_7745c5c3_Var10 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(props.Title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 74, Col: 73} + } + _, 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, 11, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(props.Message) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/modal.templ`, Line: 78, Col: 52} + } + _, 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, 12, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 = []any{"btn", props.ConfirmClass} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pkg/web/components/search_templ.go b/pkg/web/components/search_templ.go index f27707f..6fc7f33 100644 --- a/pkg/web/components/search_templ.go +++ b/pkg/web/components/search_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/sidebar_templ.go b/pkg/web/components/sidebar_templ.go index a66fd90..f26a365 100644 --- a/pkg/web/components/sidebar_templ.go +++ b/pkg/web/components/sidebar_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/status.templ b/pkg/web/components/status.templ index 63ea2de..ad255c0 100644 --- a/pkg/web/components/status.templ +++ b/pkg/web/components/status.templ @@ -1,7 +1,7 @@ package components templ Status() { -
-
+
+
} diff --git a/pkg/web/components/status_templ.go b/pkg/web/components/status_templ.go index 0a2f7e1..41694c7 100644 --- a/pkg/web/components/status_templ.go +++ b/pkg/web/components/status_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. @@ -29,7 +29,7 @@ func Status() templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pkg/web/handler/dashboard.go b/pkg/web/handler/dashboard.go index b2a5fc1..4c32a37 100644 --- a/pkg/web/handler/dashboard.go +++ b/pkg/web/handler/dashboard.go @@ -20,6 +20,27 @@ func DashboardHandler(w http.ResponseWriter, r *http.Request) { return } + // Check if board view is requested + isBoardView := r.URL.Query().Get("board") == "true" + + if isBoardView { + boardProps := routes.BoardViewProps{ + BaseURL: "/?board=true", + QueryParam: query, + Issues: issues, + EmptyText: "No issues found", + } + + if hx.IsHxRequest() { + routes.BoardViewContent(boardProps).Render(r.Context(), w) + return + } + + routes.BoardView(boardProps).Render(r.Context(), w) + return + } + + // Regular dashboard view var selectedIssue *models.Issue if len(issues) > 0 { selectedIssue = issues[0] diff --git a/pkg/web/handler/issues.go b/pkg/web/handler/issues.go index 76f1e94..3306c7a 100644 --- a/pkg/web/handler/issues.go +++ b/pkg/web/handler/issues.go @@ -2,6 +2,7 @@ package handler import ( "context" + "html" "net/http" "strings" @@ -25,6 +26,7 @@ type UpdateIssueForm struct { Title *string `form:"title" validate:"omitempty,max=255"` Description *string `form:"description" validate:"omitempty,max=2000"` Status *models.Status `form:"status" validate:"omitempty,oneof=open in_progress closed"` + CloseReason *string `form:"close_reason" validate:"omitempty,max=2000"` IssueType *models.IssueType `form:"issue_type" validate:"omitempty,oneof=task bug feature chore"` Priority *int `form:"priority" validate:"omitempty,gte=0,lte=4"` } @@ -56,7 +58,16 @@ func CreateIssue(w http.ResponseWriter, r *http.Request) { } if hx.IsHxRequest() { - w.Header().Set("HX-Refresh", "true") + // Check if 'from' parameter says board view + from := r.URL.Query().Get("from") + referer := r.Header.Get("Referer") + boardView := from == "board" || strings.Contains(referer, "board=true") + + if boardView { + w.Header().Set("HX-Redirect", "/?board=true") + } else { + w.Header().Set("HX-Redirect", "/?selected-issue="+issue.ID) + } return } @@ -111,19 +122,20 @@ func GetIssue(w http.ResponseWriter, r *http.Request) { comments := r.Context().Value(commentsKey).([]*models.Comment) hx := HTMX(r) + from := r.URL.Query().Get("from") + detailProps := routes.IssueDetailProps{ + Issue: issue, + Comments: comments, + From: from, + } + if !hx.IsHxRequest() && strings.Contains(r.Header.Get("Accept"), "text/html") { - routes.IssueDetail(routes.IssueDetailProps{ - Issue: issue, - Comments: comments, - }).Render(r.Context(), w) + routes.IssueDetail(detailProps).Render(r.Context(), w) return } if hx.IsHxRequest() { - routes.IssueDetailContent(routes.IssueDetailProps{ - Issue: issue, - Comments: comments, - }).Render(r.Context(), w) + routes.IssueDetailContent(detailProps).Render(r.Context(), w) return } @@ -165,7 +177,16 @@ func UpdateIssue(w http.ResponseWriter, r *http.Request) { } if hx.IsHxRequest() { - w.Header().Set("HX-Refresh", "true") + // Check if 'from' parameter says board view + from := r.URL.Query().Get("from") + referer := r.Header.Get("Referer") + boardView := from == "board" || strings.Contains(referer, "board=true") + + if boardView { + w.Header().Set("HX-Redirect", "/?board=true") + } else { + w.Header().Set("HX-Redirect", "/?selected-issue="+issue.ID) + } return } @@ -189,7 +210,15 @@ func UpdateAssignee(w http.ResponseWriter, r *http.Request) { } if HTMX(r).IsHxRequest() { - w.Header().Set("HX-Refresh", "true") + // Check if we're in board view + referer := r.Header.Get("Referer") + boardView := strings.Contains(referer, "board=true") + + if boardView { + w.Header().Set("HX-Redirect", "/?board=true&selected-issue="+issue.ID) + } else { + w.Header().Set("HX-Redirect", "/?selected-issue="+issue.ID) + } return } @@ -206,7 +235,59 @@ func DeleteIssue(w http.ResponseWriter, r *http.Request) { } if HTMX(r).IsHxRequest() { - w.Header().Set("HX-Redirect", "/") + // Check if 'from' parameter indicates board view + from := r.URL.Query().Get("from") + referer := r.Header.Get("Referer") + boardView := from == "board" || strings.Contains(referer, "board=true") + + if boardView { + w.Header().Set("HX-Redirect", "/?board=true") + } else { + w.Header().Set("HX-Redirect", "/") + } + return + } + + 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 + } + + issueVal := r.Context().Value(issueKey) + issue, ok := issueVal.(*models.Issue) + if !ok || issue == nil { + http.Error(w, "Issue not found in context", http.StatusInternalServerError) + return + } + + closeReason := r.FormValue("close_reason") + + if closeReason == "" { + if HTMX(r).IsHxRequest() { + w.WriteHeader(http.StatusBadRequest) + HTMX(r).WriteString("
Closing reason is required
") + } 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() { + w.WriteHeader(http.StatusInternalServerError) + HTMX(r).WriteString("
Failed to close issue: " + html.EscapeString(err.Error()) + "
") + } else { + http.Error(w, "Failed to close issue: "+err.Error(), http.StatusInternalServerError) + } + return + } + + if HTMX(r).IsHxRequest() { + w.Header().Set("HX-Refresh", "true") return } @@ -234,6 +315,9 @@ func (f *UpdateIssueForm) toChanges() map[string]any { if f.Status != nil { changes["status"] = *f.Status } + if f.CloseReason != nil { + changes["close_reason"] = *f.CloseReason + } if f.IssueType != nil { changes["issue_type"] = *f.IssueType } diff --git a/pkg/web/handler/modals.go b/pkg/web/handler/modals.go index c4e7fc3..b9bb2fa 100644 --- a/pkg/web/handler/modals.go +++ b/pkg/web/handler/modals.go @@ -2,14 +2,24 @@ package handler import ( "net/http" + "net/url" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/web/components" ) func CreateIssueFormModal(w http.ResponseWriter, r *http.Request) { + // Get the 'from' parameter to track where the user came from + from := r.URL.Query().Get("from") + postAction := "/issues" + if from != "" { + v := url.Values{} + v.Set("from", from) + postAction += "?" + v.Encode() + } + modalContent := components.IssueForm(components.IssueFormProps{ - PostAction: "/issues", + PostAction: postAction, Status: "open", IssueType: "task", Priority: 0, @@ -26,20 +36,34 @@ func CreateIssueFormModal(w http.ResponseWriter, r *http.Request) { } func EditIssueFormModal(w http.ResponseWriter, r *http.Request) { - issue := r.Context().Value(issueKey).(*models.Issue) - - if issue == nil { + issueVal := r.Context().Value(issueKey) + issue, ok := issueVal.(*models.Issue) + if !ok || issue == nil { http.Error(w, "Issue not found in context", http.StatusInternalServerError) return } + // Get the 'from' parameter to track where the user came from + from := r.URL.Query().Get("from") + patchAction := "/issues/" + issue.ID + deleteAction := "/issues/" + issue.ID + "/delete" + if from != "" { + v := url.Values{} + v.Set("from", from) + q := v.Encode() + patchAction += "?" + q + deleteAction += "?" + q + } + modalContent := components.IssueForm(components.IssueFormProps{ - PatchAction: "/issues/" + issue.ID, - Title: issue.Title, - Description: issue.Description, - Status: string(issue.Status), - IssueType: string(issue.IssueType), - Priority: issue.Priority, + PatchAction: patchAction, + DeleteAction: deleteAction, + Title: issue.Title, + Description: issue.Description, + Status: string(issue.Status), + CloseReason: issue.CloseReason, + IssueType: string(issue.IssueType), + Priority: issue.Priority, }) modal := components.Modal(components.ModalProps{ @@ -52,9 +76,9 @@ func EditIssueFormModal(w http.ResponseWriter, r *http.Request) { } func AssigneeFormModal(w http.ResponseWriter, r *http.Request) { - issue := r.Context().Value(issueKey).(*models.Issue) - - if issue == nil { + issueVal := r.Context().Value(issueKey) + issue, ok := issueVal.(*models.Issue) + if !ok || issue == nil { http.Error(w, "Issue not found in context", http.StatusInternalServerError) return } @@ -72,3 +96,59 @@ func AssigneeFormModal(w http.ResponseWriter, r *http.Request) { }) modal.Render(r.Context(), w) } + +func DeleteIssueConfirmModal(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 + } + + // Get the 'from' parameter to track where the user came from + from := r.URL.Query().Get("from") + deleteAction := "/issues/" + issue.ID + if from != "" { + v := url.Values{} + v.Set("from", from) + deleteAction += "?" + v.Encode() + } + + confirmModal := components.ConfirmModal(components.ConfirmModalProps{ + Title: "Delete Issue", + Message: "Are you sure you want to delete this issue? This action cannot be undone.", + ConfirmText: "Delete Issue", + CancelText: "Cancel", + ConfirmAction: deleteAction, + ConfirmClass: "btn-error", + IssueID: issue.ID, + }) + + confirmModal.Render(r.Context(), w) +} + +func CloseIssueFormModal(w http.ResponseWriter, r *http.Request) { + issueVal := r.Context().Value(issueKey) + issue, ok := issueVal.(*models.Issue) + if !ok || 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) +} diff --git a/pkg/web/handler/task.go b/pkg/web/handler/task.go index 08984bb..b68b11d 100644 --- a/pkg/web/handler/task.go +++ b/pkg/web/handler/task.go @@ -30,7 +30,11 @@ func HandleTaskStatus(w http.ResponseWriter, r *http.Request) { hx := HTMX(r) if hx.IsHxRequest() { - hx.WriteString(`` + taskFeedback.Message + ``) + hx.WriteString(` +
+ +
`) + return } @@ -39,6 +43,7 @@ func HandleTaskStatus(w http.ResponseWriter, r *http.Request) { } func HandleTaskStatusModal(w http.ResponseWriter, r *http.Request) { + time.Sleep(100 * time.Millisecond) err := components.Modal(components.ModalProps{ ID: "task-status-modal", Title: "Task Status", @@ -59,8 +64,8 @@ func feedbackList(feedback ValidationFeedback) templ.Component { } else { io.WriteString(w, `

`+"✅ "+check.Message+`

`) } - } + return nil }) } diff --git a/pkg/web/input.css b/pkg/web/input.css index 73bfd82..1341d1f 100644 --- a/pkg/web/input.css +++ b/pkg/web/input.css @@ -1,4 +1,10 @@ @import 'tailwindcss'; + +/* Alpine.js x-cloak: hide until Alpine initializes */ +[x-cloak] { + display: none !important; +} + @source "./**/*.templ"; @source "./**/*.go"; @source "./components/**/*.templ"; diff --git a/pkg/web/routes/boardview.templ b/pkg/web/routes/boardview.templ new file mode 100644 index 0000000..13927dc --- /dev/null +++ b/pkg/web/routes/boardview.templ @@ -0,0 +1,147 @@ +package routes + +import ( + "fmt" + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/pkg/web/components" +) + +type BoardViewProps struct { + Issues []*models.Issue + BaseURL string + QueryParam string + EmptyText string +} + +templ BoardView(props BoardViewProps) { + @BaseLayout() { + @BoardViewContent(props) + } +} + +templ BoardViewContent(props BoardViewProps) { +
+
+
+ @components.SearchForm(components.SearchFormProps{ + RootURL: props.BaseURL, + SearchQuery: props.QueryParam, + Target: "#board-container", + }) +
+
+
+
+ + +
+ +
+
+
+
+ @BoardColumns(props.Issues) +
+
+} + +templ BoardColumns(issues []*models.Issue) { + {{ + openIssues := []*models.Issue{} + inProgressIssues := []*models.Issue{} + closedIssues := []*models.Issue{} + + for _, issue := range issues { + switch issue.Status { + case "open": + openIssues = append(openIssues, issue) + case "in_progress": + inProgressIssues = append(inProgressIssues, issue) + case "closed": + closedIssues = append(closedIssues, issue) + } + } + }} +
+ @BoardColumn("Open", "open", openIssues, "badge-info") + @BoardColumn("In Progress", "in_progress", inProgressIssues, "badge-warning") + @BoardColumn("Closed", "closed", closedIssues, "badge-success") +
+} + +templ BoardColumn(title string, status string, issues []*models.Issue, badgeClass string) { +
+
+

{ title }

+ { fmt.Sprintf("%d", len(issues)) } +
+
+ if len(issues) == 0 { +
Drop issues here
+ } + for _, issue := range issues { + @BoardCard(issue) + } +
+
+} + +templ BoardCard(issue *models.Issue) { +
+
+
+

{ issue.Title }

+
+ + +
+
+
+ { issue.ID } + @components.TypeBadge(issue.IssueType) + @components.PriorityBadge(issue.Priority) +
+ if issue.Description != "" { +

{ issue.Description }

+ } +
+
+} diff --git a/pkg/web/routes/boardview_templ.go b/pkg/web/routes/boardview_templ.go new file mode 100644 index 0000000..406eedf --- /dev/null +++ b/pkg/web/routes/boardview_templ.go @@ -0,0 +1,427 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1001 +package routes + +//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 ( + "fmt" + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/pkg/web/components" +) + +type BoardViewProps struct { + Issues []*models.Issue + BaseURL string + QueryParam string + EmptyText string +} + +func BoardView(props BoardViewProps) 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_Var2 := 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 { + 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_Err = BoardViewContent(props).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) + templ_7745c5c3_Err = BaseLayout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func BoardViewContent(props BoardViewProps) 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_Var3 := templ.GetChildren(ctx) + if templ_7745c5c3_Var3 == nil { + templ_7745c5c3_Var3 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = components.SearchForm(components.SearchFormProps{ + RootURL: props.BaseURL, + SearchQuery: props.QueryParam, + Target: "#board-container", + }).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = BoardColumns(props.Issues).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func BoardColumns(issues []*models.Issue) 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_Var4 := templ.GetChildren(ctx) + if templ_7745c5c3_Var4 == nil { + templ_7745c5c3_Var4 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + openIssues := []*models.Issue{} + inProgressIssues := []*models.Issue{} + closedIssues := []*models.Issue{} + + for _, issue := range issues { + switch issue.Status { + case "open": + openIssues = append(openIssues, issue) + case "in_progress": + inProgressIssues = append(inProgressIssues, issue) + case "closed": + closedIssues = append(closedIssues, issue) + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = BoardColumn("Open", "open", openIssues, "badge-info").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = BoardColumn("In Progress", "in_progress", inProgressIssues, "badge-warning").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = BoardColumn("Closed", "closed", closedIssues, "badge-success").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func BoardColumn(title string, status string, issues []*models.Issue, badgeClass string) 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_Var5 := templ.GetChildren(ctx) + if templ_7745c5c3_Var5 == nil { + templ_7745c5c3_Var5 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 88, Col: 44} + } + _, 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, 8, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 = []any{"badge", badgeClass} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(issues))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 89, Col: 71} + } + _, 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, 11, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if len(issues) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
Drop issues here
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + for _, issue := range issues { + templ_7745c5c3_Err = BoardCard(issue).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func BoardCard(issue *models.Issue) 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_Var12 := templ.GetChildren(ctx) + if templ_7745c5c3_Var12 == nil { + templ_7745c5c3_Var12 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "

") + 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/boardview.templ`, Line: 110, Col: 80} + } + _, 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, 17, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(issue.ID) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 138, Col: 65} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = components.TypeBadge(issue.IssueType).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = components.PriorityBadge(issue.Priority).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if issue.Description != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var18 string + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Description) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 143, Col: 81} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/pkg/web/routes/dashboard.templ b/pkg/web/routes/dashboard.templ index 7dbf3c7..fc09df2 100644 --- a/pkg/web/routes/dashboard.templ +++ b/pkg/web/routes/dashboard.templ @@ -21,62 +21,85 @@ templ Dashboard(props DashboardProps) { } templ DashboardContent(props DashboardProps) { -
-
-
-
- @components.SearchForm(components.SearchFormProps{ - RootURL: props.BaseURL, - SearchQuery: props.QueryParam, - Target: "#issue-list-container", - }) -
- +
+
+
+ @components.SearchForm(components.SearchFormProps{ + RootURL: props.BaseURL, + SearchQuery: props.QueryParam, + Target: "#issue-list-container", + })
-
- @DashboardIssueList(props.Issues, props.SelectedIssue.ID) +
+
+
+ + +
+ + +
+
+ if props.SelectedIssue != nil { + + if props.SelectedIssue.Status != "closed" { + + } + + } +
-
- if props.SelectedIssue != nil { - - - -
- @components.IssueDetail(components.IssueDetailProps{ - Issue: props.SelectedIssue, - }) +
+
+
+ @DashboardIssueList(props.Issues, props.SelectedIssue.ID)
- } +
+
+ if props.SelectedIssue != nil { +
+ @components.IssueDetail(components.IssueDetailProps{ + Issue: props.SelectedIssue, + }) +
+ } +
} @@ -116,7 +139,16 @@ templ DashboardIssueRows(issues []*models.Issue, selectedID string) { hx-target="main" hx-vals={ `{"selected-issue": "` + issue.ID + `"}` } hx-swap="innerHTML" + data-issue-id={ issue.ID } > + { issue.ID } { issue.Title } diff --git a/pkg/web/routes/dashboard_templ.go b/pkg/web/routes/dashboard_templ.go index 9c1de81..269ddca 100644 --- a/pkg/web/routes/dashboard_templ.go +++ b/pkg/web/routes/dashboard_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.977 +// templ: version: v0.3.1001 package routes //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -90,7 +90,7 @@ func DashboardContent(props DashboardProps) templ.Component { templ_7745c5c3_Var3 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -102,7 +102,66 @@ func DashboardContent(props DashboardProps) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if props.SelectedIssue != nil { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if props.SelectedIssue.Status != "closed" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -110,51 +169,12 @@ func DashboardContent(props DashboardProps) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if props.SelectedIssue != nil { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -164,12 +184,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, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -198,7 +218,7 @@ func DashboardIssueList(issues []*models.Issue, selectedID string) templ.Compone templ_7745c5c3_Var7 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -218,7 +238,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, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -248,7 +268,7 @@ func DashboardIssueRows(issues []*models.Issue, selectedID string) templ.Compone } ctx = templ.ClearChildren(ctx) if len(issues) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "No issues") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "No issues") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -263,7 +283,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, 13, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" hx-swap=\"innerHTML\" data-issue-id=\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var12 string templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(issue.ID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 120, Col: 43} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/dashboard.templ`, Line: 142, Col: 27} } _, 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, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, 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: 152, Col: 43} + } + _, 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, 22, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, 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: 153, Col: 46} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -323,7 +369,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, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -331,7 +377,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, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -339,7 +385,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, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pkg/web/routes/issue_detail.templ b/pkg/web/routes/issue_detail.templ index 676ed3b..d483a83 100644 --- a/pkg/web/routes/issue_detail.templ +++ b/pkg/web/routes/issue_detail.templ @@ -8,14 +8,29 @@ import ( type IssueDetailProps struct { Issue *models.Issue Comments []*models.Comment + From string // "board" when navigating from board view +} + +type IssueDetailModalProps struct { + Issue *models.Issue + Comments []*models.Comment + From string } templ IssueDetailContent(props IssueDetailProps) { + {{ + backURL := "/" + editURL := "/issues/" + props.Issue.ID + "/edit" + if props.From == "board" { + backURL = "/?board=true" + editURL += "?from=board" + } + }}
@components.IssueDetail(components.IssueDetailProps{Issue: props.Issue}) @@ -57,3 +74,15 @@ templ IssueDetail(props IssueDetailProps) { @IssueDetailContent(props) } } + +templ IssueDetailModalContent(props IssueDetailModalProps) { +
+
+ @components.IssueDetail(components.IssueDetailProps{Issue: props.Issue}) +
+ @components.CommentSection(components.CommentSectionProps{ + IssueID: props.Issue.ID, + Comments: props.Comments, + }) +
+} diff --git a/pkg/web/routes/issue_detail_templ.go b/pkg/web/routes/issue_detail_templ.go index 057479e..6139e82 100644 --- a/pkg/web/routes/issue_detail_templ.go +++ b/pkg/web/routes/issue_detail_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.977 +// templ: version: v0.3.1001 package routes //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -16,6 +16,13 @@ import ( type IssueDetailProps struct { Issue *models.Issue Comments []*models.Comment + From string // "board" when navigating from board view +} + +type IssueDetailModalProps struct { + Issue *models.Issue + Comments []*models.Comment + From string } func IssueDetailContent(props IssueDetailProps) templ.Component { @@ -39,7 +46,26 @@ func IssueDetailContent(props IssueDetailProps) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + backURL := "/" + editURL := "/issues/" + props.Issue.ID + "/edit" + if props.From == "board" { + backURL = "/?board=true" + editURL += "?from=board" + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\">Edit ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if props.Issue.Status != "closed" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -81,7 +117,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, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -92,7 +128,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, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -116,12 +152,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 +175,55 @@ 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 + } + return nil + }) +} + +func IssueDetailModalContent(props IssueDetailModalProps) 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_Var7 := templ.GetChildren(ctx) + if templ_7745c5c3_Var7 == nil { + templ_7745c5c3_Var7 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = components.IssueDetail(components.IssueDetailProps{Issue: props.Issue}).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = components.CommentSection(components.CommentSectionProps{ + IssueID: props.Issue.ID, + Comments: props.Comments, + }).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pkg/web/routes/layout.templ b/pkg/web/routes/layout.templ index 3b7b754..cf1596a 100644 --- a/pkg/web/routes/layout.templ +++ b/pkg/web/routes/layout.templ @@ -14,6 +14,7 @@ var baseLayout = components.LayoutProps{ Scripts: []components.Script{ {Src: "/assets/js/htmx.min.js", Defer: true}, {Src: "/assets/js/alpine.min.js", Defer: true}, + {Src: "/assets/js/board-drag-drop.js", Defer: true}, }, }, Routes: []components.NavRoutes{ diff --git a/pkg/web/routes/layout_templ.go b/pkg/web/routes/layout_templ.go index 6211e01..88f4554 100644 --- a/pkg/web/routes/layout_templ.go +++ b/pkg/web/routes/layout_templ.go @@ -1,6 +1,6 @@ // Code generated by templ - DO NOT EDIT. -// templ: version: v0.3.977 +// templ: version: v0.3.1001 package routes //lint:file-ignore SA4006 This context is only used if a nested component is present. @@ -22,6 +22,7 @@ var baseLayout = components.LayoutProps{ Scripts: []components.Script{ {Src: "/assets/js/htmx.min.js", Defer: true}, {Src: "/assets/js/alpine.min.js", Defer: true}, + {Src: "/assets/js/board-drag-drop.js", Defer: true}, }, }, Routes: []components.NavRoutes{ diff --git a/pkg/web/server/routes.go b/pkg/web/server/routes.go index 236d917..2c4b916 100644 --- a/pkg/web/server/routes.go +++ b/pkg/web/server/routes.go @@ -47,6 +47,9 @@ 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("/delete", handler.DeleteIssueConfirmModal) r.Get("/assignee", handler.AssigneeFormModal) r.Patch("/assignee", handler.UpdateAssignee) r.Delete("/", handler.DeleteIssue)