-
Notifications
You must be signed in to change notification settings - Fork 1
Extension Development 'How To' Guide
Click here for details
-
Locate the settings contribution file for your extension. This is identified by the
settings
value in yourmanifest.json
file. Typically, it issettings.json
in yourcontributions
folder. -
Add an object into the array for each setting that you want to add. Here is the general format to use:
{ "label": "%localizeKey_for_settings_group_name_to_display_in_settings_ui%", "description": "%localizeKey_for_settings_group_description_to_display_in_settings_ui%", "properties": { "<my extension name>.<my setting name>": { "label": "%localizeKey_for_setting_name_to_display_in_settings_ui%", "description": "%localizeKey_for_setting_description_to_display_in_settings_ui%", "default": <default value for this setting>, "derivesFrom": <optional name of other setting that will be used as the starting value of this setting the first time it is loaded> } } }
For example, the following could be the data for a setting called "colorScheme" in an extension named "myExtension":
{ "label": "%myExtension_setting_group_name%", "description": "%myExtension_setting_group_description%", "properties": { "myExtension.colorScheme": { "label": "%myExtension_setting_color_scheme%", "description": "%myExtension_setting_color_scheme_user_description%", "default": "standard" } } }
-
Provide localization values for all of the
LocalizeKey
values in the labels and descriptions from yoursettings.json
file. See the How To about localizing strings for more information. You should provide localization values for English (en
) in addition to any other languages you are targeting. -
In your extension's
d.ts
file, declare all of the settings you defined in step 2.export interface SettingTypes { /** Color scheme used by myExtension */ 'myExtension.colorScheme': string; }
-
In your extension's
activate()
function, add a validator for each setting to determine whether a new value is valid. Also, add the validator tocontext.registrations
so it will be removed if your extension is deactivated.const colorSchemeValidationPromise = papi.settings.registerValidator( 'myExtension.colorScheme', async (newValue) => typeof newValue === 'string' && (newValue as string).length > 0, ); context.registrations.add(await colorSchemeValidationPromise);
NOTE: Settings apply to your extension regardless of what project a user has open. If you want a different value to be configurable on a per project basis, use a project setting instead.
Click here for details
-
In your
web-view.tsx
file, import theuseSetting
hook from the PAPI.import { useSetting } from '@papi/frontend/react'
-
Call the hook with the setting key of the setting you want to retrieve or set and a default value. If the default value is not a
boolean
,integer
, orstring
it should be wrapped in auseMemo
to be made stable. The hook returns[setting, setSetting, resetSetting, isLoading]
.-
setting
is the current state of the setting, eitherdefaultState
, the stored value, or aPlatformError
if loading the value fails. See Determine if a value is of type PlatformError -
setSetting
is a function that updates the setting to a new value. -
resetSetting
is a function that removes the setting and resets the value todefaultState
from the settings contribution. -
isLoading
is a boolean that tells you if the value of the setting is still loading, so you can wait for the setting to load and create responsive UI's.
const [setting, setSetting, resetSetting, isLoading] = useSetting( 'helloWorld.personName', 'Kathy', );
-
Now you can use and manipulate the application setting with setting
, setSetting
, and resetSetting
.
Click here for details
We can use and change application settings throughout the platform with the following functions, and a setting key, which is the string id of the setting for which the value is being get, set, or subscribed to.
-
Import the papi object.
- In HTML web-view:
const papi = require('@papi/frontend');
- In backend:
import { papi } from '@papi/backend';
-
To get a setting value, call
papi.settings.get
with the relevant setting key. This function returns the value of the specified setting, parsed to an object. Returns default setting if setting does not exist.const settingValue = await papi.settings.get('helloWorld.personName');
-
To set a setting value, call
papi.settings.set
with the relevant setting key, and the value you would like to set it to. This function returns information that the PAPI uses to interpret whether to send out updates. Defaults totrue
(meaning send updates only for this data type).const updateInstructions = await papi.settings.set('helloWorld.personName', 'Ben');
-
To reset a setting to its default value, call
papi.settings.reset
with the relevant setting key. This function returnstrue
if successfully reset the project setting.false
otherwise.const isResetSuccessful = await papi.settings.reset('helloWorld.personName');
-
To subscribe to a setting, call
papi.settings.subscribe
. You need to pass it the setting key, a callback function that will be called whenever the specified setting is updated, and optional subscriber options that adjust how the subscriber emits updates. If there is an error while retrieving the updated data, the callback will run with aPlatformError
instead of the data. See Determine if a value is of type PlatformError. This function returns anUnsubscriber
that should be called whenever the subscription should be deleted.let name; const unsubscriber = await papi.settings.subscribe( 'helloWorld.personName', (newValue: string | PlatformError) => { if (!isPlatformError(newValue)) name = newValue; }, { retrieveDataImmediately: false, whichUpdates: '*' } )
Click here for details
-
Locate the project settings contribution file for your extension. This is identified by the
projectSettings
value in yourmanifest.json
file. Typically, it isprojectSettings.json
in yourcontributions
folder. -
Add an object into the array for each project setting that you want to add. Here is the general format to use:
{ "label": "%localizeKey_for_project_settings_group_name_to_display_in_settings_ui%", "description": "%localizeKey_for_project_settings_group_description_to_display_in_settings_ui%", "properties": { "<my extension name>.<my setting name>": { "label": "%localizeKey_for_project_setting_name_to_display_in_settings_ui%", "description": "%localizeKey_for_project_setting_description_to_display_in_settings_ui%", "default": <default value for this setting>, "includeProjectInterfaces": <optional regular expressions identifying which projects this setting applies to>, "excludeProjectInterfaces": <optional regular expressions identifying which projects this setting does not apply to>, "includePdpFactoryIds": <optional regular expressions identifying which projects this setting applies to>, "excludePdpFactoryIds": <optional regular expressions identifying which projects this setting does not apply to> } } }
For example, the following could be the data for a project setting called "specialFeatureEnabled" in an extension named "myExtension":
{ "label": "%myExtension_project_setting_group_name%", "description": "%myExtension_project_setting_group_description%", "properties": { "myExtension.specialFeatureEnabled": { "label": "%myExtension_project_setting_special_feature%", "description": "%myExtension_project_setting_special_feature_user_description%", "default": false, "includeProjectInterfaces": ["Scripture"] } } }
If you want to limit your extension to working with
Scripture
projects, you will want to specify that as a value forincludeProjectInterfaces
. If you don't want to limit your extension to particular types of projects, then you don't need to specify a value. UsingincludePdpFactoryIds
,excludePdpFactoryIds
, andexcludeProjectInterfaces
values is advanced and not necessary in most cases.Here is more information about:
If you are working with custom project types you created, then you will need to understand project interfaces in more detail.
-
Provide localization values for all of the
LocalizeKey
values in the labels and descriptions from yourprojectSettings.json
file. See the How To about localizing strings for more information. You should provide localization values for English (en
) in addition to any other languages you are targeting. -
In your extension's
d.ts
file, declare all of the settings you defined in step 2.export interface ProjectSettingTypes { /** Determines whether "special feature" is enabled within myExtension */ 'myExtension.specialFeatureEnabled': boolean; }
-
In your extension's
activate()
function, add a validator for each project setting to determine whether a new value is valid. Also, add the validator tocontext.registrations
so it will be removed if your extension is deactivated.const specialFeaturePromise = papi.projectSettings.registerValidator( 'myExtension.specialFeatureEnabled', async (newValue) => typeof newValue === 'boolean', ); context.registrations.add(await specialFeaturePromise);
Click here for details
-
In your
web-view.tsx
file, import theuseProjectSetting
hook from the PAPI.import { useProjectSetting } from '@papi/frontend/react'
-
Call the hook with the project data provider source of the project (relevant
projectId
), the setting key of the setting you want to retrieve or set, and a default value. If the default value is not aboolean
,integer
, orstring
it should be wrapped in auseMemo
to be made stable. The hook returns[setting, setSetting, resetSetting, isLoading]
.-
setting
is the current state of the setting, eitherdefaultState
, the stored value, or aPlatformError
if loading the value fails. See Determine if a value is of type PlatformError. -
setSetting
is a function that updates the setting to a new value. -
resetSetting
is a function that removes the setting and resets the value todefaultState
from the settings contribution. -
isLoading
is a boolean that tells you if the value of the setting is still loading, so you can wait for the setting to load and create responsive UI's.
const [setting, setSetting, resetSetting, isLoading] = useProjectSetting( projectId, 'helloWorld.headerColor', 'Red', );
-
Now you can use and manipulate the project setting with setting
, setSetting
, and resetSetting
.
Click here for details
-
Locate the menu contribution file for your extension. This is identified by the
menus
value in yourmanifest.json
file. Typically, it ismenus.json
in yourcontributions
folder. -
Update the menu contribution file with the menu items you want to add.
Here is a default, empty contribution:
{ "mainMenu": { "columns": {}, "groups": {}, "items": [] }, "defaultWebViewTopMenu": { "columns": {}, "groups": {}, "items": [] }, "defaultWebViewContextMenu": { "groups": {}, "items": [] }, "webViewMenus": {} }
TODO: UPDATE THIS SECTION ONCE WE HAVE SETTLED ON EXTENSIBILITY POINTS FOR MENUS.
Click here for details
-
Select the target locale for localizing values. The locale will need to be identified as a BCP 47 language tag. Normally a 2 letter language code (e.g.,
pt
for Portuguese) would be a good choice. However, if you are trying to localize for a particular language variant that could meaningfully different between members of the same language community, you might want to be more specific (e.g.,pt-BR
for Brazilian Portuguese). -
Identify the
LocalizeKey
values that you want to localize. These will all be of the form%_____%
. That is, they all start and end with a percentage sign (%
). If you are adding UI in your extension, the values will be used in your code. If you want to see allLocalizeKey
values currently loaded by the application and their associated localized strings, do the following:- Select
Help
>Open Developer Documentation
in the application menu. A web browser should open to "Live PAPI documentation". - Click on the
INSPECTOR
to open a pane at the bottom of the browser. - Paste the following in the box on left side:
{ "jsonrpc": "2.0", "method": "object:platform.localizationDataServiceDataProvider-data.retrieveCurrentLocalizedStringData", "params": [], "id": 1000 }
- Click the "Play" button immediately above the box where you pasted the JSON value in the previous step.
- Copy value of
result
in the returned payload.
- Select
-
Locate the localized string contribution file for your extension. This is identified by the
localizedStrings
value in yourmanifest.json
file. Typically, it islocalizedStrings.json
in yourcontributions
folder. -
Create a section in the
localizedStrings
file for your selected locale. Enter the localized values. For example:{ "metadata": {}, "localizedStrings": { "fr": { "%general_open%": "Ouvrir", } } }
NOTE: Localized strings are all part of a global namespace in the application. If you provide a localization value for a given LocalizeKey
, that localization value will be used everywhere in the application that uses the same LocalizeKey
. It doesn't matter what code (e.g., the core platform, your extension, a different extension, etc.) is displaying the string.
Click here for details
If you want extensions (yours and others) to be able to call useData
to read/write data that your extension controls, you need to create a DataProvider
and register it with the application when it starts up.
Here is an example of how you could do this for a service in an extension named myExtension
that defines a data type called Footnote
that is an array of strings tied to a ScriptureReference
.
-
In your extension's
d.ts
file, create a type that defines the named data types you want to expose./** * Data types for reading and writing custom footnote data */ export type FootnoteProviderDataTypes = { Footnotes: DataProviderDataType<ScriptureReference, string[], string[]>; };
The first generic parameter of DataProviderDataType
is the "selector". It is some sort of identifier for indexing into the data in your DataProvider
. The second generic parameter is the data type you will return via a get
method. The third generic parameter is the data type you will accept via a set
method. If your data provider won't allow getting or setting a particular data type, you can specify the parameter as never
.
-
In the same
d.ts
file, create a type that uses the new type from the previous step with theIDataProvider
interface and includes any documentation you want users to see./** * Service that reads and writes custom footnote data * * Use the "myExtension.footnotes" data provider name to access the service. */ export type IFootnoteEnhancementService = IDataProvider<FootnoteProviderDataTypes>;
-
In the same
d.ts
file, declare your data provider name.export interface DataProviders { /** Use this to work with custom footnotes */ 'myExtension.footnotes': IFootnoteEnhancementService; }
-
Create a class that derives from
DataProviderEngine
and implements the type from step 1. The return values of yourset
functions indicate whether subscribers need to callget
again to refresh to the latest data. If your data provider doesn't allowset
orget
calls, you can throw an error in the body of the function. However, you must define aget
andset
call for each data type.class FootnotesDataProviderEngine extends DataProviderEngine<FootnoteProviderDataTypes> implements IDataProviderEngine<FootnoteProviderDataTypes> { async getFootnotes(scriptureReference: ScriptureReference): Promise<string[]> { // body of function } async setFootnotes(scriptureReference: ScriptureReference, footnotesToSave: string[]): Promise<DataProviderUpdateInstructions<FootnoteProviderDataTypes>> { // body of function } }
-
Register the data provider engine using the same name as declared in step 3.
const dataProvider = await papi.dataProviders.registerEngine('myExtension.footnotes', new FootnotesDataProviderEngine());
-
Ensure that when your extension is deactivated, your data provider is disposed. This can be done easily by adding it to
context.registrations
in youractivate()
function.context.registrations.add(dataProvider);
Click here for details
-
Modify your
manifest.json
file to addcreateProcess
to theelevatedPrivileges
array."elevatedPrivileges": ["createProcess"],
-
Make sure you declare the
ExecutionActivationContext
parameter of youractivate()
function. This should be in yourmain.ts
file (or whatever other file you defined as "main" in yourmanifest.json
).export async function activate(context: ExecutionActivationContext) {
-
Update your
activate
function to callfork
orspawn
. Note that ifcreateProcess
is not included in yourmanifest.json
file, thencontext.elevatedPrivileges.createProcess
will beundefined
.if (context.elevatedPrivileges.createProcess) context.elevatedPrivileges.createProcess.spawn(context.executionToken, 'commandToRun', ['arg1', 'arg2'], { stdio: ['pipe', 'pipe', 'pipe'] });
Note: Any process directly created using elevatedPrivileges.createProcess
will be terminated when the application closes. If you create any additional processes from within these processes, those "second level processes" will not be automatically terminated.
Note: Remember that the application runs on Windows, macOS, and Linux. If you need to differentiate what you run based on OS, you can use createProcess.osData
to determine which OS you are running on.
Click here for details
-
Modify your
manifest.json
file to addmanageExtensions
to theelevatedPrivileges
array."elevatedPrivileges": ["manageExtensions"],
-
Make sure you declare the
ExecutionActivationContext
parameter of youractivate()
function. This should be in yourmain.ts
file (or whatever other file you defined as "main" in yourmanifest.json
).export async function activate(context: ExecutionActivationContext) {
-
Add code to manage extensions using
context.elevatedPrivileges.manageExtensions
functions as appropriate. Note that ifmanageExtensions
is not included in yourmanifest.json
file, thencontext.elevatedPrivileges.manageExtensions
will beundefined
.
Click here for details
PlatformError
is an error type with stronger typing of properties than Error
. It is used to represent errors that are returned by the platform. You can see this error when subscribing to data from various sources including project data and settings. You can check if a value is a PlatformError
object using isPlatformError()
. This function accepts a value and returns true
if the value is a PlatformError
and false
otherwise.
-
Import
isPlatformError()
from theplatform-bible-utils
library.import { isPlatformError } from 'platform-bible-utils`;
-
Check if a value is a
PlatformError
, in this example we use a setting value.const [setting, setSetting] = useSetting('helloWorld.personName', 'Kathy'); if (isPlatformError(setting)) return '';
Are there other things you would like to see in this list? Feel free to create a new issue and describe what you think would be helpful!
Note that code style and other such documentation is stored in the Paranext wiki and covers all Paranext repositories.