Skip to content

Commit 2b3495d

Browse files
authored
Merge branch 'develop' into ocrvs-10132
2 parents 9aa6d6f + b3ea495 commit 2b3495d

File tree

5 files changed

+259
-118
lines changed

5 files changed

+259
-118
lines changed

packages/client/src/v2-events/features/events/actions/print-certificate/PrintCertificate.interaction.stories.tsx

Lines changed: 89 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
generateEventDocument,
2222
generateWorkqueues,
2323
tennisClubMembershipEvent,
24-
field
24+
never
2525
} from '@opencrvs/commons/client'
2626
import { tennisClubMembershipEventDocument } from '@client/v2-events/features/events/fixtures'
2727
import { ROUTES, routesConfig } from '@client/v2-events/routes'
@@ -47,6 +47,94 @@ const tRPCMsw = createTRPCMsw<AppRouter>({
4747
transformer: { input: superjson, output: superjson }
4848
})
4949

50+
export const NoTemplateAvailable: Story = {
51+
parameters: {
52+
chromatic: { disableSnapshot: true },
53+
reactRouter: {
54+
router: routesConfig,
55+
initialPath: ROUTES.V2.EVENTS.PRINT_CERTIFICATE.PAGES.buildPath({
56+
eventId: tennisClubMembershipEventDocument.id,
57+
pageId: 'collector'
58+
})
59+
},
60+
test: {
61+
// Ignoring the failed font request
62+
dangerouslyIgnoreUnhandledErrors: true
63+
},
64+
msw: {
65+
handlers: {
66+
config: [
67+
http.get(
68+
'/api/countryconfig/certificates/simple-certificate.svg',
69+
() => {
70+
return HttpResponse.text(
71+
`<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100"><text x="10" y="20">Simple Certificate</text></svg>`
72+
)
73+
}
74+
),
75+
http.get('http://localhost:2021/config', () => {
76+
return HttpResponse.json({
77+
systems: [],
78+
config: mockOfflineData.config,
79+
certificates: [
80+
{
81+
id: 'simple-certificate',
82+
isV2Template: true,
83+
event: 'tennis-club-membership',
84+
label: {
85+
id: 'certificates.simple.certificate.copy',
86+
defaultMessage: 'Simple Certificate copy',
87+
description: 'The label for a simple certificate'
88+
},
89+
conditionals: [
90+
{
91+
type: 'SHOW',
92+
conditional: never()
93+
}
94+
],
95+
isDefault: false,
96+
fee: {
97+
onTime: 7,
98+
late: 10.6,
99+
delayed: 18
100+
},
101+
svgUrl:
102+
'/api/countryconfig/certificates/simple-certificate.svg'
103+
}
104+
]
105+
})
106+
})
107+
],
108+
events: [
109+
tRPCMsw.event.config.get.query(() => {
110+
return [tennisClubMembershipEvent]
111+
}),
112+
tRPCMsw.event.get.query(() => {
113+
return tennisClubMembershipEventDocument
114+
})
115+
]
116+
}
117+
}
118+
},
119+
play: async ({ canvasElement, step }) => {
120+
const canvas = within(canvasElement)
121+
122+
await step(
123+
'Click Certification Type and find no options message',
124+
async () => {
125+
await userEvent.click(
126+
await canvas.findByTestId('select__certificateTemplateId')
127+
)
128+
await expect(
129+
await canvas.findByText(
130+
'No template available for this event, contact Admin'
131+
)
132+
).toBeVisible()
133+
}
134+
)
135+
}
136+
}
137+
50138
export const ContinuingAndGoingBack: Story = {
51139
parameters: {
52140
chromatic: { disableSnapshot: true },
@@ -289,107 +377,3 @@ export const RedirectAfterPrint: Story = {
289377
})
290378
}
291379
}
292-
293-
export const NoTemplateAvailable: Story = {
294-
parameters: {
295-
chromatic: { disableSnapshot: true },
296-
reactRouter: {
297-
router: routesConfig,
298-
initialPath: ROUTES.V2.EVENTS.PRINT_CERTIFICATE.PAGES.buildPath({
299-
eventId: tennisClubMembershipEventDocument.id,
300-
pageId: 'collector'
301-
})
302-
},
303-
test: {
304-
// Ignoring the failed font request
305-
dangerouslyIgnoreUnhandledErrors: true
306-
},
307-
msw: {
308-
handlers: {
309-
events: [
310-
tRPCMsw.event.config.get.query(() => {
311-
return [tennisClubMembershipEvent]
312-
}),
313-
tRPCMsw.event.get.query(() => {
314-
return tennisClubMembershipEventDocument
315-
})
316-
],
317-
config: [
318-
http.get(
319-
'/api/countryconfig/certificates/simple-certificate.svg',
320-
() => {
321-
return HttpResponse.text(
322-
`<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100"><text x="10" y="20">Simple Certificate</text></svg>`
323-
)
324-
}
325-
),
326-
http.get('http://localhost:2021/config', () => {
327-
return HttpResponse.json({
328-
systems: [],
329-
config: mockOfflineData.config,
330-
certificates: [
331-
{
332-
id: 'simple-certificate',
333-
isV2Template: true,
334-
event: 'tennis-club-membership',
335-
label: {
336-
id: 'certificates.simple.certificate.copy',
337-
defaultMessage: 'Simple Certificate copy',
338-
description: 'The label for a simple certificate'
339-
},
340-
conditionals: [
341-
{
342-
type: 'SHOW',
343-
conditional: field('applicant.dob')
344-
.isBefore()
345-
.date('2007-01-01')
346-
}
347-
],
348-
isDefault: false,
349-
fee: {
350-
onTime: 7,
351-
late: 10.6,
352-
delayed: 18
353-
},
354-
svgUrl:
355-
'/api/countryconfig/certificates/simple-certificate.svg'
356-
}
357-
]
358-
})
359-
})
360-
]
361-
}
362-
}
363-
},
364-
play: async ({ canvasElement, step }) => {
365-
const canvas = within(canvasElement)
366-
367-
await step('Try continuing without filling the form', async () => {
368-
await expect(
369-
await canvas.findByText('Print certified copy')
370-
).toBeInTheDocument()
371-
372-
const continueButton = await canvas.findByRole('button', {
373-
name: 'Continue'
374-
})
375-
await userEvent.click(continueButton)
376-
const requiredErrors = await canvas.findAllByText('Required')
377-
378-
await expect(requiredErrors.length).toBe(2)
379-
})
380-
381-
await step(
382-
'Click Certification Type and find no options message',
383-
async () => {
384-
await userEvent.click(
385-
await canvas.findByTestId('select__certificateTemplateId')
386-
)
387-
await expect(
388-
await canvas.findByText(
389-
'No template available for this event, contact Admin'
390-
)
391-
).toBeVisible()
392-
}
393-
)
394-
}
395-
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
5+
*
6+
* OpenCRVS is also distributed under the terms of the Civil Registration
7+
* & Healthcare Disclaimer located at http://opencrvs.org/license.
8+
*
9+
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
10+
*/
11+
import {
12+
tennisClubMembershipEvent,
13+
EventDocument,
14+
EventIndex
15+
} from '@opencrvs/commons/client'
16+
import { queryClient, trpcOptionsProxy } from '@client/v2-events/trpc'
17+
import { tennisClubMembershipEventDocument } from '@client/v2-events/features/events/fixtures'
18+
import { addLocalEventConfig, updateLocalEventIndex } from './api'
19+
20+
describe('updateLocalEventIndex', () => {
21+
beforeEach(() => {
22+
queryClient.clear()
23+
addLocalEventConfig(tennisClubMembershipEvent)
24+
})
25+
26+
afterAll(() => {
27+
queryClient.clear()
28+
})
29+
30+
it('preserves total count in cached queries after update', () => {
31+
const eventDocument = tennisClubMembershipEventDocument
32+
33+
// Prepare a cached query simulating a workqueue result
34+
const queryKey = trpcOptionsProxy.event.search.queryKey({
35+
query: { type: 'and', clauses: [{ status: 'PENDING' }] }
36+
})
37+
38+
queryClient.setQueryData(queryKey, {
39+
total: 13,
40+
results: [
41+
{ id: 'abc', status: 'PENDING' },
42+
{ id: eventDocument.id, status: 'PENDING' },
43+
{ id: 'def', status: 'REGISTERED' }
44+
] as EventIndex[]
45+
})
46+
47+
// Call the update
48+
updateLocalEventIndex(eventDocument.id, {
49+
...eventDocument,
50+
status: 'REGISTERED'
51+
} as EventDocument)
52+
53+
// Re-fetch cache
54+
const updated = queryClient.getQueryData(queryKey)
55+
56+
// total must NOT be overwritten by results.length (which is 3)
57+
expect(updated?.total).toBe(13)
58+
59+
// the event status should update correctly
60+
const updatedEvent = updated?.results.find((r) => r.id === eventDocument.id)
61+
expect(updatedEvent?.status).toBe('REGISTERED')
62+
63+
// unrelated events untouched
64+
expect(updated?.results.find((r) => r.id === 'def')?.status).toBe(
65+
'REGISTERED'
66+
)
67+
expect(updated?.results.find((r) => r.id === 'abc')?.status).toBe('PENDING')
68+
})
69+
})

packages/client/src/v2-events/features/events/useEvents/api.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
*
99
* Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.
1010
*/
11-
1211
import { matchMutation } from '@tanstack/react-query'
1312
import {
1413
DecorateQueryProcedure,
@@ -104,7 +103,7 @@ export function findLocalEventIndex(id: string): EventIndex | undefined {
104103
.flatMap(([, data]) => data?.results || [])[0]
105104
}
106105

107-
export function setEventSearchQuery(updatedEventIndex: EventIndex | undefined) {
106+
function setEventSearchQuery(updatedEventIndex: EventIndex | undefined) {
108107
if (!updatedEventIndex) {
109108
return
110109
}
@@ -144,20 +143,40 @@ export function updateLocalEventIndex(id: string, updatedEvent: EventDocument) {
144143
}),
145144
() => ({ results: [updatedEventIndex], total: 1 })
146145
)
147-
/*
148-
* Update all searches where this event is present
146+
147+
/**
148+
* Keeps the cache in sync when an event is updated.
149+
*
150+
* - Iterates through all cached queries e.g. - workqueue queries.
151+
* - If cached data exists, replaces the matching event in `results`
152+
* while preserving the original `total`.
153+
* - If no cached data exists, seeds the cache with a minimal entry
154+
* containing the updated event.
155+
*
156+
* Ensures components depending on these queries re-render with fresh data.
149157
*/
150-
getQueriesData(trpcOptionsProxy.event.search).forEach(([queryKey, data]) => {
151-
const { results } = data || { results: [] }
158+
getQueriesData(trpcOptionsProxy.event.search).forEach(([queryKey]) => {
152159
queryClient.setQueryData<inferOutput<typeof trpcOptionsProxy.event.search>>(
153160
queryKey,
154-
{
155-
total: results.length,
156-
results: results.map((eventIndex) =>
157-
eventIndex.id === id
158-
? { ...eventIndex, ...updatedEventIndex }
159-
: eventIndex
160-
)
161+
(oldData) => {
162+
// In theory, this handles a cache miss. In practice, it should never run here since
163+
// we only update events after the corresponding search query has already been fetched and cached for workqueues.
164+
// Included here to satisfy typescript.
165+
if (!oldData) {
166+
return {
167+
results: [updatedEventIndex],
168+
total: 1
169+
}
170+
}
171+
172+
return {
173+
...oldData,
174+
results: oldData.results.map((eventIndex) =>
175+
eventIndex.id === id
176+
? { ...eventIndex, ...updatedEventIndex }
177+
: eventIndex
178+
)
179+
}
161180
}
162181
)
163182
})

0 commit comments

Comments
 (0)