Skip to content

Commit 16fa21a

Browse files
committed
Extract modal types, utils
1 parent 5aa5e4b commit 16fa21a

10 files changed

Lines changed: 64 additions & 87 deletions

jest.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,14 @@ const config: Config = {
2929
*/
3030
collectCoverage: false,
3131

32-
/** Collect coverage from all source files. Excludes type declarations and test files. */
32+
/** Collect coverage from all source files. Excludes type declarations, test files, and utils. */
3333
collectCoverageFrom: [
3434
'src/**/*.{ts,tsx}',
3535
'!src/**/*.d.ts',
3636
'!src/**/__tests__/**',
3737
'!src/**/*.test.{ts,tsx}',
3838
'!src/**/*.spec.{ts,tsx}',
39+
'!src/utils/**',
3940
],
4041

4142
/** Directory for coverage output. */

src/__tests__/components/CreateProjectModal.test.tsx

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,6 @@ import papi from '@papi/frontend';
88
import { useLocalizedStrings } from '@papi/frontend/react';
99
import { CreateProjectModal } from '../../components/CreateProjectModal';
1010

11-
jest.mock('../../components/SelectInterlinearProjectModal', () => ({
12-
__esModule: true,
13-
/** Minimal re-implementation that avoids importing the real module's coverage into this suite. */
14-
isInterlinearProjectSummary(p: unknown): boolean {
15-
if (!p || typeof p !== 'object') return false;
16-
if (!('id' in p) || typeof p.id !== 'string') return false;
17-
if (!('createdAt' in p) || typeof p.createdAt !== 'string') return false;
18-
if (!('sourceProjectId' in p) || typeof p.sourceProjectId !== 'string') return false;
19-
if (
20-
!('analysisLanguages' in p) ||
21-
!Array.isArray(p.analysisLanguages) ||
22-
!p.analysisLanguages.every((l) => typeof l === 'string')
23-
)
24-
return false;
25-
if ('name' in p && typeof p.name !== 'string') return false;
26-
if ('description' in p && typeof p.description !== 'string') return false;
27-
return true;
28-
},
29-
}));
30-
3111
const testProjectId = 'test-project-id';
3212

3313
describe('CreateProjectModal', () => {

src/__tests__/components/ProjectModals.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import userEvent from '@testing-library/user-event';
77
import { makeWebViewState } from '../test-helpers';
88
import type { ModalState } from '../../components/ProjectModals';
99
import ProjectModals from '../../components/ProjectModals';
10-
import type { InterlinearProjectSummary } from '../../components/SelectInterlinearProjectModal';
10+
import type { InterlinearProjectSummary } from '../../types/interlinear-project-summary';
1111

1212
/** Minimal project summary used in tests. */
1313
const MOCK_PROJECT: InterlinearProjectSummary = {

src/__tests__/components/SelectInterlinearProjectModal.test.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ import { render, screen, waitFor } from '@testing-library/react';
66
import userEvent from '@testing-library/user-event';
77
import papi from '@papi/frontend';
88
import { useLocalizedStrings } from '@papi/frontend/react';
9-
import {
10-
SelectInterlinearProjectModal,
11-
type InterlinearProjectSummary,
12-
} from '../../components/SelectInterlinearProjectModal';
9+
import type { InterlinearProjectSummary } from '../../types/interlinear-project-summary';
10+
import { SelectInterlinearProjectModal } from '../../components/SelectInterlinearProjectModal';
1311

1412
const mockSendCommand = jest.mocked(papi.commands.sendCommand);
1513

src/components/CreateProjectModal.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ import papi, { logger } from '@papi/frontend';
22
import { useLocalizedStrings } from '@papi/frontend/react';
33
import { Button } from 'platform-bible-react';
44
import { useState, useCallback, useRef } from 'react';
5-
import {
6-
type InterlinearProjectSummary,
7-
isInterlinearProjectSummary,
8-
} from './SelectInterlinearProjectModal';
5+
import type { InterlinearProjectSummary } from '../types/interlinear-project-summary';
6+
import { isInterlinearProjectSummary } from '../utils/interlinear-project-summary';
97

108
/** Localized string keys used by {@link CreateProjectModal}. */
119
const CREATE_PROJECT_MODAL_STRING_KEYS: `%${string}%`[] = [

src/components/InterlinearizerLoader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import ContinuousScrollToggle from './ContinuousScrollToggle';
1111
import Interlinearizer from './Interlinearizer';
1212
import ProjectModals, { type ModalState } from './ProjectModals';
1313
import ScriptureNavControls from './ScriptureNavControls';
14-
import type { ActiveProjectState } from './SelectInterlinearProjectModal';
14+
import type { ActiveProjectState } from '../types/interlinear-project-summary';
1515

1616
const STRING_KEYS: `%${string}%`[] = ['%interlinearizer_continuousScrollToggle%'];
1717

src/components/ProjectModals.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@ import type { UseWebViewStateHook } from '@papi/core';
22
import { useCallback, useState } from 'react';
33
import { CreateProjectModal } from './CreateProjectModal';
44
import { ProjectMetadataModal } from './ProjectMetadataModal';
5-
import {
6-
type ActiveProjectState,
7-
type InterlinearProjectSummary,
8-
SelectInterlinearProjectModal,
9-
} from './SelectInterlinearProjectModal';
5+
import type { ActiveProjectState, InterlinearProjectSummary } from '../types/interlinear-project-summary';
6+
import { SelectInterlinearProjectModal } from './SelectInterlinearProjectModal';
107

118
/** Which modal is currently visible. Only one can be open at a time. */
129
export type ModalState = 'none' | 'select' | 'create' | 'metadata';

src/components/SelectInterlinearProjectModal.tsx

Lines changed: 2 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { useLocalizedStrings } from '@papi/frontend/react';
33
import { Info } from 'lucide-react';
44
import { Button } from 'platform-bible-react';
55
import { useCallback, useEffect, useRef, useState } from 'react';
6-
import type { InterlinearProject } from 'interlinearizer';
6+
import type { InterlinearProjectSummary } from '../types/interlinear-project-summary';
7+
import { isInterlinearProjectSummary } from '../utils/interlinear-project-summary';
78

89
/** Localized string keys used by {@link SelectInterlinearProjectModal}. */
910
const SELECT_INTERLINEAR_PROJECT_STRING_KEYS: `%${string}%`[] = [
@@ -15,56 +16,6 @@ const SELECT_INTERLINEAR_PROJECT_STRING_KEYS: `%${string}%`[] = [
1516
'%interlinearizer_modal_select_info_button_label%',
1617
];
1718

18-
/** The subset of InterlinearProject fields this modal displays and returns. */
19-
export type InterlinearProjectSummary = Pick<
20-
InterlinearProject,
21-
| 'id'
22-
| 'createdAt'
23-
| 'sourceProjectId'
24-
| 'targetProjectId'
25-
| 'analysisLanguages'
26-
| 'name'
27-
| 'description'
28-
>;
29-
30-
/** Fields of the active interlinear project persisted in WebView state. */
31-
export type ActiveProjectState = Pick<
32-
InterlinearProjectSummary,
33-
| 'id'
34-
| 'createdAt'
35-
| 'name'
36-
| 'description'
37-
| 'sourceProjectId'
38-
| 'targetProjectId'
39-
| 'analysisLanguages'
40-
>;
41-
42-
/**
43-
* Type guard for {@link InterlinearProjectSummary} parsed from unknown JSON.
44-
*
45-
* @param p - The value to test, typically a parsed JSON object of unknown shape.
46-
* @returns `true` if `p` satisfies the {@link InterlinearProjectSummary} shape, narrowing its type
47-
* accordingly.
48-
*/
49-
export function isInterlinearProjectSummary(p: unknown): p is InterlinearProjectSummary {
50-
return (
51-
!!p &&
52-
typeof p === 'object' &&
53-
'id' in p &&
54-
typeof p.id === 'string' &&
55-
'createdAt' in p &&
56-
typeof p.createdAt === 'string' &&
57-
'sourceProjectId' in p &&
58-
typeof p.sourceProjectId === 'string' &&
59-
'analysisLanguages' in p &&
60-
Array.isArray(p.analysisLanguages) &&
61-
p.analysisLanguages.every((l) => typeof l === 'string') &&
62-
(!('name' in p) || typeof p.name === 'string') &&
63-
(!('description' in p) || typeof p.description === 'string') &&
64-
(!('targetProjectId' in p) || typeof p.targetProjectId === 'string')
65-
);
66-
}
67-
6819
/**
6920
* Modal that lists all existing interlinearizer projects for a source project and lets the user
7021
* select one, view its details (via the info icon), or request that a new one be created. Fires
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { InterlinearProject } from 'interlinearizer';
2+
3+
/** The subset of InterlinearProject fields this modal displays and returns. */
4+
export type InterlinearProjectSummary = Pick<
5+
InterlinearProject,
6+
| 'id'
7+
| 'createdAt'
8+
| 'sourceProjectId'
9+
| 'targetProjectId'
10+
| 'analysisLanguages'
11+
| 'name'
12+
| 'description'
13+
>;
14+
15+
/** Fields of the active interlinear project persisted in WebView state. */
16+
export type ActiveProjectState = Pick<
17+
InterlinearProjectSummary,
18+
| 'id'
19+
| 'createdAt'
20+
| 'name'
21+
| 'description'
22+
| 'sourceProjectId'
23+
| 'targetProjectId'
24+
| 'analysisLanguages'
25+
>;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { InterlinearProjectSummary } from '../types/interlinear-project-summary';
2+
3+
/**
4+
* Type guard for {@link InterlinearProjectSummary} parsed from unknown JSON.
5+
*
6+
* @param p - The value to test, typically a parsed JSON object of unknown shape.
7+
* @returns `true` if `p` satisfies the {@link InterlinearProjectSummary} shape, narrowing its type
8+
* accordingly.
9+
*/
10+
export function isInterlinearProjectSummary(p: unknown): p is InterlinearProjectSummary {
11+
return (
12+
!!p &&
13+
typeof p === 'object' &&
14+
'id' in p &&
15+
typeof p.id === 'string' &&
16+
'createdAt' in p &&
17+
typeof p.createdAt === 'string' &&
18+
'sourceProjectId' in p &&
19+
typeof p.sourceProjectId === 'string' &&
20+
'analysisLanguages' in p &&
21+
Array.isArray(p.analysisLanguages) &&
22+
p.analysisLanguages.every((l) => typeof l === 'string') &&
23+
(!('name' in p) || typeof p.name === 'string') &&
24+
(!('description' in p) || typeof p.description === 'string') &&
25+
(!('targetProjectId' in p) || typeof p.targetProjectId === 'string')
26+
);
27+
}

0 commit comments

Comments
 (0)