diff --git a/packages/eslint-plugin-zowe-explorer/CHANGELOG.md b/packages/eslint-plugin-zowe-explorer/CHANGELOG.md index ee5da00885..73e0d5ac57 100644 --- a/packages/eslint-plugin-zowe-explorer/CHANGELOG.md +++ b/packages/eslint-plugin-zowe-explorer/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to the "eslint-plugin-zowe-explorer" package will be documen ### Bug fixes +## `2.12.1` + ## `2.12.0` ## `2.11.2` diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index e60ca4eb87..1907e9db60 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -8,6 +8,8 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t ### Bug fixes +## `2.12.1` + ## `2.12.0` ### New features and enhancements diff --git a/packages/zowe-explorer-ftp-extension/CHANGELOG.md b/packages/zowe-explorer-ftp-extension/CHANGELOG.md index 6149e0dba7..4689ba55ee 100644 --- a/packages/zowe-explorer-ftp-extension/CHANGELOG.md +++ b/packages/zowe-explorer-ftp-extension/CHANGELOG.md @@ -6,14 +6,19 @@ All notable changes to the "zowe-explorer-ftp-extension" extension will be docum ### Bug fixes -## `2.12.0` +## `2.12.1` ### Bug fixes -- Fixed ECONNRESET error when trying to upload or create an empty data set member. [#2350](https://github.com/zowe/vscode-extension-for-zowe/issues/2350) - Fixed issue where temporary files for e-tag comparison were not deleted after use. - Fixed issue where another connection attempt was made inside `putContents` (in `getContentsTag`) even though a connection was already active. +## `2.12.0` + +### Bug fixes + +- Fixed ECONNRESET error when trying to upload or create an empty data set member. [#2350](https://github.com/zowe/vscode-extension-for-zowe/issues/2350) + ## `2.11.2` ### Bug fixes diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index c6e4c1b370..ddf3e1af95 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -10,6 +10,18 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen ### Bug fixes +## `2.12.1` + +### Bug fixes + +- Fix issue with certain actions displaying profiles that are not registered with the tree that is providing the action. [#2534](https://github.com/zowe/vscode-extension-for-zowe/issues/2534) +- Update when the option to submit local file as JCL will be displayed in context menus. [#2541](https://github.com/zowe/vscode-extension-for-zowe/issues/2541) +- Solved issue with a conflicting keybinding for `Edit History`, changed keybinding to `Ctrl`+`Alt`+`y` for Windows and `⌘ Cmd`+`⌥ Opt`+`y` for macOS. [#2543](https://github.com/zowe/vscode-extension-for-zowe/issues/2543) +- Removed duplicate context menu items displayed in USS view that now exist within the `Manage Profile` option.[#2547](https://github.com/zowe/vscode-extension-for-zowe/issues/2547) +- Fixed issue where sort PDS feature applied the date description to members without a valid date [#2552](https://github.com/zowe/vscode-extension-for-zowe/issues/2552) +- Fixed VSC Compare function, not working with Favorites from Zowe Explorer. [#2549](https://github.com/zowe/vscode-extension-for-zowe/pull/2549) +- Fixed issue where setting `zowe.security.checkForCustomCredentialManagers` appeared in all scopes instead of just the user scope [#2555](https://github.com/zowe/vscode-extension-for-zowe/issues/2555) + ## `2.12.0` ### New features and enhancements diff --git a/packages/zowe-explorer/__tests__/__unit__/command/mvsCommandHandler.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/command/mvsCommandHandler.unit.test.ts index 96a536b1d0..75d10db2ab 100644 --- a/packages/zowe-explorer/__tests__/__unit__/command/mvsCommandHandler.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/command/mvsCommandHandler.unit.test.ts @@ -21,6 +21,7 @@ import { ZoweDatasetNode } from "../../../src/dataset/ZoweDatasetNode"; import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; import * as globals from "../../../src/globals"; import { ZoweLogger } from "../../../src/utils/LoggerUtils"; +import { ProfileManagement } from "../../../src/utils/ProfileManagement"; describe("mvsCommandActions unit testing", () => { const showErrorMessage = jest.fn(); @@ -59,6 +60,10 @@ describe("mvsCommandActions unit testing", () => { }); Object.defineProperty(ZoweLogger, "trace", { value: jest.fn(), configurable: true }); Object.defineProperty(ZoweLogger, "error", { value: jest.fn(), configurable: true }); + Object.defineProperty(ProfileManagement, "getRegisteredProfileNameList", { + value: jest.fn().mockReturnValue(["firstName", "secondName"]), + configurable: true, + }); createQuickPick.mockReturnValue({ placeholder: 'Choose "Create new..." to define a new profile or select an existing profile to add to the Data Set Explorer', @@ -404,25 +409,6 @@ describe("mvsCommandActions unit testing", () => { expect(showInputBox.mock.calls.length).toBe(0); }); - it("tests the issueMvsCommand function no profiles error", async () => { - Object.defineProperty(profileLoader.Profiles, "getInstance", { - value: jest.fn(() => { - return { - allProfiles: [], - defaultProfile: undefined, - checkCurrentProfile: jest.fn(() => { - return profilesForValidation; - }), - validateProfiles: jest.fn(), - getBaseProfile: jest.fn(), - validProfile: ValidProfileEnum.VALID, - }; - }), - }); - await mvsActions.issueMvsCommand(); - expect(showInformationMessage.mock.calls[0][0]).toEqual("No profiles available"); - }); - it("tests the issueMvsCommand prompt credentials", async () => { Object.defineProperty(profileLoader.Profiles, "getInstance", { value: jest.fn(() => { @@ -626,4 +612,27 @@ describe("mvsCommandActions unit testing", () => { expect(showErrorMessage.mock.calls.length).toBe(1); expect(showErrorMessage.mock.calls[0][0]).toContain(testError.message); }); + + it("tests the issueMvsCommand function no profiles error", async () => { + Object.defineProperty(profileLoader.Profiles, "getInstance", { + value: jest.fn(() => { + return { + allProfiles: [], + defaultProfile: undefined, + checkCurrentProfile: jest.fn(() => { + return profilesForValidation; + }), + validateProfiles: jest.fn(), + getBaseProfile: jest.fn(), + validProfile: ValidProfileEnum.VALID, + }; + }), + }); + Object.defineProperty(ProfileManagement, "getRegisteredProfileNameList", { + value: jest.fn().mockReturnValue([]), + configurable: true, + }); + await mvsActions.issueMvsCommand(); + expect(showInformationMessage.mock.calls[0][0]).toEqual("No profiles available"); + }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/command/mvsCommandHandlerTheia.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/command/mvsCommandHandlerTheia.unit.test.ts index e3b6249913..766e487e8b 100644 --- a/packages/zowe-explorer/__tests__/__unit__/command/mvsCommandHandlerTheia.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/command/mvsCommandHandlerTheia.unit.test.ts @@ -18,6 +18,7 @@ import * as utils from "../../../src/utils/ProfilesUtils"; import { imperative } from "@zowe/cli"; import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; import { ZoweLogger } from "../../../src/utils/LoggerUtils"; +import { ProfileManagement } from "../../../src/utils/ProfileManagement"; describe("mvsCommandActions unit testing", () => { const showErrorMessage = jest.fn(); @@ -54,6 +55,10 @@ describe("mvsCommandActions unit testing", () => { }); Object.defineProperty(ZoweLogger, "error", { value: jest.fn(), configurable: true }); Object.defineProperty(ZoweLogger, "trace", { value: jest.fn(), configurable: true }); + Object.defineProperty(ProfileManagement, "getRegisteredProfileNameList", { + value: jest.fn().mockReturnValue(["firstName", "secondName"]), + configurable: true, + }); const ProgressLocation = jest.fn().mockImplementation(() => { return { diff --git a/packages/zowe-explorer/__tests__/__unit__/command/tsoCommandHandler.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/command/tsoCommandHandler.unit.test.ts index 746f4fef32..7cc2e3bc8b 100644 --- a/packages/zowe-explorer/__tests__/__unit__/command/tsoCommandHandler.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/command/tsoCommandHandler.unit.test.ts @@ -21,6 +21,7 @@ import { ZoweDatasetNode } from "../../../src/dataset/ZoweDatasetNode"; import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; import * as globals from "../../../src/globals"; import { ZoweLogger } from "../../../src/utils/LoggerUtils"; +import { ProfileManagement } from "../../../src/utils/ProfileManagement"; describe("TsoCommandHandler unit testing", () => { const showErrorMessage = jest.fn(); @@ -118,6 +119,10 @@ describe("TsoCommandHandler unit testing", () => { Object.defineProperty(vscode.window, "createOutputChannel", { value: createOutputChannel }); Object.defineProperty(vscode, "ProgressLocation", { value: ProgressLocation }); Object.defineProperty(vscode.window, "withProgress", { value: withProgress }); + Object.defineProperty(ProfileManagement, "getRegisteredProfileNameList", { + value: jest.fn().mockReturnValue(["firstName", "secondName"]), + configurable: true, + }); mockLoadNamedProfile.mockReturnValue({ profile: { name: "aProfile", type: "zosmf" } }); getConfiguration.mockReturnValue({ @@ -406,25 +411,6 @@ describe("TsoCommandHandler unit testing", () => { expect(showInputBox.mock.calls.length).toBe(0); }); - it("tests the issueTsoCommand function no profiles error", async () => { - Object.defineProperty(profileLoader.Profiles, "getInstance", { - value: jest.fn(() => { - return { - allProfiles: [], - defaultProfile: undefined, - checkCurrentProfile: jest.fn(() => { - return profilesForValidation; - }), - validateProfiles: jest.fn(), - getBaseProfile: jest.fn(), - validProfile: ValidProfileEnum.VALID, - }; - }), - }); - await tsoActions.issueTsoCommand(); - expect(showInformationMessage.mock.calls[0][0]).toEqual("No profiles available"); - }); - it("tests the issueTsoCommand prompt credentials", async () => { Object.defineProperty(profileLoader.Profiles, "getInstance", { value: jest.fn(() => { @@ -645,4 +631,27 @@ describe("TsoCommandHandler unit testing", () => { name: "test1", }); }); + + it("tests the issueTsoCommand function no profiles error", async () => { + Object.defineProperty(profileLoader.Profiles, "getInstance", { + value: jest.fn(() => { + return { + allProfiles: [], + defaultProfile: undefined, + checkCurrentProfile: jest.fn(() => { + return profilesForValidation; + }), + validateProfiles: jest.fn(), + getBaseProfile: jest.fn(), + validProfile: ValidProfileEnum.VALID, + }; + }), + }); + Object.defineProperty(ProfileManagement, "getRegisteredProfileNameList", { + value: jest.fn().mockReturnValue([]), + configurable: true, + }); + await tsoActions.issueTsoCommand(); + expect(showInformationMessage.mock.calls[0][0]).toEqual("No profiles available"); + }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/dataset/actions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/dataset/actions.unit.test.ts index d15404a1de..0380be8683 100644 --- a/packages/zowe-explorer/__tests__/__unit__/dataset/actions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/dataset/actions.unit.test.ts @@ -991,6 +991,100 @@ describe("Dataset Actions Unit Tests - Function saveFile", () => { afterAll(() => jest.restoreAllMocks()); + it("To check Compare Function is getting triggered from Favorites", async () => { + globals.defineGlobals(""); + const globalMocks = createGlobalMocks(); + const blockMocks = createBlockMocks(); + + // Create nodes for Session section + const node = new ZoweDatasetNode( + "HLQ.TEST.AFILE", + vscode.TreeItemCollapsibleState.None, + blockMocks.datasetSessionNode, + null, + undefined, + undefined, + blockMocks.imperativeProfile + ); + node.contextValue = globals.DS_PDS_CONTEXT; + const childNode = new ZoweDatasetNode( + "MEM", + vscode.TreeItemCollapsibleState.None, + node, + null, + undefined, + undefined, + blockMocks.imperativeProfile + ); + + // Create nodes for Favorites section + const favProfileNode = new ZoweDatasetNode( + "sestest", + vscode.TreeItemCollapsibleState.Expanded, + blockMocks.datasetFavoritesNode, + null, + globals.FAV_PROFILE_CONTEXT + ); + const favoriteNode = new ZoweDatasetNode( + "HLQ.TEST.AFILE", + vscode.TreeItemCollapsibleState.Expanded, + favProfileNode, + null, + undefined, + undefined, + blockMocks.imperativeProfile + ); + favoriteNode.contextValue = globals.DS_PDS_CONTEXT + globals.FAV_SUFFIX; + const favoriteChildNode = new ZoweDatasetNode( + "MEM", + vscode.TreeItemCollapsibleState.None, + favoriteNode, + null, + undefined, + undefined, + blockMocks.imperativeProfile + ); + + // Push nodes into respective Session or Favorites sections + node.children.push(childNode); + blockMocks.testDatasetTree.mSessionNodes.find((child) => child.label.toString().trim() === "sestest").children.push(node); + favoriteNode.children.push(favoriteChildNode); + blockMocks.testDatasetTree.mFavorites.push(favProfileNode); + blockMocks.testDatasetTree.mFavorites[0].children.push(favoriteNode); + + mocked(sharedUtils.concatChildNodes).mockReturnValueOnce([favoriteNode, favoriteChildNode]); + blockMocks.testDatasetTree.getChildren.mockReturnValueOnce(blockMocks.testDatasetTree.mSessionNodes); + mocked(zowe.List.dataSet).mockResolvedValue({ + success: true, + commandResponse: "", + apiResponse: { + items: [{ dsname: "HLQ.TEST.AFILE" }, { dsname: "HLQ.TEST.AFILE(MEM)" }], + }, + }); + mocked(zowe.Upload.pathToDataSet).mockResolvedValueOnce({ + success: true, + commandResponse: "success", + apiResponse: [ + { + etag: "123", + }, + ], + }); + mocked(vscode.window.withProgress).mockImplementation((progLocation, callback) => { + return callback(); + }); + blockMocks.profileInstance.loadNamedProfile.mockReturnValueOnce(blockMocks.imperativeProfile); + mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); + const testDocument = createTextDocument("HLQ.TEST.AFILE(MEM)", blockMocks.datasetSessionNode); + jest.spyOn(favoriteChildNode, "getEtag").mockImplementation(() => "123"); + (testDocument as any).fileName = path.join(globals.DS_DIR, blockMocks.imperativeProfile.name, testDocument.fileName); + await dsActions.saveFile(testDocument, blockMocks.testDatasetTree); + + expect(mocked(sharedUtils.concatChildNodes)).toBeCalled(); + expect(mocked(globalMocks.statusBarMsgSpy)).toBeCalledWith("success", globals.STATUS_BAR_TIMEOUT_MS); + expect(blockMocks.profileInstance.loadNamedProfile).toBeCalledWith(blockMocks.imperativeProfile.name); + }); + it("Checking common dataset saving action when no session is defined", async () => { globals.defineGlobals(""); createGlobalMocks(); @@ -1391,137 +1485,6 @@ describe("Dataset Actions Unit Tests - Function saveFile", () => { expect(mocked(sharedUtils.concatChildNodes)).toBeCalled(); expect(mocked(Gui.setStatusBarMessage)).toBeCalledWith("success", globals.STATUS_BAR_TIMEOUT_MS); }); - it("Checking common dataset saving failed due to conflict with server version", async () => { - globals.defineGlobals(""); - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const node = new ZoweDatasetNode( - "HLQ.TEST.AFILE", - vscode.TreeItemCollapsibleState.None, - blockMocks.datasetSessionNode, - null, - undefined, - undefined, - blockMocks.imperativeProfile - ); - blockMocks.datasetSessionNode.children.push(node); - - mocked(sharedUtils.concatChildNodes).mockReturnValueOnce([node]); - blockMocks.testDatasetTree.getChildren.mockReturnValueOnce([blockMocks.datasetSessionNode]); - mocked(zowe.List.dataSet).mockResolvedValue({ - success: true, - commandResponse: "", - apiResponse: { - items: [{ dsname: "HLQ.TEST.AFILE" }], - }, - }); - mocked(zowe.Upload.pathToDataSet).mockResolvedValueOnce({ - success: false, - commandResponse: "Rest API failure with HTTP(S) status 412", - apiResponse: [], - }); - - mocked(vscode.window.withProgress).mockImplementation((progLocation, callback) => { - return callback(); - }); - const profile = blockMocks.imperativeProfile; - profile.profile.encoding = 1047; - blockMocks.profileInstance.loadNamedProfile.mockReturnValueOnce(blockMocks.imperativeProfile); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - Object.defineProperty(wsUtils, "markDocumentUnsaved", { - value: jest.fn(), - configurable: true, - }); - Object.defineProperty(context, "isTypeUssTreeNode", { - value: jest.fn().mockReturnValueOnce(false), - configurable: true, - }); - Object.defineProperty(ZoweExplorerApiRegister.getMvsApi, "getContents", { - value: jest.fn(), - configurable: true, - }); - - const testDocument = createTextDocument("HLQ.TEST.AFILE", blockMocks.datasetSessionNode); - (testDocument as any).fileName = path.join(globals.DS_DIR, testDocument.fileName); - const logSpy = jest.spyOn(ZoweLogger, "warn"); - const commandSpy = jest.spyOn(vscode.commands, "executeCommand"); - - await dsActions.saveFile(testDocument, blockMocks.testDatasetTree); - - expect(logSpy).toBeCalledWith("Remote file has changed. Presenting with way to resolve file."); - expect(mocked(sharedUtils.concatChildNodes)).toBeCalled(); - expect(commandSpy).toBeCalledWith("workbench.files.action.compareWithSaved"); - logSpy.mockClear(); - commandSpy.mockClear(); - }); - - it("Checking common dataset saving failed due to conflict with server version when file size has not changed", async () => { - globals.defineGlobals(""); - createGlobalMocks(); - const blockMocks = createBlockMocks(); - const node = new ZoweDatasetNode( - "HLQ.TEST.AFILE", - vscode.TreeItemCollapsibleState.None, - blockMocks.datasetSessionNode, - null, - undefined, - undefined, - blockMocks.imperativeProfile - ); - blockMocks.datasetSessionNode.children.push(node); - - mocked(sharedUtils.concatChildNodes).mockReturnValueOnce([node]); - blockMocks.testDatasetTree.getChildren.mockReturnValueOnce([blockMocks.datasetSessionNode]); - mocked(zowe.List.dataSet).mockResolvedValue({ - success: true, - commandResponse: "", - apiResponse: { - items: [{ dsname: "HLQ.TEST.AFILE" }], - }, - }); - mocked(zowe.Upload.pathToDataSet).mockResolvedValueOnce({ - success: false, - commandResponse: "Rest API failure with HTTP(S) status 412", - apiResponse: [], - }); - - mocked(vscode.window.withProgress).mockImplementation((progLocation, callback) => { - return callback(); - }); - const profile = blockMocks.imperativeProfile; - profile.profile.encoding = 1047; - blockMocks.profileInstance.loadNamedProfile.mockReturnValueOnce(blockMocks.imperativeProfile); - mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - Object.defineProperty(wsUtils, "markDocumentUnsaved", { - value: jest.fn(), - configurable: true, - }); - Object.defineProperty(context, "isTypeUssTreeNode", { - value: jest.fn().mockReturnValueOnce(false), - configurable: true, - }); - Object.defineProperty(ZoweExplorerApiRegister.getMvsApi, "getContents", { - value: jest.fn(), - configurable: true, - }); - - const testDocument = createTextDocument("HLQ.TEST.AFILE", blockMocks.datasetSessionNode); - (testDocument as any).fileName = path.join(globals.DS_DIR, testDocument.fileName); - const logSpy = jest.spyOn(ZoweLogger, "warn"); - const commandSpy = jest.spyOn(vscode.commands, "executeCommand"); - const applyEditSpy = jest.spyOn(vscode.workspace, "applyEdit"); - jest.spyOn(fs, "statSync").mockReturnValueOnce({ size: 0 } as any); - - await dsActions.saveFile(testDocument, blockMocks.testDatasetTree); - - expect(logSpy).toBeCalledWith("Remote file has changed. Presenting with way to resolve file."); - expect(mocked(sharedUtils.concatChildNodes)).toBeCalled(); - expect(commandSpy).toBeCalledWith("workbench.files.action.compareWithSaved"); - expect(applyEditSpy).toHaveBeenCalledTimes(2); - logSpy.mockClear(); - commandSpy.mockClear(); - applyEditSpy.mockClear(); - }); }); describe("Dataset Actions Unit Tests - Function showAttributes", () => { @@ -3455,6 +3418,28 @@ describe("Dataset Actions Unit Tests - Function openPS", () => { expect(mocked(vscode.workspace.openTextDocument)).toBeCalledWith(sharedUtils.getDocumentFilePath(node.label.toString(), node)); }); + it("Checking of opening for common dataset without supporting ongoing actions", async () => { + globals.defineGlobals(""); + createGlobalMocks(); + const blockMocks = createBlockMocks(); + + mocked(blockMocks.mvsApi.getContents).mockResolvedValueOnce({ + success: true, + commandResponse: "", + apiResponse: { + etag: "123", + }, + }); + mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); + const node = new ZoweDatasetNode("node", vscode.TreeItemCollapsibleState.None, blockMocks.datasetSessionNode, null); + node.ongoingActions = undefined as any; + + await dsActions.openPS(node, true, blockMocks.testDatasetTree); + + expect(mocked(fs.existsSync)).toBeCalledWith(path.join(globals.DS_DIR, node.getSessionNode().label.toString(), node.label.toString())); + expect(mocked(vscode.workspace.openTextDocument)).toBeCalledWith(sharedUtils.getDocumentFilePath(node.label.toString(), node)); + }); + it("Checking of failed attempt to open dataset", async () => { globals.defineGlobals(""); const globalMocks = createGlobalMocks(); @@ -3472,20 +3457,20 @@ describe("Dataset Actions Unit Tests - Function openPS", () => { expect(mocked(Gui.errorMessage)).toBeCalledWith("Error: testError"); }); - it("Check for invalid/null response without supporting ongoing actions", async () => { + it("Check for invalid/null response when contents are already fetched", async () => { globals.defineGlobals(""); const globalMocks = createGlobalMocks(); const blockMocks = createBlockMocks(); - globalMocks.getContentsSpy.mockResolvedValueOnce(null); + globalMocks.getContentsSpy.mockClear(); + mocked(fs.existsSync).mockReturnValueOnce(true); mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); - const node = new ZoweDatasetNode("node", vscode.TreeItemCollapsibleState.None, blockMocks.datasetSessionNode, null); + const node = new ZoweDatasetNode("node", vscode.TreeItemCollapsibleState.None, blockMocks.datasetSessionNode, null, null, "abc"); node.ongoingActions = undefined as any; - try { - await dsActions.openPS(node, true, blockMocks.testDatasetTree); - } catch (err) { - expect(err.message).toBe("Response was null or invalid."); - } + await dsActions.openPS(node, true, blockMocks.testDatasetTree); + + expect(globalMocks.getContentsSpy).not.toHaveBeenCalled(); + expect(node.getEtag()).toBe("abc"); }); it("Checking of opening for PDS Member", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/job/actions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/job/actions.unit.test.ts index 0e11306ac0..f645da00b8 100644 --- a/packages/zowe-explorer/__tests__/__unit__/job/actions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/job/actions.unit.test.ts @@ -42,6 +42,7 @@ import * as sharedUtils from "../../../src/shared/utils"; import { ZoweLogger } from "../../../src/utils/LoggerUtils"; import { SpoolFile } from "../../../src/SpoolProvider"; import { ZosJobsProvider } from "../../../src/job/ZosJobsProvider"; +import { ProfileManagement } from "../../../src/utils/ProfileManagement"; const activeTextEditorDocument = jest.fn(); @@ -78,7 +79,9 @@ function createGlobalMocks() { mockJobArray: [], testJobsTree: null as any, jesApi: null as any, + mockProfileInstance: null, }; + newMocks.mockProfileInstance = createInstanceOfProfile(newMocks.imperativeProfile); newMocks.testJobsTree = createJobsTree(newMocks.session, newMocks.iJob, newMocks.imperativeProfile, newMocks.treeView); newMocks.mockJobArray = [newMocks.JobNode1, newMocks.JobNode2, newMocks.JobNode3] as any; newMocks.jesApi = createJesApi(newMocks.imperativeProfile); @@ -116,7 +119,7 @@ function createGlobalMocks() { get: activeTextEditorDocument, configurable: true, }); - Object.defineProperty(Profiles, "getInstance", { value: jest.fn(), configurable: true }); + Object.defineProperty(Profiles, "getInstance", { value: jest.fn().mockResolvedValue(newMocks.mockProfileInstance), configurable: true }); const executeCommand = jest.fn(); Object.defineProperty(vscode.commands, "executeCommand", { value: executeCommand, configurable: true }); Object.defineProperty(SpoolProvider, "encodeJobFile", { value: jest.fn(), configurable: true }); @@ -125,6 +128,10 @@ function createGlobalMocks() { Object.defineProperty(ZoweLogger, "debug", { value: jest.fn(), configurable: true }); Object.defineProperty(ZoweLogger, "trace", { value: jest.fn(), configurable: true }); Object.defineProperty(vscode.window, "showInformationMessage", { value: jest.fn(), configurable: true }); + Object.defineProperty(ProfileManagement, "getRegisteredProfileNameList", { + value: jest.fn().mockReturnValue([newMocks.imperativeProfile.name]), + configurable: true, + }); function settingJobObjects(job: zowe.IJob, setjobname: string, setjobid: string, setjobreturncode: string): zowe.IJob { job.jobname = setjobname; job.jobid = setjobid; diff --git a/packages/zowe-explorer/__tests__/__unit__/shared/utils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/shared/utils.unit.test.ts index 4fa7ad9190..149c2724cb 100644 --- a/packages/zowe-explorer/__tests__/__unit__/shared/utils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/shared/utils.unit.test.ts @@ -277,6 +277,7 @@ describe("Test force upload", () => { ussNode: new ZoweUSSNode(null, null, null, null, null), showInformationMessage: jest.fn(), showWarningMessage: jest.fn(), + showErrorMessage: jest.fn(), getMvsApi: jest.fn(), getUssApi: jest.fn(), withProgress: jest.fn(), @@ -297,6 +298,10 @@ describe("Test force upload", () => { value: newVariables.showWarningMessage, configurable: true, }); + Object.defineProperty(vscode.window, "showErrorMessage", { + value: newVariables.showErrorMessage, + configurable: true, + }); Object.defineProperty(ZoweExplorerApiRegister, "getMvsApi", { value: newVariables.getMvsApi, configurable: true, @@ -336,6 +341,7 @@ describe("Test force upload", () => { }, expect.any(Function) ); + expect(blockMocks.showInformationMessage.mock.calls[1][0]).toBe(blockMocks.fileResponse.commandResponse); }); it("should successfully call upload for a data set if user clicks 'Yes'", async () => { @@ -350,6 +356,7 @@ describe("Test force upload", () => { }, expect.any(Function) ); + expect(blockMocks.showInformationMessage.mock.calls[1][0]).toBe(blockMocks.fileResponse.commandResponse); }); it("should cancel upload if user clicks 'No'", async () => { @@ -368,6 +375,37 @@ describe("Test force upload", () => { "A merge conflict has been detected. Since you are running inside Theia editor, a merge conflict resolution is not available yet." ); }); + + it("should show error message if file fails to upload", async () => { + const blockMocks = await createBlockMocks(); + blockMocks.showInformationMessage.mockResolvedValueOnce("Yes"); + blockMocks.withProgress.mockResolvedValueOnce({ ...blockMocks.fileResponse, success: false }); + await sharedUtils.willForceUpload(blockMocks.ussNode, blockMocks.mockDoc, null); + expect(blockMocks.withProgress).toBeCalledWith( + { + location: vscode.ProgressLocation.Notification, + title: "Saving file...", + }, + expect.any(Function) + ); + expect(blockMocks.showErrorMessage.mock.calls[0][0]).toBe(blockMocks.fileResponse.commandResponse); + }); + + it("should show error message if upload throws an error", async () => { + const blockMocks = await createBlockMocks(); + blockMocks.showInformationMessage.mockResolvedValueOnce("Yes"); + const testError = new Error("Task failed successfully"); + blockMocks.withProgress.mockRejectedValueOnce(testError); + await sharedUtils.willForceUpload(blockMocks.ussNode, blockMocks.mockDoc, null, { name: "fakeProfile" } as any); + expect(blockMocks.withProgress).toBeCalledWith( + { + location: vscode.ProgressLocation.Notification, + title: "Saving file...", + }, + expect.any(Function) + ); + expect(blockMocks.showErrorMessage.mock.calls[0][0]).toBe(`Error: ${testError.message}`); + }); }); describe("Shared Utils Unit Tests - Function filterTreeByString", () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/uss/actions.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/uss/actions.unit.test.ts index 4d054e113a..57a79fe578 100644 --- a/packages/zowe-explorer/__tests__/__unit__/uss/actions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/uss/actions.unit.test.ts @@ -445,6 +445,34 @@ describe("USS Action Unit Tests - Function saveUSSFile", () => { return newMocks; } + it("To check Compare Function is getting triggered from Favorites", async () => { + const globalMocks = createGlobalMocks(); + const blockMocks = await createBlockMocks(globalMocks); + + // Create nodes for Session section + const node = new ZoweUSSNode("HLQ.TEST.AFILE", vscode.TreeItemCollapsibleState.Expanded, blockMocks.node, null, "/"); + const childNode = new ZoweUSSNode("MEM", vscode.TreeItemCollapsibleState.None, node, null, "/"); + node.children.push(childNode); + blockMocks.testUSSTree.mSessionNodes.find((child) => child.label.toString().trim() === "usstest").children.push(node); + + // Create nodes for Favorites section + const favProfileNode = new ZoweUSSNode("usstest", vscode.TreeItemCollapsibleState.Expanded, blockMocks.node, null, "/"); + const favoriteNode = new ZoweUSSNode("HLQ.TEST.AFILE", vscode.TreeItemCollapsibleState.Expanded, favProfileNode, null, "/"); + const favoriteChildNode = new ZoweUSSNode("MEM", vscode.TreeItemCollapsibleState.None, favoriteNode, null, "/"); + favoriteNode.children.push(favoriteChildNode); + blockMocks.testUSSTree.mFavorites.push(favProfileNode); + blockMocks.testUSSTree.mFavorites[0].children.push(favoriteNode); + mocked(sharedUtils.concatChildNodes).mockReturnValueOnce([favoriteNode, favoriteChildNode]); + + const testDocument = createTextDocument("HLQ.TEST.AFILE(MEM)", blockMocks.ussNode); + jest.spyOn(favoriteChildNode, "getEtag").mockImplementation(() => "123"); + (testDocument as any).fileName = path.join(globals.USS_DIR, "usstest/user/usstest/HLQ.TEST.AFILE/MEM"); + + await ussNodeActions.saveUSSFile(testDocument, blockMocks.testUSSTree); + + expect(mocked(sharedUtils.concatChildNodes)).toBeCalled(); + }); + it("Testing that saveUSSFile is executed successfully", async () => { const globalMocks = createGlobalMocks(); const blockMocks = await createBlockMocks(globalMocks); @@ -500,41 +528,6 @@ describe("USS Action Unit Tests - Function saveUSSFile", () => { expect(globalMocks.showErrorMessage.mock.calls[0][0]).toBe("Error: Test Error"); expect(mocked(vscode.workspace.applyEdit)).toHaveBeenCalledTimes(2); }); - - it("Tests that saveUSSFile fails when HTTP error occurs", async () => { - const globalMocks = createGlobalMocks(); - const blockMocks = await createBlockMocks(globalMocks); - - globalMocks.withProgress.mockImplementation((progLocation, callback) => callback()); - globalMocks.fileToUSSFile.mockResolvedValue(blockMocks.testResponse); - globalMocks.concatChildNodes.mockReturnValue([blockMocks.ussNode.children[0]]); - const downloadResponse = createFileResponse({ etag: "" }); - blockMocks.testResponse.success = false; - blockMocks.testResponse.commandResponse = "Rest API failure with HTTP(S) status 412"; - - globalMocks.withProgress.mockRejectedValueOnce(Error("Rest API failure with HTTP(S) status 412")); - globalMocks.ussFile.mockResolvedValueOnce(downloadResponse); - Object.defineProperty(wsUtils, "markDocumentUnsaved", { - value: jest.fn(), - configurable: true, - }); - Object.defineProperty(context, "isTypeUssTreeNode", { - value: jest.fn().mockReturnValueOnce(true), - configurable: true, - }); - const logSpy = jest.spyOn(ZoweLogger, "warn"); - const commandSpy = jest.spyOn(vscode.commands, "executeCommand"); - - try { - await ussNodeActions.saveUSSFile(blockMocks.testDoc, blockMocks.testUSSTree); - } catch (e) { - expect(e.message).toBe("vscode.Position is not a constructor"); - } - expect(logSpy).toBeCalledWith("Remote file has changed. Presenting with way to resolve file."); - expect(commandSpy).toBeCalledWith("workbench.files.action.compareWithSaved"); - logSpy.mockClear(); - commandSpy.mockClear(); - }); }); describe("USS Action Unit Tests - Functions uploadDialog & uploadFile", () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/ProfileManagement.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/ProfileManagement.unit.test.ts index 2cd395f690..e5c1ad0340 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/ProfileManagement.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/ProfileManagement.unit.test.ts @@ -10,6 +10,7 @@ */ import { ZoweDatasetNode } from "../../../src/dataset/ZoweDatasetNode"; +import * as globals from "../../../src/globals"; import * as sharedMock from "../../../__mocks__/mockCreators/shared"; import * as dsMock from "../../../__mocks__/mockCreators/datasets"; import * as unixMock from "../../../__mocks__/mockCreators/uss"; @@ -21,6 +22,7 @@ import { Profiles } from "../../../src/Profiles"; import * as vscode from "vscode"; import { imperative } from "@zowe/cli"; import { ZoweUSSNode } from "../../../src/uss/ZoweUSSNode"; +import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; jest.mock("fs"); jest.mock("vscode"); @@ -256,4 +258,80 @@ describe("ProfileManagement unit tests", () => { expect(mocks.commandSpy).toHaveBeenLastCalledWith("zowe.ds.disableValidation", mocks.mockDsSessionNode); }); }); + describe("getRegisteredProfileNameList unit tests", () => { + function createBlockMocks(globalMocks): any { + const theMocks = { + registry: { + registeredMvsApiTypes: jest.fn(), + registeredUssApiTypes: jest.fn(), + registeredJesApiTypes: jest.fn(), + }, + }; + globalMocks.mockProfileInstance.allProfiles = [{ name: "sestest" }]; + jest.spyOn(globalMocks.mockProfileInstance, "loadNamedProfile").mockReturnValue(sharedMock.createValidIProfile()); + Object.defineProperty(ZoweExplorerApiRegister, "getInstance", { + value: jest.fn().mockReturnValue(theMocks.registry), + configurable: true, + }); + return theMocks; + } + afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + it("should return zosmf profile registered with the MVS tree", () => { + const blockMocks = createBlockMocks(createGlobalMocks()); + blockMocks.registry.registeredMvsApiTypes = jest.fn().mockReturnValueOnce("zosmf"); + expect(ProfileManagement.getRegisteredProfileNameList(globals.Trees.MVS)).toEqual(["sestest"]); + }); + it("should return zosmf profile registered with the USS tree", () => { + const blockMocks = createBlockMocks(createGlobalMocks()); + blockMocks.registry.registeredUssApiTypes = jest.fn().mockReturnValueOnce("zosmf"); + expect(ProfileManagement.getRegisteredProfileNameList(globals.Trees.USS)).toEqual(["sestest"]); + }); + it("should return zosmf profile registered with the JES tree", () => { + const blockMocks = createBlockMocks(createGlobalMocks()); + blockMocks.registry.registeredJesApiTypes = jest.fn().mockReturnValueOnce("zosmf"); + expect(ProfileManagement.getRegisteredProfileNameList(globals.Trees.JES)).toEqual(["sestest"]); + }); + it("should return empty array with no profiles in allProfiles", () => { + const globalMocks = createGlobalMocks(); + const blockMocks = createBlockMocks(globalMocks); + globalMocks.mockProfileInstance.allProfiles = []; + const regSpy = jest.spyOn(blockMocks.registry, "registeredJesApiTypes"); + expect(ProfileManagement.getRegisteredProfileNameList(globals.Trees.JES)).toEqual([]); + expect(regSpy).not.toBeCalled(); + }); + it("should return empty array when profile in allProfiles doesn't load", () => { + const globalMocks = createGlobalMocks(); + const blockMocks = createBlockMocks(globalMocks); + jest.spyOn(globalMocks.mockProfileInstance, "loadNamedProfile").mockReturnValue(undefined); + const regSpy = jest.spyOn(blockMocks.registry, "registeredJesApiTypes"); + expect(ProfileManagement.getRegisteredProfileNameList(globals.Trees.JES)).toEqual([]); + expect(regSpy).not.toBeCalled(); + }); + it("should return empty array when profile type isn't registered", () => { + const blockMocks = createBlockMocks(createGlobalMocks()); + blockMocks.registry.registeredJesApiTypes = jest.fn().mockReturnValueOnce("zftp"); + expect(ProfileManagement.getRegisteredProfileNameList(globals.Trees.JES)).toEqual([]); + }); + it("should return empty array when unkown tree is forcefully passed", () => { + createBlockMocks(createGlobalMocks()); + expect(ProfileManagement.getRegisteredProfileNameList("fake" as any)).toEqual([]); + }); + it("should catch error and log a warning then return empty array", () => { + const globalMocks = createGlobalMocks(); + createBlockMocks(globalMocks); + const thrownError = new Error("fake error"); + const warnSpy = jest.spyOn(ZoweLogger, "warn"); + Object.defineProperty(Profiles, "getInstance", { + value: jest.fn().mockImplementationOnce(() => { + throw thrownError; + }), + configurable: true, + }); + expect(ProfileManagement.getRegisteredProfileNameList(globals.Trees.JES)).toEqual([]); + expect(warnSpy).toBeCalledWith(thrownError); + }); + }); }); diff --git a/packages/zowe-explorer/i18n/sample/src/shared/actions.i18n.json b/packages/zowe-explorer/i18n/sample/src/shared/actions.i18n.json index c840862098..621c4ea91e 100644 --- a/packages/zowe-explorer/i18n/sample/src/shared/actions.i18n.json +++ b/packages/zowe-explorer/i18n/sample/src/shared/actions.i18n.json @@ -6,5 +6,8 @@ "memberHistory.option.prompt.open": "Select a recent member to open", "memberHistory.options.prompt": "Select a recent member to open", "enterPattern.pattern": "No selection made. Operation cancelled.", - "getRecentMembers.empty": "No recent members found." + "getRecentMembers.empty": "No recent members found.", + "saveFile.info.compare": "Compare", + "saveFile.info.overwrite": "Overwrite", + "saveFile.info.confirmCompare": "The content of the file is newer. Compare your version with latest or overwrite the content of the file with your changes." } diff --git a/packages/zowe-explorer/package.json b/packages/zowe-explorer/package.json index a5d29c0c2b..189b6b3de0 100644 --- a/packages/zowe-explorer/package.json +++ b/packages/zowe-explorer/package.json @@ -92,8 +92,8 @@ }, { "command": "zowe.editHistory", - "key": "ctrl+y", - "mac": "cmd+y" + "key": "ctrl+alt+y", + "mac": "cmd+alt+y" }, { "command": "zowe.uss.copyUssFile", @@ -809,14 +809,14 @@ "menus": { "editor/context": [ { - "when": "editorFocus", + "when": "editorFocus && focusedView != 'workbench.panel.output'", "command": "zowe.ds.submitJcl", "group": "000_zowe_dsMainframeInteraction@1" } ], "explorer/context": [ { - "when": "!explorerResourceIsFolder", + "when": "!explorerResourceIsFolder && !listMultiSelection", "command": "zowe.ds.submitJcl", "group": "000_zowe_dsMainframeInteraction@1" } @@ -999,31 +999,11 @@ "command": "zowe.ds.filterBy", "group": "inline@0" }, - { - "when": "view == zowe.uss.explorer && viewItem =~ /^(?!.*_fav.*)ussSession.*/ && !listMultiSelection", - "command": "zowe.uss.ssoLogout", - "group": "098_zowe_ussProfileAuthentication@5" - }, { "when": "view == zowe.uss.explorer && viewItem =~ /^(?!.*_fav.*)ussSession.*/ && !listMultiSelection", "command": "zowe.editHistory", "group": "100_zowe_editHistory@100" }, - { - "when": "viewItem =~ /^(?!.*_fav.*)ussSession.*/ && !listMultiSelection", - "command": "zowe.uss.editSession", - "group": "099_zowe_ussProfileModification@1" - }, - { - "when": "viewItem =~ /^(?!.*_fav.*)ussSession.*/", - "command": "zowe.uss.removeSession", - "group": "099_zowe_ussProfileModification@98" - }, - { - "when": "viewItem =~ /^(?!.*_fav.*)ussSession.*/ && !listMultiSelection", - "command": "zowe.uss.deleteProfile", - "group": "099_zowe_ussProfileModification@99" - }, { "when": "view == zowe.ds.explorer && viewItem =~ /^session.*/ && !listMultiSelection", "command": "zowe.ds.sortBy", @@ -1891,7 +1871,7 @@ "zowe.security.checkForCustomCredentialManagers": { "type": "boolean", "description": "%zowe.security.checkForCustomCredentialManagers%", - "scope": "window", + "scope": "application", "default": "true" }, "zowe.security.secureCredentialsEnabled": { diff --git a/packages/zowe-explorer/src/command/MvsCommandHandler.ts b/packages/zowe-explorer/src/command/MvsCommandHandler.ts index 941fb13bc3..19898d318f 100644 --- a/packages/zowe-explorer/src/command/MvsCommandHandler.ts +++ b/packages/zowe-explorer/src/command/MvsCommandHandler.ts @@ -20,6 +20,7 @@ import * as nls from "vscode-nls"; import { ZoweCommandProvider } from "../abstract/ZoweCommandProvider"; import { SettingsConfig } from "../utils/SettingsConfig"; import { ZoweLogger } from "../utils/LoggerUtils"; +import { ProfileManagement } from "../utils/ProfileManagement"; // Set up localization nls.config({ @@ -65,6 +66,7 @@ export class MvsCommandHandler extends ZoweCommandProvider { */ public async issueMvsCommand(session?: imperative.Session, command?: string, node?: IZoweTreeNode): Promise { ZoweLogger.trace("MvsCommandHandler.issueMvsCommand called."); + const profiles = Profiles.getInstance(); let profile: imperative.IProfileLoaded; if (node) { await this.checkCurrentProfile(node); @@ -76,11 +78,8 @@ export class MvsCommandHandler extends ZoweCommandProvider { } } if (!session) { - const profiles = Profiles.getInstance(); - const allProfiles: imperative.IProfileLoaded[] = profiles.allProfiles; - const profileNamesList = allProfiles.map((temprofile) => { - return temprofile.name; - }); + const allProfiles = profiles.allProfiles; + const profileNamesList = ProfileManagement.getRegisteredProfileNameList(globals.Trees.MVS); if (profileNamesList.length) { const quickPickOptions: vscode.QuickPickOptions = { placeHolder: localize("issueMvsCommand.quickPickOption", "Select the Profile to use to submit the command"), @@ -94,9 +93,9 @@ export class MvsCommandHandler extends ZoweCommandProvider { } profile = allProfiles.filter((temprofile) => temprofile.name === sesName)[0]; if (!node) { - await Profiles.getInstance().checkCurrentProfile(profile); + await profiles.checkCurrentProfile(profile); } - if (Profiles.getInstance().validProfile !== ValidProfileEnum.INVALID) { + if (profiles.validProfile !== ValidProfileEnum.INVALID) { session = ZoweExplorerApiRegister.getMvsApi(profile).getSession(); } else { Gui.errorMessage(localize("issueMvsCommand.checkProfile", "Profile is invalid")); @@ -110,7 +109,7 @@ export class MvsCommandHandler extends ZoweCommandProvider { profile = node.getProfile(); } try { - if (Profiles.getInstance().validProfile !== ValidProfileEnum.INVALID) { + if (profiles.validProfile !== ValidProfileEnum.INVALID) { const commandApi = ZoweExplorerApiRegister.getInstance().getCommandApi(profile); if (commandApi) { let command1: string = command; diff --git a/packages/zowe-explorer/src/command/TsoCommandHandler.ts b/packages/zowe-explorer/src/command/TsoCommandHandler.ts index 5b39b5d8d3..2565e9cc10 100644 --- a/packages/zowe-explorer/src/command/TsoCommandHandler.ts +++ b/packages/zowe-explorer/src/command/TsoCommandHandler.ts @@ -20,6 +20,7 @@ import { ZoweCommandProvider } from "../abstract/ZoweCommandProvider"; import { IStartTsoParms, imperative } from "@zowe/cli"; import { SettingsConfig } from "../utils/SettingsConfig"; import { ZoweLogger } from "../utils/LoggerUtils"; +import { ProfileManagement } from "../utils/ProfileManagement"; // Set up localization nls.config({ @@ -65,6 +66,7 @@ export class TsoCommandHandler extends ZoweCommandProvider { */ public async issueTsoCommand(session?: imperative.Session, command?: string, node?: IZoweTreeNode): Promise { ZoweLogger.trace("TsoCommandHandler.issueTsoCommand called."); + const profiles = Profiles.getInstance(); let profile: imperative.IProfileLoaded; if (node) { await this.checkCurrentProfile(node); @@ -76,12 +78,8 @@ export class TsoCommandHandler extends ZoweCommandProvider { } } if (!session) { - const profiles = Profiles.getInstance(); - const allProfiles: imperative.IProfileLoaded[] = profiles.allProfiles; - const profileNamesList = allProfiles.map((temprofile) => { - return temprofile.name; - }); - if (profileNamesList.length) { + const profileNamesList = ProfileManagement.getRegisteredProfileNameList(globals.Trees.MVS); + if (profileNamesList.length > 0) { const quickPickOptions: vscode.QuickPickOptions = { placeHolder: localize("issueTsoCommand.quickPickOption", "Select the Profile to use to submit the TSO command"), ignoreFocusOut: true, @@ -92,11 +90,12 @@ export class TsoCommandHandler extends ZoweCommandProvider { Gui.showMessage(localize("issueTsoCommand.cancelled", "Operation Cancelled")); return; } + const allProfiles = profiles.allProfiles; profile = allProfiles.filter((temprofile) => temprofile.name === sesName)[0]; if (!node) { - await Profiles.getInstance().checkCurrentProfile(profile); + await profiles.checkCurrentProfile(profile); } - if (Profiles.getInstance().validProfile !== ValidProfileEnum.INVALID) { + if (profiles.validProfile !== ValidProfileEnum.INVALID) { session = ZoweExplorerApiRegister.getMvsApi(profile).getSession(); } else { Gui.errorMessage(localize("issueTsoCommand.checkProfile", "Profile is invalid")); @@ -110,7 +109,7 @@ export class TsoCommandHandler extends ZoweCommandProvider { profile = node.getProfile(); } try { - if (Profiles.getInstance().validProfile !== ValidProfileEnum.INVALID) { + if (profiles.validProfile !== ValidProfileEnum.INVALID) { const commandApi = ZoweExplorerApiRegister.getInstance().getCommandApi(profile); if (commandApi) { let tsoParams: IStartTsoParms; diff --git a/packages/zowe-explorer/src/dataset/ZoweDatasetNode.ts b/packages/zowe-explorer/src/dataset/ZoweDatasetNode.ts index 30242cb10c..6647ba3db4 100644 --- a/packages/zowe-explorer/src/dataset/ZoweDatasetNode.ts +++ b/packages/zowe-explorer/src/dataset/ZoweDatasetNode.ts @@ -346,11 +346,22 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod } if (sort.method === DatasetSortOpts.LastModified) { - const dateA = dayjs(a.stats?.modifiedDate); - const dateB = dayjs(b.stats?.modifiedDate); + const dateA = dayjs(a.stats?.modifiedDate ?? null); + const dateB = dayjs(b.stats?.modifiedDate ?? null); - a.description = dateA.isValid() ? dateA.format("YYYY/MM/DD HH:mm:ss") : undefined; - b.description = dateB.isValid() ? dateB.format("YYYY/MM/DD HH:mm:ss") : undefined; + const aValid = dateA.isValid(); + const bValid = dateB.isValid(); + + a.description = aValid ? dateA.format("YYYY/MM/DD HH:mm:ss") : undefined; + b.description = bValid ? dateB.format("YYYY/MM/DD HH:mm:ss") : undefined; + + if (!aValid) { + return sortGreaterThan; + } + + if (!bValid) { + return sortLessThan; + } // for dates that are equal down to the second, fallback to sorting by name if (dateA.isSame(dateB, "second")) { diff --git a/packages/zowe-explorer/src/dataset/actions.ts b/packages/zowe-explorer/src/dataset/actions.ts index 4c17d6ca9f..1bc3be5aa8 100644 --- a/packages/zowe-explorer/src/dataset/actions.ts +++ b/packages/zowe-explorer/src/dataset/actions.ts @@ -36,11 +36,12 @@ import * as contextually from "../shared/context"; import { markDocumentUnsaved, setFileSaved } from "../utils/workspace"; import { IUploadOptions } from "@zowe/zos-files-for-zowe-sdk"; import { ZoweLogger } from "../utils/LoggerUtils"; - import { promiseStatus, PromiseStatuses } from "promise-status-async"; +import { ProfileManagement } from "../utils/ProfileManagement"; // Set up localization import * as nls from "vscode-nls"; +import { resolveFileConflict } from "../shared/actions"; nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone, @@ -496,8 +497,8 @@ export async function openPS( const documentFilePath = getDocumentFilePath(label, node); let responsePromise = node.ongoingActions ? node.ongoingActions[api.NodeAction.Download] : null; - // If the local copy does not exist, fetch contents - if (!fs.existsSync(documentFilePath)) { + // If there is no ongoing action and the local copy does not exist, fetch contents + if (responsePromise == null && !fs.existsSync(documentFilePath)) { const prof = node.getProfile(); ZoweLogger.info(localize("openPS.openDataSet", "Opening {0}", label)); if (node.ongoingActions) { @@ -518,8 +519,10 @@ export async function openPS( } } - const response = await responsePromise; - node.setEtag(response?.apiResponse?.etag); + if (responsePromise != null) { + const response = await responsePromise; + node.setEtag(response.apiResponse.etag); + } statusMsg.dispose(); const document = await vscode.workspace.openTextDocument(getDocumentFilePath(label, node)); await api.Gui.showTextDocument(document, { preview: node.wasDoubleClicked != null ? !node.wasDoubleClicked : shouldPreview }); @@ -1003,10 +1006,7 @@ export async function submitJcl(datasetProvider: api.IZoweTree { - return profile.name; - }); + const profileNamesList = ProfileManagement.getRegisteredProfileNameList(globals.Trees.JES); if (profileNamesList.length) { const quickPickOptions: vscode.QuickPickOptions = { placeHolder: localize("submitJcl.qp.placeholder", "Select the Profile to use to submit the job"), @@ -1589,6 +1589,9 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: api.IZ const ending = doc.fileName.substring(start); const sesName = ending.substring(0, ending.indexOf(path.sep)); const profile = Profiles.getInstance().loadNamedProfile(sesName); + const fileLabel = doc.fileName.split("/").slice(-1)[0]; + const dataSetName = fileLabel.substring(0, fileLabel.indexOf("(")); + const memberName = fileLabel.substring(fileLabel.indexOf("(") + 1, fileLabel.indexOf(")")); if (!profile) { const sessionError = localize("saveFile.session.error", "Could not locate session when saving data set."); ZoweLogger.error(sessionError); @@ -1596,8 +1599,16 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: api.IZ return; } - // get session from session name - const sesNode = (await datasetProvider.getChildren()).find((child) => child.label.toString().trim() === sesName); + const etagFavorites = ( + datasetProvider.mFavorites + .find((child) => child.label.toString().trim() === sesName) + ?.children.find((child) => child.label.toString().trim() === dataSetName) + ?.children.find((child) => child.label.toString().trim() === memberName) as api.IZoweDatasetTreeNode + )?.getEtag(); + const sesNode = + etagFavorites !== "" && etagFavorites !== undefined + ? datasetProvider.mFavorites.find((child) => child.label.toString().trim() === sesName) + : datasetProvider.mSessionNodes.find((child) => child.label.toString().trim() === sesName); if (!sesNode) { // if saving from favorites, a session might not exist for this node ZoweLogger.debug(localize("saveFile.missingSessionNode", "Could not find session node")); @@ -1645,6 +1656,7 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: api.IZ returnEtag: true, }; + const prof = node?.getProfile() ?? profile; try { const uploadResponse = await api.Gui.withProgress( { @@ -1652,7 +1664,6 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: api.IZ title: localize("saveFile.progress.title", "Saving data set..."), }, () => { - const prof = node?.getProfile() ?? profile; if (prof.profile?.encoding) { uploadOptions.encoding = prof.profile.encoding; } @@ -1670,7 +1681,7 @@ export async function saveFile(doc: vscode.TextDocument, datasetProvider: api.IZ setFileSaved(true); } } else if (!uploadResponse.success && uploadResponse.commandResponse.includes("Rest API failure with HTTP(S) status 412")) { - await compareFileContent(doc, node, label, null, profile); + resolveFileConflict(node, profile, doc, fileLabel, label); } else { await markDocumentUnsaved(doc); api.Gui.errorMessage(uploadResponse.commandResponse); diff --git a/packages/zowe-explorer/src/globals.ts b/packages/zowe-explorer/src/globals.ts index 17cdb9b1fe..f52ae42730 100644 --- a/packages/zowe-explorer/src/globals.ts +++ b/packages/zowe-explorer/src/globals.ts @@ -122,6 +122,11 @@ export const configurationDictionary: { [k: string]: string } = { "Zowe-USS-Persistent": SETTINGS_USS_HISTORY, "Zowe-Jobs-Persistent": SETTINGS_JOBS_HISTORY, }; +export enum Trees { + USS, + MVS, + JES, +} export enum CreateDataSetTypeWithKeysEnum { DATA_SET_BINARY, diff --git a/packages/zowe-explorer/src/shared/actions.ts b/packages/zowe-explorer/src/shared/actions.ts index c1c308497b..05de1895b8 100644 --- a/packages/zowe-explorer/src/shared/actions.ts +++ b/packages/zowe-explorer/src/shared/actions.ts @@ -12,15 +12,17 @@ import * as vscode from "vscode"; import * as globals from "../globals"; import { openPS } from "../dataset/actions"; -import { Gui, IZoweDatasetTreeNode, IZoweUSSTreeNode, IZoweNodeType, IZoweTree } from "@zowe/zowe-explorer-api"; +import { Gui, IZoweDatasetTreeNode, IZoweUSSTreeNode, IZoweNodeType, IZoweTree, imperative } from "@zowe/zowe-explorer-api"; import { Profiles } from "../Profiles"; -import { filterTreeByString } from "../shared/utils"; +import { compareFileContent, filterTreeByString, willForceUpload } from "../shared/utils"; import { FilterItem, FilterDescriptor } from "../utils/ProfilesUtils"; import * as contextually from "../shared/context"; import * as nls from "vscode-nls"; import { getIconById, IconId } from "../generators/icons"; import { ZoweLogger } from "../utils/LoggerUtils"; +import { markDocumentUnsaved } from "../utils/workspace"; + // Set up localization nls.config({ messageFormat: nls.MessageFormat.bundle, @@ -244,3 +246,41 @@ export function resetValidationSettings(node: IZoweNodeType, setting: boolean): } return node; } + +export function resolveFileConflict( + node: IZoweDatasetTreeNode | IZoweUSSTreeNode, + profile: imperative.IProfileLoaded, + doc: vscode.TextDocument, + docName: string, + label?: string, + binary?: boolean +): void { + const compareBtn = localize("saveFile.info.compare", "Compare"); + const overwriteBtn = localize("saveFile.info.overwrite", "Overwrite"); + const infoMsg = localize( + "saveFile.info.confirmCompare", + "The content of the file is newer. Compare your version with latest or overwrite the content of the file with your changes." + ); + ZoweLogger.info(infoMsg); + Gui.infoMessage(infoMsg, { + items: [compareBtn, overwriteBtn], + }).then(async (selection) => { + switch (selection) { + case compareBtn: { + ZoweLogger.info(`${compareBtn} chosen.`); + await compareFileContent(doc, node, label, binary, profile); + break; + } + case overwriteBtn: { + ZoweLogger.info(`${overwriteBtn} chosen.`); + await willForceUpload(node, doc, label, profile, binary); + break; + } + default: { + ZoweLogger.info("Operation cancelled, file unsaved."); + await markDocumentUnsaved(doc); + break; + } + } + }); +} diff --git a/packages/zowe-explorer/src/shared/utils.ts b/packages/zowe-explorer/src/shared/utils.ts index de75cf1e0a..a20a1f9d79 100644 --- a/packages/zowe-explorer/src/shared/utils.ts +++ b/packages/zowe-explorer/src/shared/utils.ts @@ -24,6 +24,7 @@ import { IUploadOptions } from "@zowe/zos-files-for-zowe-sdk"; import { ZoweLogger } from "../utils/LoggerUtils"; import { isTypeUssTreeNode } from "./context"; import { markDocumentUnsaved } from "../utils/workspace"; +import { errorHandling } from "../utils/ProfilesUtils"; // Set up localization nls.config({ @@ -262,9 +263,8 @@ export function willForceUpload( doc: vscode.TextDocument, remotePath: string, profile?: imperative.IProfileLoaded, - binary?: boolean, - returnEtag?: boolean -): void { + binary?: boolean +): Thenable { // setup to handle both cases (dataset & USS) let title: string; if (isZoweDatasetTreeNode(node)) { @@ -281,24 +281,32 @@ export function willForceUpload( ); } // Don't wait for prompt to return since this would block the save queue - Gui.infoMessage(localize("saveFile.info.confirmUpload", "Would you like to overwrite the remote file?"), { + return Gui.infoMessage(localize("saveFile.info.confirmUpload", "Would you like to overwrite the remote file?"), { items: [localize("saveFile.overwriteConfirmation.yes", "Yes"), localize("saveFile.overwriteConfirmation.no", "No")], }).then(async (selection) => { if (selection === localize("saveFile.overwriteConfirmation.yes", "Yes")) { - const uploadResponse = await Gui.withProgress( - { - location: vscode.ProgressLocation.Notification, - title, - }, - () => { - return uploadContent(node, doc, remotePath, profile, binary, null, returnEtag); - } - ); - if (uploadResponse.success) { - Gui.showMessage(uploadResponse.commandResponse); - if (node) { - node.setEtag(uploadResponse.apiResponse[0].etag); + try { + const uploadResponse = await Gui.withProgress( + { + location: vscode.ProgressLocation.Notification, + title, + }, + () => { + return uploadContent(node, doc, remotePath, profile, binary, null, true); + } + ); + if (uploadResponse.success) { + Gui.showMessage(uploadResponse.commandResponse); + if (node && uploadResponse?.apiResponse[0]?.etag) { + node.setEtag(uploadResponse.apiResponse[0].etag); + } + } else { + await markDocumentUnsaved(doc); + Gui.errorMessage(uploadResponse.commandResponse); } + } catch (err) { + await markDocumentUnsaved(doc); + await errorHandling(err, profile.name); } } else { Gui.showMessage(localize("uploadContent.cancelled", "Upload cancelled.")); diff --git a/packages/zowe-explorer/src/uss/actions.ts b/packages/zowe-explorer/src/uss/actions.ts index 8ff901468d..2d61c41e95 100644 --- a/packages/zowe-explorer/src/uss/actions.ts +++ b/packages/zowe-explorer/src/uss/actions.ts @@ -29,6 +29,7 @@ import { fileExistsCaseSensitveSync } from "./utils"; import { UssFileTree, UssFileType } from "./FileStructure"; import { ZoweLogger } from "../utils/LoggerUtils"; import { AttributeView } from "./AttributeView"; +import { resolveFileConflict } from "../shared/actions"; // Set up localization nls.config({ @@ -246,6 +247,21 @@ export async function changeFileType(node: IZoweUSSTreeNode, binary: boolean, us ussFileProvider.refresh(); } +function findEtag(node: IZoweUSSTreeNode, directories: Array, index: number): boolean { + if (node === undefined || directories.indexOf(node.label.toString().trim()) === -1) { + return false; + } + if (directories.indexOf(node.label.toString().trim()) === directories.length - 1) { + return node.getEtag() !== ""; + } + + let flag: boolean = false; + for (const child of node.children) { + flag = flag || findEtag(child, directories, directories.indexOf(node.label.toString().trim()) + 1); + } + return flag; +} + /** * Uploads the file to the mainframe * @@ -260,25 +276,27 @@ export async function saveUSSFile(doc: vscode.TextDocument, ussFileProvider: IZo const ending = doc.fileName.substring(start); const sesName = ending.substring(0, ending.indexOf(path.sep)); const remote = ending.substring(sesName.length).replace(/\\/g, "/"); + const directories = doc.fileName.split(path.sep).splice(doc.fileName.split(path.sep).indexOf("_U_") + 1); + directories.splice(1, 2); + const profileSesnode: IZoweUSSTreeNode = ussFileProvider.mSessionNodes.find((child) => child.label.toString().trim() === sesName); + const etagProfiles = findEtag(profileSesnode, directories, 0); + const favoritesSesNode: IZoweUSSTreeNode = ussFileProvider.mFavorites.find((child) => child.label.toString().trim() === sesName); + const etagFavorites = findEtag(favoritesSesNode, directories, 0); // get session from session name let binary; - const sesNode: IZoweUSSTreeNode = ussFileProvider.mSessionNodes.find( - (child) => child.getProfileName() && child.getProfileName() === sesName.trim() - ); + let sesNode: IZoweUSSTreeNode; + if ((etagProfiles && etagFavorites) || etagProfiles) { + sesNode = profileSesnode; + } else if (etagFavorites) { + sesNode = favoritesSesNode; + } if (sesNode) { binary = Object.keys(sesNode.binaryFiles).find((child) => child === remote) !== undefined; } // Get specific node based on label and parent tree (session / favorites) - let nodes: IZoweUSSTreeNode[]; - if (!sesNode || sesNode.children.length === 0) { - // saving from favorites - nodes = concatChildNodes(ussFileProvider.mFavorites); - } else { - // saving from session - nodes = concatChildNodes([sesNode]); - } + const nodes: IZoweUSSTreeNode[] = concatChildNodes(sesNode ? [sesNode] : ussFileProvider.mSessionNodes); const node = nodes.find((zNode) => { if (contextually.isText(zNode)) { return zNode.fullPath.trim() === remote; @@ -326,7 +344,8 @@ export async function saveUSSFile(doc: vscode.TextDocument, ussFileProvider: IZo // TODO: error handling must not be zosmf specific const errorMessage = err ? err.message : err.toString(); if (errorMessage.includes("Rest API failure with HTTP(S) status 412")) { - await compareFileContent(doc, node, null, binary); + const fileLabel = doc.fileName.split("/").slice(-1)[0]; + resolveFileConflict(node, sesNode.getProfile(), doc, fileLabel, remote, binary); } else { await markDocumentUnsaved(doc); await errorHandling(err, sesName); diff --git a/packages/zowe-explorer/src/utils/ProfileManagement.ts b/packages/zowe-explorer/src/utils/ProfileManagement.ts index 5825a8fdac..304e8fbe8e 100644 --- a/packages/zowe-explorer/src/utils/ProfileManagement.ts +++ b/packages/zowe-explorer/src/utils/ProfileManagement.ts @@ -27,6 +27,24 @@ nls.config({ const localize: nls.LocalizeFunc = nls.loadMessageBundle(); export class ProfileManagement { + public static getRegisteredProfileNameList(registeredTree: globals.Trees): string[] { + let profileNamesList: string[] = []; + try { + profileNamesList = Profiles.getInstance() + .allProfiles.map((profile) => profile.name) + .filter((profileName) => { + const profile = Profiles.getInstance().loadNamedProfile(profileName); + if (profile) { + return this.isProfileRegisteredWithTree(registeredTree, profile); + } + return false; + }); + return profileNamesList; + } catch (err) { + ZoweLogger.warn(err); + return profileNamesList; + } + } public static async manageProfile(node: IZoweTreeNode): Promise { const profile = node.getProfile(); let selected: vscode.QuickPickItem; @@ -274,4 +292,23 @@ export class ProfileManagement { } return vscode.commands.executeCommand("zowe.jobs.disableValidation", node); } + private static isProfileRegisteredWithTree(tree: globals.Trees, profile: imperative.IProfileLoaded): boolean { + switch (tree) { + case globals.Trees.MVS: { + const mvsProfileTypes = ZoweExplorerApiRegister.getInstance().registeredMvsApiTypes(); + return mvsProfileTypes.includes(profile.type); + } + case globals.Trees.USS: { + const ussProfileTypes = ZoweExplorerApiRegister.getInstance().registeredUssApiTypes(); + return ussProfileTypes.includes(profile.type); + } + case globals.Trees.JES: { + const jesProfileTypes = ZoweExplorerApiRegister.getInstance().registeredJesApiTypes(); + return jesProfileTypes.includes(profile.type); + } + default: { + return false; + } + } + } }