Skip to content

Commit 8b4bac0

Browse files
Disable buttons while submitting/loading, update docs, make implicit zero-check in ContinuousView explicit
1 parent a45537f commit 8b4bac0

6 files changed

Lines changed: 39 additions & 15 deletions

File tree

src/components/ContinuousView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ export default function ContinuousView({
247247
// One ref slot per phrase so we can call scrollIntoView on the focused one.
248248
const phraseRefs = useRef<(HTMLSpanElement | null)[]>([]);
249249

250-
const atStart = !phraseEntries.length || !focusPhraseIndex;
250+
const atStart = phraseEntries.length === 0 || focusPhraseIndex === 0;
251251
const atEnd = !phraseEntries.length || focusPhraseIndex >= phraseEntries.length - 1;
252252
const stripOpacityClass = isVisible ? 'tw:opacity-100' : 'tw:opacity-0';
253253

src/components/ProjectMetadataModal.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,12 @@ export function ProjectMetadataModal({
244244
{localizedStrings['%interlinearizer_modal_metadata_delete_confirm_body%']}
245245
</p>
246246
<div className="tw-flex tw-gap-2 tw-justify-end">
247-
<Button variant="secondary" size="sm" onClick={() => setConfirmingDelete(false)}>
247+
<Button
248+
variant="secondary"
249+
size="sm"
250+
onClick={() => setConfirmingDelete(false)}
251+
disabled={isSubmitting}
252+
>
248253
{localizedStrings['%interlinearizer_modal_metadata_delete_confirm_cancel%']}
249254
</Button>
250255
<Button

src/components/SelectInterlinearProjectModal.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export function SelectInterlinearProjectModal({
8383
);
8484

8585
const [projects, setProjects] = useState<InterlinearProjectSummary[]>([]);
86+
const [isLoading, setIsLoading] = useState(true);
8687

8788
/**
8889
* Fetches interlinear projects for `sourceProjectId` and updates the `projects` state. Logs and
@@ -92,6 +93,7 @@ export function SelectInterlinearProjectModal({
9293
* sent.
9394
*/
9495
const loadProjects = useCallback(async () => {
96+
setIsLoading(true);
9597
try {
9698
const json = await papi.commands.sendCommand(
9799
'interlinearizer.getProjectsForSource',
@@ -114,6 +116,8 @@ export function SelectInterlinearProjectModal({
114116
await papi.notifications
115117
.send({ message: '%interlinearizer_error_load_projects_failed%', severity: 'error' })
116118
.catch(() => {});
119+
} finally {
120+
setIsLoading(false);
117121
}
118122
}, [sourceProjectId]);
119123

@@ -173,10 +177,10 @@ export function SelectInterlinearProjectModal({
173177
)}
174178

175179
<div className="tw-flex tw-gap-2 tw-justify-end">
176-
<Button variant="secondary" onClick={onClose}>
180+
<Button variant="secondary" onClick={onClose} disabled={isLoading}>
177181
{localizedStrings['%interlinearizer_modal_select_cancel%']}
178182
</Button>
179-
<Button onClick={onCreateNew}>
183+
<Button onClick={onCreateNew} disabled={isLoading}>
180184
{localizedStrings['%interlinearizer_modal_select_create_new%']}
181185
</Button>
182186
</div>

src/main.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ const openWebViewsByProject = new Map<string, string>();
7373
*
7474
* @param projectId - Project to open; if omitted a picker dialog is shown.
7575
* @returns The WebView ID of the opened (or focused) tab, or `undefined` if the user cancels.
76-
* @throws If `papi.dialogs.selectProject` or `papi.webViews.openWebView` rejects.
76+
* @throws If `papi.dialogs.selectProject` rejects (e.g. platform error while showing the dialog).
77+
* @throws If `papi.webViews.openWebView` rejects (e.g. the platform cannot open or focus the tab).
7778
*/
7879
async function openInterlinearizer(projectId?: string): Promise<string | undefined> {
7980
const resolvedProjectId =
@@ -98,7 +99,8 @@ async function openInterlinearizer(projectId?: string): Promise<string | undefin
9899
*
99100
* @param webViewId - ID of an open WebView whose project to use; if omitted falls back to a picker.
100101
* @returns The WebView ID of the opened (or focused) tab, or `undefined` if the user cancels.
101-
* @throws If `papi.webViews.getOpenWebViewDefinition` or `openInterlinearizer` rejects.
102+
* @throws If `papi.webViews.getOpenWebViewDefinition` rejects.
103+
* @throws Any error thrown by {@link openInterlinearizer} (dialog or WebView platform errors).
102104
*/
103105
async function openInterlinearizerForWebView(webViewId?: string): Promise<string | undefined> {
104106
if (!webViewId) return openInterlinearizer();
@@ -154,7 +156,11 @@ async function createInterlinearProject(
154156
*
155157
* @param interlinearProjectId - UUID of the interlinearizer project to delete.
156158
* @returns A promise that resolves when the deletion (or no-op) is complete.
157-
* @throws Re-throws storage errors after logging/notifying so the caller can handle failure UX.
159+
* @throws {SyntaxError} If the project-IDs index contains invalid JSON (propagated from
160+
* {@link projectStorage.deleteProject} via {@link readIds}).
161+
* @throws If `papi.storage.deleteUserData` rejects for a non-ENOENT reason, or if
162+
* `papi.storage.writeUserData` rejects when updating the index. All storage errors are logged and
163+
* shown as a notification before being re-thrown so the caller can handle failure UX.
158164
*/
159165
async function deleteInterlinearProject(interlinearProjectId: string): Promise<void> {
160166
try {
@@ -219,8 +225,10 @@ async function updateProjectMetadata(
219225
*
220226
* @param sourceProjectId - Platform.Bible project ID of the source text to query.
221227
* @returns A JSON string of `InterlinearProject[]`, or `"[]"` if none exist.
222-
* @throws The storage error when the underlying read fails, so callers can distinguish an outage
223-
* from an empty list.
228+
* @throws {SyntaxError} If the project-IDs index or any project record contains invalid JSON.
229+
* @throws If `papi.storage.readUserData` rejects for a non-ENOENT reason (propagated from
230+
* {@link projectStorage.getProjectsForSource}). Callers can use this to distinguish a storage
231+
* outage from a legitimately empty list.
224232
*/
225233
async function getProjectsForSource(sourceProjectId: string): Promise<string> {
226234
try {
@@ -347,7 +355,8 @@ export async function activate(context: ExecutionActivationContext): Promise<voi
347355
updateProjectMetadata,
348356
{
349357
method: {
350-
summary: 'Update the name and description of an existing interlinearizer project',
358+
summary:
359+
'Update the name, description, and analysis language of an existing interlinearizer project',
351360
params: [
352361
{
353362
name: 'interlinearProjectId',

src/parsers/pt9/interlinearXmlParser.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,12 @@ function extractLexemesFromCluster(clusterElement: ParsedCluster): LexemeData[]
160160
* Parses a string to a non-negative integer, returning `undefined` for empty or non-integer values.
161161
* Used to validate XML attribute strings before converting them to numeric ranges.
162162
*
163-
* @param raw - The string to parse (e.g. an XML attribute value).
164-
* @returns The parsed non-negative integer, or `undefined` if the input is empty or non-integer.
163+
* @param raw - The string to parse (e.g. an XML attribute value). `undefined` is accepted because
164+
* fast-xml-parser may omit attributes at runtime despite the declared type.
165+
* @returns The parsed non-negative integer, or `undefined` if the input is absent, empty, or
166+
* non-integer.
165167
*/
166-
const parseStrictNumber = (raw: string): number | undefined => {
168+
const parseStrictNumber = (raw: string | undefined): number | undefined => {
167169
if (raw === undefined || raw.trim() === '') return undefined;
168170
if (!/^\d+$/.test(raw.trim())) return undefined;
169171
return Number.parseInt(raw, 10);

src/services/projectStorage.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,12 @@ export async function updateProjectMetadata(
256256
*
257257
* @param token - The execution token for storage access.
258258
* @param id - The project UUID to delete.
259-
* @throws {SyntaxError} If the `projectIds` storage value contains invalid JSON.
260-
* @throws If `papi.storage.writeUserData` rejects when updating the index.
259+
* @throws {SyntaxError} If the `projectIds` storage value contains invalid JSON (from
260+
* {@link readIds}).
261+
* @throws If `papi.storage.deleteUserData` throws for a reason other than ENOENT (non-ENOENT errors
262+
* are rethrown).
263+
* @throws If `papi.storage.writeUserData` rejects when updating `PROJECT_IDS_KEY`.
264+
* @throws Any error propagated from {@link readIds}, `enqueueProjectOp`, or `enqueueIndexOp`.
261265
*/
262266
export async function deleteProject(token: ExecutionToken, id: string): Promise<void> {
263267
await enqueueProjectOp(id, async () => {

0 commit comments

Comments
 (0)