fix(activity): address code review feedback and improve timeline UX

- Extract shared timeAgo utility, remove duplicates from comment-card and issue-detail
- Remove unused replies prop from CommentCard
- Fix recursive delete to remove all descendant replies, not just direct children
- Improve formatActivity with human-readable status/priority labels and actor names
- Validate parent comment exists and belongs to same issue before creating reply
- Add priority_changed activity recording in activity listeners
- Fix activity SQL query to sort ASC (was DESC, then re-sorted in handler)
- Fix reply-input layout alignment and test submit button selector
- Minor: .gitignore additions, button dark mode aria-expanded fix

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-03-29 00:21:46 +08:00
parent 5d2e62ccde
commit b2ee151306
11 changed files with 267 additions and 122 deletions

View file

@ -59,6 +59,7 @@ func registerActivityListeners(bus *events.Bus, queries *db.Queries) {
}
statusChanged, _ := payload["status_changed"].(bool)
priorityChanged, _ := payload["priority_changed"].(bool)
assigneeChanged, _ := payload["assignee_changed"].(bool)
descriptionChanged, _ := payload["description_changed"].(bool)
@ -84,6 +85,28 @@ func registerActivityListeners(bus *events.Bus, queries *db.Queries) {
}
}
if priorityChanged {
prevPriority, _ := payload["prev_priority"].(string)
details, _ := json.Marshal(map[string]string{
"from": prevPriority,
"to": issue.Priority,
})
activity, err := queries.CreateActivity(ctx, db.CreateActivityParams{
WorkspaceID: parseUUID(issue.WorkspaceID),
IssueID: parseUUID(issue.ID),
ActorType: util.StrToText(e.ActorType),
ActorID: parseUUID(e.ActorID),
Action: "priority_changed",
Details: details,
})
if err != nil {
slog.Error("activity: failed to record priority change",
"issue_id", issue.ID, "error", err)
} else {
publishActivityEvent(bus, e, activity)
}
}
if assigneeChanged {
prevAssigneeType, _ := payload["prev_assignee_type"].(*string)
prevAssigneeID, _ := payload["prev_assignee_id"].(*string)
@ -119,6 +142,35 @@ func registerActivityListeners(bus *events.Bus, queries *db.Queries) {
}
}
if dueDateChanged, _ := payload["due_date_changed"].(bool); dueDateChanged {
prevDueDate := ""
if v, ok := payload["prev_due_date"].(*string); ok && v != nil {
prevDueDate = *v
}
newDueDate := ""
if issue.DueDate != nil {
newDueDate = *issue.DueDate
}
details, _ := json.Marshal(map[string]string{
"from": prevDueDate,
"to": newDueDate,
})
activity, err := queries.CreateActivity(ctx, db.CreateActivityParams{
WorkspaceID: parseUUID(issue.WorkspaceID),
IssueID: parseUUID(issue.ID),
ActorType: util.StrToText(e.ActorType),
ActorID: parseUUID(e.ActorID),
Action: "due_date_changed",
Details: details,
})
if err != nil {
slog.Error("activity: failed to record due date change",
"issue_id", issue.ID, "error", err)
} else {
publishActivityEvent(bus, e, activity)
}
}
if descriptionChanged {
activity, err := queries.CreateActivity(ctx, db.CreateActivityParams{
WorkspaceID: parseUUID(issue.WorkspaceID),
@ -205,7 +257,7 @@ func publishActivityEvent(bus *events.Bus, original events.Event, activity db.Ac
"id": util.UUIDToString(activity.ID),
"actor_type": actorType,
"actor_id": util.UUIDToString(activity.ActorID),
"action": &action,
"action": action,
"details": json.RawMessage(activity.Details),
"created_at": util.TimestampToString(activity.CreatedAt),
},