Skip to content

Commit 8259994

Browse files
Fix analysis language default, button state, and storage error propagation
- Default analysis language to "und" (undetermined) instead of "en" - Normalize whitespace-only language input to "und" on submit - Disable the create button while submission is in progress - Rethrow storage errors in getProjectsForSource so callers can distinguish an outage from an empty list
1 parent e1a4e21 commit 8259994

4 files changed

Lines changed: 54 additions & 11 deletions

File tree

src/__tests__/components/CreateProjectModal.test.tsx

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ describe('CreateProjectModal', () => {
5555
expect(papi.commands.sendCommand).toHaveBeenCalledWith(
5656
'interlinearizer.createProject',
5757
testProjectId,
58-
'en',
58+
'und',
5959
undefined,
6060
undefined,
6161
),
@@ -73,7 +73,7 @@ describe('CreateProjectModal', () => {
7373
expect(papi.commands.sendCommand).toHaveBeenCalledWith(
7474
'interlinearizer.createProject',
7575
testProjectId,
76-
'en',
76+
'und',
7777
'My Project',
7878
'My Desc',
7979
),
@@ -164,6 +164,44 @@ describe('CreateProjectModal', () => {
164164
expect(onClose).not.toHaveBeenCalled();
165165
});
166166

167+
it('defaults analysis language to "und" when the language input contains only whitespace', async () => {
168+
render(<CreateProjectModal projectId={testProjectId} onClose={() => {}} />);
169+
170+
const languageInput = screen.getByLabelText(/analysis language/i);
171+
await userEvent.clear(languageInput);
172+
await userEvent.type(languageInput, ' ');
173+
await userEvent.click(screen.getByRole('button', { name: /^create$/i }));
174+
175+
await waitFor(() =>
176+
expect(papi.commands.sendCommand).toHaveBeenCalledWith(
177+
'interlinearizer.createProject',
178+
testProjectId,
179+
'und',
180+
undefined,
181+
undefined,
182+
),
183+
);
184+
});
185+
186+
it('disables the create button while a submission is in progress', async () => {
187+
let resolveCommand: (value: undefined) => void = () => {};
188+
jest.mocked(papi.commands.sendCommand).mockImplementation(
189+
() =>
190+
new Promise((resolve) => {
191+
resolveCommand = resolve;
192+
}),
193+
);
194+
render(<CreateProjectModal projectId={testProjectId} onClose={() => {}} />);
195+
196+
const createButton = screen.getByRole('button', { name: /^create$/i });
197+
await userEvent.click(createButton);
198+
199+
expect(createButton).toBeDisabled();
200+
resolveCommand(undefined);
201+
202+
await waitFor(() => expect(createButton).not.toBeDisabled());
203+
});
204+
167205
it('does not call onClose or onProjectCreated when sendCommand rejects, but sends an error notification', async () => {
168206
jest.mocked(papi.commands.sendCommand).mockRejectedValue(new Error('network error'));
169207
const onProjectCreated = jest.fn();

src/__tests__/main.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -761,13 +761,11 @@ describe('main', () => {
761761
expect(result).toBe(JSON.stringify([stubProject]));
762762
});
763763

764-
it('returns "[]" and logs an error when storage throws', async () => {
764+
it('throws and logs an error when storage throws', async () => {
765765
mockGetProjectsForSource.mockRejectedValue(new Error('disk full'));
766766
const handler = await getProjectsForSourceHandler();
767767

768-
const result = await handler('src-project');
769-
770-
expect(result).toBe('[]');
768+
await expect(handler('src-project')).rejects.toThrow('disk full');
771769
expect(__mockLogger.error).toHaveBeenCalledWith(
772770
'Interlinearizer: failed to list projects for source',
773771
expect.any(Error),

src/components/CreateProjectModal.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ export function CreateProjectModal({
4848

4949
const [name, setName] = useState('');
5050
const [description, setDescription] = useState('');
51-
const [analysisLanguage, setAnalysisLanguage] = useState('en');
51+
const [analysisLanguage, setAnalysisLanguage] = useState('und');
52+
const [isSubmitting, setIsSubmitting] = useState(false);
5253

5354
/**
5455
* Sends the `interlinearizer.createProject` command with the collected form values, notifies the
@@ -58,11 +59,13 @@ export function CreateProjectModal({
5859
* @returns A promise that resolves when the command completes or the error notification is sent.
5960
*/
6061
const handleSubmit = useCallback(async () => {
62+
setIsSubmitting(true);
63+
const normalizedAnalysisLanguage = analysisLanguage.trim() || 'und';
6164
try {
6265
const projectJson = await papi.commands.sendCommand(
6366
'interlinearizer.createProject',
6467
projectId,
65-
analysisLanguage,
68+
normalizedAnalysisLanguage,
6669
name || undefined,
6770
description || undefined,
6871
);
@@ -75,6 +78,8 @@ export function CreateProjectModal({
7578
await papi.notifications
7679
.send({ message: '%interlinearizer_error_create_project_failed%', severity: 'error' })
7780
.catch(() => {});
81+
} finally {
82+
setIsSubmitting(false);
7883
}
7984
}, [projectId, analysisLanguage, name, description, onClose, onProjectCreated]);
8085

@@ -125,7 +130,7 @@ export function CreateProjectModal({
125130
<Button variant="secondary" onClick={onClose}>
126131
{localizedStrings['%interlinearizer_modal_create_cancel%']}
127132
</Button>
128-
<Button onClick={handleSubmit}>
133+
<Button onClick={handleSubmit} disabled={isSubmitting}>
129134
{localizedStrings['%interlinearizer_modal_create_submit%']}
130135
</Button>
131136
</div>

src/main.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,15 +212,17 @@ async function updateProjectMetadata(
212212
* "select existing" when the user opens the project menu.
213213
*
214214
* @param sourceProjectId - Platform.Bible project ID of the source text to query.
215-
* @returns A JSON string of `InterlinearProject[]`, or `"[]"` if none exist or storage fails.
215+
* @returns A JSON string of `InterlinearProject[]`, or `"[]"` if none exist.
216+
* @throws The storage error when the underlying read fails, so callers can distinguish an outage
217+
* from an empty list.
216218
*/
217219
async function getProjectsForSource(sourceProjectId: string): Promise<string> {
218220
try {
219221
const projects = await projectStorage.getProjectsForSource(executionToken, sourceProjectId);
220222
return JSON.stringify(projects);
221223
} catch (e) {
222224
logger.error('Interlinearizer: failed to list projects for source', e);
223-
return '[]';
225+
throw e;
224226
}
225227
}
226228

0 commit comments

Comments
 (0)