diff --git a/__tests__/__unit__/ZoweUSSNode.unit.test.ts b/__tests__/__unit__/ZoweUSSNode.unit.test.ts index 781f7e1151..6b50ff3510 100644 --- a/__tests__/__unit__/ZoweUSSNode.unit.test.ts +++ b/__tests__/__unit__/ZoweUSSNode.unit.test.ts @@ -199,4 +199,22 @@ describe("Unit Tests (Jest)", () => { utils.labelHack(rootNode); expect(rootNode.label === "gappy"); }); + + /************************************************************************************************************* + * Checks that getEtag() returns a value + *************************************************************************************************************/ + it("Checks that getEtag() returns a value", async () => { + const rootNode = new ZoweUSSNode("gappy", vscode.TreeItemCollapsibleState.Collapsed, null, session, null, null, null, "123"); + expect(rootNode.getEtag() === "123"); + }); + + /************************************************************************************************************* + * Checks that setEtag() assigns a value + *************************************************************************************************************/ + it("Checks that setEtag() assigns a value", async () => { + const rootNode = new ZoweUSSNode("gappy", vscode.TreeItemCollapsibleState.Collapsed, null, session, null, null, null, "123"); + expect(rootNode.getEtag() === "123"); + rootNode.setEtag("ABC"); + expect(rootNode.getEtag() === "ABC"); + }); }); diff --git a/__tests__/__unit__/__snapshots__/ZoweUSSNode.unit.test.ts.snap b/__tests__/__unit__/__snapshots__/ZoweUSSNode.unit.test.ts.snap index 7579cad107..06f30977ed 100644 --- a/__tests__/__unit__/__snapshots__/ZoweUSSNode.unit.test.ts.snap +++ b/__tests__/__unit__/__snapshots__/ZoweUSSNode.unit.test.ts.snap @@ -8,6 +8,7 @@ ZoweUSSNode { "collapsibleState": 0, "contextValue": "textFile", "dirty": false, + "etag": "", "fullPath": "", "iconPath": "Ref: 'document.svg'", "label": "testFile", @@ -18,6 +19,7 @@ ZoweUSSNode { "collapsibleState": 1, "contextValue": "directory", "dirty": false, + "etag": "", "fullPath": "", "iconPath": "Ref: 'folder.svg'", "label": "testDir", @@ -28,6 +30,7 @@ ZoweUSSNode { "collapsibleState": 1, "contextValue": "uss_session", "dirty": true, + "etag": "", "fullPath": "", "iconPath": "Ref: 'folder.svg'", "label": "root", diff --git a/__tests__/__unit__/__snapshots__/extension.unit.test.ts.snap b/__tests__/__unit__/__snapshots__/extension.unit.test.ts.snap index 61967664f8..1f925f866d 100644 --- a/__tests__/__unit__/__snapshots__/extension.unit.test.ts.snap +++ b/__tests__/__unit__/__snapshots__/extension.unit.test.ts.snap @@ -37,21 +37,11 @@ Array [ "group": "inline", "when": "view == zowe.uss.explorer && viewItem != favorite && viewItem != uss_session && viewItem != directory && viewItem != directory_fav && viewItem != uss_session_fav", }, - Object { - "command": "zowe.uss.safeSaveUSS", - "group": "inline", - "when": "view == zowe.uss.explorer && viewItem != favorite && viewItem != uss_session && viewItem != directory && viewItem != directory_fav && viewItem != uss_session_fav", - }, Object { "command": "zowe.uss.refreshUSS", "group": "0_mainframeInteraction", "when": "view == zowe.uss.explorer && viewItem != favorite && viewItem != uss_session && viewItem != directory && viewItem != directory_fav && viewItem != uss_session_fav", }, - Object { - "command": "zowe.uss.safeSaveUSS", - "group": "0_mainframeInteraction", - "when": "view == zowe.uss.explorer && viewItem != favorite && viewItem != uss_session && viewItem != directory && viewItem != directory_fav && viewItem != uss_session_fav", - }, Object { "command": "zowe.uss.deleteNode", "group": "6_modification@2", @@ -132,21 +122,11 @@ Array [ "group": "inline", "when": "view == zowe.explorer && viewItem == member", }, - Object { - "command": "zowe.safeSave", - "group": "inline", - "when": "view == zowe.explorer && viewItem == member", - }, Object { "command": "zowe.refreshNode", "group": "0_mainframeInteraction", "when": "view == zowe.explorer && viewItem == member", }, - Object { - "command": "zowe.safeSave", - "group": "0_mainframeInteraction", - "when": "view == zowe.explorer && viewItem == member", - }, Object { "command": "zowe.deleteMember", "group": "6_modification@2", @@ -173,7 +153,7 @@ Array [ "when": "view == zowe.explorer && viewItem == member_fav", }, Object { - "command": "zowe.safeSave", + "command": "zowe.refreshNode", "group": "inline", "when": "view == zowe.explorer && viewItem == member_fav", }, @@ -182,11 +162,6 @@ Array [ "group": "0_mainframeInteraction", "when": "view == zowe.explorer && viewItem == member_fav", }, - Object { - "command": "zowe.safeSave", - "group": "0_mainframeInteraction", - "when": "view == zowe.explorer && viewItem == member_fav", - }, Object { "command": "zowe.deleteMember", "group": "6_modification@2", @@ -213,17 +188,17 @@ Array [ "when": "view == zowe.explorer && viewItem == ds", }, Object { - "command": "zowe.safeSave", - "group": "inline", - "when": "view == zowe.explorer && viewItem == ds", + "command": "zowe.renameDataSetMember", + "group": "6_modification@2", + "when": "view == zowe.explorer && viewItem == member_fav", }, Object { "command": "zowe.refreshNode", - "group": "0_mainframeInteraction", + "group": "inline", "when": "view == zowe.explorer && viewItem == ds", }, Object { - "command": "zowe.safeSave", + "command": "zowe.refreshNode", "group": "0_mainframeInteraction", "when": "view == zowe.explorer && viewItem == ds", }, @@ -268,17 +243,22 @@ Array [ "when": "view == zowe.explorer && viewItem == ds_fav", }, Object { - "command": "zowe.safeSave", - "group": "inline", - "when": "view == zowe.explorer && viewItem == ds_fav", + "command": "zowe.copyDataSet", + "group": "9_cutcopypaste", + "when": "view == zowe.explorer && viewItem == ds", + }, + Object { + "command": "zowe.pasteDataSet", + "group": "9_cutcopypaste", + "when": "view == zowe.explorer && viewItem == ds", }, Object { "command": "zowe.refreshNode", - "group": "0_mainframeInteraction", + "group": "inline", "when": "view == zowe.explorer && viewItem == ds_fav", }, Object { - "command": "zowe.safeSave", + "command": "zowe.refreshNode", "group": "0_mainframeInteraction", "when": "view == zowe.explorer && viewItem == ds_fav", }, diff --git a/__tests__/__unit__/extension.unit.test.ts b/__tests__/__unit__/extension.unit.test.ts index b3b2c143eb..cc02bef55b 100644 --- a/__tests__/__unit__/extension.unit.test.ts +++ b/__tests__/__unit__/extension.unit.test.ts @@ -101,7 +101,7 @@ describe("Extension Unit Tests", () => { sessNode.contextValue = extension.DS_SESSION_CONTEXT; sessNode.pattern = "test hlq"; - const ussNode = new ZoweUSSNode("usstest", vscode.TreeItemCollapsibleState.Expanded, null, session, null); + const ussNode = new ZoweUSSNode("usstest", vscode.TreeItemCollapsibleState.Expanded, null, session, null, null, null, "123"); ussNode.contextValue = extension.USS_SESSION_CONTEXT; ussNode.fullPath = "/u/myuser"; @@ -124,6 +124,7 @@ describe("Extension Unit Tests", () => { const rmdirSync = jest.fn(); const readFileSync = jest.fn(); const showErrorMessage = jest.fn(); + const showWarningMessage = jest.fn(); const showInputBox = jest.fn(); const showOpenDialog = jest.fn(); const showQuickBox = jest.fn(); @@ -204,6 +205,8 @@ describe("Extension Unit Tests", () => { const copyDataSet = jest.fn(); const findFavoritedNode = jest.fn(); const findNonFavoritedNode = jest.fn(); + const concatChildNodes = jest.fn(); + const concatUSSChildNodes = jest.fn(); let mockClipboardData: string; const clipboard = { writeText: jest.fn().mockImplementation((value) => mockClipboardData = value), @@ -246,7 +249,7 @@ describe("Extension Unit Tests", () => { refreshElement: mockUSSRefreshElement, getChildren: mockGetUSSChildren, initializeUSSFavorites: mockInitializeUSS, - ussFilterPrompt: ussPattern + ussFilterPrompt: ussPattern, }; }); const JobsTree = jest.fn().mockImplementation(() => { @@ -291,7 +294,8 @@ describe("Extension Unit Tests", () => { }; }) }); - + Object.defineProperty(utils, "concatChildNodes", {value: concatChildNodes}); + Object.defineProperty(utils, "concatUSSChildNodes", {value: concatUSSChildNodes}); Object.defineProperty(fs, "mkdirSync", {value: mkdirSync}); Object.defineProperty(brtimperative, "CliProfileManager", {value: CliProfileManager}); Object.defineProperty(vscode.window, "createTreeView", {value: createTreeView}); @@ -312,6 +316,7 @@ describe("Extension Unit Tests", () => { Object.defineProperty(fs, "readFileSync", {value: readFileSync}); Object.defineProperty(fsextra, "moveSync", {value: moveSync}); Object.defineProperty(vscode.window, "showErrorMessage", {value: showErrorMessage}); + Object.defineProperty(vscode.window, "showWarningMessage", {value: showWarningMessage}); Object.defineProperty(vscode.window, "showInputBox", {value: showInputBox}); Object.defineProperty(vscode.window, "showQuickBox", {value: showQuickBox}); Object.defineProperty(vscode.window, "activeTextEditor", {value: activeTextEditor}); @@ -335,7 +340,6 @@ describe("Extension Unit Tests", () => { Object.defineProperty(vscode.workspace, "openTextDocument", {value: openTextDocument}); Object.defineProperty(vscode.window, "showInformationMessage", {value: showInformationMessage}); Object.defineProperty(vscode.window, "showTextDocument", {value: showTextDocument}); - Object.defineProperty(vscode.window, "showErrorMessage", {value: showErrorMessage}); Object.defineProperty(vscode.window, "showOpenDialog", {value: showOpenDialog}); Object.defineProperty(vscode.window, "showQuickPick", {value: showQuickPick}); Object.defineProperty(vscode.window, "withProgress", {value: withProgress}); @@ -528,7 +532,7 @@ describe("Extension Unit Tests", () => { expect(createTreeView.mock.calls[0][0]).toBe("zowe.explorer"); expect(createTreeView.mock.calls[1][0]).toBe("zowe.uss.explorer"); // tslint:disable-next-line: no-magic-numbers - expect(registerCommand.mock.calls.length).toBe(65); + expect(registerCommand.mock.calls.length).toBe(63); registerCommand.mock.calls.forEach((call, i ) => { expect(registerCommand.mock.calls[i][1]).toBeInstanceOf(Function); }); @@ -552,7 +556,6 @@ describe("Extension Unit Tests", () => { "zowe.editMember", "zowe.removeSession", "zowe.removeFavorite", - "zowe.safeSave", "zowe.saveSearch", "zowe.removeSavedSearch", "zowe.submitJcl", @@ -567,7 +570,6 @@ describe("Extension Unit Tests", () => { "zowe.uss.addSession", "zowe.uss.refreshAll", "zowe.uss.refreshUSS", - "zowe.uss.safeSaveUSS", "zowe.uss.fullPath", "zowe.uss.ZoweUSSNode.open", "zowe.uss.removeSession", @@ -750,13 +752,22 @@ describe("Extension Unit Tests", () => { dataSet.mockReset(); showTextDocument.mockReset(); + const response: brightside.IZosFilesResponse = { + success: true, + commandResponse: null, + apiResponse: { + etag: "123" + } + }; + dataSet.mockReturnValueOnce(response); await extension.refreshPS(node); expect(dataSet.mock.calls.length).toBe(1); expect(dataSet.mock.calls[0][0]).toBe(node.getSession()); expect(dataSet.mock.calls[0][1]).toBe(node.label); expect(dataSet.mock.calls[0][2]).toEqual({ - file: path.join(extension.DS_DIR, node.getSessionNode().label, node.label ) + file: path.join(extension.DS_DIR, node.getSessionNode().label, node.label), + returnEtag: true }); expect(openTextDocument.mock.calls.length).toBe(1); expect(openTextDocument.mock.calls[0][0]).toBe(path.join(extension.DS_DIR, @@ -797,6 +808,7 @@ describe("Extension Unit Tests", () => { openTextDocument.mockResolvedValueOnce({isDirty: true}); dataSet.mockReset(); showTextDocument.mockReset(); + dataSet.mockReturnValueOnce(response); node.contextValue = extension.DS_PDS_CONTEXT + extension.FAV_SUFFIX; await extension.refreshPS(node); @@ -805,6 +817,7 @@ describe("Extension Unit Tests", () => { dataSet.mockReset(); openTextDocument.mockReset(); + dataSet.mockReturnValueOnce(response); parent.contextValue = extension.DS_PDS_CONTEXT + extension.FAV_SUFFIX; await extension.refreshPS(child); @@ -813,6 +826,7 @@ describe("Extension Unit Tests", () => { dataSet.mockReset(); openTextDocument.mockReset(); + dataSet.mockReturnValueOnce(response); parent.contextValue = extension.FAVORITE_CONTEXT; await extension.refreshPS(child); @@ -822,12 +836,6 @@ describe("Extension Unit Tests", () => { showErrorMessage.mockReset(); dataSet.mockReset(); openTextDocument.mockReset(); - parent.contextValue = "turnip"; - await extension.safeSave(child); - expect(openTextDocument.mock.calls.length).toBe(0); - expect(dataSet.mock.calls.length).toBe(0); - expect(showErrorMessage.mock.calls.length).toBe(1); - expect(showErrorMessage.mock.calls[0][0]).toEqual("safeSave() called from invalid node."); }); @@ -1528,6 +1536,25 @@ describe("Extension Unit Tests", () => { validateRange: null, validatePosition: null }; + const testDoc0: vscode.TextDocument = { + fileName: path.join(extension.DS_DIR, "HLQ.TEST.AFILE"), + uri: null, + isUntitled: null, + languageId: null, + version: null, + isDirty: null, + isClosed: null, + save: null, + eol: null, + lineCount: null, + lineAt: null, + offsetAt: null, + positionAt: null, + getText: null, + getWordRangeAtPosition: null, + validateRange: null, + validatePosition: null + }; const testResponse = { success: true, @@ -1536,12 +1563,40 @@ describe("Extension Unit Tests", () => { items: [] } }; + // If session node is not defined, it should take the session from Profile + const sessionwocred = new brtimperative.Session({ + user: "", + password: "", + hostname: "fake", + protocol: "https", + type: "basic", + }); + // testing if no session is defined (can happen while saving from favorites) + const nodeWitoutSession = new ZoweNode("HLQ.TEST.AFILE", vscode.TreeItemCollapsibleState.None, null, null); + testTree.getChildren.mockReturnValueOnce([nodeWitoutSession]); + concatChildNodes.mockReturnValueOnce([nodeWitoutSession]); + createBasicZosmfSession.mockReturnValueOnce(sessionwocred); + await extension.saveFile(testDoc0, testTree); + expect(createBasicZosmfSession.mock.calls.length).toBe(1); + expect(createBasicZosmfSession.mock.results[0].value).toEqual(sessionwocred); + + // testing if no documentSession is found (no session + no profile) + createBasicZosmfSession.mockReset(); + testTree.getChildren.mockReset(); + showErrorMessage.mockReset(); + testTree.getChildren.mockReturnValueOnce([nodeWitoutSession]); + createBasicZosmfSession.mockReturnValueOnce(null); + await extension.saveFile(testDoc0, testTree); + expect(showErrorMessage.mock.calls.length).toBe(1); + expect(showErrorMessage.mock.calls[0][0]).toBe("Couldn't locate session when saving data set!"); + + testTree.getChildren.mockReset(); + createBasicZosmfSession.mockReset(); + testTree.getChildren.mockReturnValueOnce([new ZoweNode("node", vscode.TreeItemCollapsibleState.None, sessNode, null), sessNode]); dataSetList.mockReset(); showErrorMessage.mockReset(); - dataSetList.mockResolvedValueOnce(testResponse); - await extension.saveFile(testDoc, testTree); expect(dataSetList.mock.calls.length).toBe(1); @@ -1550,18 +1605,39 @@ describe("Extension Unit Tests", () => { expect(showErrorMessage.mock.calls.length).toBe(1); expect(showErrorMessage.mock.calls[0][0]).toBe("Data set failed to save. Data set may have been deleted on mainframe."); - testResponse.apiResponse.items = ["Item1"]; + const node = new ZoweNode("HLQ.TEST.AFILE", vscode.TreeItemCollapsibleState.None, sessNode, null); + sessNode.children.push(node); + testResponse.apiResponse.items = [{dsname: "HLQ.TEST.AFILE"}, {dsname: "HLQ.TEST.AFILE(mem)"}]; dataSetList.mockReset(); pathToDataSet.mockReset(); showErrorMessage.mockReset(); - + concatChildNodes.mockReset(); + const mockSetEtag = jest.spyOn(node, "setEtag").mockImplementation(() => null); + mockSetEtag.mockReset(); + const uploadResponse: brightside.IZosFilesResponse = { + success: true, + commandResponse: "success", + apiResponse: [{ + etag: "123" + }] + }; + concatChildNodes.mockReturnValueOnce([sessNode.children[0]]); testTree.getChildren.mockReturnValueOnce([sessNode]); dataSetList.mockResolvedValueOnce(testResponse); + dataSetList.mockResolvedValueOnce(testResponse); + withProgress.mockResolvedValueOnce(uploadResponse); testResponse.success = true; pathToDataSet.mockResolvedValueOnce(testResponse); await extension.saveFile(testDoc, testTree); + expect(concatChildNodes.mock.calls.length).toBe(1); + expect(showInformationMessage.mock.calls.length).toBe(1); + expect(showInformationMessage.mock.calls[0][0]).toBe("success"); + expect(mockSetEtag).toHaveBeenCalledTimes(1); + expect(mockSetEtag).toHaveBeenCalledWith("123"); + + concatChildNodes.mockReturnValueOnce([sessNode.children[0]]); testTree.getChildren.mockReturnValueOnce([sessNode]); dataSetList.mockResolvedValueOnce(testResponse); testResponse.success = false; @@ -1620,14 +1696,40 @@ describe("Extension Unit Tests", () => { dataSetList.mockReset(); showErrorMessage.mockReset(); + sessNode.children.push(new ZoweNode("HLQ.TEST.AFILE(mem)", vscode.TreeItemCollapsibleState.None, sessNode, null)); testTree.getChildren.mockReturnValueOnce([sessNode]); dataSetList.mockResolvedValueOnce(testResponse); testResponse.success = true; + concatChildNodes.mockReset(); + concatChildNodes.mockReturnValueOnce(sessNode.children); await extension.saveFile(testDoc3, testTree); + expect(concatChildNodes.mock.calls.length).toBe(1); + testTree.getChildren.mockReturnValueOnce([new ZoweNode("node", vscode.TreeItemCollapsibleState.None, sessNode, null), sessNode]); dataSetList.mockReset(); showErrorMessage.mockReset(); + + testTree.getChildren.mockReturnValueOnce([sessNode]); + dataSetList.mockResolvedValueOnce(testResponse); + concatChildNodes.mockReset(); + concatChildNodes.mockReturnValueOnce(sessNode.children); + testResponse.success = false; + testResponse.commandResponse = "Rest API failure with HTTP(S) status 412"; + withProgress.mockResolvedValueOnce(testResponse); + dataSet.mockReset(); + const downloadResponse = { + success: true, + commandResponse: "", + apiResponse: { + etag: "" + } + }; + dataSet.mockResolvedValue(downloadResponse); + + await extension.saveFile(testDoc, testTree); + expect(showWarningMessage.mock.calls[0][0]).toBe("Remote file has been modified in the meantime.\nSelect 'Compare' to resolve the conflict."); + expect(concatChildNodes.mock.calls.length).toBe(1); }); it("Testing that refreshAll is executed successfully", async () => { @@ -1647,6 +1749,14 @@ describe("Extension Unit Tests", () => { const child = new ZoweNode("child", vscode.TreeItemCollapsibleState.None, parent, null); existsSync.mockReturnValue(null); + const response: brightside.IZosFilesResponse = { + success: true, + commandResponse: null, + apiResponse: { + etag: "123" + } + }; + withProgress.mockReturnValue(response); openTextDocument.mockResolvedValueOnce("test doc"); await extension.openPS(node, true); @@ -1824,126 +1934,6 @@ describe("Extension Unit Tests", () => { showErrorMessage.mockReset(); }); - it("Testing that safeSave is executed successfully", async () => { - dataSet.mockReset(); - openTextDocument.mockReset(); - showTextDocument.mockReset(); - showInformationMessage.mockReset(); - - const node = new ZoweNode("node", vscode.TreeItemCollapsibleState.None, sessNode, null); - const parent = new ZoweNode("parent", vscode.TreeItemCollapsibleState.Collapsed, sessNode, null); - const child = new ZoweNode("child", vscode.TreeItemCollapsibleState.None, parent, null); - - openTextDocument.mockResolvedValueOnce("test"); - - await extension.safeSave(node); - - expect(dataSet.mock.calls.length).toBe(1); - expect(dataSet.mock.calls[0][0]).toBe(session); - expect(dataSet.mock.calls[0][1]).toBe(node.label); - expect(dataSet.mock.calls[0][2]).toEqual({file: extension.getDocumentFilePath(node.label, node)}); - expect(openTextDocument.mock.calls.length).toBe(1); - expect(openTextDocument.mock.calls[0][0]).toBe(path.join(extension.DS_DIR, - node.getSessionNode().label.trim(), node.label )); - expect(showTextDocument.mock.calls.length).toBe(1); - expect(showTextDocument.mock.calls[0][0]).toBe("test"); - expect(save.mock.calls.length).toBe(1); - - dataSet.mockReset(); - dataSet.mockRejectedValueOnce(Error("not found")); - - await extension.safeSave(node); - - expect(showInformationMessage.mock.calls.length).toBe(1); - expect(showInformationMessage.mock.calls[0][0]).toBe("Unable to find file: " + node.label + " was probably deleted."); - - dataSet.mockReset(); - showErrorMessage.mockReset(); - dataSet.mockRejectedValueOnce(Error("")); - - await extension.safeSave(child); - - expect(showErrorMessage.mock.calls.length).toBe(1); - expect(showErrorMessage.mock.calls[0][0]).toEqual(""); - - openTextDocument.mockResolvedValueOnce("test"); - openTextDocument.mockResolvedValueOnce("test"); - - dataSet.mockReset(); - openTextDocument.mockReset(); - node.contextValue = extension.DS_PDS_CONTEXT + extension.FAV_SUFFIX; - await extension.safeSave(node); - expect(openTextDocument.mock.calls.length).toBe(1); - expect(dataSet.mock.calls.length).toBe(1); - - dataSet.mockReset(); - openTextDocument.mockReset(); - parent.contextValue = extension.DS_PDS_CONTEXT + extension.FAV_SUFFIX; - await extension.safeSave(child); - expect(openTextDocument.mock.calls.length).toBe(1); - expect(dataSet.mock.calls.length).toBe(1); - - dataSet.mockReset(); - openTextDocument.mockReset(); - parent.contextValue = extension.FAVORITE_CONTEXT; - await extension.safeSave(child); - expect(openTextDocument.mock.calls.length).toBe(1); - expect(dataSet.mock.calls.length).toBe(1); - - showErrorMessage.mockReset(); - dataSet.mockReset(); - openTextDocument.mockReset(); - parent.contextValue = "turnip"; - await extension.safeSave(child); - expect(openTextDocument.mock.calls.length).toBe(0); - expect(dataSet.mock.calls.length).toBe(0); - expect(showErrorMessage.mock.calls.length).toBe(1); - expect(showErrorMessage.mock.calls[0][0]).toEqual("safeSave() called from invalid node."); - }); - - it("Testing that safeSaveUSS is executed successfully", async () => { - ussFile.mockReset(); - openTextDocument.mockReset(); - showTextDocument.mockReset(); - showInformationMessage.mockReset(); - save.mockReset(); - - const node = new ZoweUSSNode("node", vscode.TreeItemCollapsibleState.None, ussNode, null, null); - const parent = new ZoweUSSNode("parent", vscode.TreeItemCollapsibleState.Collapsed, ussNode, null, null); - const child = new ZoweUSSNode("child", vscode.TreeItemCollapsibleState.None, parent, null, null); - - openTextDocument.mockResolvedValueOnce("test"); - - await extension.safeSaveUSS(node); - - expect(ussFile.mock.calls.length).toBe(1); - expect(ussFile.mock.calls[0][0]).toBe(node.getSession()); - expect(ussFile.mock.calls[0][1]).toBe(node.fullPath); - expect(ussFile.mock.calls[0][2]).toEqual({file: extension.getUSSDocumentFilePath(node)}); - expect(openTextDocument.mock.calls.length).toBe(1); - expect(openTextDocument.mock.calls[0][0]).toBe(path.join(extension.getUSSDocumentFilePath(node))); - expect(showTextDocument.mock.calls.length).toBe(1); - expect(showTextDocument.mock.calls[0][0]).toBe("test"); - expect(save.mock.calls.length).toBe(1); - - ussFile.mockReset(); - ussFile.mockRejectedValueOnce(Error("not found")); - - await extension.safeSaveUSS(node); - - expect(showInformationMessage.mock.calls.length).toBe(1); - expect(showInformationMessage.mock.calls[0][0]).toBe("Unable to find file: " + node.fullPath + " was probably deleted."); - - ussFile.mockReset(); - showErrorMessage.mockReset(); - ussFile.mockRejectedValueOnce(Error("")); - - await extension.safeSaveUSS(child); - - expect(showErrorMessage.mock.calls.length).toBe(1); - expect(showErrorMessage.mock.calls[0][0]).toEqual(""); - }); - it("Testing that refreshUSS correctly executes with and without error", async () => { const node = new ZoweUSSNode("test-node", vscode.TreeItemCollapsibleState.None, ussNode, null, "/"); const parent = new ZoweUSSNode("parent", vscode.TreeItemCollapsibleState.Collapsed, node, null, "/"); @@ -1958,14 +1948,22 @@ describe("Extension Unit Tests", () => { ussFile.mockReset(); showTextDocument.mockReset(); executeCommand.mockReset(); - + const response: brightside.IZosFilesResponse = { + success: true, + commandResponse: null, + apiResponse: { + etag: "132" + } + }; + ussFile.mockReturnValueOnce(response); await extension.refreshUSS(node); expect(ussFile.mock.calls.length).toBe(1); expect(ussFile.mock.calls[0][0]).toBe(node.getSession()); expect(ussFile.mock.calls[0][1]).toBe(node.fullPath); expect(ussFile.mock.calls[0][2]).toEqual({ - file: extension.getUSSDocumentFilePath(node) + file: extension.getUSSDocumentFilePath(node), + returnEtag: true, }); expect(openTextDocument.mock.calls.length).toBe(1); expect(openTextDocument.mock.calls[0][0]).toBe(path.join(extension.getUSSDocumentFilePath(node))); @@ -2314,6 +2312,15 @@ describe("Extension Unit Tests", () => { existsSync.mockReturnValue(null); openTextDocument.mockResolvedValueOnce("test.doc"); + const response: brightside.IZosFilesResponse = { + success: true, + commandResponse: null, + apiResponse: { + etag: "123" + } + }; + withProgress.mockReturnValue(response); + await extension.openUSS(node, false, true); expect(existsSync.mock.calls.length).toBe(1); @@ -2387,7 +2394,7 @@ describe("Extension Unit Tests", () => { expect(showErrorMessage.mock.calls[1][0]).toBe("open() called from invalid node."); }); - it("Tests that openUSS executes successfully with favorited files", async () => { + it("Tests that openUSS executes successfully with favored files", async () => { ussFile.mockReset(); openTextDocument.mockReset(); showTextDocument.mockReset(); @@ -2403,7 +2410,7 @@ describe("Extension Unit Tests", () => { favoriteFile.contextValue = extension.DS_TEXT_FILE_CONTEXT + extension.FAV_SUFFIX; const favoriteParent = new ZoweUSSNode("favParent", vscode.TreeItemCollapsibleState.Collapsed, favoriteSession, null, "/"); favoriteParent.contextValue = extension.USS_DIR_CONTEXT + extension.FAV_SUFFIX; - // Set up child of favoriteDir - make sure we can open the child of a favorited directory + // Set up child of favoriteDir - make sure we can open the child of a favored directory const child = new ZoweUSSNode("favChild", vscode.TreeItemCollapsibleState.Collapsed, favoriteParent, null, "/favDir"); child.contextValue = extension.DS_TEXT_FILE_CONTEXT; @@ -2433,6 +2440,15 @@ describe("Extension Unit Tests", () => { existsSync.mockReturnValue(null); openTextDocument.mockResolvedValueOnce("test.doc"); + const response: brightside.IZosFilesResponse = { + success: true, + commandResponse: null, + apiResponse: { + etag: "123" + } + }; + withProgress.mockReturnValue(response); + await extension.openUSS(node, false, true); expect(existsSync.mock.calls.length).toBe(1); @@ -2555,8 +2571,10 @@ describe("Extension Unit Tests", () => { it("Testing that saveUSSFile is executed successfully", async () => { + withProgress.mockReset(); + const testDoc: vscode.TextDocument = { - fileName: path.join(extension.USS_DIR, ussNode.label, "testFile"), + fileName: path.join(extension.USS_DIR, "usstest", "/u/myuser/testFile"), uri: null, isUntitled: null, languageId: null, @@ -2582,19 +2600,31 @@ describe("Extension Unit Tests", () => { items: [] } }; + + fileList.mockResolvedValueOnce(testResponse); + ussNode.mProfileName = "usstest"; + ussNode.dirty = true; + const node = new ZoweUSSNode("u/myuser/testFile", vscode.TreeItemCollapsibleState.None, ussNode, null, "/"); + ussNode.children.push(node); testUSSTree.getChildren.mockReturnValueOnce([ new ZoweUSSNode("testFile", vscode.TreeItemCollapsibleState.None, ussNode, null, "/"), sessNode]); - - testResponse.apiResponse.items = ["Item1"]; + testResponse.apiResponse.items = [{name: "testFile", mode: "-rwxrwx"}]; fileToUSSFile.mockReset(); showErrorMessage.mockReset(); - + concatUSSChildNodes.mockReset(); + const mockGetEtag = jest.spyOn(node, "getEtag").mockImplementation(() => "123"); testResponse.success = true; - fileToUSSFile.mockResolvedValueOnce(testResponse); + fileToUSSFile.mockResolvedValue(testResponse); withProgress.mockReturnValueOnce(testResponse); - + concatUSSChildNodes.mockReturnValueOnce([ussNode.children[0]]); await extension.saveUSSFile(testDoc, testUSSTree); + expect(concatUSSChildNodes.mock.calls.length).toBe(1); + expect(mockGetEtag).toBeCalledTimes(1); + expect(mockGetEtag).toReturnWith("123"); + + concatUSSChildNodes.mockReset(); + concatUSSChildNodes.mockReturnValueOnce([ussNode.children[0]]); testResponse.success = false; testResponse.commandResponse = "Save failed"; fileToUSSFile.mockResolvedValueOnce(testResponse); @@ -2602,6 +2632,11 @@ describe("Extension Unit Tests", () => { await extension.saveUSSFile(testDoc, testUSSTree); + expect(showErrorMessage.mock.calls.length).toBe(1); + expect(showErrorMessage.mock.calls[0][0]).toBe("Save failed"); + + concatUSSChildNodes.mockReset(); + concatUSSChildNodes.mockReturnValueOnce([ussNode.children[0]]); showErrorMessage.mockReset(); withProgress.mockRejectedValueOnce(Error("Test Error")); @@ -2609,58 +2644,29 @@ describe("Extension Unit Tests", () => { expect(showErrorMessage.mock.calls.length).toBe(1); expect(showErrorMessage.mock.calls[0][0]).toBe("Test Error"); - const testDoc2: vscode.TextDocument = { - fileName: path.normalize("/sestest/HLQ.TEST.AFILE"), - uri: null, - isUntitled: null, - languageId: null, - version: null, - isDirty: null, - isClosed: null, - save: null, - eol: null, - lineCount: null, - lineAt: null, - offsetAt: null, - positionAt: null, - getText: null, - getWordRangeAtPosition: null, - validateRange: null, - validatePosition: null - }; - - testUSSTree.getChildren.mockReturnValueOnce([sessNode]); - - await extension.saveUSSFile(testDoc2, testUSSTree); - - const testDoc3: vscode.TextDocument = { - fileName: path.join(extension.DS_DIR, "/sestest/HLQ.TEST.AFILE(mem)"), - uri: null, - isUntitled: null, - languageId: null, - version: null, - isDirty: null, - isClosed: null, - save: null, - eol: null, - lineCount: null, - lineAt: null, - offsetAt: null, - positionAt: null, - getText: null, - getWordRangeAtPosition: null, - validateRange: null, - validatePosition: null + concatUSSChildNodes.mockReset(); + concatUSSChildNodes.mockReturnValueOnce([ussNode.children[0]]); + showWarningMessage.mockReset(); + testResponse.success = false; + testResponse.commandResponse = "Rest API failure with HTTP(S) status 412"; + testDoc.getText = jest.fn(); + ussFile.mockReset(); + withProgress.mockRejectedValueOnce(Error("Rest API failure with HTTP(S) status 412")); + const downloadResponse = { + success: true, + commandResponse: "", + apiResponse: { + etag: "" + } }; - - fileToUSSFile.mockReset(); - showErrorMessage.mockReset(); - - testUSSTree.getChildren.mockReturnValueOnce([sessNode]); - testResponse.success = true; - fileToUSSFile.mockResolvedValueOnce(testResponse); - - await extension.saveUSSFile(testDoc3, testUSSTree); + ussFile.mockResolvedValueOnce(downloadResponse); + try { + await extension.saveUSSFile(testDoc, testUSSTree); + } catch (e) { + // this is OK. We are interested in the next expect (showWarninMessage) to fullfil + expect(e.message).toBe("vscode.Position is not a constructor"); + } + expect(showWarningMessage.mock.calls[0][0]).toBe("Remote file has been modified in the meantime.\nSelect 'Compare' to resolve the conflict."); }); describe("Add Jobs Session Unit Test", () => { diff --git a/i18n/sample/package.i18n.json b/i18n/sample/package.i18n.json index f1f7674d33..544acef3b5 100644 --- a/i18n/sample/package.i18n.json +++ b/i18n/sample/package.i18n.json @@ -26,7 +26,6 @@ "uss.removeFavorite": "Remove Favorite", "removeSavedSearch": "Remove Search", "removeSession": "Remove Profile", - "safeSave": "Safe Save, Merge if Necessary", "saveSearch": "Save Search", "submitJcl": "Submit JCL", "submitMember": "Submit Job", @@ -38,7 +37,6 @@ "uss.fullPath": "Search Unix System Services (USS) by Entering a Path", "uss.refreshUSS": "Pull from Mainframe", "uss.removeSession": "Remove Profile", - "uss.safeSaveUSS": "Safe Save, Merge if Necessary", "uss.binary": "Toggle Binary", "uss.uploadDialog": "Upload Files...", "uss.text": "Toggle Text", diff --git a/i18n/sample/src/extension.i18n.json b/i18n/sample/src/extension.i18n.json index 53cf66463e..66bf989920 100644 --- a/i18n/sample/src/extension.i18n.json +++ b/i18n/sample/src/extension.i18n.json @@ -82,12 +82,6 @@ "refreshUSS.error.notFound": "not found", "refreshUSS.file1": "Unable to find file: ", "refreshUSS.file2": " was probably deleted.", - "safeSave.log.debug.request": "safe save requested for node: ", - "safeSave.error.invalidNode": "safeSave() called from invalid node.", - "safeSave.log.debug.invoke": "Invoking safesave for data set ", - "safeSave.error.notFound": "not found", - "safeSave.file1": "Unable to find file: ", - "safeSave.file2": " was probably deleted.", "saveFile.log.debug.request": "requested to save data set: ", "saveFile.log.debug.path": "path.relative returned a non-blank directory.", "saveFile.log.debug.directory": "Assuming we are not in the DS_DIR directory: ", @@ -96,6 +90,8 @@ "saveFile.log.error.session": "Couldn't locate session when saving data set!", "saveFile.log.debug.saving": "Saving file ", "saveFile.error.saveFailed": "Data set failed to save. Data set may have been deleted on mainframe.", + "saveFile.error.ZosmfEtagMismatchError": "Rest API failure with HTTP(S) status 412", + "saveFile.error.etagMismatch": "Remote file has been modified in the meantime.\nSelect 'Compare' to resolve the conflict.", "saveFile.response.save.title": "Saving data set...", "saveUSSFile.log.debug.saveRequest": "save requested for USS file ", "saveUSSFile.response.title": "Saving file...", diff --git a/package.json b/package.json index 9eaa8c6fe9..e2434b702d 100644 --- a/package.json +++ b/package.json @@ -185,14 +185,6 @@ "command": "zowe.removeSession", "title": "%removeSession%" }, - { - "command": "zowe.safeSave", - "title": "%safeSave%", - "icon": { - "light": "./resources/light/upload.svg", - "dark": "./resources/dark/upload.svg" - } - }, { "command": "zowe.saveSearch", "title": "%saveSearch%" @@ -265,14 +257,6 @@ "command": "zowe.uss.removeSession", "title": "%uss.removeSession%" }, - { - "command": "zowe.uss.safeSaveUSS", - "title": "%uss.safeSaveUSS%", - "icon": { - "light": "./resources/light/upload.svg", - "dark": "./resources/dark/upload.svg" - } - }, { "command": "zowe.uss.saveSearch", "title": "%saveSearch%" @@ -459,21 +443,11 @@ "command": "zowe.uss.refreshUSS", "group": "inline" }, - { - "when": "view == zowe.uss.explorer && viewItem != favorite && viewItem != uss_session && viewItem != directory && viewItem != directory_fav && viewItem != uss_session_fav", - "command": "zowe.uss.safeSaveUSS", - "group": "inline" - }, { "when": "view == zowe.uss.explorer && viewItem != favorite && viewItem != uss_session && viewItem != directory && viewItem != directory_fav && viewItem != uss_session_fav", "command": "zowe.uss.refreshUSS", "group": "0_mainframeInteraction" }, - { - "when": "view == zowe.uss.explorer && viewItem != favorite && viewItem != uss_session && viewItem != directory && viewItem != directory_fav && viewItem != uss_session_fav", - "command": "zowe.uss.safeSaveUSS", - "group": "0_mainframeInteraction" - }, { "when": "view == zowe.uss.explorer && viewItem != uss_session && viewItem != favorite && viewItem != textFile_fav && viewItem != binaryFile_fav && viewItem != directory_fav && viewItem != uss_session_fav", "command": "zowe.uss.deleteNode", @@ -554,21 +528,11 @@ "command": "zowe.refreshNode", "group": "inline" }, - { - "when": "view == zowe.explorer && viewItem == member", - "command": "zowe.safeSave", - "group": "inline" - }, { "when": "view == zowe.explorer && viewItem == member", "command": "zowe.refreshNode", "group": "0_mainframeInteraction" }, - { - "when": "view == zowe.explorer && viewItem == member", - "command": "zowe.safeSave", - "group": "0_mainframeInteraction" - }, { "when": "view == zowe.explorer && viewItem == member", "command": "zowe.deleteMember", @@ -596,7 +560,7 @@ }, { "when": "view == zowe.explorer && viewItem == member_fav", - "command": "zowe.safeSave", + "command": "zowe.refreshNode", "group": "inline" }, { @@ -604,11 +568,6 @@ "command": "zowe.refreshNode", "group": "0_mainframeInteraction" }, - { - "when": "view == zowe.explorer && viewItem == member_fav", - "command": "zowe.safeSave", - "group": "0_mainframeInteraction" - }, { "when": "view == zowe.explorer && viewItem == member_fav", "command": "zowe.deleteMember", @@ -635,18 +594,18 @@ "group": "inline" }, { - "when": "view == zowe.explorer && viewItem == ds", - "command": "zowe.safeSave", - "group": "inline" + "when": "view == zowe.explorer && viewItem == member_fav", + "command": "zowe.renameDataSetMember", + "group": "6_modification@2" }, { "when": "view == zowe.explorer && viewItem == ds", "command": "zowe.refreshNode", - "group": "0_mainframeInteraction" + "group": "inline" }, { "when": "view == zowe.explorer && viewItem == ds", - "command": "zowe.safeSave", + "command": "zowe.refreshNode", "group": "0_mainframeInteraction" }, { @@ -690,18 +649,23 @@ "group": "inline" }, { - "when": "view == zowe.explorer && viewItem == ds_fav", - "command": "zowe.safeSave", - "group": "inline" + "when": "view == zowe.explorer && viewItem == ds", + "command": "zowe.copyDataSet", + "group": "9_cutcopypaste" + }, + { + "when": "view == zowe.explorer && viewItem == ds", + "command": "zowe.pasteDataSet", + "group": "9_cutcopypaste" }, { "when": "view == zowe.explorer && viewItem == ds_fav", "command": "zowe.refreshNode", - "group": "0_mainframeInteraction" + "group": "inline" }, { "when": "view == zowe.explorer && viewItem == ds_fav", - "command": "zowe.safeSave", + "command": "zowe.refreshNode", "group": "0_mainframeInteraction" }, { @@ -976,10 +940,6 @@ "command": "zowe.removeSession", "when": "never" }, - { - "command": "zowe.safeSave", - "when": "never" - }, { "command": "zowe.saveSearch", "when": "never" @@ -1000,10 +960,6 @@ "command": "zowe.uss.refreshUSS", "when": "never" }, - { - "command": "zowe.uss.safeSaveUSS", - "when": "never" - }, { "command": "zowe.uss.saveSearch", "when": "never" @@ -1227,7 +1183,7 @@ "vscode-nls-dev": "^3.2.6" }, "dependencies": { - "@brightside/core": "^2.35.0", + "@brightside/core": "^2.36.0", "@types/fs-extra": "^7.0.0", "fs-extra": "^8.0.1", "gulp-cli": "^2.2.0", diff --git a/package.nls.json b/package.nls.json index ded38a7ed3..2e79693a6e 100644 --- a/package.nls.json +++ b/package.nls.json @@ -26,7 +26,6 @@ "uss.removeFavorite": "Remove Favorite", "removeSavedSearch": "Remove Search", "removeSession": "Remove Profile", - "safeSave": "Safe Save, Merge if Necessary", "saveSearch": "Save Search", "submitJcl": "Submit JCL", "submitMember": "Submit Job", @@ -38,7 +37,6 @@ "uss.fullPath": "Search Unix System Services (USS) by Entering a Path", "uss.refreshUSS": "Pull from Mainframe", "uss.removeSession": "Remove Profile", - "uss.safeSaveUSS": "Safe Save, Merge if Necessary", "uss.binary": "Toggle Binary", "uss.uploadDialog": "Upload Files...", "uss.text": "Toggle Text", diff --git a/src/ZoweNode.ts b/src/ZoweNode.ts index c495192fa4..34a695efd6 100644 --- a/src/ZoweNode.ts +++ b/src/ZoweNode.ts @@ -38,8 +38,12 @@ export class ZoweNode extends vscode.TreeItem { * @param {ZoweNode} mParent * @param {Session} session */ - constructor(label: string, collapsibleState: vscode.TreeItemCollapsibleState, - public mParent: ZoweNode, private session: Session, contextOverride?: string) { + constructor(label: string, + collapsibleState: vscode.TreeItemCollapsibleState, + public mParent: ZoweNode, + private session: Session, + contextOverride?: string, + private etag?: string) { super(label, collapsibleState); if (contextOverride) { this.contextValue = contextOverride; @@ -51,6 +55,7 @@ export class ZoweNode extends vscode.TreeItem { this.contextValue = extension.DS_DS_CONTEXT; } this.tooltip = this.label; + this.etag = etag ? etag : ""; utils.applyIcons(this); } @@ -164,4 +169,22 @@ export class ZoweNode extends vscode.TreeItem { public getSessionNode(): ZoweNode { return this.session ? this : this.mParent.getSessionNode(); } + + /** + * Returns the [etag] for this node + * + * @returns {string} + */ + public getEtag(): string { + return this.etag; + } + + /** + * Set the [etag] for this node + * + * @returns {void} + */ + public setEtag(etagValue): void { + this.etag = etagValue; + } } diff --git a/src/ZoweUSSNode.ts b/src/ZoweUSSNode.ts index 5dc177cb66..3f8f1b03d5 100644 --- a/src/ZoweUSSNode.ts +++ b/src/ZoweUSSNode.ts @@ -49,7 +49,8 @@ export class ZoweUSSNode extends vscode.TreeItem { private session: Session, private parentPath: string, public binary = false, - public mProfileName?: string) { + public mProfileName?: string, + private etag?: string) { super(label, collapsibleState); if (collapsibleState !== vscode.TreeItemCollapsibleState.None) { this.contextValue = extension.USS_DIR_CONTEXT; @@ -74,6 +75,7 @@ export class ZoweUSSNode extends vscode.TreeItem { this.label = this.profileName + this.shortLabel; this.tooltip = this.profileName + this.fullPath; } + this.etag = etag ? etag : ""; utils.applyIcons(this); } @@ -217,4 +219,27 @@ export class ZoweUSSNode extends vscode.TreeItem { this.label = this.label.replace(oldReference, this.shortLabel); this.tooltip = this.tooltip.replace(oldReference, this.shortLabel); } + + /** + * Returns the [etag] for this node + * + * @returns {string} + */ + public getEtag(): string { + return this.etag; + } + + /** + * Set the [etag] for this node + * + * @returns {void} + */ + public setEtag(etagValue): void { + this.etag = etagValue; + /** + * helper method to change the node names in one go + * @param oldReference string + * @param revision string + */ + } } diff --git a/src/extension.ts b/src/extension.ts index da91ef0f6a..a457a9fdb5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -24,7 +24,7 @@ import { ZoweUSSNode } from "./ZoweUSSNode"; import * as ussActions from "./uss/ussNodeActions"; import * as mvsActions from "./mvs/mvsNodeActions"; // tslint:disable-next-line: no-duplicate-imports -import { IJobFile } from "@brightside/core"; +import { IJobFile, IUploadOptions } from "@brightside/core"; import { Profiles } from "./Profiles"; import * as nls from "vscode-nls"; import * as utils from "./utils"; @@ -40,15 +40,19 @@ export let ISTHEIA: boolean = false; // set during activate export const FAV_SUFFIX = "_fav"; export const INFORMATION_CONTEXT = "information"; export const FAVORITE_CONTEXT = "favorite"; +export const DS_FAV_CONTEXT = "ds_fav"; +export const PDS_FAV_CONTEXT = "pds_fav"; export const DS_SESSION_CONTEXT = "session"; export const DS_PDS_CONTEXT = "pds"; export const DS_DS_CONTEXT = "ds"; export const DS_MEMBER_CONTEXT = "member"; export const DS_TEXT_FILE_CONTEXT = "textFile"; +export const DS_FAV_TEXT_FILE_CONTEXT = "textFile_fav"; export const DS_BINARY_FILE_CONTEXT = "binaryFile"; export const DS_MIGRATED_FILE_CONTEXT = "migr"; export const USS_SESSION_CONTEXT = "uss_session"; export const USS_DIR_CONTEXT = "directory"; +export const USS_FAV_DIR_CONTEXT = "directory_fav"; export const JOBS_SESSION_CONTEXT = "server"; export const JOBS_JOB_CONTEXT = "job"; export const JOBS_SPOOL_CONTEXT = "spool"; @@ -163,7 +167,6 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand("zowe.editMember", (node) => openPS(node, false)); vscode.commands.registerCommand("zowe.removeSession", async (node) => datasetProvider.deleteSession(node)); vscode.commands.registerCommand("zowe.removeFavorite", async (node) => datasetProvider.removeFavorite(node)); - vscode.commands.registerCommand("zowe.safeSave", async (node) => safeSave(node)); vscode.commands.registerCommand("zowe.saveSearch", async (node) => datasetProvider.addFavorite(node)); vscode.commands.registerCommand("zowe.removeSavedSearch", async (node) => datasetProvider.removeFavorite(node)); vscode.commands.registerCommand("zowe.submitJcl", async () => submitJcl(datasetProvider)); @@ -205,7 +208,6 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand("zowe.uss.addSession", async () => addUSSSession(ussFileProvider)); vscode.commands.registerCommand("zowe.uss.refreshAll", () => ussActions.refreshAllUSS(ussFileProvider)); vscode.commands.registerCommand("zowe.uss.refreshUSS", (node) => refreshUSS(node)); - vscode.commands.registerCommand("zowe.uss.safeSaveUSS", async (node) => safeSaveUSS(node)); vscode.commands.registerCommand("zowe.uss.fullPath", (node) => ussFileProvider.ussFilterPrompt(node)); vscode.commands.registerCommand("zowe.uss.ZoweUSSNode.open", (node) => openUSS(node, false, true)); vscode.commands.registerCommand("zowe.uss.removeSession", async (node) => ussFileProvider.deleteSession(node)); @@ -306,7 +308,7 @@ export async function activate(context: vscode.ExtensionContext) { } /** - * Allow the user to subbmit a TSO command to the selected server. Response is written + * Allow the user to submit a TSO command to the selected server. Response is written * to the output channel. * @param outputChannel The Output Channel to write the command and response to */ @@ -1470,15 +1472,18 @@ export async function openPS(node: ZoweNode, previewMember: boolean) { } log.debug(localize("openPS.log.debug.openDataSet", "opening physical sequential data set from label ") + label); // if local copy exists, open that instead of pulling from mainframe - if (!fs.existsSync(getDocumentFilePath(label, node))) { - await vscode.window.withProgress({ + const documentFilePath = getDocumentFilePath(label, node); + if (!fs.existsSync(documentFilePath)) { + const response = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: "Opening data set..." }, function downloadDataset() { return zowe.Download.dataSet(node.getSession(), label, { // TODO MISSED TESTING - file: getDocumentFilePath(label, node) + file: documentFilePath, + returnEtag: true }); }); + node.setEtag(response.apiResponse.etag); } const document = await vscode.workspace.openTextDocument(getDocumentFilePath(label, node)); if (previewMember === true) { @@ -1537,10 +1542,14 @@ export async function refreshPS(node: ZoweNode) { default: throw Error(localize("refreshPS.error.invalidNode", "refreshPS() called from invalid node.")); } - await zowe.Download.dataSet(node.getSession(), label, { - file: getDocumentFilePath(label, node) + const documentFilePath = getDocumentFilePath(label, node); + const response = await zowe.Download.dataSet(node.getSession(), label, { + file: documentFilePath, + returnEtag: true }); - const document = await vscode.workspace.openTextDocument(getDocumentFilePath(label, node)); + node.setEtag(response.apiResponse.etag); + + const document = await vscode.workspace.openTextDocument(documentFilePath); vscode.window.showTextDocument(document); // if there are unsaved changes, vscode won't automatically display the updates, so close and reopen if (document.isDirty) { @@ -1580,10 +1589,14 @@ export async function refreshUSS(node: ZoweUSSNode) { throw Error(localize("refreshUSS.error.invalidNode", "refreshPS() called from invalid node.")); } try { - await zowe.Download.ussFile(node.getSession(), node.fullPath, { - file: getUSSDocumentFilePath(node) + const ussDocumentFilePath = getUSSDocumentFilePath(node); + const response = await zowe.Download.ussFile(node.getSession(), node.fullPath, { + file: ussDocumentFilePath, + returnEtag: true }); - const document = await vscode.workspace.openTextDocument(getUSSDocumentFilePath(node)); + node.setEtag(response.apiResponse.etag); + + const document = await vscode.workspace.openTextDocument(ussDocumentFilePath); vscode.window.showTextDocument(document); // if there are unsaved changes, vscode won't automatically display the updates, so close and reopen if (document.isDirty) { @@ -1600,77 +1613,6 @@ export async function refreshUSS(node: ZoweUSSNode) { } } -/** - * Checks if there are changes on the mainframe before pushing changes - * - * @export - * @param {ZoweNode} node The node which represents the dataset - */ -export async function safeSave(node: ZoweNode) { - - log.debug(localize("safeSave.log.debug.request", "safe save requested for node: ") + node.label); - let label; - try { - switch (node.mParent.contextValue) { - case (FAVORITE_CONTEXT): - label = node.label.trim().substring(node.label.indexOf(":") + 1).trim(); - break; - case (DS_PDS_CONTEXT + FAV_SUFFIX): - label = node.mParent.label.substring(node.mParent.label.indexOf(":") + 1).trim() + "(" + node.label.trim()+ ")"; - break; - case (DS_SESSION_CONTEXT): - label = node.label.trim(); - break; - case (DS_PDS_CONTEXT): - label = node.mParent.label.trim() + "(" + node.label.trim()+ ")"; - break; - default: - throw Error(localize("safeSave.error.invalidNode", "safeSave() called from invalid node.")); - } - log.debug(localize("safeSave.log.debug.invoke", "Invoking safesave for data set ") + label); - await zowe.Download.dataSet(node.getSession(), label, { - file: getDocumentFilePath(label, node) - }); - const document = await vscode.workspace.openTextDocument(getDocumentFilePath(label, node)); - await vscode.window.showTextDocument(document); - await vscode.window.activeTextEditor.document.save(); - } catch (err) { - if (err.message.includes(localize("safeSave.error.notFound", "not found"))) { - vscode.window.showInformationMessage(localize("safeSave.file1", "Unable to find file: ") + label + - localize("safeSave.file2", " was probably deleted.")); - } else { - vscode.window.showErrorMessage(err.message); - } - } -} - -/** - * Checks if there are changes on the mainframe before pushing changes - * - * @export - * @param {ZoweUSSNode} node The node which represents the file - */ -export async function safeSaveUSS(node: ZoweUSSNode) { - - log.debug("safe save requested for node: " + node.label); - try { - // Switch case from `safeSave` not needed, as we will only ever receive a file - log.debug("Invoking safesave for USS file " + node.fullPath); - await zowe.Download.ussFile(node.getSession(), node.fullPath, { - file: getUSSDocumentFilePath(node) - }); - const document = await vscode.workspace.openTextDocument(getUSSDocumentFilePath(node)); - await vscode.window.showTextDocument(document); - await vscode.window.activeTextEditor.document.save(); - } catch (err) { - if (err.message.includes("not found")) { - vscode.window.showInformationMessage(`Unable to find file: ${node.fullPath} was probably deleted.`); - } else { - vscode.window.showErrorMessage(err.message); - } - } -} - function checkForAddedSuffix(filename: string): boolean { // identify how close to the end of the string the last . is const dotPos = filename.length - ( 1 + filename.lastIndexOf(".") ); @@ -1700,7 +1642,8 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase const sesName = ending.substring(0, ending.indexOf(path.sep)); // get session from session name - let documentSession; + let documentSession: Session; + let node: ZoweNode; const sesNode = (await datasetProvider.getChildren()).find((child) => child.label.trim() === sesName); if (sesNode) { @@ -1714,6 +1657,7 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase } if (documentSession == null) { log.error(localize("saveFile.log.error.session", "Couldn't locate session when saving data set!")); + return vscode.window.showErrorMessage(localize("saveFile.log.error.session", "Couldn't locate session when saving data set!")); } // If not a member const label = doc.fileName.substring(doc.fileName.lastIndexOf(path.sep) + 1, @@ -1731,17 +1675,80 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: Datase vscode.window.showErrorMessage(err.message + "\n" + err.stack); } } + // Get specific node based on label and parent tree (session / favorites) + let nodes: ZoweNode[]; + let isFromFavorites: boolean; + if (!sesNode || sesNode.children.length === 0) { + // saving from favorites + nodes = utils.concatChildNodes(datasetProvider.mFavorites); + isFromFavorites = true; + } else { + // saving from session + nodes = utils.concatChildNodes([sesNode]); + isFromFavorites = false; + } + node = await nodes.find((zNode) => { + // dataset in Favorites + if (zNode.contextValue === DS_FAV_CONTEXT) { + return (zNode.label === `[${sesName}]: ${label}`); + // member in Favorites + } else if (zNode.contextValue === DS_MEMBER_CONTEXT && isFromFavorites) { + const zNodeDetails = getProfileAndDataSetName(zNode); + return (`${zNodeDetails.profileName}(${zNodeDetails.dataSetName})` === `[${sesName}]: ${label}`); + } else if (zNode.contextValue === DS_MEMBER_CONTEXT && !isFromFavorites) { + const zNodeDetails = getProfileAndDataSetName(zNode); + return (`${zNodeDetails.profileName}(${zNodeDetails.dataSetName})` === `${label}`); + } else if (zNode.contextValue === DS_DS_CONTEXT) { + return (zNode.label.trim() === label); + } else { + return false; + } + }); + + // define upload options + let uploadOptions: IUploadOptions; + if (node) { + uploadOptions = { + etag: node.getEtag(), + returnEtag: true + }; + } + try { - const response = await vscode.window.withProgress({ + const uploadResponse = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: localize("saveFile.response.save.title", "Saving data set...") }, () => { - return zowe.Upload.pathToDataSet(documentSession, doc.fileName, label); // TODO MISSED TESTING + return zowe.Upload.pathToDataSet(documentSession, doc.fileName, label, uploadOptions); // TODO MISSED TESTING }); - if (response.success) { - vscode.window.showInformationMessage(response.commandResponse); // TODO MISSED TESTING + if (uploadResponse.success) { + vscode.window.showInformationMessage(uploadResponse.commandResponse); // TODO MISSED TESTING + // set local etag with the new etag from the updated file on mainframe + node.setEtag(uploadResponse.apiResponse[0].etag); + } else if (!uploadResponse.success && uploadResponse.commandResponse.includes(localize("saveFile.error.ZosmfEtagMismatchError", "Rest API failure with HTTP(S) status 412"))) { + const downloadResponse = await zowe.Download.dataSet(documentSession, label, { + file: doc.fileName, + returnEtag: true}); + // re-assign etag, so that it can be used with subsequent requests + const downloadEtag = downloadResponse.apiResponse.etag; + if (downloadEtag !== node.getEtag()) { + node.setEtag(downloadEtag); + } + vscode.window.showWarningMessage(localize("saveFile.error.etagMismatch","Remote file has been modified in the meantime.\nSelect 'Compare' to resolve the conflict.")); + // Store document in a separate variable, to be used on merge conflict + const oldDoc = doc; + const oldDocText = oldDoc.getText(); + const startPosition = new vscode.Position(0,0); + const endPosition = new vscode.Position(oldDoc.lineCount,0); + const deleteRange = new vscode.Range(startPosition, endPosition); + await vscode.window.activeTextEditor.edit((editBuilder) => { + // re-write the old content in the editor view + editBuilder.delete(deleteRange); + editBuilder.insert(startPosition, oldDocText); + }); + await vscode.window.activeTextEditor.document.save(); } else { - vscode.window.showErrorMessage(response.commandResponse); + vscode.window.showErrorMessage(uploadResponse.commandResponse); } } catch (err) { vscode.window.showErrorMessage(err.message); // TODO MISSED TESTING @@ -1763,29 +1770,81 @@ export async function saveUSSFile(doc: vscode.TextDocument, ussFileProvider: USS const remote = ending.substring(sesName.length).replace(/\\/g, "/"); // get session from session name - let documentSession; + let documentSession: Session; let binary; + let node: ZoweUSSNode; const sesNode = (await ussFileProvider.mSessionNodes.find((child) => child.mProfileName && child.mProfileName.trim()=== sesName.trim())); if (sesNode) { documentSession = sesNode.getSession(); binary = Object.keys(sesNode.binaryFiles).find((child) => child === remote) !== undefined; } + // Get specific node based on label and parent tree (session / favorites) + let nodes: ZoweUSSNode[]; + if (!sesNode || sesNode.children.length === 0) { + // saving from favorites + nodes = utils.concatUSSChildNodes(ussFileProvider.mFavorites); + } else { + // saving from session + nodes = utils.concatUSSChildNodes([sesNode]); + } + node = await nodes.find((zNode) => { + if (zNode.contextValue === DS_FAV_TEXT_FILE_CONTEXT || zNode.contextValue === DS_TEXT_FILE_CONTEXT) { + return (zNode.fullPath.trim() === remote); + } else { + return false; + } + }); + + // define upload options + let etagToUpload: string; + let returnEtag: boolean; + if (node) { + etagToUpload = node.getEtag(); + returnEtag = true; + } try { - const response = await vscode.window.withProgress({ + const uploadResponse = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: localize("saveUSSFile.response.title", "Saving file...") }, () => { - return zowe.Upload.fileToUSSFile(documentSession, doc.fileName, remote, binary); // TODO MISSED TESTING + return zowe.Upload.fileToUSSFile(documentSession, doc.fileName, remote, binary, null, etagToUpload, returnEtag); // TODO MISSED TESTING }); - if (response.success) { - vscode.window.showInformationMessage(response.commandResponse); + if (uploadResponse.success) { + vscode.window.showInformationMessage(uploadResponse.commandResponse); + // set local etag with the new etag from the updated file on mainframe + node.setEtag(uploadResponse.apiResponse.etag); + // this part never runs! zowe.Upload.fileToUSSFile doesn't return success: false, it just throws the error which is caught below!!!!! } else { - vscode.window.showErrorMessage(response.commandResponse); + vscode.window.showErrorMessage(uploadResponse.commandResponse); } } catch (err) { - log.error(localize("saveUSSFile.log.error.save", "Error encountered when saving USS file: ") + JSON.stringify(err)); - vscode.window.showErrorMessage(err.message); + if (err.message.includes(localize("saveFile.error.ZosmfEtagMismatchError", "Rest API failure with HTTP(S) status 412"))) { + const downloadResponse = await zowe.Download.ussFile(documentSession, node.fullPath, { + file: getUSSDocumentFilePath(node), + returnEtag: true}); + // re-assign etag, so that it can be used with subsequent requests + const downloadEtag = downloadResponse.apiResponse.etag; + if (downloadEtag !== etagToUpload) { + node.setEtag(downloadEtag); + } + vscode.window.showWarningMessage(localize("saveFile.error.etagMismatch","Remote file has been modified in the meantime.\nSelect 'Compare' to resolve the conflict.")); + // Store document in a separate variable, to be used on merge conflict + const oldDoc = doc; + const oldDocText = oldDoc.getText(); + const startPosition = new vscode.Position(0,0); + const endPosition = new vscode.Position(oldDoc.lineCount,0); + const deleteRange = new vscode.Range(startPosition, endPosition); + await vscode.window.activeTextEditor.edit((editBuilder) => { + // re-write the old content in the editor view + editBuilder.delete(deleteRange); + editBuilder.insert(startPosition, oldDocText); + }); + await vscode.window.activeTextEditor.document.save(); + } else { + log.error(localize("saveUSSFile.log.error.save", "Error encountered when saving USS file: ") + JSON.stringify(err)); + vscode.window.showErrorMessage(err.message); + } } } @@ -1841,20 +1900,23 @@ export async function openUSS(node: ZoweUSSNode, download = false, previewFile: } log.debug(localize("openUSS.log.debug.request", "requesting to open a uss file ") + label); // if local copy exists, open that instead of pulling from mainframe - if (download || !fs.existsSync(getUSSDocumentFilePath(node))) { + const documentFilePath = getUSSDocumentFilePath(node); + if (download || !fs.existsSync(documentFilePath)) { const chooseBinary = node.binary || await zowe.Utilities.isFileTagBinOrAscii(node.getSession(), node.fullPath); - await vscode.window.withProgress({ + const response = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: "Opening USS file...", }, function downloadUSSFile() { return zowe.Download.ussFile(node.getSession(), node.fullPath, { // TODO MISSED TESTING - file: getUSSDocumentFilePath(node), - binary: chooseBinary + file: documentFilePath, + binary: chooseBinary, + returnEtag: true }); } ); + node.setEtag(response.apiResponse.etag); } - const document = await vscode.workspace.openTextDocument(getUSSDocumentFilePath(node)); + const document = await vscode.workspace.openTextDocument(documentFilePath); if (previewFile === true) { await vscode.window.showTextDocument(document); } diff --git a/src/utils.ts b/src/utils.ts index 7ebd50ca0a..85739fec82 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -16,6 +16,8 @@ import { CliProfileManager } from "@brightside/imperative"; import { TreeItem, QuickPickItem, QuickPick } from "vscode"; import * as extension from "../src/extension"; import * as nls from "vscode-nls"; +import { ZoweUSSNode } from "./ZoweUSSNode"; +import { ZoweNode } from "./ZoweNode"; const localize = nls.config({ messageFormat: nls.MessageFormat.file })(); /* @@ -142,3 +144,31 @@ export class JobIdFilterDescriptor extends FilterDescriptor { "Job Id search")); } } + +/************************************************************************************************************* + * Returns array of all subnodes of given node + *************************************************************************************************************/ +export function concatUSSChildNodes(nodes: ZoweUSSNode[]) { + let allNodes = new Array(); + + for (const node of nodes) { + allNodes = allNodes.concat(concatUSSChildNodes(node.children)); + allNodes.push(node); + } + + return allNodes; +} + +/************************************************************************************************************* + * Returns array of all subnodes of given node + *************************************************************************************************************/ +export function concatChildNodes(nodes: ZoweNode[]) { + let allNodes = new Array(); + + for (const node of nodes) { + allNodes = allNodes.concat(concatChildNodes(node.children)); + allNodes.push(node); + } + + return allNodes; +}