feat(approvals): t-paliad-216 — server-side hydration for back-link
Server-side additions so /inbox can render the suggest-changes back-link
without an extra client round-trip:
- ApprovalRequestView gains NextRequestID. Hydrated via correlated
subquery on previous_request_id; mig 103's partial index makes the
lookup O(1) per row.
- view_service.go approvalRowSubtitle picks up the changes_requested
case ("Abgelehnt mit Vorschlag von <decider>").
- filter_spec.go validRequestStatuses includes "changes_requested" so
user-views can filter on it.
- handlers/approvals.go isValidInboxStatus accepts "changes_requested"
on the /api/inbox/{mine,pending-mine}?status= query. Test case added
to TestParseInboxFilter_DropsUnknownStatus.
This commit is contained in:
@@ -270,7 +270,8 @@ func isValidInboxStatus(s string) bool {
|
||||
services.RequestStatusApproved,
|
||||
services.RequestStatusRejected,
|
||||
services.RequestStatusRevoked,
|
||||
services.RequestStatusSuperseded:
|
||||
services.RequestStatusSuperseded,
|
||||
services.RequestStatusChangesRequested:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
@@ -135,6 +135,7 @@ func TestParseInboxFilter_DropsUnknownStatus(t *testing.T) {
|
||||
{"rejected", "rejected"},
|
||||
{"revoked", "revoked"},
|
||||
{"superseded", "superseded"},
|
||||
{"changes_requested", "changes_requested"}, // t-paliad-216
|
||||
{"foo", ""}, // unknown — dropped
|
||||
{"DROP+TABLE", ""}, // hostile — dropped
|
||||
{"PENDING", ""}, // case mismatch — dropped (we don't normalise)
|
||||
|
||||
@@ -1080,14 +1080,20 @@ func marshalJSONOrNull(m map[string]any) ([]byte, error) {
|
||||
// server would reject, replacing the previous click-then-alert UX.
|
||||
type ApprovalRequestView struct {
|
||||
models.ApprovalRequest
|
||||
ProjectTitle string `db:"project_title" json:"project_title"`
|
||||
EntityTitle *string `db:"entity_title" json:"entity_title,omitempty"`
|
||||
RequesterName string `db:"requester_name" json:"requester_name"`
|
||||
RequesterEmail string `db:"requester_email" json:"requester_email"`
|
||||
DeciderName *string `db:"decider_name" json:"decider_name,omitempty"`
|
||||
DeciderEmail *string `db:"decider_email" json:"decider_email,omitempty"`
|
||||
ViewerCanApprove bool `db:"viewer_can_approve" json:"viewer_can_approve"`
|
||||
ViewerIsRequester bool `db:"viewer_is_requester" json:"viewer_is_requester"`
|
||||
ProjectTitle string `db:"project_title" json:"project_title"`
|
||||
EntityTitle *string `db:"entity_title" json:"entity_title,omitempty"`
|
||||
RequesterName string `db:"requester_name" json:"requester_name"`
|
||||
RequesterEmail string `db:"requester_email" json:"requester_email"`
|
||||
DeciderName *string `db:"decider_name" json:"decider_name,omitempty"`
|
||||
DeciderEmail *string `db:"decider_email" json:"decider_email,omitempty"`
|
||||
ViewerCanApprove bool `db:"viewer_can_approve" json:"viewer_can_approve"`
|
||||
ViewerIsRequester bool `db:"viewer_is_requester" json:"viewer_is_requester"`
|
||||
// NextRequestID is the forward-pointer from a changes_requested row
|
||||
// to the new pending row spawned by SuggestChanges (t-paliad-216).
|
||||
// Hydrated via correlated subquery on previous_request_id; the
|
||||
// partial index approval_requests_previous_idx keeps the lookup O(1).
|
||||
// NULL on every row that hasn't been counter-proposed.
|
||||
NextRequestID *uuid.UUID `db:"next_request_id" json:"next_request_id,omitempty"`
|
||||
}
|
||||
|
||||
// approvalEligibilitySQL is the SELECT-and-WHERE-compatible boolean
|
||||
@@ -1150,7 +1156,11 @@ const approvalRequestViewColumns = `
|
||||
du.display_name AS decider_name,
|
||||
du.email AS decider_email,
|
||||
(ar.status = 'pending' AND ar.requested_by <> $1 AND ` + approvalEligibilitySQL + `) AS viewer_can_approve,
|
||||
(ar.requested_by = $1) AS viewer_is_requester`
|
||||
(ar.requested_by = $1) AS viewer_is_requester,
|
||||
(SELECT nxt.id FROM paliad.approval_requests nxt
|
||||
WHERE nxt.previous_request_id = ar.id
|
||||
ORDER BY nxt.requested_at DESC
|
||||
LIMIT 1) AS next_request_id`
|
||||
|
||||
const approvalRequestViewJoins = `
|
||||
paliad.approval_requests ar
|
||||
|
||||
@@ -203,7 +203,7 @@ var KnownProjectEventKinds = []string{
|
||||
// filters and request-side status filters respectively.
|
||||
var (
|
||||
validEntityApprovalStatuses = []string{"approved", "pending", "legacy"}
|
||||
validRequestStatuses = []string{"pending", "approved", "rejected", "revoked"}
|
||||
validRequestStatuses = []string{"pending", "approved", "rejected", "revoked", "changes_requested"}
|
||||
validApprovalEntityTypes = []string{"deadline", "appointment"}
|
||||
validApprovalViewerRoles = []string{"approver_eligible", "self_requested", "any_visible"}
|
||||
validDeadlineStatuses = []string{"pending", "completed"}
|
||||
|
||||
@@ -569,6 +569,11 @@ func approvalRowSubtitle(r ApprovalRequestView) string {
|
||||
return "Abgelehnt"
|
||||
case "revoked":
|
||||
return "Widerrufen"
|
||||
case "changes_requested":
|
||||
if r.DeciderName != nil {
|
||||
return fmt.Sprintf("Abgelehnt mit Vorschlag von %s", *r.DeciderName)
|
||||
}
|
||||
return "Abgelehnt mit Vorschlag"
|
||||
}
|
||||
return r.Status
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user