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,
+];