1+ import type { ReactNode } from "react" ;
12// plane imports
23import type { TNotification } from "@plane/types" ;
34import {
45 convertMinutesToHoursMinutesString ,
56 renderFormattedDate ,
67 sanitizeCommentForNotification ,
7- replaceUnderscoreIfSnakeCase ,
88 stripAndTruncateHTML ,
99} from "@plane/utils" ;
1010// components
1111import { 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
13143export 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" >
0 commit comments