314 lines
10 KiB
Plaintext

package component
import (
"bytes"
"context"
"forge.cadoles.com/wpetit/clearcase/internal/core/model"
"forge.cadoles.com/wpetit/clearcase/internal/http/form"
common "forge.cadoles.com/wpetit/clearcase/internal/http/handler/webui/common/component"
"github.com/pkg/errors"
"github.com/yuin/goldmark"
"log/slog"
"strings"
)
type PullRequestPageVModel struct {
PullRequestURL string
SummaryForm *form.Form
PullRequestForm *form.Form
PullRequestTips string
Projects []*model.Project
PullRequests []*model.PullRequest
SelectedProjectID string
SelectedPullRequestID string
}
const summaryPlaceholder = `
Décrivez rapidement les modifications apportées par la PR, ClearCase utilisera le modèle de PR présent dans le dépôt (ou un modèle par défaut) afin de générer une version mise en forme et complétée.
Afin de fournir plus d'information de contexte au LLM, vous pouvez faire référence à d'autres tickets du dépôt via un ou plusieurs '#<pr_id>' et/ou des chemins vers des fichiers présents dans celui ci.
`
const bodyPlaceholder = `
Une fois votre PR générée, vous pourrez l'éditer puis la créer directement en cliquant sur le bouton 'Mettre à jour' ci-dessous.
`
const prTemplatePlaceholder = `
Vous pouvez surcharger le modèle de PR fourni par le projet en remplissant ce champ.
`
func NewPullRequestSummaryForm() *form.Form {
return form.New(
form.NewField(
"project",
form.Attrs{},
form.NonEmpty("Ce champ ne doit pas être vide."),
),
form.NewField(
"pullrequest",
form.Attrs{},
form.NonEmpty("Ce champ ne doit pas être vide."),
),
form.NewField(
"summary",
form.Attrs{
"type": "textarea",
"rows": "20",
"placeholder": strings.TrimSpace(summaryPlaceholder),
},
form.NonEmpty("Ce champ ne doit pas être vide."),
),
form.NewField(
"template",
form.Attrs{
"type": "textarea",
"rows": "20",
"placeholder": strings.TrimSpace(prTemplatePlaceholder),
},
),
)
}
func NewPullRequestForm() *form.Form {
return form.New(
form.NewField(
"title",
form.Attrs{
"type": "text",
"placeholder": "Écrivez le résumé de votre demande et cliquez sur 'Générer' pour remplir automatiquement ces champs.",
},
form.NonEmpty("Ce champ ne doit pas être vide."),
),
form.NewField(
"body",
form.Attrs{
"type": "textarea",
"rows": "20",
"placeholder": strings.TrimSpace(bodyPlaceholder),
},
form.NonEmpty("Ce champ ne doit pas être vide."),
),
)
}
templ PullRequestPage(vmodel PullRequestPageVModel) {
@common.AppPage(common.WithPageOptions(
common.WithTitle("Éditer une PR"),
)) {
if vmodel.PullRequestURL != "" {
<article class="message is-primary">
<div class="message-header">
<p>Pull Request modifiée !</p>
<button class="delete" aria-label="delete" hx-on:click="onCloseMessage(this)"></button>
</div>
<div class="message-body">
Votre PR a été mise à jour et est disponible à l'adresse suivante:
<a href={ templ.SafeURL(vmodel.PullRequestURL) } target="_blank"><code>{ vmodel.PullRequestURL }</code></a>.
</div>
</article>
<script type="text/javascript">
function clearSummary(projectId) {
sessionStorage.removeItem(`pr-summary-${projectId}`)
}
function openPR(prUrl) {
window.open(prUrl, "_blank");
}
</script>
@templ.JSFuncCall("clearSummary", vmodel.SelectedProjectID)
@templ.JSFuncCall("openPR", vmodel.PullRequestURL)
}
<div class="columns">
<div class="column is-4">
<form id="summary-form" action={ common.CurrentURL(ctx) } method="put" hx-disabled-elt="textarea, input, select, button" hx-on:htmx:before-send="savePreferred()" hx-indicator="#generation-progress">
<h2 class="title is-size-3">Résumé de la PR</h2>
@common.FormSelect(
vmodel.SummaryForm, "pr-project", "project", "Projet",
common.WithOptions(projectsToOptions(vmodel.Projects)...),
common.WithAttrs(
"hx-get", string(common.CurrentURL(ctx, common.WithoutValues("project", "*"))),
"hx-target", "body",
"hx-push-url", "true",
),
)
@common.FormSelect(
vmodel.SummaryForm, "pr-pullrequest", "pullrequest", "PR",
common.WithOptions(pullRequestsToOptions(vmodel.PullRequests)...),
common.WithAttrs(
"hx-get", string(common.CurrentURL(ctx, common.WithoutValues("pullrequest", "*"))),
"hx-target", "body",
"hx-push-url", "true",
),
)
@common.FormTextarea(
vmodel.SummaryForm, "pr-summary", "summary", "Résumé",
common.WithTextareaAttrs(
"hx-on:change", "onSummaryChange(event)",
),
)
<details class="my-3">
<summary class="is-clickable">Paramètres avancés</summary>
@common.FormTextarea(
vmodel.SummaryForm, "pr-template", "template", "Surcharger le modèle de demande",
common.WithTextareaAttrs(
"hx-on:change", "onPullRequestTemplateChange(event)",
),
)
</details>
<div class="buttons is-right">
<button type="submit" class="button is-info is-large">
<span class="icon">
<i class="fa fa-robot"></i>
</span>
<span>Générer</span>
</button>
</div>
</form>
</div>
<div class="column">
<h2 class="title is-size-3">Votre PR</h2>
<form action={ common.CurrentURL(ctx) } method="post" hx-disabled-elt="textarea, input, select, button" hx-indicator="#generation-progress">
@common.FormField(vmodel.PullRequestForm, "pr-title", "title", "Titre")
@common.FormTextarea(vmodel.PullRequestForm, "pr-body", "body", "Corps")
<div class="buttons is-right">
<button type="submit" class="button is-info is-large">
<span class="icon">
<i class="fa fa-rocket"></i>
</span>
<span>Mettre à jour</span>
</button>
</div>
</form>
</div>
</div>
<progress id="generation-progress" class="htmx-indicator progress"></progress>
if vmodel.PullRequestTips != "" {
{{ html := markdownToHTML(ctx, vmodel.PullRequestTips) }}
if html != "" {
<article class="message is-info mt-5">
<div class="message-header">
<p><span class="icon"><i class="fa fa-lightbulb"></i></span>Questionnements</p>
<button class="delete" aria-label="delete" hx-on:click="onCloseMessage(this)"></button>
</div>
<div class="message-body">
<div class="content">
<p>Utilisez ces quelques questions pour réfléchir aux éléments d'informations nécessaire à la bonne rédaction de votre PR:</p>
@templ.Raw(html)
</div>
</div>
</article>
}
}
<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("pr-project").value;
sessionStorage.setItem(`pr-summary-${projectId}`, summary);
}
function onPullRequestTemplateChange(evt) {
const prTemplate = evt.currentTarget.value;
const pullRequestId = document.getElementById("pr-pullrequest").value;
localStorage.setItem(`pr-template-${pullRequestId}`, prTemplate);
}
function savePreferred() {
savePreferredProject()
savePreferredPullRequest()
}
function savePreferredProject() {
const projectId = document.getElementById("pr-project").value;
localStorage.setItem(`preferred-project`, projectId);
}
function restorePreferredProject() {
const preferredProject = localStorage.getItem(`preferred-project`);
if (!preferredProject) return;
const projectElement = document.getElementById("pr-project");
if (!projectElement) return;
if (preferredProject === projectElement.value) return;
projectElement.value = preferredProject;
projectElement.dispatchEvent(new Event('change'));
}
function savePreferredPullRequest() {
const pullRequestId = document.getElementById("pr-pullrequest").value;
localStorage.setItem(`preferred-pullrequest`, pullRequestId);
}
function restorePreferredPullRequest() {
const preferredPullRequest = localStorage.getItem(`preferred-pullrequest`);
if (!preferredPullRequest) return;
const pullRequestElement = document.getElementById("pr-pullrequest");
if (!pullRequestElement) return;
if (preferredPullRequest === pullRequestElement.value) return;
pullRequestElement.value = preferredPullRequest;
pullRequestElement.dispatchEvent(new Event('change'));
}
function restoreLastSummary() {
const summaryTextarea = document.getElementById("pr-summary");
if (!summaryTextarea) return;
const summary = summaryTextarea.value;
if (summary !== "") return;
const projectId = document.getElementById("pr-project").value;
if (!projectId) return;
const savedSummary = sessionStorage.getItem(`pr-summary-${projectId}`);
if (!savedSummary) return;
summaryTextarea.value = savedSummary;
}
function restorePullRequestTemplate() {
const prTemplateTextarea = document.getElementById("pr-template");
if (!prTemplateTextarea) return;
const prTemplate = prTemplateTextarea.value;
if (prTemplate !== "") return;
const projectId = document.getElementById("pr-project").value;
if (!projectId) return;
const savedprTemplate = localStorage.getItem(`pr-template-${projectId}`);
if (!savedprTemplate) return;
prTemplateTextarea.value = savedprTemplate;
}
htmx.onLoad(function(){
restoreLastSummary();
restorePreferredProject();
restorePreferredPullRequest();
restorePullRequestTemplate();
})
</script>
}
}
func projectsToOptions(projects []*model.Project) []string {
options := make([]string, 0, len(projects))
options = append(options, "", "")
for _, p := range projects {
options = append(options, p.Name, p.ID)
}
return options
}
func pullRequestsToOptions(pullRequests []*model.PullRequest) []string {
options := make([]string, 0, len(pullRequests))
options = append(options, "", "")
for _, pr := range pullRequests {
options = append(options, pr.Title, pr.ID)
}
return options
}
func markdownToHTML(ctx context.Context, text string) string {
var buff bytes.Buffer
if err := goldmark.Convert([]byte(text), &buff); err != nil {
slog.ErrorContext(ctx, "could not convert markdown to html", slog.Any("error", errors.WithStack(err)))
return ""
}
return buff.String()
}