Skip to content

Commit 38d5c6f

Browse files
authored
Merge pull request #3 from Paca-AI/feature/enhance-webhook-event-data
feat: enhance webhook event data with task references and project names
2 parents 474d220 + d9df59c commit 38d5c6f

3 files changed

Lines changed: 87 additions & 9 deletions

File tree

backend/plugin_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,43 @@ func TestActivityEventDispatchSkipsUnsubscribed(t *testing.T) {
197197
t.Fatal("expected delivery to fail outside WASM runtime")
198198
}
199199
}
200+
201+
// TestTaskRefAndProjectNameInSummaryText verifies that the human-readable
202+
// "text" summary built for a delivery includes the task's alias (project
203+
// task_id_prefix + task_number, e.g. "ABC-123") and is prefixed with the
204+
// project name.
205+
func TestTaskRefAndProjectNameInSummaryText(t *testing.T) {
206+
p, tc := setupPlugin(t)
207+
208+
tc.DB.SeedRows("projects",
209+
[]string{"id", "name", "task_id_prefix"},
210+
[][]any{{testProjectID, "Website Redesign", "ABC"}})
211+
tc.DB.SeedRows("tasks",
212+
[]string{"id", "title", "task_number", "project_id"},
213+
[][]any{{"task-1", "Fix login bug", 123, testProjectID}})
214+
215+
payload := map[string]any{
216+
"project_id": testProjectID,
217+
"task_id": "task-1",
218+
}
219+
220+
if got, want := p.taskRef(payload), `task ABC-123 "Fix login bug"`; got != want {
221+
t.Fatalf("taskRef = %q, want %q", got, want)
222+
}
223+
if got, want := p.projectName(payload), "Website Redesign"; got != want {
224+
t.Fatalf("projectName = %q, want %q", got, want)
225+
}
226+
227+
_, text := p.buildEventData("task.deleted", payload)
228+
if want := `Someone deleted task ABC-123 "Fix login bug"`; text != want {
229+
t.Fatalf("buildEventData text = %q, want %q", text, want)
230+
}
231+
232+
// A project with no task_id_prefix configured falls back to no alias.
233+
tc.DB.SeedRows("projects",
234+
[]string{"id", "name", "task_id_prefix"},
235+
[][]any{{testProjectID, "Website Redesign", ""}})
236+
if got, want := p.taskRef(payload), `task "Fix login bug"`; got != want {
237+
t.Fatalf("taskRef with no prefix = %q, want %q", got, want)
238+
}
239+
}

backend/webhooks.go

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -503,10 +503,7 @@ func (p *webhookPlugin) buildEventData(topic string, payload map[string]any) (ma
503503
Title string `json:"title"`
504504
}
505505
_ = json.Unmarshal([]byte(rawContent), &c)
506-
if c.Title == "" {
507-
return map[string]any{}, fmt.Sprintf("%s created a task", actor)
508-
}
509-
return map[string]any{"title": c.Title}, fmt.Sprintf("%s created a task: %q", actor, c.Title)
506+
return map[string]any{"title": c.Title}, fmt.Sprintf("%s created %s", actor, p.taskRef(payload))
510507

511508
case "task.updated":
512509
ref := p.taskRef(payload)
@@ -635,21 +632,48 @@ func (p *webhookPlugin) lookupName(sqlStr, param string) string {
635632
return newRowScanner(result.Columns, result.Rows[0]).str("name")
636633
}
637634

638-
// taskRef returns a quoted reference to the task ("task \"Fix login bug\"")
639-
// for use in a summary line, falling back to "a task" when the title can't
640-
// be resolved (e.g. task_id missing from the payload).
635+
// taskRef returns a quoted reference to the task, prefixed with its
636+
// human-readable alias when the project has a task ID prefix configured
637+
// (e.g. `task ABC-123 "Fix login bug"`), falling back to "a task" when the
638+
// title can't be resolved (e.g. task_id missing from the payload).
641639
func (p *webhookPlugin) taskRef(payload map[string]any) string {
642640
taskID, _ := payload["task_id"].(string)
643641
if taskID == "" {
644642
return "a task"
645643
}
646-
title := p.lookupName(`SELECT title AS name FROM tasks WHERE id = $1`, taskID)
644+
result, err := p.db.Query(`SELECT title, task_number, project_id FROM tasks WHERE id = $1`, taskID)
645+
if err != nil || len(result.Rows) == 0 {
646+
return "a task"
647+
}
648+
sc := newRowScanner(result.Columns, result.Rows[0])
649+
title := sc.str("title")
647650
if title == "" {
648651
return "a task"
649652
}
653+
if alias := p.taskAlias(sc.str("project_id"), sc.intVal("task_number")); alias != "" {
654+
return fmt.Sprintf("task %s %q", alias, title)
655+
}
650656
return fmt.Sprintf("task %q", title)
651657
}
652658

659+
// taskAlias formats a task's human-readable alias (e.g. "ABC-123") from its
660+
// project's task_id_prefix and the task's sequential task_number, or ""
661+
// when the project has no prefix configured or the task number is unset.
662+
func (p *webhookPlugin) taskAlias(projectID string, taskNumber int) string {
663+
if projectID == "" || taskNumber <= 0 {
664+
return ""
665+
}
666+
result, err := p.db.Query(`SELECT task_id_prefix FROM projects WHERE id = $1`, projectID)
667+
if err != nil || len(result.Rows) == 0 {
668+
return ""
669+
}
670+
prefix := newRowScanner(result.Columns, result.Rows[0]).str("task_id_prefix")
671+
if prefix == "" {
672+
return ""
673+
}
674+
return fmt.Sprintf("%s-%d", prefix, taskNumber)
675+
}
676+
653677
// blockContentToText converts a field-change value (decoded from JSON into
654678
// an `any` by fieldChange) back into BlockNote JSON text and extracts plain
655679
// text from it. Used for fields like "description" that store BlockNote
@@ -701,6 +725,16 @@ func extractBlockText(raw string) string {
701725
return ""
702726
}
703727

728+
// projectName resolves the display name of the event's project, or "" when
729+
// project_id is missing from the payload or doesn't match a project.
730+
func (p *webhookPlugin) projectName(payload map[string]any) string {
731+
projectID, _ := payload["project_id"].(string)
732+
if projectID == "" {
733+
return ""
734+
}
735+
return p.lookupName(`SELECT name FROM projects WHERE id = $1`, projectID)
736+
}
737+
704738
// taskURL builds a link to the task on the Paca web app, using the host's
705739
// PUBLIC_URL config value. Returns "" when PUBLIC_URL isn't configured or
706740
// the event has no task_id/project_id (e.g. webhook.test).
@@ -725,6 +759,10 @@ func (p *webhookPlugin) deliver(sc *scanner, eventType string, payload map[strin
725759
targetURL := sc.str("url")
726760

727761
data, text := p.buildEventData(eventType, payload)
762+
if pname := p.projectName(payload); pname != "" {
763+
data["project_name"] = pname
764+
text = fmt.Sprintf("[%s] %s", pname, text)
765+
}
728766
if url := p.taskURL(payload); url != "" {
729767
data["url"] = url
730768
text = text + " - " + url

plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"id": "com.paca.webhook",
33
"displayName": "Webhooks",
44
"description": "Sends HTTP webhooks to a URL of your choice when task activity happens in a project.",
5-
"version": "0.1.1",
5+
"version": "0.1.2",
66
"permissions": ["db.read", "db.write", "events.subscribe"],
77
"backend": {
88
"allowedConfigKeys": ["ENCRYPTION_KEY", "PUBLIC_URL"],

0 commit comments

Comments
 (0)