Skip to content

Commit 5499e49

Browse files
[WEB-5574]chore: notification card refactor (#8234)
* chore: notification card refactor * chore: moved base activity types to constants package
1 parent 3c8624b commit 5499e49

File tree

4 files changed

+192
-65
lines changed

4 files changed

+192
-65
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { replaceUnderscoreIfSnakeCase } from "@plane/utils";
2+
import type { TNotificationContentMap } from "@/components/workspace-notifications/sidebar/notification-card/content";
3+
4+
// Additional notification content map for CE (empty - EE extends this)
5+
export const ADDITIONAL_NOTIFICATION_CONTENT_MAP: TNotificationContentMap = {};
6+
7+
// Fallback action renderer for fields not in the map
8+
export const renderAdditionalAction = (notificationField: string, verb: string | undefined) => {
9+
const baseAction = !["comment", "archived_at"].includes(notificationField) ? verb : "";
10+
return `${baseAction} ${replaceUnderscoreIfSnakeCase(notificationField)}`;
11+
};
12+
13+
// Fallback value renderer for fields not in the map
14+
export const renderAdditionalValue = (
15+
_notificationField: string | undefined,
16+
newValue: string | undefined,
17+
_oldValue: string | undefined
18+
) => newValue;
19+
20+
export const shouldShowConnector = (notificationField: string | undefined) =>
21+
!["comment", "archived_at", "None", "assignees", "labels", "start_date", "target_date", "parent"].includes(
22+
notificationField || ""
23+
);
24+
25+
export const shouldRender = (notificationField: string | undefined, verb: string | undefined) => verb !== "deleted";

apps/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { observer } from "mobx-react";
22
// plane imports
3-
import type { E_SORT_ORDER, TActivityFilters } from "@plane/constants";
4-
import { EActivityFilterType, filterActivityOnSelectedFilters } from "@plane/constants";
3+
import type { E_SORT_ORDER, TActivityFilters, EActivityFilterType } from "@plane/constants";
4+
import { BASE_ACTIVITY_FILTER_TYPES, filterActivityOnSelectedFilters } from "@plane/constants";
55
import type { TCommentsOperations } from "@plane/types";
66
// components
77
import { CommentCard } from "@/components/comments/card/root";
@@ -52,13 +52,6 @@ export const IssueActivityCommentRoot = observer(function IssueActivityCommentRo
5252

5353
const filteredActivityAndComments = filterActivityOnSelectedFilters(activityAndComments, selectedFilters);
5454

55-
const BASE_ACTIVITY_FILTER_TYPES = [
56-
EActivityFilterType.ACTIVITY,
57-
EActivityFilterType.STATE,
58-
EActivityFilterType.ASSIGNEE,
59-
EActivityFilterType.DEFAULT,
60-
];
61-
6255
return (
6356
<div>
6457
{filteredActivityAndComments.map((activityComment, index) => {

apps/web/core/components/workspace-notifications/sidebar/notification-card/content.tsx

Lines changed: 158 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,144 @@
1+
import type { ReactNode } from "react";
12
// plane imports
23
import type { TNotification } from "@plane/types";
34
import {
45
convertMinutesToHoursMinutesString,
56
renderFormattedDate,
67
sanitizeCommentForNotification,
7-
replaceUnderscoreIfSnakeCase,
88
stripAndTruncateHTML,
99
} from "@plane/utils";
1010
// components
1111
import { LiteTextEditor } from "@/components/editor/lite-text";
12+
import {
13+
ADDITIONAL_NOTIFICATION_CONTENT_MAP,
14+
renderAdditionalAction,
15+
renderAdditionalValue,
16+
shouldShowConnector,
17+
} from "@/plane-web/components/workspace-notifications/notification-card/content";
18+
19+
// Types
20+
export type TNotificationFieldData = {
21+
field: string | undefined;
22+
newValue: string | undefined;
23+
oldValue: string | undefined;
24+
verb: string | undefined;
25+
};
26+
27+
export type TNotificationContentDetails = {
28+
action?: ReactNode;
29+
value?: ReactNode;
30+
showConnector?: boolean;
31+
};
32+
33+
export type TNotificationContentHandler = (data: TNotificationFieldData) => TNotificationContentDetails | null;
34+
35+
export type TNotificationContentMap = {
36+
[key: string]: TNotificationContentHandler;
37+
};
38+
39+
// Base notification content map for core fields
40+
export const BASE_NOTIFICATION_CONTENT_MAP: TNotificationContentMap = {
41+
duplicate: ({ verb }) => ({
42+
action:
43+
verb === "created"
44+
? "marked that this work item is a duplicate of"
45+
: "marked that this work item is not a duplicate",
46+
value: null,
47+
showConnector: false,
48+
}),
49+
assignees: ({ newValue, oldValue }) => ({
50+
action: newValue !== "" ? "added assignee" : "removed assignee",
51+
value: newValue !== "" ? newValue : oldValue,
52+
showConnector: false,
53+
}),
54+
start_date: ({ newValue }) => ({
55+
action: newValue !== "" ? "set start date" : "removed the start date",
56+
value: renderFormattedDate(newValue),
57+
showConnector: false,
58+
}),
59+
target_date: ({ newValue }) => ({
60+
action: newValue !== "" ? "set due date" : "removed the due date",
61+
value: renderFormattedDate(newValue),
62+
showConnector: false,
63+
}),
64+
labels: ({ newValue, oldValue }) => ({
65+
action: newValue !== "" ? "added label" : "removed label",
66+
value: newValue !== "" ? newValue : oldValue,
67+
showConnector: false,
68+
}),
69+
parent: ({ newValue, oldValue }) => ({
70+
action: newValue !== "" ? "added parent" : "removed parent",
71+
value: newValue !== "" ? newValue : oldValue,
72+
showConnector: false,
73+
}),
74+
relates_to: () => ({
75+
action: "marked that this work item is related to",
76+
value: null,
77+
showConnector: true,
78+
}),
79+
comment: ({ newValue }, renderCommentBox?: boolean) => ({
80+
action: "commented",
81+
value: renderCommentBox ? null : sanitizeCommentForNotification(newValue),
82+
showConnector: false,
83+
}),
84+
archived_at: ({ newValue }) => ({
85+
action: newValue === "restore" ? "restored the work item" : "archived the work item",
86+
value: null,
87+
showConnector: false,
88+
}),
89+
None: () => ({
90+
action: null,
91+
value: "the work item and assigned it to you.",
92+
showConnector: false,
93+
}),
94+
// Fields below only define value - action falls through to default handler
95+
attachment: () => ({
96+
action: null,
97+
value: "the work item",
98+
showConnector: true,
99+
}),
100+
description: ({ newValue }) => ({
101+
value: stripAndTruncateHTML(newValue || "", 55),
102+
showConnector: true,
103+
}),
104+
estimate_time: ({ newValue, oldValue }) => ({
105+
value:
106+
newValue !== ""
107+
? convertMinutesToHoursMinutesString(Number(newValue))
108+
: convertMinutesToHoursMinutesString(Number(oldValue)),
109+
showConnector: true,
110+
}),
111+
};
112+
113+
// Helper to get content details from maps
114+
const getNotificationContentDetails = (
115+
fieldData: TNotificationFieldData,
116+
renderCommentBox?: boolean
117+
): TNotificationContentDetails | null => {
118+
const { field } = fieldData;
119+
if (!field) return null;
120+
121+
// Check base map first
122+
const baseHandler = BASE_NOTIFICATION_CONTENT_MAP[field];
123+
if (baseHandler) {
124+
// Special case for comment field that needs renderCommentBox
125+
if (field === "comment") {
126+
return (baseHandler as (data: TNotificationFieldData, renderCommentBox?: boolean) => TNotificationContentDetails)(
127+
fieldData,
128+
renderCommentBox
129+
);
130+
}
131+
return baseHandler(fieldData);
132+
}
133+
134+
// Check additional map from plane-web (EE extensions)
135+
const additionalHandler = ADDITIONAL_NOTIFICATION_CONTENT_MAP[field];
136+
if (additionalHandler) {
137+
return additionalHandler(fieldData);
138+
}
139+
140+
return null;
141+
};
12142

13143
export function NotificationContent({
14144
notification,
@@ -29,79 +159,51 @@ export function NotificationContent({
29159
const oldValue = data?.issue_activity.old_value;
30160
const verb = data?.issue_activity.verb;
31161

162+
const fieldData: TNotificationFieldData = {
163+
field: notificationField,
164+
newValue,
165+
oldValue,
166+
verb,
167+
};
168+
32169
const renderTriggerName = () => (
33170
<span className="text-primary font-medium">
34171
{triggeredBy?.is_bot ? triggeredBy.first_name : triggeredBy?.display_name}{" "}
35172
</span>
36173
);
37174

38-
const renderAction = () => {
39-
if (!notificationField) return "";
40-
if (notificationField === "duplicate")
41-
return verb === "created"
42-
? "marked that this work item is a duplicate of"
43-
: "marked that this work item is not a duplicate";
44-
if (notificationField === "assignees") {
45-
return newValue !== "" ? "added assignee" : "removed assignee";
46-
}
47-
if (notificationField === "start_date") {
48-
return newValue !== "" ? "set start date" : "removed the start date";
49-
}
50-
if (notificationField === "target_date") {
51-
return newValue !== "" ? "set due date" : "removed the due date";
52-
}
53-
if (notificationField === "labels") {
54-
return newValue !== "" ? "added label" : "removed label";
55-
}
56-
if (notificationField === "parent") {
57-
return newValue !== "" ? "added parent" : "removed parent";
58-
}
59-
if (notificationField === "relates_to") return "marked that this work item is related to";
60-
if (notificationField === "comment") return "commented";
61-
if (notificationField === "archived_at") {
62-
return newValue === "restore" ? "restored the work item" : "archived the work item";
63-
}
64-
if (notificationField === "None") return null;
175+
// Get content details from map
176+
const contentDetails = getNotificationContentDetails(fieldData, renderCommentBox);
65177

66-
const baseAction = !["comment", "archived_at"].includes(notificationField) ? verb : "";
67-
return `${baseAction} ${replaceUnderscoreIfSnakeCase(notificationField)}`;
178+
// Render action - use map value if defined, otherwise fall through to default handler
179+
// Note: undefined = fall through to default, null = explicitly no action text
180+
const renderAction = (): ReactNode => {
181+
if (!notificationField) return "";
182+
// Check if action is explicitly defined in map (including null)
183+
if (contentDetails && "action" in contentDetails) return contentDetails.action;
184+
// Fallback to default action handler for fields not in map or without action defined
185+
return renderAdditionalAction(notificationField, verb);
68186
};
69187

70-
const renderValue = () => {
71-
if (notificationField === "None") return "the work item and assigned it to you.";
72-
if (notificationField === "comment") return renderCommentBox ? null : sanitizeCommentForNotification(newValue);
73-
if (notificationField === "target_date" || notificationField === "start_date") return renderFormattedDate(newValue);
74-
if (notificationField === "attachment") return "the work item";
75-
if (notificationField === "description") return stripAndTruncateHTML(newValue || "", 55);
76-
if (notificationField === "archived_at") return null;
77-
if (notificationField === "assignees") return newValue !== "" ? newValue : oldValue;
78-
if (notificationField === "labels") return newValue !== "" ? newValue : oldValue;
79-
if (notificationField === "parent") return newValue !== "" ? newValue : oldValue;
80-
if (notificationField === "estimate_time")
81-
return newValue !== ""
82-
? convertMinutesToHoursMinutesString(Number(newValue))
83-
: convertMinutesToHoursMinutesString(Number(oldValue));
84-
return newValue;
188+
// Render value - use map value if defined, otherwise fall through to default handler
189+
const renderValue = (): ReactNode => {
190+
// Check if value is explicitly defined in map
191+
if (contentDetails && "value" in contentDetails) return contentDetails.value;
192+
// Fallback to default value handler for fields not in map or without value defined
193+
return renderAdditionalValue(notificationField, newValue, oldValue);
85194
};
86195

87-
const shouldShowConnector = ![
88-
"comment",
89-
"archived_at",
90-
"None",
91-
"assignees",
92-
"labels",
93-
"start_date",
94-
"target_date",
95-
"parent",
96-
].includes(notificationField || "");
196+
// Determine if connector should be shown - prefer map value, fallback to function
197+
const showConnector =
198+
contentDetails?.showConnector !== undefined ? contentDetails.showConnector : shouldShowConnector(notificationField);
97199

98200
return (
99201
<>
100202
{renderTriggerName()}
101203
<span className="text-tertiary">{renderAction()} </span>
102204
{verb !== "deleted" && (
103205
<>
104-
{shouldShowConnector && <span className="text-tertiary">to </span>}
206+
{showConnector && <span className="text-tertiary">to </span>}
105207
<span className="text-primary font-medium">{renderValue()}</span>
106208
{notificationField === "comment" && renderCommentBox && (
107209
<div className="scale-75 origin-left">

packages/constants/src/issue/filter.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,10 @@ export const filterActivityOnSelectedFilters = (
353353
});
354354

355355
export const ENABLE_ISSUE_DEPENDENCIES = false;
356+
357+
export const BASE_ACTIVITY_FILTER_TYPES = [
358+
EActivityFilterType.ACTIVITY,
359+
EActivityFilterType.STATE,
360+
EActivityFilterType.ASSIGNEE,
361+
EActivityFilterType.DEFAULT,
362+
];

0 commit comments

Comments
 (0)