@@ -41,8 +41,9 @@ type PendingReplace =
4141 * on Save As.
4242 * @param props.loadFromProject - Loads a project's analysis + config into the draft (the "Open"
4343 * flow).
44- * @param props.newDraft - Starts a fresh, empty draft (the "New" flow); no backend project is
45- * created until Save As.
44+ * @param props.newDraft - Seeds the in-memory draft with empty analysis and the given config;
45+ * called by {@link createAndPersistProject} before the backend round-trip so the editor is ready
46+ * regardless of whether persistence succeeds.
4647 * @param props.markSynced - Marks the draft as saved (clears `dirty`) after a successful Save As,
4748 * given the analysis that was persisted; a no-op if an edit landed during the save.
4849 * @param props.modal - Which modal is currently open.
@@ -108,6 +109,12 @@ export default function ProjectModals({
108109 /** A draft-replacing action awaiting confirmation because the draft has unsaved changes. */
109110 const [ pendingReplace , setPendingReplace ] = useState < PendingReplace | undefined > ( undefined ) ;
110111
112+ /**
113+ * Whether the project-creation round-trip is in flight. Used to disable the Create / Cancel
114+ * buttons in the create modal while the backend persists the new project.
115+ */
116+ const [ isCreating , setIsCreating ] = useState ( false ) ;
117+
111118 /**
112119 * Whether the discard-and-replace confirmation flow is in flight (either opening an existing
113120 * project or starting a new draft); disables DiscardDraftConfirm buttons to prevent races.
@@ -200,25 +207,49 @@ export default function ProjectModals({
200207 ) ;
201208
202209 /**
203- * Starts a fresh, empty draft with the given config — the "New" flow. Seeds the draft (carrying
204- * the typed name/description as the Save As prefill), clears the active project so there is no
205- * Save target yet — Save routes to Save As until the draft is saved — and dismisses the modal. No
206- * backend project is created until the user explicitly saves .
210+ * Creates a new project in storage with the given config and an empty analysis, then seeds the
211+ * draft from it so Save targets the newly created project. This is the "New" flow: the project is
212+ * persisted immediately so it shows up in "Select Interlinear Project" right away. On failure the
213+ * backend has already sent an error notification; here we only log and clear the active project .
207214 *
208215 * @param config - The configuration collected by the New dialog.
216+ * @returns A promise that resolves once the project is created (or the failure is handled).
209217 */
210- const startNewDraft = useCallback (
211- ( config : CreateDraftConfig ) => {
218+ const createAndPersistProject = useCallback (
219+ async ( config : CreateDraftConfig ) => {
212220 newDraft ( {
213221 analysisLanguages : config . analysisLanguages ,
214222 ...( config . name !== undefined && { suggestedName : config . name } ) ,
215223 ...( config . description !== undefined && { suggestedDescription : config . description } ) ,
216224 } ) ;
217- resetActiveProject ( ) ;
225+ try {
226+ const createdJson = await papi . commands . sendCommand (
227+ 'interlinearizer.createProject' ,
228+ projectId ,
229+ config . analysisLanguages ,
230+ undefined ,
231+ config . name ,
232+ config . description ,
233+ ) ;
234+ const created : unknown = JSON . parse ( createdJson ) ;
235+ if ( ! isInterlinearProjectSummary ( created ) ) {
236+ await papi . notifications
237+ . send ( { message : '%interlinearizer_error_create_project_failed%' , severity : 'error' } )
238+ . catch ( ( ) => { } ) ;
239+ resetActiveProject ( ) ;
240+ setCreateSourceIsSelect ( false ) ;
241+ setModal ( 'none' ) ;
242+ return ;
243+ }
244+ setActiveProject ( created ) ;
245+ } catch ( e ) {
246+ logger . error ( 'Interlinearizer: failed to create project from New dialog' , e ) ;
247+ resetActiveProject ( ) ;
248+ }
218249 setCreateSourceIsSelect ( false ) ;
219250 setModal ( 'none' ) ;
220251 } ,
221- [ newDraft , resetActiveProject , setModal ] ,
252+ [ newDraft , projectId , resetActiveProject , setActiveProject , setModal ] ,
222253 ) ;
223254
224255 /**
@@ -236,21 +267,26 @@ export default function ProjectModals({
236267 ) ;
237268
238269 /**
239- * Called when the New dialog is submitted. Starts the new draft immediately, or defers behind the
240- * unsaved-changes confirmation when the draft is dirty. Starting a draft is synchronous (no
241- * backend round-trip), so no in-flight re-entry guard is needed .
270+ * Called when the New dialog is submitted. Creates and persists the project immediately, or
271+ * defers behind the unsaved-changes confirmation when the draft is dirty. Disables the modal
272+ * buttons via `isCreating` during the backend round-trip .
242273 *
243274 * @param config - The configuration collected by the New dialog.
244275 */
245276 const handleCreateDraft = useCallback (
246- ( config : CreateDraftConfig ) => {
277+ async ( config : CreateDraftConfig ) => {
247278 if ( dirty ) {
248279 setPendingReplace ( { kind : 'new' , config } ) ;
249280 return ;
250281 }
251- startNewDraft ( config ) ;
282+ setIsCreating ( true ) ;
283+ try {
284+ await createAndPersistProject ( config ) ;
285+ } finally {
286+ setIsCreating ( false ) ;
287+ }
252288 } ,
253- [ dirty , startNewDraft ] ,
289+ [ createAndPersistProject , dirty ] ,
254290 ) ;
255291
256292 /** Confirms the deferred draft-replacing action after the user accepts losing unsaved changes. */
@@ -266,13 +302,13 @@ export default function ProjectModals({
266302 if ( pendingReplace . kind === 'open' ) {
267303 await openProject ( pendingReplace . project ) ;
268304 } else {
269- startNewDraft ( pendingReplace . config ) ;
305+ await createAndPersistProject ( pendingReplace . config ) ;
270306 }
271307 } finally {
272308 setIsReplacing ( false ) ;
273309 setPendingReplace ( undefined ) ;
274310 }
275- } , [ isReplacing , openProject , pendingReplace , startNewDraft ] ) ;
311+ } , [ createAndPersistProject , isReplacing , openProject , pendingReplace ] ) ;
276312
277313 /** Cancels the deferred action, returning to the underlying modal with the draft untouched. */
278314 const handleCancelReplace = useCallback ( ( ) => setPendingReplace ( undefined ) , [ ] ) ;
@@ -426,6 +462,7 @@ export default function ProjectModals({
426462 { modal === 'create' && (
427463 < CreateProjectModal
428464 defaultAnalysisLanguage = { defaultAnalysisLanguage }
465+ isSubmitting = { isCreating }
429466 onClose = { handleCreateClose }
430467 onCreateDraft = { handleCreateDraft }
431468 />
0 commit comments