From fda6942d1a2a4e9fee4fc9b93a297dac57e593ba Mon Sep 17 00:00:00 2001 From: Richard Killen Date: Fri, 28 Jan 2022 15:00:13 -0600 Subject: [PATCH 1/3] Add "Save As..." option to file menu; fix crash when project model file missing --- electron/app/js/ipcRendererPreload.js | 2 + electron/app/js/model.js | 8 ++- electron/app/js/project.js | 33 ++++++++++- electron/app/js/wktWindow.js | 14 +++++ electron/app/locales/en/electron.json | 5 ++ electron/app/locales/en/webui.json | 2 + electron/app/main.js | 4 ++ webui/src/js/models/wdt-model-definition.js | 12 +++- webui/src/js/utils/project-io.js | 61 ++++++++++++++------- webui/src/js/windowStateUtils.js | 7 +++ 10 files changed, 121 insertions(+), 27 deletions(-) diff --git a/electron/app/js/ipcRendererPreload.js b/electron/app/js/ipcRendererPreload.js index c38a89b40..0ec54f089 100644 --- a/electron/app/js/ipcRendererPreload.js +++ b/electron/app/js/ipcRendererPreload.js @@ -55,6 +55,7 @@ contextBridge.exposeInMainWorld( 'start-add-archive-file', 'start-close-project', 'start-save-project', + 'start-save-project-as', 'start-offline-discover', 'start-online-discover', 'show-console-out-line', @@ -129,6 +130,7 @@ contextBridge.exposeInMainWorld( 'get-url-catalog', 'get-wdt-domain-types', 'get-image-contents', + 'choose-project-file', 'confirm-project-file', 'prompt-save-before-close', 'close-project', diff --git a/electron/app/js/model.js b/electron/app/js/model.js index ab7462097..0cde15824 100644 --- a/electron/app/js/model.js +++ b/electron/app/js/model.js @@ -1,6 +1,6 @@ /** * @license - * Copyright (c) 2021, Oracle and/or its affiliates. + * Copyright (c) 2021, 2022, Oracle and/or its affiliates. * Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. */ const {app, dialog} = require('electron'); @@ -95,8 +95,10 @@ async function saveContentsOfModelFiles(projectDirectory, models) { async function _getModelFileContent(projectDirectory, modelFile) { const effectiveModelFile = fsUtils.getAbsolutePath(modelFile, projectDirectory); - return new Promise(resolve => { - readFile(effectiveModelFile, {encoding: 'utf8'}).then(data => resolve(data)); + return new Promise((resolve, reject) => { + readFile(effectiveModelFile, {encoding: 'utf8'}) + .then(data => resolve(data)) + .catch(err => reject(err)); }); } diff --git a/electron/app/js/project.js b/electron/app/js/project.js index e5dc87cab..310af6303 100644 --- a/electron/app/js/project.js +++ b/electron/app/js/project.js @@ -1,6 +1,6 @@ /** * @license - * Copyright (c) 2021, Oracle and/or its affiliates. + * Copyright (c) 2021, 2022, Oracle and/or its affiliates. * Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. */ const {app, dialog} = require('electron'); @@ -91,7 +91,15 @@ async function openProject(targetWindow) { return; } - await openProjectFile(targetWindow, openResponse.filePaths[0]); + const projectFileName = openResponse.filePaths[0]; + await openProjectFile(targetWindow, projectFileName) + .catch(err => { + dialog.showErrorBox( + i18n.t('dialog-openProjectFileErrorTitle'), + i18n.t('dialog-openProjectFileErrorMessage', { projectFileName: projectFileName, err: err }), + ); + getLogger().error('Failed to open project file %s: %s', projectFileName, err); + }); } async function openProjectFile(targetWindow, projectFile) { @@ -133,12 +141,29 @@ async function confirmProjectFile(targetWindow) { return [projectFile, projectName, projectUuid]; } +// choose a new project file for save. +// return null values if no project file was established or selected. +// usually called by the choose-project-file IPC invocation. +async function chooseProjectFile(targetWindow) { + const projectFile = await _chooseProjectSaveFile(targetWindow); + const projectName = projectFile ? _generateProjectName(projectFile) : null; + const projectUuid = projectFile ? _generateProjectUuid() : null; + app.addRecentDocument(projectFile); + return [projectFile, projectName, projectUuid]; +} + // initiate the save process by sending a message to the web app. // usually called from a menu click. function startSaveProject(targetWindow) { sendToWindow(targetWindow, 'start-save-project'); } +// initiate the save-as process by sending a message to the web app. +// usually called from a menu click. +function startSaveProjectAs(targetWindow) { + sendToWindow(targetWindow, 'start-save-project-as'); +} + // save the specified project and model contents to the project file. // usually invoked by the save-project IPC invocation. async function saveProject(targetWindow, projectFile, projectContents, externalFileContents) { @@ -809,6 +834,7 @@ function getProjectFileName(dialogReturnedFileName) { module.exports = { chooseArchiveFile, chooseModelFile, + chooseProjectFile, chooseVariableFile, closeProject, confirmProjectFile, @@ -825,5 +851,6 @@ module.exports = { sendProjectOpened, showExistingProjectWindow, startCloseProject, - startSaveProject + startSaveProject, + startSaveProjectAs }; diff --git a/electron/app/js/wktWindow.js b/electron/app/js/wktWindow.js index ada42fbf3..42a428304 100644 --- a/electron/app/js/wktWindow.js +++ b/electron/app/js/wktWindow.js @@ -168,6 +168,20 @@ class WktAppMenu { } project.startSaveProject(focusedWindow); } + }, + { + id: 'saveAs', + label: i18n.t('menu-file-saveAs'), + enabled: !this._hasOpenDialog, + click(item, focusedWindow) { + if (!focusedWindow) { + return dialog.showErrorBox( + i18n.t('menu-file-saveAs-errorTitle'), + i18n.t('menu-file-saveAs-errorContent') + ); + } + project.startSaveProjectAs(focusedWindow); + } } ] }, diff --git a/electron/app/locales/en/electron.json b/electron/app/locales/en/electron.json index dd516a0ab..7a6ef6a04 100644 --- a/electron/app/locales/en/electron.json +++ b/electron/app/locales/en/electron.json @@ -16,6 +16,9 @@ "menu-file-saveAll": "Save All", "menu-file-saveAll-errorTitle": "Cannot Save", "menu-file-saveAll-errorContent": "There are currently no active projects to save", + "menu-file-saveAs": "Save As...", + "menu-file-saveAs-errorTitle": "Cannot Save As", + "menu-file-saveAs-errorContent": "There are currently no active projects to save", "menu-file-exit": "Exit", "menu-edit": "Edit", @@ -129,6 +132,8 @@ "dialog-openProjectWindow": "Open WebLogic Kubernetes Toolkit Project", "dialog-saveProjectFileErrorTitle": "Project File Save Error", "dialog-saveProjectFileErrorMessage": "Failed to write the project file {{projectFileName}}: {{err}}", + "dialog-openProjectFileErrorTitle": "Failed to Open Project File", + "dialog-openProjectFileErrorMessage": "Unable to open the project file {{projectFileName}}: {{err}}", "dialog-openProjectFileParseErrorTitle": "Failed to Parse Project File", "dialog-openProjectFileParseErrorMessage": "Unable to read the project file {{projectFileName}} as JSON: {{err}}", "dialog-openProjectFileReadErrorTitle": "Failed to Read Project File", diff --git a/electron/app/locales/en/webui.json b/electron/app/locales/en/webui.json index 5e84682de..900148486 100644 --- a/electron/app/locales/en/webui.json +++ b/electron/app/locales/en/webui.json @@ -1221,6 +1221,8 @@ "discover-catch-all-error-message": "Failed to discover: {{error}}", "save-all-failed-title": "Save All Failed", "save-all-catch-all-error-message": "Failed to save all: {{error}}", + "save-as-failed-title": "Save As Failed", + "save-as-catch-all-error-message": "Failed to save as: {{error}}", "network-page-title": "Network Configuration", "network-page-proceed": "Connection was established. Click Restart Application to save these settings and restart the application.", diff --git a/electron/app/main.js b/electron/app/main.js index 91de7d994..7f4a747bb 100644 --- a/electron/app/main.js +++ b/electron/app/main.js @@ -583,6 +583,10 @@ class Main { return project.confirmProjectFile(event.sender.getOwnerBrowserWindow()); }); + ipcMain.handle('choose-project-file',async (event) => { + return project.chooseProjectFile(event.sender.getOwnerBrowserWindow()); + }); + ipcMain.handle('save-project',async (event, projectFile, projectContents, externalFileContents) => { return project.saveProject(event.sender.getOwnerBrowserWindow(), projectFile, projectContents, externalFileContents); diff --git a/webui/src/js/models/wdt-model-definition.js b/webui/src/js/models/wdt-model-definition.js index 7267ac16b..bfcdd785f 100644 --- a/webui/src/js/models/wdt-model-definition.js +++ b/webui/src/js/models/wdt-model-definition.js @@ -1,6 +1,6 @@ /** * @license - * Copyright (c) 2021, Oracle and/or its affiliates. + * Copyright (c) 2021, 2022, Oracle and/or its affiliates. * Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. */ 'use strict'; @@ -287,6 +287,16 @@ define(['knockout', 'utils/observable-properties', 'js-yaml', 'utils/validation- } }; + /** + * Clear the model file names (model, properties, and archive) so they will revert to default names. + * This is useful when saving a project and its files with a different name. + */ + this.clearModelFileNames = () => { + this.modelFiles.value = []; + this.propertiesFiles.value = []; + this.archiveFiles.value = []; + }; + /** * Update the model, variable, and archive contents from the project's model content. * Assume that the modelFile attributes were already set from createGroup() in project load. diff --git a/webui/src/js/utils/project-io.js b/webui/src/js/utils/project-io.js index f34ff5998..18d86707b 100644 --- a/webui/src/js/utils/project-io.js +++ b/webui/src/js/utils/project-io.js @@ -1,6 +1,6 @@ /** * @license - * Copyright (c) 2021, Oracle and/or its affiliates. + * Copyright (c) 2021, 2022, Oracle and/or its affiliates. * Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. */ 'use strict'; @@ -14,9 +14,8 @@ define(['knockout', 'models/wkt-project', 'utils/i18n'], function (ko, project, i18n) { function ProjectIo() { - // verify that a project file is assigned to this project, assigning if necessary. + // verify that a project file is assigned to this project, choosing if necessary. // save the project contents to the specified file. - // if project name and UUID are specified, this is a new file, so assign those. this.saveProject = async(forceSave) => { const projectNotSaved = !project.getProjectFileName(); @@ -28,30 +27,52 @@ define(['knockout', 'models/wkt-project', 'utils/i18n'], return {saved: false, reason: i18n.t('project-io-user-cancelled-save-message')}; } - project.setProjectFileName(projectFile); + return saveToFile(projectFile, projectName, projectUuid); + } - // if project name or UUID are null, they were previously assigned. - if(projectName) { - project.setProjectName(projectName); - } - if(projectUuid) { - project.setProjectUuid(projectUuid); - } + return {saved: true}; + }; - let projectContents = project.getProjectContents(); - let modelContents = project.wdtModel.getModelContents(); - const saveResult = await window.api.ipc.invoke('save-project', projectFile, projectContents, - modelContents); + // select a new project file for the project, and save the project contents to the specified file. + this.saveProjectAs = async() => { + const [projectFile, projectName, projectUuid] = await window.api.ipc.invoke('choose-project-file'); + // if the project file is null, the user cancelled when selecting a new file. + if(!projectFile) { + return {saved: false, reason: i18n.t('project-io-user-cancelled-save-message')}; + } - if(saveResult['model']) { - project.wdtModel.setSpecifiedModelFiles(saveResult['model']); - } + project.wdtModel.clearModelFileNames(); + return saveToFile(projectFile, projectName, projectUuid); + }; - project.setNotDirty(); + // save the project to the specified project file with name and UUID. + // if project file is null, do not save. + // if project name and UUID are specified, this is a new file, so assign those. + // return object with saved status and reason if not saved. + async function saveToFile(projectFile, projectName, projectUuid) { + + project.setProjectFileName(projectFile); + + // if project name or UUID are null, they were previously assigned. + if(projectName) { + project.setProjectName(projectName); } + if(projectUuid) { + project.setProjectUuid(projectUuid); + } + + let projectContents = project.getProjectContents(); + let modelContents = project.wdtModel.getModelContents(); + const saveResult = await window.api.ipc.invoke('save-project', projectFile, projectContents, + modelContents); + if(saveResult['model']) { + project.wdtModel.setSpecifiedModelFiles(saveResult['model']); + } + + project.setNotDirty(); return {saved: true}; - }; + } // close the project in this window. // if the project is dirty, ask the user if they want to save the project. diff --git a/webui/src/js/windowStateUtils.js b/webui/src/js/windowStateUtils.js index 924789fba..0ffe338ac 100644 --- a/webui/src/js/windowStateUtils.js +++ b/webui/src/js/windowStateUtils.js @@ -50,6 +50,13 @@ function(wktProject, wktConsole, wdtDiscoverer, dialogHelper, projectIO, }); }); + window.api.ipc.receive('start-save-project-as', () => { + blurSelection(); + projectIO.saveProjectAs().catch(err => { + displayCatchAllError('save-as', err).then(); + }); + }); + window.api.ipc.receive('show-console-out-line', (line) => { wktConsole.addLine(line, 'out'); }); From 3f73097985dfa9f2f51c33c46f34d6526feb5d21 Mon Sep 17 00:00:00 2001 From: Richard Killen Date: Fri, 28 Jan 2022 16:44:18 -0600 Subject: [PATCH 2/3] Copy the archive file before applying updates for "Save As..." --- electron/app/js/ipcRendererPreload.js | 1 + electron/app/js/project.js | 18 ++++++++++++++++++ electron/app/main.js | 4 ++++ webui/src/js/models/wdt-model-definition.js | 1 + webui/src/js/utils/project-io.js | 8 ++++++++ 5 files changed, 32 insertions(+) diff --git a/electron/app/js/ipcRendererPreload.js b/electron/app/js/ipcRendererPreload.js index 0ec54f089..340e6b26a 100644 --- a/electron/app/js/ipcRendererPreload.js +++ b/electron/app/js/ipcRendererPreload.js @@ -121,6 +121,7 @@ contextBridge.exposeInMainWorld( 'choose-oracle-home', 'choose-variable-file', 'choose-extra-path-directory', + 'export-archive-file', 'restart-network-settings', 'try-network-settings', 'is-dev-mode', diff --git a/electron/app/js/project.js b/electron/app/js/project.js index 310af6303..06716e412 100644 --- a/electron/app/js/project.js +++ b/electron/app/js/project.js @@ -279,6 +279,19 @@ async function promptSaveBeforeClose(targetWindow) { return responses[result.response]; } +// export the archive file to the default location for a different project file +async function exportArchiveFile(targetWindow, archivePath, projectFile) { + const sourceProjectDir = _getProjectDirectory(targetWindow); + const sourcePath = path.join(sourceProjectDir, archivePath); + + const targetDirectoryName = _getDefaultModelsDirectoryName(projectFile); + const targetPath = path.join(path.dirname(projectFile), targetDirectoryName, 'archive.zip'); + await mkdir(path.dirname(targetPath), {recursive: true}); + + getLogger().debug('Copying archive ' + archivePath + ' to ' + targetPath); + await copyFile(sourcePath, targetPath); +} + // Private helper methods // async function _createNewProjectFile(targetWindow, projectFileName) { @@ -790,6 +803,10 @@ function _getProjectFilePath(targetWindow) { function _getDefaultModelsPath(targetWindow) { const projectFilePath = _getProjectFilePath(targetWindow); + return _getDefaultModelsDirectoryName(projectFilePath); +} + +function _getDefaultModelsDirectoryName(projectFilePath) { const projectFilePrefix = path.basename(projectFilePath, path.extname(projectFilePath)); return projectFilePrefix + '-models'; } @@ -841,6 +858,7 @@ module.exports = { createNewProject, getModelFileContent, getWindowForProject, + exportArchiveFile, isWktProjectFile, openProject, openProjectFile, diff --git a/electron/app/main.js b/electron/app/main.js index 7f4a747bb..cc0cddd60 100644 --- a/electron/app/main.js +++ b/electron/app/main.js @@ -600,6 +600,10 @@ class Main { return project.promptSaveBeforeClose(event.sender.getOwnerBrowserWindow()); }); + ipcMain.handle('export-archive-file', async (event, archivePath, projectFile) => { + return project.exportArchiveFile(event.sender.getOwnerBrowserWindow(), archivePath, projectFile); + }); + ipcMain.handle('run-offline-discover',async (event, discoverConfig) => { return wdtDiscovery.runOfflineDiscover(event.sender.getOwnerBrowserWindow(), discoverConfig); }); diff --git a/webui/src/js/models/wdt-model-definition.js b/webui/src/js/models/wdt-model-definition.js index bfcdd785f..e6c566cf8 100644 --- a/webui/src/js/models/wdt-model-definition.js +++ b/webui/src/js/models/wdt-model-definition.js @@ -292,6 +292,7 @@ define(['knockout', 'utils/observable-properties', 'js-yaml', 'utils/validation- * This is useful when saving a project and its files with a different name. */ this.clearModelFileNames = () => { + this.modelFileContents = {}; this.modelFiles.value = []; this.propertiesFiles.value = []; this.archiveFiles.value = []; diff --git a/webui/src/js/utils/project-io.js b/webui/src/js/utils/project-io.js index 18d86707b..5132e843f 100644 --- a/webui/src/js/utils/project-io.js +++ b/webui/src/js/utils/project-io.js @@ -41,7 +41,15 @@ define(['knockout', 'models/wkt-project', 'utils/i18n'], return {saved: false, reason: i18n.t('project-io-user-cancelled-save-message')}; } + // copy the archive file before archive updates are applied during save + const currentArchiveFile = project.wdtModel.archiveFile(); + if(currentArchiveFile) { + await window.api.ipc.invoke('export-archive-file', currentArchiveFile, projectFile); + } + + // this will cause the model files to be written with new names project.wdtModel.clearModelFileNames(); + return saveToFile(projectFile, projectName, projectUuid); }; From 187cd1959dfe4634a5d98c800c7b0214ebe3f62f Mon Sep 17 00:00:00 2001 From: Richard Killen Date: Fri, 28 Jan 2022 16:50:24 -0600 Subject: [PATCH 3/3] Allow absolute paths for archive copy --- electron/app/js/project.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/electron/app/js/project.js b/electron/app/js/project.js index 06716e412..25cb705b0 100644 --- a/electron/app/js/project.js +++ b/electron/app/js/project.js @@ -281,15 +281,17 @@ async function promptSaveBeforeClose(targetWindow) { // export the archive file to the default location for a different project file async function exportArchiveFile(targetWindow, archivePath, projectFile) { - const sourceProjectDir = _getProjectDirectory(targetWindow); - const sourcePath = path.join(sourceProjectDir, archivePath); + if(!path.isAbsolute(archivePath)) { + const sourceProjectDir = _getProjectDirectory(targetWindow); + archivePath = path.join(sourceProjectDir, archivePath); + } const targetDirectoryName = _getDefaultModelsDirectoryName(projectFile); const targetPath = path.join(path.dirname(projectFile), targetDirectoryName, 'archive.zip'); await mkdir(path.dirname(targetPath), {recursive: true}); getLogger().debug('Copying archive ' + archivePath + ' to ' + targetPath); - await copyFile(sourcePath, targetPath); + await copyFile(archivePath, targetPath); } // Private helper methods