diff --git a/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts b/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts index e324c5b56f..2d93aa32fa 100644 --- a/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts +++ b/packages/zowe-explorer-api/src/tree/IZoweTreeNode.ts @@ -226,8 +226,14 @@ export interface IZoweUSSTreeNode extends IZoweTreeNode { /** * Refreshes node and reopens it. * @param hasClosedInstance + * @deprecated Use reopen instead. Will be removed by version 2.0. */ refreshAndReopen?(hasClosedInstance?: boolean); + /** + * Reopens a file if it was closed (e.g. while it was being renamed). + * @param hasClosedInstance + */ + reopen?(hasClosedInstance?: boolean); /** * Adds a search node to the USS favorites list * diff --git a/packages/zowe-explorer/__tests__/__unit__/uss/USSTree.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/uss/USSTree.unit.test.ts index 9c10d6135f..1b97f039ca 100644 --- a/packages/zowe-explorer/__tests__/__unit__/uss/USSTree.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/uss/USSTree.unit.test.ts @@ -54,6 +54,9 @@ async function createGlobalMocks() { mockDisableValidationContext: jest.fn(), mockEnableValidationContext: jest.fn(), mockCheckCurrentProfile: jest.fn(), + mockTextDocumentDirty: { fileName: `/test/path/temp/_U_/sestest/test/node`, isDirty: true }, + mockTextDocumentClean: { fileName: `/test/path/temp/_U_/sestest/testClean/node`, isDirty: false }, + mockTextDocuments: [], mockProfilesInstance: null, withProgress: jest.fn(), closeOpenedTextFile: jest.fn(), @@ -72,6 +75,8 @@ async function createGlobalMocks() { profilesForValidation: { status: "active", name: "fake" }, }; + globalMocks.mockTextDocuments.push(globalMocks.mockTextDocumentDirty); + globalMocks.mockTextDocuments.push(globalMocks.mockTextDocumentClean); globalMocks.testBaseProfile.profile.tokenType = "tokenType"; globalMocks.testBaseProfile.profile.tokenValue = "testTokenValue"; globalMocks.testCombinedProfile.profile.tokenType = "tokenType"; @@ -120,6 +125,10 @@ async function createGlobalMocks() { Object.defineProperty(vscode.window, "showInputBox", { value: globalMocks.showInputBox, configurable: true }); Object.defineProperty(vscode, "ProgressLocation", { value: globalMocks.ProgressLocation, configurable: true }); Object.defineProperty(vscode.window, "withProgress", { value: globalMocks.withProgress, configurable: true }); + Object.defineProperty(vscode.workspace, "textDocuments", { + value: globalMocks.mockTextDocuments, + configurable: true, + }); Object.defineProperty(Profiles, "getInstance", { value: jest.fn().mockReturnValue(globalMocks.mockProfilesInstance), configurable: true, @@ -845,6 +854,68 @@ describe("USSTree Unit Tests - Function USSTree.findNonFavoritedNode()", () => { }); }); +describe("USSTree Unit Tests - Function USSTree.findMatchInLoadedChildren()", () => { + it("Testing that findMatchInLoadedChildren() can find a nested child node by fullPath", async () => { + const globalMocks = await createGlobalMocks(); + const sessionNode = globalMocks.testTree.mSessionNodes[1]; + const ussChild = new ZoweUSSNode( + "ussChild", + vscode.TreeItemCollapsibleState.Expanded, + globalMocks.testUSSNode, + globalMocks.testSession, + globalMocks.testUSSNode.fullPath, + false, + globalMocks.testProfile.name + ); + globalMocks.testUSSNode.children.push(ussChild); + sessionNode.children.push(globalMocks.testUSSNode); + + const matchingNode = await globalMocks.testTree.findMatchInLoadedChildren(sessionNode, ussChild.fullPath); + expect(matchingNode).toStrictEqual(ussChild); + }); +}); + +describe("USSTree Unit Tests - Function USSTree.renameUSSNode()", () => { + it("Checking common run of function", async () => { + const globalMocks = await createGlobalMocks(); + const ussSessionNode = createUSSSessionNode(globalMocks.testSession, globalMocks.testProfile); + const ussNode = createUSSNode(globalMocks.testSession, globalMocks.testProfile); + const renameSpy = jest.spyOn(ussNode, "rename"); + + ussSessionNode.children.push(ussNode); + globalMocks.testTree.mSessionNodes[1].children.push(ussNode); + + await globalMocks.testTree.renameUSSNode(ussNode, "/u/myuser/renamed"); + + expect(renameSpy).toBeCalledTimes(1); + expect(renameSpy).toBeCalledWith("/u/myuser/renamed"); + }); +}); + +describe("USSTree Unit Tests - Function USSTree.renameFavorite()", () => { + it("Checking common run of function", async () => { + const globalMocks = await createGlobalMocks(); + const ussFavNode = createFavoriteUSSNode(globalMocks.testSession, globalMocks.testProfile); + const ussFavNodeParent = new ZoweUSSNode( + "sestest", + vscode.TreeItemCollapsibleState.Expanded, + null, + globalMocks.testSession, + null, + false, + globalMocks.testProfile.name + ); + ussFavNodeParent.children.push(ussFavNode); + globalMocks.testTree.mFavorites.push(ussFavNodeParent); + const renameSpy = jest.spyOn(ussFavNode, "rename"); + + await globalMocks.testTree.renameFavorite(ussFavNode, "/u/myuser/renamed"); + + expect(renameSpy).toBeCalledTimes(1); + expect(renameSpy).toBeCalledWith("/u/myuser/renamed"); + }); +}); + describe("USSTree Unit Tests - Function USSTree.saveSearch()", () => { async function createBlockMocks(globalMocks) { const newMocks = { @@ -938,24 +1009,80 @@ describe("USSTree Unit Tests - Function USSTree.rename()", () => { return newMocks; } - it("Tests that USSTree.rename() is executed successfully for non-favorited node that is also in Favorites", async () => { + it("Tests that USSTree.rename() shows error if an open dirty file's fullpath includes that of the node being renamed", async () => { + // Open dirty file defined by globalMocks.mockTextDocumentDirty, with filepath including "sestest/test/node" const globalMocks = await createGlobalMocks(); createBlockMocks(globalMocks); + const testUSSDir = new ZoweUSSNode( + "test", + vscode.TreeItemCollapsibleState.Expanded, + globalMocks.testUSSNode, + globalMocks.testSession, + "/", + false, + globalMocks.testProfile.name + ); + Object.defineProperty(testUSSDir, "getUSSDocumentFilePath", { + value: jest.fn(() => { + return "/test/path/temp/_U_/sestest/test"; + }), + }); + const vscodeErrorMsgSpy = jest.spyOn(vscode.window, "showErrorMessage"); + const getAllLoadedItemsSpy = jest.spyOn(globalMocks.testTree, "getAllLoadedItems"); + + await globalMocks.testTree.rename(testUSSDir); + + expect(vscodeErrorMsgSpy.mock.calls.length).toBe(1); + expect(vscodeErrorMsgSpy.mock.calls[0][0]).toContain("because you have unsaved changes in this"); + expect(getAllLoadedItemsSpy.mock.calls.length).toBe(0); + }); + it("Tests that USSTree.rename() shows no error if an open clean file's fullpath includes that of the node being renamed", async () => { + // Open clean file defined by globalMocks.mockTextDocumentClean, with filepath including "sestest/test2/node" + const globalMocks = await createGlobalMocks(); + createBlockMocks(globalMocks); + const testUSSDir = new ZoweUSSNode( + "testClean", // This name intentionally contains the mock dirty document path as a substring to test for false positives + vscode.TreeItemCollapsibleState.Expanded, + globalMocks.testUSSNode, + globalMocks.testSession, + "/", + false, + globalMocks.testProfile.name + ); + Object.defineProperty(testUSSDir, "getUSSDocumentFilePath", { + value: jest.fn(() => { + return "/test/path/temp/_U_/sestest/testClean"; + }), + }); + const vscodeErrorMsgSpy = jest.spyOn(vscode.window, "showErrorMessage"); + + await globalMocks.testTree.rename(testUSSDir); + + expect(vscodeErrorMsgSpy.mock.calls.length).toBe(0); + }); + + it("Tests that USSTree.rename() is executed successfully for non-favorited node that is also in Favorites", async () => { + const globalMocks = await createGlobalMocks(); + const blockMocks = createBlockMocks(globalMocks); + const ussFavNode = blockMocks.ussFavNode; globalMocks.testUSSNode.fullPath = globalMocks.testUSSNode.fullPath + "/usstest"; globalMocks.testTree.mSessionNodes[1].children.push(globalMocks.testUSSNode); const renameNode = jest.spyOn(globalMocks.testUSSNode, "rename"); - const removeFavorite = jest.spyOn(globalMocks.testTree, "removeFavorite"); - const addFavorite = jest.spyOn(globalMocks.testTree, "addFavorite"); + const renameFavoriteSpy = jest.spyOn(globalMocks.testTree, "renameFavorite"); renameNode.mockResolvedValue(false); globalMocks.showInputBox.mockReturnValueOnce("new name"); + Object.defineProperty(globalMocks.testTree, "findFavoritedNode", { + value: jest.fn(() => { + return { ussFavNode }; + }), + }); await globalMocks.testTree.rename(globalMocks.testUSSNode); expect(globalMocks.showErrorMessage.mock.calls.length).toBe(0); expect(globalMocks.renameUSSFile.mock.calls.length).toBe(1); - expect(removeFavorite.mock.calls.length).toBe(1); - expect(addFavorite.mock.calls.length).toBe(1); + expect(renameFavoriteSpy).toHaveBeenCalledTimes(1); }); it("Tests that USSTree.rename() is executed successfully for non-favorited node with no Favorite equivalent", async () => { @@ -964,9 +1091,9 @@ describe("USSTree Unit Tests - Function USSTree.rename()", () => { globalMocks.testTree.mFavorites = []; globalMocks.testUSSNode.fullPath = globalMocks.testUSSNode.fullPath + "/usstest"; globalMocks.testTree.mSessionNodes[1].children.push(globalMocks.testUSSNode); + const renameUSSNodeSpy = jest.spyOn(globalMocks.testTree, "renameUSSNode"); + const renameFavoriteSpy = jest.spyOn(globalMocks.testTree, "renameFavorite"); const renameNode = jest.spyOn(globalMocks.testUSSNode, "rename"); - const removeFavorite = jest.spyOn(globalMocks.testTree, "removeFavorite"); - const addFavorite = jest.spyOn(globalMocks.testTree, "addFavorite"); renameNode.mockResolvedValue(false); globalMocks.showInputBox.mockReturnValueOnce("new name"); @@ -974,39 +1101,38 @@ describe("USSTree Unit Tests - Function USSTree.rename()", () => { await globalMocks.testTree.rename(globalMocks.testUSSNode); expect(globalMocks.showErrorMessage.mock.calls.length).toBe(0); expect(globalMocks.renameUSSFile.mock.calls.length).toBe(1); - expect(removeFavorite.mock.calls.length).toBe(0); - expect(addFavorite.mock.calls.length).toBe(0); + expect(renameUSSNodeSpy).toHaveBeenCalledTimes(0); + expect(renameFavoriteSpy).toHaveBeenCalledTimes(0); }); it("Tests that USSTree.rename() is executed successfully for a favorited USS file", async () => { const globalMocks = await createGlobalMocks(); const blockMocks = createBlockMocks(globalMocks); globalMocks.testTree.mSessionNodes[1].children.push(globalMocks.testUSSNode); - const removeFavorite = jest.spyOn(globalMocks.testTree, "removeFavorite"); - const addFavorite = jest.spyOn(globalMocks.testTree, "addFavorite"); + const renameUSSNodeSpy = jest.spyOn(globalMocks.testTree, "renameUSSNode"); + const renameFavoriteSpy = jest.spyOn(globalMocks.testTree, "renameFavorite"); + globalMocks.showInputBox.mockReturnValueOnce("new name"); await globalMocks.testTree.rename(blockMocks.ussFavNode); expect(globalMocks.showErrorMessage.mock.calls.length).toBe(0); expect(globalMocks.renameUSSFile.mock.calls.length).toBe(1); - expect(removeFavorite.mock.calls.length).toBe(1); - expect(addFavorite.mock.calls.length).toBe(1); + expect(renameUSSNodeSpy.mock.calls.length).toBe(1); + expect(renameFavoriteSpy.mock.calls.length).toBe(1); }); it("Tests that USSTree.rename() is executed successfully for a favorited USS file, when tree is not expanded", async () => { const globalMocks = await createGlobalMocks(); const blockMocks = createBlockMocks(globalMocks); - const removeFavorite = jest.spyOn(globalMocks.testTree, "removeFavorite"); - const addFavorite = jest.spyOn(globalMocks.testTree, "addFavorite"); + const renameUSSNode = jest.spyOn(globalMocks.testTree, "renameUSSNode"); globalMocks.showInputBox.mockReturnValueOnce("new name"); await globalMocks.testTree.rename(blockMocks.ussFavNode); expect(globalMocks.showErrorMessage.mock.calls.length).toBe(0); expect(globalMocks.renameUSSFile.mock.calls.length).toBe(1); - expect(removeFavorite.mock.calls.length).toBe(1); - expect(addFavorite.mock.calls.length).toBe(1); + expect(renameUSSNode.mock.calls.length).toBe(1); }); it("Tests that USSTree.rename() exits when blank input is provided", async () => { diff --git a/packages/zowe-explorer/__tests__/__unit__/uss/ZoweUSSNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/uss/ZoweUSSNode.unit.test.ts index 832ec21a18..3c2b9603ab 100644 --- a/packages/zowe-explorer/__tests__/__unit__/uss/ZoweUSSNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/uss/ZoweUSSNode.unit.test.ts @@ -27,6 +27,7 @@ import { import { createUSSTree } from "../../../__mocks__/mockCreators/uss"; import * as fs from "fs"; import * as path from "path"; +import * as workspaceUtils from "../../../src/utils/workspace"; import * as globals from "../../../src/globals"; jest.mock("fs"); @@ -126,6 +127,7 @@ async function createGlobalMocks() { }); Object.defineProperty(zowe, "Download", { value: globalMocks.Download, configurable: true }); Object.defineProperty(zowe, "Utilities", { value: globalMocks.Utilities, configurable: true }); + Object.defineProperty(workspaceUtils, "closeOpenedTextFile", { value: jest.fn(), configurable: true }); Object.defineProperty(globalMocks.Download, "ussFile", { value: globalMocks.ussFile, configurable: true }); Object.defineProperty(zowe, "Delete", { value: globalMocks.Delete, configurable: true }); Object.defineProperty(fs, "existsSync", { value: globalMocks.existsSync, configurable: true }); @@ -448,51 +450,6 @@ describe("ZoweUSSNode Unit Tests - Function node.refreshUSS()", () => { }); }); -describe("ZoweUSSNode Unit Tests - Function node.refreshAndReopen()", () => { - it("Tests that refreshAndReopen works for a folder", async () => { - const globalMocks = await createGlobalMocks(); - const ussDir = new ZoweUSSNode( - "usstest", - vscode.TreeItemCollapsibleState.Expanded, - null, - globalMocks.session, - null, - null, - globalMocks.profileOne.name, - "123" - ); - ussDir.contextValue = globals.USS_DIR_CONTEXT; - const vscodeCommandSpy = jest.spyOn(vscode.commands, "executeCommand"); - - await ussDir.refreshAndReopen(); - - expect(vscodeCommandSpy).toHaveBeenCalledWith("zowe.uss.refreshAll"); - }); - it("Tests that refreshAndReopen works for a file with closed tab", async () => { - const globalMocks = await createGlobalMocks(); - const hasClosedTab = true; - const ussFile = new ZoweUSSNode( - "usstest", - vscode.TreeItemCollapsibleState.None, - null, - globalMocks.session, - null, - null, - globalMocks.profileOne.name, - "123" - ); - ussFile.contextValue = globals.DS_TEXT_FILE_CONTEXT; - const vscodeCommandSpy = jest.spyOn(vscode.commands, "executeCommand"); - - await ussFile.refreshAndReopen(hasClosedTab); - - expect(vscodeCommandSpy.mock.calls[0][0]).toEqual("zowe.uss.refreshUSSInTree"); - expect(vscodeCommandSpy.mock.calls[0][1]).toEqual(ussFile); - expect(vscodeCommandSpy.mock.calls[1][0]).toEqual("zowe.uss.ZoweUSSNode.open"); - expect(vscodeCommandSpy.mock.calls[1][1]).toEqual(ussFile); - }); -}); - describe("ZoweUSSNode Unit Tests - Function node.getEtag()", () => { it("Tests that getEtag() returns a value", async () => { const globalMocks = await createGlobalMocks(); @@ -531,57 +488,114 @@ describe("ZoweUSSNode Unit Tests - Function node.setEtag()", () => { }); }); -describe("ZoweUSSNode Unit Tests - Function node.refreshAndReopen()", () => { - it("Tests that refreshAndReopen() refreshes a directory", async () => { +describe("ZoweUSSNode Unit Tests - Function node.rename()", () => { + async function createBlockMocks(globalMocks) { + const newMocks = { + ussDir: new ZoweUSSNode( + "usstest", + vscode.TreeItemCollapsibleState.Collapsed, + null, + globalMocks.session, + "/u/user", + null, + globalMocks.profileOne.name, + "123" + ), + }; + newMocks.ussDir.contextValue = globals.USS_DIR_CONTEXT; + return newMocks; + } + it("Tests that rename updates and refreshes the UI components of the node", async () => { const globalMocks = await createGlobalMocks(); + const blockMocks = await createBlockMocks(globalMocks); + const vscodeCommandSpy = jest.spyOn(vscode.commands, "executeCommand"); - const refreshAllSpy = jest.spyOn(vscode.commands, "executeCommand"); + const newFullPath = "/u/user/newName"; + await blockMocks.ussDir.rename(newFullPath); - const rootNode = new ZoweUSSNode( - "gappy", + // Expect renamed node's labels to be updated with newName + expect(blockMocks.ussDir.fullPath).toEqual(newFullPath); + expect(blockMocks.ussDir.shortLabel).toEqual("newName"); + expect(blockMocks.ussDir.label).toEqual("newName"); + expect(blockMocks.ussDir.tooltip).toEqual(newFullPath); + + // Expect node to be refreshed in UI after rename + expect(vscodeCommandSpy.mock.calls[0][0]).toEqual("zowe.uss.refreshUSSInTree"); + expect(vscodeCommandSpy.mock.calls[0][1]).toEqual(blockMocks.ussDir); + + vscodeCommandSpy.mockClear(); + }); + it("Tests that rename updates and refreshes the UI components of any loaded children for a node", async () => { + const globalMocks = await createGlobalMocks(); + const blockMocks = await createBlockMocks(globalMocks); + // Child dir of blockMocks.ussDir + const ussSubDir = new ZoweUSSNode( + "ussSubDir", vscode.TreeItemCollapsibleState.Collapsed, + blockMocks.ussDir, + globalMocks.session, + "/u/user/ussDir", null, + globalMocks.profileOne.name, + "123" + ); + ussSubDir.contextValue = globals.USS_DIR_CONTEXT; + blockMocks.ussDir.children.push(ussSubDir); + // ussSubDir child file + const ussSubDirChild = new ZoweUSSNode( + "ussChildFile", + vscode.TreeItemCollapsibleState.None, + ussSubDir, globalMocks.session, + "/u/user/ussDir/ussSubDir", null, - false, globalMocks.profileOne.name, "123" ); - rootNode.contextValue = globals.USS_DIR_CONTEXT; + ussSubDirChild.contextValue = globals.DS_TEXT_FILE_CONTEXT; + ussSubDir.children.push(ussSubDirChild); + + const newFullPath = "/u/user/newName"; + await blockMocks.ussDir.rename(newFullPath); - await rootNode.refreshAndReopen(); + // Expect renamed ussDir's subdirectory's short labels to be updated with newName + expect(ussSubDir.fullPath).toContain(newFullPath); + expect(ussSubDir.tooltip).toContain(newFullPath); - expect(refreshAllSpy).toHaveBeenCalledWith("zowe.uss.refreshAll"); - refreshAllSpy.mockClear(); + // Expect ussDir's nested file's short labels to be updated with newName + expect(ussSubDirChild.fullPath).toContain(newFullPath); + expect(ussSubDirChild.tooltip).toContain(newFullPath); }); +}); - it("Tests that refreshAndReopen() refreshes a file", async () => { +describe("ZoweUSSNode Unit Tests - Function node.reopen()", () => { + it("Tests that reopen works for a file with closed tab", async () => { const globalMocks = await createGlobalMocks(); - - const refreshAllSpy = jest.spyOn(vscode.commands, "executeCommand"); - - const rootNode = new ZoweUSSNode( - "gappy", - vscode.TreeItemCollapsibleState.Collapsed, + const hasClosedTab = true; + const ussFile = new ZoweUSSNode( + "usstest", + vscode.TreeItemCollapsibleState.None, null, globalMocks.session, null, - false, + null, globalMocks.profileOne.name, "123" ); - rootNode.contextValue = globals.DS_TEXT_FILE_CONTEXT; + ussFile.contextValue = globals.DS_TEXT_FILE_CONTEXT; + const vscodeCommandSpy = jest.spyOn(vscode.commands, "executeCommand"); - await rootNode.refreshAndReopen(); + await ussFile.reopen(hasClosedTab); - expect(refreshAllSpy).toHaveBeenCalledWith("zowe.uss.refreshUSSInTree", rootNode); - refreshAllSpy.mockClear(); + expect(vscodeCommandSpy.mock.calls[0][0]).toEqual("zowe.uss.ZoweUSSNode.open"); + expect(vscodeCommandSpy.mock.calls[0][1]).toEqual(ussFile); + vscodeCommandSpy.mockClear(); }); - it("Tests that refreshAndReopen() opens a file if asked to refresh a closed file", async () => { + it("Tests that reopen() opens a file if asked to refresh a closed file", async () => { const globalMocks = await createGlobalMocks(); - const refreshAllSpy = jest.spyOn(vscode.commands, "executeCommand"); + const vscodeCommandSpy = jest.spyOn(vscode.commands, "executeCommand"); const rootNode = new ZoweUSSNode( "gappy", @@ -595,10 +609,10 @@ describe("ZoweUSSNode Unit Tests - Function node.refreshAndReopen()", () => { ); rootNode.contextValue = globals.DS_TEXT_FILE_CONTEXT; - await rootNode.refreshAndReopen(true); + await rootNode.reopen(true); - expect(refreshAllSpy).toHaveBeenCalledWith("zowe.uss.ZoweUSSNode.open", rootNode); - refreshAllSpy.mockClear(); + expect(vscodeCommandSpy).toHaveBeenCalledWith("zowe.uss.ZoweUSSNode.open", rootNode); + vscodeCommandSpy.mockClear(); }); }); 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 9bd203a4ff..9b0ac41f19 100644 --- a/packages/zowe-explorer/__tests__/__unit__/uss/actions.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/uss/actions.unit.test.ts @@ -168,84 +168,6 @@ function createGlobalMocks() { return globalMocks; } -describe("USS Action Unit Tests - Function renameUSSNode", () => { - async function createBlockMocks(globalMocks) { - const newMocks = { - testFavNode: createFavoriteUSSNode(globalMocks.testSession, globalMocks.testProfile), - testUSSNode: createUSSNode(globalMocks.testSession, globalMocks.testProfile), - testUSSTree: null, - mockUssApi: null, - }; - - newMocks.testUSSTree = createUSSTree( - [createFavoriteUSSNode(globalMocks.testSession, globalMocks.testProfile)], - [createUSSSessionNode(globalMocks.testSession, globalMocks.testProfile)], - createTreeView() - ); - newMocks.testUSSTree.mFavorites.push(newMocks.testFavNode); - globalMocks.mockShowInputBox.mockReturnValue("testNewNodeLabel"); - jest.spyOn(newMocks.testFavNode, "getUSSDocumentFilePath").mockImplementation(jest.fn()); - jest.spyOn(newMocks.testUSSNode, "getUSSDocumentFilePath").mockImplementation(jest.fn()); - jest.spyOn(workspaceUtils, "closeOpenedTextFile").mockImplementation(jest.fn()); - - // Mock the USS API so that getSession returns the correct value - newMocks.mockUssApi = await ZoweExplorerApiRegister.getUssApi(globalMocks.testProfile); - const getUssApiMock = jest.fn(); - getUssApiMock.mockReturnValue(newMocks.mockUssApi); - ZoweExplorerApiRegister.getUssApi = getUssApiMock.bind(ZoweExplorerApiRegister); - - jest.spyOn(newMocks.testUSSNode, "getChildren").mockResolvedValueOnce([]); - - return newMocks; - } - - it("Tests if renameUSSNode works when called from a favorite", async () => { - const globalMocks = await createGlobalMocks(); - const blockMocks = await createBlockMocks(globalMocks); - - await ussNodeActions.renameUSSNode( - blockMocks.testFavNode, - blockMocks.testUSSTree, - blockMocks.testFavNode.fullPath - ); - - expect(blockMocks.testFavNode.label).toEqual("[sestest]: testNewNodeLabel"); - }); - - it("Tests if renameUSSNode works when called from a non-favorite", async () => { - const globalMocks = await createGlobalMocks(); - const blockMocks = await createBlockMocks(globalMocks); - - await ussNodeActions.renameUSSNode( - blockMocks.testUSSNode, - blockMocks.testUSSTree, - blockMocks.testUSSNode.fullPath - ); - - expect(blockMocks.testUSSNode.label).toEqual("testNewNodeLabel"); - }); - - it("Tests if renameUSSNode successfully handles errors from the ZoweExplorerApiRegister.rename function", async () => { - const globalMocks = await createGlobalMocks(); - const blockMocks = await createBlockMocks(globalMocks); - - jest.spyOn(blockMocks.testUSSNode, "rename").mockRejectedValue("Test error!"); - - let error; - try { - await ussNodeActions.renameUSSNode( - blockMocks.testUSSNode, - blockMocks.testUSSTree, - blockMocks.testUSSNode.fullPath - ); - } catch (err) { - error = err; - } - - expect(error).toEqual("Test error!"); - }); -}); - describe("USS Action Unit Tests - Function createUSSNodeDialog", () => { async function createBlockMocks(globalMocks) { const newMocks = { diff --git a/packages/zowe-explorer/i18n/sample/src/uss/USSTree.i18n.json b/packages/zowe-explorer/i18n/sample/src/uss/USSTree.i18n.json index b98ade324f..b879bae37e 100644 --- a/packages/zowe-explorer/i18n/sample/src/uss/USSTree.i18n.json +++ b/packages/zowe-explorer/i18n/sample/src/uss/USSTree.i18n.json @@ -1,8 +1,9 @@ { "Favorites": "Favorites", - "renameUSSNode.enterName": "Enter a new name for the {0}", - "renameUSSNode.error": "Unable to rename node: ", - "renameUSSNode.duplicateName": "A {0} already exists with this name. Please choose a different name.", + "renameUSS.unsavedWork": "Unable to rename {0} because you have unsaved changes in this {1}. Please save your work before renaming the {1}.", + "renameUSS.enterName": "Enter a new name for the {0}", + "renameUSS.error": "Unable to rename node: ", + "renameUSS.duplicateName": "A {0} already exists with this name. Please choose a different name.", "removeFavProfile.confirm": "This will remove all favorited USS items for profile {0}. Continue?", "removeFavProfile.continue": "Continue", "removeFavProfile.cancel": "Cancel", diff --git a/packages/zowe-explorer/i18n/sample/src/uss/actions.i18n.json b/packages/zowe-explorer/i18n/sample/src/uss/actions.i18n.json index 335546df6d..7eccf7cc20 100644 --- a/packages/zowe-explorer/i18n/sample/src/uss/actions.i18n.json +++ b/packages/zowe-explorer/i18n/sample/src/uss/actions.i18n.json @@ -3,7 +3,6 @@ "createUSSNode.fileLocation.prompt": "Choose a location to create the {0}", "createUSSNode.name": "Name of file or directory", "createUSSNode.error.create": "Unable to create node: ", - "renameUSSNode.error": "Unable to rename node: ", "uploadFile.putContents": "Uploading USS file", "copyPath.infoMessage": "Copy Path is not yet supported in Theia.", "saveUSSFile.log.debug.saveRequest": "save requested for USS file ", diff --git a/packages/zowe-explorer/src/shared/context.ts b/packages/zowe-explorer/src/shared/context.ts index 7869e783c7..82488cf054 100644 --- a/packages/zowe-explorer/src/shared/context.ts +++ b/packages/zowe-explorer/src/shared/context.ts @@ -11,6 +11,7 @@ import * as globals from "../globals"; import { TreeItem } from "vscode"; +import { IZoweTreeNode } from "@zowe/zowe-explorer-api"; /** * @@ -328,6 +329,15 @@ export function isSessionFavorite(node: TreeItem): boolean { return new RegExp("^(" + globals.FAVORITE_CONTEXT + ")").test(node.contextValue); } +/** + * Helper function to determine if node is located anywhere in a Favorites section (including as a child, grandchild, etc). + * @param node + * @returns true if node is located in Favorites, false otherwise + */ +export function isFavoriteDescendant(node: IZoweTreeNode): boolean { + return isFavorite(node.getSessionNode()); +} + /** * Helper function which identifies if the node is Vsam * @param node diff --git a/packages/zowe-explorer/src/shared/utils.ts b/packages/zowe-explorer/src/shared/utils.ts index 90612cbdea..eb26e379cd 100644 --- a/packages/zowe-explorer/src/shared/utils.ts +++ b/packages/zowe-explorer/src/shared/utils.ts @@ -171,6 +171,11 @@ export function checkForAddedSuffix(filename: string): boolean { ); } +export function checkIfChildPath(parentPath: string, childPath: string): boolean { + const relativePath = path.relative(parentPath, childPath); + return relativePath && !relativePath.startsWith("..") && !path.isAbsolute(relativePath); +} + /** * Function that rewrites the document in the active editor thus marking it dirty * @param {vscode.TextDocument} doc - document to rewrite diff --git a/packages/zowe-explorer/src/uss/USSTree.ts b/packages/zowe-explorer/src/uss/USSTree.ts index 266b103075..06cd8c27f6 100644 --- a/packages/zowe-explorer/src/uss/USSTree.ts +++ b/packages/zowe-explorer/src/uss/USSTree.ts @@ -14,7 +14,7 @@ import * as globals from "../globals"; import * as path from "path"; import { IProfileLoaded, Logger, Session } from "@zowe/imperative"; import { FilterItem, FilterDescriptor, resolveQuickPickHelper, errorHandling } from "../utils/ProfilesUtils"; -import { sortTreeItems, getAppName } from "../shared/utils"; +import { sortTreeItems, getAppName, checkIfChildPath } from "../shared/utils"; import { IZoweTree, IZoweUSSTreeNode, ValidProfileEnum, PersistenceSchemaEnum } from "@zowe/zowe-explorer-api"; import { Profiles } from "../Profiles"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; @@ -88,16 +88,43 @@ export class USSTree extends ZoweTreeProvider implements IZoweTree this.checkDuplicateLabel(parentPath + value, loadedNodes), @@ -105,24 +132,31 @@ export class USSTree extends ZoweTreeProvider implements IZoweTree temp.label === node.getLabel() && temp.contextValue.includes(node.contextValue) - ); + return matchingNodeInFavs; } /** * Finds the equivalent node not as a favorite * - * @param node + * @param node The favorited node you want to find the non-favorite equivalent for. */ public findNonFavoritedNode(node: IZoweUSSTreeNode): IZoweUSSTreeNode { + let matchingNode: IZoweUSSTreeNode; const profileName = node.getProfileName(); - const sessionNode = this.mSessionNodes.find((session) => session.label.includes(profileName)); - return sessionNode.children.find((temp) => temp.label === node.label); + const sessionNode = this.mSessionNodes.find((session) => session.getProfileName() === profileName.trim()); + if (sessionNode) { + matchingNode = this.findMatchInLoadedChildren(sessionNode, node.fullPath); + } + return matchingNode; + } + + /** + * This function is for renaming the non-favorited equivalent of a favorited node for a given profile. + * @param profileLabel + * @param oldNamePath + * @param newNamePath + */ + public async renameUSSNode(node: IZoweUSSTreeNode, newNamePath: string) { + const matchingNode: IZoweUSSTreeNode = this.findNonFavoritedNode(node); + if (matchingNode) { + matchingNode.rename(newNamePath); + } + } + + /** + * Renames a node from the favorites list + * + * @param node + */ + public async renameFavorite(node: IZoweUSSTreeNode, newNamePath: string) { + const matchingNode: IZoweUSSTreeNode = this.findFavoritedNode(node); + if (matchingNode) { + await matchingNode.rename(newNamePath); + this.refreshElement(this.mFavoriteSession); // Needed in case the node appears multiple times in Favorites (e.g. as child, grandchild) + } } /** @@ -806,6 +870,27 @@ export class USSTree extends ZoweTreeProvider implements IZoweTree child.fullPath === fullPath); + if (match === undefined) { + // Is match contained within one of the children? + for (const node of parentNode.children) { + const isFullPathChild: boolean = checkIfChildPath(node.fullPath, fullPath); + if (isFullPathChild) { + return this.findMatchInLoadedChildren(node, fullPath); + } + } + } + return match; + } + /** * Adds a single session to the USS tree * diff --git a/packages/zowe-explorer/src/uss/ZoweUSSNode.ts b/packages/zowe-explorer/src/uss/ZoweUSSNode.ts index 0f6e9f518a..f3b5a28634 100644 --- a/packages/zowe-explorer/src/uss/ZoweUSSNode.ts +++ b/packages/zowe-explorer/src/uss/ZoweUSSNode.ts @@ -302,7 +302,7 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { } /** - * Helper method to change the node names in one go + * Helper method to change the UI node names in one go * @param newFullPath string */ public async rename(newFullPath: string) { @@ -310,31 +310,38 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode { const hasClosedInstance = await closeOpenedTextFile(currentFilePath); this.fullPath = newFullPath; this.shortLabel = newFullPath.split("/").pop(); - if (contextually.isFavorite(this)) { - this.shortLabel = `[${this.getProfileName()}]: ${this.shortLabel}`; - } this.label = this.shortLabel; this.tooltip = injectAdditionalDataToTooltip(this, newFullPath); - + // Update the full path of any children already loaded locally + if (this.children.length > 0) { + this.children.forEach((child) => { + const newChildFullPath = newFullPath + "/" + child.shortLabel; + child.rename(newChildFullPath); + }); + } + await vscode.commands.executeCommand("zowe.uss.refreshUSSInTree", this); return hasClosedInstance; } /** - * Refreshes node and reopens it. + * Reopens a file if it was closed (e.g. while it was being renamed). * @param hasClosedInstance */ - public async refreshAndReopen(hasClosedInstance = false) { - if (this.isFolder) { - await vscode.commands.executeCommand("zowe.uss.refreshAll"); - } else { - await vscode.commands.executeCommand("zowe.uss.refreshUSSInTree", this); - } - + public async reopen(hasClosedInstance = false) { if (!this.isFolder && (hasClosedInstance || (this.binary && this.downloaded))) { await vscode.commands.executeCommand("zowe.uss.ZoweUSSNode.open", this); } } + /** + * Refreshes node and reopens it. + * @param hasClosedInstance + * @deprecated To be removed by version 2.0. Use reopen instead. + */ + public async refreshAndReopen(hasClosedInstance = false) { + this.reopen(hasClosedInstance); + } + /** * Helper method which sets an icon of node and initiates reloading of tree * @param iconPath diff --git a/packages/zowe-explorer/src/uss/actions.ts b/packages/zowe-explorer/src/uss/actions.ts index 73eae66a11..a0f0430031 100644 --- a/packages/zowe-explorer/src/uss/actions.ts +++ b/packages/zowe-explorer/src/uss/actions.ts @@ -108,58 +108,6 @@ export async function createUSSNodeDialog(node: IZoweUSSTreeNode, ussFileProvide } } -/** - * Process for renaming a USS Node. This could be a Favorite Node - * - * @param {IZoweTreeNode} originalNode - * @param {USSTree} ussFileProvider - * @param {string} filePath - */ -export async function renameUSSNode( - originalNode: IZoweUSSTreeNode, - ussFileProvider: IZoweTree, - filePath: string -) { - // Could be a favorite or regular entry always deal with the regular entry - const isFav = originalNode.contextValue.endsWith(globals.FAV_SUFFIX); - const oldLabel = originalNode.label; - const parentPath = originalNode.fullPath.substr(0, originalNode.fullPath.indexOf(oldLabel)); - // Check if an old favorite exists for this node - const oldFavorite: IZoweUSSTreeNode = isFav - ? originalNode - : ussFileProvider.mFavorites.find( - (temp: ZoweUSSNode) => - temp.shortLabel === oldLabel && - temp.fullPath.substr(0, temp.fullPath.indexOf(oldLabel)) === parentPath - ); - const newName = await vscode.window.showInputBox({ value: oldLabel }); - if (newName && newName !== oldLabel) { - try { - let newNamePath = path.join(parentPath + newName); - newNamePath = newNamePath.replace(/\\/g, "/"); // Added to cover Windows backslash issue - const oldNamePath = originalNode.fullPath; - - const hasClosedTab = await originalNode.rename(newNamePath); - await ZoweExplorerApiRegister.getUssApi(originalNode.getProfile()).rename(oldNamePath, newNamePath); - await deleteFromDisk(originalNode, filePath); - await originalNode.refreshAndReopen(hasClosedTab); - - if (oldFavorite) { - ussFileProvider.removeFavorite(oldFavorite); - await oldFavorite.rename(newNamePath); - ussFileProvider.addFavorite(oldFavorite); - } - } catch (err) { - errorHandling( - err, - originalNode.mProfileName, - localize("renameUSSNode.error", "Unable to rename node: ") + err.message - ); - throw err; - } - } -} - /** * Marks file as deleted from disk *