Add the Google Calendar settings screen and wire Push to Calendar (Feature 16) #129

2 changed files with 92 additions and 11 deletions
Showing only changes of commit 5c8484cfb8 - Show all commits
+67
View File
@@ -269,4 +269,71 @@ describe('week mutations', () => {
hook.rerender({ week: LATER })
expect(weekMutations(queryClient, NEXT)).toEqual([])
})
it('leaves behind what settles in a week no longer shown, unless that week still has one pending or it is the failure that speaks for it', async () => {
const friday = deferred()
const monday = deferred()
const nextPush = deferred()
const laterPush = deferred()
stubServer({
[`GET ${PLAN}`]: () => jsonResponse(200, planBody()),
[`PUT ${FRIDAY}`]: () => friday.reply,
[`PUT ${MONDAY}`]: () => monday.reply,
[`GET /api/v1/plans/${NEXT}`]: () => errorResponse(404, 'not_found'),
[`POST /api/v1/plans/${NEXT}/push-calendar`]: () => nextPush.reply,
[`GET /api/v1/plans/${LATER}`]: () => errorResponse(404, 'not_found'),
[`POST /api/v1/plans/${LATER}/push-calendar`]: () => laterPush.reply,
})
const api = createApiClient(fakeLocation('/plan'), createNetworkStatus(window), createVerifiedSession(localStorage))
await api.loadMe()
const queryClient = createQueryClient()
const wrapper = ({ children }: { children: ReactNode }) => (
<ApiProvider client={api}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</ApiProvider>
)
const hook = renderHook(
({ week }) => ({ plan: usePlan(week), swap: useSwapDay(week), push: usePushToCalendar(week) }),
{ wrapper, initialProps: { week: WEEK } },
)
await waitFor(() => expect(hook.result.current.plan.isSuccess).toBe(true))
// Two swaps of the week and a push of the next are left pending, and a
// third week is shown.
act(() => hook.result.current.swap.mutate(ORZO))
act(() => hook.result.current.swap.mutate(FISH))
await waitFor(() => expect(weekMutations(queryClient, WEEK)).toEqual(['swap pending', 'swap pending']))
hook.rerender({ week: NEXT })
await waitFor(() => expect(hook.result.current.plan.isSuccess).toBe(true))
act(() => hook.result.current.push.mutate())
hook.rerender({ week: LATER })
await waitFor(() => expect(hook.result.current.plan.isSuccess).toBe(true))
expect(weekMutations(queryClient, NEXT)).toEqual(['push pending'])
// The later swap settles first: the earlier, still pending, must see it,
// so both stay.
await act(async () => monday.send(jsonResponse(200, planBody({ 0: { id: 'r1', name: 'Fish tacos' } }))))
await waitFor(() => expect(weekMutations(queryClient, WEEK)).toEqual(['swap pending', 'swap success']))
// The push succeeds while the third week is shown: it is left behind,
// and the week's pending swap is not.
await act(async () => nextPush.send(jsonResponse(200, { ...planBody(), week_start: NEXT })))
await waitFor(() => expect(weekMutations(queryClient, NEXT)).toEqual([]))
expect(weekMutations(queryClient, WEEK)).toEqual(['swap pending', 'swap success'])
// Once the earlier swap fails, the week has none pending: both are left
// behind, the failure not being the week's latest.
await act(async () => friday.send(errorResponse(409, 'dinner_not_plannable')))
await waitFor(() => expect(weekMutations(queryClient, WEEK)).toEqual([]))
// A push of the third week fails once another is shown: it stays, to be
// shown on coming back, until yet another week is.
act(() => hook.result.current.push.mutate())
hook.rerender({ week: NEXT })
await waitFor(() => expect(hook.result.current.plan.isSuccess).toBe(true))
await act(async () => laterPush.send(errorResponse(500, 'internal')))
await waitFor(() => expect(weekMutations(queryClient, LATER)).toEqual(['push error']))
hook.rerender({ week: WEEK })
expect(weekMutations(queryClient, LATER)).toEqual([])
})
})
+25 -11
View File
@@ -185,20 +185,34 @@ type WeekMutation = 'swap' | 'push'
// useWeekMutationsKept bounds the mutations of kind the cache keeps. When a
// week is shown, the settled ones of every other week are removed, unless
// that week still has one pending: a pending one, and those an earlier
// pending one must see, stay across leaving its week and coming back. The
// cache holds at most the week shown's and those of weeks left pending.
// pending one must see, stay across leaving its week and coming back. When
// one settles, the same is done, except that a week's latest, when it
// failed, stays until another week is shown, so that coming straight back to
// it shows the failure. The cache holds at most the week shown's, those of
// weeks left pending, and one failure of each week whose last pending one
// settled since the week shown was.
function useWeekMutationsKept(weekStart: string, kind: WeekMutation) {
const queryClient = useQueryClient()
useEffect(() => {
const mutations = queryClient.getMutationCache()
const left = mutations.findAll({
predicate: ({ options: { mutationKey: key } }) =>
key !== undefined && key.length === 3 && key[0] === 'plans' && key[1] !== weekStart && key[2] === kind,
})
const pending = new Set(left.filter((m) => m.state.status === 'pending').map((m) => m.options.mutationKey![1]))
for (const mutation of left) {
if (!pending.has(mutation.options.mutationKey![1])) mutations.remove(mutation)
const isKind = (key: readonly unknown[] | undefined) => key !== undefined && key.length === 3 && key[0] === 'plans' && key[2] === kind
function sweep(settled: boolean) {
const left = mutations.findAll({
predicate: ({ options: { mutationKey: key } }) => isKind(key) && key![1] !== weekStart,
})
const pending = new Set(left.filter((m) => m.state.status === 'pending').map((m) => m.options.mutationKey![1]))
const latest = new Map(left.map((m) => [m.options.mutationKey![1], m]))
for (const mutation of left) {
const week = mutation.options.mutationKey![1]
if (pending.has(week)) continue
if (settled && latest.get(week) === mutation && mutation.state.status === 'error') continue
mutations.remove(mutation)
}
}
sweep(false)
return mutations.subscribe((event) => {
if (event.type === 'updated' && isKind(event.mutation.options.mutationKey) && event.mutation.state.status !== 'pending') sweep(true)
})
}, [queryClient, weekStart, kind])
}
@@ -252,7 +266,7 @@ export function useSwapDay(weekStart: string): SwapDay {
const isLatest = () => mutations.findAll({ mutationKey }).every((other) => other.mutationId <= mutation.mutationId)
const mutation = mutations.build<Plan, Error, Swap, { previous: PlanDay; swapped: PlanDay }>(queryClient, {
mutationKey,
// Kept until dismissed or its week is left settled
// Kept until dismissed or settled in a week not shown
// (useWeekMutationsKept): a later swap must stay in the cache while an
// earlier one is pending.
gcTime: Infinity,
@@ -350,7 +364,7 @@ export function usePushToCalendar(weekStart: string): PushToCalendar {
function mutate() {
const mutation = mutations.build<Plan, Error, void, unknown>(queryClient, {
mutationKey,
// Kept until reset or its week is left settled (useWeekMutationsKept):
// Kept until reset or settled in a week not shown (useWeekMutationsKept):
// a push pending in a week left must still be found when it is shown
// again.
gcTime: Infinity,