[WEB-5574]chore: notification card refactor (#8234)

* chore: notification card refactor

* chore: moved base activity types to constants package
This commit is contained in:
Vamsi Krishna 2025-12-24 20:32:50 +05:30 committed by GitHub
parent 3c8624b1ba
commit 5499e49b72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 192 additions and 65 deletions

View file

@ -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";

View file

@ -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 (
<div>
{filteredActivityAndComments.map((activityComment, index) => {

View file

@ -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 = () => (
<span className="text-primary font-medium">
{triggeredBy?.is_bot ? triggeredBy.first_name : triggeredBy?.display_name}{" "}
</span>
);
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({
<span className="text-tertiary">{renderAction()} </span>
{verb !== "deleted" && (
<>
{shouldShowConnector && <span className="text-tertiary">to </span>}
{showConnector && <span className="text-tertiary">to </span>}
<span className="text-primary font-medium">{renderValue()}</span>
{notificationField === "comment" && renderCommentBox && (
<div className="scale-75 origin-left">

View file

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