// SPDX-License-Identifier: AGPL-3.0-or-later
import {
Button,
Card,
ErrorNotice,
LoadMore,
LogList,
SectionTitle,
Stack,
Text,
__,
sprintf,
useAdminSettings,
useGraph,
useGraphMutation,
useToaster,
} from '@alphone/frontend-sdk'
import type { AdminSettingsRead, GraphFailure } from '@alphone/frontend-sdk'
import { useEffect, useId, useMemo, useRef, useState } from 'react'
import { EntryCells } from './cells'
import { draftCells, entriesOf, holdsInput, localDay, typedEntry } from './entries'
import type { RepeaterRow } from './entries'
import { entryMessage, outcomeOf, refetchAfter } from './entryOutcome'
import { EntryRow } from './EntryRow'
import { focusFirstControl } from './focus'
import { addContactFieldEntryMutation } from './operations'
import { useEntryActions } from './useEntryActions'
import type { GoneHandler } from './useEntryActions'
/**
* Returns how many entries a repeater draws: none until the page size arrives, every one when it cannot be read.
* @param sizing - The read of the admin settings.
* @param pages - The pages the reader opened.
* @param total - The entries the repeater holds.
* @returns The count to draw.
*/
function entriesToDraw({ settings, failed }: AdminSettingsRead, pages: number, total: number) {
if (settings === undefined) {
return failed ? total : 0
}
return pages * settings.listPageSize
}
/**
* Renders one repeater of a contact as a card: its heading in the header, one add form and a page of its entries.
* @param props - The contact, the repeater, its stored value and the report of a repeater gone.
* @returns The repeater card.
*/
export function RepeaterEntries({
contactId,
field,
stored,
onGone,
}: {
contactId: string
field: RepeaterRow
stored: unknown
onGone: GoneHandler
}) {
const heading = useId()
const entries = useMemo(() => entriesOf(stored), [stored])
const sizing = useAdminSettings()
const [pages, setPages] = useState(1)
const shown = entriesToDraw(sizing, pages, entries.length)
const actions = useEntryActions({ contactId, field, entries, shown, onGone })
const listed = actions.listed
const more = {
hasNextPage: listed.length > 0 && listed.length < entries.length,
isFetchingNextPage: false,
fetchNextPage: async () => setPages((held) => held + 1),
}
return (
{field.label}
{actions.failure !== '' && {actions.failure}}
{entries.length === 0 && {__('No entries yet.', 'alphone-fields')}}
{listed.length > 0 && (
{listed.map((entry) => (
))}
)}
{__('Load more entries', 'alphone-fields')}
)
}
/**
* Renders the form adding one entry, its dates starting on today.
* @param props - The contact, the repeater, the count of focus requests, and what runs on an add and a repeater gone.
* @returns The add form.
*/
function AddEntryForm({
contactId,
field,
focusCount,
onAdded,
onGone,
}: {
contactId: string
field: RepeaterRow
focusCount: number
onAdded: (from: Element | null) => void
onGone: GoneHandler
}) {
const [picked, setPicked] = useState>(() => new Map())
const [add, runAdd] = useGraphMutation(addContactFieldEntryMutation)
const graph = useGraph()
const toaster = useToaster()
const formRef = useRef(null)
const shown = draftCells(field.subFields, picked, localDay(new Date()))
const label = sprintf(__('Add an entry to %(label)s', 'alphone-fields'), { label: field.label })
const fallback = __('The entry could not be added.', 'alphone-fields')
useEffect(() => {
if (focusCount > 0) {
focusFirstControl(formRef.current as HTMLFormElement)
}
}, [focusCount])
const submit = async () => {
const from = document.activeElement
const result = await runAdd({ contactId, field: field.name, entry: typedEntry(field.subFields, shown) })
refetchAfter(graph, result.error)
const outcome = outcomeOf(result.error)
if (outcome === 'done') {
setPicked(new Map())
toaster.show(__('Entry added.', 'alphone-fields'))
onAdded(from)
} else if (outcome === 'field-gone') {
onGone(entryMessage(result.error as GraphFailure, fallback), from)
}
}
return (
)
}