Skip to content

Commit

Permalink
Moved listing available projects to the PDPFE, added helloWorld.delet…
Browse files Browse the repository at this point in the history
…eProject, misc fixes
  • Loading branch information
tjcouch-sil committed May 17, 2024
1 parent abdff3f commit 9b2f4b2
Show file tree
Hide file tree
Showing 8 changed files with 118 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"%mainMenu_helloWorldSubmenu%": "Hello World Projects",
"%mainMenu_openHelloWorldProject%": "Open Hello World Project",
"%mainMenu_createNewHelloWorldProject%": "Create New Hello World Project",
"%mainMenu_deleteHelloWorldProject%": "Delete 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",
"%project_settings_helloWorld_group1_label%": "Hello World Project Settings",
Expand Down
7 changes: 7 additions & 0 deletions extensions/src/hello-world/contributions/menus.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@
"group": "helloWorld.helloWorldProjects",
"order": 2,
"command": "helloWorld.createNewProject"
},
{
"label": "%mainMenu_deleteHelloWorldProject%",
"localizeNotes": "Application main menu > Project > Hello World Projects > Delete Hello World Project",
"group": "helloWorld.helloWorldProjects",
"order": 3,
"command": "helloWorld.deleteProject"
}
]
},
Expand Down
23 changes: 20 additions & 3 deletions extensions/src/hello-world/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,6 @@ export async function activate(context: ExecutionActivationContext): Promise<voi
const helloWorldPdpefPromise = papi.projectDataProviders.registerProjectDataProviderEngineFactory(
'helloWorld',
helloWorldProjectDataProviderEngineFactory,
helloWorldProjectDataProviderEngineFactory.getAvailableProjects.bind(
helloWorldProjectDataProviderEngineFactory,
),
);

const openHelloWorldProjectPromise = papi.commands.registerCommand(
Expand All @@ -226,6 +223,25 @@ export async function activate(context: ExecutionActivationContext): Promise<voi
},
);

const deleteHelloWorldProjectPromise = papi.commands.registerCommand(
'helloWorld.deleteProject',
async (projectId) => {
const projectIdToDelete =
projectId ??
(await papi.dialogs.selectProject({
includeProjectTypes: 'helloWorld',
title: 'Delete Hello World Project',
prompt: 'Please choose a project to delete:',
}));

if (!projectIdToDelete) return false;

// TODO: close web views if this is successful (we don't currently have a way to close them or
// to query for open ones)
return helloWorldProjectDataProviderEngineFactory.deleteProject(projectIdToDelete);
},
);

const helloWorldPersonNamePromise = papi.settings.registerValidator(
'helloWorld.personName',
async (newValue) => typeof newValue === 'string',
Expand Down Expand Up @@ -302,6 +318,7 @@ export async function activate(context: ExecutionActivationContext): Promise<voi
await helloWorldProjectWebViewProviderPromise,
await openHelloWorldProjectPromise,
await createNewHelloWorldProjectPromise,
await deleteHelloWorldProjectPromise,
await helloWorldPersonNamePromise,
await helloWorldHeaderSizePromise,
await helloWorldHeaderColorPromise,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,6 @@ class HelloWorldProjectDataProviderEngineFactory
await this.saveAllProjectData();
}

/**
* Returns a list of metadata objects for all projects that can be the targets of PDPs created by
* this factory
*/
async getAvailableProjects(): Promise<ProjectMetadata[]> {
const allAvailableProjects = Object.entries(await this.getAllProjectData());
return allAvailableProjects.map(([projectId, projectData]) => ({
Expand Down Expand Up @@ -110,23 +106,56 @@ class HelloWorldProjectDataProviderEngineFactory
return newProjectId;
}

/**
* Deletes a Hello World project
*
* @param projectId Optional project ID of the project to delete. Prompts the user to select a
* project if not provided
* @returns `true` if successfully deleted
*/
async deleteProject(projectId: string): Promise<boolean> {
const allProjectData = await this.getAllProjectData();

if (!allProjectData[projectId]) return false;

delete allProjectData[projectId];
this.allProjectDataCached = allProjectData;
await this.saveAllProjectData();
return true;
}

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;
// Look for all projects with the same name and a number after their name, and find the first number missing
const projectNameRegex = new RegExp(`${projectName} ?(?<number>\\d*)`);

const nameNumbers = new Set<number>();
Object.entries(allProjectData).forEach(([, projectData]) => {
if (!projectData?.projectName) return;
const matches = projectNameRegex.exec(projectData.projectName);
if (!matches) return;

if (matches.groups?.number) {
const nameNumber = parseInt(matches.groups.number, 10);
if (typeof nameNumber === 'number') nameNumbers.add(nameNumber);
}
// There was no number, so consider that to be 0
else nameNumbers.add(0);
});

let projectNameCount = 0;

if (nameNumbers.size > 0) {
const nameNumbersArray = [...nameNumbers];
nameNumbersArray.sort((a, b) => (a > b ? 1 : -1));
const nameNumberMissingIndex = nameNumbersArray.findIndex(
(nameNumber, i) => nameNumber !== i,
);
projectNameCount =
nameNumberMissingIndex === -1 ? nameNumbersArray.length : nameNumberMissingIndex;
}

return `${projectName}${projectNameCount !== 0 ? ` ${projectNameCount}` : ''}`;
Expand Down
10 changes: 9 additions & 1 deletion extensions/src/hello-world/src/types/hello-world.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,14 +195,22 @@ declare module 'papi-shared-types' {
* @returns WebView id for new Hello World Project WebView or `undefined` if the user canceled
* the dialog
*/
'helloWorld.openProject': (projectId: string | undefined) => Promise<string | undefined>;
'helloWorld.openProject': (projectId?: string) => Promise<string | undefined>;
/**
* Creates a new Hello World project with a random name
*
* @param openWebView Whether to open a web view for the new project. Defaults to true
* @returns Project id of the new hello world project
*/
'helloWorld.createNewProject': (openWebView?: boolean) => Promise<string>;
/**
* Deletes a Hello World project
*
* @param projectId Optional project ID of the project to delete. Prompts the user to select a
* project if not provided
* @returns `true` if successfully deleted
*/
'helloWorld.deleteProject': (projectId?: string) => Promise<boolean>;
}

export interface ProjectDataProviders {
Expand Down
50 changes: 26 additions & 24 deletions lib/papi-dts/papi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3388,6 +3388,24 @@ declare module 'shared/services/data-provider.service' {
const dataProviderService: DataProviderService;
export default dataProviderService;
}
declare module 'shared/models/project-metadata.model' {
import { ProjectTypes } from 'papi-shared-types';
/**
* Low-level information describing a project that Platform.Bible directly manages and uses to load
* project data
*/
export type ProjectMetadata = {
/** ID of the project (must be unique and case insensitive) */
id: string;
/** Short name of the project (not necessarily unique) */
name: string;
/**
* Indicates what sort of project this is which implies its data shape (e.g., what data streams
* should be available)
*/
projectType: ProjectTypes;
};
}
declare module 'shared/models/project-data-provider-engine.model' {
import {
ProjectTypes,
Expand All @@ -3404,6 +3422,7 @@ declare module 'shared/models/project-data-provider-engine.model' {
DataProviderEngine,
} from 'shared/models/data-provider-engine.model';
import { DataProviderDataType } from 'shared/models/data-provider.model';
import { ProjectMetadata } from 'shared/models/project-metadata.model';
/** All possible types for ProjectDataProviderEngines: IProjectDataProviderEngine<ProjectDataType> */
export type ProjectDataProviderEngineTypes = {
[ProjectType in ProjectTypes]: IProjectDataProviderEngine<ProjectType>;
Expand All @@ -3422,14 +3441,19 @@ declare module 'shared/models/project-data-provider-engine.model' {
* `projectType` is created by the Project Data Provider Factory with that project's `projectType`.
*/
export interface IProjectDataProviderEngineFactory<ProjectType extends ProjectTypes> {
/**
* Get a list of metadata objects for all projects that can be the targets of PDPs created by this
* factory engine
*/
getAvailableProjects(): Promise<ProjectMetadata[]>;
/**
* Create a {@link IProjectDataProviderEngine} for the project requested so the papi can create an
* {@link IProjectDataProvider} for the project. This project will have the same `projectType` as
* this Project Data Provider Engine Factory
*
* @param projectId Id of the project for which to create a {@link IProjectDataProviderEngine}
* @returns A {@link IProjectDataProviderEngine} for the project passed in or a Promise that
* resolves to one
* @returns A promise that resolves to a {@link IProjectDataProviderEngine} for the project passed
* in
*/
createProjectDataProviderEngine(
projectId: string,
Expand Down Expand Up @@ -3512,24 +3536,6 @@ declare module 'shared/models/project-data-provider-engine.model' {
}
> {}
}
declare module 'shared/models/project-metadata.model' {
import { ProjectTypes } from 'papi-shared-types';
/**
* Low-level information describing a project that Platform.Bible directly manages and uses to load
* project data
*/
export type ProjectMetadata = {
/** ID of the project (must be unique and case insensitive) */
id: string;
/** Short name of the project (not necessarily unique) */
name: string;
/**
* Indicates what sort of project this is which implies its data shape (e.g., what data streams
* should be available)
*/
projectType: ProjectTypes;
};
}
declare module 'shared/models/project-data-provider-factory.interface' {
import { Dispose } from 'platform-bible-utils';
import { ProjectMetadata } from 'shared/models/project-metadata.model';
Expand Down Expand Up @@ -4425,21 +4431,17 @@ declare module 'shared/services/project-data-provider.service' {
import { ProjectTypes, ProjectDataProviders } from 'papi-shared-types';
import { IProjectDataProviderEngineFactory } from 'shared/models/project-data-provider-engine.model';
import { Dispose } from 'platform-bible-utils';
import { ProjectMetadata } from '@papi/core';
/**
* Add a new Project Data Provider Factory to PAPI that uses the given engine. There must not be an
* existing factory already that handles the same project type or this operation will fail.
*
* @param projectType Type of project that pdpEngineFactory supports
* @param pdpEngineFactory Used in a ProjectDataProviderFactory to create ProjectDataProviders
* @param projectMetadataProvider Used in a ProjectDataProviderFactory to create
* ProjectDataProviders
* @returns Promise that resolves to a disposable object when the registration operation completes
*/
export function registerProjectDataProviderEngineFactory<ProjectType extends ProjectTypes>(
projectType: ProjectType,
pdpEngineFactory: IProjectDataProviderEngineFactory<ProjectType>,
projectMetadataProvider: () => Promise<ProjectMetadata[]>,
): Promise<Dispose>;
/**
* Get a Project Data Provider for the given project ID.
Expand Down
10 changes: 8 additions & 2 deletions src/shared/models/project-data-provider-engine.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '@shared/models/project-data-provider.model';
import IDataProviderEngine, { DataProviderEngine } from '@shared/models/data-provider-engine.model';
import { DataProviderDataType } from '@shared/models/data-provider.model';
import { ProjectMetadata } from '@shared/models/project-metadata.model';

/** All possible types for ProjectDataProviderEngines: IProjectDataProviderEngine<ProjectDataType> */
export type ProjectDataProviderEngineTypes = {
Expand All @@ -31,14 +32,19 @@ export type ProjectDataProviderEngineTypes = {
* `projectType` is created by the Project Data Provider Factory with that project's `projectType`.
*/
export interface IProjectDataProviderEngineFactory<ProjectType extends ProjectTypes> {
/**
* Get a list of metadata objects for all projects that can be the targets of PDPs created by this
* factory engine
*/
getAvailableProjects(): Promise<ProjectMetadata[]>;
/**
* Create a {@link IProjectDataProviderEngine} for the project requested so the papi can create an
* {@link IProjectDataProvider} for the project. This project will have the same `projectType` as
* this Project Data Provider Engine Factory
*
* @param projectId Id of the project for which to create a {@link IProjectDataProviderEngine}
* @returns A {@link IProjectDataProviderEngine} for the project passed in or a Promise that
* resolves to one
* @returns A promise that resolves to a {@link IProjectDataProviderEngine} for the project passed
* in
*/
createProjectDataProviderEngine(
projectId: string,
Expand Down
16 changes: 2 additions & 14 deletions src/shared/services/project-data-provider.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,28 @@ class ProjectDataProviderFactory<ProjectType extends ProjectTypes>
private readonly projectType: ProjectType;
private readonly pdpCleanupList: UnsubscriberAsyncList;
private readonly pdpEngineFactory: IProjectDataProviderEngineFactory<ProjectType>;
private readonly projectMetadataProvider: () => Promise<ProjectMetadata[]>;

/**
* Create a new PDP factory that is used to create PDPs
*
* @param projectType Specified which project type this PDP factory supports
* @param pdpEngineFactory Object that can create the engines for PDPs
* @param projectMetadataProvider Function that returns a list of metadata objects for all
* projects that can be the targets of PDPs created by this factory.
*/
constructor(
projectType: ProjectType,
pdpEngineFactory: IProjectDataProviderEngineFactory<ProjectType>,
projectMetadataProvider: () => Promise<ProjectMetadata[]>,
) {
this.projectType = projectType;
this.pdpCleanupList = new UnsubscriberAsyncList(`PDP Factory for ${projectType}`);
this.pdpEngineFactory = pdpEngineFactory;
this.projectMetadataProvider = projectMetadataProvider;
}

/**
* Returns a list of metadata objects for all projects that can be the targets of PDPs created by
* this factory
*/
getAvailableProjects(): Promise<ProjectMetadata[]> {
return this.projectMetadataProvider();
return this.pdpEngineFactory.getAvailableProjects();
}

/** Disposes of all PDPs that were created by this PDP Factory */
Expand Down Expand Up @@ -117,21 +112,14 @@ function getProjectDataProviderFactoryId(projectType: ProjectTypes) {
*
* @param projectType Type of project that pdpEngineFactory supports
* @param pdpEngineFactory Used in a ProjectDataProviderFactory to create ProjectDataProviders
* @param projectMetadataProvider Used in a ProjectDataProviderFactory to create
* ProjectDataProviders
* @returns Promise that resolves to a disposable object when the registration operation completes
*/
export async function registerProjectDataProviderEngineFactory<ProjectType extends ProjectTypes>(
projectType: ProjectType,
pdpEngineFactory: IProjectDataProviderEngineFactory<ProjectType>,
projectMetadataProvider: () => Promise<ProjectMetadata[]>,
): Promise<Dispose> {
const factoryId = getProjectDataProviderFactoryId(projectType);
const factory = new ProjectDataProviderFactory(
projectType,
pdpEngineFactory,
projectMetadataProvider,
);
const factory = new ProjectDataProviderFactory(projectType, pdpEngineFactory);
return networkObjectService.set<ProjectDataProviderFactory<ProjectType>>(
factoryId,
factory,
Expand Down

0 comments on commit 9b2f4b2

Please sign in to comment.