Add the Google Calendar settings screen and wire Push to Calendar (Feature 16) #129
@@ -392,6 +392,41 @@ describe('PlanPage', () => {
|
||||
expect(planReads(fetchMock)).toBe(2)
|
||||
})
|
||||
|
||||
it('pushes nothing again until the changed plan is read and shown', async () => {
|
||||
let read: () => Response | Promise<Response> = () => jsonResponse(200, planBody('draft'))
|
||||
let push = () => errorResponse(409, 'plan_changed')
|
||||
const fetchMock = stubServer({
|
||||
[`GET ${THIS_WEEK}`]: () => read(),
|
||||
[`POST ${PUSH}`]: () => push(),
|
||||
})
|
||||
await renderPage()
|
||||
|
||||
await screen.findByText('Beef tacos')
|
||||
const reread = deferred()
|
||||
read = () => reread.response
|
||||
const pushButton = screen.getByRole('button', { name: 'Looks good — push to Calendar' })
|
||||
fireEvent.click(pushButton)
|
||||
await waitFor(() => expect(planReads(fetchMock)).toBe(2))
|
||||
// While the changed plan is read, the push stays busy: neither Push
|
||||
// nor Retry sends it again.
|
||||
expect(screen.getByRole('button', { name: 'Pushing to Calendar…' })).toBeDisabled()
|
||||
expect(screen.queryByRole('button', { name: 'Retry' })).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Pushing to Calendar…' }))
|
||||
expect(screen.getByText('Beef tacos')).toBeInTheDocument()
|
||||
expect(posts(fetchMock)).toEqual([`${PUSH} undefined`])
|
||||
|
||||
reread.send(jsonResponse(200, planBody('draft', ['Lasagna', ...NAMES.slice(1)])))
|
||||
const alert = await screen.findByRole('alert')
|
||||
expect(alert).toHaveTextContent("Couldn't push to Calendar. The plan changed while it was being pushed; push it again.")
|
||||
expect(screen.getByText('Lasagna')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Looks good — push to Calendar' })).toBeEnabled()
|
||||
|
||||
push = () => jsonResponse(200, syncedNow())
|
||||
fireEvent.click(within(alert).getByRole('button', { name: 'Retry' }))
|
||||
expect(await screen.findByText('Synced to Google Calendar just now')).toBeInTheDocument()
|
||||
expect(posts(fetchMock)).toEqual([`${PUSH} undefined`, `${PUSH} undefined`])
|
||||
})
|
||||
|
||||
it('rebuilds a draft at once, showing skeleton tiles until the new plan is read', async () => {
|
||||
let plan = planBody('draft')
|
||||
let job = draftJob('running')
|
||||
|
||||
+6
-4
@@ -280,8 +280,10 @@ export function useSwapDay(weekStart: string): SwapDay {
|
||||
// chosen Google calendar (POST /api/v1/plans/{weekStart}/push-calendar). The
|
||||
// server answers with the plan as synced, which replaces the one shown, as a
|
||||
// swap's answer does. A push refused because the plan changed meanwhile
|
||||
// reads the plan again; one Google refused the grant for reads the Google
|
||||
// Calendar connection again, which is now reconnect required.
|
||||
// reads the plan again, and stays pending until that read lands: nothing is
|
||||
// pushed again before the changed plan is shown. One Google refused the
|
||||
// grant for reads the Google Calendar connection again, which is now
|
||||
// reconnect required.
|
||||
export function usePushToCalendar(weekStart: string) {
|
||||
const { apiFetch } = useApi()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -292,9 +294,9 @@ export function usePushToCalendar(weekStart: string) {
|
||||
return parsePlan(`POST ${path}`, weekStart, await apiFetch<unknown>(path, { method: 'POST' }), false)
|
||||
},
|
||||
onSuccess: (plan) => queryClient.setQueryData<Plan>(key, plan),
|
||||
onError: (error) => {
|
||||
if (error instanceof ApiError && error.code === 'plan_changed') void queryClient.invalidateQueries({ queryKey: key })
|
||||
onError: async (error) => {
|
||||
noteGoogleFailure(queryClient, error)
|
||||
if (error instanceof ApiError && error.code === 'plan_changed') await queryClient.invalidateQueries({ queryKey: key })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -129,6 +129,12 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.reconnectStatus {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.reconnectText {
|
||||
margin: 0;
|
||||
font-family: var(--font-body);
|
||||
|
||||
@@ -279,7 +279,14 @@ describe('CalendarSettingsPage', () => {
|
||||
})
|
||||
await renderPage()
|
||||
|
||||
expect(await screen.findByRole('region', { name: 'Reconnect Google Calendar' })).toBeInTheDocument()
|
||||
const banner = await screen.findByRole('region', { name: 'Reconnect Google Calendar' })
|
||||
// Why and what to do are announced as the screen turns to the banner.
|
||||
const alert = screen.getByRole('alert')
|
||||
expect(banner).toContainElement(alert)
|
||||
expect(alert).toHaveTextContent(
|
||||
'Google access was revoked or expired — reconnect' + 'Nothing can be pushed to Calendar until you connect again.',
|
||||
)
|
||||
expect(within(alert).queryByRole('link')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('combobox')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Connected · Dinners')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -96,7 +96,8 @@ function Connection({ connection }: { connection: GoogleConnection }) {
|
||||
}
|
||||
|
||||
// ReconnectBanner is the grape banner of a grant Google refused, with why
|
||||
// and Reconnect.
|
||||
// and Reconnect. Why and what to do are an alert, as ErrorBanner's message
|
||||
// is: a calendar request that meets the refusal turns the screen to it.
|
||||
function ReconnectBanner({ error }: { error: string | null }) {
|
||||
const titleId = useId()
|
||||
return (
|
||||
@@ -104,8 +105,10 @@ function ReconnectBanner({ error }: { error: string | null }) {
|
||||
<h2 id={titleId} className={styles.reconnectTitle}>
|
||||
Reconnect Google Calendar
|
||||
</h2>
|
||||
{error !== null && <p className={styles.reconnectText}>{error}</p>}
|
||||
<p className={styles.reconnectText}>Nothing can be pushed to Calendar until you connect again.</p>
|
||||
<div role="alert" className={styles.reconnectStatus}>
|
||||
{error !== null && <p className={styles.reconnectText}>{error}</p>}
|
||||
<p className={styles.reconnectText}>Nothing can be pushed to Calendar until you connect again.</p>
|
||||
</div>
|
||||
<ConnectLink icon={RefreshCw}>Reconnect</ConnectLink>
|
||||
</section>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user