Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions pkg/form/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package form
import (
"github.com/labstack/echo/v4"
"github.com/occult/pagode/pkg/context"
"github.com/romsar/gonertia/v2"
)

// Form represents a form that can be submitted and validated.
Expand Down Expand Up @@ -30,6 +31,9 @@ type Form interface {

// GetFieldErrors returns the validation errors for a given struct field.
GetFieldErrors(fieldName string) []string

// GetErrors returns all validation errors keyed by field name.
GetErrors() map[string][]string
}

// Get gets a form from the context or initializes a new copy if one is not set.
Expand All @@ -51,3 +55,22 @@ func Clear(ctx echo.Context) {
func Submit(ctx echo.Context, form Form) error {
return form.Submit(ctx, form)
}

// ShareErrors shares form validation errors as Inertia props.
// It merges the errors with existing Inertia props to preserve
// previously set shared data like auth, flash, etc.
func ShareErrors(ctx echo.Context, form Form) {
errors := form.GetErrors()
if len(errors) == 0 {
return
}

existingProps := gonertia.PropsFromContext(ctx.Request().Context())
if existingProps == nil {
existingProps = make(map[string]any)
}
existingProps["errors"] = errors

newCtx := gonertia.SetProps(ctx.Request().Context(), existingProps)
ctx.SetRequest(ctx.Request().WithContext(newCtx))
}
108 changes: 108 additions & 0 deletions pkg/form/form_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package form

import (
"net/http/httptest"
"testing"

"github.com/labstack/echo/v4"
"github.com/occult/pagode/pkg/context"
"github.com/occult/pagode/pkg/tests"
"github.com/romsar/gonertia/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -61,3 +63,109 @@ func TestGetClear(t *testing.T) {
assert.Empty(t, got.Name)
})
}

func TestShareErrorsPreservesExistingProps(t *testing.T) {
e := echo.New()
req := httptest.NewRequest("POST", "/test", nil)
rec := httptest.NewRecorder()
ctx := e.NewContext(req, rec)

// Set up existing Inertia props
existingProps := map[string]any{
"auth": map[string]any{
"user": map[string]any{"name": "Test User"},
},
"flash": map[string]any{
"success": []string{"Welcome!"},
},
}
newReqCtx := gonertia.SetProps(req.Context(), existingProps)
ctx.SetRequest(req.WithContext(newReqCtx))

// Create a form with errors
form := &mockForm{}
form.SetFieldError("Email", "Email is required")
form.SetFieldError("Password", "Password is required")

// Share errors
ShareErrors(ctx, form)

// Get props from context
props := gonertia.PropsFromContext(ctx.Request().Context())
require.NotNil(t, props)

// Verify existing props are preserved
auth, ok := props["auth"].(map[string]any)
require.True(t, ok, "auth prop should exist")
user, ok := auth["user"].(map[string]any)
require.True(t, ok, "auth.user should exist")
assert.Equal(t, "Test User", user["name"])

flash, ok := props["flash"].(map[string]any)
require.True(t, ok, "flash prop should exist")
success, ok := flash["success"].([]string)
require.True(t, ok, "flash.success should exist")
assert.Equal(t, []string{"Welcome!"}, success)

// Verify errors are added
errors, ok := props["errors"].(map[string][]string)
require.True(t, ok, "errors prop should exist")
assert.Equal(t, []string{"Email is required"}, errors["Email"])
assert.Equal(t, []string{"Password is required"}, errors["Password"])
}

func TestShareErrorsWorksWithNoExistingProps(t *testing.T) {
e := echo.New()
req := httptest.NewRequest("POST", "/test", nil)
rec := httptest.NewRecorder()
ctx := e.NewContext(req, rec)

// Create a form with errors
form := &mockForm{}
form.SetFieldError("Name", "Name is required")

// Share errors (no existing props)
ShareErrors(ctx, form)

// Get props from context
props := gonertia.PropsFromContext(ctx.Request().Context())
require.NotNil(t, props)

// Verify errors are added
errors, ok := props["errors"].(map[string][]string)
require.True(t, ok, "errors prop should exist")
assert.Equal(t, []string{"Name is required"}, errors["Name"])
}

func TestShareErrorsNoOpWhenNoErrors(t *testing.T) {
e := echo.New()
req := httptest.NewRequest("POST", "/test", nil)
rec := httptest.NewRecorder()
ctx := e.NewContext(req, rec)

// Set up existing Inertia props
existingProps := map[string]any{
"auth": map[string]any{
"user": "test",
},
}
newReqCtx := gonertia.SetProps(req.Context(), existingProps)
ctx.SetRequest(req.WithContext(newReqCtx))

// Create a form with NO errors
form := &mockForm{}

// Share errors (should be no-op)
ShareErrors(ctx, form)

// Get props from context
props := gonertia.PropsFromContext(ctx.Request().Context())
require.NotNil(t, props)

// Verify existing props are untouched
assert.Equal(t, "test", props["auth"].(map[string]any)["user"])

// Verify no errors key was added
_, hasErrors := props["errors"]
assert.False(t, hasErrors, "errors prop should not exist when there are no errors")
}
7 changes: 7 additions & 0 deletions pkg/form/submission.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ func (f *Submission) GetFieldErrors(fieldName string) []string {
return f.errors[fieldName]
}

func (f *Submission) GetErrors() map[string][]string {
if f.errors == nil {
return make(map[string][]string)
}
return f.errors
}

// setErrorMessages sets errors messages on the submission for all fields that failed validation.
func (f *Submission) setErrorMessages(err error) {
// Only this is supported right now
Expand Down
Loading