Skip to content

Commit

Permalink
Made PDPFE.createProjectDataProviderEngine asynchronous, finished hel…
Browse files Browse the repository at this point in the history
…lo world test project type including extension data and project settings
  • Loading branch information
tjcouch-sil committed May 16, 2024
1 parent 47d67ca commit abdff3f
Show file tree
Hide file tree
Showing 13 changed files with 758 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@
"metadata": {},
"localizedStrings": {
"en": {
"%mainMenu_helloWorldSubmenu%": "Hello World Projects",
"%mainMenu_openHelloWorldProject%": "Open Hello World Project",
"%mainMenu_createNewHelloWorldProject%": "Create New Hello World Project",
"%settings_hello_world_group1_label%": "Hello World Settings",
"%settings_hello_world_personName_label%": "Selected Person's Name on Hello World Web View"
"%settings_hello_world_personName_label%": "Selected Person's Name on Hello World Web View",
"%project_settings_helloWorld_group1_label%": "Hello World Project Settings",
"%project_settings_helloWorld_headerSize_label%": "Header Size",
"%project_settings_helloWorld_headerSize_description%": "Size of the header font in `em`",
"%project_settings_helloWorld_headerColor_label%": "Header Color",
"%project_settings_helloWorld_headerColor_description%": "Color of the headers (must be a valid [HTML color name](https://htmlcolorcodes.com/color-names/))"
}
}
}
28 changes: 24 additions & 4 deletions extensions/src/hello-world/contributions/menus.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
{
"mainMenu": {
"columns": {},
"groups": {},
"groups": {
"helloWorld.helloWorldProjects": {
"order": 1.5,
"isExtensible": true,
"menuItem": "helloWorld.projectSubmenu"
}
},
"items": [
{
"label": "%mainMenu_openHelloWorldProject%",
"localizeNotes": "Application main menu > Project > Open Hello World Project",
"id": "helloWorld.projectSubmenu",
"label": "%mainMenu_helloWorldSubmenu%",
"localizeNotes": "Application main menu > Project > Hello World Projects",
"group": "platform.projectProjects",
"order": 1000.1,
"order": 1000.1
},
{
"label": "%mainMenu_openHelloWorldProject%",
"localizeNotes": "Application main menu > Project > Hello World Projects > Open Hello World Project",
"group": "helloWorld.helloWorldProjects",
"order": 1,
"command": "helloWorld.openProject"
},
{
"label": "%mainMenu_createNewHelloWorldProject%",
"localizeNotes": "Application main menu > Project > Hello World Projects > Create New Hello World Project",
"group": "helloWorld.helloWorldProjects",
"order": 2,
"command": "helloWorld.createNewProject"
}
]
},
Expand Down
20 changes: 19 additions & 1 deletion extensions/src/hello-world/contributions/projectSettings.json
Original file line number Diff line number Diff line change
@@ -1 +1,19 @@
[]
[
{
"label": "%project_settings_helloWorld_group1_label%",
"properties": {
"helloWorld.headerSize": {
"label": "%project_settings_helloWorld_headerSize_label%",
"description": "%project_settings_helloWorld_headerSize_description%",
"default": 15,
"includeProjectTypes": ["^helloWorld$"]
},
"helloWorld.headerColor": {
"label": "%project_settings_helloWorld_headerColor_label%",
"description": "%project_settings_helloWorld_headerColor_description%",
"default": "Black",
"includeProjectTypes": ["^helloWorld$"]
}
}
}
]
53 changes: 51 additions & 2 deletions extensions/src/hello-world/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ import helloWorldReactWebViewStyles from './web-views/hello-world.web-view.scss?
import helloWorldReactWebView2 from './web-views/hello-world-2.web-view?inline';
import helloWorldReactWebView2Styles from './web-views/hello-world-2.web-view.scss?inline';
import helloWorldHtmlWebView from './web-views/hello-world.web-view.html?inline';
import helloWorldProjectDataProviderEngineFactory from './models/hello-world-project-data-provider-engine-factory.model';
import HelloWorldProjectDataProviderEngineFactory from './models/hello-world-project-data-provider-engine-factory.model';
import helloWorldProjectWebView from './web-views/hello-world-project.web-view?inline';
import helloWorldProjectWebViewStyles from './web-views/hello-world-project.web-view.scss?inline';
import { HTML_COLOR_NAMES } from './util';

/** User data storage key for all hello world project data */
const allProjectDataStorageKey = 'allHelloWorldProjectData';

type IWebViewProviderWithType = IWebViewProvider & { webViewType: string };

Expand Down Expand Up @@ -179,22 +183,64 @@ function helloException(message: string) {
export async function activate(context: ExecutionActivationContext): Promise<void> {
logger.info('Hello world is activating!');

async function readRawDataForAllProjects(): Promise<string> {
try {
return await papi.storage.readUserData(context.executionToken, allProjectDataStorageKey);
} catch {
// No project data found or some other issue. With more important project data, we would be
// more careful not to do something that would overwrite project data if there were an error
return '{}';
}
}

async function writeRawDataForAllProjects(data: string): Promise<void> {
return papi.storage.writeUserData(context.executionToken, allProjectDataStorageKey, data);
}

const helloWorldProjectDataProviderEngineFactory = new HelloWorldProjectDataProviderEngineFactory(
readRawDataForAllProjects,
writeRawDataForAllProjects,
);

const helloWorldPdpefPromise = papi.projectDataProviders.registerProjectDataProviderEngineFactory(
'helloWorld',
helloWorldProjectDataProviderEngineFactory,
helloWorldProjectDataProviderEngineFactory.getAvailableProjects,
helloWorldProjectDataProviderEngineFactory.getAvailableProjects.bind(
helloWorldProjectDataProviderEngineFactory,
),
);

const openHelloWorldProjectPromise = papi.commands.registerCommand(
'helloWorld.openProject',
openHelloWorldProjectWebView,
);

const createNewHelloWorldProjectPromise = papi.commands.registerCommand(
'helloWorld.createNewProject',
async (openWebView = true) => {
const projectId = await helloWorldProjectDataProviderEngineFactory.createNewProject();

if (openWebView) papi.commands.sendCommand('helloWorld.openProject', projectId);

return projectId;
},
);

const helloWorldPersonNamePromise = papi.settings.registerValidator(
'helloWorld.personName',
async (newValue) => typeof newValue === 'string',
);

const helloWorldHeaderSizePromise = papi.projectSettings.registerValidator(
'helloWorld.headerSize',
async (newValue) => typeof newValue === 'number' && Number.isInteger(newValue) && newValue > 0,
);

const helloWorldHeaderColorPromise = papi.projectSettings.registerValidator(
'helloWorld.headerColor',
async (newValue) => HTML_COLOR_NAMES.includes(newValue),
);

const helloWorldProjectWebViewProviderPromise = papi.webViewProviders.register(
helloWorldProjectWebViewProvider.webViewType,
helloWorldProjectWebViewProvider,
Expand Down Expand Up @@ -255,7 +301,10 @@ export async function activate(context: ExecutionActivationContext): Promise<voi
await helloWorldPdpefPromise,
await helloWorldProjectWebViewProviderPromise,
await openHelloWorldProjectPromise,
await createNewHelloWorldProjectPromise,
await helloWorldPersonNamePromise,
await helloWorldHeaderSizePromise,
await helloWorldHeaderColorPromise,
await htmlWebViewProviderPromise,
await reactWebViewProviderPromise,
await reactWebView2ProviderPromise,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,134 @@ import {
IProjectDataProviderEngineFactory,
ProjectMetadata,
} from '@papi/core';
import HelloWorldProjectDataProviderEngine from './hello-world-project-data-provider-engine.model';
import { newGuid } from 'platform-bible-utils';
import HelloWorldProjectDataProviderEngine, {
HelloWorldProjectData,
} from './hello-world-project-data-provider-engine.model';
import { ELIGIBLE_NEW_NAMES } from '../util';

export type AllHelloWorldProjectData = { [projectId: string]: HelloWorldProjectData | undefined };

function createEmptyHelloWorldProjectData(projectName: string): HelloWorldProjectData {
return {
names: new Set(),
numbers: {},
settings: {},
extensionData: {},
projectName,
};
}

class HelloWorldProjectDataProviderEngineFactory
implements IProjectDataProviderEngineFactory<'helloWorld'>
{
/** Do not use directly as it may not have a value. Use `getAllProjectData` */
private allProjectDataCached: AllHelloWorldProjectData | undefined;
private saveAllProjectData: () => Promise<void>;

constructor(
private readRawDataForAllProjects: () => Promise<string>,
writeRawDataForAllProjects: (data: string) => Promise<void>,
) {
this.saveAllProjectData = async () => {
const allProjectData = await this.getAllProjectData();

// Serialize by making the 'names' Set into an array
const allProjectDataRaw = JSON.stringify(allProjectData, (key, value) =>
key === 'names' ? [...value] : value,
);

return writeRawDataForAllProjects(allProjectDataRaw);
};
}

async getAllProjectData(): Promise<AllHelloWorldProjectData> {
if (this.allProjectDataCached) return this.allProjectDataCached;

// We don't have the data, so we need to go get it
const allProjectDataRaw = await this.readRawDataForAllProjects();

// Deserialize by putting the 'names' array back into a Set
const allProjectData: AllHelloWorldProjectData = JSON.parse(allProjectDataRaw, (key, value) =>
key === 'names' ? new Set(value) : value,
);

this.allProjectDataCached = allProjectData;
return allProjectData;
}

async setAndSaveProjectData(projectId: string, projectData: HelloWorldProjectData) {
const allProjectData = await this.getAllProjectData();
allProjectData[projectId] = projectData;
this.allProjectDataCached = allProjectData;
await this.saveAllProjectData();
}

const helloWorldProjectDataProviderEngineFactory: IProjectDataProviderEngineFactory<'helloWorld'> & {
getAvailableProjects(): Promise<ProjectMetadata[]>;
} = {
/**
* Returns a list of metadata objects for all projects that can be the targets of PDPs created by
* this factory
*/
async getAvailableProjects() {
return [];
},
createProjectDataProviderEngine(): IProjectDataProviderEngine<'helloWorld'> {
return new HelloWorldProjectDataProviderEngine();
},
};

export default helloWorldProjectDataProviderEngineFactory;
async getAvailableProjects(): Promise<ProjectMetadata[]> {
const allAvailableProjects = Object.entries(await this.getAllProjectData());
return allAvailableProjects.map(([projectId, projectData]) => ({
projectType: 'helloWorld',
id: projectId,
name: projectData?.projectName ?? projectId,
}));
}

async createProjectDataProviderEngine(
projectId: string,
): Promise<IProjectDataProviderEngine<'helloWorld'>> {
const allProjectData = await this.getAllProjectData();
const projectData: HelloWorldProjectData =
allProjectData[projectId] ?? createEmptyHelloWorldProjectData(projectId);
return new HelloWorldProjectDataProviderEngine(projectData, (data) => {
return this.setAndSaveProjectData(projectId, data);
});
}

/**
* Creates a new project with a random name
*
* @returns Project id of the new hello world project
*/
async createNewProject(): Promise<string> {
const allProjectData = await this.getAllProjectData();

let newProjectId = newGuid();
// In production code, please ensure uniqueness better than this
while (allProjectData[newProjectId]) {
newProjectId = newGuid();
}
const newProjectData = createEmptyHelloWorldProjectData(await this.#getUniqueProjectName());

await this.setAndSaveProjectData(newProjectId, newProjectData);

return newProjectId;
}

async #getUniqueProjectName(): Promise<string> {
const projectName = ELIGIBLE_NEW_NAMES[Math.floor(ELIGIBLE_NEW_NAMES.length * Math.random())];
let projectNameCount = 0;

const allProjectData = await this.getAllProjectData();

while (
Object.entries(allProjectData).some(
// I don't think there is another way to increment projectNameCount indefinitely and to
// check for uniqueness. Not really sure why this rule is flagging this
// eslint-disable-next-line no-loop-func
([, projectData]) =>
projectData?.projectName ===
`${projectName}${projectNameCount !== 0 ? ` ${projectNameCount}` : ''}`,
)
) {
projectNameCount += 1;
}

return `${projectName}${projectNameCount !== 0 ? ` ${projectNameCount}` : ''}`;
}
}

export default HelloWorldProjectDataProviderEngineFactory;
Loading

0 comments on commit abdff3f

Please sign in to comment.