Skip to content

Commit 1ba82e0

Browse files
committed
Fix broken new-interlinear-project
1 parent 31ee21d commit 1ba82e0

1 file changed

Lines changed: 55 additions & 18 deletions

File tree

src/components/modals/ProjectModals.tsx

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)