Skip to content

Commit 1810821

Browse files
committed
split home webview and component + prepare dynamic types
1 parent 7b43857 commit 1810821

21 files changed

Lines changed: 7128 additions & 4776 deletions

File tree

Lines changed: 284 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,286 @@
11
import papi, { logger } from '@papi/frontend';
22
import { useData, useDataProvider, useLocalizedStrings, useSetting } from '@papi/frontend/react';
3-
import { HomeDialog } from 'platform-bible-react';
4-
5-
globalThis.webViewComponent = HomeDialog({
6-
useLocalizedStrings: useLocalizedStrings,
7-
useDataProvider: useDataProvider,
8-
useData: useData,
9-
papi: papi,
10-
logger: logger,
11-
useSetting: useSetting,
12-
});
3+
import { HomeDialog, useEvent } from 'platform-bible-react';
4+
import {
5+
getErrorMessage,
6+
isErrorMessageAboutParatextBlockingInternetAccess,
7+
isErrorMessageAboutRegistryAuthFailure,
8+
isPlatformError,
9+
LocalizeKey,
10+
newGuid,
11+
LocalProjectInfo,
12+
SharedProjectsInfo,
13+
} from 'platform-bible-utils';
14+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
15+
16+
const HOME_STRING_KEYS: LocalizeKey[] = [
17+
'%resources_action%',
18+
'%resources_activity%',
19+
'%resources_clearSearch%',
20+
'%home_dialog_title%',
21+
'%resources_filterInput%',
22+
'%resources_fullName%',
23+
'%resources_get%',
24+
'%resources_getResources%',
25+
'%resources_items%',
26+
'%resources_language%',
27+
'%resources_loading%',
28+
'%resources_noProjects%',
29+
'%resources_noProjectsInstruction%',
30+
'%resources_noSearchResults%',
31+
'%resources_open%',
32+
'%resources_searchedFor%',
33+
'%resources_sync%',
34+
];
35+
36+
const defaultExcludePdpFactoryIds: string[] = [];
37+
const defaultInterfaceLanguages: string[] = ['en'];
38+
39+
globalThis.webViewComponent = function HomeDialogWebView() {
40+
const isMounted = useRef(false);
41+
useEffect(() => {
42+
isMounted.current = true;
43+
return () => {
44+
isMounted.current = false;
45+
};
46+
}, []);
47+
48+
const [localizedStrings] = useLocalizedStrings(HOME_STRING_KEYS);
49+
50+
const dblResourcesProvider = useDataProvider('platformGetResources.dblResourcesProvider');
51+
52+
const [showGetResourcesButton, setShowGetResourcesButton] = useState<boolean | undefined>(
53+
undefined,
54+
);
55+
56+
const [resourcesList] = useData('platformGetResources.dblResourcesProvider').DblResources(
57+
undefined,
58+
[],
59+
);
60+
61+
const [isSendReceiveAvailable, setIsSendReceiveAvailable] = useState<boolean | undefined>(
62+
undefined,
63+
);
64+
65+
const openResourceOrProject = (projectId: string, isEditable: boolean) =>
66+
papi.commands.sendCommand(
67+
isEditable
68+
? 'platformScriptureEditor.openScriptureEditor'
69+
: 'platformScriptureEditor.openResourceViewer',
70+
projectId,
71+
);
72+
73+
const openGetResources = () => papi.commands.sendCommand('platformGetResources.openGetResources');
74+
75+
const checkIfSendReceiveAvailable = useCallback(async () => {
76+
const isAvailable = await papi.commands.sendCommand(
77+
'platformGetResources.isSendReceiveAvailable',
78+
);
79+
if (isMounted.current) {
80+
setIsSendReceiveAvailable(isAvailable);
81+
}
82+
}, []);
83+
84+
const [isSendReceiveInProgress, setIsSendReceiveInProgress] = useState<boolean>(false);
85+
const [activeSendReceiveProjects, setActiveSendReceiveProjects] = useState<string[]>([]);
86+
87+
const [sharedProjectsInfo, setSharedProjectsInfo] = useState<SharedProjectsInfo>();
88+
const [isLoadingRemoteProjects, setIsLoadingRemoteProjects] = useState<boolean>(true);
89+
90+
const sharedProjectErrorNotificationId = useMemo(() => newGuid(), []);
91+
92+
const sendReceiveProject = async (projectId: string) => {
93+
if (!isSendReceiveAvailable) return;
94+
95+
try {
96+
setIsSendReceiveInProgress(true);
97+
setActiveSendReceiveProjects((prev) => [...prev, projectId]);
98+
99+
await papi.commands.sendCommand('paratextBibleSendReceive.sendReceiveProjects', [projectId]);
100+
101+
if (isMounted.current) {
102+
setActiveSendReceiveProjects((prev) => prev.filter((id) => id !== projectId));
103+
setIsSendReceiveInProgress(false);
104+
}
105+
} catch (e) {
106+
logger.warn(
107+
`Home web view failed to reload after running S/R for project ${projectId}: ${e}`,
108+
);
109+
if (isMounted.current) {
110+
setActiveSendReceiveProjects((prev) => prev.filter((id) => id !== projectId));
111+
setIsSendReceiveInProgress(false);
112+
}
113+
}
114+
};
115+
116+
useEffect(() => {
117+
const fetchAvailability = async () => {
118+
if (dblResourcesProvider) {
119+
const isGetDblResourcesAvailable = await dblResourcesProvider.isGetDblResourcesAvailable();
120+
if (isMounted.current) {
121+
setShowGetResourcesButton(isGetDblResourcesAvailable);
122+
}
123+
} else {
124+
setShowGetResourcesButton(undefined);
125+
}
126+
};
127+
128+
fetchAvailability();
129+
}, [dblResourcesProvider]);
130+
131+
useEffect(() => {
132+
checkIfSendReceiveAvailable();
133+
}, [checkIfSendReceiveAvailable]);
134+
135+
useEvent(
136+
papi.network.getNetworkEvent('platform.onDidReloadExtensions'),
137+
checkIfSendReceiveAvailable,
138+
);
139+
140+
useEffect(() => {
141+
if (!isSendReceiveAvailable) {
142+
setIsLoadingRemoteProjects(false);
143+
return;
144+
}
145+
146+
let promiseIsCurrent = true;
147+
const getSharedProjects = async () => {
148+
try {
149+
const projectsInfo = await papi.commands.sendCommand(
150+
'paratextBibleSendReceive.getSharedProjects',
151+
);
152+
153+
if (promiseIsCurrent && isMounted.current) {
154+
setIsLoadingRemoteProjects(false);
155+
setSharedProjectsInfo(projectsInfo);
156+
}
157+
} catch (e) {
158+
const errorMessage = getErrorMessage(e);
159+
if (isErrorMessageAboutParatextBlockingInternetAccess(errorMessage)) {
160+
papi.notifications.send({
161+
severity: 'error',
162+
message: '%data_loading_error_paratextData_internet_disabled%',
163+
clickCommandLabel: '%general_open%',
164+
clickCommand: 'paratextRegistration.showParatextRegistration',
165+
notificationId: sharedProjectErrorNotificationId,
166+
});
167+
} else if (isErrorMessageAboutRegistryAuthFailure(errorMessage)) {
168+
papi.notifications.send({
169+
severity: 'error',
170+
message: '%data_loading_error_paratextData_auth_failure%',
171+
clickCommandLabel: '%general_open%',
172+
clickCommand: 'paratextRegistration.showParatextRegistration',
173+
notificationId: sharedProjectErrorNotificationId,
174+
});
175+
} else {
176+
logger.warn(`Home web view failed to get shared projects: ${errorMessage}`);
177+
}
178+
179+
if (promiseIsCurrent && isMounted.current) {
180+
setIsLoadingRemoteProjects(false);
181+
}
182+
}
183+
};
184+
185+
if (isSendReceiveInProgress) {
186+
return;
187+
}
188+
if (!isSendReceiveAvailable) {
189+
setIsLoadingRemoteProjects(false);
190+
return;
191+
}
192+
getSharedProjects();
193+
194+
return () => {
195+
// Mark this promise as old and not to be used
196+
promiseIsCurrent = false;
197+
};
198+
}, [isSendReceiveAvailable, isSendReceiveInProgress, sharedProjectErrorNotificationId]);
199+
200+
const [localProjectResourceInfo, setLocalProjectsInfo] = useState<LocalProjectInfo[]>([]);
201+
const [isLoadingLocalProjects, setIsLoadingLocalProjects] = useState<boolean>(true);
202+
203+
const [excludePdpFactoryIdsInHomePossiblyError] = useSetting(
204+
'platformGetResources.excludePdpFactoryIdsInHome',
205+
defaultExcludePdpFactoryIds,
206+
);
207+
208+
const excludePdpFactoryIds = useMemo(() => {
209+
if (isPlatformError(excludePdpFactoryIdsInHomePossiblyError)) {
210+
logger.warn(
211+
'Failed to load setting: platformGetResources.excludePdpFactoryIdsInHome',
212+
excludePdpFactoryIdsInHomePossiblyError,
213+
);
214+
return defaultExcludePdpFactoryIds;
215+
}
216+
return excludePdpFactoryIdsInHomePossiblyError;
217+
}, [excludePdpFactoryIdsInHomePossiblyError]);
218+
219+
useEffect(() => {
220+
let promiseIsCurrent = true;
221+
const getLocalProjects = async () => {
222+
const projectMetadata = await papi.projectLookup.getMetadataForAllProjects({
223+
includeProjectInterfaces: ['platformScripture.USJ_Chapter'],
224+
excludePdpFactoryIds,
225+
});
226+
const projectInfo: LocalProjectInfo[] = await Promise.all(
227+
projectMetadata.map(async (data) => {
228+
const pdp = await papi.projectDataProviders.get('platform.base', data.id);
229+
const isEditable = await pdp.getSetting('platform.isEditable');
230+
return {
231+
id: data.id,
232+
isEditable: isEditable,
233+
fullName: await pdp.getSetting('platform.fullName'),
234+
name: await pdp.getSetting('platform.name'),
235+
language: await pdp.getSetting('platform.language'),
236+
// TODO: fix when the papi offers a way to distinguish between types
237+
type: isEditable ? 'project' : 'resource',
238+
};
239+
}),
240+
);
241+
242+
if (promiseIsCurrent && isMounted.current) {
243+
setIsLoadingLocalProjects(false);
244+
setLocalProjectsInfo(projectInfo);
245+
}
246+
};
247+
248+
if (isSendReceiveInProgress) {
249+
return;
250+
}
251+
getLocalProjects();
252+
253+
return () => {
254+
// Mark this promise as old and not to be used
255+
promiseIsCurrent = false;
256+
};
257+
// resourcesList is only used to trigger a re-fetch of installed resources when the list of resources changes
258+
}, [isSendReceiveInProgress, excludePdpFactoryIds, resourcesList]);
259+
260+
const [interfaceLanguages] = useSetting('platform.interfaceLanguage', defaultInterfaceLanguages);
261+
262+
const uiLocales = useMemo(() => {
263+
if (isPlatformError(interfaceLanguages)) {
264+
logger.warn('Failed to load setting: platform.interfaceLanguage', interfaceLanguages);
265+
return defaultInterfaceLanguages;
266+
}
267+
return interfaceLanguages;
268+
}, [interfaceLanguages]);
269+
270+
return (
271+
<HomeDialog
272+
localizedStrings={localizedStrings}
273+
uiLocales={uiLocales}
274+
onOpenGetResources={openGetResources}
275+
onOpenResourceOrProject={openResourceOrProject}
276+
onSendReceiveProject={sendReceiveProject}
277+
showGetResourcesButton={showGetResourcesButton}
278+
isSendReceiveInProgress={isSendReceiveInProgress}
279+
isLoadingLocalProjects={isLoadingLocalProjects}
280+
isLoadingRemoteProjects={isLoadingRemoteProjects}
281+
localProjectResourceInfo={localProjectResourceInfo}
282+
sharedProjectsInfo={sharedProjectsInfo}
283+
activeSendReceiveProjects={activeSendReceiveProjects}
284+
/>
285+
);
286+
};

extensions/src/platform-scripture/src/types/platform-scripture.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,7 @@ declare module 'platform-scripture' {
807807

808808
// #endregion
809809
// #region Send/Receive Types
810+
//TODO: can we just remove them from here and use those from platform-bible-utils? Which extension uses them?
810811

811812
/**
812813
* In what state the project to S/R is

lib/platform-bible-react/dist/index.cjs

Lines changed: 12 additions & 1 deletion
Large diffs are not rendered by default.

lib/platform-bible-react/dist/index.cjs.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/platform-bible-react/dist/index.d.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,36 @@ type LocalizedStringValue = string;
141141
interface LanguageStrings {
142142
[k: LocalizeKey]: LocalizedStringValue;
143143
}
144+
type EditedStatus = undefined | "" | "edited" | "new" | "unregistered";
145+
type SharedProjectInfo = {
146+
id: string;
147+
name: string;
148+
fullName: string;
149+
language: string;
150+
editedStatus: EditedStatus;
151+
lastSendReceiveDate: string;
152+
/** Names of admins on this project. Only filled if project is new */
153+
adminNames?: string[];
154+
warnings?: string[];
155+
};
156+
type SharedProjectsInfo = {
157+
[projectId: string]: SharedProjectInfo;
158+
};
159+
type LocalProjectInfo = {
160+
id: string;
161+
isEditable: boolean;
162+
fullName: string;
163+
name: string;
164+
language: string;
165+
type: ProjectTypeKey;
166+
};
167+
declare const PROJECT_TYPE_KEYS: readonly [
168+
"project",
169+
"resource",
170+
"dictionary",
171+
"media"
172+
];
173+
type ProjectTypeKey = (typeof PROJECT_TYPE_KEYS)[number];
144174
export type ChapterRangeSelectorProps = {
145175
/** The selected start chapter */
146176
startChapter: number;
@@ -1627,6 +1657,21 @@ export declare const Tooltip: React$1.FC<TooltipPrimitive.TooltipProps>;
16271657
export declare const TooltipTrigger: React$1.ForwardRefExoticComponent<TooltipPrimitive.TooltipTriggerProps & React$1.RefAttributes<HTMLButtonElement>>;
16281658
/** @inheritdoc Tooltip */
16291659
export declare const TooltipContent: React$1.ForwardRefExoticComponent<Omit<TooltipPrimitive.TooltipContentProps & React$1.RefAttributes<HTMLDivElement>, "ref"> & React$1.RefAttributes<HTMLDivElement>>;
1660+
export type HomeDialogProps = {
1661+
localizedStrings?: LanguageStrings;
1662+
uiLocales?: Intl.LocalesArgument;
1663+
onOpenGetResources?: () => void;
1664+
onOpenResourceOrProject?: (projectId: string, isEditable: boolean) => void;
1665+
onSendReceiveProject?: (projectId: string) => void;
1666+
showGetResourcesButton?: boolean;
1667+
isSendReceiveInProgress?: boolean;
1668+
isLoadingLocalProjects?: boolean;
1669+
isLoadingRemoteProjects?: boolean;
1670+
localProjectResourceInfo?: LocalProjectInfo[];
1671+
sharedProjectsInfo?: SharedProjectsInfo;
1672+
activeSendReceiveProjects?: string[];
1673+
};
1674+
export declare function HomeDialog({ localizedStrings, uiLocales, onOpenGetResources, onOpenResourceOrProject, onSendReceiveProject, showGetResourcesButton, isSendReceiveInProgress, isLoadingLocalProjects, isLoadingRemoteProjects, localProjectResourceInfo, sharedProjectsInfo, activeSendReceiveProjects, }: HomeDialogProps): import("react/jsx-runtime").JSX.Element;
16301675
/**
16311676
* Adds an event handler to an event so the event handler runs when the event is emitted. Use
16321677
* `papi.network.getNetworkEvent` to use a networked event with this hook.

0 commit comments

Comments
 (0)