feat: issue creation

This commit is contained in:
wpetit 2025-02-22 18:01:45 +01:00
parent 5b148cb4bb
commit 2d83ca9d20
10 changed files with 258 additions and 40 deletions

4
go.mod
View File

@ -6,7 +6,7 @@ toolchain go1.23.6
require (
code.gitea.io/sdk/gitea v0.20.0
github.com/a-h/templ v0.3.819
github.com/a-h/templ v0.3.833
github.com/bornholm/genai v0.0.0-20250222092500-1076426da67c
github.com/caarlos0/env/v11 v11.2.2
github.com/davecgh/go-spew v1.1.1
@ -45,7 +45,7 @@ require (
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/oauth2 v0.17.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/tools v0.24.0 // indirect
google.golang.org/appengine v1.6.8 // indirect

6
go.sum
View File

@ -12,6 +12,8 @@ github.com/RealAlexandreAI/json-repair v0.0.14 h1:4kTqotVonDVTio5n2yweRUELVcNe2x
github.com/RealAlexandreAI/json-repair v0.0.14/go.mod h1:GKJi5borR78O8c7HCVbgqjhoiVibZ6hJldxbc6dGrAI=
github.com/a-h/templ v0.3.819 h1:KDJ5jTFN15FyJnmSmo2gNirIqt7hfvBD2VXVDTySckM=
github.com/a-h/templ v0.3.819/go.mod h1:iDJKJktpttVKdWoTkRNNLcllRI+BlpopJc+8au3gOUo=
github.com/a-h/templ v0.3.833 h1:L/KOk/0VvVTBegtE0fp2RJQiBm7/52Zxv5fqlEHiQUU=
github.com/a-h/templ v0.3.833/go.mod h1:cAu4AiZhtJfBjMY0HASlyzvkrtjnHWPeEsyGK2YYmfk=
github.com/bornholm/genai v0.0.0-20250222092500-1076426da67c h1:bI0ebsgO1/7Jx6+ZQdDF/I6tTZxyB5hODYz7x/XxwK8=
github.com/bornholm/genai v0.0.0-20250222092500-1076426da67c/go.mod h1:MnuvwSsBEWv/joeK/WgUyfZfOLcLTpd81NJdWoRpRfI=
github.com/caarlos0/env/v11 v11.2.2 h1:95fApNrUyueipoZN/EhA8mMxiNxrBwDa+oAZrMWl3Kg=
@ -95,8 +97,8 @@ golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -18,8 +18,26 @@ type Forge struct {
}
// CreateIssue implements port.Forge.
func (f *Forge) CreateIssue(ctx context.Context, projectID string, title string, content string) error {
return nil
func (f *Forge) CreateIssue(ctx context.Context, rawProjectID string, title string, body string) (string, error) {
projectID, err := strconv.ParseInt(rawProjectID, 10, 64)
if err != nil {
return "", errors.WithStack(err)
}
project, _, err := f.client.GetRepoByID(projectID)
if err != nil {
return "", errors.WithStack(err)
}
issue, _, err := f.client.CreateIssue(project.Owner.UserName, project.Name, gitea.CreateIssueOption{
Title: title,
Body: body,
})
if err != nil {
return "", errors.WithStack(err)
}
return issue.HTMLURL, nil
}
// GetIssueTemplate implements port.Forge.

View File

@ -13,7 +13,7 @@ var (
type Forge interface {
GetProjects(ctx context.Context) ([]*model.Project, error)
CreateIssue(ctx context.Context, projectID string, title string, content string) error
CreateIssue(ctx context.Context, projectID string, title string, body string) (string, error)
GetIssues(ctx context.Context, projectID string, issueIDs ...string) ([]*model.Issue, error)
GetIssueTemplate(ctx context.Context, projectID string) (string, error)
}

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log/slog"
"strings"
"time"
_ "embed"
@ -39,6 +40,20 @@ type IssueManager struct {
projectCache *cache.Cache[[]*model.Project]
}
func (m *IssueManager) CreateIssue(ctx context.Context, user *model.User, projectID string, title string, body string) (string, error) {
forge, err := m.getUserForge(ctx, user)
if err != nil {
return "", errors.WithStack(err)
}
issueURL, err := forge.CreateIssue(ctx, projectID, title, body)
if err != nil {
return "", errors.WithStack(err)
}
return issueURL, nil
}
func (m *IssueManager) GetUserProjects(ctx context.Context, user *model.User) ([]*model.Project, error) {
cacheKey := fmt.Sprintf("%s/%s", user.Provider, user.ID)
@ -62,15 +77,15 @@ func (m *IssueManager) GetUserProjects(ctx context.Context, user *model.User) ([
return projects, nil
}
func (m *IssueManager) GenerateIssue(ctx context.Context, user *model.User, projectID string, issueSummary string) (string, error) {
func (m *IssueManager) GenerateIssue(ctx context.Context, user *model.User, projectID string, issueSummary string) (string, string, error) {
systemPrompt, err := m.getIssueSystemPrompt(ctx, user, projectID)
if err != nil {
return "", errors.WithStack(err)
return "", "", errors.WithStack(err)
}
userPrompt, err := m.getIssueUserPrompt(ctx, user, projectID, issueSummary)
if err != nil {
return "", errors.WithStack(err)
return "", "", errors.WithStack(err)
}
messages := []llm.Message{
@ -80,10 +95,22 @@ func (m *IssueManager) GenerateIssue(ctx context.Context, user *model.User, proj
res, err := m.llmClient.ChatCompletion(ctx, llm.WithMessages(messages...))
if err != nil {
return "", errors.WithStack(err)
return "", "", errors.WithStack(err)
}
return res.Message().Content(), nil
body := res.Message().Content()
messages = append(messages, res.Message())
messages = append(messages, llm.NewMessage(llm.RoleUser, "Generate a title for this issue. Keep it descriptive, simple and short. Do not write anything else."))
res, err = m.llmClient.ChatCompletion(ctx, llm.WithMessages(messages...))
if err != nil {
return "", "", errors.WithStack(err)
}
title := toTitle(res.Message().Content())
return title, body, nil
}
func (m *IssueManager) getIssueSystemPrompt(ctx context.Context, user *model.User, projectID string) (string, error) {
@ -156,3 +183,8 @@ func NewIssueManager(llmClient llm.Client, forgeFactories ...ForgeFactory) *Issu
projectCache: cache.New[[]*model.Project](time.Minute*5, (time.Minute*5)/2),
}
}
func toTitle(str string) string {
str = strings.ToLower(str)
return strings.ToUpper(string(str[0])) + str[1:]
}

View File

@ -9,6 +9,7 @@ You are an expert software developer with extensive experience in writing clear
- Expected behavior.
- Actual behavior.
- Any relevant error messages or logs.
- Always use the user prompt context main language.
2. **Additional Context**:
- Include any other relevant information that might help in understanding or resolving the issue/request.

View File

@ -7,6 +7,7 @@ import (
)
type IssuePageVModel struct {
IssueURL string
SummaryForm *form.Form
IssueForm *form.Form
Projects []*model.Project
@ -42,7 +43,7 @@ func NewIssueForm() *form.Form {
form.NonEmpty("Ce champs ne doit pas être vide."),
),
form.NewField(
"content",
"body",
form.Attrs{
"type": "textarea",
"rows": "20",
@ -59,11 +60,33 @@ templ IssuePage(vmodel IssuePageVModel) {
<div class="buttons is-right">
<a class="button is-medium" href={ common.BaseURL(ctx, common.WithPath("/auth/logout")) }>Se déconnecter</a>
</div>
if vmodel.IssueURL != "" {
<article class="message is-primary">
<div class="message-header">
<p>Demande créée !</p>
<button class="delete" aria-label="delete" hx-on:click="onCloseMessage(this)"></button>
</div>
<div class="message-body">
Votre demande a été créée et est disponible à l'adresse suivante:
<a href={ templ.SafeURL(vmodel.IssueURL) } target="_blank"><code>{ vmodel.IssueURL }</code></a>.
</div>
</article>
<script type="text/javascript">
function clearSummary(projectId) {
sessionStorage.removeItem(`summary-${projectId}`)
}
function openIssue(issueUrl) {
window.open(issueUrl, "_blank");
}
</script>
@templ.JSFuncCall("clearSummary", vmodel.SelectedProjectID)
@templ.JSFuncCall("openIssue", vmodel.IssueURL)
}
<progress id="generation-progress" class="htmx-indicator progress"></progress>
<div class="columns">
<div class="column is-4">
<form id="summary-form" action={ common.CurrentURL(ctx) } method="post" hx-disabled-elt="#summary-form textarea, #summary-form select, #summary-form button" hx-indicator="#generation-progress">
<form id="summary-form" action={ common.CurrentURL(ctx) } method="put" hx-disabled-elt="#summary-form textarea, #summary-form select, #summary-form button" hx-indicator="#generation-progress">
<h2 class="title is-size-2">Résumé de la demande</h2>
<progress id="generation-progress" class="htmx-indicator progress"></progress>
@common.FormSelect(
vmodel.SummaryForm, "issue-project", "project", "Projet",
common.WithOptions(projectsToOptions(vmodel.Projects)...),
@ -71,6 +94,7 @@ templ IssuePage(vmodel IssuePageVModel) {
"hx-get", string(common.CurrentURL(ctx, common.WithoutValues("project", "*"))),
"hx-target", "body",
"hx-push-url", "true",
"hx-on:htmx:beforeSend", "onProjectChange(event)",
),
)
@common.FormTextarea(
@ -84,35 +108,58 @@ templ IssuePage(vmodel IssuePageVModel) {
<span class="icon">
<i class="fa fa-robot"></i>
</span>
<span>Génerer le ticket</span>
<span>Générer le ticket</span>
</button>
</div>
</form>
</div>
<div class="column">
<h2 class="title is-size-2">Votre demande</h2>
@common.FormField(vmodel.IssueForm, "issue-content", "title", "Titre")
@common.FormTextarea(vmodel.IssueForm, "issue-content", "content", "Contenu")
<div class="buttons is-right">
<button disabled type="submit" class="button is-primary is-large">
<span class="icon">
<i class="fa fa-rocket"></i>
</span>
<span>Créer le ticket</span>
</button>
</div>
<form action={ common.CurrentURL(ctx) } method="post" hx-disabled-elt="textarea, input, select, button" hx-indicator="#generation-progress">
@common.FormField(vmodel.IssueForm, "issue-title", "title", "Titre")
@common.FormTextarea(vmodel.IssueForm, "issue-body", "body", "Corps")
<div class="buttons is-right">
<button type="submit" class="button is-primary is-large">
<span class="icon">
<i class="fa fa-rocket"></i>
</span>
<span>Créer le ticket</span>
</button>
</div>
</form>
</div>
</div>
</section>
</div>
<script type="text/javascript">
function onCloseMessage(closeElement) {
closeElement.closest('.message').style.display = 'none';
}
function onSummaryChange(evt) {
const summary = evt.currentTarget.value;
const projectId = document.getElementById("issue-project").value;
sessionStorage.setItem(`summary-${projectId}`, summary);
}
htmx.onLoad(function(){
function onProjectChange(evt) {
const projectId = evt.currentTarget.value;
localStorage.setItem(`preferred-project`, projectId);
}
function restorePreferredProject() {
const preferredProject = localStorage.getItem(`preferred-project`);
if (!preferredProject) return;
const projectElement = document.getElementById("issue-project");
if (!projectElement) return;
if (preferredProject === projectElement.value) return;
projectElement.value = preferredProject;
projectElement.dispatchEvent(new Event('change'));
}
function restoreLastSummary() {
const summaryTextarea = document.getElementById("issue-summary");
if (!summaryTextarea) return;
const summary = summaryTextarea.value;
if (summary !== "") return;
const projectId = document.getElementById("issue-project").value;
@ -120,6 +167,11 @@ templ IssuePage(vmodel IssuePageVModel) {
const savedSummary = sessionStorage.getItem(`summary-${projectId}`);
if (!savedSummary) return;
summaryTextarea.value = savedSummary;
}
htmx.onLoad(function(){
restoreLastSummary();
restorePreferredProject();
})
</script>
}

View File

@ -15,6 +15,7 @@ import (
)
type IssuePageVModel struct {
IssueURL string
SummaryForm *form.Form
IssueForm *form.Form
Projects []*model.Project
@ -50,7 +51,7 @@ func NewIssueForm() *form.Form {
form.NonEmpty("Ce champs ne doit pas être vide."),
),
form.NewField(
"content",
"body",
form.Attrs{
"type": "textarea",
"rows": "20",
@ -102,16 +103,60 @@ func IssuePage(vmodel IssuePageVModel) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">Se déconnecter</a></div><div class=\"columns\"><div class=\"column is-4\"><form id=\"summary-form\" action=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">Se déconnecter</a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 templ.SafeURL = common.CurrentURL(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4)))
if vmodel.IssueURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<article class=\"message is-primary\"><div class=\"message-header\"><p>Demande créée !</p><button class=\"delete\" aria-label=\"delete\" hx-on:click=\"onCloseMessage(this)\"></button></div><div class=\"message-body\">Votre demande a été créée et est disponible à l'adresse suivante: <a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 templ.SafeURL = templ.SafeURL(vmodel.IssueURL)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" target=\"_blank\"><code>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(vmodel.IssueURL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/http/handler/webui/issue/component/issue_page.templ`, Line: 71, Col: 89}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</code></a>.</div></article><script type=\"text/javascript\">\n\t\t\t\t\t\tfunction clearSummary(projectId) {\n\t\t\t\t\t\t\tsessionStorage.removeItem(`summary-${projectId}`)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfunction openIssue(issueUrl) {\n\t\t\t\t\t\t\twindow.open(issueUrl, \"_blank\");\n\t\t\t\t\t\t}\n\t\t\t\t\t</script> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.JSFuncCall("clearSummary", vmodel.SelectedProjectID).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.JSFuncCall("openIssue", vmodel.IssueURL).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<progress id=\"generation-progress\" class=\"htmx-indicator progress\"></progress><div class=\"columns\"><div class=\"column is-4\"><form id=\"summary-form\" action=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" method=\"post\" hx-disabled-elt=\"#summary-form textarea, #summary-form select, #summary-form button\" hx-indicator=\"#generation-progress\"><h2 class=\"title is-size-2\">Résumé de la demande</h2><progress id=\"generation-progress\" class=\"htmx-indicator progress\"></progress>")
var templ_7745c5c3_Var6 templ.SafeURL = common.CurrentURL(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var6)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" method=\"put\" hx-disabled-elt=\"#summary-form textarea, #summary-form select, #summary-form button\" hx-indicator=\"#generation-progress\"><h2 class=\"title is-size-2\">Résumé de la demande</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -122,6 +167,7 @@ func IssuePage(vmodel IssuePageVModel) templ.Component {
"hx-get", string(common.CurrentURL(ctx, common.WithoutValues("project", "*"))),
"hx-target", "body",
"hx-push-url", "true",
"hx-on:htmx:beforeSend", "onProjectChange(event)",
),
).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
@ -136,19 +182,28 @@ func IssuePage(vmodel IssuePageVModel) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"buttons is-right\"><button type=\"submit\" class=\"button is-primary is-large\"><span class=\"icon\"><i class=\"fa fa-robot\"></i></span> <span>Génerer le ticket</span></button></div></form></div><div class=\"column\"><h2 class=\"title is-size-2\">Votre demande</h2>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"buttons is-right\"><button type=\"submit\" class=\"button is-primary is-large\"><span class=\"icon\"><i class=\"fa fa-robot\"></i></span> <span>Générer le ticket</span></button></div></form></div><div class=\"column\"><h2 class=\"title is-size-2\">Votre demande</h2><form action=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = common.FormField(vmodel.IssueForm, "issue-content", "title", "Titre").Render(ctx, templ_7745c5c3_Buffer)
var templ_7745c5c3_Var7 templ.SafeURL = common.CurrentURL(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var7)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = common.FormTextarea(vmodel.IssueForm, "issue-content", "content", "Contenu").Render(ctx, templ_7745c5c3_Buffer)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" method=\"post\" hx-disabled-elt=\"textarea, input, select, button\" hx-indicator=\"#generation-progress\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"buttons is-right\"><button disabled type=\"submit\" class=\"button is-primary is-large\"><span class=\"icon\"><i class=\"fa fa-rocket\"></i></span> <span>Créer le ticket</span></button></div></div></div></section></div><script type=\"text/javascript\">\n\t\tfunction onSummaryChange(evt) {\n\t\t\tconst summary = evt.currentTarget.value;\n\t\t\tconst projectId = document.getElementById(\"issue-project\").value;\n\t\t\tsessionStorage.setItem(`summary-${projectId}`, summary);\n\t\t}\n\t\thtmx.onLoad(function(){\n\t\t\tconst summaryTextarea = document.getElementById(\"issue-summary\");\n const summary = summaryTextarea.value;\n\t\t\tif (summary !== \"\") return;\n\t\t\tconst projectId = document.getElementById(\"issue-project\").value;\n\t\t\tif (!projectId) return;\n\t\t\tconst savedSummary = sessionStorage.getItem(`summary-${projectId}`);\n\t\t\tif (!savedSummary) return;\n\t\t\tsummaryTextarea.value = savedSummary;\n })\n\t\t</script>")
templ_7745c5c3_Err = common.FormField(vmodel.IssueForm, "issue-title", "title", "Titre").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = common.FormTextarea(vmodel.IssueForm, "issue-body", "body", "Corps").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"buttons is-right\"><button type=\"submit\" class=\"button is-primary is-large\"><span class=\"icon\"><i class=\"fa fa-rocket\"></i></span> <span>Créer le ticket</span></button></div></form></div></div></section></div><script type=\"text/javascript\">\n\t\tfunction onCloseMessage(closeElement) {\n\t\t\tcloseElement.closest('.message').style.display = 'none';\n\t\t}\n\n\t\tfunction onSummaryChange(evt) {\n\t\t\tconst summary = evt.currentTarget.value;\n\t\t\tconst projectId = document.getElementById(\"issue-project\").value;\n\t\t\tsessionStorage.setItem(`summary-${projectId}`, summary);\n\t\t}\n\n\t\tfunction onProjectChange(evt) {\n\t\t\tconst projectId = evt.currentTarget.value;\n\t\t\tlocalStorage.setItem(`preferred-project`, projectId);\n\t\t}\n\n\t\tfunction restorePreferredProject() {\n\t\t\tconst preferredProject = localStorage.getItem(`preferred-project`);\n\t\t\tif (!preferredProject) return;\n\t\t\tconst projectElement = document.getElementById(\"issue-project\");\n\t\t\tif (!projectElement) return;\n\t\t\tif (preferredProject === projectElement.value) return;\n\t\t\tprojectElement.value = preferredProject;\n\t\t\tprojectElement.dispatchEvent(new Event('change'));\n\t\t}\n\n\t\tfunction restoreLastSummary() {\n\t\t\tconst summaryTextarea = document.getElementById(\"issue-summary\");\n\t\t\tif (!summaryTextarea) return;\n const summary = summaryTextarea.value;\n\t\t\tif (summary !== \"\") return;\n\t\t\tconst projectId = document.getElementById(\"issue-project\").value;\n\t\t\tif (!projectId) return;\n\t\t\tconst savedSummary = sessionStorage.getItem(`summary-${projectId}`);\n\t\t\tif (!savedSummary) return;\n\t\t\tsummaryTextarea.value = savedSummary;\n\t\t}\n\n\t\thtmx.onLoad(function(){\n\t\t\trestoreLastSummary();\n\t\t\trestorePreferredProject();\n })\n\t\t</script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View File

@ -23,7 +23,8 @@ func NewHandler(issueManager *service.IssueManager) *Handler {
}
h.mux.HandleFunc("GET /", h.getIssuePage)
h.mux.HandleFunc("POST /", h.handleIssueSummary)
h.mux.HandleFunc("PUT /", h.handleIssueSummaryForm)
h.mux.HandleFunc("POST /", h.handleIssueForm)
return h
}

View File

@ -68,7 +68,7 @@ func (h *Handler) fillIssuePageSelectedProject(ctx context.Context, vmodel *comp
return nil
}
func (h *Handler) handleIssueSummary(w http.ResponseWriter, r *http.Request) {
func (h *Handler) handleIssueSummaryForm(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
issueSummaryForm := component.NewIssueSummaryForm()
@ -105,13 +105,70 @@ func (h *Handler) handleIssueSummary(w http.ResponseWriter, r *http.Request) {
return
}
issueContent, err := h.issueManager.GenerateIssue(ctx, httpCtx.User(ctx), projectID, summary)
issueTitle, issueBody, err := h.issueManager.GenerateIssue(ctx, httpCtx.User(ctx), projectID, summary)
if err != nil {
h.handleError(w, r, errors.WithStack(err))
return
}
vmodel.IssueForm.Field("content").Set("value", issueContent)
vmodel.IssueForm.Field("title").Set("value", issueTitle)
vmodel.IssueForm.Field("body").Set("value", issueBody)
page := component.IssuePage(*vmodel)
templ.Handler(page).ServeHTTP(w, r)
}
func (h *Handler) handleIssueForm(w http.ResponseWriter, r *http.Request) {
issueForm := component.NewIssueForm()
if err := issueForm.Handle(r); err != nil {
h.handleError(w, r, errors.WithStack(err))
return
}
vmodel, err := h.fillIssuePageVModel(r)
if err != nil {
h.handleError(w, r, errors.WithStack(err))
return
}
if errs := issueForm.Validate(); errs != nil {
vmodel.IssueForm = issueForm
page := component.IssuePage(*vmodel)
templ.Handler(page).ServeHTTP(w, r)
return
}
projectID := r.URL.Query().Get("project")
title, err := form.FormFieldAttr[string](issueForm, "title", "value")
if err != nil {
h.handleError(w, r, errors.WithStack(err))
return
}
body, err := form.FormFieldAttr[string](issueForm, "body", "value")
if err != nil {
h.handleError(w, r, errors.WithStack(err))
return
}
ctx := r.Context()
user := httpCtx.User(ctx)
issueURL, err := h.issueManager.CreateIssue(ctx, user, projectID, title, body)
if err != nil {
h.handleError(w, r, errors.WithStack(err))
return
}
vmodel.IssueURL = issueURL
vmodel.SummaryForm.Field("summary").Set("value", "")
vmodel.IssueForm.Field("title").Set("value", "")
vmodel.IssueForm.Field("body").Set("value", "")
page := component.IssuePage(*vmodel)
templ.Handler(page).ServeHTTP(w, r)