[WEB-5574]chore: notification card refactor (#8234)
* chore: notification card refactor * chore: moved base activity types to constants package
This commit is contained in:
parent
3c8624b1ba
commit
5499e49b72
4 changed files with 192 additions and 65 deletions
|
|
@ -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";
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import type { E_SORT_ORDER, TActivityFilters } from "@plane/constants";
|
import type { E_SORT_ORDER, TActivityFilters, EActivityFilterType } from "@plane/constants";
|
||||||
import { EActivityFilterType, filterActivityOnSelectedFilters } from "@plane/constants";
|
import { BASE_ACTIVITY_FILTER_TYPES, filterActivityOnSelectedFilters } from "@plane/constants";
|
||||||
import type { TCommentsOperations } from "@plane/types";
|
import type { TCommentsOperations } from "@plane/types";
|
||||||
// components
|
// components
|
||||||
import { CommentCard } from "@/components/comments/card/root";
|
import { CommentCard } from "@/components/comments/card/root";
|
||||||
|
|
@ -52,13 +52,6 @@ export const IssueActivityCommentRoot = observer(function IssueActivityCommentRo
|
||||||
|
|
||||||
const filteredActivityAndComments = filterActivityOnSelectedFilters(activityAndComments, selectedFilters);
|
const filteredActivityAndComments = filterActivityOnSelectedFilters(activityAndComments, selectedFilters);
|
||||||
|
|
||||||
const BASE_ACTIVITY_FILTER_TYPES = [
|
|
||||||
EActivityFilterType.ACTIVITY,
|
|
||||||
EActivityFilterType.STATE,
|
|
||||||
EActivityFilterType.ASSIGNEE,
|
|
||||||
EActivityFilterType.DEFAULT,
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{filteredActivityAndComments.map((activityComment, index) => {
|
{filteredActivityAndComments.map((activityComment, index) => {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,144 @@
|
||||||
|
import type { ReactNode } from "react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import type { TNotification } from "@plane/types";
|
import type { TNotification } from "@plane/types";
|
||||||
import {
|
import {
|
||||||
convertMinutesToHoursMinutesString,
|
convertMinutesToHoursMinutesString,
|
||||||
renderFormattedDate,
|
renderFormattedDate,
|
||||||
sanitizeCommentForNotification,
|
sanitizeCommentForNotification,
|
||||||
replaceUnderscoreIfSnakeCase,
|
|
||||||
stripAndTruncateHTML,
|
stripAndTruncateHTML,
|
||||||
} from "@plane/utils";
|
} from "@plane/utils";
|
||||||
// components
|
// components
|
||||||
import { LiteTextEditor } from "@/components/editor/lite-text";
|
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({
|
export function NotificationContent({
|
||||||
notification,
|
notification,
|
||||||
|
|
@ -29,71 +159,43 @@ export function NotificationContent({
|
||||||
const oldValue = data?.issue_activity.old_value;
|
const oldValue = data?.issue_activity.old_value;
|
||||||
const verb = data?.issue_activity.verb;
|
const verb = data?.issue_activity.verb;
|
||||||
|
|
||||||
|
const fieldData: TNotificationFieldData = {
|
||||||
|
field: notificationField,
|
||||||
|
newValue,
|
||||||
|
oldValue,
|
||||||
|
verb,
|
||||||
|
};
|
||||||
|
|
||||||
const renderTriggerName = () => (
|
const renderTriggerName = () => (
|
||||||
<span className="text-primary font-medium">
|
<span className="text-primary font-medium">
|
||||||
{triggeredBy?.is_bot ? triggeredBy.first_name : triggeredBy?.display_name}{" "}
|
{triggeredBy?.is_bot ? triggeredBy.first_name : triggeredBy?.display_name}{" "}
|
||||||
</span>
|
</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) return "";
|
||||||
if (notificationField === "duplicate")
|
// Check if action is explicitly defined in map (including null)
|
||||||
return verb === "created"
|
if (contentDetails && "action" in contentDetails) return contentDetails.action;
|
||||||
? "marked that this work item is a duplicate of"
|
// Fallback to default action handler for fields not in map or without action defined
|
||||||
: "marked that this work item is not a duplicate";
|
return renderAdditionalAction(notificationField, verb);
|
||||||
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)}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderValue = () => {
|
// Render value - use map value if defined, otherwise fall through to default handler
|
||||||
if (notificationField === "None") return "the work item and assigned it to you.";
|
const renderValue = (): ReactNode => {
|
||||||
if (notificationField === "comment") return renderCommentBox ? null : sanitizeCommentForNotification(newValue);
|
// Check if value is explicitly defined in map
|
||||||
if (notificationField === "target_date" || notificationField === "start_date") return renderFormattedDate(newValue);
|
if (contentDetails && "value" in contentDetails) return contentDetails.value;
|
||||||
if (notificationField === "attachment") return "the work item";
|
// Fallback to default value handler for fields not in map or without value defined
|
||||||
if (notificationField === "description") return stripAndTruncateHTML(newValue || "", 55);
|
return renderAdditionalValue(notificationField, newValue, oldValue);
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldShowConnector = ![
|
// Determine if connector should be shown - prefer map value, fallback to function
|
||||||
"comment",
|
const showConnector =
|
||||||
"archived_at",
|
contentDetails?.showConnector !== undefined ? contentDetails.showConnector : shouldShowConnector(notificationField);
|
||||||
"None",
|
|
||||||
"assignees",
|
|
||||||
"labels",
|
|
||||||
"start_date",
|
|
||||||
"target_date",
|
|
||||||
"parent",
|
|
||||||
].includes(notificationField || "");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -101,7 +203,7 @@ export function NotificationContent({
|
||||||
<span className="text-tertiary">{renderAction()} </span>
|
<span className="text-tertiary">{renderAction()} </span>
|
||||||
{verb !== "deleted" && (
|
{verb !== "deleted" && (
|
||||||
<>
|
<>
|
||||||
{shouldShowConnector && <span className="text-tertiary">to </span>}
|
{showConnector && <span className="text-tertiary">to </span>}
|
||||||
<span className="text-primary font-medium">{renderValue()}</span>
|
<span className="text-primary font-medium">{renderValue()}</span>
|
||||||
{notificationField === "comment" && renderCommentBox && (
|
{notificationField === "comment" && renderCommentBox && (
|
||||||
<div className="scale-75 origin-left">
|
<div className="scale-75 origin-left">
|
||||||
|
|
|
||||||
|
|
@ -353,3 +353,10 @@ export const filterActivityOnSelectedFilters = (
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ENABLE_ISSUE_DEPENDENCIES = false;
|
export const ENABLE_ISSUE_DEPENDENCIES = false;
|
||||||
|
|
||||||
|
export const BASE_ACTIVITY_FILTER_TYPES = [
|
||||||
|
EActivityFilterType.ACTIVITY,
|
||||||
|
EActivityFilterType.STATE,
|
||||||
|
EActivityFilterType.ASSIGNEE,
|
||||||
|
EActivityFilterType.DEFAULT,
|
||||||
|
];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue