diff --git a/extensions/src/hello-world/contributions/localizedStrings.json b/extensions/src/hello-world/contributions/localizedStrings.json index bfb3df83e2..bee29b1b4d 100644 --- a/extensions/src/hello-world/contributions/localizedStrings.json +++ b/extensions/src/hello-world/contributions/localizedStrings.json @@ -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", diff --git a/extensions/src/hello-world/contributions/menus.json b/extensions/src/hello-world/contributions/menus.json index 595e199b40..a5adb1a2c3 100644 --- a/extensions/src/hello-world/contributions/menus.json +++ b/extensions/src/hello-world/contributions/menus.json @@ -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" } ] }, diff --git a/extensions/src/hello-world/src/main.ts b/extensions/src/hello-world/src/main.ts index e0e2416422..f2219c6803 100644 --- a/extensions/src/hello-world/src/main.ts +++ b/extensions/src/hello-world/src/main.ts @@ -205,9 +205,6 @@ export async function activate(context: ExecutionActivationContext): Promise { + 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', @@ -302,6 +318,7 @@ export async function activate(context: ExecutionActivationContext): Promise { const allAvailableProjects = Object.entries(await this.getAllProjectData()); return allAvailableProjects.map(([projectId, projectData]) => ({ @@ -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 { + const allProjectData = await this.getAllProjectData(); + + if (!allProjectData[projectId]) return false; + + delete allProjectData[projectId]; + this.allProjectDataCached = allProjectData; + await this.saveAllProjectData(); + return true; + } + async #getUniqueProjectName(): Promise { 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} ?(?\\d*)`); + + const nameNumbers = new Set(); + 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}` : ''}`; diff --git a/extensions/src/hello-world/src/types/hello-world.d.ts b/extensions/src/hello-world/src/types/hello-world.d.ts index 8e057c70c3..0b8de45ff4 100644 --- a/extensions/src/hello-world/src/types/hello-world.d.ts +++ b/extensions/src/hello-world/src/types/hello-world.d.ts @@ -195,7 +195,7 @@ 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; + 'helloWorld.openProject': (projectId?: string) => Promise; /** * Creates a new Hello World project with a random name * @@ -203,6 +203,14 @@ declare module 'papi-shared-types' { * @returns Project id of the new hello world project */ 'helloWorld.createNewProject': (openWebView?: boolean) => Promise; + /** + * 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; } export interface ProjectDataProviders { diff --git a/lib/papi-dts/papi.d.ts b/lib/papi-dts/papi.d.ts index 88c0df5497..d2547b9ef2 100644 --- a/lib/papi-dts/papi.d.ts +++ b/lib/papi-dts/papi.d.ts @@ -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, @@ -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 */ export type ProjectDataProviderEngineTypes = { [ProjectType in ProjectTypes]: IProjectDataProviderEngine; @@ -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 { + /** + * Get a list of metadata objects for all projects that can be the targets of PDPs created by this + * factory engine + */ + getAvailableProjects(): Promise; /** * 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, @@ -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'; @@ -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: ProjectType, pdpEngineFactory: IProjectDataProviderEngineFactory, - projectMetadataProvider: () => Promise, ): Promise; /** * Get a Project Data Provider for the given project ID. diff --git a/src/shared/models/project-data-provider-engine.model.ts b/src/shared/models/project-data-provider-engine.model.ts index cbd416e51e..3808dc5fa5 100644 --- a/src/shared/models/project-data-provider-engine.model.ts +++ b/src/shared/models/project-data-provider-engine.model.ts @@ -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 */ export type ProjectDataProviderEngineTypes = { @@ -31,14 +32,19 @@ export type ProjectDataProviderEngineTypes = { * `projectType` is created by the Project Data Provider Factory with that project's `projectType`. */ export interface IProjectDataProviderEngineFactory { + /** + * Get a list of metadata objects for all projects that can be the targets of PDPs created by this + * factory engine + */ + getAvailableProjects(): Promise; /** * 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, diff --git a/src/shared/services/project-data-provider.service.ts b/src/shared/services/project-data-provider.service.ts index 5291756b7b..f8086b09cf 100644 --- a/src/shared/services/project-data-provider.service.ts +++ b/src/shared/services/project-data-provider.service.ts @@ -25,25 +25,20 @@ class ProjectDataProviderFactory private readonly projectType: ProjectType; private readonly pdpCleanupList: UnsubscriberAsyncList; private readonly pdpEngineFactory: IProjectDataProviderEngineFactory; - private readonly projectMetadataProvider: () => Promise; /** * 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, - projectMetadataProvider: () => Promise, ) { this.projectType = projectType; this.pdpCleanupList = new UnsubscriberAsyncList(`PDP Factory for ${projectType}`); this.pdpEngineFactory = pdpEngineFactory; - this.projectMetadataProvider = projectMetadataProvider; } /** @@ -51,7 +46,7 @@ class ProjectDataProviderFactory * this factory */ getAvailableProjects(): Promise { - return this.projectMetadataProvider(); + return this.pdpEngineFactory.getAvailableProjects(); } /** Disposes of all PDPs that were created by this PDP Factory */ @@ -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: ProjectType, pdpEngineFactory: IProjectDataProviderEngineFactory, - projectMetadataProvider: () => Promise, ): Promise { const factoryId = getProjectDataProviderFactoryId(projectType); - const factory = new ProjectDataProviderFactory( - projectType, - pdpEngineFactory, - projectMetadataProvider, - ); + const factory = new ProjectDataProviderFactory(projectType, pdpEngineFactory); return networkObjectService.set>( factoryId, factory,