11import type { UseWebViewStateHook } from '@papi/core' ;
22import papi , { logger } from '@papi/frontend' ;
3- import type { DraftProject } from 'interlinearizer' ;
3+ import type { DraftProject , TextAnalysis } from 'interlinearizer' ;
44import { useCallback , useState } from 'react' ;
55import type { NewDraftConfig , OpenableProject } from '../../hooks/useDraftProject' ;
66import type { InterlinearProjectSummary } from '../../types/interlinear-project-summary' ;
@@ -23,11 +23,12 @@ type PendingReplace =
2323 | { kind : 'open' ; project : InterlinearProjectSummary } ;
2424
2525/**
26- * Single mount point for all project-related dialogs. Renders at most one of
27- * {@link SelectInterlinearProjectModal}, {@link CreateProjectModal}, {@link ProjectMetadataModal},
28- * {@link SaveAsProjectModal}, or the {@link DiscardDraftConfirm} guard; manages the shared WebView
29- * state for the active project; and routes New / Open / Save As through the draft (rather than
30- * persisting projects directly on every edit).
26+ * Single mount point for all project-related dialogs. Renders the active one of
27+ * {@link SelectInterlinearProjectModal}, {@link CreateProjectModal}, {@link ProjectMetadataModal}, or
28+ * {@link SaveAsProjectModal}, with the {@link DiscardDraftConfirm} guard overlaid on top when a
29+ * draft-replacing action is pending (so canceling returns to the underlying modal with its state
30+ * intact); manages the shared WebView state for the active project; and routes New / Open / Save As
31+ * through the draft (rather than persisting projects directly on every edit).
3132 *
3233 * @param props - Component props
3334 * @param props.activeProject - The currently active interlinear project (the Save target), read
@@ -40,7 +41,8 @@ type PendingReplace =
4041 * on Save As.
4142 * @param props.loadFromProject - Loads a project's analysis + config into the draft (the "Open"
4243 * flow).
43- * @param props.markSynced - Marks the draft as saved (clears `dirty`) after a successful Save As.
44+ * @param props.markSynced - Marks the draft as saved (clears `dirty`) after a successful Save As,
45+ * given the analysis that was persisted; a no-op if an edit landed during the save.
4446 * @param props.modal - Which modal is currently open.
4547 * @param props.projectId - PAPI source project ID passed from the host.
4648 * @param props.resetDraft - Resets the draft to an empty baseline with the given config (the "New"
@@ -68,7 +70,7 @@ export default function ProjectModals({
6870 dirty : boolean ;
6971 getDraftSnapshot : ( ) => DraftProject | undefined ;
7072 loadFromProject : ( project : OpenableProject ) => void ;
71- markSynced : ( ) => void ;
73+ markSynced : ( savedAnalysis : TextAnalysis ) => void ;
7274 modal : ModalState ;
7375 projectId : string ;
7476 resetDraft : ( config : NewDraftConfig ) => void ;
@@ -282,7 +284,7 @@ export default function ProjectModals({
282284 JSON . stringify ( snapshot . analysis ) ,
283285 ) ;
284286 setActiveProject ( created ) ;
285- markSynced ( ) ;
287+ markSynced ( snapshot . analysis ) ;
286288 setModal ( 'none' ) ;
287289 } catch ( e ) {
288290 logger . error ( 'Interlinearizer: failed to save draft as new project' , e ) ;
@@ -311,7 +313,7 @@ export default function ProjectModals({
311313 JSON . stringify ( snapshot . analysis ) ,
312314 ) ;
313315 setActiveProject ( project ) ;
314- markSynced ( ) ;
316+ markSynced ( snapshot . analysis ) ;
315317 setModal ( 'none' ) ;
316318 } catch ( e ) {
317319 logger . error ( 'Interlinearizer: failed to overwrite project with draft' , e ) ;
@@ -358,54 +360,55 @@ export default function ProjectModals({
358360
359361 return (
360362 < div >
361- { pendingReplace ? (
362- < DiscardDraftConfirm onConfirm = { handleConfirmReplace } onCancel = { handleCancelReplace } />
363- ) : (
364- < >
365- { modal === 'select' && (
366- < SelectInterlinearProjectModal
367- sourceProjectId = { projectId }
368- onSelect = { handleSelectProject }
369- onCreateNew = { handleSelectCreateNew }
370- onClose = { handleSelectClose }
371- onViewInfo = { handleViewInfo }
372- />
373- ) }
363+ { modal === 'select' && (
364+ < SelectInterlinearProjectModal
365+ sourceProjectId = { projectId }
366+ onSelect = { handleSelectProject }
367+ onCreateNew = { handleSelectCreateNew }
368+ onClose = { handleSelectClose }
369+ onViewInfo = { handleViewInfo }
370+ />
371+ ) }
374372
375- { modal === 'create' && (
376- < CreateProjectModal
377- defaultAnalysisLanguage = { defaultAnalysisLanguage }
378- onClose = { handleCreateClose }
379- onCreateDraft = { handleCreateDraft }
380- />
381- ) }
373+ { modal === 'create' && (
374+ < CreateProjectModal
375+ defaultAnalysisLanguage = { defaultAnalysisLanguage }
376+ onClose = { handleCreateClose }
377+ onCreateDraft = { handleCreateDraft }
378+ />
379+ ) }
382380
383- { modal === 'saveAs' && (
384- < SaveAsProjectModal
385- sourceProjectId = { projectId }
386- defaultName = { draftSnapshot ?. suggestedName }
387- defaultDescription = { draftSnapshot ?. suggestedDescription }
388- onSaveNew = { handleSaveAsNew }
389- onOverwrite = { handleOverwrite }
390- onClose = { handleSaveAsClose }
391- />
392- ) }
381+ { modal === 'saveAs' && (
382+ < SaveAsProjectModal
383+ sourceProjectId = { projectId }
384+ defaultName = { draftSnapshot ?. suggestedName }
385+ defaultDescription = { draftSnapshot ?. suggestedDescription }
386+ onSaveNew = { handleSaveAsNew }
387+ onOverwrite = { handleOverwrite }
388+ onClose = { handleSaveAsClose }
389+ />
390+ ) }
393391
394- { modal === 'metadata' && resolvedMetadataProject && (
395- < ProjectMetadataModal
396- interlinearProjectId = { resolvedMetadataProject . id }
397- name = { resolvedMetadataProject . name }
398- description = { resolvedMetadataProject . description }
399- sourceProjectId = { resolvedMetadataProject . sourceProjectId }
400- targetProjectId = { resolvedMetadataProject . targetProjectId }
401- analysisLanguages = { resolvedMetadataProject . analysisLanguages }
402- createdAt = { resolvedMetadataProject . createdAt }
403- onClose = { handleMetadataClose }
404- onProjectSaved = { handleMetadataProjectSaved }
405- onProjectDeleted = { handleMetadataProjectDeleted }
406- />
407- ) }
408- </ >
392+ { modal === 'metadata' && resolvedMetadataProject && (
393+ < ProjectMetadataModal
394+ interlinearProjectId = { resolvedMetadataProject . id }
395+ name = { resolvedMetadataProject . name }
396+ description = { resolvedMetadataProject . description }
397+ sourceProjectId = { resolvedMetadataProject . sourceProjectId }
398+ targetProjectId = { resolvedMetadataProject . targetProjectId }
399+ analysisLanguages = { resolvedMetadataProject . analysisLanguages }
400+ createdAt = { resolvedMetadataProject . createdAt }
401+ onClose = { handleMetadataClose }
402+ onProjectSaved = { handleMetadataProjectSaved }
403+ onProjectDeleted = { handleMetadataProjectDeleted }
404+ />
405+ ) }
406+
407+ { /* The discard guard overlays the active modal rather than replacing it, so canceling
408+ returns to that modal with its in-progress input intact, and confirming an Open does not
409+ unmount (and re-fetch) the still-open select modal underneath. */ }
410+ { pendingReplace && (
411+ < DiscardDraftConfirm onConfirm = { handleConfirmReplace } onCancel = { handleCancelReplace } />
409412 ) }
410413 </ div >
411414 ) ;
0 commit comments