diff --git a/apps/web/ce/components/workspace-notifications/notification-card/content.ts b/apps/web/ce/components/workspace-notifications/notification-card/content.ts new file mode 100644 index 000000000..74cdc6199 --- /dev/null +++ b/apps/web/ce/components/workspace-notifications/notification-card/content.ts @@ -0,0 +1,25 @@ +import { replaceUnderscoreIfSnakeCase } from "@plane/utils"; +import type { TNotificationContentMap } from "@/components/workspace-notifications/sidebar/notification-card/content"; + +// Additional notification content map for CE (empty - EE extends this) +export const ADDITIONAL_NOTIFICATION_CONTENT_MAP: TNotificationContentMap = {}; + +// Fallback action renderer for fields not in the map +export const renderAdditionalAction = (notificationField: string, verb: string | undefined) => { + const baseAction = !["comment", "archived_at"].includes(notificationField) ? verb : ""; + return `${baseAction} ${replaceUnderscoreIfSnakeCase(notificationField)}`; +}; + +// Fallback value renderer for fields not in the map +export const renderAdditionalValue = ( + _notificationField: string | undefined, + newValue: string | undefined, + _oldValue: string | undefined +) => newValue; + +export const shouldShowConnector = (notificationField: string | undefined) => + !["comment", "archived_at", "None", "assignees", "labels", "start_date", "target_date", "parent"].includes( + notificationField || "" + ); + +export const shouldRender = (notificationField: string | undefined, verb: string | undefined) => verb !== "deleted"; diff --git a/apps/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx b/apps/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx index 844034189..ec802686c 100644 --- a/apps/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx +++ b/apps/web/core/components/issues/issue-detail/issue-activity/activity-comment-root.tsx @@ -1,7 +1,7 @@ import { observer } from "mobx-react"; // plane imports -import type { E_SORT_ORDER, TActivityFilters } from "@plane/constants"; -import { EActivityFilterType, filterActivityOnSelectedFilters } from "@plane/constants"; +import type { E_SORT_ORDER, TActivityFilters, EActivityFilterType } from "@plane/constants"; +import { BASE_ACTIVITY_FILTER_TYPES, filterActivityOnSelectedFilters } from "@plane/constants"; import type { TCommentsOperations } from "@plane/types"; // components import { CommentCard } from "@/components/comments/card/root"; @@ -52,13 +52,6 @@ export const IssueActivityCommentRoot = observer(function IssueActivityCommentRo const filteredActivityAndComments = filterActivityOnSelectedFilters(activityAndComments, selectedFilters); - const BASE_ACTIVITY_FILTER_TYPES = [ - EActivityFilterType.ACTIVITY, - EActivityFilterType.STATE, - EActivityFilterType.ASSIGNEE, - EActivityFilterType.DEFAULT, - ]; - return (
{filteredActivityAndComments.map((activityComment, index) => { diff --git a/apps/web/core/components/workspace-notifications/sidebar/notification-card/content.tsx b/apps/web/core/components/workspace-notifications/sidebar/notification-card/content.tsx index 9871c6c04..f8feb54a8 100644 --- a/apps/web/core/components/workspace-notifications/sidebar/notification-card/content.tsx +++ b/apps/web/core/components/workspace-notifications/sidebar/notification-card/content.tsx @@ -1,14 +1,144 @@ +import type { ReactNode } from "react"; // plane imports import type { TNotification } from "@plane/types"; import { convertMinutesToHoursMinutesString, renderFormattedDate, sanitizeCommentForNotification, - replaceUnderscoreIfSnakeCase, stripAndTruncateHTML, } from "@plane/utils"; // components import { LiteTextEditor } from "@/components/editor/lite-text"; +import { + ADDITIONAL_NOTIFICATION_CONTENT_MAP, + renderAdditionalAction, + renderAdditionalValue, + shouldShowConnector, +} from "@/plane-web/components/workspace-notifications/notification-card/content"; + +// Types +export type TNotificationFieldData = { + field: string | undefined; + newValue: string | undefined; + oldValue: string | undefined; + verb: string | undefined; +}; + +export type TNotificationContentDetails = { + action?: ReactNode; + value?: ReactNode; + showConnector?: boolean; +}; + +export type TNotificationContentHandler = (data: TNotificationFieldData) => TNotificationContentDetails | null; + +export type TNotificationContentMap = { + [key: string]: TNotificationContentHandler; +}; + +// Base notification content map for core fields +export const BASE_NOTIFICATION_CONTENT_MAP: TNotificationContentMap = { + duplicate: ({ verb }) => ({ + action: + verb === "created" + ? "marked that this work item is a duplicate of" + : "marked that this work item is not a duplicate", + value: null, + showConnector: false, + }), + assignees: ({ newValue, oldValue }) => ({ + action: newValue !== "" ? "added assignee" : "removed assignee", + value: newValue !== "" ? newValue : oldValue, + showConnector: false, + }), + start_date: ({ newValue }) => ({ + action: newValue !== "" ? "set start date" : "removed the start date", + value: renderFormattedDate(newValue), + showConnector: false, + }), + target_date: ({ newValue }) => ({ + action: newValue !== "" ? "set due date" : "removed the due date", + value: renderFormattedDate(newValue), + showConnector: false, + }), + labels: ({ newValue, oldValue }) => ({ + action: newValue !== "" ? "added label" : "removed label", + value: newValue !== "" ? newValue : oldValue, + showConnector: false, + }), + parent: ({ newValue, oldValue }) => ({ + action: newValue !== "" ? "added parent" : "removed parent", + value: newValue !== "" ? newValue : oldValue, + showConnector: false, + }), + relates_to: () => ({ + action: "marked that this work item is related to", + value: null, + showConnector: true, + }), + comment: ({ newValue }, renderCommentBox?: boolean) => ({ + action: "commented", + value: renderCommentBox ? null : sanitizeCommentForNotification(newValue), + showConnector: false, + }), + archived_at: ({ newValue }) => ({ + action: newValue === "restore" ? "restored the work item" : "archived the work item", + value: null, + showConnector: false, + }), + None: () => ({ + action: null, + value: "the work item and assigned it to you.", + showConnector: false, + }), + // Fields below only define value - action falls through to default handler + attachment: () => ({ + action: null, + value: "the work item", + showConnector: true, + }), + description: ({ newValue }) => ({ + value: stripAndTruncateHTML(newValue || "", 55), + showConnector: true, + }), + estimate_time: ({ newValue, oldValue }) => ({ + value: + newValue !== "" + ? convertMinutesToHoursMinutesString(Number(newValue)) + : convertMinutesToHoursMinutesString(Number(oldValue)), + showConnector: true, + }), +}; + +// Helper to get content details from maps +const getNotificationContentDetails = ( + fieldData: TNotificationFieldData, + renderCommentBox?: boolean +): TNotificationContentDetails | null => { + const { field } = fieldData; + if (!field) return null; + + // Check base map first + const baseHandler = BASE_NOTIFICATION_CONTENT_MAP[field]; + if (baseHandler) { + // Special case for comment field that needs renderCommentBox + if (field === "comment") { + return (baseHandler as (data: TNotificationFieldData, renderCommentBox?: boolean) => TNotificationContentDetails)( + fieldData, + renderCommentBox + ); + } + return baseHandler(fieldData); + } + + // Check additional map from plane-web (EE extensions) + const additionalHandler = ADDITIONAL_NOTIFICATION_CONTENT_MAP[field]; + if (additionalHandler) { + return additionalHandler(fieldData); + } + + return null; +}; export function NotificationContent({ notification, @@ -29,71 +159,43 @@ export function NotificationContent({ const oldValue = data?.issue_activity.old_value; const verb = data?.issue_activity.verb; + const fieldData: TNotificationFieldData = { + field: notificationField, + newValue, + oldValue, + verb, + }; + const renderTriggerName = () => ( {triggeredBy?.is_bot ? triggeredBy.first_name : triggeredBy?.display_name}{" "} ); - const renderAction = () => { + // Get content details from map + const contentDetails = getNotificationContentDetails(fieldData, renderCommentBox); + + // Render action - use map value if defined, otherwise fall through to default handler + // Note: undefined = fall through to default, null = explicitly no action text + const renderAction = (): ReactNode => { if (!notificationField) return ""; - if (notificationField === "duplicate") - return verb === "created" - ? "marked that this work item is a duplicate of" - : "marked that this work item is not a duplicate"; - if (notificationField === "assignees") { - return newValue !== "" ? "added assignee" : "removed assignee"; - } - if (notificationField === "start_date") { - return newValue !== "" ? "set start date" : "removed the start date"; - } - if (notificationField === "target_date") { - return newValue !== "" ? "set due date" : "removed the due date"; - } - if (notificationField === "labels") { - return newValue !== "" ? "added label" : "removed label"; - } - if (notificationField === "parent") { - return newValue !== "" ? "added parent" : "removed parent"; - } - if (notificationField === "relates_to") return "marked that this work item is related to"; - if (notificationField === "comment") return "commented"; - if (notificationField === "archived_at") { - return newValue === "restore" ? "restored the work item" : "archived the work item"; - } - if (notificationField === "None") return null; - - const baseAction = !["comment", "archived_at"].includes(notificationField) ? verb : ""; - return `${baseAction} ${replaceUnderscoreIfSnakeCase(notificationField)}`; + // Check if action is explicitly defined in map (including null) + if (contentDetails && "action" in contentDetails) return contentDetails.action; + // Fallback to default action handler for fields not in map or without action defined + return renderAdditionalAction(notificationField, verb); }; - const renderValue = () => { - if (notificationField === "None") return "the work item and assigned it to you."; - if (notificationField === "comment") return renderCommentBox ? null : sanitizeCommentForNotification(newValue); - if (notificationField === "target_date" || notificationField === "start_date") return renderFormattedDate(newValue); - if (notificationField === "attachment") return "the work item"; - if (notificationField === "description") return stripAndTruncateHTML(newValue || "", 55); - if (notificationField === "archived_at") return null; - if (notificationField === "assignees") return newValue !== "" ? newValue : oldValue; - if (notificationField === "labels") return newValue !== "" ? newValue : oldValue; - if (notificationField === "parent") return newValue !== "" ? newValue : oldValue; - if (notificationField === "estimate_time") - return newValue !== "" - ? convertMinutesToHoursMinutesString(Number(newValue)) - : convertMinutesToHoursMinutesString(Number(oldValue)); - return newValue; + // Render value - use map value if defined, otherwise fall through to default handler + const renderValue = (): ReactNode => { + // Check if value is explicitly defined in map + if (contentDetails && "value" in contentDetails) return contentDetails.value; + // Fallback to default value handler for fields not in map or without value defined + return renderAdditionalValue(notificationField, newValue, oldValue); }; - const shouldShowConnector = ![ - "comment", - "archived_at", - "None", - "assignees", - "labels", - "start_date", - "target_date", - "parent", - ].includes(notificationField || ""); + // Determine if connector should be shown - prefer map value, fallback to function + const showConnector = + contentDetails?.showConnector !== undefined ? contentDetails.showConnector : shouldShowConnector(notificationField); return ( <> @@ -101,7 +203,7 @@ export function NotificationContent({ {renderAction()} {verb !== "deleted" && ( <> - {shouldShowConnector && to } + {showConnector && to } {renderValue()} {notificationField === "comment" && renderCommentBox && (
diff --git a/packages/constants/src/issue/filter.ts b/packages/constants/src/issue/filter.ts index 5a8c35a4f..126f88609 100644 --- a/packages/constants/src/issue/filter.ts +++ b/packages/constants/src/issue/filter.ts @@ -353,3 +353,10 @@ export const filterActivityOnSelectedFilters = ( }); export const ENABLE_ISSUE_DEPENDENCIES = false; + +export const BASE_ACTIVITY_FILTER_TYPES = [ + EActivityFilterType.ACTIVITY, + EActivityFilterType.STATE, + EActivityFilterType.ASSIGNEE, + EActivityFilterType.DEFAULT, +];