From a3d2b0e4f3d87719f388f4548bb33e4547836935 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Thu, 9 Oct 2025 10:30:37 +0545 Subject: [PATCH 01/19] feat(OUT-2459): add main page for notification center --- src/app/notification-center/page.tsx | 32 ++++++++++++++++++++++++++++ src/types/common.ts | 22 +++++++++++++++++++ src/utils/CopilotAPI.ts | 15 +++++++++++++ src/utils/redirect.ts | 13 ++++++++--- 4 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 src/app/notification-center/page.tsx diff --git a/src/app/notification-center/page.tsx b/src/app/notification-center/page.tsx new file mode 100644 index 000000000..beacf6655 --- /dev/null +++ b/src/app/notification-center/page.tsx @@ -0,0 +1,32 @@ +import { SilentError } from '@/components/templates/SilentError' +import { NotificationInProductCtaParamsSchema } from '@/types/common' +import { UserType } from '@/types/interfaces' +import { CopilotAPI } from '@/utils/CopilotAPI' +import { redirectIfTaskCta } from '@/utils/redirect' +import z from 'zod' + +async function getNotificationDetail(token: string) { + const copilot = new CopilotAPI(token) + const tokenPayload = await copilot.getTokenPayload() + + if (!tokenPayload) throw new Error('Failed to get token payload') + + return await copilot.getIUNotification(z.string().parse(tokenPayload.notificationId), tokenPayload.workspaceId) // notification "id" is expected in tokenPayload +} + +export default async function NotificationCenter({ searchParams }: { searchParams: { token: string } }) { + const token = searchParams.token + if (!z.string().safeParse(token).success) { + return + } + + const notificationDetail = await getNotificationDetail(token) + if (!notificationDetail) return + + const params = NotificationInProductCtaParamsSchema.parse(notificationDetail.deliveryTargets?.inProduct?.ctaParams) + + redirectIfTaskCta(params, UserType.INTERNAL_USER, true) + + // Silent Error is shown if redirect fails. Only possible reason for redirect to not work can be of the taskId not found + return +} diff --git a/src/types/common.ts b/src/types/common.ts index 50d97908b..e03975666 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -22,6 +22,7 @@ export const TokenSchema = z.object({ .optional(), internalUserId: z.string().optional(), workspaceId: z.string(), + notificationId: z.string().optional(), }) export type Token = z.infer @@ -276,3 +277,24 @@ export type UrlActionParamsType = { pf?: string oldPf?: string } + +export const NotificationInProductCtaParamsSchema = z.object({ + taskId: z.string(), + commentId: z.string().optional(), +}) + +export const NotificationResponseSchema = z.object({ + deliveryTargets: z + .object({ + inProduct: z + .object({ + title: z.string().optional(), + isRead: z.boolean().optional(), + ctaParams: NotificationInProductCtaParamsSchema.optional(), + }) + .optional(), + }) + .optional(), + id: z.string(), +}) +export type NotificationResponseType = z.infer diff --git a/src/utils/CopilotAPI.ts b/src/utils/CopilotAPI.ts index afcd7db76..2147da160 100644 --- a/src/utils/CopilotAPI.ts +++ b/src/utils/CopilotAPI.ts @@ -29,6 +29,8 @@ import { NotificationCreatedResponse, NotificationCreatedResponseSchema, NotificationRequestBody, + NotificationResponseSchema, + NotificationResponseType, Token, TokenSchema, WorkspaceResponse, @@ -265,6 +267,18 @@ export class CopilotAPI { await Promise.all(deletePromises) } + async _getIUNotification(id: string, workspaceId: string): Promise { + console.info('CopilotAPI#_deleteNotification', this.token) + const response = await this.manualFetch( + `notifications/${id}`, + { + includeRead: 'true', + }, + workspaceId, + ) + return NotificationResponseSchema.parse(response.data) + } + async _getClientNotifications( recipientClientId: string, recipientCompanyId: string, @@ -346,4 +360,5 @@ export class CopilotAPI { deleteNotification = this.wrapWithRetry(this._deleteNotification) bulkDeleteNotifications = this.wrapWithRetry(this._bulkDeleteNotifications) manualFetch = this.wrapWithRetry(this._manualFetch) + getIUNotification = this.wrapWithRetry(this._getIUNotification) } diff --git a/src/utils/redirect.ts b/src/utils/redirect.ts index f03e861eb..66509a169 100644 --- a/src/utils/redirect.ts +++ b/src/utils/redirect.ts @@ -5,17 +5,24 @@ import { redirect } from 'next/navigation' import { z } from 'zod' import { UserType } from '@/types/interfaces' -export const redirectIfTaskCta = (searchParams: Record, userType: UserType) => { +export const redirectIfTaskCta = ( + searchParams: Record, + userType: UserType, + fromNotificationCenter: boolean = false, +) => { const taskId = z.string().safeParse(searchParams.taskId) const commentId = z.string().safeParse(searchParams.commentId) if (taskId.data) { + const notificationCenterParam = fromNotificationCenter ? '&fromNotificationCenter=1' : '' if (commentId.data) { redirect( - `${apiUrl}/detail/${taskId.data}/${userType}?token=${z.string().parse(searchParams.token)}&commentId=${commentId.data}&isRedirect=1`, + `${apiUrl}/detail/${taskId.data}/${userType}?token=${z.string().parse(searchParams.token)}&commentId=${commentId.data}&isRedirect=1${notificationCenterParam}`, ) } - redirect(`${apiUrl}/detail/${taskId.data}/${userType}?token=${z.string().parse(searchParams.token)}&isRedirect=1`) + redirect( + `${apiUrl}/detail/${taskId.data}/${userType}?token=${z.string().parse(searchParams.token)}&isRedirect=1${notificationCenterParam}`, + ) } } From ca4a7b8373e9d168c2ae2f788aabba03c7526195 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Thu, 9 Oct 2025 10:33:23 +0545 Subject: [PATCH 02/19] wip(OUT-2459): UI changes for request redirect from notification center --- src/app/detail/[task_id]/[user_type]/page.tsx | 19 +++++- src/app/detail/ui/ResponsiveStack.tsx | 24 ++++++-- src/app/detail/ui/Sidebar.tsx | 59 ++++++++++++------- src/app/detail/ui/ToggleController.tsx | 4 +- src/redux/features/taskDetailsSlice.ts | 6 ++ 5 files changed, 81 insertions(+), 31 deletions(-) diff --git a/src/app/detail/[task_id]/[user_type]/page.tsx b/src/app/detail/[task_id]/[user_type]/page.tsx index 9a9b4b3a3..5e972ac66 100644 --- a/src/app/detail/[task_id]/[user_type]/page.tsx +++ b/src/app/detail/[task_id]/[user_type]/page.tsx @@ -80,7 +80,7 @@ export default async function TaskDetailPage({ searchParams, }: { params: { task_id: string; task_name: string; user_type: UserType } - searchParams: { token: string; isRedirect?: string } + searchParams: { token: string; isRedirect?: string; fromNotificationCenter?: string } }) { const { token } = searchParams const { task_id, user_type } = params @@ -115,6 +115,8 @@ export default async function TaskDetailPage({ href: `/detail/${id}/${user_type}?token=${token}`, })) + const fromNotificationCenter = !!searchParams.fromNotificationCenter + return ( } - + {isPreviewMode ? ( @@ -203,7 +205,18 @@ export default async function TaskDetailPage({ - + { +export const ResponsiveStack = ({ + children, + fromNotificationCenter, +}: { + children: ReactNode + fromNotificationCenter: boolean +}) => { const { showSidebar } = useSelector(selectTaskDetails) + useEffect(() => { + store.dispatch(setFromNotificationCenter(fromNotificationCenter)) + }, [fromNotificationCenter]) + return ( - + {children} ) diff --git a/src/app/detail/ui/Sidebar.tsx b/src/app/detail/ui/Sidebar.tsx index 55b13edc2..f32ef8672 100644 --- a/src/app/detail/ui/Sidebar.tsx +++ b/src/app/detail/ui/Sidebar.tsx @@ -24,15 +24,22 @@ import { createDateFromFormattedDateString, formatDate } from '@/utils/dateHelpe import { getSelectedUserIds, getSelectorAssignee, getSelectorAssigneeFromTask } from '@/utils/selector' import { NoAssignee } from '@/utils/noAssignee' import { shouldConfirmBeforeReassignment } from '@/utils/shouldConfirmBeforeReassign' -import { Box, Skeleton, Stack, styled, Typography } from '@mui/material' +import { Box, Skeleton, Stack, styled, SxProps, Typography } from '@mui/material' import { useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { z } from 'zod' import { ClientDetailAppBridge } from '@/app/detail/ui/ClientDetailAppBridge' -const StyledText = styled(Typography)(({ theme }) => ({ +type StyledTypographyProps = { + display?: string +} + +const StyledText = styled(Typography, { + shouldForwardProp: (prop: string) => prop !== 'display', // don't pass to DOM +})(({ theme, display }) => ({ color: theme.color.gray[500], width: '80px', + display, })) export const Sidebar = ({ @@ -57,7 +64,7 @@ export const Sidebar = ({ portalUrl?: string }) => { const { activeTask, workflowStates, assignee, previewMode } = useSelector(selectTaskBoard) - const { showSidebar, showConfirmAssignModal } = useSelector(selectTaskDetails) + const { showSidebar, showConfirmAssignModal, fromNotificationCenter } = useSelector(selectTaskDetails) const [isHydrated, setIsHydrated] = useState(false) @@ -262,26 +269,34 @@ export const Sidebar = ({ return ( `1px solid ${theme.color.borders.border2}`, - height: '100vh', - display: showSidebar ? 'block' : 'none', - width: isMobile && showSidebar ? '100vw' : '25vw', - }} + {...(!fromNotificationCenter && { + sx: { + borderLeft: (theme) => `1px solid ${theme.color.borders.border2}`, + height: '100vh', + display: showSidebar ? 'block' : 'none', + width: isMobile && showSidebar ? '100vw' : '25vw', + }, + })} > - - - - - Properties - - - - + {!fromNotificationCenter && ( + + + + + Properties + + + + + )} - + - + Status {workflowStates.length > 0 && statusValue ? ( // show skelete if statusValue and workflow state list is empty @@ -312,7 +327,7 @@ export const Sidebar = ({ )} - + Assignee {assignee.length > 0 ? ( // show skeleton if assignee list is empty @@ -363,7 +378,7 @@ export const Sidebar = ({ )} - + Due date { - const { showSidebar } = useSelector(selectTaskDetails) + const { showSidebar, fromNotificationCenter } = useSelector(selectTaskDetails) const matches = useMediaQuery('(max-width:600px)') const nonMobile = useMediaQuery('(min-width:600px)') @@ -21,7 +21,7 @@ export const ToggleController = ({ children }: { children: ReactNode }) => { return ( { state.expandedComments = action.payload }, + setFromNotificationCenter: (state, action: { payload: boolean }) => { + state.fromNotificationCenter = action.payload + }, }, }) @@ -62,6 +67,7 @@ export const { toggleShowConfirmAssignModal, setOpenImage, setExpandedComments, + setFromNotificationCenter, } = taskDetailsSlice.actions export default taskDetailsSlice.reducer From aecf94ca4c0de7c18c0f1f7cf291dae77c27e092 Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Fri, 10 Oct 2025 15:07:41 +0545 Subject: [PATCH 03/19] feat(OUT-2459): Notification center view changes in sidebar and notification response data from assembly api --- src/app/detail/ui/Sidebar.tsx | 51 ++++++++++++---------------- src/app/notification-center/page.tsx | 2 +- src/utils/CopilotAPI.ts | 2 +- 3 files changed, 23 insertions(+), 32 deletions(-) diff --git a/src/app/detail/ui/Sidebar.tsx b/src/app/detail/ui/Sidebar.tsx index f32ef8672..0b198e83c 100644 --- a/src/app/detail/ui/Sidebar.tsx +++ b/src/app/detail/ui/Sidebar.tsx @@ -11,7 +11,6 @@ import { ConfirmUI } from '@/components/layouts/ConfirmUI' import { AppMargin, SizeofAppMargin } from '@/hoc/AppMargin' import { useHandleSelectorComponent } from '@/hooks/useHandleSelectorComponent' import { useWindowWidth } from '@/hooks/useWindowWidth' -import { AssigneePlaceholder } from '@/icons' import { selectTaskBoard } from '@/redux/features/taskBoardSlice' import { selectTaskDetails, setShowSidebar, toggleShowConfirmAssignModal } from '@/redux/features/taskDetailsSlice' import store from '@/redux/store' @@ -149,14 +148,14 @@ export const Sidebar = ({ } } - if (!showSidebar) { + if (!showSidebar || fromNotificationCenter) { return ( `1px solid ${theme.color.borders.border2}`, - height: '100vh', - display: showSidebar ? 'block' : 'none', - width: isMobile && showSidebar ? '100vw' : '25vw', - }, - })} + sx={{ + borderLeft: (theme) => `1px solid ${theme.color.borders.border2}`, + height: '100vh', + display: showSidebar ? 'block' : 'none', + width: isMobile && showSidebar ? '100vw' : '25vw', + }} > - {!fromNotificationCenter && ( - - - - - Properties - - - - - )} + + + + + Properties + + + + - + - + Status {workflowStates.length > 0 && statusValue ? ( // show skelete if statusValue and workflow state list is empty @@ -327,7 +318,7 @@ export const Sidebar = ({ )} - + Assignee {assignee.length > 0 ? ( // show skeleton if assignee list is empty @@ -378,7 +369,7 @@ export const Sidebar = ({ )} - + Due date diff --git a/src/utils/CopilotAPI.ts b/src/utils/CopilotAPI.ts index 2147da160..23f2a098a 100644 --- a/src/utils/CopilotAPI.ts +++ b/src/utils/CopilotAPI.ts @@ -276,7 +276,7 @@ export class CopilotAPI { }, workspaceId, ) - return NotificationResponseSchema.parse(response.data) + return NotificationResponseSchema.parse(response) } async _getClientNotifications( From ffc05a4c2869910dbe06e06342350ab7ab28602a Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Fri, 10 Oct 2025 16:21:05 +0545 Subject: [PATCH 04/19] feat(OUT-2461): highlight comment card container on notifications --- src/app/detail/ui/Comments.tsx | 1 + src/app/globals.css | 13 +++++++++++++ src/components/cards/CommentCard.tsx | 3 +++ src/hooks/useScrollToElement.ts | 28 +++++++++++++++++++--------- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/app/detail/ui/Comments.tsx b/src/app/detail/ui/Comments.tsx index c6d39ba64..6b60a9112 100644 --- a/src/app/detail/ui/Comments.tsx +++ b/src/app/detail/ui/Comments.tsx @@ -40,6 +40,7 @@ export const Comments = ({ comment, createComment, deleteComment, task_id, stabl /> void @@ -57,6 +58,7 @@ export const CommentCard = ({ task_id: string optimisticUpdates: OptimisticUpdate[] commentInitiator: IAssigneeCombined | undefined + 'data-comment-card'?: string }) => { const [showReply, setShowReply] = useState(false) const [isHovered, setIsHovered] = useState(false) @@ -193,6 +195,7 @@ export const CommentCard = ({ }, [comment]) return ( (isReadOnly ? `${theme.color.gray[100]}` : `${theme.color.base.white}`), overflow: 'hidden', diff --git a/src/hooks/useScrollToElement.ts b/src/hooks/useScrollToElement.ts index 8a4494f4f..b37d02730 100644 --- a/src/hooks/useScrollToElement.ts +++ b/src/hooks/useScrollToElement.ts @@ -18,15 +18,25 @@ const useScrollToElement = (paramName: string) => { const scrollToElement = () => { const targetElement = document.getElementById(elementId) - if (targetElement) { - setTimeout(() => { - targetElement.scrollIntoView({ - behavior: 'smooth', - block: 'center', - }) - }, 100) - observer.disconnect() - } + if (!targetElement) return + const commentCard = targetElement.querySelector('[data-comment-card]') as HTMLElement | null + + setTimeout(() => { + targetElement.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }) + }, 100) + + setTimeout(() => { + if (commentCard) { + commentCard.classList.add('highlight-fade') + setTimeout(() => { + commentCard.classList.remove('highlight-fade') + }, 500) + } + }, 600) + observer.disconnect() } scrollToElement() From 9ca138cb79d932b33689d19a1761c40b5ec9d79e Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Fri, 10 Oct 2025 17:48:11 +0545 Subject: [PATCH 05/19] fix(OUT-2461): highlight animation time on comments --- src/app/globals.css | 9 ++++++--- src/hooks/useScrollToElement.ts | 3 --- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 008f66e68..1d76efcf3 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -89,14 +89,17 @@ a { } .highlight-fade { - animation: fadeHighlight 0.5s forwards; + animation: fadeHighlight 1s forwards; } @keyframes fadeHighlight { - from { + 0% { + background-color: transparent; + } + 50% { background-color: rgba(252, 243, 200, 0.5); } - to { + 100% { background-color: transparent; } } diff --git a/src/hooks/useScrollToElement.ts b/src/hooks/useScrollToElement.ts index b37d02730..37747bfb2 100644 --- a/src/hooks/useScrollToElement.ts +++ b/src/hooks/useScrollToElement.ts @@ -31,9 +31,6 @@ const useScrollToElement = (paramName: string) => { setTimeout(() => { if (commentCard) { commentCard.classList.add('highlight-fade') - setTimeout(() => { - commentCard.classList.remove('highlight-fade') - }, 500) } }, 600) observer.disconnect() From 5c61347106f6a225b8467dd392ebde40c22fd4de Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Wed, 5 Nov 2025 14:23:51 +0545 Subject: [PATCH 06/19] fix(OUT-2555): disabled changing assignee/ viewer from notification-center-view --- src/app/detail/ui/Sidebar.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/detail/ui/Sidebar.tsx b/src/app/detail/ui/Sidebar.tsx index b02459139..e351afed7 100644 --- a/src/app/detail/ui/Sidebar.tsx +++ b/src/app/detail/ui/Sidebar.tsx @@ -269,11 +269,11 @@ export const Sidebar = ({ } buttonContent={ @@ -309,11 +309,11 @@ export const Sidebar = ({ hideIusList name="Set client visibility" onChange={handleTaskViewerChange} - disabled={disabled && !previewMode} + disabled={(disabled && !previewMode) || fromNotificationCenter} initialValue={taskViewerValue || undefined} buttonContent={ } buttonContent={ @@ -482,11 +482,11 @@ export const Sidebar = ({ } outlined={true} @@ -537,11 +537,11 @@ export const Sidebar = ({ hideIusList name="Set client visibility" onChange={handleTaskViewerChange} - disabled={disabled && !previewMode} // allow visibility change in preview mode + disabled={(disabled && !previewMode) || fromNotificationCenter} // allow visibility change in preview mode initialValue={taskViewerValue || undefined} buttonContent={ } outlined={true} From 21b930babf6fa039ed25efc2472eab5f26146e24 Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Wed, 5 Nov 2025 15:14:17 +0545 Subject: [PATCH 07/19] fix(OUT-2554): redirection to board from notification-center-view on task deletion. If task is deleted in real time, the notification-center-view was getting redirected to board and the notification for IU was not being deleted. - Added checks in redirection logic to return early if the user is from notification-center. - Disabled "Goto Tasks" button on No Tasks found page. Although this might not happen, being extra secure here. - Added missing logic to delete notification for IUs if the task is deleted. --- .../api/tasks/task-notifications.service.ts | 4 +- .../layouts/DeletedTaskRedirectPage.tsx | 42 +++++++++++-------- src/hoc/RealTime.tsx | 6 ++- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/app/api/tasks/task-notifications.service.ts b/src/app/api/tasks/task-notifications.service.ts index acf1cf88b..19e0545e8 100644 --- a/src/app/api/tasks/task-notifications.service.ts +++ b/src/app/api/tasks/task-notifications.service.ts @@ -26,8 +26,10 @@ export class TaskNotificationsService extends BaseService { const handleNotificationRead = { [AssigneeType.client]: this.notificationService.markClientNotificationAsRead, [AssigneeType.company]: this.notificationService.markAsReadForAllRecipients, + [AssigneeType.internalUser]: (task: Task) => + this.notificationService.deleteInternalUserNotificationsForTask(task.id), } - // @ts-expect-error This is completely safe + await handleNotificationRead[task?.assigneeType]?.(task) } } diff --git a/src/components/layouts/DeletedTaskRedirectPage.tsx b/src/components/layouts/DeletedTaskRedirectPage.tsx index 22529275d..7dec096e5 100644 --- a/src/components/layouts/DeletedTaskRedirectPage.tsx +++ b/src/components/layouts/DeletedTaskRedirectPage.tsx @@ -1,12 +1,15 @@ import { UserRole } from '@/app/api/core/types/user' import { AppMargin, SizeofAppMargin } from '@/hoc/AppMargin' import { TasksListIcon } from '@/icons' +import { selectTaskDetails } from '@/redux/features/taskDetailsSlice' import { SxCenter } from '@/utils/mui' import { Box, Button, Stack, Typography } from '@mui/material' import Link from 'next/link' +import { useSelector } from 'react-redux' import { z } from 'zod' export const DeletedTaskRedirectPage = ({ userType, token }: { userType: UserRole; token: string }) => { + const { fromNotificationCenter } = useSelector(selectTaskDetails) return ( <> @@ -39,24 +42,27 @@ export const DeletedTaskRedirectPage = ({ userType, token }: { userType: UserRol This task has been deleted. You can view your other tasks on the Tasks homepage. - - - + {/* disable board navigation on notification-center-view */} + {!fromNotificationCenter && ( + + + + )} diff --git a/src/hoc/RealTime.tsx b/src/hoc/RealTime.tsx index d3478bae2..240101b23 100644 --- a/src/hoc/RealTime.tsx +++ b/src/hoc/RealTime.tsx @@ -3,6 +3,7 @@ import { RealtimeHandler } from '@/lib/realtime' import { supabase } from '@/lib/supabase' import { selectTaskBoard } from '@/redux/features/taskBoardSlice' +import { selectTaskDetails } from '@/redux/features/taskDetailsSlice' import { Token } from '@/types/common' import { TaskResponse } from '@/types/dto/tasks.dto' import { AssigneeType } from '@prisma/client' @@ -29,7 +30,7 @@ export const RealTime = ({ tokenPayload: Token }) => { const { tasks, accessibleTasks, token, activeTask, assignee, accesibleTaskIds } = useSelector(selectTaskBoard) - const { showUnarchived, showArchived } = useSelector(selectTaskBoard) + const { fromNotificationCenter } = useSelector(selectTaskDetails) const pathname = usePathname() const router = useRouter() @@ -46,7 +47,8 @@ export const RealTime = ({ } const redirectToBoard = (updatedTask: RealTimeTaskResponse) => { - if (!pathname.includes('detail')) return + //disable board navigation if not in details page or from notification-center-view + if (!pathname.includes('detail') || fromNotificationCenter) return const isClientUser = pathname.includes('cu') const isAccessibleSubtask = updatedTask.parentId && accessibleTasks.some((task) => task.id === updatedTask.parentId) From 46e989ac9119a1300bf496a069cbcb7cbac58c7d Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Wed, 5 Nov 2025 16:31:13 +0545 Subject: [PATCH 08/19] fix(OUT-2554): passed fromNotificationCenter to deletedTaskRedirectPage --- src/app/detail/[task_id]/[user_type]/page.tsx | 10 ++++++++-- .../layouts/DeletedTaskRedirectPage.tsx | 17 +++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/app/detail/[task_id]/[user_type]/page.tsx b/src/app/detail/[task_id]/[user_type]/page.tsx index 631506111..987f249a2 100644 --- a/src/app/detail/[task_id]/[user_type]/page.tsx +++ b/src/app/detail/[task_id]/[user_type]/page.tsx @@ -103,10 +103,17 @@ export default async function TaskDetailPage({ if (!tokenPayload) { throw new Error('Please provide a Valid Token') } + const fromNotificationCenter = !!searchParams.fromNotificationCenter console.info(`app/detail/${task_id}/${user_type}/page.tsx | Serving user ${token} with payload`, tokenPayload) if (!task) { - return + return ( + + ) } const isPreviewMode = !!getPreviewMode(tokenPayload) @@ -116,7 +123,6 @@ export default async function TaskDetailPage({ href: `/detail/${id}/${user_type}?token=${token}`, })) - const fromNotificationCenter = !!searchParams.fromNotificationCenter // flag that determines if the current user is the task viewer const isViewer = checkIfTaskViewer(task.viewers, tokenPayload) diff --git a/src/components/layouts/DeletedTaskRedirectPage.tsx b/src/components/layouts/DeletedTaskRedirectPage.tsx index 7dec096e5..613ad9109 100644 --- a/src/components/layouts/DeletedTaskRedirectPage.tsx +++ b/src/components/layouts/DeletedTaskRedirectPage.tsx @@ -1,15 +1,24 @@ +'use client' + import { UserRole } from '@/app/api/core/types/user' import { AppMargin, SizeofAppMargin } from '@/hoc/AppMargin' import { TasksListIcon } from '@/icons' -import { selectTaskDetails } from '@/redux/features/taskDetailsSlice' + import { SxCenter } from '@/utils/mui' import { Box, Button, Stack, Typography } from '@mui/material' import Link from 'next/link' -import { useSelector } from 'react-redux' + import { z } from 'zod' -export const DeletedTaskRedirectPage = ({ userType, token }: { userType: UserRole; token: string }) => { - const { fromNotificationCenter } = useSelector(selectTaskDetails) +export const DeletedTaskRedirectPage = ({ + userType, + token, + fromNotificationCenter, +}: { + userType: UserRole + token: string + fromNotificationCenter: boolean +}) => { return ( <> From 187af5923f43a19821dfa66ea29cb74e225de5bd Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Wed, 5 Nov 2025 16:53:51 +0545 Subject: [PATCH 09/19] fix(OUT-2554): immage, attachment issue on notification-center-view when stale uploadFn is passed to tapwrite --- src/app/detail/[task_id]/[user_type]/page.tsx | 1 + src/app/detail/ui/TaskEditor.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/detail/[task_id]/[user_type]/page.tsx b/src/app/detail/[task_id]/[user_type]/page.tsx index 987f249a2..62e5ae862 100644 --- a/src/app/detail/[task_id]/[user_type]/page.tsx +++ b/src/app/detail/[task_id]/[user_type]/page.tsx @@ -199,6 +199,7 @@ export default async function TaskDetailPage({ await deleteAttachment(token, id) }} userType={params.user_type} + token={token} /> {subTaskStatus.canCreateSubtask && ( diff --git a/src/app/detail/ui/TaskEditor.tsx b/src/app/detail/ui/TaskEditor.tsx index cc1dfb117..7da3f9eed 100644 --- a/src/app/detail/ui/TaskEditor.tsx +++ b/src/app/detail/ui/TaskEditor.tsx @@ -31,6 +31,7 @@ interface Prop { postAttachment: (postAttachmentPayload: CreateAttachmentRequest) => void deleteAttachment: (id: string) => void userType: UserType + token: string } export const TaskEditor = ({ @@ -44,11 +45,12 @@ export const TaskEditor = ({ postAttachment, deleteAttachment, userType, + token, }: Prop) => { const [updateTitle, setUpdateTitle] = useState('') const [updateDetail, setUpdateDetail] = useState('') const { showConfirmDeleteModal, openImage } = useSelector(selectTaskDetails) - const { token, activeTask } = useSelector(selectTaskBoard) + const { activeTask } = useSelector(selectTaskBoard) const [isUserTyping, setIsUserTyping] = useState(false) const [activeUploads, setActiveUploads] = useState(0) From 8439e0078d8b900cd662fd17732fd72b822d7ec0 Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Wed, 5 Nov 2025 17:14:27 +0545 Subject: [PATCH 10/19] fix(OUT-2565): disabled subtask navigation from notification-center-view + some design fixes --- src/app/detail/ui/Subtasks.tsx | 3 +++ src/app/detail/ui/TaskCardList.tsx | 25 ++++++++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/app/detail/ui/Subtasks.tsx b/src/app/detail/ui/Subtasks.tsx index 7bddbca44..f19af6eeb 100644 --- a/src/app/detail/ui/Subtasks.tsx +++ b/src/app/detail/ui/Subtasks.tsx @@ -10,6 +10,7 @@ import { useDebounce } from '@/hooks/useDebounce' import { GrayAddMediumIcon } from '@/icons' import { selectAuthDetails } from '@/redux/features/authDetailsSlice' import { selectTaskBoard } from '@/redux/features/taskBoardSlice' +import { selectTaskDetails } from '@/redux/features/taskDetailsSlice' import { CreateTaskRequest, TaskResponse } from '@/types/dto/tasks.dto' import { fetcher } from '@/utils/fetcher' import { generateRandomString } from '@/utils/generateRandomString' @@ -40,6 +41,7 @@ export const Subtasks = ({ const [openTaskForm, setOpenTaskForm] = useState(false) const { workflowStates, assignee, activeTask } = useSelector(selectTaskBoard) const { tokenPayload } = useSelector(selectAuthDetails) + const { fromNotificationCenter } = useSelector(selectTaskDetails) const [optimisticUpdates, setOptimisticUpdates] = useState([]) //might need this server-temp id maps in the future. const [lastUpdated, setLastUpdated] = useState() const handleFormCancel = () => setOpenTaskForm(false) @@ -200,6 +202,7 @@ export const Subtasks = ({ variant="subtask" mode={mode} handleUpdate={handleSubTaskUpdate} + disableNavigation={fromNotificationCenter} /> ) })} diff --git a/src/app/detail/ui/TaskCardList.tsx b/src/app/detail/ui/TaskCardList.tsx index 4acdddd34..68882abd1 100644 --- a/src/app/detail/ui/TaskCardList.tsx +++ b/src/app/detail/ui/TaskCardList.tsx @@ -66,9 +66,19 @@ interface TaskCardListProps { handleUpdate?: (taskId: string, changes: Partial, updater: () => Promise) => Promise isTemp?: boolean sx?: SxProps | undefined + disableNavigation?: boolean } -export const TaskCardList = ({ task, variant, workflowState, mode, handleUpdate, isTemp, sx }: TaskCardListProps) => { +export const TaskCardList = ({ + task, + variant, + workflowState, + mode, + handleUpdate, + isTemp, + sx, + disableNavigation = false, +}: TaskCardListProps) => { const { assignee, workflowStates, previewMode, token, confirmAssignModalId, assigneeCache, confirmViewershipModalId } = useSelector(selectTaskBoard) const { tokenPayload } = useSelector(selectAuthDetails) @@ -173,8 +183,13 @@ export const TaskCardList = ({ task, variant, workflowState, mode, handleUpdate, justifyContent: 'flex-end', minWidth: 0, ':hover': { - cursor: 'pointer', - background: (theme) => (variant == 'subtask-board' ? theme.color.gray[150] : theme.color.gray[100]), + cursor: disableNavigation ? 'auto' : 'pointer', + background: (theme) => + disableNavigation + ? theme.color.base.white + : variant == 'subtask-board' + ? theme.color.gray[150] + : theme.color.gray[100], }, ':focus-visible': { outline: (theme) => `1px solid ${theme.color.borders.focusBorder2}`, @@ -220,7 +235,7 @@ export const TaskCardList = ({ task, variant, workflowState, mode, handleUpdate, disabled={checkIfTaskViewer(task.viewers, tokenPayload)} /> - {isTemp || variant === 'subtask-board' ? ( + {isTemp || variant === 'subtask-board' || disableNavigation ? (
- + {(task.subtaskCount > 0 || task.isArchived) && ( From 5f2fb66807085d272f35629ac857dbc5dc80274c Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Thu, 6 Nov 2025 19:40:02 +0545 Subject: [PATCH 11/19] fix(OUT-2571): attachment layout on hover on notification-center-view and small devices having cusor, removed hover none on small screens --- src/components/AttachmentLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AttachmentLayout.tsx b/src/components/AttachmentLayout.tsx index 9ae706807..67144e188 100644 --- a/src/components/AttachmentLayout.tsx +++ b/src/components/AttachmentLayout.tsx @@ -178,7 +178,7 @@ const AttachmentLayout: React.FC = ({ maxWidth: '100%', overflow: 'hidden', '&:hover': { - border: isXsScreen ? 'none' : (theme) => `1px solid ${theme.color.gray[selected ? 600 : 300]}`, + border: (theme) => `1px solid ${theme.color.gray[selected ? 600 : 300]}`, '& .download-btn': { opacity: 1, }, From 29bee80c412c58000133432583206104e7393981 Mon Sep 17 00:00:00 2001 From: Rojan Rajbhandari Date: Fri, 7 Nov 2025 13:05:47 +0545 Subject: [PATCH 12/19] feat(OUT-2574): make infra regions reproducable with vercel.json IaC --- vercel.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/vercel.json b/vercel.json index 976c04176..274e0133d 100644 --- a/vercel.json +++ b/vercel.json @@ -1,4 +1,9 @@ { + "$schema": "https://openapi.vercel.sh/vercel.json", + "regions": [ + "iad1", + "pdx1" + ], "buildCommand": "./scripts/build.sh", "crons": [ { From 8467e3364e14132f60621c604d58584a86366ef5 Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Fri, 7 Nov 2025 14:20:44 +0545 Subject: [PATCH 13/19] fix(OUT-2572): options of delayTouchStart was not being passed to ModifiedTouchBackend of dnd, made sure this is passed so the touch start for dnd on mobile will be delayed by 100 ms --- src/hoc/ModifiedBackend.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/hoc/ModifiedBackend.ts b/src/hoc/ModifiedBackend.ts index 72cf26291..57d70690e 100644 --- a/src/hoc/ModifiedBackend.ts +++ b/src/hoc/ModifiedBackend.ts @@ -9,8 +9,8 @@ const shouldIgnoreTarget = (target: any) => { return false } -const createModifiedBackend = (Backend: BackendFactory, manager?: any, context?: any) => { - const instance = Backend(manager, context) +const createModifiedBackend = (Backend: BackendFactory, manager?: any, context?: any, options?: any) => { + const instance = Backend(manager, context, options) const listeners = [ 'handleTopDragStart', @@ -39,4 +39,5 @@ const createModifiedBackend = (Backend: BackendFactory, manager?: any, context?: export const ModifiedHTML5Backend = (manager: any, context: any) => createModifiedBackend(HTML5Backend, manager, context) -export const ModifiedTouchBackend = (manager: any, context: any) => createModifiedBackend(TouchBackend, manager, context) +export const ModifiedTouchBackend = (manager: any, context: any, options: any) => + createModifiedBackend(TouchBackend, manager, context, options) From 477bcc6824d1984a93fc8ca53b25d142dff4f8a7 Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Fri, 7 Nov 2025 14:52:05 +0545 Subject: [PATCH 14/19] fix(OUT-2572): added a support to detech touch device in dnd wrapper --- src/hoc/DndWrapper.tsx | 4 +++- src/hooks/useIsTouchDevice.ts | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/hooks/useIsTouchDevice.ts diff --git a/src/hoc/DndWrapper.tsx b/src/hoc/DndWrapper.tsx index 95d283cf1..8feae32ba 100644 --- a/src/hoc/DndWrapper.tsx +++ b/src/hoc/DndWrapper.tsx @@ -5,9 +5,11 @@ import { DndProvider } from 'react-dnd' import { useMediaQuery } from '@mui/material' import { ModifiedHTML5Backend, ModifiedTouchBackend } from './ModifiedBackend' +import { useIsTouchDevice } from '@/hooks/useIsTouchDevice' export const DndWrapper = ({ children }: { children: ReactNode }) => { const [screenType, setScreenType] = useState<'mobile' | 'nonMobile' | undefined>(undefined) + const isTouch = useIsTouchDevice() const isMobileScreen = useMediaQuery('(max-width:600px)') useEffect(() => { @@ -22,7 +24,7 @@ export const DndWrapper = ({ children }: { children: ReactNode }) => { return ( { + const [isTouchDevice, setIsTouchDevice] = useState(false) + + useEffect(() => { + if (typeof window !== 'undefined') { + const mediaQuery = window.matchMedia('(pointer: coarse) and (hover: none)') + setIsTouchDevice(mediaQuery.matches) + + const handleChange = (e: MediaQueryListEvent) => { + setIsTouchDevice(e.matches) + } + + mediaQuery.addEventListener('change', handleChange) + return () => { + mediaQuery.removeEventListener('change', handleChange) + } + } + }, []) + + return isTouchDevice +} From 134e8dbd82a9419cbf039dedb77700f68f7814b7 Mon Sep 17 00:00:00 2001 From: arpandhakal-lgtm Date: Mon, 10 Nov 2025 11:38:09 +0545 Subject: [PATCH 15/19] fix(OUT-2572): removed listening to change events on media query on matching medias to detect touch screens altogether because the effect runs only once --- src/hooks/useIsTouchDevice.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/hooks/useIsTouchDevice.ts b/src/hooks/useIsTouchDevice.ts index f9702db1b..9800ebddd 100644 --- a/src/hooks/useIsTouchDevice.ts +++ b/src/hooks/useIsTouchDevice.ts @@ -7,15 +7,6 @@ export const useIsTouchDevice = () => { if (typeof window !== 'undefined') { const mediaQuery = window.matchMedia('(pointer: coarse) and (hover: none)') setIsTouchDevice(mediaQuery.matches) - - const handleChange = (e: MediaQueryListEvent) => { - setIsTouchDevice(e.matches) - } - - mediaQuery.addEventListener('change', handleChange) - return () => { - mediaQuery.removeEventListener('change', handleChange) - } } }, []) From 03fc3c460b7fff29e4dede845b3e5708647cbc17 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Mon, 1 Dec 2025 20:15:11 +0545 Subject: [PATCH 16/19] fix(OUT-2694): check for storage access, log and throw error --- src/app/_cache/forageStorage.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/app/_cache/forageStorage.ts b/src/app/_cache/forageStorage.ts index 6bcb43f44..51f01f701 100644 --- a/src/app/_cache/forageStorage.ts +++ b/src/app/_cache/forageStorage.ts @@ -25,10 +25,31 @@ export async function migrateAssignees(lookupKey: string) { export async function getAssignees(lookupKey: string): Promise { if (typeof window === 'undefined') return [] + + const hasStorageAccess = await document.hasStorageAccess() + if (!hasStorageAccess) { + console.error('Storage access not granted') + throw new Error( + "Storage access not granted. Under Chrome's Settings > Privacy and Security, make sure 'Third-party cookies' is allowed.", + ) + } + + await document.requestStorageAccess() return (await localforage.getItem(`assignees.${lookupKey}`)) ?? [] } export async function setAssignees(lookupKey: string, value: any) { if (typeof window === 'undefined') return + + const hasStorageAccess = await document.hasStorageAccess() + console.log({ hasStorageAccess }) + + if (!hasStorageAccess) { + console.error('Storage access not granted') + throw new Error( + "Storage access not granted. Under Chrome's Settings > Privacy and Security, make sure 'Third-party cookies' is allowed.", + ) + } + return await localforage.setItem(`assignees.${lookupKey}`, value) } From 73946a3e9c2cbb747ee703b8af28b43b93ab0c37 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Tue, 2 Dec 2025 08:46:50 +0545 Subject: [PATCH 17/19] refactor(OUT-2694): log cleanup --- src/app/_cache/forageStorage.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/_cache/forageStorage.ts b/src/app/_cache/forageStorage.ts index 51f01f701..b82d7920a 100644 --- a/src/app/_cache/forageStorage.ts +++ b/src/app/_cache/forageStorage.ts @@ -42,8 +42,6 @@ export async function setAssignees(lookupKey: string, value: any) { if (typeof window === 'undefined') return const hasStorageAccess = await document.hasStorageAccess() - console.log({ hasStorageAccess }) - if (!hasStorageAccess) { console.error('Storage access not granted') throw new Error( From 29519cdff184a02ec2483e42e2ae9aafe2ce8064 Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Wed, 3 Dec 2025 16:34:41 +0545 Subject: [PATCH 18/19] refactor(OUT-2694): implement proper error handling --- src/app/_cache/forageStorage.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/app/_cache/forageStorage.ts b/src/app/_cache/forageStorage.ts index b82d7920a..aa387f252 100644 --- a/src/app/_cache/forageStorage.ts +++ b/src/app/_cache/forageStorage.ts @@ -26,28 +26,35 @@ export async function migrateAssignees(lookupKey: string) { export async function getAssignees(lookupKey: string): Promise { if (typeof window === 'undefined') return [] - const hasStorageAccess = await document.hasStorageAccess() - if (!hasStorageAccess) { + try { + if (!(await document.hasStorageAccess())) { + console.info('Browswer has no storage access') + await document.requestStorageAccess() + } + + return (await localforage.getItem(`assignees.${lookupKey}`)) ?? [] + } catch (error: unknown) { console.error('Storage access not granted') throw new Error( "Storage access not granted. Under Chrome's Settings > Privacy and Security, make sure 'Third-party cookies' is allowed.", ) } - - await document.requestStorageAccess() - return (await localforage.getItem(`assignees.${lookupKey}`)) ?? [] } export async function setAssignees(lookupKey: string, value: any) { if (typeof window === 'undefined') return - const hasStorageAccess = await document.hasStorageAccess() - if (!hasStorageAccess) { + try { + if (!(await document.hasStorageAccess())) { + console.info('Browswer has no storage access') + await document.requestStorageAccess() + } + + return await localforage.setItem(`assignees.${lookupKey}`, value) + } catch (error: unknown) { console.error('Storage access not granted') throw new Error( "Storage access not granted. Under Chrome's Settings > Privacy and Security, make sure 'Third-party cookies' is allowed.", ) } - - return await localforage.setItem(`assignees.${lookupKey}`, value) } From ea6b8d10d3db2bebb746bb1326a931104397ca3d Mon Sep 17 00:00:00 2001 From: SandipBajracharya Date: Fri, 5 Dec 2025 10:00:43 +0545 Subject: [PATCH 19/19] refactor(OUT-2750): suppress and log error when storage access not granted --- src/app/_cache/forageStorage.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/_cache/forageStorage.ts b/src/app/_cache/forageStorage.ts index aa387f252..e8baf6391 100644 --- a/src/app/_cache/forageStorage.ts +++ b/src/app/_cache/forageStorage.ts @@ -34,10 +34,10 @@ export async function getAssignees(lookupKey: string): Promise(`assignees.${lookupKey}`)) ?? [] } catch (error: unknown) { - console.error('Storage access not granted') - throw new Error( + console.error( "Storage access not granted. Under Chrome's Settings > Privacy and Security, make sure 'Third-party cookies' is allowed.", ) + return [] } } @@ -52,8 +52,7 @@ export async function setAssignees(lookupKey: string, value: any) { return await localforage.setItem(`assignees.${lookupKey}`, value) } catch (error: unknown) { - console.error('Storage access not granted') - throw new Error( + console.error( "Storage access not granted. Under Chrome's Settings > Privacy and Security, make sure 'Third-party cookies' is allowed.", ) }