import type { SectionReadFailure } from './sectionReadRefusal'; /** * ONE STATE FOR A LIST, DECIDED ONCE. * * THE DEFECT THIS EXISTS TO END, measured on the Automated Tasks screen: the error and the empty state * were INDEPENDENT branches, so a failed load rendered both at the same time. * * Unable to load automated tasks. Please try again. * No automated tasks yet. * Create a custom task or set a trigger on a questionnaire... * * The reader is told the list could not be read, and immediately under it, that there is nothing in it * and they should make one. The second sentence gives them a reason to disbelieve the first, and then * instructs. Somebody who follows it creates a rule the practice may already have, and the practice * assigns the same questionnaire twice. * * WHY A DERIVED VALUE RATHER THAN A CONDITION ADDED TO THE EMPTY BRANCH. Writing * `rows.length === 0 && !loadError` fixes the screenshot and leaves the next list one forgotten * condition away from the same bug - and the bug is invisible in review, because each branch reads * correctly on its own. Three booleans have eight combinations and only five of them are states this * screen has; a single value has exactly the states it names. Two of these can never render together * because there is only one of them. * * THE ORDER IS THE PRECEDENCE AND IT IS NOT ARBITRARY: * * loading first, because a read still in flight has not yet said anything about the world - and an * empty list drawn while the request is open is the same false assertion arriving early. * stale before both refusals, whenever a failure arrives with rows already in hand: those rows were * read, so a sentence saying nothing could be read asserts there are none. They are whole rather than * partial, so what belongs beside them is their age and not what is missing. * denied before failed, because they are different sentences: one cannot succeed on a retry. * failed before empty, which is the whole of this card - a refusal is never emptiness. * empty before ready, which is only arithmetic. * * WHY THIS IS NOT `ResourceListStatus`, WHICH ENFORCES THE SAME INVARIANT. That component holds the rule * for the CLINICAL lists and it also renders them - an alert banner and a one-line empty message. It is * the right answer wherever those are the right shapes. It cannot serve a screen whose loading state is a * skeleton row and whose empty state is an icon, a heading and an instruction, because it draws neither, * and a caller that keeps its own empty state alongside it is back to two branches. So the RULE is split * from the PAINT: this decides which state, the screen draws it. Prefer the component where it fits. * * IT DOES NOT DECIDE THE COPY. What a denied list says has to name what was denied, and only the caller * knows the subject - `sectionReadDeniedMessage` is the helper for that sentence. This answers which * state, never which words. */ export type ListReadState = 'loading' | 'denied' | 'failed' | 'stale' | 'empty' | 'ready'; /** * @param loading whether the read is still in flight * @param failure what came back, from `sectionReadFailure` - `null` when the read succeeded * @param itemCount how many rows are in hand, from this read or the last one that succeeded */ export const listReadState = ( loading: boolean, failure: SectionReadFailure | null, itemCount: number, ): ListReadState => { if (loading) return 'loading'; if (failure && itemCount > 0) return 'stale'; if (failure === 'denied') return 'denied'; if (failure) return 'failed'; return itemCount === 0 ? 'empty' : 'ready'; };