diff --git a/backend/events.go b/backend/events.go index 61ee21e..3eca068 100644 --- a/backend/events.go +++ b/backend/events.go @@ -37,6 +37,12 @@ type notificationPayload struct { RecipientName string `json:"recipient_name"` ActorName string `json:"actor_name"` LinkURL string `json:"link_url"` + // EntityTitle is the title of the task (assigned/mentioned/ + // task_description_mentioned) or document (doc_mentioned) the + // notification is about. May be empty — an older core that predates the + // field, or one that couldn't resolve the title — in which case the copy + // below falls back to its original title-less phrasing. + EntityTitle string `json:"entity_title"` } // handleEvent returns an EventHandler that sends the appropriate email for diff --git a/backend/templates.go b/backend/templates.go index fa158d9..b90b933 100644 --- a/backend/templates.go +++ b/backend/templates.go @@ -278,36 +278,63 @@ type notificationCopy struct { buttonLabel string // "View task" / "View document" } +// copyForNotificationTopic builds the per-topic copy. When the event carries +// an entity_title (the task/document the notification is about), it's woven +// into both the heading (which doubles as the subject) and the body so the +// recipient knows *which* task/doc from the subject line alone; when it's +// empty (older core, or title couldn't be resolved) the original title-less +// phrasing is used verbatim, so this degrades gracefully. func copyForNotificationTopic(topic string, data notificationPayload) notificationCopy { actor := data.ActorName if actor == "" { actor = "Someone" } + title := strings.TrimSpace(data.EntityTitle) switch topic { case topicNotificationMentioned: - return notificationCopy{ + c := notificationCopy{ heading: "You were mentioned", paragraph: fmt.Sprintf("%s mentioned you in a comment.", actor), buttonLabel: "View task", } + if title != "" { + c.heading = fmt.Sprintf(`You were mentioned in "%s"`, title) + c.paragraph = fmt.Sprintf(`%s mentioned you in a comment on "%s".`, actor, title) + } + return c case topicNotificationDocMentioned: - return notificationCopy{ + c := notificationCopy{ heading: "You were mentioned", paragraph: fmt.Sprintf("%s mentioned you in a document.", actor), buttonLabel: "View document", } + if title != "" { + c.heading = fmt.Sprintf(`You were mentioned in "%s"`, title) + c.paragraph = fmt.Sprintf(`%s mentioned you in the document "%s".`, actor, title) + } + return c case topicNotificationTaskDescMentioned: - return notificationCopy{ + c := notificationCopy{ heading: "You were mentioned", paragraph: fmt.Sprintf("%s mentioned you in a task's description.", actor), buttonLabel: "View task", } + if title != "" { + c.heading = fmt.Sprintf(`You were mentioned in "%s"`, title) + c.paragraph = fmt.Sprintf(`%s mentioned you in the description of "%s".`, actor, title) + } + return c default: // topicNotificationAssigned - return notificationCopy{ + c := notificationCopy{ heading: "A task was assigned to you", paragraph: fmt.Sprintf("%s assigned a task to you.", actor), buttonLabel: "View task", } + if title != "" { + c.heading = fmt.Sprintf("Task assigned: %s", title) + c.paragraph = fmt.Sprintf(`%s assigned you the task "%s".`, actor, title) + } + return c } } diff --git a/backend/templates_test.go b/backend/templates_test.go index 4106cfd..a3a2ac7 100644 --- a/backend/templates_test.go +++ b/backend/templates_test.go @@ -129,3 +129,55 @@ func TestRenderLayout_EscapesBrandingFields(t *testing.T) { t.Error("expected invalid branding PrimaryColorLight to fall back to defaultPrimaryColor") } } + +// TestRenderNotificationEmail_IncludesEntityTitle covers the entity_title +// support: when the event carries the task/document title it must appear in +// both the subject and the body, and when it's empty the copy must fall back +// to the original title-less phrasing unchanged. +func TestRenderNotificationEmail_IncludesEntityTitle(t *testing.T) { + const title = "Ship the landing page" + + cases := []struct { + name string + topic string + wantText string // a phrase that must appear in the body paragraph when titled + }{ + {"assigned", topicNotificationAssigned, "assigned you the task \"" + title + "\""}, + {"comment", topicNotificationMentioned, "mentioned you in a comment on \"" + title + "\""}, + {"doc", topicNotificationDocMentioned, "mentioned you in the document \"" + title + "\""}, + {"taskdesc", topicNotificationTaskDescMentioned, "in the description of \"" + title + "\""}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + data := notificationPayload{ActorName: "Ada", EntityTitle: title, LinkURL: "https://paca.example/x"} + subject, html, text := renderNotificationEmail(nil, tc.topic, data) + + if !strings.Contains(subject, title) { + t.Errorf("subject should contain the title %q, got %q", title, subject) + } + if !strings.Contains(text, tc.wantText) { + t.Errorf("plain-text body should contain %q, got %q", tc.wantText, text) + } + if !strings.Contains(html, title) { + t.Errorf("html body should contain the title %q", title) + } + }) + } +} + +func TestRenderNotificationEmail_FallsBackWithoutTitle(t *testing.T) { + // No EntityTitle → original copy, no stray quotes, no title fragment. + data := notificationPayload{ActorName: "Ada"} + subject, _, text := renderNotificationEmail(nil, topicNotificationAssigned, data) + + if subject != "A task was assigned to you" { + t.Errorf("expected the original title-less subject, got %q", subject) + } + if !strings.Contains(text, "Ada assigned a task to you.") { + t.Errorf("expected the original title-less body, got %q", text) + } + if strings.Contains(text, "\"\"") { + t.Errorf("empty title must not leave empty quotes in the body: %q", text) + } +} diff --git a/plugin.json b/plugin.json index 7b26fb1..a2d334b 100644 --- a/plugin.json +++ b/plugin.json @@ -2,7 +2,7 @@ "id": "com.paca.smtp", "displayName": "SMTP Email", "description": "Sends transactional emails — new-account invites, task-assignment and mention notifications — over an admin-configured SMTP server.", - "version": "0.1.2", + "version": "0.1.3", "minCoreVersion": "v0.13.3", "permissions": ["db.read", "db.write", "events.subscribe", "email.send", "users.password_set_token.issue"], "backend": {