import React, { useEffect } from 'react'; import { SearchControl } from "../ui/search-control"; import { DismissibleAlert } from '../ui/dismissible-alert'; import { ChevronLeft, ChevronRight } from 'lucide-react'; import { Bundle } from 'fhir/r4'; import { PaginatorConfig, usePaginatorServiceContext } from '../../services'; import { AppSpinner } from '../index.ts'; import { Button } from '../ui/button'; import './Paginator.css'; interface PaginatorProps { fetchFunction: (pageSize: number, pageParam?: string, query?: string) => Promise>; children: (items: T[], loading: boolean) => React.ReactNode; config?: PaginatorConfig; searchPlaceholder?: string; emptyMessage?: string; /** What an empty result says while a search is typed, so a miss never reads as an empty list. */ noMatchMessage?: string; /** * WHAT A REFUSED LIST SAYS, NAMING WHAT WAS REFUSED. * * The rule is that a refusal renders as a denial naming what is denied, so "this list" is not a * denial and the subject has to come from the caller who knows it. The default below still refuses * honestly - it just cannot be specific, which is why every caller that can be should pass this. */ deniedMessage?: React.ReactNode; className?: string; context?: string; } export const Paginator = ({ fetchFunction, children, config = {}, searchPlaceholder = "Search...", emptyMessage = "No items found.", deniedMessage, noMatchMessage, className, context }: PaginatorProps) => { const { state, actions } = usePaginatorServiceContext(); const { items, loading, displaySearchQuery, canGoNext, canGoPrev, readFailure } = state; const { search, paginate, initializeFromUrl, refresh } = actions; useEffect(() => { initializeFromUrl(fetchFunction, config, context); }, [initializeFromUrl, fetchFunction, config, context]); return (
{/* THREE REASONS A LIST IS EMPTY, AND ONLY ONE OF THEM IS "THERE IS NOTHING". Asked in order of how specific the answer is: a refusal is about this reader, a fault is about this moment, and an empty list is about the practice. The empty sentence used to speak for all three, so a permission boundary read as a practice with no patients - and the reader's honest conclusion was that the import failed. */} {!loading && items.length === 0 && readFailure === 'denied' && ( {deniedMessage ?? 'Your role does not include reading this list, so it cannot be shown. ' + 'That is a refusal rather than a loading problem, so trying again will ' + 'not change it. Ask an administrator if you need it.'} )} {/* THE RETRY BELONGS ONLY HERE. Offering one under a refusal hands somebody a button whose every press reproduces the refusal that put it on screen. */} {!loading && items.length === 0 && readFailure === 'error' && (
Could not load this list. Try again.
)} {/* ROWS IN HAND AND A FAILURE, WHICH THE THREE BRANCHES ABOVE CANNOT EXPRESS. Each of them is gated on an EMPTY list, so a read that got five pages and was refused the sixth fell through to the denial and asserted the reader had no access to a list they were looking at. Measured on the staff Patients page: five pages 200, page six 403, and a clinician told they could not read patients whose records they had just opened. ⭐ The rows stay. What could not be read is said BESIDE them rather than instead of them, and the sentence does not claim the whole list is refused - that claim is the false one here, and it is why `deniedMessage` is deliberately not reused. */} {!loading && items.length > 0 && readFailure && ( {readFailure === 'denied' ? 'This list is incomplete because access to some items was denied. Retrying will not show them.' : 'Some of this list could not be loaded, so what is shown is not all of it.'} )} {!loading && items.length === 0 && !readFailure && ( {noMatchMessage && displaySearchQuery.trim() ? noMatchMessage : emptyMessage} )} {loading ? ( ) : ( <> {children(items, loading)} {items.length > 0 && (canGoPrev || canGoNext) && (
)} )}
); }; export default Paginator;