From 0904f2335314d4e918274821c427a00ff85476aa Mon Sep 17 00:00:00 2001 From: Katherine Jensen Date: Tue, 17 Oct 2023 08:46:12 -0700 Subject: [PATCH 1/2] Create use-data-hook generator and use-project-data-hook --- lib/papi-dts/papi.d.ts | 19 +- .../create-use-data-hook.utils.ts | 196 ++++++++++++++++++ src/renderer/hooks/papi-hooks/index.ts | 3 + .../hooks/papi-hooks/use-data.hook.ts | 147 +------------ .../hooks/papi-hooks/use-project-data.hook.ts | 39 ++++ 5 files changed, 260 insertions(+), 144 deletions(-) create mode 100644 src/renderer/hooks/hook-generators/create-use-data-hook.utils.ts create mode 100644 src/renderer/hooks/papi-hooks/use-project-data.hook.ts diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index b0e5650ae1..a861f77828 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -2551,14 +2551,14 @@ declare module 'renderer/hooks/papi-hooks/use-data-provider.hook' { ) => T | undefined; export default useDataProvider; } -declare module 'renderer/hooks/papi-hooks/use-data.hook' { +declare module 'renderer/hooks/hook-generators/create-use-data-hook.utils' { import { DataProviderDataTypes, DataProviderSubscriberOptions, DataProviderUpdateInstructions, } from 'shared/models/data-provider.model'; import IDataProvider from 'shared/models/data-provider.interface'; - type UseDataHook = { + export type UseDataHook = { [DataType in string]: ( dataProviderSource: string | IDataProvider | undefined, selector: TDataTypes[TDataType]['selector'], @@ -2575,6 +2575,15 @@ declare module 'renderer/hooks/papi-hooks/use-data.hook' { boolean, ]; }; + function createUseDataHook( + useDataProviderHook: ( + dataProviderSource: string | IDataProvider | undefined, + ) => IDataProvider | undefined, + ): UseDataHook; + export default createUseDataHook; +} +declare module 'renderer/hooks/papi-hooks/use-data.hook' { + import { UseDataHook } from 'renderer/hooks/hook-generators/create-use-data-hook.utils'; /** * Special React hook that subscribes to run a callback on a data provider's data with specified * selector on any data type that data provider serves. @@ -2703,6 +2712,10 @@ declare module 'renderer/hooks/papi-hooks/use-project-data-provider.hook' { ) => IDataProvider | undefined; export default useProjectDataProvider; } +declare module 'renderer/hooks/papi-hooks/use-project-data.hook' { + const useProjectData: import('renderer/hooks/hook-generators/create-use-data-hook.utils').UseDataHook; + export default useProjectData; +} 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'; @@ -2710,6 +2723,7 @@ declare module 'renderer/hooks/papi-hooks/index' { 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'; export interface PapiHooks { usePromise: typeof usePromise; @@ -2754,6 +2768,7 @@ declare module 'renderer/hooks/papi-hooks/index' { * - `isLoading`: whether the data with the data type and selector is awaiting retrieval from the data provider */ useData: typeof useData; + useProjectData: typeof useProjectData; useSetting: typeof useSetting; } /** diff --git a/src/renderer/hooks/hook-generators/create-use-data-hook.utils.ts b/src/renderer/hooks/hook-generators/create-use-data-hook.utils.ts new file mode 100644 index 0000000000..092a70ac6d --- /dev/null +++ b/src/renderer/hooks/hook-generators/create-use-data-hook.utils.ts @@ -0,0 +1,196 @@ +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'; + +export type UseDataHook = { + [DataType in string]: ( + dataProviderSource: string | IDataProvider | undefined, + selector: TDataTypes[TDataType]['selector'], + defaultValue: TDataTypes[TDataType]['getData'], + subscriberOptions?: DataProviderSubscriberOptions, + ) => [ + TDataTypes[TDataType]['getData'], + ( + | (( + newData: TDataTypes[TDataType]['setData'], + ) => Promise>) + | undefined + ), + boolean, + ]; +}; + +function createUseDataHook( + useDataProviderHook: ( + dataProviderSource: string | IDataProvider | undefined, + ) => IDataProvider | undefined, +): UseDataHook { + function createUseDataHookForDataTypeInternal(dataType: string) { + return ( + dataProviderSource: string | IDataProvider | undefined, + selector: TDataTypes[TDataType]['selector'], + defaultValue: TDataTypes[TDataType]['getData'], + subscriberOptions?: DataProviderSubscriberOptions, + ): [ + TDataTypes[TDataType]['getData'], + ( + | (( + newData: TDataTypes[TDataType]['setData'], + ) => Promise>) + | undefined + ), + boolean, + ] => { + // The data from the data provider at this selector + const [data, setDataInternal] = useState(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(true); + + // Wrap subscribe so we can call it as a normal PapiEvent in useEvent + const wrappedSubscribeEvent: PapiEventAsync | undefined = + useMemo( + () => + dataProvider + ? async (eventCallback: PapiEventHandler) => { + 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}` + ] as DataProviderSubscriber + )( + 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}` + ] as DataProviderSetter + )(selector, newData) + : undefined, + [dataProvider, selector], + ); + + return [data, setData, isLoading]; + }; + } + + // People can make whatever data hook they want + const useDataCachedHooks: UseDataHook = {}; + + // Note: the following comment uses @, not the actual @ character, to hackily provide @param and + // such on this object. JSDoc does not usually allow these on the object. One day, we may be able to + // put this comment on an actual function, so we can fix the comments back to using real @ + /** JSDOC SOURCE UseDataHook + * Special React hook that subscribes to run a callback on a data provider's data with specified + * selector on any data type that data provider serves. + * + * Usage: Specify the data type on the data provider with `useData.` and use like any other + * React hook. For example, `useData.Verse` lets you subscribe to verses from a data provider. + * + * @example When subscribing to JHN 11:35 on the `'quickVerse.quickVerse'` data provider, we need + * to tell the useData.Verse hook what types we are using, so we use the QuickVerseDataTypes and specify + * that we are using the 'Verse' data types as follows: + * ```typescript + * const [verseText, setVerseText, verseTextIsLoading] = useData.Verse( + * 'quickVerse.quickVerse', + * 'JHN 11:35', + * 'Verse text goes here', + * ); + * ``` + * + * @param `dataProviderSource` string name of data provider to get OR dataProvider (result of useDataProvider if you + * want to consolidate and only get the data provider once) + * + * @param `selector` tells the provider what data this listener is listening for + * + * @param `defaultValue` the initial value to return while first awaiting the data + * + * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be updated every render + * + * @param `subscriberOptions` various options to adjust how the subscriber emits updates + * + * WARNING: If provided, MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be updated every render + * @returns `[data, setData, isLoading]` + * - `data`: the current value for the data from the data provider with the specified data type and selector, either the defaultValue or the resolved data + * - `setData`: asynchronous function to request that the data provider update the data at this data type and selector. Returns true if successful. + * Note that this function does not update the data. The data provider sends out an update to this subscription if it successfully updates data. + * - `isLoading`: whether the data with the data type and selector is awaiting retrieval from the data provider + */ + 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; diff --git a/src/renderer/hooks/papi-hooks/index.ts b/src/renderer/hooks/papi-hooks/index.ts index 41903583d6..8b7b3ef531 100644 --- a/src/renderer/hooks/papi-hooks/index.ts +++ b/src/renderer/hooks/papi-hooks/index.ts @@ -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 './use-project-data-provider.hook'; // Declare an interface for the object we're exporting so that JSDoc comments propagate @@ -15,6 +16,7 @@ export interface PapiHooks { useDataProvider: typeof useDataProvider; /** JSDOC DESTINATION UseDataHook */ useData: typeof useData; + useProjectData: typeof useProjectData; useSetting: typeof useSetting; } @@ -28,6 +30,7 @@ const papiHooks: PapiHooks = { useProjectDataProvider, useDataProvider, useData, + useProjectData, useSetting, }; diff --git a/src/renderer/hooks/papi-hooks/use-data.hook.ts b/src/renderer/hooks/papi-hooks/use-data.hook.ts index a3f5080899..9493c77258 100644 --- a/src/renderer/hooks/papi-hooks/use-data.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-data.hook.ts @@ -1,120 +1,9 @@ -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 useDataProvider from '@renderer/hooks/papi-hooks/use-data-provider.hook'; -import { isString } from '@shared/utils/util'; +import createUseDataHook, { + UseDataHook, +} from '@renderer/hooks/hook-generators/create-use-data-hook.utils'; -type UseDataHook = { - [DataType in string]: ( - dataProviderSource: string | IDataProvider | undefined, - selector: TDataTypes[TDataType]['selector'], - defaultValue: TDataTypes[TDataType]['getData'], - subscriberOptions?: DataProviderSubscriberOptions, - ) => [ - TDataTypes[TDataType]['getData'], - ( - | (( - newData: TDataTypes[TDataType]['setData'], - ) => Promise>) - | undefined - ), - boolean, - ]; -}; - -function createUseDataHook(dataType: string) { - return ( - dataProviderSource: string | IDataProvider | undefined, - selector: TDataTypes[TDataType]['selector'], - defaultValue: TDataTypes[TDataType]['getData'], - subscriberOptions?: DataProviderSubscriberOptions, - ): [ - TDataTypes[TDataType]['getData'], - ( - | (( - newData: TDataTypes[TDataType]['setData'], - ) => Promise>) - | undefined - ), - boolean, - ] => { - // The data from the data provider at this selector - const [data, setDataInternal] = useState(defaultValue); - - // Get the data provider for this data provider name - const dataProvider = useDataProvider>(dataProviderSource); - - // Indicates if the data with the selector is awaiting retrieval from the data provider - const [isLoading, setIsLoading] = useState(true); - - // Wrap subscribe so we can call it as a normal PapiEvent in useEvent - const wrappedSubscribeEvent: PapiEventAsync | undefined = - useMemo( - () => - dataProvider - ? async (eventCallback: PapiEventHandler) => { - 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}` - ] as DataProviderSubscriber - )( - 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}` - ] as DataProviderSetter - )(selector, newData) - : undefined, - [dataProvider, selector], - ); - - return [data, setData, isLoading]; - }; -} - -// People can make whatever data hook they want -const useDataCachedHooks: UseDataHook = {}; +createUseDataHook(useDataProvider); // Note: the following comment uses @, not the actual @ character, to hackily provide @param and // such on this object. JSDoc does not usually allow these on the object. One day, we may be able to @@ -155,32 +44,6 @@ const useDataCachedHooks: UseDataHook = {}; * Note that this function does not update the data. The data provider sends out an update to this subscription if it successfully updates data. * - `isLoading`: whether the data with the data type and selector is awaiting retrieval from the data provider */ -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 = createUseDataHook(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'); - }, -}); +const useData: UseDataHook = createUseDataHook(useDataProvider); export default useData; diff --git a/src/renderer/hooks/papi-hooks/use-project-data.hook.ts b/src/renderer/hooks/papi-hooks/use-project-data.hook.ts new file mode 100644 index 0000000000..6194e7ec24 --- /dev/null +++ b/src/renderer/hooks/papi-hooks/use-project-data.hook.ts @@ -0,0 +1,39 @@ +import createUseDataHook from '@renderer/hooks/hook-generators/create-use-data-hook.utils'; +// import { ProjectTypes, ProjectDataTypes } from 'papi-shared-types'; +// import { +// DataProviderSubscriberOptions, +// DataProviderUpdateInstructions, +// } from '@shared/models/data-provider.model'; +import IDataProvider from '@shared/models/data-provider.interface'; +import useProjectDataProvider from '@renderer/hooks/papi-hooks/use-project-data-provider.hook'; + +// type stuff = ProjectDataTypes['MyExtensionProjectTypeName']['ExtensionData']['getData']; + +// type UseProjectDataHook = { +// [DataType in string]: < +// ProjectType extends ProjectTypes, +// TDataType extends keyof ProjectDataTypes[ProjectType], +// >( +// projectDataProviderSource: string | IDataProvider | undefined, +// selector: ProjectDataTypes[ProjectType][TDataType]['selector'], +// defaultValue: ProjectDataTypes[ProjectType][TDataType]['getData'], +// subscriberOptions?: DataProviderSubscriberOptions, +// ) => [ +// ProjectDataTypes[ProjectType][TDataType]['getData'], +// ( +// | (( +// newData: ProjectDataTypes[ProjectType][TDataType]['setData'], +// ) => Promise>) +// | undefined +// ), +// boolean, +// ]; +// }; + +const useProjectData = createUseDataHook( + useProjectDataProvider as ( + dataProviderSource: string | IDataProvider | undefined, + ) => IDataProvider | undefined, +); + +export default useProjectData; From e14e7c645a737150e79e66c7fd38d7b2cf65be19 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Wed, 18 Oct 2023 12:09:51 -0500 Subject: [PATCH 2/2] Tuned up types and JSDocs for useProjectData and friends --- .../web-views/hello-world.web-view.tsx | 13 + lib/papi-dts/papi.d.ts | 273 +++++++++++++++++- src/declarations/papi-shared-types.ts | 4 +- ....utils.ts => create-use-data-hook.util.ts} | 53 +--- src/renderer/hooks/papi-hooks/index.ts | 1 + .../papi-hooks/use-data-provider.hook.ts | 1 - .../hooks/papi-hooks/use-data.hook.ts | 35 ++- .../use-project-data-provider.hook.ts | 5 +- .../hooks/papi-hooks/use-project-data.hook.ts | 151 ++++++++-- 9 files changed, 442 insertions(+), 94 deletions(-) rename src/renderer/hooks/hook-generators/{create-use-data-hook.utils.ts => create-use-data-hook.util.ts} (70%) diff --git a/extensions/src/hello-world/web-views/hello-world.web-view.tsx b/extensions/src/hello-world/web-views/hello-world.web-view.tsx index 361b13b864..fcc3b45aa4 100644 --- a/extensions/src/hello-world/web-views/hello-world.web-view.tsx +++ b/extensions/src/hello-world/web-views/hello-world.web-view.tsx @@ -63,6 +63,10 @@ globalThis.webViewComponent = function HelloWorld() { const [rows, setRows] = useState(initializeRows()); const [selectedRows, setSelectedRows] = useState(new Set()); 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( @@ -110,6 +114,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( + '32664dc3288a28df2e2bb75ded887fc8f17a15fb', + verseRef, + 'Loading WEB Verse', + ); */ + return (
@@ -151,6 +162,8 @@ globalThis.webViewComponent = function HelloWorld() {
{john11}

Psalm 1

{psalm1}
+ {/*

{verseRef.toString()} WEB

+
{webVerse}
*/}
diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index a861f77828..371d108100 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -2551,15 +2551,27 @@ declare module 'renderer/hooks/papi-hooks/use-data-provider.hook' { ) => T | undefined; export default useDataProvider; } -declare module 'renderer/hooks/hook-generators/create-use-data-hook.utils' { +declare module 'renderer/hooks/hook-generators/create-use-data-hook.util' { import { DataProviderDataTypes, DataProviderSubscriberOptions, DataProviderUpdateInstructions, } from 'shared/models/data-provider.model'; import IDataProvider from 'shared/models/data-provider.interface'; + /** + * Proxy object that provides hooks to use data provider data with various data types + * + * @example `useData.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.` + */ export type UseDataHook = { - [DataType in string]: ( + [DataType in string]: < + TDataTypes extends DataProviderDataTypes, + TDataType extends keyof TDataTypes, + >( dataProviderSource: string | IDataProvider | undefined, selector: TDataTypes[TDataType]['selector'], defaultValue: TDataTypes[TDataType]['getData'], @@ -2583,17 +2595,37 @@ declare module 'renderer/hooks/hook-generators/create-use-data-hook.utils' { export default createUseDataHook; } declare module 'renderer/hooks/papi-hooks/use-data.hook' { - import { UseDataHook } from 'renderer/hooks/hook-generators/create-use-data-hook.utils'; + import { UseDataHook } from 'renderer/hooks/hook-generators/create-use-data-hook.util'; /** + * ```typescript + * useData.DataType( + * dataProviderSource: string | IDataProvider | undefined, + * selector: TDataTypes[TDataType]['selector'], + * defaultValue: TDataTypes[TDataType]['getData'], + * subscriberOptions?: DataProviderSubscriberOptions, + * ): [ + * TDataTypes[TDataType]['getData'], + * ( + * | (( + * newData: TDataTypes[TDataType]['setData'], + * ) => Promise>) + * | undefined + * ), + * boolean, + * ] + * ``` + * * Special React hook that subscribes to run a callback on a data provider's data with specified * selector on any data type that data provider serves. * * Usage: Specify the data type on the data provider with `useData.` and use like any other - * React hook. For example, `useData.Verse` lets you subscribe to verses from a data provider. + * React hook. Specify the generic types in order to receive type support from Intellisense. For + * example, `useData.Verse` lets you subscribe to verses from a data + * provider that serves `QuickVerseDataTypes`. * * @example When subscribing to JHN 11:35 on the `'quickVerse.quickVerse'` data provider, we need - * to tell the useData.Verse hook what types we are using, so we use the QuickVerseDataTypes and specify - * that we are using the 'Verse' data types as follows: + * to tell the useData.Verse hook what types we are using, so we use the `QuickVerseDataTypes` data + * types and specify that we are using the `'Verse'` data type as follows: * ```typescript * const [verseText, setVerseText, verseTextIsLoading] = useData.Verse( * 'quickVerse.quickVerse', @@ -2619,6 +2651,11 @@ declare module 'renderer/hooks/papi-hooks/use-data.hook' { * - `setData`: asynchronous function to request that the data provider update the data at this data type and selector. Returns true if successful. * Note that this function does not update the data. The data provider sends out an update to this subscription if it successfully updates data. * - `isLoading`: whether the data with the data type and selector is awaiting retrieval from the data provider + * + * @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.` */ const useData: UseDataHook; export default useData; @@ -2704,8 +2741,8 @@ declare module 'renderer/hooks/papi-hooks/use-project-data-provider.hook' { * 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. + * @ProjectType `ProjectType` - 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: ( projectDataProviderSource: string | IDataProvider | undefined, @@ -2713,7 +2750,123 @@ declare module 'renderer/hooks/papi-hooks/use-project-data-provider.hook' { export default useProjectDataProvider; } declare module 'renderer/hooks/papi-hooks/use-project-data.hook' { - const useProjectData: import('renderer/hooks/hook-generators/create-use-data-hook.utils').UseDataHook; + import { + DataProviderDataTypes, + DataProviderSubscriberOptions, + DataProviderUpdateInstructions, + } from 'shared/models/data-provider.model'; + import IDataProvider from 'shared/models/data-provider.interface'; + /** + * Proxy object that provides hooks to use project data provider data with various data types + * + * @example `useProjectData.VerseUSFM(...)` + * + * @type `TProjectDataTypes` - the project data types associated with the `projectType` used. You + * can specify this type with `ProjectDataTypes['']` + * @type `TDataType` - the specific data type on this project you want to use. Must match the data + * type specified in `useProjectData.` + * + * Unfortunately, accessing properties from the `DataProviderDataType`s in `ProjectDataTypes` did + * not work. Maybe TypeScript refuses to look at all properties in each member of `ProjectDataTypes` + * and tell that they're all `DataProviderDataType`s. So this did not work because it refused to + * accept that `'selector'` was a valid member: + * `ProjectDataTypes[ProjectType][TDataType]['selector']` + * + * As such, this hook proxy actually has the same types as `UseDataHook` but with a couple of + * things renamed for easier readability. + */ + type UseProjectDataHook = { + [DataType in string]: < + TProjectDataTypes extends DataProviderDataTypes, + TDataType extends keyof TProjectDataTypes, + >( + projectDataProviderSource: string | IDataProvider | undefined, + selector: TProjectDataTypes[TDataType]['selector'], + defaultValue: TProjectDataTypes[TDataType]['getData'], + subscriberOptions?: DataProviderSubscriberOptions, + ) => [ + TProjectDataTypes[TDataType]['getData'], + ( + | (( + newData: TProjectDataTypes[TDataType]['setData'], + ) => Promise>) + | undefined + ), + boolean, + ]; + }; + /** + * ```typescript + * useProjectData.DataType( + * projectDataProviderSource: string | IDataProvider | undefined, + * selector: TProjectDataTypes[TDataType]['selector'], + * defaultValue: TProjectDataTypes[TDataType]['getData'], + * subscriberOptions?: DataProviderSubscriberOptions, + * ): [ + * TProjectDataTypes[TDataType]['getData'], + * ( + * | (( + * newData: TProjectDataTypes[TDataType]['setData'], + * ) => Promise>) + * | undefined + * ), + * boolean, + * ] + * ``` + * + * Special React hook that subscribes to run a callback on a project data provider's data with + * specified selector on any data type that the project data provider serves according to its + * projectType. + * + * Usage: Specify the data type on the project data provider with `useProjectData.` and + * use like any other React hook. Specify the generic types in order to receive type + * support from Intellisense. For example, `useProjectData.VerseUSFM` + * lets you subscribe to verse USFM from a project data provider for the `ParatextStandard` + * `projectType`. + * + * @example When subscribing to JHN 11:35 Verse USFM info on a `ParatextStandard` project with + * projectId `32664dc3288a28df2e2bb75ded887fc8f17a15fb`, we need to tell the + * `useProjectData.VerseUSFM` hook what types we are using, so we specify the project data types as + * `ProjectDataTypes['ParatextStandard']` and specify that we are using the `'VerseUSFM'` data type + * as follows: + * ```typescript + * const [verse, setVerse, verseIsLoading] = useProjectData.VerseUSFM( + * '32664dc3288a28df2e2bb75ded887fc8f17a15fb', + * useMemo(() => new VerseRef('JHN', '11', '35', ScrVers.English), []), + * 'Loading verse ', + * ); + * ``` + * + * @param `projectDataProviderSource` string name of the id of the project to get OR + * projectDataProvider (result of useProjectDataProvider if you + * want to consolidate and only get the project data provider once) + * + * @param `selector` tells the provider what data this listener is listening for + * + * @param `defaultValue` the initial value to return while first awaiting the data + * + * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be updated every render + * + * @param `subscriberOptions` various options to adjust how the subscriber emits updates + * + * WARNING: If provided, MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be updated every render + * @returns `[data, setData, isLoading]` + * - `data`: the current value for the data from the project data provider for the specified + * project id with the specified data type and selector, either the defaultValue or the resolved + * data + * - `setData`: asynchronous function to request that the data provider for the specified project + * id update the data at this data type and selector. Returns true if successful. + * Note that this function does not update the data. The project data provider sends out an + * update to this subscription if it successfully updates data. + * - `isLoading`: whether the data with the data type and selector is awaiting retrieval from the + * project data provider for this project id + * + * @type `TProjectDataTypes` - the project data types associated with the `projectType` used. You + * can specify this type with `ProjectDataTypes['']` + * @type `TDataType` - the specific data type on this project you want to use. Must match the data + * type specified in `useProjectData.` + */ + const useProjectData: UseProjectDataHook; export default useProjectData; } declare module 'renderer/hooks/papi-hooks/index' { @@ -2732,15 +2885,35 @@ declare module 'renderer/hooks/papi-hooks/index' { useProjectDataProvider: typeof useProjectDataProvider; useDataProvider: typeof useDataProvider; /** + * ```typescript + * useData.DataType( + * dataProviderSource: string | IDataProvider | undefined, + * selector: TDataTypes[TDataType]['selector'], + * defaultValue: TDataTypes[TDataType]['getData'], + * subscriberOptions?: DataProviderSubscriberOptions, + * ): [ + * TDataTypes[TDataType]['getData'], + * ( + * | (( + * newData: TDataTypes[TDataType]['setData'], + * ) => Promise>) + * | undefined + * ), + * boolean, + * ] + * ``` + * * Special React hook that subscribes to run a callback on a data provider's data with specified * selector on any data type that data provider serves. * * Usage: Specify the data type on the data provider with `useData.` and use like any other - * React hook. For example, `useData.Verse` lets you subscribe to verses from a data provider. + * React hook. Specify the generic types in order to receive type support from Intellisense. For + * example, `useData.Verse` lets you subscribe to verses from a data + * provider that serves `QuickVerseDataTypes`. * * @example When subscribing to JHN 11:35 on the `'quickVerse.quickVerse'` data provider, we need - * to tell the useData.Verse hook what types we are using, so we use the QuickVerseDataTypes and specify - * that we are using the 'Verse' data types as follows: + * to tell the useData.Verse hook what types we are using, so we use the `QuickVerseDataTypes` data + * types and specify that we are using the `'Verse'` data type as follows: * ```typescript * const [verseText, setVerseText, verseTextIsLoading] = useData.Verse( * 'quickVerse.quickVerse', @@ -2766,8 +2939,84 @@ declare module 'renderer/hooks/papi-hooks/index' { * - `setData`: asynchronous function to request that the data provider update the data at this data type and selector. Returns true if successful. * Note that this function does not update the data. The data provider sends out an update to this subscription if it successfully updates data. * - `isLoading`: whether the data with the data type and selector is awaiting retrieval from the data provider + * + * @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.` */ useData: typeof useData; + /** + * ```typescript + * useProjectData.DataType( + * projectDataProviderSource: string | IDataProvider | undefined, + * selector: TProjectDataTypes[TDataType]['selector'], + * defaultValue: TProjectDataTypes[TDataType]['getData'], + * subscriberOptions?: DataProviderSubscriberOptions, + * ): [ + * TProjectDataTypes[TDataType]['getData'], + * ( + * | (( + * newData: TProjectDataTypes[TDataType]['setData'], + * ) => Promise>) + * | undefined + * ), + * boolean, + * ] + * ``` + * + * Special React hook that subscribes to run a callback on a project data provider's data with + * specified selector on any data type that the project data provider serves according to its + * projectType. + * + * Usage: Specify the data type on the project data provider with `useProjectData.` and + * use like any other React hook. Specify the generic types in order to receive type + * support from Intellisense. For example, `useProjectData.VerseUSFM` + * lets you subscribe to verse USFM from a project data provider for the `ParatextStandard` + * `projectType`. + * + * @example When subscribing to JHN 11:35 Verse USFM info on a `ParatextStandard` project with + * projectId `32664dc3288a28df2e2bb75ded887fc8f17a15fb`, we need to tell the + * `useProjectData.VerseUSFM` hook what types we are using, so we specify the project data types as + * `ProjectDataTypes['ParatextStandard']` and specify that we are using the `'VerseUSFM'` data type + * as follows: + * ```typescript + * const [verse, setVerse, verseIsLoading] = useProjectData.VerseUSFM( + * '32664dc3288a28df2e2bb75ded887fc8f17a15fb', + * useMemo(() => new VerseRef('JHN', '11', '35', ScrVers.English), []), + * 'Loading verse ', + * ); + * ``` + * + * @param `projectDataProviderSource` string name of the id of the project to get OR + * projectDataProvider (result of useProjectDataProvider if you + * want to consolidate and only get the project data provider once) + * + * @param `selector` tells the provider what data this listener is listening for + * + * @param `defaultValue` the initial value to return while first awaiting the data + * + * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be updated every render + * + * @param `subscriberOptions` various options to adjust how the subscriber emits updates + * + * WARNING: If provided, MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be updated every render + * @returns `[data, setData, isLoading]` + * - `data`: the current value for the data from the project data provider for the specified + * project id with the specified data type and selector, either the defaultValue or the resolved + * data + * - `setData`: asynchronous function to request that the data provider for the specified project + * id update the data at this data type and selector. Returns true if successful. + * Note that this function does not update the data. The project data provider sends out an + * update to this subscription if it successfully updates data. + * - `isLoading`: whether the data with the data type and selector is awaiting retrieval from the + * project data provider for this project id + * + * @type `TProjectDataTypes` - the project data types associated with the `projectType` used. You + * can specify this type with `ProjectDataTypes['']` + * @type `TDataType` - the specific data type on this project you want to use. Must match the data + * type specified in `useProjectData.` + */ useProjectData: typeof useProjectData; useSetting: typeof useSetting; } diff --git a/src/declarations/papi-shared-types.ts b/src/declarations/papi-shared-types.ts index 20a8ae5621..5adf489471 100644 --- a/src/declarations/papi-shared-types.ts +++ b/src/declarations/papi-shared-types.ts @@ -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; } diff --git a/src/renderer/hooks/hook-generators/create-use-data-hook.utils.ts b/src/renderer/hooks/hook-generators/create-use-data-hook.util.ts similarity index 70% rename from src/renderer/hooks/hook-generators/create-use-data-hook.utils.ts rename to src/renderer/hooks/hook-generators/create-use-data-hook.util.ts index 092a70ac6d..bf3a948973 100644 --- a/src/renderer/hooks/hook-generators/create-use-data-hook.utils.ts +++ b/src/renderer/hooks/hook-generators/create-use-data-hook.util.ts @@ -12,8 +12,20 @@ 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(...)` + * + * @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.` + */ export type UseDataHook = { - [DataType in string]: ( + [DataType in string]: < + TDataTypes extends DataProviderDataTypes, + TDataType extends keyof TDataTypes, + >( dataProviderSource: string | IDataProvider | undefined, selector: TDataTypes[TDataType]['selector'], defaultValue: TDataTypes[TDataType]['getData'], @@ -124,45 +136,6 @@ function createUseDataHook( // People can make whatever data hook they want const useDataCachedHooks: UseDataHook = {}; - // Note: the following comment uses @, not the actual @ character, to hackily provide @param and - // such on this object. JSDoc does not usually allow these on the object. One day, we may be able to - // put this comment on an actual function, so we can fix the comments back to using real @ - /** JSDOC SOURCE UseDataHook - * Special React hook that subscribes to run a callback on a data provider's data with specified - * selector on any data type that data provider serves. - * - * Usage: Specify the data type on the data provider with `useData.` and use like any other - * React hook. For example, `useData.Verse` lets you subscribe to verses from a data provider. - * - * @example When subscribing to JHN 11:35 on the `'quickVerse.quickVerse'` data provider, we need - * to tell the useData.Verse hook what types we are using, so we use the QuickVerseDataTypes and specify - * that we are using the 'Verse' data types as follows: - * ```typescript - * const [verseText, setVerseText, verseTextIsLoading] = useData.Verse( - * 'quickVerse.quickVerse', - * 'JHN 11:35', - * 'Verse text goes here', - * ); - * ``` - * - * @param `dataProviderSource` string name of data provider to get OR dataProvider (result of useDataProvider if you - * want to consolidate and only get the data provider once) - * - * @param `selector` tells the provider what data this listener is listening for - * - * @param `defaultValue` the initial value to return while first awaiting the data - * - * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be updated every render - * - * @param `subscriberOptions` various options to adjust how the subscriber emits updates - * - * WARNING: If provided, MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be updated every render - * @returns `[data, setData, isLoading]` - * - `data`: the current value for the data from the data provider with the specified data type and selector, either the defaultValue or the resolved data - * - `setData`: asynchronous function to request that the data provider update the data at this data type and selector. Returns true if successful. - * Note that this function does not update the data. The data provider sends out an update to this subscription if it successfully updates data. - * - `isLoading`: whether the data with the data type and selector is awaiting retrieval from the data provider - */ const useData: UseDataHook = new Proxy(useDataCachedHooks, { get(obj, prop) { // Pass promises through diff --git a/src/renderer/hooks/papi-hooks/index.ts b/src/renderer/hooks/papi-hooks/index.ts index 8b7b3ef531..6f0abf60b7 100644 --- a/src/renderer/hooks/papi-hooks/index.ts +++ b/src/renderer/hooks/papi-hooks/index.ts @@ -16,6 +16,7 @@ export interface PapiHooks { useDataProvider: typeof useDataProvider; /** JSDOC DESTINATION UseDataHook */ useData: typeof useData; + /** JSDOC DESTINATION UseProjectDataHook */ useProjectData: typeof useProjectData; useSetting: typeof useSetting; } diff --git a/src/renderer/hooks/papi-hooks/use-data-provider.hook.ts b/src/renderer/hooks/papi-hooks/use-data-provider.hook.ts index 8286607553..77d2d01c88 100644 --- a/src/renderer/hooks/papi-hooks/use-data-provider.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-data-provider.hook.ts @@ -13,7 +13,6 @@ import createUseNetworkObjectHook from '@renderer/hooks/hook-generators/create-u * @type `T` - the type of data provider to return. Use `IDataProvider`, * 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 diff --git a/src/renderer/hooks/papi-hooks/use-data.hook.ts b/src/renderer/hooks/papi-hooks/use-data.hook.ts index 9493c77258..850e853607 100644 --- a/src/renderer/hooks/papi-hooks/use-data.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-data.hook.ts @@ -1,23 +1,41 @@ import useDataProvider from '@renderer/hooks/papi-hooks/use-data-provider.hook'; import createUseDataHook, { UseDataHook, -} from '@renderer/hooks/hook-generators/create-use-data-hook.utils'; - -createUseDataHook(useDataProvider); +} from '@renderer/hooks/hook-generators/create-use-data-hook.util'; // Note: the following comment uses @, not the actual @ character, to hackily provide @param and // such on this object. JSDoc does not usually allow these on the object. One day, we may be able to // put this comment on an actual function, so we can fix the comments back to using real @ /** JSDOC SOURCE UseDataHook + * ```typescript + * useData.DataType( + * dataProviderSource: string | IDataProvider | undefined, + * selector: TDataTypes[TDataType]['selector'], + * defaultValue: TDataTypes[TDataType]['getData'], + * subscriberOptions?: DataProviderSubscriberOptions, + * ): [ + * TDataTypes[TDataType]['getData'], + * ( + * | (( + * newData: TDataTypes[TDataType]['setData'], + * ) => Promise>) + * | undefined + * ), + * boolean, + * ] + * ``` + * * Special React hook that subscribes to run a callback on a data provider's data with specified * selector on any data type that data provider serves. * * Usage: Specify the data type on the data provider with `useData.` and use like any other - * React hook. For example, `useData.Verse` lets you subscribe to verses from a data provider. + * React hook. Specify the generic types in order to receive type support from Intellisense. For + * example, `useData.Verse` lets you subscribe to verses from a data + * provider that serves `QuickVerseDataTypes`. * * @example When subscribing to JHN 11:35 on the `'quickVerse.quickVerse'` data provider, we need - * to tell the useData.Verse hook what types we are using, so we use the QuickVerseDataTypes and specify - * that we are using the 'Verse' data types as follows: + * to tell the useData.Verse hook what types we are using, so we use the `QuickVerseDataTypes` data + * types and specify that we are using the `'Verse'` data type as follows: * ```typescript * const [verseText, setVerseText, verseTextIsLoading] = useData.Verse( * 'quickVerse.quickVerse', @@ -43,6 +61,11 @@ createUseDataHook(useDataProvider); * - `setData`: asynchronous function to request that the data provider update the data at this data type and selector. Returns true if successful. * Note that this function does not update the data. The data provider sends out an update to this subscription if it successfully updates data. * - `isLoading`: whether the data with the data type and selector is awaiting retrieval from the data provider + * + * @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.` */ const useData: UseDataHook = createUseDataHook(useDataProvider); diff --git a/src/renderer/hooks/papi-hooks/use-project-data-provider.hook.ts b/src/renderer/hooks/papi-hooks/use-project-data-provider.hook.ts index 1508b18894..d320e1552d 100644 --- a/src/renderer/hooks/papi-hooks/use-project-data-provider.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-project-data-provider.hook.ts @@ -11,10 +11,9 @@ import IDataProvider from '@shared/models/data-provider.interface'; * 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. + * @ProjectType `ProjectType` - 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 = createUseNetworkObjectHook( papiFrontendProjectDataProviderService.getProjectDataProvider, ) as ( diff --git a/src/renderer/hooks/papi-hooks/use-project-data.hook.ts b/src/renderer/hooks/papi-hooks/use-project-data.hook.ts index 6194e7ec24..73ad94e970 100644 --- a/src/renderer/hooks/papi-hooks/use-project-data.hook.ts +++ b/src/renderer/hooks/papi-hooks/use-project-data.hook.ts @@ -1,39 +1,130 @@ -import createUseDataHook from '@renderer/hooks/hook-generators/create-use-data-hook.utils'; -// import { ProjectTypes, ProjectDataTypes } from 'papi-shared-types'; -// import { -// DataProviderSubscriberOptions, -// DataProviderUpdateInstructions, -// } from '@shared/models/data-provider.model'; +import createUseDataHook from '@renderer/hooks/hook-generators/create-use-data-hook.util'; +import { + DataProviderDataTypes, + DataProviderSubscriberOptions, + DataProviderUpdateInstructions, +} from '@shared/models/data-provider.model'; import IDataProvider from '@shared/models/data-provider.interface'; import useProjectDataProvider from '@renderer/hooks/papi-hooks/use-project-data-provider.hook'; -// type stuff = ProjectDataTypes['MyExtensionProjectTypeName']['ExtensionData']['getData']; +/** + * Proxy object that provides hooks to use project data provider data with various data types + * + * @example `useProjectData.VerseUSFM(...)` + * + * @type `TProjectDataTypes` - the project data types associated with the `projectType` used. You + * can specify this type with `ProjectDataTypes['']` + * @type `TDataType` - the specific data type on this project you want to use. Must match the data + * type specified in `useProjectData.` + * + * Unfortunately, accessing properties from the `DataProviderDataType`s in `ProjectDataTypes` did + * not work. Maybe TypeScript refuses to look at all properties in each member of `ProjectDataTypes` + * and tell that they're all `DataProviderDataType`s. So this did not work because it refused to + * accept that `'selector'` was a valid member: + * `ProjectDataTypes[ProjectType][TDataType]['selector']` + * + * As such, this hook proxy actually has the same types as `UseDataHook` but with a couple of + * things renamed for easier readability. + */ +type UseProjectDataHook = { + [DataType in string]: < + TProjectDataTypes extends DataProviderDataTypes, + TDataType extends keyof TProjectDataTypes, + >( + projectDataProviderSource: string | IDataProvider | undefined, + selector: TProjectDataTypes[TDataType]['selector'], + defaultValue: TProjectDataTypes[TDataType]['getData'], + subscriberOptions?: DataProviderSubscriberOptions, + ) => [ + TProjectDataTypes[TDataType]['getData'], + ( + | (( + newData: TProjectDataTypes[TDataType]['setData'], + ) => Promise>) + | undefined + ), + boolean, + ]; +}; -// type UseProjectDataHook = { -// [DataType in string]: < -// ProjectType extends ProjectTypes, -// TDataType extends keyof ProjectDataTypes[ProjectType], -// >( -// projectDataProviderSource: string | IDataProvider | undefined, -// selector: ProjectDataTypes[ProjectType][TDataType]['selector'], -// defaultValue: ProjectDataTypes[ProjectType][TDataType]['getData'], -// subscriberOptions?: DataProviderSubscriberOptions, -// ) => [ -// ProjectDataTypes[ProjectType][TDataType]['getData'], -// ( -// | (( -// newData: ProjectDataTypes[ProjectType][TDataType]['setData'], -// ) => Promise>) -// | undefined -// ), -// boolean, -// ]; -// }; - -const useProjectData = createUseDataHook( +// Note: the following comment uses @, not the actual @ character, to hackily provide @param and +// such on this object. JSDoc does not usually allow these on the object. One day, we may be able to +// put this comment on an actual function, so we can fix the comments back to using real @ +/** JSDOC SOURCE UseProjectDataHook + * ```typescript + * useProjectData.DataType( + * projectDataProviderSource: string | IDataProvider | undefined, + * selector: TProjectDataTypes[TDataType]['selector'], + * defaultValue: TProjectDataTypes[TDataType]['getData'], + * subscriberOptions?: DataProviderSubscriberOptions, + * ): [ + * TProjectDataTypes[TDataType]['getData'], + * ( + * | (( + * newData: TProjectDataTypes[TDataType]['setData'], + * ) => Promise>) + * | undefined + * ), + * boolean, + * ] + * ``` + * + * Special React hook that subscribes to run a callback on a project data provider's data with + * specified selector on any data type that the project data provider serves according to its + * projectType. + * + * Usage: Specify the data type on the project data provider with `useProjectData.` and + * use like any other React hook. Specify the generic types in order to receive type + * support from Intellisense. For example, `useProjectData.VerseUSFM` + * lets you subscribe to verse USFM from a project data provider for the `ParatextStandard` + * `projectType`. + * + * @example When subscribing to JHN 11:35 Verse USFM info on a `ParatextStandard` project with + * projectId `32664dc3288a28df2e2bb75ded887fc8f17a15fb`, we need to tell the + * `useProjectData.VerseUSFM` hook what types we are using, so we specify the project data types as + * `ProjectDataTypes['ParatextStandard']` and specify that we are using the `'VerseUSFM'` data type + * as follows: + * ```typescript + * const [verse, setVerse, verseIsLoading] = useProjectData.VerseUSFM( + * '32664dc3288a28df2e2bb75ded887fc8f17a15fb', + * useMemo(() => new VerseRef('JHN', '11', '35', ScrVers.English), []), + * 'Loading verse ', + * ); + * ``` + * + * @param `projectDataProviderSource` string name of the id of the project to get OR + * projectDataProvider (result of useProjectDataProvider if you + * want to consolidate and only get the project data provider once) + * + * @param `selector` tells the provider what data this listener is listening for + * + * @param `defaultValue` the initial value to return while first awaiting the data + * + * WARNING: MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be updated every render + * + * @param `subscriberOptions` various options to adjust how the subscriber emits updates + * + * WARNING: If provided, MUST BE STABLE - const or wrapped in useState, useMemo, etc. The reference must not be updated every render + * @returns `[data, setData, isLoading]` + * - `data`: the current value for the data from the project data provider for the specified + * project id with the specified data type and selector, either the defaultValue or the resolved + * data + * - `setData`: asynchronous function to request that the data provider for the specified project + * id update the data at this data type and selector. Returns true if successful. + * Note that this function does not update the data. The project data provider sends out an + * update to this subscription if it successfully updates data. + * - `isLoading`: whether the data with the data type and selector is awaiting retrieval from the + * project data provider for this project id + * + * @type `TProjectDataTypes` - the project data types associated with the `projectType` used. You + * can specify this type with `ProjectDataTypes['']` + * @type `TDataType` - the specific data type on this project you want to use. Must match the data + * type specified in `useProjectData.` + */ +const useProjectData: UseProjectDataHook = createUseDataHook( useProjectDataProvider as ( dataProviderSource: string | IDataProvider | undefined, ) => IDataProvider | undefined, -); +) as UseProjectDataHook; export default useProjectData;