feat(04-02): TasksDeps, task handlers, router task routes
- Add handlers_tasks.go: TasksDeps, TaskNewFormHandler, TaskCancelNewHandler, TaskCreateHandler, TaskShowHandler, TaskDeleteConfirmHandler, TaskDeleteHandler, plus stub Edit/Update/Reorder handlers - Add task routes to router.go (static before parametric per Pitfall 1) - Add TasksDeps param to NewRouter; update main.go and all test callers - Move TaskColumns/TaskColumnLabels to templates package to avoid import cycle
This commit is contained in:
parent
2be4cb6bc9
commit
181ae79369
8 changed files with 328 additions and 27 deletions
|
|
@ -77,8 +77,9 @@ func main() {
|
|||
|
||||
deps := web.AuthDeps{Queries: q, Store: store, Secure: secure, Limiter: rl}
|
||||
tabloDeps := web.TablosDeps{Queries: q}
|
||||
taskDeps := web.TasksDeps{Queries: q}
|
||||
|
||||
router := web.NewRouter(pool, "./static", deps, tabloDeps, csrfKey, env)
|
||||
router := web.NewRouter(pool, "./static", deps, tabloDeps, taskDeps, csrfKey, env)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":" + port,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ func newTestRouterWithCSRF(q *sqlc.Queries, store *auth.Store) http.Handler {
|
|||
csrfKey[i] = byte(i + 1)
|
||||
}
|
||||
deps := AuthDeps{Queries: q, Store: store, Secure: false}
|
||||
return NewRouter(stubPinger{}, "./static", deps, TablosDeps{Queries: q}, csrfKey, "dev", "localhost")
|
||||
return NewRouter(stubPinger{}, "./static", deps, TablosDeps{Queries: q}, TasksDeps{Queries: q}, csrfKey, "dev", "localhost")
|
||||
}
|
||||
|
||||
// extractCSRFToken performs a GET request and extracts the _csrf token from the
|
||||
|
|
|
|||
|
|
@ -33,14 +33,14 @@ var testCSRFKey = func() []byte {
|
|||
// Referer header are accepted.
|
||||
func newTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler {
|
||||
deps := AuthDeps{Queries: q, Store: store, Secure: false}
|
||||
return NewRouter(stubPinger{}, "./static", deps, TablosDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||
return NewRouter(stubPinger{}, "./static", deps, TablosDeps{Queries: q}, TasksDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||
}
|
||||
|
||||
// newTestRouterWithLimiter builds a router with an injected LimiterStore,
|
||||
// enabling rate-limit tests to use a fake clock.
|
||||
func newTestRouterWithLimiter(q *sqlc.Queries, store *auth.Store, rl *auth.LimiterStore) http.Handler {
|
||||
deps := AuthDeps{Queries: q, Store: store, Secure: false, Limiter: rl}
|
||||
return NewRouter(stubPinger{}, "./static", deps, TablosDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||
return NewRouter(stubPinger{}, "./static", deps, TablosDeps{Queries: q}, TasksDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||
}
|
||||
|
||||
// getCSRFToken performs a GET request to path and extracts the CSRF token
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import (
|
|||
func newTabloTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler {
|
||||
authDeps := AuthDeps{Queries: q, Store: store, Secure: false}
|
||||
tabloDeps := TablosDeps{Queries: q}
|
||||
return NewRouter(stubPinger{}, "./static", authDeps, tabloDeps, testCSRFKey, "dev", "localhost")
|
||||
return NewRouter(stubPinger{}, "./static", authDeps, tabloDeps, TasksDeps{Queries: q}, testCSRFKey, "dev", "localhost")
|
||||
}
|
||||
|
||||
// loginUser signs up a user and returns the session cookie set after signup.
|
||||
|
|
|
|||
303
backend/internal/web/handlers_tasks.go
Normal file
303
backend/internal/web/handlers_tasks.go
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"backend/internal/auth"
|
||||
"backend/internal/db/sqlc"
|
||||
"backend/templates"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
// TasksDeps holds dependencies for all task handlers.
|
||||
type TasksDeps struct {
|
||||
Queries *sqlc.Queries
|
||||
}
|
||||
|
||||
// validTaskStatuses is the set of accepted status string values.
|
||||
var validTaskStatuses = map[string]sqlc.TaskStatus{
|
||||
"todo": sqlc.TaskStatusTodo,
|
||||
"in_progress": sqlc.TaskStatusInProgress,
|
||||
"in_review": sqlc.TaskStatusInReview,
|
||||
"done": sqlc.TaskStatusDone,
|
||||
}
|
||||
|
||||
// parseTaskStatus parses a status string and returns the sqlc.TaskStatus. If
|
||||
// the string is not a valid status, it defaults to TaskStatusTodo.
|
||||
func parseTaskStatus(s string) sqlc.TaskStatus {
|
||||
if ts, ok := validTaskStatuses[s]; ok {
|
||||
return ts
|
||||
}
|
||||
return sqlc.TaskStatusTodo
|
||||
}
|
||||
|
||||
// loadOwnedTabloForTask is the shared preamble for all /tablos/{id}/tasks/{task_id}*
|
||||
// handlers. It calls loadOwnedTablo for tablo ownership verification, then parses
|
||||
// the {task_id} URL param and fetches the task (verifying it belongs to the tablo).
|
||||
// Returns (tablo, task, user, true) on success. On failure it writes the appropriate
|
||||
// HTTP response and returns false; callers must return immediately.
|
||||
func loadOwnedTabloForTask(w http.ResponseWriter, r *http.Request, deps TasksDeps) (sqlc.Tablo, sqlc.Task, *auth.User, bool) {
|
||||
// Re-use TablosDeps to call the existing loadOwnedTablo helper.
|
||||
tablo, user, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries})
|
||||
if !ok {
|
||||
return sqlc.Tablo{}, sqlc.Task{}, nil, false
|
||||
}
|
||||
|
||||
taskID, err := uuid.Parse(chi.URLParam(r, "task_id"))
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return sqlc.Tablo{}, sqlc.Task{}, nil, false
|
||||
}
|
||||
|
||||
task, err := deps.Queries.GetTaskByID(r.Context(), sqlc.GetTaskByIDParams{
|
||||
ID: taskID,
|
||||
TabloID: tablo.ID,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
http.NotFound(w, r)
|
||||
return sqlc.Tablo{}, sqlc.Task{}, nil, false
|
||||
}
|
||||
slog.Default().Error("tasks: GetTaskByID failed", "id", taskID, "err", err)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return sqlc.Tablo{}, sqlc.Task{}, nil, false
|
||||
}
|
||||
|
||||
return tablo, task, user, true
|
||||
}
|
||||
|
||||
// TaskNewFormHandler handles GET /tablos/{id}/tasks/new?status={status}.
|
||||
// Returns the TaskCreateForm fragment for HTMX insertion into the add-task slot.
|
||||
func TaskNewFormHandler(deps TasksDeps) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
tablo, _, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
statusStr := r.URL.Query().Get("status")
|
||||
status := parseTaskStatus(statusStr)
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_ = templates.TaskCreateFormFragment(
|
||||
tablo.ID,
|
||||
status,
|
||||
templates.TaskCreateForm{},
|
||||
templates.TaskCreateErrors{},
|
||||
csrf.Token(r),
|
||||
).Render(r.Context(), w)
|
||||
}
|
||||
}
|
||||
|
||||
// TaskCancelNewHandler handles GET /tablos/{id}/tasks/cancel-new?status={status}.
|
||||
// Returns the AddTaskTrigger fragment to restore the "+ Add task" button.
|
||||
func TaskCancelNewHandler(deps TasksDeps) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
tablo, _, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
statusStr := r.URL.Query().Get("status")
|
||||
status := parseTaskStatus(statusStr)
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_ = templates.AddTaskTrigger(tablo.ID, status, csrf.Token(r)).Render(r.Context(), w)
|
||||
}
|
||||
}
|
||||
|
||||
// TaskCreateHandler handles POST /tablos/{id}/tasks.
|
||||
// Validates title, inserts the task at max+100 position, and returns the new
|
||||
// TaskCard fragment (HTMX) or redirects to the tablo detail page (non-HTMX).
|
||||
//
|
||||
// Security invariants:
|
||||
// - title validated non-empty, max 255 chars (T-04-04)
|
||||
// - status validated against known TaskStatus constants (T-04-05)
|
||||
// - tablo ownership verified by loadOwnedTablo (T-04-07)
|
||||
func TaskCreateHandler(deps TasksDeps) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
tablo, _, ok := loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
|
||||
title := strings.TrimSpace(r.PostFormValue("title"))
|
||||
statusStr := r.PostFormValue("status")
|
||||
status := parseTaskStatus(statusStr)
|
||||
|
||||
var errs templates.TaskCreateErrors
|
||||
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")
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
_ = templates.TaskCreateFormFragment(
|
||||
tablo.ID,
|
||||
status,
|
||||
templates.TaskCreateForm{Title: title, Status: statusStr},
|
||||
errs,
|
||||
csrf.Token(r),
|
||||
).Render(ctx, w)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/tablos/"+tablo.ID.String(), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// Compute next position: max + 100
|
||||
maxPos, err := deps.Queries.MaxPositionByTabloAndStatus(ctx, sqlc.MaxPositionByTabloAndStatusParams{
|
||||
TabloID: tablo.ID,
|
||||
Status: status,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Default().Error("tasks create: MaxPositionByTabloAndStatus failed", "tablo_id", tablo.ID, "err", err)
|
||||
errs.General = "Something went wrong. Please try again."
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
_ = templates.TaskCreateFormFragment(
|
||||
tablo.ID,
|
||||
status,
|
||||
templates.TaskCreateForm{Title: title, Status: statusStr},
|
||||
errs,
|
||||
csrf.Token(r),
|
||||
).Render(ctx, w)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/tablos/"+tablo.ID.String(), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
task, err := deps.Queries.InsertTask(ctx, sqlc.InsertTaskParams{
|
||||
TabloID: tablo.ID,
|
||||
Title: title,
|
||||
Description: pgtype.Text{Valid: false},
|
||||
Status: status,
|
||||
Position: maxPos + 100,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Default().Error("tasks create: InsertTask failed", "tablo_id", tablo.ID, "err", err)
|
||||
errs.General = "Something went wrong. Please try again."
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
_ = templates.TaskCreateFormFragment(
|
||||
tablo.ID,
|
||||
status,
|
||||
templates.TaskCreateForm{Title: title, Status: statusStr},
|
||||
errs,
|
||||
csrf.Token(r),
|
||||
).Render(ctx, w)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/tablos/"+tablo.ID.String(), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// HTMX: set retarget/reswap headers and return combined card+OOB fragment.
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
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)).Render(ctx, w)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/tablos/"+tablo.ID.String(), http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
// TaskShowHandler handles GET /tablos/{id}/tasks/{task_id}/show.
|
||||
// Returns the TaskCard fragment — used by the cancel paths after edit or delete-confirm.
|
||||
func TaskShowHandler(deps TasksDeps) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
tablo, task, _, ok := loadOwnedTabloForTask(w, r, deps)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_ = templates.TaskCard(tablo.ID, task, csrf.Token(r)).Render(r.Context(), w)
|
||||
}
|
||||
}
|
||||
|
||||
// TaskDeleteConfirmHandler handles GET /tablos/{id}/tasks/{task_id}/delete-confirm.
|
||||
// Returns the delete confirmation fragment for HTMX outerHTML swap.
|
||||
func TaskDeleteConfirmHandler(deps TasksDeps) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
tablo, task, _, ok := loadOwnedTabloForTask(w, r, deps)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_ = templates.TaskDeleteConfirmFragment(tablo.ID, task, csrf.Token(r)).Render(r.Context(), w)
|
||||
}
|
||||
}
|
||||
|
||||
// TaskDeleteHandler handles POST /tablos/{id}/tasks/{task_id}/delete.
|
||||
// Hard-deletes the task and returns:
|
||||
// - HTMX: 200 + empty div with id="task-{task_id}" (TASK-06)
|
||||
// - Non-HTMX: 303 redirect to /tablos/{id}
|
||||
//
|
||||
// Security: loadOwnedTabloForTask verifies tablo ownership AND task-to-tablo
|
||||
// binding (T-04-03 — tablo_id in WHERE clause).
|
||||
func TaskDeleteHandler(deps TasksDeps) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
tablo, task, _, ok := loadOwnedTabloForTask(w, r, deps)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if err := deps.Queries.DeleteTask(r.Context(), sqlc.DeleteTaskParams{
|
||||
ID: task.ID,
|
||||
TabloID: tablo.ID,
|
||||
}); err != nil {
|
||||
slog.Default().Error("tasks delete: DeleteTask failed", "id", task.ID, "err", err)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Header.Get("HX-Request") == "true" {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
// Return empty zone div so HTMX removes the card from the DOM (TASK-06).
|
||||
fmt.Fprintf(w, `<div id="task-%s" class="task-card-zone"></div>`, task.ID.String())
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/tablos/"+tablo.ID.String(), http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
// TaskEditHandler handles GET /tablos/{id}/tasks/{task_id}/edit.
|
||||
// Stub — returns 501 Not Implemented until Plan 03 implements it.
|
||||
func TaskEditHandler(deps TasksDeps) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "not implemented", http.StatusNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
// TaskUpdateHandler handles POST /tablos/{id}/tasks/{task_id}.
|
||||
// Stub — returns 501 Not Implemented until Plan 03 implements it.
|
||||
func TaskUpdateHandler(deps TasksDeps) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "not implemented", http.StatusNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
// TaskReorderHandler handles POST /tablos/{id}/tasks/reorder.
|
||||
// Stub — returns 501 Not Implemented until Plan 03 implements it.
|
||||
func TaskReorderHandler(deps TasksDeps) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "not implemented", http.StatusNotImplemented)
|
||||
}
|
||||
}
|
||||
|
|
@ -24,28 +24,12 @@ import (
|
|||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
// TasksDeps holds dependencies for task handlers.
|
||||
// Stub declared here so tests compile; will be moved to handlers_tasks.go in Plan 02.
|
||||
type TasksDeps struct {
|
||||
Queries *sqlc.Queries
|
||||
}
|
||||
|
||||
// newTaskTestRouter builds a router with both TablosDeps and TasksDeps wired.
|
||||
// In Plan 02, TasksDeps will be passed to NewRouter; for now we pass only
|
||||
// TablosDeps (which is what NewRouter currently accepts) so tests compile.
|
||||
// The Task routes will be added to NewRouter in Plan 02.
|
||||
func newTaskTestRouter(q *sqlc.Queries, store *auth.Store) http.Handler {
|
||||
authDeps := AuthDeps{Queries: q, Store: store, Secure: false}
|
||||
tabloDeps := TablosDeps{Queries: q}
|
||||
return NewRouter(stubPinger{}, "./static", authDeps, tabloDeps, testCSRFKey, "dev", "localhost")
|
||||
}
|
||||
|
||||
// insertTestTablo is a helper that creates a tablo owned by the given user for
|
||||
// use in task tests.
|
||||
func insertTestTablo(t *testing.T, ctx context.Context, q *sqlc.Queries, userID interface{ GetID() interface{} }) sqlc.Tablo {
|
||||
t.Helper()
|
||||
t.Skip("handlers_tasks not yet implemented")
|
||||
return sqlc.Tablo{}
|
||||
taskDeps := TasksDeps{Queries: q}
|
||||
return NewRouter(stubPinger{}, "./static", authDeps, tabloDeps, taskDeps, testCSRFKey, "dev", "localhost")
|
||||
}
|
||||
|
||||
// ---- TestTasksKanbanRenders (TASK-01) ----
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ func TestHealthz_Down(t *testing.T) {
|
|||
// was public. The HTMX demo content is tested by
|
||||
// TestProtected_HomeAuthRendersUserEmail in handlers_auth_test.go.
|
||||
func TestIndex_UnauthRedirects(t *testing.T) {
|
||||
router := NewRouter(stubPinger{}, "./static", AuthDeps{}, TablosDeps{}, testCSRFKey, "dev")
|
||||
router := NewRouter(stubPinger{}, "./static", AuthDeps{}, TablosDeps{}, TasksDeps{}, testCSRFKey, "dev")
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
|
|
@ -81,7 +81,7 @@ func TestIndex_UnauthRedirects(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDemoTime_Fragment(t *testing.T) {
|
||||
router := NewRouter(stubPinger{}, "./static", AuthDeps{}, TablosDeps{}, testCSRFKey, "dev")
|
||||
router := NewRouter(stubPinger{}, "./static", AuthDeps{}, TablosDeps{}, TasksDeps{}, testCSRFKey, "dev")
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/demo/time", nil)
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ func TestDemoTime_Fragment(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRequestID_HeaderSet(t *testing.T) {
|
||||
router := NewRouter(stubPinger{}, "./static", AuthDeps{}, TablosDeps{}, testCSRFKey, "dev")
|
||||
router := NewRouter(stubPinger{}, "./static", AuthDeps{}, TablosDeps{}, TasksDeps{}, testCSRFKey, "dev")
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ type Pinger interface {
|
|||
// trustedOrigins is an optional list of additional origins for the CSRF
|
||||
// referer check (used in integration tests to allow localhost requests without
|
||||
// a Referer header). In production, pass no extra args — leave empty.
|
||||
func NewRouter(pinger Pinger, staticDir string, deps AuthDeps, tabloDeps TablosDeps, csrfKey []byte, env string, trustedOrigins ...string) http.Handler {
|
||||
func NewRouter(pinger Pinger, staticDir string, deps AuthDeps, tabloDeps TablosDeps, taskDeps TasksDeps, csrfKey []byte, env string, trustedOrigins ...string) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Use(RequestIDMiddleware)
|
||||
r.Use(chimw.RealIP)
|
||||
|
|
@ -90,6 +90,19 @@ func NewRouter(pinger Pinger, staticDir string, deps AuthDeps, tabloDeps TablosD
|
|||
r.Get("/tablos/{id}/delete-confirm", TabloDeleteConfirmHandler(tabloDeps))
|
||||
r.Get("/tablos/{id}/delete-cancel", TabloDeleteCancelHandler(tabloDeps))
|
||||
r.Post("/tablos/{id}/delete", TabloDeleteHandler(tabloDeps))
|
||||
// Task routes — static segments BEFORE parametric (Pitfall 1).
|
||||
// /tablos/{id}/tasks/new and /tablos/{id}/tasks/cancel-new are static
|
||||
// segments relative to /tablos/{id}/tasks/* and must come first.
|
||||
r.Get("/tablos/{id}/tasks/new", TaskNewFormHandler(taskDeps))
|
||||
r.Get("/tablos/{id}/tasks/cancel-new", TaskCancelNewHandler(taskDeps))
|
||||
r.Post("/tablos/{id}/tasks", TaskCreateHandler(taskDeps))
|
||||
r.Post("/tablos/{id}/tasks/reorder", TaskReorderHandler(taskDeps))
|
||||
// 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}/edit", TaskEditHandler(taskDeps))
|
||||
r.Post("/tablos/{id}/tasks/{task_id}", TaskUpdateHandler(taskDeps))
|
||||
r.Get("/tablos/{id}/tasks/{task_id}/delete-confirm", TaskDeleteConfirmHandler(taskDeps))
|
||||
r.Post("/tablos/{id}/tasks/{task_id}/delete", TaskDeleteHandler(taskDeps))
|
||||
})
|
||||
|
||||
r.Get("/healthz", HealthzHandler(pinger))
|
||||
|
|
|
|||
Loading…
Reference in a new issue