Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create Data Provider API #122

Merged
merged 42 commits into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
bfb2510
Added DataProviderService and example data provider
tjcouch-sil Mar 21, 2023
e5ca8af
Changed arrow functions to class methods to make sure either way works
tjcouch-sil Mar 24, 2023
11c667f
Split off IDataProviderEngine that extensions implement to provide da…
tjcouch-sil Mar 24, 2023
c9df9cf
Started working on selector events
tjcouch-sil Mar 28, 2023
50496e3
Revert "Started working on selector events" - trying another method to
tjcouch-sil Mar 28, 2023
3b384bb
Added subscribe to DataProvider for getting data updates
tjcouch-sil Mar 30, 2023
79247bd
Stopped data provider update events from irrelevant data changes by d…
tjcouch-sil Mar 31, 2023
af582d2
Added options to configure data provider subscription, fixed greeting…
tjcouch-sil Mar 31, 2023
84419fd
Fixed usePromise not working with returning functions
tjcouch-sil Mar 31, 2023
c409abe
Added useEventAsync for async (un)subscribers, allow undefined passed…
tjcouch-sil Mar 31, 2023
e6fe83a
Added first-pass useData hook
tjcouch-sil Mar 31, 2023
e01e77c
Added Quick Verse data provider, test useData hook
tjcouch-sil Mar 31, 2023
911839b
Updated useData so it returns similar to useState, added default valu…
tjcouch-sil Mar 31, 2023
ab8997d
Added isLoading on useData so React knows when the data is correct
tjcouch-sil Mar 31, 2023
cbf917b
Debounced verse reference input, subscribed to quick verse updates on…
tjcouch-sil Apr 1, 2023
d370c16
Added latest-edited scripture selector to quick verse
tjcouch-sil Apr 1, 2023
b56f345
Added IDataProviderEngine.forceUpdate which allows the DPE to choose …
tjcouch-sil Apr 1, 2023
d6da885
Split Data Provider TData into TGetData and TSetData because I alread…
tjcouch-sil Apr 1, 2023
1fd1c95
Split useDataProvider out into a new hook from useData, tested arbitr…
tjcouch-sil Apr 1, 2023
21c8f6d
Changed GreetingsDataProvider to an object for testing, other code re…
tjcouch-sil Apr 3, 2023
ed80cbd
Fixed couple of deep copy update bugs, misc code review tweaks
tjcouch-sil Apr 3, 2023
7ba2475
Reworked data provider subscriber options, other code review changes
tjcouch-sil Apr 6, 2023
39d43b4
Reworked data provider subscriber options, other code review changes …
tjcouch-sil Apr 6, 2023
6dde1b0
Layered over DataProviderEngine.set to support calling it internally,…
tjcouch-sil Apr 6, 2023
8b1d642
Prevent people who have data provider from running notifyUpdate. Only…
tjcouch-sil Apr 6, 2023
513f9b5
Prevent people who have data provider from running notifyUpdate. Only…
tjcouch-sil Apr 6, 2023
3bd919f
Added notifyUpdate return like set, code review tweaks
tjcouch-sil Apr 10, 2023
9956582
Fixed empty object not triggering subscriber updates on first update,…
tjcouch-sil Apr 10, 2023
1000fcc
Refactored useDataProvider so it now accepts its own return
tjcouch-sil Apr 10, 2023
217488f
Improved documentation for useDataProvider and useData
tjcouch-sil Apr 10, 2023
cb86ffc
Made network objects more thread-safe by using the request registrati…
tjcouch-sil Apr 11, 2023
09748ec
Stopped returning isDisposed from useDataProvider hook
tjcouch-sil Apr 11, 2023
2420400
Added useDataProvider to papi.react.hooks
tjcouch-sil Apr 11, 2023
23bd230
Improved JSDocs for useData and useDataProvider
tjcouch-sil Apr 11, 2023
8090ce8
Memozied NetworkObjectService.get when sending promises so it does no…
tjcouch-sil Apr 11, 2023
66f53e5
Removed getResourcesPath command to stop that annoying pop-up error
tjcouch-sil Apr 11, 2023
0a964a8
Added check to make sure we aren't making multiple of the same networ…
tjcouch-sil Apr 11, 2023
67c1742
Merge remote-tracking branch 'origin/main' into 59-data-provider
tjcouch-sil Apr 12, 2023
d9d5483
Renamed data provider files to match new convention, other lint fixes
tjcouch-sil Apr 12, 2023
915bcbc
Fixed types on papi react stuff, fixed Text Field not being controllable
tjcouch-sil Apr 12, 2023
201b50e
Added MUI credit
tjcouch-sil Apr 12, 2023
4bb2809
Changed logger.log to logger.info, enforced papi react imports as typ…
tjcouch-sil Apr 12, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"paranext",
"proxied",
"Steenwyk",
"stringifiable",
"Unregistering",
"unregisters",
"unsub",
Expand Down
149 changes: 148 additions & 1 deletion extensions/hello-someone/hello-someone.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,154 @@ logger.log('Hello Someone is importing!');

const unsubscribers = [];

const greetingsDataProviderEngine = {
people: {
bill: 'Hi, my name is Bill!',
kathy: 'Hello. My name is Kathy.',
},
/**
* @param {string} selector
* @param {string} data
*/
set: async (selector, data) => {
// Don't change everyone's greeting, you heathen!
if (selector === '*') return false;

// If there is no change in the greeting, don't update
if (data === greetingsDataProviderEngine.people[selector.toLowerCase()])
return false;

// Update the greeting and send an update
greetingsDataProviderEngine.people[selector.toLowerCase()] = data;
return true;
},

/**
* @param {string} selector
*/
async get(selector) {
if (selector === '*') return this.people;
return this.people[selector.toLowerCase()];
},

/** Test method to make sure people can use data providers' custom methods */
// eslint-disable-next-line class-methods-use-this
testRandomMethod: async (things) => {
const result = `Greetings data provider got testRandomMethod! ${things}`;
logger.info(result);
return result;
},
};

exports.activate = async () => {
logger.log('Hello Someone is activating!');

const greetingsDataProviderInfo = papi.dataProvider.registerEngine(
'hello-someone.greetings',
greetingsDataProviderEngine,
);

papi.webViews.addWebView({
contentType: 'html',
contents: `<html>
<head>
</head>
<body>
<div>Hello Someone!!</div>
<div><input id="greetings-input" value="Bill" /><button id="greetings-button" type="button">Greet</button></div>
<div id="greetings-button-output"></div>
<div><input id="set-greetings-input" value="Hey, my name is Bill. I got set by the webview!" /><button id="set-greetings-button" type="button">Set Greeting</button></div>
<div id="all-greetings"></div>
<div id="any-greetings-update-count">Any Greetings Updates: 0</div>
<div id="bill-any-greetings-update-count">Any Greetings Updates (via Bill): 0</div>
<div id="bill-greetings-update-count">Bill Greetings Updates: 0</div>
<script>
// Enable webview debugging
console.debug('Debug Hello Someone WebView');

function print(input) {
papi.logger.log(input);
}

const dataProviderInfoPromise = papi.dataProvider.get('hello-someone.greetings');

async function setupWebView() {
const { dataProvider: greetingsDataProvider } = await dataProviderInfoPromise;

// Attach handler for greetings-button
const greetingsButton = document.getElementById("greetings-button");
greetingsButton.addEventListener("click", async () => {
const greetingsInput = document.getElementById('greetings-input');
const greeting = await greetingsDataProvider.get(greetingsInput.value.toLowerCase());
const helloSomeoneOutput = document.getElementById("greetings-button-output");
helloSomeoneOutput.innerHTML = greeting;
print(greeting);
});
greetingsButton.addEventListener("contextmenu", async (e) => {
e.preventDefault();
const greetingsInput = document.getElementById('greetings-input');
const name = greetingsInput.value.toLowerCase();
const promises = new Array(10000);
for (let i = 0; i < 10000; i += 1)
promises[i] = greetingsDataProvider.get(name);
await Promise.all(promises);
print('Done');
});

// Attach handler for set-greetings-button
const setGreetingsButton = document.getElementById("set-greetings-button");
setGreetingsButton.addEventListener("click", async () => {
const greetingsInput = document.getElementById('greetings-input');
const name = greetingsInput.value;
const setGreetingsInput = document.getElementById('set-greetings-input');
const success = await greetingsDataProvider.set(name.toLowerCase(), setGreetingsInput.value);
const helloSomeoneOutput = document.getElementById("greetings-button-output");
helloSomeoneOutput.innerHTML = \`\${success ? 'Successfully' : 'Unsuccessfully'} set \${name}'s greeting!\`;
});

// Update the 'all greetings' display on load and on greetings updates
greetingsDataProvider.subscribe('*', (greetings) => {
const allGreetings = document.getElementById("all-greetings");
const greetingsString = JSON.stringify(greetings, null, 2);
allGreetings.innerHTML = greetingsString;
print(greetingsString);
});

// Update the greetings count on any greeting update
let anyGreetingsUpdateCount = 0;
greetingsDataProvider.subscribe('*', () => {
anyGreetingsUpdateCount += 1;
const anyGreetingsUpdateCountDiv = document.getElementById("any-greetings-update-count");
anyGreetingsUpdateCountDiv.innerHTML = \`Any Greetings Updates: \${anyGreetingsUpdateCount}\`;
}, { retrieveDataImmediately: false });

// Update the greetings count on greetings updates
let billAnyGreetingsUpdateCount = 0;
greetingsDataProvider.subscribe('BiLl', () => {
billAnyGreetingsUpdateCount += 1;
const billAnyGreetingsUpdateCountDiv = document.getElementById("bill-any-greetings-update-count");
billAnyGreetingsUpdateCountDiv.innerHTML = \`Any Greetings Updates (via Bill): \${billAnyGreetingsUpdateCount}\`;
}, { retrieveDataImmediately: false, whichUpdates: 'all' });

// Update the greetings count on greetings updates
let billGreetingsUpdateCount = -1;
greetingsDataProvider.subscribe('bIlL', () => {
billGreetingsUpdateCount += 1;
const billGreetingsUpdateCountDiv = document.getElementById("bill-greetings-update-count");
billGreetingsUpdateCountDiv.innerHTML = \`Bill Greetings Updates: \${billGreetingsUpdateCount}\`;
});
}

if (document.readyState === 'loading')
document.addEventListener("DOMContentLoaded", setupWebView);
else setupWebView();

//# sourceURL=hello-someone-webview.js
</script>
</body>
</html>`,
});

const unsubPromises = [
papi.commands.registerCommand('hello-someone.hello-someone', (someone) => {
return `Hello ${someone}!`;
Expand All @@ -34,7 +179,9 @@ exports.activate = async () => {
).then(() => {
logger.log('Hello Someone is finished activating!');
return papi.util.aggregateUnsubscriberAsyncs(
unsubPromises.map((unsubPromise) => unsubPromise.unsubscriber),
unsubPromises
.map((unsubPromise) => unsubPromise.unsubscriber)
.concat([greetingsDataProviderInfo.dispose]),
);
});
};
Expand Down
23 changes: 21 additions & 2 deletions extensions/hello-world/hello-world.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const getReactComponent = (name, functionModifier = '') =>
const {
react: {
context: { TestContext },
hooks: { usePromise },
hooks: { useData, usePromise },
components: { PButton }
},
logger
Expand Down Expand Up @@ -43,6 +43,8 @@ const getReactComponent = (name, functionModifier = '') =>
'retrieving',
);

const [latestVerseText] = useData('quick-verse.quick-verse', 'latest', 'Loading latest Scripture text...');

return createElement('div', null,
createElement('div', null,
createElement(
Expand Down Expand Up @@ -79,6 +81,9 @@ const getReactComponent = (name, functionModifier = '') =>
},
'Throw test exception'
)
),
createElement('div', null,
latestVerseText
)
);
}`;
Expand Down Expand Up @@ -171,16 +176,30 @@ exports.activate = async () => {
logger.error(\`Could not get Scripture from bible-api! Reason: \${e}\`),
);
});

//# sourceURL=hello-world-webview.js
</script>
</body>
</html>`,
});

papi.webViews.addWebView({
await papi.webViews.addWebView({
componentName: 'HelloWorld',
contents: getReactComponent('Hello World React Webview'),
});

const { dataProvider: greetingsDataProvider } = await papi.dataProvider.get(
'hello-someone.greetings',
);

// Test subscribing to a data provider
const unsubGreetings = await greetingsDataProvider.subscribe(
'Bill',
(billGreeting) => logger.log(`Bill's greeting: ${billGreeting}`),
);

unsubPromises.push(unsubGreetings);

return Promise.all(
unsubPromises.map((unsubPromise) => unsubPromise.promise),
).then(() => {
Expand Down
10 changes: 10 additions & 0 deletions extensions/quick-verse/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "quick-verse",
"version": "0.0.1",
"description": "Simple quick verse extension for Paranext",
"author": "TJ Couch",
"license": "MIT",
"main": "quick-verse.js",
"activationEvents": [
]
}
11 changes: 11 additions & 0 deletions extensions/quick-verse/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "quick-verse",
"version": "0.0.1",
"description": "Simple quick verse extension for Paranext",
"main": "quick-verse.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "TJ Couch",
"license": "MIT"
}
13 changes: 13 additions & 0 deletions extensions/quick-verse/quick-verse.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import IDataProvider, {
DataProviderSubscriber,
} from '@shared/models/IDataProvider';

export type QuickVerseSetData = string | { text: string; isHeresy: boolean };

export interface QuickVerseDataProvider
extends IDataProvider<string, string, QuickVerseSetData> {
set(selector: string, data: QuickVerseSetData): Promise<boolean>;
setHeresy(verseRef: string, verseText: string): Promise<boolean>;
get(selector: string): Promise<string>;
subscribe: DataProviderSubscriber<string, string>;
}
Loading