-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSelectInterlinearProjectModal.tsx
More file actions
160 lines (151 loc) · 6.41 KB
/
SelectInterlinearProjectModal.tsx
File metadata and controls
160 lines (151 loc) · 6.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import papi, { logger } from '@papi/frontend';
import { useLocalizedStrings } from '@papi/frontend/react';
import { Info } from 'lucide-react';
import { Button } from 'platform-bible-react';
import { useCallback, useEffect, useRef, useState } from 'react';
import type { InterlinearProjectSummary } from '../types/interlinear-project-summary';
import { isInterlinearProjectSummary } from '../types/typeGuards';
/** Localized string keys used by {@link SelectInterlinearProjectModal}. */
const SELECT_INTERLINEAR_PROJECT_STRING_KEYS: `%${string}%`[] = [
'%interlinearizer_modal_select_title%',
'%interlinearizer_modal_select_none%',
'%interlinearizer_modal_select_create_new%',
'%interlinearizer_modal_select_cancel%',
'%interlinearizer_modal_select_name_unnamed%',
'%interlinearizer_modal_select_info_button_label%',
];
/**
* Modal that lists all existing interlinearizer projects for a source project and lets the user
* select one, view its details (via the info icon), or request that a new one be created. Fires
* `interlinearizer.getProjectsForSource` to load the list on mount.
*
* @param props - Component props.
* @param props.sourceProjectId - Platform.Bible project ID whose interlinear projects to list.
* @param props.onSelect - Called with the chosen project when the user picks an existing one.
* @param props.onCreateNew - Called when the user chooses to create a new project instead.
* @param props.onClose - Called when the user cancels without selecting.
* @param props.onViewInfo - Called with a project when the user clicks its info icon, so the caller
* can open the project metadata modal for that project.
* @returns The modal overlay with the project list, or nothing while strings are loading.
*/
export function SelectInterlinearProjectModal({
sourceProjectId,
onSelect,
onCreateNew,
onClose,
onViewInfo,
}: Readonly<{
sourceProjectId: string;
onSelect: (project: InterlinearProjectSummary) => void;
onCreateNew: () => void;
onClose: () => void;
onViewInfo: (project: InterlinearProjectSummary) => void;
}>) {
const [localizedStrings, stringsLoading] = useLocalizedStrings(
SELECT_INTERLINEAR_PROJECT_STRING_KEYS,
);
const [projects, setProjects] = useState<InterlinearProjectSummary[]>([]);
const [isLoading, setIsLoading] = useState(true);
/** Incremented each time a load starts; lets an in-flight response detect it has been superseded. */
const loadGenRef = useRef(0);
/**
* Fetches interlinear projects for `sourceProjectId` and updates the `projects` state. Logs and
* shows a notification on failure. Ignores the response if a newer load has started since this
* one was initiated.
*
* @returns A promise that resolves when the project list is loaded or the error notification is
* sent.
*/
const loadProjects = useCallback(async () => {
loadGenRef.current += 1;
const gen = loadGenRef.current;
setIsLoading(true);
setProjects([]);
try {
const json = await papi.commands.sendCommand(
'interlinearizer.getProjectsForSource',
sourceProjectId,
);
if (gen !== loadGenRef.current) return;
const parsed: unknown = JSON.parse(json);
if (!Array.isArray(parsed)) {
logger.warn('Interlinearizer: getProjectsForSource returned non-array', parsed);
return;
}
const valid = parsed.filter(isInterlinearProjectSummary);
if (valid.length !== parsed.length)
logger.warn(
'Interlinearizer: skipped malformed project entries',
parsed.length - valid.length,
);
setProjects(valid);
} catch (e) {
logger.error('Interlinearizer: failed to load projects for source', e);
await papi.notifications
.send({ message: '%interlinearizer_error_load_projects_failed%', severity: 'error' })
.catch(() => {});
} finally {
if (gen === loadGenRef.current) setIsLoading(false);
}
}, [sourceProjectId]);
useEffect(() => {
loadProjects();
}, [loadProjects]);
/* v8 ignore next */ if (stringsLoading) return undefined;
return (
<div className="tw:modal-overlay">
<dialog
aria-labelledby="select-project-modal-title"
aria-modal="true"
className="tw:modal-dialog tw:rounded-lg tw:w-lg"
open
>
<h2 id="select-project-modal-title" className="tw:modal-title">
{localizedStrings['%interlinearizer_modal_select_title%']}
</h2>
{projects.length === 0 ? (
<p className="tw:text-sm tw:text-muted-foreground tw:mb-4">
{localizedStrings['%interlinearizer_modal_select_none%']}
</p>
) : (
<ul className="tw:flex tw:flex-col tw:gap-1 tw:mb-4 tw:max-h-96 tw:overflow-y-auto">
{projects.map((project) => (
<li key={project.id} className="tw:flex tw:items-center tw:gap-1">
<button
type="button"
className="tw:flex-1 tw:flex tw:items-center tw:gap-2 tw:rounded tw:border tw:border-border tw:bg-muted/40 tw:px-3 tw:py-2 tw:text-left tw:text-sm tw:hover:bg-muted/70 tw:transition-colors tw:min-w-0"
onClick={() => onSelect(project)}
>
<span className="tw:font-medium tw:text-foreground tw:truncate">
{project.name ??
localizedStrings['%interlinearizer_modal_select_name_unnamed%']}
</span>
<span className="tw:font-mono tw:text-xs tw:text-muted-foreground tw:shrink-0">
{project.analysisLanguages.join(', ')}
</span>
</button>
<Button
variant="ghost"
size="icon"
aria-label={localizedStrings['%interlinearizer_modal_select_info_button_label%']}
className="tw:shrink-0"
onClick={() => onViewInfo(project)}
>
<Info size={15} />
</Button>
</li>
))}
</ul>
)}
<div className="tw:flex tw:gap-2 tw:justify-end">
<Button variant="secondary" onClick={onClose} disabled={isLoading}>
{localizedStrings['%interlinearizer_modal_select_cancel%']}
</Button>
<Button onClick={onCreateNew} disabled={isLoading}>
{localizedStrings['%interlinearizer_modal_select_create_new%']}
</Button>
</div>
</dialog>
</div>
);
}