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
40 changes: 40 additions & 0 deletions backend/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,43 @@ func TestActivityEventDispatchSkipsUnsubscribed(t *testing.T) {
t.Fatal("expected delivery to fail outside WASM runtime")
}
}

// TestTaskRefAndProjectNameInSummaryText verifies that the human-readable
// "text" summary built for a delivery includes the task's alias (project
// task_id_prefix + task_number, e.g. "ABC-123") and is prefixed with the
// project name.
func TestTaskRefAndProjectNameInSummaryText(t *testing.T) {
p, tc := setupPlugin(t)

tc.DB.SeedRows("projects",
[]string{"id", "name", "task_id_prefix"},
[][]any{{testProjectID, "Website Redesign", "ABC"}})
tc.DB.SeedRows("tasks",
[]string{"id", "title", "task_number", "project_id"},
[][]any{{"task-1", "Fix login bug", 123, testProjectID}})

payload := map[string]any{
"project_id": testProjectID,
"task_id": "task-1",
}

if got, want := p.taskRef(payload), `task ABC-123 "Fix login bug"`; got != want {
t.Fatalf("taskRef = %q, want %q", got, want)
}
if got, want := p.projectName(payload), "Website Redesign"; got != want {
t.Fatalf("projectName = %q, want %q", got, want)
}

_, text := p.buildEventData("task.deleted", payload)
if want := `Someone deleted task ABC-123 "Fix login bug"`; text != want {
t.Fatalf("buildEventData text = %q, want %q", text, want)
}

// A project with no task_id_prefix configured falls back to no alias.
tc.DB.SeedRows("projects",
[]string{"id", "name", "task_id_prefix"},
[][]any{{testProjectID, "Website Redesign", ""}})
if got, want := p.taskRef(payload), `task "Fix login bug"`; got != want {
t.Fatalf("taskRef with no prefix = %q, want %q", got, want)
}
}
54 changes: 46 additions & 8 deletions backend/webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,10 +503,7 @@ func (p *webhookPlugin) buildEventData(topic string, payload map[string]any) (ma
Title string `json:"title"`
}
_ = json.Unmarshal([]byte(rawContent), &c)
if c.Title == "" {
return map[string]any{}, fmt.Sprintf("%s created a task", actor)
}
return map[string]any{"title": c.Title}, fmt.Sprintf("%s created a task: %q", actor, c.Title)
return map[string]any{"title": c.Title}, fmt.Sprintf("%s created %s", actor, p.taskRef(payload))

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

// taskRef returns a quoted reference to the task ("task \"Fix login bug\"")
// for use in a summary line, falling back to "a task" when the title can't
// be resolved (e.g. task_id missing from the payload).
// taskRef returns a quoted reference to the task, prefixed with its
// human-readable alias when the project has a task ID prefix configured
// (e.g. `task ABC-123 "Fix login bug"`), falling back to "a task" when the
// title can't be resolved (e.g. task_id missing from the payload).
func (p *webhookPlugin) taskRef(payload map[string]any) string {
taskID, _ := payload["task_id"].(string)
if taskID == "" {
return "a task"
}
title := p.lookupName(`SELECT title AS name FROM tasks WHERE id = $1`, taskID)
result, err := p.db.Query(`SELECT title, task_number, project_id FROM tasks WHERE id = $1`, taskID)
if err != nil || len(result.Rows) == 0 {
return "a task"
}
sc := newRowScanner(result.Columns, result.Rows[0])
title := sc.str("title")
if title == "" {
return "a task"
}
if alias := p.taskAlias(sc.str("project_id"), sc.intVal("task_number")); alias != "" {
return fmt.Sprintf("task %s %q", alias, title)
}
return fmt.Sprintf("task %q", title)
}

// taskAlias formats a task's human-readable alias (e.g. "ABC-123") from its
// project's task_id_prefix and the task's sequential task_number, or ""
// when the project has no prefix configured or the task number is unset.
func (p *webhookPlugin) taskAlias(projectID string, taskNumber int) string {
if projectID == "" || taskNumber <= 0 {
return ""
}
result, err := p.db.Query(`SELECT task_id_prefix FROM projects WHERE id = $1`, projectID)
if err != nil || len(result.Rows) == 0 {
return ""
}
prefix := newRowScanner(result.Columns, result.Rows[0]).str("task_id_prefix")
if prefix == "" {
return ""
}
return fmt.Sprintf("%s-%d", prefix, taskNumber)
}

// blockContentToText converts a field-change value (decoded from JSON into
// an `any` by fieldChange) back into BlockNote JSON text and extracts plain
// text from it. Used for fields like "description" that store BlockNote
Expand Down Expand Up @@ -701,6 +725,16 @@ func extractBlockText(raw string) string {
return ""
}

// projectName resolves the display name of the event's project, or "" when
// project_id is missing from the payload or doesn't match a project.
func (p *webhookPlugin) projectName(payload map[string]any) string {
projectID, _ := payload["project_id"].(string)
if projectID == "" {
return ""
}
return p.lookupName(`SELECT name FROM projects WHERE id = $1`, projectID)
}

// taskURL builds a link to the task on the Paca web app, using the host's
// PUBLIC_URL config value. Returns "" when PUBLIC_URL isn't configured or
// the event has no task_id/project_id (e.g. webhook.test).
Expand All @@ -725,6 +759,10 @@ func (p *webhookPlugin) deliver(sc *scanner, eventType string, payload map[strin
targetURL := sc.str("url")

data, text := p.buildEventData(eventType, payload)
if pname := p.projectName(payload); pname != "" {
data["project_name"] = pname
text = fmt.Sprintf("[%s] %s", pname, text)
}
if url := p.taskURL(payload); url != "" {
data["url"] = url
text = text + " - " + url
Expand Down
2 changes: 1 addition & 1 deletion plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "com.paca.webhook",
"displayName": "Webhooks",
"description": "Sends HTTP webhooks to a URL of your choice when task activity happens in a project.",
"version": "0.1.1",
"version": "0.1.2",
"permissions": ["db.read", "db.write", "events.subscribe"],
"backend": {
"allowedConfigKeys": ["ENCRYPTION_KEY", "PUBLIC_URL"],
Expand Down
Loading