From 8f63a1befc46ab2ef1a9353b16d7d5689a33cddf Mon Sep 17 00:00:00 2001 From: Prios Shrestha <30313649+priosshrsth@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:33:34 +0545 Subject: [PATCH 1/2] OUT-4027 | Fix 500 when deleting a task whose label row is missing (#1397) deleteLabel passed `id: currentLabel?.id` straight into label.delete, so when findFirst matched nothing Prisma got `{ id: undefined }` and threw PrismaClientValidationError, failing the whole delete transaction. Return early instead. --- .../label-mapping.service.test.ts | 44 +++++++++++++++++++ .../label-mapping/label-mapping.service.ts | 3 +- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 src/app/api/label-mapping/label-mapping.service.test.ts diff --git a/src/app/api/label-mapping/label-mapping.service.test.ts b/src/app/api/label-mapping/label-mapping.service.test.ts new file mode 100644 index 000000000..24a195ede --- /dev/null +++ b/src/app/api/label-mapping/label-mapping.service.test.ts @@ -0,0 +1,44 @@ +const mockLabelFindFirst = jest.fn() +const mockLabelDelete = jest.fn() + +jest.mock('@/lib/db', () => ({ + __esModule: true, + default: { + getInstance: () => ({ + label: { findFirst: mockLabelFindFirst, delete: mockLabelDelete }, + }), + }, +})) + +jest.mock('@/utils/CopilotAPI', () => ({ CopilotAPI: jest.fn() })) + +import { LabelMappingService } from '@api/label-mapping/label-mapping.service' +import User from '@api/core/models/User.model' +import { UserRole } from '@api/core/types/user' + +const user = { + workspaceId: 'ws-1', + role: UserRole.IU, + internalUserId: 'iu-1', + token: 'token', +} as unknown as User + +describe('LabelMappingService#deleteLabel', () => { + beforeEach(() => jest.clearAllMocks()) + + it('deletes the matching label row', async () => { + mockLabelFindFirst.mockResolvedValue({ id: 'label-1' }) + + await new LabelMappingService(user).deleteLabel('ASS10-009') + + expect(mockLabelDelete).toHaveBeenCalledWith({ where: { id: 'label-1' } }) + }) + + it('no-ops when the label row is already gone', async () => { + mockLabelFindFirst.mockResolvedValue(null) + + await new LabelMappingService(user).deleteLabel('ASS10-009') + + expect(mockLabelDelete).not.toHaveBeenCalled() + }) +}) diff --git a/src/app/api/label-mapping/label-mapping.service.ts b/src/app/api/label-mapping/label-mapping.service.ts index 42007a03c..b0d9c2abd 100644 --- a/src/app/api/label-mapping/label-mapping.service.ts +++ b/src/app/api/label-mapping/label-mapping.service.ts @@ -175,9 +175,10 @@ export class LabelMappingService extends BaseService { label, }, }) + if (!currentLabel) return await this.db.label.delete({ where: { - id: currentLabel?.id, + id: currentLabel.id, }, }) } From 6cf8b5cb93db963fb5bd4f542ae16f42763e0a0c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 22 Aug 2026 04:17:07 +0000 Subject: [PATCH 2/2] fix(POR-22653): handle platform module-not-found errors gracefully Wrap notification-center and configure-tasks-app server loads in try/catch so platform errors (e.g. Module not found / parameter_invalid_empty) render SilentError instead of unhandled Next.js request errors in Sentry. Also harden CopilotAPI#getNotificationSettings to return empty settings when app install listing or settings fetch fails. Co-authored-by: Neil Raina --- src/app/configure-tasks-app/page.tsx | 36 ++++++++++++++++++++++------ src/app/notification-center/page.tsx | 23 +++++++++++++----- src/utils/CopilotAPI.ts | 20 +++++++++++++--- 3 files changed, 63 insertions(+), 16 deletions(-) diff --git a/src/app/configure-tasks-app/page.tsx b/src/app/configure-tasks-app/page.tsx index db28eac69..9a0bc17d7 100644 --- a/src/app/configure-tasks-app/page.tsx +++ b/src/app/configure-tasks-app/page.tsx @@ -17,7 +17,9 @@ import { AutoArchiveSection } from '@/app/configure-tasks-app/ui/AutoArchiveSect import { ClientViewSettingsSection } from '@/app/configure-tasks-app/ui/ClientViewSettingsSection' import { StatusCustomizationSection } from '@/app/configure-tasks-app/ui/StatusCustomizationSection' import { ClientViewSettings } from '@/types/dto/workspaceSettings.dto' +import { SilentError } from '@/components/templates/SilentError' import { Stack } from '@mui/material' +import { z } from 'zod' async function getAllWorkflowStates(token: string): Promise { const res = await fetch(`${apiUrl}/api/workflow-states?token=${token}`, { @@ -58,6 +60,23 @@ async function getWorkspaceSetting(token: string): Promise<{ autoArchiveAfterDay return await res.json() } +async function loadConfigureTasksAppPageData(token: string) { + try { + const [workflowStates, assignee, templates, tokenPayload, workspaceSetting] = await Promise.all([ + getAllWorkflowStates(token), + addTypeToAssignee(await getAssigneeList(token)), + getAllTemplates(token), + getTokenPayload(token), + getWorkspaceSetting(token), + ]) + + return { workflowStates, assignee, templates, tokenPayload, workspaceSetting } + } catch (error) { + console.warn('configure-tasks-app: failed to load configuration', error) + return null + } +} + interface ConfigureTasksAppPageProps { searchParams: Promise<{ token: string @@ -67,13 +86,16 @@ interface ConfigureTasksAppPageProps { export default async function ConfigureTasksAppPage(props: ConfigureTasksAppPageProps) { const searchParams = await props.searchParams const { token } = searchParams - const [workflowStates, assignee, templates, tokenPayload, workspaceSetting] = await Promise.all([ - getAllWorkflowStates(token), - addTypeToAssignee(await getAssigneeList(token)), - getAllTemplates(token), - getTokenPayload(token), - getWorkspaceSetting(token), - ]) + if (!z.string().safeParse(token).success) { + return + } + + const pageData = await loadConfigureTasksAppPageData(token) + if (!pageData) { + return + } + + const { workflowStates, assignee, templates, tokenPayload, workspaceSetting } = pageData return ( }) { @@ -21,13 +21,24 @@ export default async function NotificationCenter(props: { searchParams: Promise< return } - const notificationDetail = await getNotificationDetail(token) + let notificationDetail + try { + notificationDetail = await getNotificationDetail(token) + } catch (error) { + console.warn('notification-center: failed to load notification', error) + return + } + if (!notificationDetail) return - const params = NotificationInProductCtaParamsSchema.parse(notificationDetail.deliveryTargets?.inProduct?.ctaParams) + const params = NotificationInProductCtaParamsSchema.safeParse( + notificationDetail.deliveryTargets?.inProduct?.ctaParams, + ) + if (!params.success) { + return + } - redirectIfTaskCta({ ...params, ...searchParams }, UserType.INTERNAL_USER, true) + redirectIfTaskCta({ ...params.data, ...searchParams }, 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/utils/CopilotAPI.ts b/src/utils/CopilotAPI.ts index eed475e79..27f7c7494 100644 --- a/src/utils/CopilotAPI.ts +++ b/src/utils/CopilotAPI.ts @@ -361,15 +361,29 @@ export class CopilotAPI { async _getNotificationSettings(): Promise { console.info('CopilotAPI#_getNotificationSettings') const appId = z.string({ message: 'Missing AppID in environment' }).parse(APP_ID) - const installs = await this.copilot.listAppInstalls() + + let installs + try { + installs = await this.copilot.listAppInstalls() + } catch (error) { + console.warn('CopilotAPI#_getNotificationSettings | Failed to list app installs', error) + return { notifications: [] } + } + const install = installs.find((entry) => entry.appId === appId) if (!install?.id) { console.info('CopilotAPI#_getNotificationSettings | No matching app install in workspace; no settings') return { notifications: [] } } const workspaceId = await this._resolveWorkspaceId() - const response = await this._manualFetch(`installs/${install.id}/notification-settings`, undefined, workspaceId) - return NotificationSettingsResponseSchema.parse(response) + + try { + const response = await this._manualFetch(`installs/${install.id}/notification-settings`, undefined, workspaceId) + return NotificationSettingsResponseSchema.parse(response) + } catch (error) { + console.warn('CopilotAPI#_getNotificationSettings | Failed to fetch notification settings', error) + return { notifications: [] } + } } // A single IU's live per-category notification preferences. Never cached — the platform evaluates