diff --git a/src/components/FeedbackForms/MissingRecord/AuthorsField.tsx b/src/components/FeedbackForms/MissingRecord/AuthorsField.tsx index b6618d6b2..d0c2c3810 100644 --- a/src/components/FeedbackForms/MissingRecord/AuthorsField.tsx +++ b/src/components/FeedbackForms/MissingRecord/AuthorsField.tsx @@ -1,9 +1,10 @@ import { FormControl, FormLabel, Checkbox, FormErrorMessage } from '@chakra-ui/react'; import { useFormContext, useFieldArray, useWatch, Controller } from 'react-hook-form'; -import { AuthorsTable } from './AuthorsTable'; +import { forwardRef } from 'react'; +import { AuthorsTable, AuthorsTableHandle } from './AuthorsTable'; import { FormValues } from './types'; -export const AuthorsField = () => { +export const AuthorsField = forwardRef(function AuthorsField(_, ref) { const { control, formState: { errors }, @@ -17,31 +18,25 @@ export const AuthorsField = () => { return ( <> - + Authors - {!noAuthors && ( - <> - - - )} + {!noAuthors && } - <> - {authors.length === 0 && ( - - ( - - Abstract has no author(s) - - )} - /> - {errors.noAuthors && errors.noAuthors.message} - - )} - + {authors.length === 0 && ( + + ( + + Abstract has no author(s) + + )} + /> + {errors.noAuthors && errors.noAuthors.message} + + )} ); -}; +}); diff --git a/src/components/FeedbackForms/MissingRecord/AuthorsTable.tsx b/src/components/FeedbackForms/MissingRecord/AuthorsTable.tsx index 5f478dc0a..c7bc5d1c7 100644 --- a/src/components/FeedbackForms/MissingRecord/AuthorsTable.tsx +++ b/src/components/FeedbackForms/MissingRecord/AuthorsTable.tsx @@ -8,12 +8,30 @@ import { getPaginationRowModel, useReactTable, } from '@tanstack/react-table'; -import { useState, ChangeEvent, MouseEvent, useRef, useMemo, KeyboardEvent } from 'react'; +import { + useState, + ChangeEvent, + MouseEvent, + useRef, + useMemo, + KeyboardEvent, + forwardRef, + useImperativeHandle, +} from 'react'; import { useFieldArray } from 'react-hook-form'; import { FormValues, IAuthor } from './types'; import { PaginationControls } from '@/components/Pagination'; -export const AuthorsTable = ({ editable }: { editable: boolean }) => { +const columnHelper = createColumnHelper(); + +export interface AuthorsTableHandle { + flush: () => void; +} + +export const AuthorsTable = forwardRef(function AuthorsTable( + { editable }, + ref, +) { const { fields: authors, append, @@ -42,15 +60,11 @@ export const AuthorsTable = ({ editable }: { editable: boolean }) => { const newAuthorNameRef = useRef(); - const isValidAuthor = (author: IAuthor) => { - return author && typeof author.name === 'string' && author.name.length > 1; - }; + const isValidAuthor = (author: IAuthor) => author && typeof author.name === 'string' && author.name.length > 1; const newAuthorIsValid = isValidAuthor(newAuthor); - const editAuthorIsValid = isValidAuthor(editAuthor.author); - const columnHelper = createColumnHelper(); const columns = useMemo(() => { return [ columnHelper.display({ @@ -63,14 +77,14 @@ export const AuthorsTable = ({ editable }: { editable: boolean }) => { }), columnHelper.accessor('aff', { cell: (info) => info.getValue(), - header: 'Affilication', + header: 'Affiliation', }), columnHelper.accessor('orcid', { cell: (info) => info.getValue(), header: 'ORCiD', }), ]; - }, [columnHelper]); + }, []); const table = useReactTable({ columns, @@ -97,9 +111,25 @@ export const AuthorsTable = ({ editable }: { editable: boolean }) => { append(newAuthor); // clear input fields setNewAuthor(null); - newAuthorNameRef.current.focus(); + newAuthorNameRef.current?.focus(); }; + // Flush any in-progress row (new or being edited) when navigating away + useImperativeHandle( + ref, + () => ({ + flush: () => { + if (editAuthorIsValid && editAuthor.index !== -1) { + handleApplyEditAuthor(); + } + if (isValidAuthor(newAuthor)) { + handleAddAuthor(); + } + }, + }), + [newAuthor, editAuthor, editAuthorIsValid], + ); + // Changes to fields for existing authors const handleEditAuthor = (e: MouseEvent) => { @@ -332,4 +362,4 @@ export const AuthorsTable = ({ editable }: { editable: boolean }) => { ); -}; +}); diff --git a/src/components/FeedbackForms/MissingRecord/DraftBanner.test.tsx b/src/components/FeedbackForms/MissingRecord/DraftBanner.test.tsx new file mode 100644 index 000000000..25d3c9f32 --- /dev/null +++ b/src/components/FeedbackForms/MissingRecord/DraftBanner.test.tsx @@ -0,0 +1,30 @@ +import { describe, test, expect, vi } from 'vitest'; +import { render, screen } from '@/test-utils'; +import { DraftBanner } from './DraftBanner'; + +describe('DraftBanner', () => { + test('renders nothing when show is false', () => { + render(); + expect(screen.queryByRole('alert')).toBeNull(); + }); + + test('renders banner with message when show is true', () => { + render(); + expect(screen.getByRole('alert')).toBeInTheDocument(); + expect(screen.getByText(/unsaved draft/i)).toBeInTheDocument(); + }); + + test('calls onRestore when Restore button is clicked', async () => { + const onRestore = vi.fn(); + const { user } = render(); + await user.click(screen.getByRole('button', { name: /restore/i })); + expect(onRestore).toHaveBeenCalledOnce(); + }); + + test('calls onDismiss when Dismiss button is clicked', async () => { + const onDismiss = vi.fn(); + const { user } = render(); + await user.click(screen.getByRole('button', { name: /dismiss/i })); + expect(onDismiss).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/components/FeedbackForms/MissingRecord/DraftBanner.tsx b/src/components/FeedbackForms/MissingRecord/DraftBanner.tsx new file mode 100644 index 000000000..937e11baf --- /dev/null +++ b/src/components/FeedbackForms/MissingRecord/DraftBanner.tsx @@ -0,0 +1,28 @@ +import { Alert, AlertDescription, AlertIcon, Button, HStack } from '@chakra-ui/react'; + +interface DraftBannerProps { + show: boolean; + onRestore: () => void; + onDismiss: () => void; +} + +export function DraftBanner({ show, onRestore, onDismiss }: DraftBannerProps) { + if (!show) { + return null; + } + + return ( + + + You have an unsaved draft for this form. + + + + + + ); +} diff --git a/src/components/FeedbackForms/MissingRecord/FormChecklist.test.tsx b/src/components/FeedbackForms/MissingRecord/FormChecklist.test.tsx new file mode 100644 index 000000000..2a29a487d --- /dev/null +++ b/src/components/FeedbackForms/MissingRecord/FormChecklist.test.tsx @@ -0,0 +1,98 @@ +import { describe, test, expect } from 'vitest'; +import { render, screen } from '@/test-utils'; +import { FormProvider, useForm } from 'react-hook-form'; +import { FormValues } from './types'; +import { FormChecklist } from './FormChecklist'; + +function Wrapper({ values }: { values: Partial }) { + const methods = useForm({ + defaultValues: { + name: '', + email: '', + isNew: true, + bibcode: '', + collection: [], + title: '', + noAuthors: false, + authors: [], + publication: '', + pubDate: '', + urls: [], + abstract: '', + keywords: [], + references: [], + comments: '', + ...values, + }, + }); + return ( + + + + ); +} + +describe('FormChecklist', () => { + test('renders 6 checklist items', () => { + render(); + expect(screen.getAllByRole('listitem')).toHaveLength(6); + }); + + test('all items incomplete when form is empty', () => { + render(); + screen.getAllByRole('listitem').forEach((item) => { + expect(item).toHaveAttribute('data-complete', 'false'); + }); + }); + + test('marks Name complete when name has a value', () => { + render(); + expect(screen.getByTestId('checklist-name')).toHaveAttribute('data-complete', 'true'); + }); + + test('marks Email complete when email is present', () => { + render(); + expect(screen.getByTestId('checklist-email')).toHaveAttribute('data-complete', 'true'); + }); + + test('marks Title complete when title is present', () => { + render(); + expect(screen.getByTestId('checklist-title')).toHaveAttribute('data-complete', 'true'); + }); + + test('marks Authors complete when authors list is non-empty', () => { + render(); + expect(screen.getByTestId('checklist-authors')).toHaveAttribute('data-complete', 'true'); + }); + + test('marks Authors complete when noAuthors is true', () => { + render(); + expect(screen.getByTestId('checklist-authors')).toHaveAttribute('data-complete', 'true'); + }); + + test('shows progress count', () => { + render(); + expect(screen.getByText(/2 of 6/i)).toBeInTheDocument(); + }); + + test('shows 0 of 6 when all fields empty', () => { + render(); + expect(screen.getByText(/0 of 6/i)).toBeInTheDocument(); + }); + + test('shows 6 of 6 when all required fields complete', () => { + render( + , + ); + expect(screen.getByText(/6 of 6/i)).toBeInTheDocument(); + }); +}); diff --git a/src/components/FeedbackForms/MissingRecord/FormChecklist.tsx b/src/components/FeedbackForms/MissingRecord/FormChecklist.tsx new file mode 100644 index 000000000..6727d39cb --- /dev/null +++ b/src/components/FeedbackForms/MissingRecord/FormChecklist.tsx @@ -0,0 +1,70 @@ +import { Box, HStack, List, ListItem, Text } from '@chakra-ui/react'; +import { CheckCircleIcon } from '@chakra-ui/icons'; +import { useFormContext, useWatch } from 'react-hook-form'; +import { isNonEmptyString } from 'ramda-adjunct'; +import { isValidEmail } from '@/utils/common/isValidEmail'; +import { FormValues } from './types'; + +interface ChecklistItem { + id: string; + label: string; + isComplete: boolean; +} + +export function FormChecklist() { + const { control } = useFormContext(); + + const [name, email, title, publication, pubDate, authors, noAuthors] = useWatch< + FormValues, + ['name', 'email', 'title', 'publication', 'pubDate', 'authors', 'noAuthors'] + >({ + control, + name: ['name', 'email', 'title', 'publication', 'pubDate', 'authors', 'noAuthors'], + }); + + const items: ChecklistItem[] = [ + { id: 'name', label: 'Name', isComplete: isNonEmptyString(name) }, + { id: 'email', label: 'Email', isComplete: !!email && isValidEmail(email) }, + { id: 'title', label: 'Title', isComplete: isNonEmptyString(title) }, + { + id: 'authors', + label: 'Author(s)', + isComplete: (authors?.length ?? 0) > 0 || noAuthors === true, + }, + { id: 'publication', label: 'Publication', isComplete: isNonEmptyString(publication) }, + { id: 'pubDate', label: 'Publication Date', isComplete: isNonEmptyString(pubDate) }, + ]; + + const completedCount = items.filter((i) => i.isComplete).length; + + return ( + + + Required Fields + + + {completedCount} of {items.length} + + + {items.map((item) => ( + + + + + {item.label} + + + + ))} + + + ); +} diff --git a/src/components/FeedbackForms/MissingRecord/PubDateField.tsx b/src/components/FeedbackForms/MissingRecord/PubDateField.tsx index a97f6e865..11156a541 100644 --- a/src/components/FeedbackForms/MissingRecord/PubDateField.tsx +++ b/src/components/FeedbackForms/MissingRecord/PubDateField.tsx @@ -5,14 +5,25 @@ import { FormValues } from './types'; export const PubDateField = () => { const { register, + setValue, formState: { errors }, } = useFormContext(); + const { onChange: _onChange, ...rest } = register('pubDate'); + return ( Publication Date - - {errors.pubDate && errors.pubDate.message} + { + e.target.value = e.target.value.replace(/[^\d-]/g, ''); + setValue('pubDate', e.target.value, { shouldValidate: true, shouldDirty: true }); + }} + /> + {errors.pubDate?.message} ); }; diff --git a/src/components/FeedbackForms/MissingRecord/RecordPanel.test.tsx b/src/components/FeedbackForms/MissingRecord/RecordPanel.test.tsx new file mode 100644 index 000000000..721425996 --- /dev/null +++ b/src/components/FeedbackForms/MissingRecord/RecordPanel.test.tsx @@ -0,0 +1,160 @@ +import { render } from '@/test-utils'; +import { waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { RecordPanel } from './RecordPanel'; +import type { UserEvent } from '@testing-library/user-event'; + +// --- mocks --- + +vi.mock('@/api/search/search', () => ({ + useGetSingleRecord: vi.fn(() => ({ + data: null as null, + isFetching: false, + isSuccess: false, + error: null as null, + refetch: vi.fn(), + })), +})); + +vi.mock('@/lib/useGetResourceLinks', async () => { + const actual = await vi.importActual('@/lib/useGetResourceLinks'); + return { + ...actual, + useGetResourceLinks: vi.fn(() => ({ + data: [] as import('@/lib/useGetResourceLinks').IResourceUrl[], + isSuccess: false, + isFetching: false, + refetch: vi.fn(), + })), + }; +}); + +vi.mock('@/lib/useGetUserEmail', () => ({ + useGetUserEmail: vi.fn((): string | null => null), +})); + +vi.mock('@/api/feedback/feedback', () => ({ + useFeedback: vi.fn(() => ({ mutate: vi.fn(), isLoading: false })), +})); + +vi.mock('react-google-recaptcha-v3', () => ({ + useGoogleReCaptcha: vi.fn(() => ({ executeRecaptcha: vi.fn() })), +})); + +// --- helpers --- + +const defaultProps = { + onOpenAlert: vi.fn(), + onCloseAlert: vi.fn(), + isFocused: false, +}; + +type QueryHelpers = Pick, 'getByLabelText' | 'getByRole' | 'getByText'>; + +/** + * Fill all required fields so the form becomes valid. + * Uses the noAuthors checkbox instead of adding an author row. + */ +const fillRequiredFields = async (user: UserEvent, queries: QueryHelpers) => { + const { getByLabelText, getByRole } = queries; + await user.type(getByLabelText('Name*'), 'Jane Doe'); + await user.type(getByLabelText('Email*'), 'jane@example.com'); + await user.type(getByLabelText('Title*'), 'A Great Paper'); + await user.type(getByLabelText('Publication*'), 'Nature'); + await user.type(getByLabelText('Publication Date*'), '2024-01'); + await user.click(getByRole('checkbox', { name: /Abstract has no author/i })); +}; + +// --- tests --- + +describe('RecordPanel — New Record mode (isNew=true)', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('renders all required field labels', () => { + const { getByLabelText } = render(); + + // Chakra appends '*' directly to the label text for required fields + expect(getByLabelText('Name*')).toBeInTheDocument(); + expect(getByLabelText('Email*')).toBeInTheDocument(); + expect(getByLabelText('Title*')).toBeInTheDocument(); + expect(getByLabelText('Publication*')).toBeInTheDocument(); + expect(getByLabelText('Publication Date*')).toBeInTheDocument(); + }); + + test('Preview button is disabled when form is empty', () => { + const { getByRole } = render(); + const previewBtn = getByRole('button', { name: /preview/i }); + expect(previewBtn).toBeDisabled(); + }); + + test('Preview button becomes enabled when all required fields are filled', async () => { + const result = render(); + const { user, getByRole } = result; + + const previewBtn = getByRole('button', { name: /preview/i }); + expect(previewBtn).toBeDisabled(); + + await fillRequiredFields(user, result); + + await waitFor(() => expect(previewBtn).not.toBeDisabled()); + }); + + test('Preview button stays disabled when only some required fields are filled', async () => { + // Verifies that all required fields must be filled — not just one. + const result = render(); + const { user, getByRole, getByLabelText } = result; + + const previewBtn = getByRole('button', { name: /preview/i }); + expect(previewBtn).toBeDisabled(); + + // Fill everything except pubDate + await user.type(getByLabelText('Name*'), 'Jane Doe'); + await user.type(getByLabelText('Email*'), 'jane@example.com'); + await user.type(getByLabelText('Title*'), 'A Great Paper'); + await user.type(getByLabelText('Publication*'), 'Nature'); + await user.click(getByRole('checkbox', { name: /Abstract has no author/i })); + + // Button should still be disabled — pubDate is missing + await waitFor(() => expect(previewBtn).toBeDisabled()); + }); + + test('noAuthors checkbox is visible when no authors have been added', () => { + const { getByRole } = render(); + expect(getByRole('checkbox', { name: /Abstract has no author/i })).toBeInTheDocument(); + }); +}); + +describe('RecordPanel — Edit Record mode (isNew=false)', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('renders bibcode input and Load button', () => { + const { getByLabelText, getByRole } = render(); + + // In edit mode the label reads "SciX-ID / DOI / Bibcode" + expect(getByLabelText(/SciX-ID \/ DOI \/ Bibcode/i)).toBeInTheDocument(); + expect(getByRole('button', { name: /load/i })).toBeInTheDocument(); + }); + + test('Load button is disabled when bibcode field is empty', () => { + const { getByRole } = render(); + + const loadBtn = getByRole('button', { name: /load/i }); + expect(loadBtn).toBeDisabled(); + }); + + test('Load button becomes enabled when bibcode field has a value', async () => { + const { user, getByRole, getByLabelText } = render(); + + const loadBtn = getByRole('button', { name: /load/i }); + expect(loadBtn).toBeDisabled(); + + const bibcodeInput = getByLabelText(/SciX-ID \/ DOI \/ Bibcode/i); + await user.type(bibcodeInput, '2024ApJ...123..456A'); + + await waitFor(() => expect(loadBtn).not.toBeDisabled()); + }); +}); diff --git a/src/components/FeedbackForms/MissingRecord/RecordPanel.tsx b/src/components/FeedbackForms/MissingRecord/RecordPanel.tsx index df9658653..660a2ba7e 100644 --- a/src/components/FeedbackForms/MissingRecord/RecordPanel.tsx +++ b/src/components/FeedbackForms/MissingRecord/RecordPanel.tsx @@ -1,6 +1,8 @@ import { AlertStatus, + Box, Button, + ButtonGroup, Checkbox, CheckboxGroup, Flex, @@ -16,17 +18,23 @@ import { } from '@chakra-ui/react'; import { omit } from 'ramda'; -import { MouseEvent, useEffect, useMemo, useState } from 'react'; +import { MouseEvent, useEffect, useMemo, useRef, useState } from 'react'; import { Controller, FormProvider, useForm } from 'react-hook-form'; import { AuthorsField } from './AuthorsField'; +import { AuthorsTableHandle } from './AuthorsTable'; import { BibcodeField } from './BibcodeField'; +import { DraftBanner } from './DraftBanner'; +import { FormChecklist } from './FormChecklist'; import { getDiffSections, getDiffString, processFormValues } from './DiffUtil'; import { KeywordsField } from './KeywordsField'; import { PubDateField } from './PubDateField'; -import { ReferencesField } from './ReferencesField'; +import { RecordWizard } from './RecordWizard'; +import { ReferencesField, ReferencesTableHandle } from './ReferencesField'; import { DiffSection, FormValues, IAuthor, IKeyword, IReference } from './types'; -import { UrlsField } from './UrlsField'; +import { UrlsField, UrlsTableHandle } from './UrlsField'; import { DiffSectionPanel } from './DiffSectionPanel'; +import { useFormDraft } from './useFormDraft'; +import { COLLECTIONS } from './types'; import { AxiosError } from 'axios'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -34,21 +42,20 @@ import { SimpleLink } from '@/components/SimpleLink'; import { PreviewModal } from '@/components/FeedbackForms'; import { IResourceUrl, useGetResourceLinks } from '@/lib/useGetResourceLinks'; import { useGetUserEmail } from '@/lib/useGetUserEmail'; -import { parsePublicationDate } from '@/utils/common/parsePublicationDate'; import type { Database, IDocsEntity } from '@/api/search/types'; import type { IFeedbackParams } from '@/api/feedback/types'; import { useGetSingleRecord } from '@/api/search/search'; - -const collections: { value: Database; label: string }[] = [ - { value: 'astronomy', label: 'Astronomy and Astrophysics' }, - { value: 'physics', label: 'Physics and Geophysics' }, - { value: 'earthscience', label: 'Earth Science' }, - { value: 'general', label: 'General' }, -]; +import { LocalSettings } from '@/types'; type State = 'idle' | 'loading-record' | 'loading-urls' | 'submitting' | 'preview'; +type FormMode = 'expert' | 'guided'; + +// accepts YYYY-MM or YYYY-MM-DD; lax on day (00 is valid for ADS) +const PUB_DATE_RE = /^\d{4}-\d{2}(-\d{2})?$/; -const isInvalidPubDate = (pubdate: string) => parsePublicationDate(pubdate) === null; +function isInvalidPubDate(pubdate: string): boolean { + return !PUB_DATE_RE.test(pubdate); +} const validationSchema = z .object({ @@ -95,26 +102,41 @@ const validationSchema = z return z.NEVER; }); -export const RecordPanel = ({ - isNew, - onOpenAlert, - onCloseAlert, - isFocused, - bibcode, -}: { +function getDraftKey(isNew: boolean, bibcode?: string): string | null { + if (isNew) { + return LocalSettings.FEEDBACK_DRAFT_NEW; + } + if (bibcode) { + return `feedback-draft:edit-record:${bibcode}`; + } + return null; +} + +function getInitialMode(): FormMode { + try { + const stored = localStorage.getItem(LocalSettings.FEEDBACK_FORM_MODE); + return stored === 'guided' ? 'guided' : 'expert'; + } catch { + return 'expert'; + } +} + +interface RecordPanelProps { isNew: boolean; onOpenAlert: (params: { status: AlertStatus; title: string; description?: string }) => void; onCloseAlert: () => void; isFocused: boolean; bibcode?: string; -}) => { +} + +export function RecordPanel({ isNew, onOpenAlert, onCloseAlert, isFocused, bibcode }: RecordPanelProps) { const userEmail = useGetUserEmail(); const initialFormValues = { name: '', email: userEmail ?? '', bibcode: bibcode ?? '', - isNew: isNew, + isNew, collection: [] as Database[], title: '', noAuthors: false, @@ -128,14 +150,12 @@ export const RecordPanel = ({ comments: '', }; - // original form values from existing record - // used for diff view const [recordOriginalFormValues, setRecordOriginalFormValues] = useState(initialFormValues); const formMethods = useForm({ defaultValues: recordOriginalFormValues, resolver: zodResolver(validationSchema), - mode: 'onSubmit', + mode: 'onTouched', reValidateMode: 'onBlur', shouldFocusError: true, }); @@ -150,6 +170,18 @@ export const RecordPanel = ({ setFocus, } = formMethods; + const authorsRef = useRef(null); + const referencesRef = useRef(null); + const urlsRef = useRef(null); + + // New records use a fixed key; edit records scope to bibcode + const draftKey = getDraftKey(isNew, bibcode); + + const { hasDraft, getDraftValues, clearDraft, cancelPendingSave } = useFormDraft(draftKey, formMethods); + const [showDraftBanner, setShowDraftBanner] = useState(hasDraft); + + const [formMode, setFormMode] = useState(() => (isNew ? getInitialMode() : 'expert')); + const { isOpen: isPreviewOpen, onOpen: openPreview, onClose: closePreview } = useDisclosure(); const [state, setState] = useState(bibcode ? 'loading-record' : 'idle'); @@ -233,7 +265,7 @@ export const RecordPanel = ({ diff: diffString, }); } catch { - onOpenAlert({ status: 'error', title: 'There was a problem processing diff. Plesae try again.' }); + onOpenAlert({ status: 'error', title: 'Could not generate diff preview. Please try again.' }); setState('idle'); } } else if (state === 'preview') { @@ -368,9 +400,18 @@ export const RecordPanel = ({ setState('submitting'); }; + const handleStandardPreview = handleSubmit(() => { + authorsRef.current?.flush(); + referencesRef.current?.flush(); + urlsRef.current?.flush(); + handlePreview(); + }); + // submitted const handleOnSuccess = () => { onOpenAlert({ status: 'success', title: 'Feedback submitted successfully' }); + clearDraft(); + setShowDraftBanner(false); if (isNew) { reset(); } else { @@ -396,103 +437,170 @@ export const RecordPanel = ({ setState('idle'); }; + const handleRestoreDraft = () => { + const draft = getDraftValues(); + if (!draft) { + return; + } + cancelPendingSave(); + // preserve authenticated email over any email stored in the draft + const mergedEmail = userEmail ?? draft.email; + reset({ ...draft, email: mergedEmail }); + setShowDraftBanner(false); + }; + + const handleDismissDraft = () => { + clearDraft(); + setShowDraftBanner(false); + }; + + const handleModeChange = (mode: FormMode) => { + setFormMode(mode); + try { + localStorage.setItem(LocalSettings.FEEDBACK_FORM_MODE, mode); + } catch { + // ignore storage errors + } + }; + return ( - - - Name - - {errors.name && errors.name.message} - - - Email - - {errors.email && errors.email.message} - - - - - - {(isNew || (!isNew && !!recordOriginalFormValues.title)) && ( - <> - - Collection - ( - - - {collections.map((c) => ( - - {c.label} - - ))} - - - )} - /> - + + + {isNew && ( + + + + + + + )} - - Title - + {formMode !== 'guided' && ( + + + Name + + {errors.name?.message} - - - - - - Publication - - - - - - - - - - Abstract -