Skip to content

Implement "Save as..." #81

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions electron/app/js/ipcRendererPreload.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -120,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',
Expand All @@ -129,6 +131,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',
Expand Down
8 changes: 5 additions & 3 deletions electron/app/js/model.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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));
});
}

Expand Down
53 changes: 50 additions & 3 deletions electron/app/js/project.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -254,6 +279,21 @@ 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) {
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(archivePath, targetPath);
}

// Private helper methods
//
async function _createNewProjectFile(targetWindow, projectFileName) {
Expand Down Expand Up @@ -765,6 +805,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';
}
Expand Down Expand Up @@ -809,12 +853,14 @@ function getProjectFileName(dialogReturnedFileName) {
module.exports = {
chooseArchiveFile,
chooseModelFile,
chooseProjectFile,
chooseVariableFile,
closeProject,
confirmProjectFile,
createNewProject,
getModelFileContent,
getWindowForProject,
exportArchiveFile,
isWktProjectFile,
openProject,
openProjectFile,
Expand All @@ -825,5 +871,6 @@ module.exports = {
sendProjectOpened,
showExistingProjectWindow,
startCloseProject,
startSaveProject
startSaveProject,
startSaveProjectAs
};
14 changes: 14 additions & 0 deletions electron/app/js/wktWindow.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
]
},
Expand Down
5 changes: 5 additions & 0 deletions electron/app/locales/en/electron.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions electron/app/locales/en/webui.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
8 changes: 8 additions & 0 deletions electron/app/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -596,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);
});
Expand Down
13 changes: 12 additions & 1 deletion webui/src/js/models/wdt-model-definition.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -287,6 +287,17 @@ 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.modelFileContents = {};
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.
Expand Down
69 changes: 49 additions & 20 deletions webui/src/js/utils/project-io.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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();

Expand All @@ -28,30 +27,60 @@ 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']);
}
// 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);
}

project.setNotDirty();
// this will cause the model files to be written with new names
project.wdtModel.clearModelFileNames();

return saveToFile(projectFile, projectName, projectUuid);
};

// 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.
Expand Down
7 changes: 7 additions & 0 deletions webui/src/js/windowStateUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down