Skip to content

Commit 7914c97

Browse files
authored
Implement "Save as..." (#81)
* Add "Save As..." option to file menu; fix crash when project model file missing * Copy the archive file before applying updates for "Save As..." * Allow absolute paths for archive copy
1 parent 417da4f commit 7914c97

File tree

10 files changed

+155
-27
lines changed

10 files changed

+155
-27
lines changed

electron/app/js/ipcRendererPreload.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ contextBridge.exposeInMainWorld(
5555
'start-add-archive-file',
5656
'start-close-project',
5757
'start-save-project',
58+
'start-save-project-as',
5859
'start-offline-discover',
5960
'start-online-discover',
6061
'show-console-out-line',
@@ -120,6 +121,7 @@ contextBridge.exposeInMainWorld(
120121
'choose-oracle-home',
121122
'choose-variable-file',
122123
'choose-extra-path-directory',
124+
'export-archive-file',
123125
'restart-network-settings',
124126
'try-network-settings',
125127
'is-dev-mode',
@@ -129,6 +131,7 @@ contextBridge.exposeInMainWorld(
129131
'get-url-catalog',
130132
'get-wdt-domain-types',
131133
'get-image-contents',
134+
'choose-project-file',
132135
'confirm-project-file',
133136
'prompt-save-before-close',
134137
'close-project',

electron/app/js/model.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @license
3-
* Copyright (c) 2021, Oracle and/or its affiliates.
3+
* Copyright (c) 2021, 2022, Oracle and/or its affiliates.
44
* Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
55
*/
66
const {app, dialog} = require('electron');
@@ -95,8 +95,10 @@ async function saveContentsOfModelFiles(projectDirectory, models) {
9595

9696
async function _getModelFileContent(projectDirectory, modelFile) {
9797
const effectiveModelFile = fsUtils.getAbsolutePath(modelFile, projectDirectory);
98-
return new Promise(resolve => {
99-
readFile(effectiveModelFile, {encoding: 'utf8'}).then(data => resolve(data));
98+
return new Promise((resolve, reject) => {
99+
readFile(effectiveModelFile, {encoding: 'utf8'})
100+
.then(data => resolve(data))
101+
.catch(err => reject(err));
100102
});
101103
}
102104

electron/app/js/project.js

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @license
3-
* Copyright (c) 2021, Oracle and/or its affiliates.
3+
* Copyright (c) 2021, 2022, Oracle and/or its affiliates.
44
* Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
55
*/
66
const {app, dialog} = require('electron');
@@ -91,7 +91,15 @@ async function openProject(targetWindow) {
9191
return;
9292
}
9393

94-
await openProjectFile(targetWindow, openResponse.filePaths[0]);
94+
const projectFileName = openResponse.filePaths[0];
95+
await openProjectFile(targetWindow, projectFileName)
96+
.catch(err => {
97+
dialog.showErrorBox(
98+
i18n.t('dialog-openProjectFileErrorTitle'),
99+
i18n.t('dialog-openProjectFileErrorMessage', { projectFileName: projectFileName, err: err }),
100+
);
101+
getLogger().error('Failed to open project file %s: %s', projectFileName, err);
102+
});
95103
}
96104

97105
async function openProjectFile(targetWindow, projectFile) {
@@ -133,12 +141,29 @@ async function confirmProjectFile(targetWindow) {
133141
return [projectFile, projectName, projectUuid];
134142
}
135143

144+
// choose a new project file for save.
145+
// return null values if no project file was established or selected.
146+
// usually called by the choose-project-file IPC invocation.
147+
async function chooseProjectFile(targetWindow) {
148+
const projectFile = await _chooseProjectSaveFile(targetWindow);
149+
const projectName = projectFile ? _generateProjectName(projectFile) : null;
150+
const projectUuid = projectFile ? _generateProjectUuid() : null;
151+
app.addRecentDocument(projectFile);
152+
return [projectFile, projectName, projectUuid];
153+
}
154+
136155
// initiate the save process by sending a message to the web app.
137156
// usually called from a menu click.
138157
function startSaveProject(targetWindow) {
139158
sendToWindow(targetWindow, 'start-save-project');
140159
}
141160

161+
// initiate the save-as process by sending a message to the web app.
162+
// usually called from a menu click.
163+
function startSaveProjectAs(targetWindow) {
164+
sendToWindow(targetWindow, 'start-save-project-as');
165+
}
166+
142167
// save the specified project and model contents to the project file.
143168
// usually invoked by the save-project IPC invocation.
144169
async function saveProject(targetWindow, projectFile, projectContents, externalFileContents) {
@@ -254,6 +279,21 @@ async function promptSaveBeforeClose(targetWindow) {
254279
return responses[result.response];
255280
}
256281

282+
// export the archive file to the default location for a different project file
283+
async function exportArchiveFile(targetWindow, archivePath, projectFile) {
284+
if(!path.isAbsolute(archivePath)) {
285+
const sourceProjectDir = _getProjectDirectory(targetWindow);
286+
archivePath = path.join(sourceProjectDir, archivePath);
287+
}
288+
289+
const targetDirectoryName = _getDefaultModelsDirectoryName(projectFile);
290+
const targetPath = path.join(path.dirname(projectFile), targetDirectoryName, 'archive.zip');
291+
await mkdir(path.dirname(targetPath), {recursive: true});
292+
293+
getLogger().debug('Copying archive ' + archivePath + ' to ' + targetPath);
294+
await copyFile(archivePath, targetPath);
295+
}
296+
257297
// Private helper methods
258298
//
259299
async function _createNewProjectFile(targetWindow, projectFileName) {
@@ -765,6 +805,10 @@ function _getProjectFilePath(targetWindow) {
765805

766806
function _getDefaultModelsPath(targetWindow) {
767807
const projectFilePath = _getProjectFilePath(targetWindow);
808+
return _getDefaultModelsDirectoryName(projectFilePath);
809+
}
810+
811+
function _getDefaultModelsDirectoryName(projectFilePath) {
768812
const projectFilePrefix = path.basename(projectFilePath, path.extname(projectFilePath));
769813
return projectFilePrefix + '-models';
770814
}
@@ -809,12 +853,14 @@ function getProjectFileName(dialogReturnedFileName) {
809853
module.exports = {
810854
chooseArchiveFile,
811855
chooseModelFile,
856+
chooseProjectFile,
812857
chooseVariableFile,
813858
closeProject,
814859
confirmProjectFile,
815860
createNewProject,
816861
getModelFileContent,
817862
getWindowForProject,
863+
exportArchiveFile,
818864
isWktProjectFile,
819865
openProject,
820866
openProjectFile,
@@ -825,5 +871,6 @@ module.exports = {
825871
sendProjectOpened,
826872
showExistingProjectWindow,
827873
startCloseProject,
828-
startSaveProject
874+
startSaveProject,
875+
startSaveProjectAs
829876
};

electron/app/js/wktWindow.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,20 @@ class WktAppMenu {
168168
}
169169
project.startSaveProject(focusedWindow);
170170
}
171+
},
172+
{
173+
id: 'saveAs',
174+
label: i18n.t('menu-file-saveAs'),
175+
enabled: !this._hasOpenDialog,
176+
click(item, focusedWindow) {
177+
if (!focusedWindow) {
178+
return dialog.showErrorBox(
179+
i18n.t('menu-file-saveAs-errorTitle'),
180+
i18n.t('menu-file-saveAs-errorContent')
181+
);
182+
}
183+
project.startSaveProjectAs(focusedWindow);
184+
}
171185
}
172186
]
173187
},

electron/app/locales/en/electron.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
"menu-file-saveAll": "Save All",
1717
"menu-file-saveAll-errorTitle": "Cannot Save",
1818
"menu-file-saveAll-errorContent": "There are currently no active projects to save",
19+
"menu-file-saveAs": "Save As...",
20+
"menu-file-saveAs-errorTitle": "Cannot Save As",
21+
"menu-file-saveAs-errorContent": "There are currently no active projects to save",
1922
"menu-file-exit": "Exit",
2023

2124
"menu-edit": "Edit",
@@ -129,6 +132,8 @@
129132
"dialog-openProjectWindow": "Open WebLogic Kubernetes Toolkit Project",
130133
"dialog-saveProjectFileErrorTitle": "Project File Save Error",
131134
"dialog-saveProjectFileErrorMessage": "Failed to write the project file {{projectFileName}}: {{err}}",
135+
"dialog-openProjectFileErrorTitle": "Failed to Open Project File",
136+
"dialog-openProjectFileErrorMessage": "Unable to open the project file {{projectFileName}}: {{err}}",
132137
"dialog-openProjectFileParseErrorTitle": "Failed to Parse Project File",
133138
"dialog-openProjectFileParseErrorMessage": "Unable to read the project file {{projectFileName}} as JSON: {{err}}",
134139
"dialog-openProjectFileReadErrorTitle": "Failed to Read Project File",

electron/app/locales/en/webui.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,6 +1224,8 @@
12241224
"discover-catch-all-error-message": "Failed to discover: {{error}}",
12251225
"save-all-failed-title": "Save All Failed",
12261226
"save-all-catch-all-error-message": "Failed to save all: {{error}}",
1227+
"save-as-failed-title": "Save As Failed",
1228+
"save-as-catch-all-error-message": "Failed to save as: {{error}}",
12271229

12281230
"network-page-title": "Network Configuration",
12291231
"network-page-proceed": "Connection was established. Click Restart Application to save these settings and restart the application.",

electron/app/main.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,10 @@ class Main {
583583
return project.confirmProjectFile(event.sender.getOwnerBrowserWindow());
584584
});
585585

586+
ipcMain.handle('choose-project-file',async (event) => {
587+
return project.chooseProjectFile(event.sender.getOwnerBrowserWindow());
588+
});
589+
586590
ipcMain.handle('save-project',async (event, projectFile, projectContents,
587591
externalFileContents) => {
588592
return project.saveProject(event.sender.getOwnerBrowserWindow(), projectFile, projectContents, externalFileContents);
@@ -596,6 +600,10 @@ class Main {
596600
return project.promptSaveBeforeClose(event.sender.getOwnerBrowserWindow());
597601
});
598602

603+
ipcMain.handle('export-archive-file', async (event, archivePath, projectFile) => {
604+
return project.exportArchiveFile(event.sender.getOwnerBrowserWindow(), archivePath, projectFile);
605+
});
606+
599607
ipcMain.handle('run-offline-discover',async (event, discoverConfig) => {
600608
return wdtDiscovery.runOfflineDiscover(event.sender.getOwnerBrowserWindow(), discoverConfig);
601609
});

webui/src/js/models/wdt-model-definition.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @license
3-
* Copyright (c) 2021, Oracle and/or its affiliates.
3+
* Copyright (c) 2021, 2022, Oracle and/or its affiliates.
44
* Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
55
*/
66
'use strict';
@@ -287,6 +287,17 @@ define(['knockout', 'utils/observable-properties', 'js-yaml', 'utils/validation-
287287
}
288288
};
289289

290+
/**
291+
* Clear the model file names (model, properties, and archive) so they will revert to default names.
292+
* This is useful when saving a project and its files with a different name.
293+
*/
294+
this.clearModelFileNames = () => {
295+
this.modelFileContents = {};
296+
this.modelFiles.value = [];
297+
this.propertiesFiles.value = [];
298+
this.archiveFiles.value = [];
299+
};
300+
290301
/**
291302
* Update the model, variable, and archive contents from the project's model content.
292303
* Assume that the modelFile attributes were already set from createGroup() in project load.

webui/src/js/utils/project-io.js

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @license
3-
* Copyright (c) 2021, Oracle and/or its affiliates.
3+
* Copyright (c) 2021, 2022, Oracle and/or its affiliates.
44
* Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
55
*/
66
'use strict';
@@ -14,9 +14,8 @@ define(['knockout', 'models/wkt-project', 'utils/i18n'],
1414
function (ko, project, i18n) {
1515
function ProjectIo() {
1616

17-
// verify that a project file is assigned to this project, assigning if necessary.
17+
// verify that a project file is assigned to this project, choosing if necessary.
1818
// save the project contents to the specified file.
19-
// if project name and UUID are specified, this is a new file, so assign those.
2019
this.saveProject = async(forceSave) => {
2120
const projectNotSaved = !project.getProjectFileName();
2221

@@ -28,30 +27,60 @@ define(['knockout', 'models/wkt-project', 'utils/i18n'],
2827
return {saved: false, reason: i18n.t('project-io-user-cancelled-save-message')};
2928
}
3029

31-
project.setProjectFileName(projectFile);
30+
return saveToFile(projectFile, projectName, projectUuid);
31+
}
3232

33-
// if project name or UUID are null, they were previously assigned.
34-
if(projectName) {
35-
project.setProjectName(projectName);
36-
}
37-
if(projectUuid) {
38-
project.setProjectUuid(projectUuid);
39-
}
33+
return {saved: true};
34+
};
4035

41-
let projectContents = project.getProjectContents();
42-
let modelContents = project.wdtModel.getModelContents();
43-
const saveResult = await window.api.ipc.invoke('save-project', projectFile, projectContents,
44-
modelContents);
36+
// select a new project file for the project, and save the project contents to the specified file.
37+
this.saveProjectAs = async() => {
38+
const [projectFile, projectName, projectUuid] = await window.api.ipc.invoke('choose-project-file');
39+
// if the project file is null, the user cancelled when selecting a new file.
40+
if(!projectFile) {
41+
return {saved: false, reason: i18n.t('project-io-user-cancelled-save-message')};
42+
}
4543

46-
if(saveResult['model']) {
47-
project.wdtModel.setSpecifiedModelFiles(saveResult['model']);
48-
}
44+
// copy the archive file before archive updates are applied during save
45+
const currentArchiveFile = project.wdtModel.archiveFile();
46+
if(currentArchiveFile) {
47+
await window.api.ipc.invoke('export-archive-file', currentArchiveFile, projectFile);
48+
}
4949

50-
project.setNotDirty();
50+
// this will cause the model files to be written with new names
51+
project.wdtModel.clearModelFileNames();
52+
53+
return saveToFile(projectFile, projectName, projectUuid);
54+
};
55+
56+
// save the project to the specified project file with name and UUID.
57+
// if project file is null, do not save.
58+
// if project name and UUID are specified, this is a new file, so assign those.
59+
// return object with saved status and reason if not saved.
60+
async function saveToFile(projectFile, projectName, projectUuid) {
61+
62+
project.setProjectFileName(projectFile);
63+
64+
// if project name or UUID are null, they were previously assigned.
65+
if(projectName) {
66+
project.setProjectName(projectName);
67+
}
68+
if(projectUuid) {
69+
project.setProjectUuid(projectUuid);
5170
}
5271

72+
let projectContents = project.getProjectContents();
73+
let modelContents = project.wdtModel.getModelContents();
74+
const saveResult = await window.api.ipc.invoke('save-project', projectFile, projectContents,
75+
modelContents);
76+
77+
if(saveResult['model']) {
78+
project.wdtModel.setSpecifiedModelFiles(saveResult['model']);
79+
}
80+
81+
project.setNotDirty();
5382
return {saved: true};
54-
};
83+
}
5584

5685
// close the project in this window.
5786
// if the project is dirty, ask the user if they want to save the project.

webui/src/js/windowStateUtils.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ function(wktProject, wktConsole, wdtDiscoverer, dialogHelper, projectIO,
5050
});
5151
});
5252

53+
window.api.ipc.receive('start-save-project-as', () => {
54+
blurSelection();
55+
projectIO.saveProjectAs().catch(err => {
56+
displayCatchAllError('save-as', err).then();
57+
});
58+
});
59+
5360
window.api.ipc.receive('show-console-out-line', (line) => {
5461
wktConsole.addLine(line, 'out');
5562
});

0 commit comments

Comments
 (0)