@openinc/parse-server-opendash 4.1.4 → 4.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,6 +32,7 @@ export declare namespace Permissions {
32
32
  template_manage = "service:can-manage-template",
33
33
  masterdata_manage = "service:can-manage-masterdata",
34
34
  formconfig_manage = "service:can-manage-formconfig",
35
+ tag_create = "service:can-create-tag",
35
36
  analytics_view = "service:can-view-analytics"
36
37
  }
37
38
  enum CORE {
@@ -47,6 +47,9 @@ var Permissions;
47
47
  SERVICE["template_manage"] = "service:can-manage-template";
48
48
  SERVICE["masterdata_manage"] = "service:can-manage-masterdata";
49
49
  SERVICE["formconfig_manage"] = "service:can-manage-formconfig";
50
+ // Narrow master-data capability: create a tag inline from a tag picker,
51
+ // without granting full master-data management.
52
+ SERVICE["tag_create"] = "service:can-create-tag";
50
53
  // Analytics / monitoring
51
54
  SERVICE["analytics_view"] = "service:can-view-analytics";
52
55
  })(SERVICE = Permissions.SERVICE || (Permissions.SERVICE = {}));
@@ -0,0 +1,21 @@
1
+ import { _User } from "../types/index.js";
2
+ /**
3
+ * App-relative deep link to a service ticket (`OD3_Service_Ticket`).
4
+ *
5
+ * Notifications store links relative to the app root rather than absolute:
6
+ * the same notification is rendered in the frontend (which navigates by route)
7
+ * and mailed out (which needs a host), and the host depends on the recipient's
8
+ * tenant. {@link getAppBaseUrl} resolves it at delivery time.
9
+ */
10
+ export declare function ticketPath(ticketId: string): string;
11
+ /**
12
+ * Resolves the base URL of the open.DASH frontend for a given recipient, so
13
+ * links in notification emails point at the app that user actually works in.
14
+ *
15
+ * Precedence: the user's tenant `baseUrl` (multi-tenant installs serve each
16
+ * tenant from its own host), then the global `APP_URL`, then
17
+ * `PARSE_PUBLIC_SERVER_URL` with the trailing `/parse` stripped. Returns an
18
+ * empty string when nothing is configured, so callers emit the bare relative
19
+ * path instead of a link to a bogus host.
20
+ */
21
+ export declare function getAppBaseUrl(user: _User): Promise<string>;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ticketPath = ticketPath;
4
+ exports.getAppBaseUrl = getAppBaseUrl;
5
+ const index_js_1 = require("../features/config/index.js");
6
+ /**
7
+ * App-relative deep link to a service ticket (`OD3_Service_Ticket`).
8
+ *
9
+ * Notifications store links relative to the app root rather than absolute:
10
+ * the same notification is rendered in the frontend (which navigates by route)
11
+ * and mailed out (which needs a host), and the host depends on the recipient's
12
+ * tenant. {@link getAppBaseUrl} resolves it at delivery time.
13
+ */
14
+ function ticketPath(ticketId) {
15
+ return `/service/tickets/${ticketId}`;
16
+ }
17
+ /**
18
+ * Resolves the base URL of the open.DASH frontend for a given recipient, so
19
+ * links in notification emails point at the app that user actually works in.
20
+ *
21
+ * Precedence: the user's tenant `baseUrl` (multi-tenant installs serve each
22
+ * tenant from its own host), then the global `APP_URL`, then
23
+ * `PARSE_PUBLIC_SERVER_URL` with the trailing `/parse` stripped. Returns an
24
+ * empty string when nothing is configured, so callers emit the bare relative
25
+ * path instead of a link to a bogus host.
26
+ */
27
+ async function getAppBaseUrl(user) {
28
+ let baseUrl = "";
29
+ try {
30
+ const tenant = (await user.fetchWithInclude("tenant", { useMasterKey: true })).get("tenant");
31
+ baseUrl = tenant?.get("baseUrl") || "";
32
+ }
33
+ catch (error) {
34
+ // No tenant / not fetchable — fall through to the global configuration.
35
+ }
36
+ if (!baseUrl) {
37
+ try {
38
+ const config = index_js_1.ConfigInstance.getInstance();
39
+ const parseUrl = config.get("PARSE_PUBLIC_SERVER_URL") || "";
40
+ baseUrl = config.get("APP_URL") || parseUrl.replace(/\/parse\/?$/, "");
41
+ }
42
+ catch (error) {
43
+ console.warn("[@openinc/parse-server-opendash] Could not resolve an app base URL for notification links");
44
+ }
45
+ }
46
+ return (baseUrl || "").replace(/\/+$/, "");
47
+ }
@@ -7,6 +7,7 @@ exports.init = init;
7
7
  const node_1 = __importDefault(require("parse/node"));
8
8
  const i18next_1 = __importDefault(require("i18next"));
9
9
  const index_js_1 = require("../features/schema/index.js");
10
+ const appLinks_js_1 = require("../helper/appLinks.js");
10
11
  const getUserLanguage_js_1 = require("../helper/getUserLanguage.js");
11
12
  const index_js_2 = require("../types/index.js");
12
13
  async function init() {
@@ -49,7 +50,8 @@ async function notifyMentions(object, original) {
49
50
  return;
50
51
  const username = object.get("authorName") || "System";
51
52
  const messageText = messageSnippet(object.get("text"));
52
- const url = getMessageLink(object.get("context"));
53
+ // The only linkable chat context today is a service ticket's comment section.
54
+ const ticketId = getTicketId(object.get("context"));
53
55
  // Users already handled — seeded with the author so they never self-notify.
54
56
  const notified = new Set();
55
57
  const authorId = object.get("author")?.id;
@@ -71,7 +73,7 @@ async function notifyMentions(object, original) {
71
73
  await sendMentionNotification(user, "user", object, {
72
74
  username,
73
75
  messageText,
74
- url,
76
+ ticketId,
75
77
  });
76
78
  }
77
79
  // 2. Mentioned roles → their members, skipping anyone already notified.
@@ -95,7 +97,7 @@ async function notifyMentions(object, original) {
95
97
  await sendMentionNotification(member, "role", object, {
96
98
  username,
97
99
  messageText,
98
- url,
100
+ ticketId,
99
101
  role: role.get("label"),
100
102
  });
101
103
  }
@@ -105,6 +107,12 @@ async function notifyMentions(object, original) {
105
107
  * Builds and saves a single mention notification, translated into the
106
108
  * recipient's preferred language. Delivery (email + web push) is handled
107
109
  * automatically by the {@link Notification} afterSave hook.
110
+ *
111
+ * When the chat belongs to a service ticket the notification also carries that
112
+ * ticket: `data.url` gives the notification email a deep link (the host is
113
+ * resolved per recipient at delivery time) and `data.ticketId` gives the
114
+ * frontend its "open ticket" action — the same two keys assignment
115
+ * notifications use, so a mention behaves like any other ticket notification.
108
116
  */
109
117
  async function sendMentionNotification(user, kind, message, vars) {
110
118
  await i18next_1.default.changeLanguage(await (0, getUserLanguage_js_1.getUserLanguage)(user));
@@ -117,7 +125,9 @@ async function sendMentionNotification(user, kind, message, vars) {
117
125
  role: vars.role,
118
126
  }),
119
127
  data: {
120
- url: vars.url,
128
+ ...(vars.ticketId
129
+ ? { ticketId: vars.ticketId, url: (0, appLinks_js_1.ticketPath)(vars.ticketId) }
130
+ : {}),
121
131
  messageId: message.id,
122
132
  context: message.get("context"),
123
133
  translation: {
@@ -143,11 +153,11 @@ function mentionKey(mention) {
143
153
  return `${mention.className}:${mention.id}`;
144
154
  }
145
155
  /**
146
- * Derives a deep-link path from the chat's `context`. The context is the
156
+ * Extracts the ticket a chat belongs to from its `context`. The context is the
147
157
  * JSON-stringified address tuple (e.g. `["service","ticket","<id>"]`). Only the
148
- * service-ticket context is linkable today; anything else yields no link.
158
+ * service-ticket context is linkable today; anything else yields no ticket.
149
159
  */
150
- function getMessageLink(context) {
160
+ function getTicketId(context) {
151
161
  if (typeof context !== "string")
152
162
  return undefined;
153
163
  try {
@@ -155,12 +165,13 @@ function getMessageLink(context) {
155
165
  if (Array.isArray(parts) &&
156
166
  parts[0] === "service" &&
157
167
  parts[1] === "ticket" &&
168
+ typeof parts[2] === "string" &&
158
169
  parts[2]) {
159
- return `/service/tickets/${parts[2]}`;
170
+ return parts[2];
160
171
  }
161
172
  }
162
173
  catch {
163
- // context is not a JSON tuple — no link
174
+ // context is not a JSON tuple — not ticket-bound
164
175
  }
165
176
  return undefined;
166
177
  }
@@ -9,6 +9,7 @@ const web_push_1 = __importDefault(require("web-push"));
9
9
  const index_js_1 = require("../features/schema/index.js");
10
10
  const index_js_2 = require("../features/config/index.js");
11
11
  const index_js_3 = require("../features/notifications/index.js");
12
+ const appLinks_js_1 = require("../helper/appLinks.js");
12
13
  const index_js_4 = require("../types/index.js");
13
14
  async function init() {
14
15
  (0, index_js_1.beforeSaveHook)(index_js_4.Notification, async (request) => {
@@ -28,13 +29,7 @@ async function init() {
28
29
  const email = user?.getEmail();
29
30
  console.log("[@openinc/parse-server-opendash][Notification]", `notification="${object.id}"`, `type="email"`, `to="${email}"`);
30
31
  if (email) {
31
- let body = description || "";
32
- if (object.get("data")?.url) {
33
- body += "\n\n";
34
- body += index_js_2.ConfigInstance.getInstance().get("APP_URL");
35
- body += object.get("data")?.url;
36
- }
37
- await (0, index_js_3.sendSimpleEmail)(email, title, body);
32
+ await (0, index_js_3.sendSimpleEmail)(email, title, await buildEmailBody(description, object.get("data")?.url, user));
38
33
  }
39
34
  }
40
35
  // Handle Push
@@ -55,6 +50,26 @@ async function init() {
55
50
  }
56
51
  });
57
52
  }
53
+ /**
54
+ * Plain-text body of a notification email: the (already translated)
55
+ * description, followed by a link to the thing the notification is about.
56
+ *
57
+ * Producers store that link in `data.url` as an app-relative path (e.g.
58
+ * `/service/tickets/<id>`), because the host depends on the recipient's tenant
59
+ * and is only known here, at delivery time — the frontend renders the same
60
+ * notification by route and never needs it. Absolute URLs are passed through
61
+ * unchanged for producers that resolved a host themselves.
62
+ */
63
+ async function buildEmailBody(description, url, user) {
64
+ const body = description || "";
65
+ if (typeof url !== "string" || url === "") {
66
+ return body;
67
+ }
68
+ const link = /^https?:\/\//i.test(url)
69
+ ? url
70
+ : `${await (0, appLinks_js_1.getAppBaseUrl)(user)}${url}`;
71
+ return `${body}\n\n${link}`;
72
+ }
58
73
  async function sendWebPush(subscription, id, title, description, icon, data) {
59
74
  try {
60
75
  console.log("[@openinc/parse-server-opendash][Notification]", `notification="${id}"`, `type="web"`, `subscription="${subscription?.id}"`);
@@ -40,9 +40,9 @@ exports.init = init;
40
40
  const node_1 = __importStar(require("parse/node"));
41
41
  const i18next_1 = __importDefault(require("i18next"));
42
42
  const index_js_1 = require("../features/schema/index.js");
43
+ const appLinks_js_1 = require("../helper/appLinks.js");
43
44
  const getUserLanguage_js_1 = require("../helper/getUserLanguage.js");
44
45
  const index_js_2 = require("../types/index.js");
45
- const index_js_3 = require("../features/config/index.js");
46
46
  const applyRuleSet_js_1 = require("../features/ruleset/applyRuleSet.js");
47
47
  async function init() {
48
48
  (0, index_js_1.beforeSaveHook)("OD3_Service_Ticket", async (request) => {
@@ -338,10 +338,15 @@ async function notifyRecipients(userIds, roleIds, notified, ticket, options) {
338
338
  * Builds and saves a single assignment notification, translated into the
339
339
  * recipient's preferred language. Delivery (email + web push) is handled
340
340
  * automatically by the {@link Notification} afterSave hook.
341
+ *
342
+ * The deep link lives in `data.url` rather than inside the description text:
343
+ * the frontend opens the ticket from `data.ticketId` (see
344
+ * `TicketNotificationActionAdapter`) and would only render a raw URL as noise,
345
+ * while the notification email appends `data.url` — resolved against the
346
+ * recipient's tenant host — to the body.
341
347
  */
342
348
  async function sendAssignmentNotification(user, ticket, options) {
343
349
  await i18next_1.default.changeLanguage(await (0, getUserLanguage_js_1.getUserLanguage)(user));
344
- const ticketLink = await getTicketLink(ticket, user);
345
350
  const base = `openservice:ticket.assignment.${options.variant}`;
346
351
  const ticketName = ticket.get("title");
347
352
  await new index_js_2.Notification({
@@ -349,11 +354,11 @@ async function sendAssignmentNotification(user, ticket, options) {
349
354
  description: i18next_1.default.t(`${base}.description`, {
350
355
  username: options.assignerName,
351
356
  ticketName,
352
- ticketLink,
353
357
  }),
354
358
  type: options.frontpage ? "frontpage" : "mailbox",
355
359
  data: {
356
360
  ticketId: ticket.id,
361
+ url: (0, appLinks_js_1.ticketPath)(ticket.id),
357
362
  translation: {
358
363
  username: options.assignerName,
359
364
  ticketName,
@@ -390,32 +395,6 @@ function pointerIds(value) {
390
395
  })
391
396
  .filter((id) => typeof id === "string");
392
397
  }
393
- /**
394
- * Returns the link to a ticket.
395
- * @param ticket
396
- * @returns
397
- */
398
- async function getTicketLink(ticket, user) {
399
- //Get hostname from tenant or environment variable or default to localhost
400
- let hostname = "localhost";
401
- const tenant = (await user.fetchWithInclude("tenant", { useMasterKey: true })).get("tenant");
402
- try {
403
- const baseurl = tenant?.get("baseUrl");
404
- const publicurl = index_js_3.ConfigInstance.getInstance()
405
- .get("PARSE_PUBLIC_SERVER_URL")
406
- .replace("/parse", "");
407
- if (baseurl !== undefined && baseurl !== "") {
408
- hostname = baseurl;
409
- }
410
- else if (publicurl) {
411
- hostname = publicurl;
412
- }
413
- }
414
- catch (error) {
415
- console.warn("[@openinc/parse-server-opendash] Could not get a hostname, defaulting to localhost");
416
- }
417
- return `${hostname}/service/tickets/${ticket.id}`;
418
- }
419
398
  function getUsername(user) {
420
399
  if (!user)
421
400
  return "System";
@@ -3,11 +3,11 @@
3
3
  "assignment": {
4
4
  "assigned": {
5
5
  "title": "Ihnen wurde ein Ticket zugewiesen",
6
- "description": "{{username}} hat Ihnen das Ticket {{ticketName}} zugewiesen.\n\nLink: {{- ticketLink}}"
6
+ "description": "{{username}} hat Ihnen das Ticket {{ticketName}} zugewiesen."
7
7
  },
8
8
  "watcher": {
9
9
  "title": "Ein Ticket wurde zugewiesen",
10
- "description": "{{username}} hat das Ticket {{ticketName}} zugewiesen.\n\nLink: {{- ticketLink}}"
10
+ "description": "{{username}} hat das Ticket {{ticketName}} zugewiesen."
11
11
  }
12
12
  }
13
13
  }
@@ -3,11 +3,11 @@
3
3
  "assignment": {
4
4
  "assigned": {
5
5
  "title": "You were assigned to a ticket",
6
- "description": "{{username}} assigned you to ticket {{ticketName}}.\n\nLink: {{- ticketLink}}"
6
+ "description": "{{username}} assigned you to ticket {{ticketName}}."
7
7
  },
8
8
  "watcher": {
9
9
  "title": "A ticket was assigned",
10
- "description": "{{username}} assigned ticket {{ticketName}}.\n\nLink: {{- ticketLink}}"
10
+ "description": "{{username}} assigned ticket {{ticketName}}."
11
11
  }
12
12
  }
13
13
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openinc/parse-server-opendash",
3
- "version": "4.1.4",
3
+ "version": "4.1.6",
4
4
  "description": "Parse Server Cloud Code for open.INC Stack.",
5
5
  "packageManager": "pnpm@11.25.0",
6
6
  "keywords": [