feat(09-02): manage etapes
This commit is contained in:
parent
9b89282692
commit
4af623a57b
4 changed files with 334 additions and 22 deletions
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"backend/internal/db/sqlc"
|
"backend/internal/db/sqlc"
|
||||||
"backend/templates"
|
"backend/templates"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
|
|
@ -116,6 +117,42 @@ func parseOwnedEtapeID(r *http.Request, q *sqlc.Queries, tabloID uuid.UUID, raw
|
||||||
return pgtype.UUID{Bytes: etapeID, Valid: true}, true, nil
|
return pgtype.UUID{Bytes: etapeID, Valid: true}, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadOwnedEtape(w http.ResponseWriter, r *http.Request, deps EtapesDeps) (sqlc.Tablo, sqlc.Etape, bool) {
|
||||||
|
tablo, _, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries})
|
||||||
|
if !ok {
|
||||||
|
return sqlc.Tablo{}, sqlc.Etape{}, false
|
||||||
|
}
|
||||||
|
etapeID, err := uuid.Parse(chi.URLParam(r, "etape_id"))
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return sqlc.Tablo{}, sqlc.Etape{}, false
|
||||||
|
}
|
||||||
|
etape, err := deps.Queries.GetEtapeByID(r.Context(), sqlc.GetEtapeByIDParams{ID: etapeID, TabloID: tablo.ID})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return sqlc.Tablo{}, sqlc.Etape{}, false
|
||||||
|
}
|
||||||
|
slog.Default().Error("etapes: GetEtapeByID failed", "tablo_id", tablo.ID, "etape_id", etapeID, "err", err)
|
||||||
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||||
|
return sqlc.Tablo{}, sqlc.Etape{}, false
|
||||||
|
}
|
||||||
|
return tablo, etape, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTasksTab(w http.ResponseWriter, r *http.Request, q *sqlc.Queries, tablo sqlc.Tablo) {
|
||||||
|
tasks, etapes, counts, filter, ok := loadTasksTabData(w, r, q, tablo)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
if r.Header.Get("HX-Request") == "true" {
|
||||||
|
w.Header().Set("HX-Retarget", "#tasks-tab")
|
||||||
|
w.Header().Set("HX-Reswap", "outerHTML")
|
||||||
|
}
|
||||||
|
_ = templates.TasksTabFragment(tablo, tasks, etapes, counts, filter, csrf.Token(r)).Render(r.Context(), w)
|
||||||
|
}
|
||||||
|
|
||||||
func EtapeNewFormHandler(deps EtapesDeps) http.HandlerFunc {
|
func EtapeNewFormHandler(deps EtapesDeps) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
tablo, _, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries})
|
tablo, _, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries})
|
||||||
|
|
@ -127,6 +164,130 @@ func EtapeNewFormHandler(deps EtapesDeps) http.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func EtapeEditFormHandler(deps EtapesDeps) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tablo, etape, ok := loadOwnedEtape(w, r, deps)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
_ = templates.EtapeEditFormFragment(
|
||||||
|
tablo.ID,
|
||||||
|
etape,
|
||||||
|
templates.EtapeUpdateForm{Title: etape.Title, Description: etape.Description.String},
|
||||||
|
templates.EtapeUpdateErrors{},
|
||||||
|
csrf.Token(r),
|
||||||
|
).Render(r.Context(), w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func EtapeUpdateHandler(deps EtapesDeps) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tablo, etape, ok := loadOwnedEtape(w, r, deps)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx := r.Context()
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
http.Error(w, "bad request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
title := strings.TrimSpace(r.PostFormValue("title"))
|
||||||
|
description := strings.TrimSpace(r.PostFormValue("description"))
|
||||||
|
form := templates.EtapeUpdateForm{Title: title, Description: description}
|
||||||
|
var errs templates.EtapeUpdateErrors
|
||||||
|
if title == "" {
|
||||||
|
errs.Title = "Title is required"
|
||||||
|
} else if len(title) > 255 {
|
||||||
|
errs.Title = "Title must be 255 characters or fewer"
|
||||||
|
}
|
||||||
|
if errs.Title != "" {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
if r.Header.Get("HX-Request") == "true" {
|
||||||
|
w.Header().Set("HX-Retarget", "#etape-form-slot")
|
||||||
|
w.Header().Set("HX-Reswap", "innerHTML")
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||||
|
_ = templates.EtapeEditFormFragment(tablo.ID, etape, form, errs, csrf.Token(r)).Render(ctx, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := deps.Queries.UpdateEtape(ctx, sqlc.UpdateEtapeParams{
|
||||||
|
ID: etape.ID,
|
||||||
|
TabloID: tablo.ID,
|
||||||
|
Title: title,
|
||||||
|
Description: pgtype.Text{String: description, Valid: description != ""},
|
||||||
|
}); err != nil {
|
||||||
|
slog.Default().Error("etapes update: UpdateEtape failed", "tablo_id", tablo.ID, "etape_id", etape.ID, "err", err)
|
||||||
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
renderTasksTab(w, r, deps.Queries, tablo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func EtapeDeleteConfirmHandler(deps EtapesDeps) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tablo, etape, ok := loadOwnedEtape(w, r, deps)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
_ = templates.EtapeDeleteConfirmFragment(tablo.ID, etape, csrf.Token(r)).Render(r.Context(), w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func EtapeDeleteHandler(deps EtapesDeps) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tablo, etape, ok := loadOwnedEtape(w, r, deps)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := deps.Queries.DeleteEtape(r.Context(), sqlc.DeleteEtapeParams{ID: etape.ID, TabloID: tablo.ID}); err != nil {
|
||||||
|
slog.Default().Error("etapes delete: DeleteEtape failed", "tablo_id", tablo.ID, "etape_id", etape.ID, "err", err)
|
||||||
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
renderTasksTab(w, r, deps.Queries, tablo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func EtapeReorderHandler(deps EtapesDeps) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
tablo, _, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries})
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
http.Error(w, "bad request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, rawID := range r.Form["etape_id"] {
|
||||||
|
etapeID, err := uuid.Parse(rawID)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := deps.Queries.GetEtapeByID(r.Context(), sqlc.GetEtapeByIDParams{ID: etapeID, TabloID: tablo.ID}); err != nil {
|
||||||
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
slog.Default().Error("etapes reorder: GetEtapeByID failed", "tablo_id", tablo.ID, "etape_id", etapeID, "err", err)
|
||||||
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := deps.Queries.UpdateEtapePosition(r.Context(), sqlc.UpdateEtapePositionParams{
|
||||||
|
ID: etapeID,
|
||||||
|
TabloID: tablo.ID,
|
||||||
|
Position: int32((i + 1) * 100),
|
||||||
|
}); err != nil {
|
||||||
|
slog.Default().Error("etapes reorder: UpdateEtapePosition failed", "tablo_id", tablo.ID, "etape_id", etapeID, "err", err)
|
||||||
|
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderTasksTab(w, r, deps.Queries, tablo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func EtapeCancelNewHandler(deps EtapesDeps) http.HandlerFunc {
|
func EtapeCancelNewHandler(deps EtapesDeps) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if _, _, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries}); !ok {
|
if _, _, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries}); !ok {
|
||||||
|
|
@ -186,15 +347,6 @@ func EtapeCreateHandler(deps EtapesDeps) http.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks, etapes, counts, filter, ok := loadTasksTabData(w, r, deps.Queries, tablo)
|
renderTasksTab(w, r, deps.Queries, tablo)
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
if r.Header.Get("HX-Request") == "true" {
|
|
||||||
w.Header().Set("HX-Retarget", "#tasks-tab")
|
|
||||||
w.Header().Set("HX-Reswap", "outerHTML")
|
|
||||||
}
|
|
||||||
_ = templates.TasksTabFragment(tablo, tasks, etapes, counts, filter, csrf.Token(r)).Render(ctx, w)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,11 @@ func NewRouter(pinger Pinger, staticFS fs.FS, deps AuthDeps, tabloDeps TablosDep
|
||||||
r.Get("/tablos/{id}/etapes/new", EtapeNewFormHandler(etapeDeps))
|
r.Get("/tablos/{id}/etapes/new", EtapeNewFormHandler(etapeDeps))
|
||||||
r.Get("/tablos/{id}/etapes/cancel-new", EtapeCancelNewHandler(etapeDeps))
|
r.Get("/tablos/{id}/etapes/cancel-new", EtapeCancelNewHandler(etapeDeps))
|
||||||
r.Post("/tablos/{id}/etapes", EtapeCreateHandler(etapeDeps))
|
r.Post("/tablos/{id}/etapes", EtapeCreateHandler(etapeDeps))
|
||||||
|
r.Post("/tablos/{id}/etapes/reorder", EtapeReorderHandler(etapeDeps))
|
||||||
|
r.Get("/tablos/{id}/etapes/{etape_id}/edit", EtapeEditFormHandler(etapeDeps))
|
||||||
|
r.Post("/tablos/{id}/etapes/{etape_id}", EtapeUpdateHandler(etapeDeps))
|
||||||
|
r.Get("/tablos/{id}/etapes/{etape_id}/delete-confirm", EtapeDeleteConfirmHandler(etapeDeps))
|
||||||
|
r.Post("/tablos/{id}/etapes/{etape_id}/delete", EtapeDeleteHandler(etapeDeps))
|
||||||
// Parametric task routes — must come after static task segments.
|
// Parametric task routes — must come after static task segments.
|
||||||
r.Get("/tablos/{id}/tasks/{task_id}/show", TaskShowHandler(taskDeps))
|
r.Get("/tablos/{id}/tasks/{task_id}/show", TaskShowHandler(taskDeps))
|
||||||
r.Get("/tablos/{id}/tasks/{task_id}/edit", TaskEditHandler(taskDeps))
|
r.Get("/tablos/{id}/tasks/{task_id}/edit", TaskEditHandler(taskDeps))
|
||||||
|
|
|
||||||
|
|
@ -33,18 +33,54 @@ templ EtapeStrip(tabloID uuid.UUID, etapes []sqlc.Etape, counts EtapeTaskCounts,
|
||||||
<span>Unassigned</span>
|
<span>Unassigned</span>
|
||||||
<span class="rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-700">{ strconv.Itoa(counts.Unassigned) }</span>
|
<span class="rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-700">{ strconv.Itoa(counts.Unassigned) }</span>
|
||||||
</a>
|
</a>
|
||||||
for _, etape := range etapes {
|
for index, etape := range etapes {
|
||||||
<a
|
<div class="inline-flex flex-shrink-0 items-center gap-1">
|
||||||
href={ templ.SafeURL("/tablos/" + tabloID.String() + "/tasks?etape=" + etape.ID.String()) }
|
<a
|
||||||
hx-get={ "/tablos/" + tabloID.String() + "/tasks?etape=" + etape.ID.String() }
|
href={ templ.SafeURL("/tablos/" + tabloID.String() + "/tasks?etape=" + etape.ID.String()) }
|
||||||
hx-target="#tab-content"
|
hx-get={ "/tablos/" + tabloID.String() + "/tasks?etape=" + etape.ID.String() }
|
||||||
hx-swap="innerHTML"
|
hx-target="#tab-content"
|
||||||
hx-push-url={ "/tablos/" + tabloID.String() + "/tasks?etape=" + etape.ID.String() }
|
hx-swap="innerHTML"
|
||||||
class={ etapeChipClasses(filter.IsEtape(etape.ID)) }
|
hx-push-url={ "/tablos/" + tabloID.String() + "/tasks?etape=" + etape.ID.String() }
|
||||||
>
|
class={ etapeChipClasses(filter.IsEtape(etape.ID)) }
|
||||||
<span>{ etape.Title }</span>
|
>
|
||||||
<span class="rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-700">{ strconv.Itoa(etapeCount(counts, etape.ID)) }</span>
|
<span>{ etape.Title }</span>
|
||||||
</a>
|
<span class="rounded bg-slate-100 px-1.5 py-0.5 text-xs text-slate-700">{ strconv.Itoa(etapeCount(counts, etape.ID)) }</span>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ui-button ui-button-soft-neutral-md px-2"
|
||||||
|
hx-get={ "/tablos/" + tabloID.String() + "/etapes/" + etape.ID.String() + "/edit" }
|
||||||
|
hx-target="#etape-form-slot"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
aria-label={ "Edit etape: " + etape.Title }
|
||||||
|
>Edit</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ui-button ui-button-soft-danger-md px-2"
|
||||||
|
hx-get={ "/tablos/" + tabloID.String() + "/etapes/" + etape.ID.String() + "/delete-confirm" }
|
||||||
|
hx-target="#etape-form-slot"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
aria-label={ "Delete etape: " + etape.Title }
|
||||||
|
>Delete</button>
|
||||||
|
if index > 0 {
|
||||||
|
<form method="POST" action={ templ.SafeURL("/tablos/" + tabloID.String() + "/etapes/reorder") } hx-post={ "/tablos/" + tabloID.String() + "/etapes/reorder" } hx-target="#tasks-tab" hx-swap="outerHTML" class="inline">
|
||||||
|
@ui.CSRFField(csrfToken)
|
||||||
|
for _, id := range etapeReorderIDs(etapes, index, -1) {
|
||||||
|
<input type="hidden" name="etape_id" value={ id.String() }/>
|
||||||
|
}
|
||||||
|
<button type="submit" class="ui-button ui-button-soft-neutral-md px-2" aria-label={ "Move etape earlier: " + etape.Title }>Up</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
if index < len(etapes)-1 {
|
||||||
|
<form method="POST" action={ templ.SafeURL("/tablos/" + tabloID.String() + "/etapes/reorder") } hx-post={ "/tablos/" + tabloID.String() + "/etapes/reorder" } hx-target="#tasks-tab" hx-swap="outerHTML" class="inline">
|
||||||
|
@ui.CSRFField(csrfToken)
|
||||||
|
for _, id := range etapeReorderIDs(etapes, index, 1) {
|
||||||
|
<input type="hidden" name="etape_id" value={ id.String() }/>
|
||||||
|
}
|
||||||
|
<button type="submit" class="ui-button ui-button-soft-neutral-md px-2" aria-label={ "Move etape later: " + etape.Title }>Down</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -58,6 +94,100 @@ templ EtapeStrip(tabloID uuid.UUID, etapes []sqlc.Etape, counts EtapeTaskCounts,
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templ EtapeEditFormFragment(tabloID uuid.UUID, etape sqlc.Etape, form EtapeUpdateForm, errs EtapeUpdateErrors, csrfToken string) {
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
action={ templ.SafeURL("/tablos/" + tabloID.String() + "/etapes/" + etape.ID.String()) }
|
||||||
|
hx-post={ "/tablos/" + tabloID.String() + "/etapes/" + etape.ID.String() }
|
||||||
|
hx-target="#tasks-tab"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
class="rounded border border-slate-200 bg-white p-3 shadow-sm space-y-3"
|
||||||
|
>
|
||||||
|
@ui.CSRFField(csrfToken)
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
value={ form.Title }
|
||||||
|
maxlength="255"
|
||||||
|
required
|
||||||
|
placeholder="Etape title"
|
||||||
|
class="block w-full rounded border border-slate-300 px-2 py-1 text-sm placeholder-slate-400 focus:border-slate-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
@FieldError(errs.Title)
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<textarea
|
||||||
|
name="description"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Description (optional)"
|
||||||
|
class="block w-full rounded border border-slate-300 px-2 py-1 text-sm placeholder-slate-400 focus:border-slate-500 focus:outline-none"
|
||||||
|
>{ form.Description }</textarea>
|
||||||
|
</div>
|
||||||
|
if errs.General != "" {
|
||||||
|
@FieldError(errs.General)
|
||||||
|
}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
@ui.Button(ui.ButtonProps{
|
||||||
|
Label: "Save",
|
||||||
|
Variant: ui.ButtonVariantDefault,
|
||||||
|
Tone: ui.ButtonToneSolid,
|
||||||
|
Size: ui.SizeMD,
|
||||||
|
Type: "submit",
|
||||||
|
})
|
||||||
|
@ui.Button(ui.ButtonProps{
|
||||||
|
Label: "Discard",
|
||||||
|
Variant: ui.ButtonVariantNeutral,
|
||||||
|
Tone: ui.ButtonToneSoft,
|
||||||
|
Size: ui.SizeMD,
|
||||||
|
Type: "button",
|
||||||
|
Attrs: templ.Attributes{
|
||||||
|
"hx-get": "/tablos/" + tabloID.String() + "/etapes/cancel-new",
|
||||||
|
"hx-target": "#etape-form-slot",
|
||||||
|
"hx-swap": "innerHTML",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ EtapeDeleteConfirmFragment(tabloID uuid.UUID, etape sqlc.Etape, csrfToken string) {
|
||||||
|
<div class="rounded border border-slate-200 bg-white p-3 shadow-sm space-y-3">
|
||||||
|
<p class="text-sm font-semibold text-slate-800">Delete etape?</p>
|
||||||
|
<p class="text-sm text-slate-600">Tasks in this etape will stay in the tablo and move to Unassigned.</p>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
action={ templ.SafeURL("/tablos/" + tabloID.String() + "/etapes/" + etape.ID.String() + "/delete") }
|
||||||
|
hx-post={ "/tablos/" + tabloID.String() + "/etapes/" + etape.ID.String() + "/delete" }
|
||||||
|
hx-target="#tasks-tab"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
@ui.CSRFField(csrfToken)
|
||||||
|
@ui.Button(ui.ButtonProps{
|
||||||
|
Label: "Delete etape",
|
||||||
|
Variant: ui.ButtonVariantDanger,
|
||||||
|
Tone: ui.ButtonToneSolid,
|
||||||
|
Size: ui.SizeMD,
|
||||||
|
Type: "submit",
|
||||||
|
})
|
||||||
|
</form>
|
||||||
|
@ui.Button(ui.ButtonProps{
|
||||||
|
Label: "Keep etape",
|
||||||
|
Variant: ui.ButtonVariantNeutral,
|
||||||
|
Tone: ui.ButtonToneSoft,
|
||||||
|
Size: ui.SizeMD,
|
||||||
|
Type: "button",
|
||||||
|
Attrs: templ.Attributes{
|
||||||
|
"hx-get": "/tablos/" + tabloID.String() + "/etapes/cancel-new",
|
||||||
|
"hx-target": "#etape-form-slot",
|
||||||
|
"hx-swap": "innerHTML",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
templ EtapeCreateFormFragment(tabloID uuid.UUID, form EtapeCreateForm, errs EtapeCreateErrors, csrfToken string) {
|
templ EtapeCreateFormFragment(tabloID uuid.UUID, form EtapeCreateForm, errs EtapeCreateErrors, csrfToken string) {
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package templates
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"backend/internal/db/sqlc"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -35,6 +37,16 @@ type EtapeCreateErrors struct {
|
||||||
General string
|
General string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EtapeUpdateForm struct {
|
||||||
|
Title string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
type EtapeUpdateErrors struct {
|
||||||
|
Title string
|
||||||
|
General string
|
||||||
|
}
|
||||||
|
|
||||||
func (f EtapeFilter) QueryValue() string {
|
func (f EtapeFilter) QueryValue() string {
|
||||||
switch f.Kind {
|
switch f.Kind {
|
||||||
case EtapeFilterUnassigned:
|
case EtapeFilterUnassigned:
|
||||||
|
|
@ -86,3 +98,16 @@ func etapeChipClasses(active bool) string {
|
||||||
}
|
}
|
||||||
return base + " border-slate-200 bg-white text-slate-700 hover:border-slate-400"
|
return base + " border-slate-200 bg-white text-slate-700 hover:border-slate-400"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func etapeReorderIDs(etapes []sqlc.Etape, index int, direction int) []uuid.UUID {
|
||||||
|
ids := make([]uuid.UUID, 0, len(etapes))
|
||||||
|
for _, etape := range etapes {
|
||||||
|
ids = append(ids, etape.ID)
|
||||||
|
}
|
||||||
|
target := index + direction
|
||||||
|
if index < 0 || index >= len(ids) || target < 0 || target >= len(ids) {
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
ids[index], ids[target] = ids[target], ids[index]
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue