From d1ef4e94913d8a039239feb2e303f0b1642d286a Mon Sep 17 00:00:00 2001 From: Ine Maria Nilssen Aanonsen Date: Mon, 23 Feb 2026 13:51:42 +0100 Subject: [PATCH] READ ISSUES - WEB --- internal/service/beads.go | 43 ++++++++++++++-- pkg/web/components/search.templ | 26 ++++++++++ pkg/web/components/search_templ.go | 53 ++++++++++++++++++++ pkg/web/handler/index.go | 8 ++- pkg/web/routes/index.templ | 5 +- pkg/web/routes/index_templ.go | 5 +- pkg/web/routes/issue_detail.templ | 2 +- pkg/web/routes/issue_detail_templ.go | 2 +- pkg/web/routes/layout.templ | 75 +++++++++++++--------------- pkg/web/routes/layout_templ.go | 75 +++++++++++++--------------- 10 files changed, 202 insertions(+), 92 deletions(-) create mode 100644 pkg/web/components/search.templ create mode 100644 pkg/web/components/search_templ.go diff --git a/internal/service/beads.go b/internal/service/beads.go index 1a9bc66..803f182 100644 --- a/internal/service/beads.go +++ b/internal/service/beads.go @@ -28,18 +28,51 @@ func NewBeadsService(ctx context.Context, storage beads.Storage, prefix string) } func (s *BeadsService) AllIssues(ctx context.Context) ([]models.Issue, error) { - issuesPtr, err := s.Storage.SearchIssues(ctx, "", models.IssueFilter{}) + issuesPtr, err := s.SearchIssues(ctx, "", models.IssueFilter{}) if err != nil { return nil, err } + return models.IssuesPtrToIssues(issuesPtr), nil +} - if len(issuesPtr) == 0 { - return []models.Issue{}, nil +// SearchIssues returns issues matching the query (title, description, id, assignee) and optional filter. +// The query searches across title, description, and id (LIKE %query%). +// Assignee exact match is also searched when query is non-empty. +func (s *BeadsService) SearchIssues(ctx context.Context, query string, filter models.IssueFilter) ([]*models.Issue, error) { + seen := make(map[string]bool) + var merged []*models.Issue + + // Search by query (title, description, id) + issuesPtr, err := s.Storage.SearchIssues(ctx, query, filter) + if err != nil { + return nil, err + } + for _, issue := range issuesPtr { + if issue != nil && !seen[issue.ID] { + seen[issue.ID] = true + merged = append(merged, issue) + } } - issues := models.IssuesPtrToIssues(issuesPtr) + // Also search by assignee when query is non-empty (exact match) + if query != "" && filter.Assignee == nil { + assigneeFilter := filter + assigneeFilter.Assignee = &query + assigneeFilter.TitleSearch = "" + assigneeFilter.DescriptionContains = "" + assigneePtr, err := s.Storage.SearchIssues(ctx, "", assigneeFilter) + if err != nil { + return nil, err + } + for _, issue := range assigneePtr { + if issue != nil && !seen[issue.ID] { + seen[issue.ID] = true + merged = append(merged, issue) + } + } + } - return issues, nil + return merged, nil } func (s *BeadsService) DeleteIssues() error { diff --git a/pkg/web/components/search.templ b/pkg/web/components/search.templ new file mode 100644 index 0000000..602dc21 --- /dev/null +++ b/pkg/web/components/search.templ @@ -0,0 +1,26 @@ +package components + +templ SearchForm(searchQuery string) { +
+ + +
+} diff --git a/pkg/web/components/search_templ.go b/pkg/web/components/search_templ.go new file mode 100644 index 0000000..cd0b4ec --- /dev/null +++ b/pkg/web/components/search_templ.go @@ -0,0 +1,53 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.977 +package components + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +func SearchForm(searchQuery 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_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/pkg/web/handler/index.go b/pkg/web/handler/index.go index 677a033..f9bbfcb 100644 --- a/pkg/web/handler/index.go +++ b/pkg/web/handler/index.go @@ -2,7 +2,9 @@ package handler import ( "net/http" + "strings" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/web/components" "github.com/LazyBachelor/LazyPM/pkg/web/routes" ) @@ -11,16 +13,20 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) { svc := Services(r) hx := HTMX(r) - issues, err := svc.Beads.AllIssues(r.Context()) + query := strings.TrimSpace(r.URL.Query().Get("q")) + filter := models.IssueFilter{Limit: 100} + issuesPtr, err := svc.Beads.SearchIssues(r.Context(), query, filter) if err != nil { http.Error(w, "failed to retrieve issues", http.StatusInternalServerError) return } + issues := models.IssuesPtrToIssues(issuesPtr) props := routes.IndexProps{ IssueTable: components.IssueTableProps{ Issues: issues, }, + SearchQuery: query, } if hx.IsHxRequest() { diff --git a/pkg/web/routes/index.templ b/pkg/web/routes/index.templ index 3d3157d..3c66dde 100644 --- a/pkg/web/routes/index.templ +++ b/pkg/web/routes/index.templ @@ -3,7 +3,8 @@ package routes import "github.com/LazyBachelor/LazyPM/pkg/web/components" type IndexProps struct { - IssueTable components.IssueTableProps + IssueTable components.IssueTableProps + SearchQuery string } templ IndexContent(props IndexProps) { @@ -24,7 +25,7 @@ templ IndexContent(props IndexProps) { } templ Index(props IndexProps) { - @BaseLayout() { + @BaseLayout(BaseLayoutProps{SearchQuery: props.SearchQuery}) { @IndexContent(props) } } diff --git a/pkg/web/routes/index_templ.go b/pkg/web/routes/index_templ.go index 31ab021..2fd6d17 100644 --- a/pkg/web/routes/index_templ.go +++ b/pkg/web/routes/index_templ.go @@ -11,7 +11,8 @@ import templruntime "github.com/a-h/templ/runtime" import "github.com/LazyBachelor/LazyPM/pkg/web/components" type IndexProps struct { - IssueTable components.IssueTableProps + IssueTable components.IssueTableProps + SearchQuery string } func IndexContent(props IndexProps) templ.Component { @@ -103,7 +104,7 @@ func Index(props IndexProps) templ.Component { } return nil }) - templ_7745c5c3_Err = BaseLayout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var3), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = BaseLayout(BaseLayoutProps{SearchQuery: props.SearchQuery}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var3), templ_7745c5c3_Buffer) 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 606f006..92b34a7 100644 --- a/pkg/web/routes/issue_detail.templ +++ b/pkg/web/routes/issue_detail.templ @@ -77,7 +77,7 @@ templ IssueDetailContent(props IssueDetailProps) { } templ IssueDetail(props IssueDetailProps) { - @BaseLayout() { + @BaseLayout(BaseLayoutProps{SearchQuery: ""}) { @IssueDetailContent(props) } } diff --git a/pkg/web/routes/issue_detail_templ.go b/pkg/web/routes/issue_detail_templ.go index 73e3cef..79e00d3 100644 --- a/pkg/web/routes/issue_detail_templ.go +++ b/pkg/web/routes/issue_detail_templ.go @@ -239,7 +239,7 @@ func IssueDetail(props IssueDetailProps) templ.Component { } return nil }) - templ_7745c5c3_Err = BaseLayout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var10), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = BaseLayout(BaseLayoutProps{SearchQuery: ""}).Render(templ.WithChildren(ctx, templ_7745c5c3_Var10), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pkg/web/routes/layout.templ b/pkg/web/routes/layout.templ index 1bf7c63..45f079a 100644 --- a/pkg/web/routes/layout.templ +++ b/pkg/web/routes/layout.templ @@ -1,49 +1,44 @@ package routes import "github.com/LazyBachelor/LazyPM/pkg/web/components" -import "github.com/LazyBachelor/LazyPM/pkg/web/components/base" -var baseLayout = components.LayoutProps{ - Title: "Beads Test Application", - Description: "A sample application using Beads storage service", - Head: components.HeadProps{ - Links: []components.Link{ - {Href: "/assets/css/styles.css", Rel: "preload", As: "style"}, - {Href: "/assets/css/styles.css", Rel: "stylesheet"}, - {Href: "/assets/manifest.json", Rel: "manifest"}, - }, - Scripts: []components.Script{ - {Src: "/assets/js/htmx.min.js", Defer: true}, - {Src: "/assets/js/ajax.min.js", Defer: true}, - }, - }, - - Routes: []components.NavRoutes{ - {Name: "Dashboard", Path: "/", Icon: components.IconDashboard()}, - {Name: "Issues", Path: "/", Icon: components.IconIssues()}, - {Name: "Agile Board", Path: "/board", Icon: components.IconAgileBoard()}, - {Name: "Backlog", Path: "/backlog", Icon: components.IconBacklog()}, - {Name: "Reports", Path: "/reports", Icon: components.IconReports()}, - {Name: "Settings", Path: "/settings", Icon: components.IconSettings()}, - }, - - Header: components.HeaderProps{ - Title: "LazyPM Dashboard", - NavbarCenter: base.Input(base.InputProps{ - Name: "search-issue", - Type: "search", - Placeholder: "Search issues...", - Class: "", - Attrs: templ.Attributes{ - "class": "text-base-content", - }, - }), - NavbarEnd: components.Status(), - }, +type BaseLayoutProps struct { + SearchQuery string } -templ BaseLayout() { - @components.Layout(baseLayout) { +func buildLayoutProps(searchQuery string) components.LayoutProps { + return components.LayoutProps{ + Title: "Beads Test Application", + Description: "A sample application using Beads storage service", + Head: components.HeadProps{ + Links: []components.Link{ + {Href: "/assets/css/styles.css", Rel: "preload", As: "style"}, + {Href: "/assets/css/styles.css", Rel: "stylesheet"}, + {Href: "/assets/manifest.json", Rel: "manifest"}, + }, + Scripts: []components.Script{ + {Src: "/assets/js/htmx.min.js", Defer: true}, + {Src: "/assets/js/ajax.min.js", Defer: true}, + }, + }, + Routes: []components.NavRoutes{ + {Name: "Dashboard", Path: "/", Icon: components.IconDashboard()}, + {Name: "Issues", Path: "/", Icon: components.IconIssues()}, + {Name: "Agile Board", Path: "/board", Icon: components.IconAgileBoard()}, + {Name: "Backlog", Path: "/backlog", Icon: components.IconBacklog()}, + {Name: "Reports", Path: "/reports", Icon: components.IconReports()}, + {Name: "Settings", Path: "/settings", Icon: components.IconSettings()}, + }, + Header: components.HeaderProps{ + Title: "LazyPM Dashboard", + NavbarCenter: components.SearchForm(searchQuery), + NavbarEnd: components.Status(), + }, + } +} + +templ BaseLayout(props BaseLayoutProps) { + @components.Layout(buildLayoutProps(props.SearchQuery)) { { children... } } } diff --git a/pkg/web/routes/layout_templ.go b/pkg/web/routes/layout_templ.go index d6a5555..322dacb 100644 --- a/pkg/web/routes/layout_templ.go +++ b/pkg/web/routes/layout_templ.go @@ -9,48 +9,43 @@ import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" import "github.com/LazyBachelor/LazyPM/pkg/web/components" -import "github.com/LazyBachelor/LazyPM/pkg/web/components/base" -var baseLayout = components.LayoutProps{ - Title: "Beads Test Application", - Description: "A sample application using Beads storage service", - Head: components.HeadProps{ - Links: []components.Link{ - {Href: "/assets/css/styles.css", Rel: "preload", As: "style"}, - {Href: "/assets/css/styles.css", Rel: "stylesheet"}, - {Href: "/assets/manifest.json", Rel: "manifest"}, - }, - Scripts: []components.Script{ - {Src: "/assets/js/htmx.min.js", Defer: true}, - {Src: "/assets/js/ajax.min.js", Defer: true}, - }, - }, - - Routes: []components.NavRoutes{ - {Name: "Dashboard", Path: "/", Icon: components.IconDashboard()}, - {Name: "Issues", Path: "/", Icon: components.IconIssues()}, - {Name: "Agile Board", Path: "/board", Icon: components.IconAgileBoard()}, - {Name: "Backlog", Path: "/backlog", Icon: components.IconBacklog()}, - {Name: "Reports", Path: "/reports", Icon: components.IconReports()}, - {Name: "Settings", Path: "/settings", Icon: components.IconSettings()}, - }, - - Header: components.HeaderProps{ - Title: "LazyPM Dashboard", - NavbarCenter: base.Input(base.InputProps{ - Name: "search-issue", - Type: "search", - Placeholder: "Search issues...", - Class: "", - Attrs: templ.Attributes{ - "class": "text-base-content", - }, - }), - NavbarEnd: components.Status(), - }, +type BaseLayoutProps struct { + SearchQuery string } -func BaseLayout() templ.Component { +func buildLayoutProps(searchQuery string) components.LayoutProps { + return components.LayoutProps{ + Title: "Beads Test Application", + Description: "A sample application using Beads storage service", + Head: components.HeadProps{ + Links: []components.Link{ + {Href: "/assets/css/styles.css", Rel: "preload", As: "style"}, + {Href: "/assets/css/styles.css", Rel: "stylesheet"}, + {Href: "/assets/manifest.json", Rel: "manifest"}, + }, + Scripts: []components.Script{ + {Src: "/assets/js/htmx.min.js", Defer: true}, + {Src: "/assets/js/ajax.min.js", Defer: true}, + }, + }, + Routes: []components.NavRoutes{ + {Name: "Dashboard", Path: "/", Icon: components.IconDashboard()}, + {Name: "Issues", Path: "/", Icon: components.IconIssues()}, + {Name: "Agile Board", Path: "/board", Icon: components.IconAgileBoard()}, + {Name: "Backlog", Path: "/backlog", Icon: components.IconBacklog()}, + {Name: "Reports", Path: "/reports", Icon: components.IconReports()}, + {Name: "Settings", Path: "/settings", Icon: components.IconSettings()}, + }, + Header: components.HeaderProps{ + Title: "LazyPM Dashboard", + NavbarCenter: components.SearchForm(searchQuery), + NavbarEnd: components.Status(), + }, + } +} + +func BaseLayout(props BaseLayoutProps) 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 { @@ -89,7 +84,7 @@ func BaseLayout() templ.Component { } return nil }) - templ_7745c5c3_Err = components.Layout(baseLayout).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = components.Layout(buildLayoutProps(props.SearchQuery)).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err }