diff --git a/src/reactComponents/Menu.tsx b/src/reactComponents/Menu.tsx index 78b4e303..64f2a9ff 100644 --- a/src/reactComponents/Menu.tsx +++ b/src/reactComponents/Menu.tsx @@ -199,7 +199,7 @@ export function Component(props: MenuProps): React.JSX.Element { return; } try { - const array = await props.storage.listProjects(); + const array = await storageProject.listProjects(props.storage); setProjects(array); resolve(array); } catch (e) { @@ -336,7 +336,7 @@ export function Component(props: MenuProps): React.JSX.Element { } try { - const blobUrl = await props.storage.downloadProject(props.project.projectName); + const blobUrl = await storageProject.downloadProject(props.storage, props.project.projectName); const filename = props.project.projectName + storageNames.UPLOAD_DOWNLOAD_FILE_EXTENSION; // Create a temporary link to download the file @@ -389,7 +389,7 @@ export function Component(props: MenuProps): React.JSX.Element { const file = options.file as RcFile; const uploadProjectName = storageProject.makeUploadProjectName(file.name, existingProjectNames); if (props.storage) { - props.storage.uploadProject(uploadProjectName, dataUrl); + storageProject.uploadProject(props.storage, uploadProjectName, dataUrl); } }; reader.onerror = (_error) => { diff --git a/src/reactComponents/ProjectManageModal.tsx b/src/reactComponents/ProjectManageModal.tsx index c3a4285d..4b06b6f6 100644 --- a/src/reactComponents/ProjectManageModal.tsx +++ b/src/reactComponents/ProjectManageModal.tsx @@ -72,7 +72,7 @@ export default function ProjectManageModal(props: ProjectManageModalProps): Reac /** Loads projects from storage and sorts them alphabetically. */ const loadProjects = async (storage: commonStorage.Storage): Promise => { - const projects = await storage.listProjects(); + const projects = await storageProject.listProjects(storage); // Sort projects alphabetically by name projects.sort((a, b) => a.projectName.localeCompare(b.projectName)); diff --git a/src/storage/client_side_storage.ts b/src/storage/client_side_storage.ts index 0dcb83ed..57911d43 100644 --- a/src/storage/client_side_storage.ts +++ b/src/storage/client_side_storage.ts @@ -20,9 +20,7 @@ */ import * as commonStorage from './common_storage'; -import * as storageModule from './module'; -import * as storageNames from './names'; -import * as storageProject from './project'; +import * as storageModuleContent from './module_content'; // Functions for saving blocks modules to client side storage. @@ -127,13 +125,11 @@ class ClientSideStorage implements commonStorage.Storage { }); } - async listProjects(): Promise { + async listModules( + opt_modulePathFilter?: commonStorage.ModulePathFilter): + Promise<{[path: string]: storageModuleContent.ModuleContent}> { return new Promise((resolve, reject) => { - const projects: {[key: string]: storageProject.Project} = {}; // key is project name, value is Project - // The mechanisms and opModes variables hold any Mechanisms and OpModes that - // are read before the Project to which they belong is read. - const mechanisms: {[key: string]: storageModule.Mechanism[]} = {}; // key is project name, value is list of Mechanisms - const opModes: {[key: string]: storageModule.OpMode[]} = {}; // key is project name, value is list of OpModes + const pathToModuleContent: {[path: string]: storageModuleContent.ModuleContent} = {}; const openCursorRequest = this.db.transaction([MODULES_STORE_NAME], 'readonly') .objectStore(MODULES_STORE_NAME) .openCursor(); @@ -146,86 +142,22 @@ class ClientSideStorage implements commonStorage.Storage { const cursor = openCursorRequest.result; if (cursor) { const value = cursor.value; - const path = value.path; - const moduleType = value.type; - const module: storageModule.Module = { - modulePath: path, - moduleType: moduleType, - projectName: storageNames.getProjectName(path), - className: storageNames.getClassName(path), - dateModifiedMillis: value.dateModifiedMillis, - } - if (moduleType === storageModule.MODULE_TYPE_ROBOT) { - const robot: storageModule.Robot = { - ...module, - }; - const project: storageProject.Project = { - projectName: module.projectName, - robot: robot, - mechanisms: [], - opModes: [], - }; - projects[project.projectName] = project; - // Add any Mechanisms that belong to this project that have already - // been read. - if (project.projectName in mechanisms) { - project.mechanisms = mechanisms[project.projectName]; - delete mechanisms[project.projectName]; - } - // Add any OpModes that belong to this project that have already been - // read. - if (project.projectName in opModes) { - project.opModes = opModes[project.projectName]; - delete opModes[project.projectName]; - } - } else if (moduleType === storageModule.MODULE_TYPE_MECHANISM) { - const mechanism: storageModule.Mechanism = { - ...module, - }; - if (mechanism.projectName in projects) { - // If the Project to which this Mechanism belongs has already been read, - // add this Mechanism to it. - projects[mechanism.projectName].mechanisms.push(mechanism); - } else { - // Otherwise, add this Mechanism to the mechanisms local variable. - if (mechanism.projectName in mechanisms) { - mechanisms[mechanism.projectName].push(mechanism); - } else { - mechanisms[mechanism.projectName] = [mechanism]; - } - } - } else if (moduleType === storageModule.MODULE_TYPE_OPMODE) { - const opMode: storageModule.OpMode = { - ...module, - }; - if (opMode.projectName in projects) { - // If the Project to which this OpMode belongs has already been read, - // add this OpMode to it. - projects[opMode.projectName].opModes.push(opMode); - } else { - // Otherwise, add this OpMode to the opModes local variable. - if (opMode.projectName in opModes) { - opModes[opMode.projectName].push(opMode); - } else { - opModes[opMode.projectName] = [opMode]; - } - } + // 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)) { + const moduleContent = storageModuleContent.parseModuleContentText(value.content); + pathToModuleContent[modulePath] = moduleContent; } cursor.continue(); } else { // The cursor is done. We have finished reading all the modules. - const projectsToReturn: storageProject.Project[] = []; - const sortedProjectNames = Object.keys(projects).sort(); - sortedProjectNames.forEach((projectName) => { - projectsToReturn.push(projects[projectName]); - }); - resolve(projectsToReturn); + resolve(pathToModuleContent); } }; }); } - async fetchModuleContentText(modulePath: string): Promise { + async fetchModuleDateModifiedMillis(modulePath: string): Promise { return new Promise((resolve, reject) => { const getRequest = this.db.transaction([MODULES_STORE_NAME], 'readonly') .objectStore(MODULES_STORE_NAME).get(modulePath); @@ -240,192 +172,32 @@ class ClientSideStorage implements commonStorage.Storage { reject(new Error('IndexedDB get request succeeded, but the module does not exist.')); return; } - resolve(getRequest.result.content); + resolve(getRequest.result.dateModifiedMillis); }; }); } - async createProject(projectName: string, robotContent: string, opmodeContent : string): Promise { - const modulePath = storageNames.makeRobotPath(projectName); - const opmodePath = storageNames.makeModulePath(projectName, storageNames.CLASS_NAME_TELEOP); - - await this._saveModule(storageModule.MODULE_TYPE_ROBOT, modulePath, robotContent); - await this._saveModule(storageModule.MODULE_TYPE_OPMODE, opmodePath, opmodeContent); - } - - async createModule(moduleType: string, modulePath: string, moduleContentText: string): Promise { - return this._saveModule(moduleType, modulePath, moduleContentText); - } - - async saveModule(modulePath: string, moduleContentText: string): Promise { - return this._saveModule('', modulePath, moduleContentText); - } - - private async _saveModule(moduleType: string, modulePath: string, moduleContentText: string) - : Promise { - // When creating a new module, moduleType must be truthy. - // When saving an existing module, the moduleType must be falsy. + async fetchModuleContentText(modulePath: string): Promise { return new Promise((resolve, reject) => { - const transaction = this.db.transaction([MODULES_STORE_NAME], 'readwrite'); - transaction.oncomplete = () => { - resolve(); - }; - transaction.onabort = () => { - console.log('IndexedDB transaction aborted.'); - reject(new Error('IndexedDB transaction aborted.')); - }; - const modulesObjectStore = transaction.objectStore(MODULES_STORE_NAME); - const getRequest = modulesObjectStore.get(modulePath); + const getRequest = this.db.transaction([MODULES_STORE_NAME], 'readonly') + .objectStore(MODULES_STORE_NAME).get(modulePath); getRequest.onerror = () => { console.log('IndexedDB get request failed. getRequest.error is...'); console.log(getRequest.error); - throw new Error('IndexedDB get request failed.'); + reject(new Error('IndexedDB get request failed.')); }; getRequest.onsuccess = () => { - let value; if (getRequest.result === undefined) { - // The module does not exist. - // Let's make sure that's what we expected. - if (!moduleType) { - // If moduleType is not truthy, we are trying to save an existing module. - // It is unexpected that the module does not exist. - console.log('IndexedDB get request succeeded, but the module does not exist.'); - throw new Error('IndexedDB get request succeeded, but the module does not exist.'); - } - value = Object.create(null); - value.path = modulePath; - value.type = moduleType; - } else { - // The module already exists. - // Let's make sure if that's what we expected. - if (moduleType) { - // Since moduleType is truthy, we are trying to create a new module. - // It is unexpected that the module already exists. - console.log('IndexedDB get request succeeded, but the module already exist.'); - throw new Error('IndexedDB get request succeeded, but the module already exists.'); - } - value = getRequest.result; - } - value.content = moduleContentText; - value.dateModifiedMillis = Date.now(); - const putRequest = modulesObjectStore.put(value); - putRequest.onerror = () => { - console.log('IndexedDB put request failed. putRequest.error is...'); - console.log(putRequest.error); - throw new Error('IndexedDB put request failed.'); - }; - }; - }); - } - - private async _renameOrCopyProject(oldProjectName: string, newProjectName: string, copy: boolean): Promise { - return new Promise((resolve, reject) => { - const transaction = this.db.transaction([MODULES_STORE_NAME], 'readwrite'); - transaction.oncomplete = () => { - resolve(); - }; - transaction.onabort = () => { - console.log('IndexedDB transaction aborted.'); - reject(new Error('IndexedDB transaction aborted.')); - }; - const modulesObjectStore = transaction.objectStore(MODULES_STORE_NAME); - // First get the list of modules in the project. - const oldToNewModulePaths: {[key: string]: string} = {}; - const openCursorRequest = modulesObjectStore.openCursor(); - openCursorRequest.onerror = () => { - console.log('IndexedDB openCursor request failed. openCursorRequest.error is...'); - console.log(openCursorRequest.error); - throw new Error('IndexedDB openCursor request failed.'); - }; - openCursorRequest.onsuccess = () => { - const cursor = openCursorRequest.result; - if (cursor) { - const value = cursor.value; - const path = value.path; - const moduleType = value.type; - if (storageNames.getProjectName(path) === oldProjectName) { - let newPath; - if (moduleType === storageModule.MODULE_TYPE_ROBOT) { - newPath = storageNames.makeRobotPath(newProjectName); - } else { - const className = storageNames.getClassName(path); - newPath = storageNames.makeModulePath(newProjectName, className); - } - oldToNewModulePaths[path] = newPath; - } - cursor.continue(); - } else { - // Now rename the project for each of the modules. - Object.entries(oldToNewModulePaths).forEach(([oldModulePath, newModulePath]) => { - const getRequest = modulesObjectStore.get(oldModulePath); - getRequest.onerror = () => { - console.log('IndexedDB get request failed. getRequest.error is...'); - console.log(getRequest.error); - throw new Error('IndexedDB get request failed.'); - }; - getRequest.onsuccess = () => { - if (getRequest.result === undefined) { - console.log('IndexedDB get request succeeded, but the module does not exist.'); - throw new Error('IndexedDB get request succeeded, but the module does not exist.'); - } - const value = getRequest.result; - value.path = newModulePath; - value.dateModifiedMillis = Date.now(); - const putRequest = modulesObjectStore.put(value); - putRequest.onerror = () => { - console.log('IndexedDB put request failed. putRequest.error is...'); - console.log(putRequest.error); - throw new Error('IndexedDB put request failed.'); - }; - putRequest.onsuccess = () => { - if (!copy) { - const deleteRequest = modulesObjectStore.delete(oldModulePath); - deleteRequest.onerror = () => { - console.log('IndexedDB delete request failed. deleteRequest.error is...'); - console.log(deleteRequest.error); - throw new Error('IndexedDB delete request failed.'); - }; - } - }; - }; - }); + // Module does not exist. + reject(new Error('IndexedDB get request succeeded, but the module does not exist.')); + return; } + resolve(getRequest.result.content); }; }); } - async renameProject(oldProjectName: string, newProjectName: string): Promise { - return this._renameOrCopyProject(oldProjectName, newProjectName, false); - } - - async copyProject(oldProjectName: string, newProjectName: string): Promise { - return this._renameOrCopyProject(oldProjectName, newProjectName, true); - } - - async renameModule( - moduleType: string, projectName: string, - oldClassName: string, newClassName: string): Promise { - if (moduleType == storageModule.MODULE_TYPE_ROBOT) { - throw new Error('Renaming the robot module is not allowed. Call renameProject to rename the project.'); - } - return this._renameOrCopyModule( - projectName, oldClassName, newClassName, false); - } - - async copyModule( - moduleType: string, projectName: string, - oldClassName: string, newClassName: string): Promise { - if (moduleType == storageModule.MODULE_TYPE_ROBOT) { - throw new Error('Copying the robot module is not allowed. Call copyProject to rename the project.'); - } - return this._renameOrCopyModule( - projectName, oldClassName, newClassName, true); - } - - private async _renameOrCopyModule( - projectName: string, - oldClassName: string, newClassName: string, copy: boolean): Promise { - + async saveModule(modulePath: string, moduleContentText: string): Promise { return new Promise((resolve, reject) => { const transaction = this.db.transaction([MODULES_STORE_NAME], 'readwrite'); transaction.oncomplete = () => { @@ -436,22 +208,23 @@ class ClientSideStorage implements commonStorage.Storage { reject(new Error('IndexedDB transaction aborted.')); }; const modulesObjectStore = transaction.objectStore(MODULES_STORE_NAME); - const oldModulePath = storageNames.makeModulePath(projectName, oldClassName); - const newModulePath = storageNames.makeModulePath(projectName, newClassName); - const getRequest = modulesObjectStore.get(oldModulePath); + const getRequest = modulesObjectStore.get(modulePath); getRequest.onerror = () => { console.log('IndexedDB get request failed. getRequest.error is...'); console.log(getRequest.error); throw new Error('IndexedDB get request failed.'); }; getRequest.onsuccess = () => { + let value; if (getRequest.result === undefined) { - console.log('IndexedDB get request succeeded, but the module does not exist.'); - throw new Error('IndexedDB get request succeeded, but the module does not exist.'); - return; + // The module does not exist. Create it now. + value = Object.create(null); + value.path = modulePath; + } else { + // The module already exists. + value = getRequest.result; } - const value = getRequest.result; - value.path = newModulePath; + value.content = moduleContentText; value.dateModifiedMillis = Date.now(); const putRequest = modulesObjectStore.put(value); putRequest.onerror = () => { @@ -459,72 +232,11 @@ class ClientSideStorage implements commonStorage.Storage { console.log(putRequest.error); throw new Error('IndexedDB put request failed.'); }; - putRequest.onsuccess = () => { - if (!copy) { - const deleteRequest = modulesObjectStore.delete(oldModulePath); - deleteRequest.onerror = () => { - console.log('IndexedDB delete request failed. deleteRequest.error is...'); - console.log(deleteRequest.error); - throw new Error('IndexedDB delete request failed.'); - }; - deleteRequest.onsuccess = () => { - }; - } - }; - }; - }); - } - - async deleteProject(projectName: string): Promise { - return new Promise((resolve, reject) => { - const transaction = this.db.transaction([MODULES_STORE_NAME], 'readwrite'); - transaction.oncomplete = () => { - resolve(); - }; - transaction.onabort = () => { - console.log('IndexedDB transaction aborted.'); - reject(new Error('IndexedDB transaction aborted.')); - }; - const modulesObjectStore = transaction.objectStore(MODULES_STORE_NAME); - // First get the list of modulePaths in the project. - const modulePaths: string[] = []; - const openCursorRequest = modulesObjectStore.openCursor(); - openCursorRequest.onerror = () => { - console.log('IndexedDB openCursor request failed. openCursorRequest.error is...'); - console.log(openCursorRequest.error); - throw new Error('IndexedDB openCursor request failed.'); - }; - openCursorRequest.onsuccess = () => { - const cursor = openCursorRequest.result; - if (cursor) { - const value = cursor.value; - const path = value.path; - if (storageNames.getProjectName(path) === projectName) { - modulePaths.push(path); - } - cursor.continue(); - } else { - // Now delete each of the modules. - modulePaths.forEach((modulePath) => { - const deleteRequest = modulesObjectStore.delete(modulePath); - deleteRequest.onerror = () => { - console.log('IndexedDB delete request failed. deleteRequest.error is...'); - console.log(deleteRequest.error); - throw new Error('IndexedDB delete request failed.'); - }; - deleteRequest.onsuccess = () => { - }; - }); - } }; }); } - async deleteModule(moduleType: string, modulePath: string): Promise { - if (moduleType == storageModule.MODULE_TYPE_ROBOT) { - throw new Error('Deleting the robot module is not allowed. Call deleteProject to delete the project.'); - } - + async deleteModule(modulePath: string): Promise { return new Promise((resolve, reject) => { const transaction = this.db.transaction([MODULES_STORE_NAME], 'readwrite'); transaction.oncomplete = () => { @@ -545,92 +257,4 @@ class ClientSideStorage implements commonStorage.Storage { }; }); } - - async downloadProject(projectName: string): Promise { - return new Promise((resolve, reject) => { - // Collect all the modules in the project. - const classNameToModuleContentText: {[className: string]: string} = {}; // key is class name, value is module content - const openCursorRequest = this.db.transaction([MODULES_STORE_NAME], 'readonly') - .objectStore(MODULES_STORE_NAME) - .openCursor(); - openCursorRequest.onerror = () => { - console.log('IndexedDB openCursor request failed. openCursorRequest.error is...'); - console.log(openCursorRequest.error); - reject(new Error('IndexedDB openCursor request failed.')); - }; - openCursorRequest.onsuccess = async () => { - const cursor = openCursorRequest.result; - if (cursor) { - const value = cursor.value; - if (storageNames.getProjectName(value.path) === projectName) { - const className = storageNames.getClassName(value.path); - classNameToModuleContentText[className] = value.content; - } - cursor.continue(); - } else { - // The cursor is done. We have finished collecting all the modules in the project. - // Now create the blob for download. - const blobUrl = await storageProject.produceDownloadProjectBlob(classNameToModuleContentText); - resolve(blobUrl); - } - }; - }); - } - - async uploadProject(projectName: string, blobUrl: string): Promise { - return new Promise(async (resolve, reject) => { - // Process the uploaded blob to get the module types and contents. - let classNameToModuleType: {[className: string]: string}; // key is class name, value is module type - let classNameToModuleContentText: {[className: string]: string}; // key is class name, value is module content - try { - [classNameToModuleType, classNameToModuleContentText] = await storageProject.processUploadedBlob( - blobUrl); - } catch (e) { - console.log('storageProject.processUploadedBlob failed.'); - reject(new Error('storageProject.processUploadedBlob failed.')); - return; - } - - // Save each module. - const transaction = this.db.transaction([MODULES_STORE_NAME], 'readwrite'); - transaction.oncomplete = () => { - resolve(); - }; - transaction.onabort = () => { - console.log('IndexedDB transaction aborted.'); - reject(new Error('IndexedDB transaction aborted.')); - }; - const modulesObjectStore = transaction.objectStore(MODULES_STORE_NAME); - - for (const className in classNameToModuleType) { - const moduleType = classNameToModuleType[className]; - const moduleContentText = classNameToModuleContentText[className]; - const modulePath = storageNames.makeModulePath(projectName, className); - const getRequest = modulesObjectStore.get(modulePath); - getRequest.onerror = () => { - console.log('IndexedDB get request failed. getRequest.error is...'); - console.log(getRequest.error); - throw new Error('IndexedDB get request failed.'); - }; - getRequest.onsuccess = () => { - if (getRequest.result !== undefined) { - // The module already exists. That is not expected! - console.log('IndexedDB get request succeeded, but the module already exists.'); - throw new Error('IndexedDB get request succeeded, but the module already exists.'); - } - const value = Object.create(null); - value.path = modulePath; - value.type = moduleType; - value.content = moduleContentText; - value.dateModifiedMillis = Date.now(); - const putRequest = modulesObjectStore.put(value); - putRequest.onerror = () => { - console.log('IndexedDB put request failed. putRequest.error is...'); - console.log(putRequest.error); - throw new Error('IndexedDB put request failed.'); - }; - }; - } - }); - } } diff --git a/src/storage/common_storage.ts b/src/storage/common_storage.ts index 649124ca..e0395242 100644 --- a/src/storage/common_storage.ts +++ b/src/storage/common_storage.ts @@ -19,23 +19,16 @@ * @author lizlooney@google.com (Liz Looney) */ -import * as storageProject from './project'; +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; - listProjects(): Promise; + listModules(opt_modulePathFilter?: ModulePathFilter): Promise<{[path: string]: storageModuleContent.ModuleContent}>; + fetchModuleDateModifiedMillis(modulePath: string): Promise; fetchModuleContentText(modulePath: string): Promise; - createProject(projectName: string, robotContent: string, opmodeContent: string): Promise; - createModule(moduleType: string, modulePath: string, moduleContentText: string): Promise; saveModule(modulePath: string, moduleContentText: string): Promise; - renameProject(oldProjectName: string, newProjectName: string): Promise; - copyProject(oldProjectName: string, newProjectName: string): Promise; - renameModule(moduleType: string, projectName: string, oldClassName: string, newClassName: string): Promise; - copyModule(moduleType: string, projectName: string, oldClassName: string, newClassName: string): Promise; - deleteProject(projectName: string): Promise; - deleteModule(moduleType: string, modulePath: string): Promise; - downloadProject(projectName: string): Promise; - uploadProject(projectName: string, blobUrl: string): Promise; + deleteModule(modulePath: string): Promise; } - diff --git a/src/storage/names.ts b/src/storage/names.ts index f15ff361..29ebcfa5 100644 --- a/src/storage/names.ts +++ b/src/storage/names.ts @@ -75,11 +75,18 @@ export function snakeCaseToPascalCase(snakeCaseName: string): string { return pascalCaseName; } +/** + * Returns the module path prefix for the given project name. + */ +export function makeModulePathPrefix(projectName: string): string { + return projectName + '/'; +} + /** * Returns the module path for the given project name and class name. */ export function makeModulePath(projectName: string, className: string): string { - return projectName + '/' + className + JSON_FILE_EXTENSION;; + return projectName + '/' + className + JSON_FILE_EXTENSION; } /** diff --git a/src/storage/project.ts b/src/storage/project.ts index 999996b2..1187a19c 100644 --- a/src/storage/project.ts +++ b/src/storage/project.ts @@ -35,6 +35,86 @@ export type Project = { opModes: storageModule.OpMode[], }; +export async function listProjects(storage: commonStorage.Storage): Promise { + const pathToModuleContent = await storage.listModules(); + + const projects: {[key: string]: Project} = {}; // key is project name, value is Project + // The mechanisms and opModes variables hold any Mechanisms and OpModes that + // are read before the Project to which they belong is read. + const mechanisms: {[key: string]: storageModule.Mechanism[]} = {}; // key is project name, value is list of Mechanisms + const opModes: {[key: string]: storageModule.OpMode[]} = {}; // key is project name, value is list of OpModes + + for (const modulePath in pathToModuleContent) { + const moduleContent = pathToModuleContent[modulePath]; + const moduleType = moduleContent.getModuleType(); + const dateModifiedMillis = await storage.fetchModuleDateModifiedMillis(modulePath); + const module: storageModule.Module = { + modulePath: modulePath, + moduleType: moduleType, + projectName: storageNames.getProjectName(modulePath), + className: storageNames.getClassName(modulePath), + dateModifiedMillis: dateModifiedMillis, + }; + if (moduleType === storageModule.MODULE_TYPE_ROBOT) { + const robot: storageModule.Robot = module as storageModule.Robot; + const project: Project = { + projectName: module.projectName, + robot: robot, + mechanisms: [], + opModes: [], + }; + projects[project.projectName] = project; + // Add any Mechanisms that belong to this project that have already + // been read. + if (project.projectName in mechanisms) { + project.mechanisms = mechanisms[project.projectName]; + delete mechanisms[project.projectName]; + } + // Add any OpModes that belong to this project that have already been + // read. + if (project.projectName in opModes) { + project.opModes = opModes[project.projectName]; + delete opModes[project.projectName]; + } + } else if (moduleType === storageModule.MODULE_TYPE_MECHANISM) { + const mechanism: storageModule.Mechanism = module as storageModule.Mechanism; + if (mechanism.projectName in projects) { + // If the Project to which this Mechanism belongs has already been read, + // add this Mechanism to it. + projects[mechanism.projectName].mechanisms.push(mechanism); + } else { + // Otherwise, add this Mechanism to the mechanisms local variable. + if (mechanism.projectName in mechanisms) { + mechanisms[mechanism.projectName].push(mechanism); + } else { + mechanisms[mechanism.projectName] = [mechanism]; + } + } + } else if (moduleType === storageModule.MODULE_TYPE_OPMODE) { + const opMode: storageModule.OpMode = module as storageModule.OpMode; + if (opMode.projectName in projects) { + // If the Project to which this OpMode belongs has already been read, + // add this OpMode to it. + projects[opMode.projectName].opModes.push(opMode); + } else { + // Otherwise, add this OpMode to the opModes local variable. + if (opMode.projectName in opModes) { + opModes[opMode.projectName].push(opMode); + } else { + opModes[opMode.projectName] = [opMode]; + } + } + } + } + + const projectsList: Project[] = []; + const sortedProjectNames = Object.keys(projects).sort(); + sortedProjectNames.forEach((projectName) => { + projectsList.push(projects[projectName]); + }); + return projectsList; +} + /** * Creates a new project. * @param storage The storage interface to use for creating the project. @@ -42,10 +122,14 @@ export type Project = { * @returns A promise that resolves when the project has been created. */ export async function createProject( - storage: commonStorage.Storage, newProjectName: string): Promise { + storage: commonStorage.Storage, newProjectName: string): Promise { + const modulePath = storageNames.makeRobotPath(newProjectName); const robotContent = storageModuleContent.newRobotContent(newProjectName); + await storage.saveModule(modulePath, robotContent); + + const opmodePath = storageNames.makeModulePath(newProjectName, storageNames.CLASS_NAME_TELEOP); const opmodeContent = storageModuleContent.newOpModeContent(newProjectName, storageNames.CLASS_NAME_TELEOP); - await storage.createProject(newProjectName, robotContent, opmodeContent); + await storage.saveModule(opmodePath, opmodeContent); } /** @@ -56,8 +140,8 @@ export async function createProject( * @returns A promise that resolves when the project has been renamed. */ export async function renameProject( - storage: commonStorage.Storage, project: Project, newProjectName: string): Promise { - await storage.renameProject(project.projectName, newProjectName); + storage: commonStorage.Storage, project: Project, newProjectName: string): Promise { + await renameOrCopyProject(storage, project, newProjectName, true); } /** @@ -68,8 +152,26 @@ export async function renameProject( * @returns A promise that resolves when the project has been copied. */ export async function copyProject( - storage: commonStorage.Storage, project: Project, newProjectName: string): Promise { - await storage.copyProject(project.projectName, newProjectName); + storage: commonStorage.Storage, project: Project, newProjectName: string): Promise { + await renameOrCopyProject(storage, project, newProjectName, false); +} + +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)); + + for (const modulePath in pathToModuleContent) { + const className = storageNames.getClassName(modulePath); + const newModulePath = storageNames.makeModulePath(newProjectName, className); + const moduleContentText = pathToModuleContent[modulePath].getModuleContentText(); + await storage.saveModule(newModulePath, moduleContentText); + if (rename) { + await storage.deleteModule(modulePath); + } + } } /** @@ -79,8 +181,13 @@ export async function copyProject( * @returns A promise that resolves when the project has been deleted. */ export async function deleteProject( - storage: commonStorage.Storage, project: Project): Promise { - await storage.deleteProject(project.projectName); + storage: commonStorage.Storage, project: Project): Promise { + const modulePathPrefix = storageNames.makeModulePathPrefix(project.projectName); + const pathToModuleContent = await storage.listModules( + (modulePath: string) => modulePath.startsWith(modulePathPrefix)); + for (const modulePath in pathToModuleContent) { + await storage.deleteModule(modulePath); + } } /** @@ -96,7 +203,7 @@ export async function addModuleToProject( if (moduleType === storageModule.MODULE_TYPE_MECHANISM) { const mechanismContent = storageModuleContent.newMechanismContent(project.projectName, newClassName); - await storage.createModule(storageModule.MODULE_TYPE_MECHANISM, newModulePath, mechanismContent); + await storage.saveModule(newModulePath, mechanismContent); project.mechanisms.push({ modulePath: newModulePath, moduleType: storageModule.MODULE_TYPE_MECHANISM, @@ -105,7 +212,7 @@ export async function addModuleToProject( } as storageModule.Mechanism); } else if (moduleType === storageModule.MODULE_TYPE_OPMODE) { const opModeContent = storageModuleContent.newOpModeContent(project.projectName, newClassName); - await storage.createModule(storageModule.MODULE_TYPE_OPMODE, newModulePath, opModeContent); + await storage.saveModule(newModulePath, opModeContent); project.opModes.push({ modulePath: newModulePath, moduleType: storageModule.MODULE_TYPE_OPMODE, @@ -127,7 +234,7 @@ export async function removeModuleFromProject( if (module.moduleType == storageModule.MODULE_TYPE_ROBOT) { throw new Error('Removing the robot module from the project is not allowed.'); } - await storage.deleteModule(module.moduleType, modulePath); + await storage.deleteModule(modulePath); if (module.moduleType === storageModule.MODULE_TYPE_MECHANISM) { project.mechanisms = project.mechanisms.filter(m => m.modulePath !== modulePath); } else if (module.moduleType === storageModule.MODULE_TYPE_OPMODE) { @@ -142,67 +249,80 @@ export async function removeModuleFromProject( * @param project The project containing the module to rename. * @param newClassName The new name for the module. For example, GamePieceShooter. * @param oldModulePath The current path of the module. - * @returns A promise that resolves when the module has been renamed. + * @returns The new path of the module, as a promise that resolves when the module has been copied. */ export async function renameModuleInProject( - storage: commonStorage.Storage, project: Project, newClassName: string, oldModulePath: string): Promise { + storage: commonStorage.Storage, project: Project, newClassName: string, oldModulePath: string): Promise { const module = findModuleByModulePath(project, oldModulePath); - if (module) { - if (module.moduleType == storageModule.MODULE_TYPE_ROBOT) { - throw new Error('Renaming the robot module is not allowed.'); - } - const newModulePath = storageNames.makeModulePath(project.projectName, newClassName); - await storage.renameModule(module.moduleType, project.projectName, module.className, newClassName); - module.modulePath = newModulePath; - module.className = newClassName; - module.className = newClassName; - - if (module.moduleType === storageModule.MODULE_TYPE_MECHANISM) { - const mechanism = project.mechanisms.find(m => m.modulePath === module.modulePath); - if (mechanism) { - mechanism.modulePath = newModulePath; - mechanism.className = newClassName; - mechanism.className = newClassName; - } - return newModulePath; - } else if (module.moduleType === storageModule.MODULE_TYPE_OPMODE) { - const opMode = project.opModes.find(o => o.modulePath === module.modulePath); - if (opMode) { - opMode.modulePath = newModulePath; - opMode.className = newClassName; - opMode.className = newClassName; - } - return newModulePath - } + if (!module) { + throw new Error('Failed to find module with path ' + oldModulePath); + } + if (module.moduleType == storageModule.MODULE_TYPE_ROBOT) { + throw new Error('Renaming the robot module is not allowed.'); } - return ''; + return await renameOrCopyModule(storage, project, newClassName, module, true); } + /** * Copies a module in the project. * @param storage The storage interface to use for copying the module. * @param project The project containing the module to copy. * @param newClassName The new name for the module. For example, GamePieceShooter. * @param oldModulePath The current path of the module. - * @returns A promise that resolves when the module has been copied. + * @returns The new path of the module, as a promise that resolves when the module has been copied. */ export async function copyModuleInProject( - storage: commonStorage.Storage, project: Project, newClassName: string, oldModulePath: string): Promise { + storage: commonStorage.Storage, project: Project, newClassName: string, oldModulePath: string): Promise { const module = findModuleByModulePath(project, oldModulePath); - if (module) { - if (module.moduleType == storageModule.MODULE_TYPE_ROBOT) { - throw new Error('Copying the robot module is not allowed.'); - } - const newModulePath = storageNames.makeModulePath(project.projectName, newClassName); - await storage.copyModule(module.moduleType, project.projectName, module.className, newClassName); + if (!module) { + throw new Error('Failed to find module with path ' + oldModulePath); + } + if (module.moduleType == storageModule.MODULE_TYPE_ROBOT) { + throw new Error('Copying the robot module is not allowed.'); + } + return await renameOrCopyModule(storage, project, newClassName, module, false); +} - if (module.moduleType === storageModule.MODULE_TYPE_MECHANISM) { +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(); + await storage.saveModule(newModulePath, moduleContentText); + if (rename) { + // For rename, delete the old module. + await storage.deleteModule(oldModule.modulePath); + + // Update the project's mechanisms or opModes. + if (oldModule.moduleType === storageModule.MODULE_TYPE_MECHANISM) { + const mechanism = project.mechanisms.find(m => m.modulePath === oldModule.modulePath); + if (mechanism) { + mechanism.modulePath = newModulePath; + mechanism.className = newClassName; + } + } else if (oldModule.moduleType === storageModule.MODULE_TYPE_OPMODE) { + const opMode = project.opModes.find(o => o.modulePath === oldModule.modulePath); + if (opMode) { + opMode.modulePath = newModulePath; + opMode.className = newClassName; + } + } + } else { + // Update the project's mechanisms or opModes. + if (oldModule.moduleType === storageModule.MODULE_TYPE_MECHANISM) { project.mechanisms.push({ modulePath: newModulePath, moduleType: storageModule.MODULE_TYPE_MECHANISM, projectName: project.projectName, className: newClassName } as storageModule.Mechanism); - } else if (module.moduleType === storageModule.MODULE_TYPE_OPMODE) { + } else if (oldModule.moduleType === storageModule.MODULE_TYPE_OPMODE) { project.opModes.push({ modulePath: newModulePath, moduleType: storageModule.MODULE_TYPE_OPMODE, @@ -210,9 +330,9 @@ export async function copyModuleInProject( className: newClassName } as storageModule.OpMode); } - return newModulePath; } - return ''; + + return newModulePath; } /** @@ -282,8 +402,19 @@ export function findModuleByModulePath(project: Project, modulePath: string): st /** * Produce the blob for downloading a project. */ -export async function produceDownloadProjectBlob( - classNameToModuleContentText: { [key: string]: string }): Promise { +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)); + + const classNameToModuleContentText: {[className: string]: string} = {}; // value is module content text + for (const modulePath in pathToModuleContent) { + const className = storageNames.getClassName(modulePath); + const moduleContentText = pathToModuleContent[modulePath].getModuleContentText(); + classNameToModuleContentText[className] = moduleContentText; + } + const zip = new JSZip(); for (const className in classNameToModuleContentText) { const moduleContentText = classNameToModuleContentText[className]; @@ -291,8 +422,7 @@ export async function produceDownloadProjectBlob( zip.file(filename, moduleContentText); } const content = await zip.generateAsync({ type: "blob" }); - const blobUrl = URL.createObjectURL(content); - return blobUrl; + return URL.createObjectURL(content); } /** @@ -305,13 +435,23 @@ export function makeUploadProjectName( return storageNames.makeUniqueName(preferredName, existingProjectNames); } +export async function uploadProject( + storage: commonStorage.Storage, projectName: string, blobUrl: string): Promise { + // Process the uploaded blob to get the module types and contents. + const classNameToModuleContentText = await processUploadedBlob(blobUrl); + + // Save each module. + for (const className in classNameToModuleContentText) { + const moduleContentText = classNameToModuleContentText[className]; + const modulePath = storageNames.makeModulePath(projectName, className); + await storage.saveModule(modulePath, moduleContentText); + } +} + /** - * Process the uploaded blob to get the module types and contents. - * Returns a promise of classNameToModuleType and classNameToModuleContentText. + * Process the uploaded blob to get the module class names and contents. */ -export async function processUploadedBlob( - blobUrl: string) - : Promise<[{ [className: string]: string }, { [className: string]: string }]> { +async function processUploadedBlob(blobUrl: string): Promise<{ [className: string]: string }> { const prefix = 'data:application/octet-stream;base64,'; if (!blobUrl.startsWith(prefix)) { @@ -337,28 +477,21 @@ export async function processUploadedBlob( ); // Process each module's content. - const classNameToModuleType: { [className: string]: string } = {}; // key is class name, value is module type - const classNameToModuleContentText: { [className: string]: string } = {}; // key is class name, value is module content text + let foundRobot = false; + const classNameToModuleContentText: { [className: string]: string } = {}; // value is module content text for (const filename in files) { - const uploadedContent = files[filename]; - const [className, moduleType, moduleContent] = processUploadedModule( - filename, uploadedContent); - classNameToModuleType[className] = moduleType; - classNameToModuleContentText[className] = moduleContent; + const className = filename; + if (className === storageNames.CLASS_NAME_ROBOT) { + foundRobot = true; + } + // Make sure we can parse the content. + const moduleContent = storageModuleContent.parseModuleContentText(files[filename]); + classNameToModuleContentText[className] = moduleContent.getModuleContentText(); } - return [classNameToModuleType, classNameToModuleContentText]; -} + if (!foundRobot) { + throw new Error('Uploaded file did not contain a Robot.'); + } -/** - * Processes an uploaded module to get the class name, type, and content text. - */ -function processUploadedModule( - filename: string, uploadedContent: string): [string, string, string] { - - const moduleContent = storageModuleContent.parseModuleContentText(uploadedContent); - const moduleType = moduleContent.getModuleType(); - const className = filename; - const moduleContentText = moduleContent.getModuleContentText(); - return [className, moduleType, moduleContentText]; + return classNameToModuleContentText; }