Skip to content

Commit 47c6a1a

Browse files
Add auto-saving draft with Save / Save As and Wipe
Editing the interlinearizer no longer writes to the active project on every keystroke. Each source project now has an always-present draft (draft:{sourceProjectId}) that auto-saves every edit to papi storage, decoupled from the user's saved projects and never shown in the picker, so work is never lost. Persistence is now explicit: - Save writes the draft to the active project - Save As writes the draft to a new project, or overwrites an existing one - New starts an empty draft (a project is created only on Save As) - Open loads a project into the draft as a working copy - Wipe clears the draft — the current book or the whole thing Switching projects (New / Open) while the draft is dirty prompts to discard. The tab title shows a "●" marker while the draft has unsaved changes, toggled via updateWebViewDefinition (Platform.Bible exposes no native unsaved-tab indicator). Implementation: - DraftProject type + isDraftProject guard; getDraft/saveDraft storage with a per-source serialization queue; getDraft/saveDraft backend commands. - useDraftProject hook owns the draft (autosave, dirty tracking, and a draftVersion that remounts the editor on New/Open/Wipe). - New SaveAsProjectModal, WipeConfirm, and DiscardDraftConfirm; the New modal is repurposed to configure a draft rather than create a project. - removeBookFromAnalysis util backs the per-book wipe. Alignment links are intentionally not carried in the draft yet (no link-editing feature exists); Save preserves a target project's existing links. Deferred UX decisions are recorded in user-questions.md. 988 tests, 100% coverage, lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8a6f807 commit 47c6a1a

29 files changed

Lines changed: 3960 additions & 1223 deletions

__mocks__/platform-bible-react.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,42 @@ export const MOCK_VIEW_PROJECT_INFO_MENU_ITEM: MenuItemContainingCommand = {
6565
localizeNotes: '',
6666
};
6767

68+
/** Sentinel menu item passed by the mock toolbar when the save button is clicked. */
69+
export const MOCK_SAVE_MENU_ITEM: MenuItemContainingCommand = {
70+
label: '%interlinearizer_save%',
71+
command: 'interlinearizer.save',
72+
group: 'interlinearizer.file.actions',
73+
order: 1,
74+
localizeNotes: '',
75+
};
76+
77+
/** Sentinel menu item passed by the mock toolbar when the save-as button is clicked. */
78+
export const MOCK_SAVE_AS_MENU_ITEM: MenuItemContainingCommand = {
79+
label: '%interlinearizer_saveAs%',
80+
command: 'interlinearizer.openSaveAsModal',
81+
group: 'interlinearizer.file.actions',
82+
order: 2,
83+
localizeNotes: '',
84+
};
85+
86+
/** Sentinel menu item passed by the mock toolbar when the wipe-book button is clicked. */
87+
export const MOCK_WIPE_BOOK_MENU_ITEM: MenuItemContainingCommand = {
88+
label: '%interlinearizer_wipeBook%',
89+
command: 'interlinearizer.wipeBook',
90+
group: 'interlinearizer.draft.actions',
91+
order: 1,
92+
localizeNotes: '',
93+
};
94+
95+
/** Sentinel menu item passed by the mock toolbar when the wipe-draft button is clicked. */
96+
export const MOCK_WIPE_DRAFT_MENU_ITEM: MenuItemContainingCommand = {
97+
label: '%interlinearizer_wipeDraft%',
98+
command: 'interlinearizer.wipeDraft',
99+
group: 'interlinearizer.draft.actions',
100+
order: 2,
101+
localizeNotes: '',
102+
};
103+
68104

69105
/**
70106
* Stub toolbar that renders project-menu and view-info buttons using sentinel menu items so tests
@@ -127,6 +163,42 @@ export function TabToolbar({
127163
View project info
128164
</button>
129165
)}
166+
{onSelectProjectMenuItem && (
167+
<button
168+
type="button"
169+
data-testid="tab-toolbar-save"
170+
onClick={() => onSelectProjectMenuItem(MOCK_SAVE_MENU_ITEM)}
171+
>
172+
Save
173+
</button>
174+
)}
175+
{onSelectProjectMenuItem && (
176+
<button
177+
type="button"
178+
data-testid="tab-toolbar-save-as"
179+
onClick={() => onSelectProjectMenuItem(MOCK_SAVE_AS_MENU_ITEM)}
180+
>
181+
Save as
182+
</button>
183+
)}
184+
{onSelectProjectMenuItem && (
185+
<button
186+
type="button"
187+
data-testid="tab-toolbar-wipe-book"
188+
onClick={() => onSelectProjectMenuItem(MOCK_WIPE_BOOK_MENU_ITEM)}
189+
>
190+
Wipe book
191+
</button>
192+
)}
193+
{onSelectProjectMenuItem && (
194+
<button
195+
type="button"
196+
data-testid="tab-toolbar-wipe-draft"
197+
onClick={() => onSelectProjectMenuItem(MOCK_WIPE_DRAFT_MENU_ITEM)}
198+
>
199+
Wipe draft
200+
</button>
201+
)}
130202
{onSelectViewInfoMenuItem && (
131203
<button
132204
type="button"

contributions/localizedStrings.json

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
"%interlinearizer_openSelectProjectModal%": "Select Interlinear Project…",
1111
"%interlinearizer_openNewProjectModal%": "New Interlinear Project…",
1212
"%interlinearizer_openProjectInfoModal%": "View Project Info…",
13+
"%interlinearizer_save%": "Save",
14+
"%interlinearizer_saveAs%": "Save As…",
15+
"%interlinearizer_wipeBook%": "Wipe Current Book…",
16+
"%interlinearizer_wipeDraft%": "Wipe Draft…",
1317

1418
"%interlinearizer_projectSettings_title%": "Interlinearizer",
1519
"%interlinearizer_projectSettings_continuousScroll%": "Continuous Scroll",
@@ -71,11 +75,35 @@
7175
"%interlinearizer_modal_select_create_new%": "Create New",
7276
"%interlinearizer_modal_select_cancel%": "Cancel",
7377

78+
"%interlinearizer_confirm_discard_title%": "Discard unsaved changes?",
79+
"%interlinearizer_confirm_discard_body%": "Your draft has changes that haven't been saved to a project. Continuing will discard them.",
80+
"%interlinearizer_confirm_discard_ok%": "Discard",
81+
"%interlinearizer_confirm_discard_cancel%": "Cancel",
82+
83+
"%interlinearizer_modal_saveAs_title%": "Save As",
84+
"%interlinearizer_modal_saveAs_new_section%": "Save as a new project",
85+
"%interlinearizer_modal_saveAs_save_new%": "Save as New Project",
86+
"%interlinearizer_modal_saveAs_existing_section%": "Or overwrite an existing project",
87+
"%interlinearizer_modal_saveAs_none%": "No existing projects for this source yet.",
88+
"%interlinearizer_modal_saveAs_overwrite%": "Overwrite",
89+
"%interlinearizer_modal_saveAs_overwrite_confirm_body%": "Overwrite this project? Its saved analysis will be replaced with the current draft.",
90+
"%interlinearizer_modal_saveAs_overwrite_confirm_ok%": "Overwrite",
91+
"%interlinearizer_modal_saveAs_overwrite_confirm_cancel%": "Cancel",
92+
"%interlinearizer_modal_saveAs_cancel%": "Cancel",
93+
94+
"%interlinearizer_confirm_wipe_book_title%": "Wipe current book?",
95+
"%interlinearizer_confirm_wipe_book_body%": "This removes all analysis for the current book from the draft. Save afterward to persist the change to a project.",
96+
"%interlinearizer_confirm_wipe_draft_title%": "Wipe entire draft?",
97+
"%interlinearizer_confirm_wipe_draft_body%": "This removes all analysis from the draft. Save afterward to persist the change to a project.",
98+
"%interlinearizer_confirm_wipe_ok%": "Wipe",
99+
"%interlinearizer_confirm_wipe_cancel%": "Cancel",
100+
74101
"%interlinearizer_error_create_project_failed%": "Could not create the interlinearizer project. Please try again.",
75102
"%interlinearizer_error_save_metadata_failed%": "Could not save project info. Please try again.",
76103
"%interlinearizer_error_delete_project_failed%": "Could not delete the interlinearizer project. Please try again.",
77104
"%interlinearizer_error_update_project_failed%": "Could not update the interlinearizer project. Please try again.",
78-
"%interlinearizer_error_load_projects_failed%": "Could not load interlinear projects. Please try again."
105+
"%interlinearizer_error_load_projects_failed%": "Could not load interlinear projects. Please try again.",
106+
"%interlinearizer_error_save_draft_failed%": "Could not save your working draft. Please try again."
79107
}
80108
}
81109
}

contributions/menus.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@
2727
"interlinearizer.projectActions": {
2828
"column": "interlinearizer.project",
2929
"order": 1
30+
},
31+
"interlinearizer.fileActions": {
32+
"column": "interlinearizer.project",
33+
"order": 2
34+
},
35+
"interlinearizer.draftActions": {
36+
"column": "interlinearizer.project",
37+
"order": 3
3038
}
3139
},
3240
"items": [
@@ -50,6 +58,34 @@
5058
"group": "interlinearizer.projectActions",
5159
"order": 3,
5260
"command": "interlinearizer.openProjectInfoModal"
61+
},
62+
{
63+
"label": "%interlinearizer_save%",
64+
"localizeNotes": "Interlinearizer top menu > Save the current draft to the active project",
65+
"group": "interlinearizer.fileActions",
66+
"order": 1,
67+
"command": "interlinearizer.save"
68+
},
69+
{
70+
"label": "%interlinearizer_saveAs%",
71+
"localizeNotes": "Interlinearizer top menu > Save the draft to a new project or overwrite an existing one",
72+
"group": "interlinearizer.fileActions",
73+
"order": 2,
74+
"command": "interlinearizer.openSaveAsModal"
75+
},
76+
{
77+
"label": "%interlinearizer_wipeBook%",
78+
"localizeNotes": "Interlinearizer top menu > Remove the current book's analysis from the draft",
79+
"group": "interlinearizer.draftActions",
80+
"order": 1,
81+
"command": "interlinearizer.wipeBook"
82+
},
83+
{
84+
"label": "%interlinearizer_wipeDraft%",
85+
"localizeNotes": "Interlinearizer top menu > Remove all analysis from the draft",
86+
"group": "interlinearizer.draftActions",
87+
"order": 2,
88+
"command": "interlinearizer.wipeDraft"
5389
}
5490
]
5591
}

0 commit comments

Comments
 (0)