From fe01311f37a1fc96de65371cc99943e96dc6aac7 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 12 Aug 2026 16:17:19 +1000 Subject: [PATCH 1/2] PM-5848: Refresh support ticket details after read What was broken After assigning a ticket from the Support ticket list, the detail screen could still show the ticket as unassigned and offer "Assign to me" until the page was refreshed. Root cause The asynchronous mark-read completion performed a data-bearing SWR mutation that could supersede an in-flight detail revalidation and preserve stale pre-assignment data. What was changed Changed the post-read cache update to a revalidation-only SWR mutation, ensuring the detail page reloads current server state without invalidating a concurrent assignment refresh. Any added/updated tests Updated the TicketDetailPage regression test to verify that completing the mark-read request triggers a zero-argument, revalidation-only mutation. --- .../ticket-details/TicketDetailPage.spec.tsx | 33 +++---------------- .../pages/ticket-details/TicketDetailPage.tsx | 4 +-- 2 files changed, 5 insertions(+), 32 deletions(-) diff --git a/src/apps/support/src/pages/ticket-details/TicketDetailPage.spec.tsx b/src/apps/support/src/pages/ticket-details/TicketDetailPage.spec.tsx index 66b321d20..9567517b1 100644 --- a/src/apps/support/src/pages/ticket-details/TicketDetailPage.spec.tsx +++ b/src/apps/support/src/pages/ticket-details/TicketDetailPage.spec.tsx @@ -196,7 +196,7 @@ describe('TicketDetailPage reply access', () => { .toBeTruthy() }) - it('preserves freshly revalidated assignees when marking a ticket read completes', async () => { + it('revalidates fresh detail after marking the ticket read', async () => { const markReadRequest = createDeferred() mockedMarkRead.mockReturnValue(markReadRequest.promise) mockUseSWR.mockReturnValue({ @@ -211,35 +211,10 @@ describe('TicketDetailPage reply access', () => { await waitFor(() => { expect(mockMutate) - .toHaveBeenCalledWith(expect.any(Function), false) + .toHaveBeenCalledTimes(1) }) - - const updateCachedTicket = mockMutate.mock.calls[0][0] as ( - ticket?: SupportTicketDetail, - ) => SupportTicketDetail | undefined - const freshlyRevalidatedTicket: SupportTicketDetail = { - ...closedTicket, - assignees: [{ - assignedAt: '2026-08-07T01:30:00.000Z', - handle: 'support-staff', - userId: '67890', - }], - hasUnread: true, - responseCount: 1, - responses: [{ - createdAt: '2026-08-07T01:30:00.000Z', - id: 'response-1', - markdown: 'We are investigating.', - readBy: [], - userHandle: 'support-staff', - userId: '67890', - }], - } - - expect(updateCachedTicket(freshlyRevalidatedTicket)) - .toEqual({ ...freshlyRevalidatedTicket, hasUnread: false }) - expect(updateCachedTicket(undefined)) - .toBeUndefined() + expect(mockMutate.mock.calls[0]) + .toEqual([]) }) it('requires non-owner support staff to assign an open ticket before replying', () => { diff --git a/src/apps/support/src/pages/ticket-details/TicketDetailPage.tsx b/src/apps/support/src/pages/ticket-details/TicketDetailPage.tsx index 4df066db3..14eb8921a 100644 --- a/src/apps/support/src/pages/ticket-details/TicketDetailPage.tsx +++ b/src/apps/support/src/pages/ticket-details/TicketDetailPage.tsx @@ -75,9 +75,7 @@ export const TicketDetailPage: FC = () => { markedReadTicket.current = data.id markSupportTicketRead(data.id) - .then(() => mutate(current => (current - ? { ...current, hasUnread: false } - : current), false)) + .then(() => mutate()) .catch(() => undefined) }, [data, mutate]) From bc710c2668149b12951e5bd6163daa9553808726 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 12 Aug 2026 21:19:42 +1000 Subject: [PATCH 2/2] PM-5854: Make challenge link visually distinct What was broken The View challenge action on the ticket details page was clickable but appeared as ordinary gray text. Root cause The global link reset removes native color and decoration, while the Support app did not define the CSS variable used by this link. What was changed Styled the challenge link and its hover state with the shared accessible blue token and a persistent underline. Any added/updated tests Added a focused stylesheet regression assertion covering the challenge link's default and hover presentation. The existing semantic link and encoded URL assertions remain in place. --- .../ticket-details/TicketDetailPage.module.scss | 6 ++++-- .../pages/ticket-details/TicketDetailPage.spec.tsx | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/apps/support/src/pages/ticket-details/TicketDetailPage.module.scss b/src/apps/support/src/pages/ticket-details/TicketDetailPage.module.scss index 352f4e7b6..415500b69 100644 --- a/src/apps/support/src/pages/ticket-details/TicketDetailPage.module.scss +++ b/src/apps/support/src/pages/ticket-details/TicketDetailPage.module.scss @@ -12,9 +12,11 @@ text-decoration: none; } -.challengeLink { - color: var(--Actived); +.challengeLink, +.challengeLink:hover { + color: $link-blue-dark; font-weight: 700; + text-decoration: underline; } .header { diff --git a/src/apps/support/src/pages/ticket-details/TicketDetailPage.spec.tsx b/src/apps/support/src/pages/ticket-details/TicketDetailPage.spec.tsx index 66b321d20..9ea7427e2 100644 --- a/src/apps/support/src/pages/ticket-details/TicketDetailPage.spec.tsx +++ b/src/apps/support/src/pages/ticket-details/TicketDetailPage.spec.tsx @@ -1,4 +1,5 @@ /* eslint-disable import/no-extraneous-dependencies, ordered-imports/ordered-imports, react/jsx-no-bind */ +import { readFileSync } from 'fs' import { fireEvent, render, @@ -17,6 +18,7 @@ import { TicketDetailPage } from './TicketDetailPage' const mockMutate = jest.fn() const mockUseSWR = jest.fn() let mockProfile: { roles: string[]; userId: number | string } +const ticketDetailStyles = readFileSync(`${__dirname}/TicketDetailPage.module.scss`, 'utf8') interface Deferred { promise: Promise @@ -196,6 +198,17 @@ describe('TicketDetailPage reply access', () => { .toBeTruthy() }) + it('styles the challenge anchor as a visible link', () => { + const challengeLinkRule = ticketDetailStyles.match( + /\.challengeLink,\s*\.challengeLink:hover \{[^}]*\}/, + )?.[0] + + expect(challengeLinkRule) + .toContain('color: $link-blue-dark;') + expect(challengeLinkRule) + .toContain('text-decoration: underline;') + }) + it('preserves freshly revalidated assignees when marking a ticket read completes', async () => { const markReadRequest = createDeferred() mockedMarkRead.mockReturnValue(markReadRequest.promise)