fix(09): refresh etape counts on task create

This commit is contained in:
Arthur Belleville 2026-05-15 22:59:01 +02:00
parent cf07c29ae5
commit 0c95049447
No known key found for this signature in database
5 changed files with 79 additions and 5 deletions

View file

@ -302,10 +302,14 @@ func TaskCreateHandler(deps TasksDeps) http.HandlerFunc {
// HTMX: set retarget/reswap headers and return combined card+OOB fragment.
if r.Header.Get("HX-Request") == "true" {
_, refreshedEtapes, refreshedCounts, refreshedFilter, ok := loadTasksTabData(w, r, deps.Queries, tablo)
if !ok {
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("HX-Reswap", "beforeend")
w.Header().Set("HX-Retarget", "#column-"+string(status))
_ = templates.TaskCardOOB(status, task, tablo.ID, csrf.Token(r), filter).Render(ctx, w)
_ = templates.TaskCardOOB(status, task, tablo.ID, csrf.Token(r), refreshedFilter, refreshedEtapes, refreshedCounts).Render(ctx, w)
return
}
http.Redirect(w, r, "/tablos/"+tablo.ID.String(), http.StatusSeeOther)

View file

@ -167,6 +167,69 @@ func TestTaskCreate(t *testing.T) {
}
}
func TestTaskCreateRefreshesEtapeCounts(t *testing.T) {
pool, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
q := sqlc.New(pool)
store := auth.NewStore(q)
router := newTaskTestRouter(q, store)
user := preInsertUser(t, ctx, q, "taskcreatecount@example.com", "correct-horse-12")
tablo, err := q.InsertTablo(ctx, sqlc.InsertTabloParams{
UserID: user.ID,
Title: "Task Count Tablo",
Description: pgtype.Text{Valid: false},
Color: pgtype.Text{Valid: false},
})
if err != nil {
t.Fatalf("InsertTablo: %v", err)
}
etape, err := q.InsertEtape(ctx, sqlc.InsertEtapeParams{
TabloID: tablo.ID,
Title: "Design",
Description: pgtype.Text{Valid: false},
Position: 100,
})
if err != nil {
t.Fatalf("InsertEtape: %v", err)
}
cookieVal, _, err := store.Create(ctx, user.ID)
if err != nil {
t.Fatalf("store.Create: %v", err)
}
sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}
csrfToken, csrfCookies := getCSRFToken(t, router, "/tablos/"+tablo.ID.String()+"/tasks?etape="+etape.ID.String(), []*http.Cookie{sessionCookie})
form := url.Values{
"title": {"Refresh Count"},
"status": {"todo"},
"etape_id": {etape.ID.String()},
"_csrf": {csrfToken},
}
req := httptest.NewRequest(http.MethodPost, "/tablos/"+tablo.ID.String()+"/tasks?etape="+etape.ID.String(), strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("HX-Request", "true")
for _, c := range csrfCookies {
req.AddCookie(c)
}
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("POST task create status = %d; want 200", rec.Code)
}
body := rec.Body.String()
if !strings.Contains(body, `id="etape-strip"`) || !strings.Contains(body, `hx-swap-oob="outerHTML"`) {
t.Fatalf("task create response did not include OOB etape strip refresh; body: %.800s", body)
}
if !strings.Contains(body, "Design") || !strings.Contains(body, ">1<") {
t.Fatalf("task create response missing updated etape count; body: %.800s", body)
}
}
// ---- TestTaskCreateValidation (TASK-02) ----
// TestTaskCreateValidation verifies that POST /tablos/{id}/tasks with an empty

View file

@ -8,8 +8,14 @@ import (
"github.com/google/uuid"
)
templ EtapeStrip(tabloID uuid.UUID, etapes []sqlc.Etape, counts EtapeTaskCounts, filter EtapeFilter, csrfToken string) {
<div id="etape-strip" class="mb-4 space-y-3">
templ EtapeStrip(tabloID uuid.UUID, etapes []sqlc.Etape, counts EtapeTaskCounts, filter EtapeFilter, csrfToken string, oob bool) {
<div
id="etape-strip"
class="mb-4 space-y-3"
if oob {
hx-swap-oob="outerHTML"
}
>
<div class="flex items-center gap-2 overflow-x-auto pb-1">
<a
href={ templ.SafeURL("/tablos/" + tabloID.String() + "/tasks") }

View file

@ -254,7 +254,7 @@ templ TabloOverviewTabFragment(tablo sqlc.Tablo, csrfToken string) {
// Lives in tablos.templ (tablo-level concern) per plan D-07.
templ TasksTabFragment(tablo sqlc.Tablo, tasks []sqlc.Task, etapes []sqlc.Etape, counts EtapeTaskCounts, filter EtapeFilter, csrfToken string) {
<div id="tasks-tab">
@EtapeStrip(tablo.ID, etapes, counts, filter, csrfToken)
@EtapeStrip(tablo.ID, etapes, counts, filter, csrfToken, false)
@KanbanBoard(tablo.ID, csrfToken, tasks, filter)
</div>
}

View file

@ -390,9 +390,10 @@ templ TaskCardGone(taskID uuid.UUID) {
// slot to AddTaskTrigger. Used by TaskCreateHandler to perform both operations
// in a single HTMX response.
// D-08/UI-SPEC §2: OOB swap resets #add-task-slot-{status} after create.
templ TaskCardOOB(status sqlc.TaskStatus, task sqlc.Task, tabloID uuid.UUID, csrfToken string, filter EtapeFilter) {
templ TaskCardOOB(status sqlc.TaskStatus, task sqlc.Task, tabloID uuid.UUID, csrfToken string, filter EtapeFilter, etapes []sqlc.Etape, counts EtapeTaskCounts) {
@TaskCard(tabloID, task, csrfToken)
<div hx-swap-oob={ "innerHTML:#add-task-slot-" + string(status) }>
@AddTaskTrigger(tabloID, status, csrfToken, filter)
</div>
@EtapeStrip(tabloID, etapes, counts, filter, csrfToken, true)
}