Skip to content

Commit

Permalink
Create useProjectData hook (#568)
Browse files Browse the repository at this point in the history
  • Loading branch information
tjcouch-sil authored Oct 18, 2023
2 parents c969022 + 156a692 commit 216ec07
Show file tree
Hide file tree
Showing 9 changed files with 627 additions and 163 deletions.
13 changes: 13 additions & 0 deletions extensions/src/hello-world/web-views/hello-world.web-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ globalThis.webViewComponent = function HelloWorld() {
const [rows, setRows] = useState(initializeRows());
const [selectedRows, setSelectedRows] = useState(new Set<Key>());
const [scrRef, setScrRef] = useSetting('platform.verseRef', defaultScrRef);
/* const verseRef = useMemo(
() => new VerseRef(scrRef.bookNum, scrRef.chapterNum, scrRef.verseNum),
[scrRef],
); */

// Update the clicks when we are informed helloWorld has been run
useEvent(
Expand Down Expand Up @@ -126,6 +130,13 @@ globalThis.webViewComponent = function HelloWorld() {
'Loading John 1:1...',
);

// TODO: Uncomment this or similar sample code once https://github.com/paranext/paranext-core/issues/440 is resolved
/* const [webVerse] = useProjectData.VerseUSFM<ProjectDataTypes['ParatextStandard'], 'VerseUSFM'>(
'32664dc3288a28df2e2bb75ded887fc8f17a15fb',
verseRef,
'Loading WEB Verse',
); */

return (
<div>
<div className="title">
Expand Down Expand Up @@ -171,6 +182,8 @@ globalThis.webViewComponent = function HelloWorld() {
<div>{john11}</div>
<h3>Psalm 1</h3>
<div>{psalm1}</div>
{/* <h3>{verseRef.toString()} WEB</h3>
<div>{webVerse}</div> */}
<br />
<div>
<TextField label="Test Me" />
Expand Down
286 changes: 275 additions & 11 deletions lib/papi-dts/papi.d.ts

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/declarations/papi-shared-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ 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.
// `ProjectTypes` was being used. Adding a placeholder key makes TypeScript generate `papi.d.ts`
// correctly. When we add another project data type, we can remove this placeholder.
placeholder: MandatoryProjectDataType;
}

Expand Down
169 changes: 169 additions & 0 deletions src/renderer/hooks/hook-generators/create-use-data-hook.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import {
DataProviderDataTypes,
DataProviderSetter,
DataProviderSubscriber,
DataProviderSubscriberOptions,
DataProviderUpdateInstructions,
DataTypeNames,
} from '@shared/models/data-provider.model';
import IDataProvider from '@shared/models/data-provider.interface';
import useEventAsync from '@renderer/hooks/papi-hooks/use-event-async.hook';
import { useMemo, useState } from 'react';
import { PapiEventAsync, PapiEventHandler } from '@shared/models/papi-event.model';
import { isString } from '@shared/utils/util';

/**
* Proxy object that provides hooks to use data provider data with various data types
*
* @example `useData.Greeting<PeopleDataTypes, 'Greeting'>(...)`
*
* @type `TDataTypes` - the data provider data types served by the data provider whose data to use.
* @type `TDataType` - the specific data type on this data provider that you want to use. Must match
* the data type specified in `useData.<data_type>`
*/
export type UseDataHook = {
[DataType in string]: <
TDataTypes extends DataProviderDataTypes,
TDataType extends keyof TDataTypes,
>(
dataProviderSource: string | IDataProvider<TDataTypes> | undefined,
selector: TDataTypes[TDataType]['selector'],
defaultValue: TDataTypes[TDataType]['getData'],
subscriberOptions?: DataProviderSubscriberOptions,
) => [
TDataTypes[TDataType]['getData'],
(
| ((
newData: TDataTypes[TDataType]['setData'],
) => Promise<DataProviderUpdateInstructions<TDataTypes>>)
| undefined
),
boolean,
];
};

function createUseDataHook(
useDataProviderHook: (
dataProviderSource: string | IDataProvider | undefined,
) => IDataProvider | undefined,
): UseDataHook {
function createUseDataHookForDataTypeInternal(dataType: string) {
return <TDataTypes extends DataProviderDataTypes, TDataType extends typeof dataType>(
dataProviderSource: string | IDataProvider<TDataTypes> | undefined,
selector: TDataTypes[TDataType]['selector'],
defaultValue: TDataTypes[TDataType]['getData'],
subscriberOptions?: DataProviderSubscriberOptions,
): [
TDataTypes[TDataType]['getData'],
(
| ((
newData: TDataTypes[TDataType]['setData'],
) => Promise<DataProviderUpdateInstructions<TDataTypes>>)
| undefined
),
boolean,
] => {
// The data from the data provider at this selector
const [data, setDataInternal] = useState<TDataTypes[TDataType]['getData']>(defaultValue);

// Get the data provider for this data provider name
const dataProvider = useDataProviderHook(
// Type assertion needed because useDataProviderHook will have different generic types
// based on which hook we are generating, but they will all be returning an IDataProvider
dataProviderSource as string | IDataProvider | undefined,
);

// Indicates if the data with the selector is awaiting retrieval from the data provider
const [isLoading, setIsLoading] = useState<boolean>(true);

// Wrap subscribe so we can call it as a normal PapiEvent in useEvent
const wrappedSubscribeEvent: PapiEventAsync<TDataTypes[TDataType]['getData']> | undefined =
useMemo(
() =>
dataProvider
? async (eventCallback: PapiEventHandler<TDataTypes[TDataType]['getData']>) => {
const unsub =
await // We need any here because for some reason IDataProvider loses its ability to index subscribe
(
(
dataProvider as /* eslint-disable @typescript-eslint/no-explicit-any */ any
) /* eslint-enable */[
`subscribe${dataType as DataTypeNames<TDataTypes>}`
] as DataProviderSubscriber<TDataTypes[TDataType]>
)(
selector,
(subscriptionData: TDataTypes[TDataType]['getData']) => {
eventCallback(subscriptionData);
// When we receive updated data, mark that we are not loading
setIsLoading(false);
},
subscriberOptions,
);

return async () => {
// When we change data type or selector, mark that we are loading
setIsLoading(true);
return unsub();
};
}
: undefined,
[dataProvider, selector, subscriberOptions],
);

// Subscribe to the data provider
useEventAsync(wrappedSubscribeEvent, setDataInternal);

// TODO: cache latest setStateAction and fire until we have dataProvider instead of having setData be undefined until we have dataProvider?
/** Send an update to the backend to update the data. Let the update handle actually updating our data here */
const setData = useMemo(
() =>
dataProvider
? async (newData: TDataTypes[TDataType]['setData']) =>
// We need any here because for some reason IDataProvider loses its ability to index subscribe
(
(dataProvider as /* eslint-disable @typescript-eslint/no-explicit-any */ any)[
/* eslint-enable */ `set${dataType as DataTypeNames<TDataTypes>}`
] as DataProviderSetter<TDataTypes, typeof dataType>
)(selector, newData)
: undefined,
[dataProvider, selector],
);

return [data, setData, isLoading];
};
}

// People can make whatever data hook they want
const useDataCachedHooks: UseDataHook = {};

const useData: UseDataHook = new Proxy(useDataCachedHooks, {
get(obj, prop) {
// Pass promises through
if (prop === 'then') return obj[prop as keyof typeof obj];

// Special react prop to tell if it's a component
if (prop === '$$typeof') return undefined;

// If we have already generated the hook, return the cached version
if (prop in useDataCachedHooks)
return useDataCachedHooks[prop as keyof typeof useDataCachedHooks];

// Build a new useData hook
if (!isString(prop)) throw new Error('Must provide a string to the useData hook proxy');

const newHook = createUseDataHookForDataTypeInternal(prop);

// Save the hook in the cache to be used later
useDataCachedHooks[prop as keyof typeof useDataCachedHooks] = newHook;

return newHook;
},
set() {
// Doing this makes no sense
throw new Error('Cannot set useData hook');
},
});
return useData;
}

export default createUseDataHook;
4 changes: 4 additions & 0 deletions src/renderer/hooks/papi-hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 useProjectData from '@renderer/hooks/papi-hooks/use-project-data.hook';
import useProjectDataProvider from '@renderer/hooks/papi-hooks/use-project-data-provider.hook';
import useDialogCallback from '@renderer/hooks/papi-hooks/use-dialog-callback.hook';
import useDataProviderMulti from '@renderer/hooks/papi-hooks/use-data-provider-multi.hook';
Expand All @@ -19,6 +20,8 @@ export interface PapiHooks {
useDataProviderMulti: typeof useDataProviderMulti;
/** JSDOC DESTINATION UseDataHook */
useData: typeof useData;
/** JSDOC DESTINATION UseProjectDataHook */
useProjectData: typeof useProjectData;
useSetting: typeof useSetting;
}

Expand All @@ -34,6 +37,7 @@ const papiHooks: PapiHooks = {
useDataProvider,
useDataProviderMulti,
useData,
useProjectData,
useSetting,
};

Expand Down
1 change: 0 additions & 1 deletion src/renderer/hooks/papi-hooks/use-data-provider.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import createUseNetworkObjectHook from '@renderer/hooks/hook-generators/create-u
* @type `T` - the type of data provider to return. Use `IDataProvider<TDataProviderDataTypes>`,
* specifying your own types, or provide a custom data provider type
*/

const useDataProvider = createUseNetworkObjectHook(dataProviderService.get) as <
// We don't know what type the data provider serves
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
Loading

0 comments on commit 216ec07

Please sign in to comment.