Skip to content

Commit

Permalink
Merge branch 'main' into 435-basic-list-
Browse files Browse the repository at this point in the history
  • Loading branch information
rolfheij-sil committed Oct 16, 2023
2 parents 7fd2248 + d551cd2 commit 340ba07
Show file tree
Hide file tree
Showing 15 changed files with 423 additions and 65 deletions.
9 changes: 7 additions & 2 deletions extensions/src/hello-world/web-views/hello-world.web-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,20 @@ papi
globalThis.webViewComponent = function HelloWorld() {
const test = useContext(TestContext) || "Context didn't work!! :(";

const [clicks, setClicks] = useState(0);
const [clicks, setClicks] = globalThis.useWebViewState<number>('clicks', 0);
const [rows, setRows] = useState(initializeRows());
const [selectedRows, setSelectedRows] = useState(new Set<Key>());
const [scrRef, setScrRef] = useSetting('platform.verseRef', defaultScrRef);

// Update the clicks when we are informed helloWorld has been run
useEvent(
'helloWorld.onHelloWorld',
useCallback(({ times }: HelloWorldEvent) => setClicks(times), []),
useCallback(
({ times }: HelloWorldEvent) => {
if (times > clicks) setClicks(times);
},
[clicks, setClicks],
),
);

const [echoResult] = usePromise(
Expand Down
106 changes: 100 additions & 6 deletions lib/papi-dts/papi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/// <reference types="node" />
declare module 'shared/global-this.model' {
import { LogLevel } from 'electron-log';
import { FunctionComponent } from 'react';
import { FunctionComponent, Dispatch, SetStateAction } from 'react';
/**
* Variables that are defined in global scope. These must be defined in main.ts (main), index.ts (renderer), and extension-host.ts (extension host)
*/
Expand All @@ -20,9 +20,30 @@ declare module 'shared/global-this.model' {
var logLevel: LogLevel;
/**
* A function that each React WebView extension must provide for Paranext to display it.
* Only used in WebView iframes
* Only used in WebView iframes.
*/
var webViewComponent: FunctionComponent;
/**
* A React hook for working with a state object tied to a webview.
* Only used in WebView iframes.
* @param stateKey Key of the state value to use. The webview state holds a unique value per key.
* NOTE: `stateKey` needs to be a constant string, not something that could change during execution.
* @param defaultStateValue Value to use if the web view state didn't contain a value for the given 'stateKey'
* @returns string holding the state value and a function to use to update the state value
* @example const [lastPersonSeen, setLastPersonSeen] = useWebViewState('lastSeen');
*/
var useWebViewState: <T>(
stateKey: string,
defaultStateValue: NonNullable<T>,
) => [webViewState: NonNullable<T>, setWebViewState: Dispatch<SetStateAction<NonNullable<T>>>];
/**
* Retrieve the value from web view state with the given 'stateKey', if it exists.
*/
var getWebViewState: <T>(stateKey: string) => T | undefined;
/**
* Set the value for a given key in the web view state.
*/
var setWebViewState: <T>(stateKey: string, stateValue: NonNullable<T>) => void;
}
/** Type of Paranext process */
export enum ProcessType {
Expand Down Expand Up @@ -1583,6 +1604,7 @@ declare module 'papi-shared-types' {
*/
interface ProjectDataTypes {
NotesOnly: NotesOnlyProjectDataTypes;
placeholder: MandatoryProjectDataType;
}
/**
* Identifiers for all project types supported by PAPI. These are not intended to correspond 1:1
Expand Down Expand Up @@ -1724,6 +1746,8 @@ declare module 'shared/data/web-view.model' {
content: string;
/** Name of the tab for the WebView */
title?: string;
/** General object to store unique state for this webview */
state?: Record<string, string>;
};
/** WebView representation using React */
export type WebViewDefinitionReact = WebViewDefinitionBase & {
Expand Down Expand Up @@ -1906,6 +1930,40 @@ declare module 'shared/log-error.model' {
constructor(message?: string);
}
}
declare module 'renderer/services/web-view-state.service' {
/**
* Get the web view state associated with the given ID
* This function is only intended to be used at startup. getWebViewState is intended for web views to call.
* @param id ID of the web view
* @returns state object of the given web view
*/
export function getFullWebViewStateById(id: string): Record<string, string>;
/**
* Set the web view state associated with the given ID
* This function is only intended to be used at startup. setWebViewState is intended for web views to call.
* @param id ID of the web view
* @param state State to set for the given web view
*/
export function setFullWebViewStateById(id: string, state: Record<string, string>): void;
/**
* Get the web view state associated with the given ID
* @param id ID of the web view
* @param stateKey Key used to retrieve the state value
* @returns string (if it exists) containing the state for the given key of the given web view
*/
export function getWebViewStateById<T>(id: string, stateKey: string): T | undefined;
/**
* Set the web view state object associated with the given ID
* @param id ID of the web view
* @param stateKey Key for the associated state
* @param stateValue Value of the state for the given key of the given web view - must work with JSON.stringify/parse
*/
export function setWebViewStateById<T>(id: string, stateKey: string, stateValue: T): void;
/** Purge any web view state that hasn't been touched since the process has been running.
* Only call this once all web views have been loaded.
*/
export function cleanupOldWebViewState(): void;
}
declare module 'shared/services/web-view.service' {
import { Unsubscriber } from 'shared/utils/papi-util';
import { MutableRefObject } from 'react';
Expand Down Expand Up @@ -2517,22 +2575,37 @@ declare module 'renderer/hooks/papi-hooks/use-event-async.hook' {
) => void;
export default useEventAsync;
}
declare module 'renderer/hooks/hook-generators/create-use-network-object-hook.util' {
import { NetworkObject } from 'shared/models/network-object.model';
/**
* This function takes in a getNetworkObject function and creates a hook with that function in it
* which will return a network object
* @param getNetworkObject A function that takes in an id string and returns a network object
* @returns a function that takes in a networkObjectSource and returns a NetworkObject
*/
function createUseNetworkObjectHook(
getNetworkObject: (id: string) => Promise<NetworkObject<object> | undefined>,
): (
networkObjectSource: string | NetworkObject<object> | undefined,
) => NetworkObject<object> | undefined;
export default createUseNetworkObjectHook;
}
declare module 'renderer/hooks/papi-hooks/use-data-provider.hook' {
import IDataProvider from 'shared/models/data-provider.interface';
/**
* Gets a data provider with specified provider name
* @param dataProviderSource string name of the data provider to get OR dataProvider (result of useDataProvider if you
* want this hook to just return the data provider again)
* @param dataProviderSource string name of the data provider to get OR dataProvider (result of
* useDataProvider, if you want this hook to just return the data provider again)
* @returns undefined if the data provider has not been retrieved,
* data provider if it has been retrieved and is not disposed,
* and undefined again if the data provider is disposed
*
* @type `T` - the type of data provider to return. Use `IDataProvider<TDataProviderDataTypes>`,
* specifying your own types, or provide a custom data provider type
*/
function useDataProvider<T extends IDataProvider<any>>(
const useDataProvider: <T extends IDataProvider<any>>(
dataProviderSource: string | T | undefined,
): T | undefined;
) => T | undefined;
export default useDataProvider;
}
declare module 'renderer/hooks/papi-hooks/use-data.hook' {
Expand Down Expand Up @@ -2668,17 +2741,38 @@ declare module 'renderer/hooks/papi-hooks/use-setting.hook' {
) => [SettingTypes[SettingName], (newSetting: SettingTypes[SettingName]) => void];
export default useSetting;
}
declare module 'renderer/hooks/papi-hooks/use-project-data-provider.hook' {
import { ProjectDataTypes } from 'papi-shared-types';
import IDataProvider from 'shared/models/data-provider.interface';
/**
* Gets a project data provider with specified provider name
* @param projectDataProviderSource string name of the id of the project to get OR projectDataProvider (result
* of useProjectDataProvider, if you want this hook to just return the data provider again)
* @returns undefined if the project data provider has not been retrieved, the requested project
* data provider if it has been retrieved and is not disposed, and undefined again if the project
* data provider is disposed
*
* @ProjectType `T` - the project type for the project to use. The returned project data provider
* will have the project data provider type associated with this project type.
*/
const useProjectDataProvider: <ProjectType extends keyof ProjectDataTypes>(
projectDataProviderSource: string | IDataProvider<ProjectDataTypes[ProjectType]> | undefined,
) => IDataProvider<ProjectDataTypes[ProjectType]> | undefined;
export default useProjectDataProvider;
}
declare module 'renderer/hooks/papi-hooks/index' {
import usePromise from 'renderer/hooks/papi-hooks/use-promise.hook';
import useEvent from 'renderer/hooks/papi-hooks/use-event.hook';
import useEventAsync from 'renderer/hooks/papi-hooks/use-event-async.hook';
import useDataProvider from 'renderer/hooks/papi-hooks/use-data-provider.hook';
import useData from 'renderer/hooks/papi-hooks/use-data.hook';
import useSetting from 'renderer/hooks/papi-hooks/use-setting.hook';
import useProjectDataProvider from 'renderer/hooks/papi-hooks/use-project-data-provider.hook';
export interface PapiHooks {
usePromise: typeof usePromise;
useEvent: typeof useEvent;
useEventAsync: typeof useEventAsync;
useProjectDataProvider: typeof useProjectDataProvider;
useDataProvider: typeof useDataProvider;
/**
* Special React hook that subscribes to run a callback on a data provider's data with specified
Expand Down
4 changes: 4 additions & 0 deletions src/declarations/papi-shared-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ declare module 'papi-shared-types' {
*/
export interface ProjectDataTypes {
NotesOnly: NotesOnlyProjectDataTypes;
// With only one key in this interface, `papi.d.ts` was baking in the literal string when
// `SettingNames` was being used. Adding a placeholder key makes TypeScript generate `papi.d.ts`
// correctly. When we add another setting, we can remove this placeholder.
placeholder: MandatoryProjectDataType;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,28 @@ import { SavedTabInfo, TabInfo } from '@shared/data/web-view.model';
import { Button, ScriptureReference, getChaptersForBook } from 'papi-components';
import logger from '@shared/services/logger.service';
import { Typography } from '@mui/material';
import { useState } from 'react';
import { useState, useMemo } from 'react';
import settingsService from '@shared/services/settings.service';
import { fetchProjects } from '@renderer/components/project-dialogs/open-project-tab.component';
import BookSelector from '@renderer/components/run-basic-checks-dialog/book-selector.component';
import BasicChecks, {
fetchChecks,
} from '@renderer/components/run-basic-checks-dialog/basic-checks.component';
import { Project } from '@renderer/components/project-dialogs/project-list.component';
import './run-basic-checks-tab.component.scss';
import useProjectDataProvider from '@renderer/hooks/papi-hooks/use-project-data-provider.hook';
import { VerseRef } from '@sillsdev/scripture';
import usePromise from '@renderer/hooks/papi-hooks/use-promise.hook';

export const TAB_TYPE_RUN_BASIC_CHECKS = 'run-basic-checks';

// Changing global scripture reference won't effect the dialog because reference is passed in once at the start.
type RunBasicChecksTabProps = {
currentScriptureReference: ScriptureReference | null;
currentProject: Project | undefined;
currentProjectId: string | undefined;
};

export default function RunBasicChecksTab({
currentScriptureReference,
currentProject,
currentProjectId,
}: RunBasicChecksTabProps) {
const currentBookNumber = currentScriptureReference?.bookNum ?? 1;
const basicChecks = fetchChecks();
Expand Down Expand Up @@ -83,11 +84,21 @@ export default function RunBasicChecksTab({
);
};

const project = useProjectDataProvider<'ParatextStandard'>(currentProjectId);

const [projectString] = usePromise(
useMemo(() => {
return async () =>
project === undefined
? 'No current project'
: project.getVerseUSFM(new VerseRef('MAT 4:1'));
}, [project]),
'Loading',
);

return (
<div className="run-basic-checks-dialog">
<Typography variant="h5">
{currentProject ? currentProject.name : 'No Current Project'}
</Typography>
<Typography variant="h5">{`Run basic checks: ${currentProjectId}, ${projectString}`}</Typography>
{/* Should always be two columns? */}
<fieldset className="run-basic-checks-check-names">
<legend>Checks</legend>
Expand Down Expand Up @@ -120,14 +131,18 @@ export default function RunBasicChecksTab({
}

export const loadRunBasicChecksTab = (savedTabInfo: SavedTabInfo): TabInfo => {
const project = fetchProjects().find((proj) => proj.id === 'project-1');

return {
...savedTabInfo,
tabTitle: 'Run Basic Checks',
content: (
<RunBasicChecksTab
currentProject={project}
// #region Test a .NET data provider
// TODO: Uncomment this or similar sample code once https://github.com/paranext/paranext-core/issues/440 is resolved
// In the meantime, if you want to try this, copy an existing project into
// <home_dir>/.platform.bible/<project_short_name>_<project_ID_from_settings.xml>/project/paratext
// For example: "~/.platform.bible/projects/TPKJ_b4c501ad2538989d6fb723518e92408406e232d3/project/paratext"
// Then create a file named "meta.json" in the "<short_name>_<project_ID>" directory with this JSON:
currentProjectId="INSERT YOUR PROJECT ID HERE"
currentScriptureReference={settingsService.get('platform.verseRef')}
/>
),
Expand Down
13 changes: 13 additions & 0 deletions src/renderer/global-this.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import * as SillsdevScripture from '@sillsdev/scripture';
import { ProcessType } from '@shared/global-this.model';
import papi, { Papi } from '@renderer/services/papi-frontend.service';
import { getModuleSimilarApiMessage } from '@shared/utils/papi-util';
import {
getWebViewStateById,
setWebViewStateById,
} from '@renderer/services/web-view-state.service';
import useWebViewState from '@renderer/hooks/use-webview-state';

// #region webpack DefinePlugin types setup - these should be from the renderer webpack DefinePlugin

Expand Down Expand Up @@ -57,6 +62,9 @@ declare global {
var createRoot: typeof ReactDOMClient.createRoot;
var SillsdevScripture: SillsdevScriptureType;
var webViewRequire: WebViewRequire;
// Web view state functions are used in the default imports for each webview in web-view.service.ts
var getWebViewStateById: <T>(id: string, stateKey: string) => T | undefined;
var setWebViewStateById: <T>(id: string, stateKey: string, stateValue: NonNullable<T>) => void;
}
/* eslint-enable */

Expand All @@ -81,5 +89,10 @@ globalThis.ReactDOMClient = ReactDOMClient;
globalThis.createRoot = ReactDOMClient.createRoot;
globalThis.SillsdevScripture = SillsdevScripture;
globalThis.webViewRequire = webViewRequire;
// We don't expose get/setWebViewStateById in PAPI because web views don't have access to IDs
globalThis.getWebViewStateById = getWebViewStateById;
globalThis.setWebViewStateById = setWebViewStateById;
// We store the hook reference because we need it to bind it to the webview's iframe 'window' context
globalThis.useWebViewState = useWebViewState;

// #endregion
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { NetworkObject } from '@shared/models/network-object.model';
import { useMemo, useState, useCallback } from 'react';
import { isString } from '@shared/utils/util';
import useEvent from '@renderer/hooks/papi-hooks/use-event.hook';
import usePromise from '@renderer/hooks/papi-hooks/use-promise.hook';

/**
* This function takes in a getNetworkObject function and creates a hook with that function in it
* which will return a network object
* @param getNetworkObject A function that takes in an id string and returns a network object
* @returns a function that takes in a networkObjectSource and returns a NetworkObject
*/
function createUseNetworkObjectHook(
getNetworkObject: (id: string) => Promise<NetworkObject<object> | undefined>,
): (
networkObjectSource: string | NetworkObject<object> | undefined,
) => NetworkObject<object> | undefined {
return function useNetworkObject(
networkObjectSource: string | NetworkObject<object> | undefined,
): NetworkObject<object> | undefined {
// Check to see if they passed in the results of a useNetworkObject hook or undefined
const didReceiveNetworkObject = !isString(networkObjectSource);

// Get the network object for this network object name
// Note: do nothing if we already a network object, but still run this hook.
// (We must make sure to run the same number of hooks in all code paths.)
const [networkObject] = usePromise(
useMemo(() => {
return didReceiveNetworkObject
? // We already have a network object or undefined, so we don't need to run this promise
undefined
: async () =>
// We have the network object's type, so we need to get the provider
networkObjectSource ? getNetworkObject(networkObjectSource) : undefined;
}, [didReceiveNetworkObject, networkObjectSource]),
undefined,
);

// Disable this hook when the network object is disposed
// Note: do nothing if we already received a network object, but still run this hook.
// (We must make sure to run the same number of hooks in all code paths.)
const [isDisposed, setIsDisposed] = useState<boolean>(false);
useEvent(
!didReceiveNetworkObject && networkObject && !isDisposed
? networkObject.onDidDispose
: undefined,
useCallback(() => setIsDisposed(true), []),
);

// If we received a network object or undefined, return it
if (didReceiveNetworkObject) return networkObjectSource;

// If we had to get a network object, return it if it is not disposed
return networkObject && !isDisposed ? networkObject : undefined;
};
}

export default createUseNetworkObjectHook;
Loading

0 comments on commit 340ba07

Please sign in to comment.