@@ -220,15 +220,17 @@ export default function ProjectModals({
220220 * project or the draft was empty — nothing is lost. When dirty is `true` the
221221 * {@link DiscardDraftConfirm} dialog has already obtained explicit user consent to discard.
222222 *
223- * Sends an error notification on any failure path (matching the convention in { @link openProject})
224- * so the user always gets feedback even when the backend error notification does not reach the
225- * frontend (e.g. a transport-level rejection) .
223+ * The `interlinearizer.createProject` command sends its own error notification before rethrowing,
224+ * so the catch block only needs to log — callers do not need to send a second notification. This
225+ * matches { @link handleSaveAsNew}, which uses the same command and follows the same pattern .
226226 *
227227 * @param config - The configuration collected by the New dialog.
228- * @returns A promise that resolves once the project is created (or the failure is handled).
228+ * @returns `true` if the project was created and persisted successfully; `false` otherwise.
229+ * Callers are responsible for closing the modal on success and keeping it open on failure so
230+ * the user can retry without re-entering their inputs.
229231 */
230232 const createAndPersistProject = useCallback (
231- async ( config : CreateDraftConfig ) => {
233+ async ( config : CreateDraftConfig ) : Promise < boolean > => {
232234 newDraft ( {
233235 analysisLanguages : config . analysisLanguages ,
234236 ...( config . name !== undefined && { suggestedName : config . name } ) ,
@@ -254,19 +256,15 @@ export default function ProjectModals({
254256 }
255257 } catch ( e ) {
256258 logger . error ( 'Interlinearizer: failed to create project from New dialog' , e ) ;
257- await papi . notifications
258- . send ( { message : '%interlinearizer_error_create_project_failed%' , severity : 'error' } )
259- . catch ( ( ) => { } ) ;
260259 }
261260 if ( created ) {
262261 setActiveProject ( created ) ;
263262 } else {
264263 resetActiveProject ( ) ;
265264 }
266- setCreateSourceIsSelect ( false ) ;
267- setModal ( 'none' ) ;
265+ return created !== undefined ;
268266 } ,
269- [ newDraft , projectId , resetActiveProject , setActiveProject , setModal ] ,
267+ [ newDraft , projectId , resetActiveProject , setActiveProject ] ,
270268 ) ;
271269
272270 /**
@@ -286,7 +284,8 @@ export default function ProjectModals({
286284 /**
287285 * Called when the New dialog is submitted. Creates and persists the project immediately, or
288286 * defers behind the unsaved-changes confirmation when the draft is dirty. Disables the modal
289- * buttons via `isCreating` during the backend round-trip.
287+ * buttons via `isCreating` during the backend round-trip. Closes on success; stays open on
288+ * failure so the user can retry without re-entering their inputs.
290289 *
291290 * @param config - The configuration collected by the New dialog.
292291 */
@@ -298,15 +297,24 @@ export default function ProjectModals({
298297 }
299298 setIsCreating ( true ) ;
300299 try {
301- await createAndPersistProject ( config ) ;
300+ const success = await createAndPersistProject ( config ) ;
301+ if ( success ) {
302+ setCreateSourceIsSelect ( false ) ;
303+ setModal ( 'none' ) ;
304+ }
302305 } finally {
303306 setIsCreating ( false ) ;
304307 }
305308 } ,
306- [ createAndPersistProject , dirty ] ,
309+ [ createAndPersistProject , dirty , setCreateSourceIsSelect , setModal ] ,
307310 ) ;
308311
309- /** Confirms the deferred draft-replacing action after the user accepts losing unsaved changes. */
312+ /**
313+ * Confirms the deferred draft-replacing action after the user accepts losing unsaved changes. For
314+ * a deferred Open, delegates entirely to {@link openProject}. For a deferred New, closes on
315+ * success; on failure the discard confirm is dismissed but the underlying create modal stays
316+ * visible so the user can retry.
317+ */
310318 const handleConfirmReplace = useCallback ( async ( ) => {
311319 /* v8 ignore next -- the confirm only renders while a pending action exists */
312320 if ( ! pendingReplace ) return ;
@@ -321,7 +329,11 @@ export default function ProjectModals({
321329 } else {
322330 setIsCreating ( true ) ;
323331 try {
324- await createAndPersistProject ( pendingReplace . config ) ;
332+ const success = await createAndPersistProject ( pendingReplace . config ) ;
333+ if ( success ) {
334+ setCreateSourceIsSelect ( false ) ;
335+ setModal ( 'none' ) ;
336+ }
325337 } finally {
326338 setIsCreating ( false ) ;
327339 }
@@ -330,7 +342,14 @@ export default function ProjectModals({
330342 setIsReplacing ( false ) ;
331343 setPendingReplace ( undefined ) ;
332344 }
333- } , [ createAndPersistProject , isReplacing , openProject , pendingReplace ] ) ;
345+ } , [
346+ createAndPersistProject ,
347+ isReplacing ,
348+ openProject ,
349+ pendingReplace ,
350+ setCreateSourceIsSelect ,
351+ setModal ,
352+ ] ) ;
334353
335354 /** Cancels the deferred action, returning to the underlying modal with the draft untouched. */
336355 const handleCancelReplace = useCallback ( ( ) => setPendingReplace ( undefined ) , [ ] ) ;
0 commit comments