From b46119d2adcfeba73e129cd0e0ce46802f4f00c5 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Mon, 11 Aug 2025 20:51:22 -0700 Subject: [PATCH 1/2] Change listModules to take a regex pattern (as a string) so it can easily be passed to a server for server-side stroage. In names.ts, replaced makeModulePathPrefix with makeModulePathRegexPattern. In project.ts: Updated renameOrCopyProject, deleteProject, and downloadProject to pass regex string instead of filter callback to listModules. Modified renameOrCopyModule to call fetchModuleContentText instead of listModules. --- src/storage/client_side_storage.ts | 10 +++++++--- src/storage/common_storage.ts | 12 +++++++++--- src/storage/names.ts | 12 +++++++++--- src/storage/project.ts | 18 +++++------------- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/storage/client_side_storage.ts b/src/storage/client_side_storage.ts index 57911d43..899b474e 100644 --- a/src/storage/client_side_storage.ts +++ b/src/storage/client_side_storage.ts @@ -126,8 +126,12 @@ class ClientSideStorage implements commonStorage.Storage { } async listModules( - opt_modulePathFilter?: commonStorage.ModulePathFilter): - Promise<{[path: string]: storageModuleContent.ModuleContent}> { + opt_modulePathRegexPattern?: string + ): Promise<{[path: string]: storageModuleContent.ModuleContent}> { + + const regExp = opt_modulePathRegexPattern + ? new RegExp(opt_modulePathRegexPattern) + : null; return new Promise((resolve, reject) => { const pathToModuleContent: {[path: string]: storageModuleContent.ModuleContent} = {}; const openCursorRequest = this.db.transaction([MODULES_STORE_NAME], 'readonly') @@ -144,7 +148,7 @@ class ClientSideStorage implements commonStorage.Storage { const value = cursor.value; // TODO(lizlooney): do we need value.path? Is there another way to get the path? const modulePath = value.path; - if (!opt_modulePathFilter || opt_modulePathFilter(modulePath)) { + if (!regExp || regExp.test(modulePath)) { const moduleContent = storageModuleContent.parseModuleContentText(value.content); pathToModuleContent[modulePath] = moduleContent; } diff --git a/src/storage/common_storage.ts b/src/storage/common_storage.ts index e0395242..7e4651de 100644 --- a/src/storage/common_storage.ts +++ b/src/storage/common_storage.ts @@ -21,14 +21,20 @@ import * as storageModuleContent from './module_content'; -export type ModulePathFilter = (modulePath: string) => boolean; - export interface Storage { saveEntry(entryKey: string, entryValue: string): Promise; + fetchEntry(entryKey: string, defaultValue: string): Promise; - listModules(opt_modulePathFilter?: ModulePathFilter): Promise<{[path: string]: storageModuleContent.ModuleContent}>; + + listModules( + opt_modulePathRegexPattern?: string + ): Promise<{[path: string]: storageModuleContent.ModuleContent}>; + fetchModuleDateModifiedMillis(modulePath: string): Promise; + fetchModuleContentText(modulePath: string): Promise; + saveModule(modulePath: string, moduleContentText: string): Promise; + deleteModule(modulePath: string): Promise; } diff --git a/src/storage/names.ts b/src/storage/names.ts index 29ebcfa5..abf17b7e 100644 --- a/src/storage/names.ts +++ b/src/storage/names.ts @@ -76,10 +76,16 @@ export function snakeCaseToPascalCase(snakeCaseName: string): string { } /** - * Returns the module path prefix for the given project name. + * Returns the module path regex pattern for modules in the given project. */ -export function makeModulePathPrefix(projectName: string): string { - return projectName + '/'; +export function makeModulePathRegexPattern(projectName: string): string { + const prefix = projectName + '/'; + const suffix = JSON_FILE_EXTENSION; + return '^' + escapeRegExp(prefix) + '.*' + escapeRegExp(suffix) + '$'; +} + +function escapeRegExp(text: string): string { + return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** diff --git a/src/storage/project.ts b/src/storage/project.ts index 1187a19c..26b9c121 100644 --- a/src/storage/project.ts +++ b/src/storage/project.ts @@ -159,9 +159,8 @@ export async function copyProject( async function renameOrCopyProject( storage: commonStorage.Storage, project: Project, newProjectName: string, rename: boolean): Promise { - const modulePathPrefix = storageNames.makeModulePathPrefix(project.projectName); const pathToModuleContent = await storage.listModules( - (modulePath: string) => modulePath.startsWith(modulePathPrefix)); + storageNames.makeModulePathRegexPattern(project.projectName)); for (const modulePath in pathToModuleContent) { const className = storageNames.getClassName(modulePath); @@ -182,9 +181,9 @@ async function renameOrCopyProject( */ export async function deleteProject( storage: commonStorage.Storage, project: Project): Promise { - const modulePathPrefix = storageNames.makeModulePathPrefix(project.projectName); const pathToModuleContent = await storage.listModules( - (modulePath: string) => modulePath.startsWith(modulePathPrefix)); + storageNames.makeModulePathRegexPattern(project.projectName)); + for (const modulePath in pathToModuleContent) { await storage.deleteModule(modulePath); } @@ -286,14 +285,8 @@ export async function copyModuleInProject( async function renameOrCopyModule( storage: commonStorage.Storage, project: Project, newClassName: string, oldModule: storageModule.Module, rename: boolean): Promise { - const pathToModuleContent = await storage.listModules( - (modulePath: string) => modulePath === oldModule.modulePath); - if (! (oldModule.modulePath in pathToModuleContent)) { - throw new Error('Failed to find module with path ' + oldModule.modulePath); - } - const newModulePath = storageNames.makeModulePath(project.projectName, newClassName); - const moduleContentText = pathToModuleContent[oldModule.modulePath].getModuleContentText(); + const moduleContentText = await storage.fetchModuleContentText(oldModule.modulePath); await storage.saveModule(newModulePath, moduleContentText); if (rename) { // For rename, delete the old module. @@ -404,9 +397,8 @@ export function findModuleByModulePath(project: Project, modulePath: string): st */ export async function downloadProject( storage: commonStorage.Storage, projectName: string): Promise { - const modulePathPrefix = storageNames.makeModulePathPrefix(projectName); const pathToModuleContent = await storage.listModules( - (modulePath: string) => modulePath.startsWith(modulePathPrefix)); + storageNames.makeModulePathRegexPattern(projectName)); const classNameToModuleContentText: {[className: string]: string} = {}; // value is module content text for (const modulePath in pathToModuleContent) { From 3957e39140970e5a7f147bba2a9daba49b6ecfa5 Mon Sep 17 00:00:00 2001 From: Liz Looney Date: Mon, 11 Aug 2025 21:02:37 -0700 Subject: [PATCH 2/2] Changed downloadProject to take a Project instead of a projectName (string). --- src/reactComponents/Menu.tsx | 2 +- src/storage/project.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/reactComponents/Menu.tsx b/src/reactComponents/Menu.tsx index 64f2a9ff..5a2a4837 100644 --- a/src/reactComponents/Menu.tsx +++ b/src/reactComponents/Menu.tsx @@ -336,7 +336,7 @@ export function Component(props: MenuProps): React.JSX.Element { } try { - const blobUrl = await storageProject.downloadProject(props.storage, props.project.projectName); + const blobUrl = await storageProject.downloadProject(props.storage, props.project); const filename = props.project.projectName + storageNames.UPLOAD_DOWNLOAD_FILE_EXTENSION; // Create a temporary link to download the file diff --git a/src/storage/project.ts b/src/storage/project.ts index 26b9c121..82c84277 100644 --- a/src/storage/project.ts +++ b/src/storage/project.ts @@ -396,9 +396,9 @@ export function findModuleByModulePath(project: Project, modulePath: string): st * Produce the blob for downloading a project. */ export async function downloadProject( - storage: commonStorage.Storage, projectName: string): Promise { + storage: commonStorage.Storage, project: Project): Promise { const pathToModuleContent = await storage.listModules( - storageNames.makeModulePathRegexPattern(projectName)); + storageNames.makeModulePathRegexPattern(project.projectName)); const classNameToModuleContentText: {[className: string]: string} = {}; // value is module content text for (const modulePath in pathToModuleContent) {