fix(projects-detail, services): empty-list endpoints returned JSON null → tab content blank
m reported /projects/{id} loaded the chrome and tabs but every panel was
empty even with deadlines/appointments/team rows that should render.
Console error: "Cannot read properties of null (reading 'length')" at
projects-detail.js — the Project Detail page expects every list endpoint
to return [] but at least two were returning literal JSON null.
Reproduced via the in-page fetch console:
/api/projects/{id}/parties → 200, body: "null"
/api/projects/{id}/children → 200, body: "null"
/api/projects/{id}/deadlines → 200, body: "[…]" (had data, fine)
/api/projects/{id}/team → 200, body: "[…]" (had data, fine)
Root cause: every list service in internal/services declared its result
as `var rows []models.X` and returned that to the handler, which
encoding/json marshals as `null` when the SELECT returns zero rows
(nil slice, not empty slice). Most endpoints happen to have data so
the bug stayed dormant until t-paliad-038 hit /projects/{id} where
parties + children are commonly empty.
Fix at the source — every list service that JSON-marshals to a client
now initialises `rows := []models.X{}` so the encoder produces `[]`:
party_service ListForProjekt
project_service List, ListAncestors, BuildTree, GetTree
(ListChildren goes through List)
deadline_service List + ListForProjekt
appointment_service List + ListForProjekt
note_service ListForProjekt
checklist_instance_service ListForProjekt
team_service List
department_service List + ListMembers + ListWithMembers
caldav_service was deliberately left alone — its lists are admin-only
debug surfaces, not user-facing tab fillers, and changing them would
mix scopes.
Belt-and-braces on the client too — projects-detail.ts now coerces every
`await resp.json()` for an array endpoint with `?? []` so a future
service regression can't crash the page.
Verified: go build/vet/test clean, bun run build clean.
This commit is contained in:
@@ -142,7 +142,7 @@ func (s *AppointmentService) ListVisibleForUser(ctx context.Context, userID uuid
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
var rows []models.AppointmentWithProject
|
||||
rows := []models.AppointmentWithProject{}
|
||||
if err := stmt.SelectContext(ctx, &rows, args); err != nil {
|
||||
return nil, fmt.Errorf("list appointments: %w", err)
|
||||
}
|
||||
@@ -154,7 +154,7 @@ func (s *AppointmentService) ListForProjekt(ctx context.Context, userID, projekt
|
||||
if _, err := s.projects.GetByID(ctx, userID, projektID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rows []models.Appointment
|
||||
rows := []models.Appointment{}
|
||||
if err := s.db.SelectContext(ctx, &rows,
|
||||
`SELECT `+terminColumns+`
|
||||
FROM paliad.appointments
|
||||
|
||||
@@ -332,7 +332,7 @@ func (s *ChecklistInstanceService) listWithProjekt(ctx context.Context, query st
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
var rows []models.ChecklistInstanceWithProject
|
||||
rows := []models.ChecklistInstanceWithProject{}
|
||||
if err := stmt.SelectContext(ctx, &rows, args); err != nil {
|
||||
return nil, fmt.Errorf("list checklist_instances: %w", err)
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ func (s *DeadlineService) ListVisibleForUser(ctx context.Context, userID uuid.UU
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
var rows []models.DeadlineWithProject
|
||||
rows := []models.DeadlineWithProject{}
|
||||
if err := stmt.SelectContext(ctx, &rows, args); err != nil {
|
||||
return nil, fmt.Errorf("list deadlines: %w", err)
|
||||
}
|
||||
@@ -151,7 +151,7 @@ func (s *DeadlineService) ListForProjekt(ctx context.Context, userID, projektID
|
||||
if _, err := s.projects.GetByID(ctx, userID, projektID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rows []models.Deadline
|
||||
rows := []models.Deadline{}
|
||||
if err := s.db.SelectContext(ctx, &rows,
|
||||
`SELECT `+fristColumns+`
|
||||
FROM paliad.deadlines
|
||||
|
||||
@@ -45,7 +45,7 @@ type UpdateDepartmentInput struct {
|
||||
|
||||
// List returns every Dezernat (readable by any authenticated user — see RLS).
|
||||
func (s *DepartmentService) List(ctx context.Context) ([]models.Department, error) {
|
||||
var rows []models.Department
|
||||
rows := []models.Department{}
|
||||
err := s.db.SelectContext(ctx, &rows,
|
||||
`SELECT id, name, lead_user_id, office, created_at, updated_at
|
||||
FROM paliad.departments
|
||||
@@ -281,7 +281,7 @@ func (s *DepartmentService) ListWithMembers(ctx context.Context) ([]DepartmentWi
|
||||
// GetMembership returns the user's Dezernat memberships (zero or more).
|
||||
// Used by the settings page to render "Your Dezernat: <name>".
|
||||
func (s *DepartmentService) GetMembership(ctx context.Context, userID uuid.UUID) ([]models.Department, error) {
|
||||
var rows []models.Department
|
||||
rows := []models.Department{}
|
||||
err := s.db.SelectContext(ctx, &rows,
|
||||
`SELECT d.id, d.name, d.lead_user_id, d.office, d.created_at, d.updated_at
|
||||
FROM paliad.departments d
|
||||
|
||||
@@ -214,7 +214,7 @@ type noteParent struct {
|
||||
|
||||
func (s *NoteService) list(ctx context.Context, where string, arg any) ([]models.Note, error) {
|
||||
query := notizSelect + ` WHERE ` + where + ` ORDER BY n.created_at DESC`
|
||||
var rows []models.Note
|
||||
rows := []models.Note{}
|
||||
if err := s.db.SelectContext(ctx, &rows, query, arg); err != nil {
|
||||
return nil, fmt.Errorf("list notes: %w", err)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func (s *PartyService) ListForProjekt(ctx context.Context, userID, projektID uui
|
||||
if _, err := s.projects.GetByID(ctx, userID, projektID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rows []models.Party
|
||||
rows := []models.Party{}
|
||||
if err := s.db.SelectContext(ctx, &rows,
|
||||
`SELECT `+parteiColumns+`
|
||||
FROM paliad.parties
|
||||
|
||||
@@ -192,7 +192,7 @@ func (s *ProjectService) List(ctx context.Context, userID uuid.UUID, f ProjectFi
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
var rows []models.Project
|
||||
rows := []models.Project{}
|
||||
if err := stmt.SelectContext(ctx, &rows, args); err != nil {
|
||||
return nil, fmt.Errorf("list projects: %w", err)
|
||||
}
|
||||
@@ -269,7 +269,7 @@ func (s *ProjectService) ListAncestors(ctx context.Context, userID, id uuid.UUID
|
||||
for i, u := range ids {
|
||||
idStrs[i] = u.String()
|
||||
}
|
||||
var rows []models.Project
|
||||
rows := []models.Project{}
|
||||
if err := s.db.SelectContext(ctx, &rows, query, pq.StringArray(idStrs), userID, user.Role); err != nil {
|
||||
return nil, fmt.Errorf("list ancestors: %w", err)
|
||||
}
|
||||
@@ -309,7 +309,7 @@ func (s *ProjectService) BuildTree(ctx context.Context, userID uuid.UUID) ([]*Pr
|
||||
query := `SELECT ` + projektColumns + ` FROM paliad.projects p
|
||||
WHERE ` + visibilityPredicatePositional("p", 1, 2) + `
|
||||
ORDER BY p.path`
|
||||
var rows []models.Project
|
||||
rows := []models.Project{}
|
||||
if err := s.db.SelectContext(ctx, &rows, query, userID, user.Role); err != nil {
|
||||
return nil, fmt.Errorf("build tree list: %w", err)
|
||||
}
|
||||
@@ -401,7 +401,7 @@ func (s *ProjectService) GetTree(ctx context.Context, userID, id uuid.UUID) ([]m
|
||||
WHERE (p.path = $1 OR p.path LIKE $2)
|
||||
AND ` + visibilityPredicatePositional("p", 3, 4) + `
|
||||
ORDER BY p.path`
|
||||
var rows []models.Project
|
||||
rows := []models.Project{}
|
||||
if err := s.db.SelectContext(ctx, &rows, query, root.Path, prefix, userID, user.Role); err != nil {
|
||||
return nil, fmt.Errorf("get tree: %w", err)
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ func (s *TeamService) ListDirectMembers(ctx context.Context, callerID, projektID
|
||||
if _, err := s.projects.GetByID(ctx, callerID, projektID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var rows []models.ProjectTeamMemberWithUser
|
||||
rows := []models.ProjectTeamMemberWithUser{}
|
||||
err := s.db.SelectContext(ctx, &rows,
|
||||
`SELECT pt.id, pt.project_id, pt.user_id, pt.role, pt.inherited,
|
||||
pt.added_by, pt.created_at,
|
||||
@@ -145,7 +145,7 @@ func (s *TeamService) ListEffectiveMembers(ctx context.Context, callerID, projek
|
||||
WHERE r.rn = 1
|
||||
ORDER BY r.inherited ASC, r.role, u.display_name`
|
||||
|
||||
var rows []models.ProjectTeamMemberWithUser
|
||||
rows := []models.ProjectTeamMemberWithUser{}
|
||||
if err := s.db.SelectContext(ctx, &rows, query, projektID, pq.StringArray(ancestorIDs)); err != nil {
|
||||
return nil, fmt.Errorf("list effective team: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user