From 07232b7ffac2eb9b030b07b8a380508bb4ff2008 Mon Sep 17 00:00:00 2001 From: Trae Yelovich Date: Fri, 22 Nov 2024 10:48:28 -0500 Subject: [PATCH] ds: Show error for unsaved dataset rename; use mtime in ms for DS entries (#3326) * fix(ds): update rename to match FS sample; check editor before rename Signed-off-by: Trae Yelovich * chore: update changelog Signed-off-by: Trae Yelovich * refactor: create common fn TreeViewUtils.promptedForUnsavedResource Signed-off-by: Trae Yelovich * tests: resolve failing tests, add cases for promptedForUnsavedResource Signed-off-by: Trae Yelovich * tests: patch coverage for DatasetTree, USSTree Signed-off-by: Trae Yelovich * test: mtime update in USS provider Signed-off-by: Trae Yelovich * refactor: shorten logic in promptedForUnsavedResource Signed-off-by: Trae Yelovich * remove changes to DatasetFS rename Signed-off-by: Trae Yelovich * chore: add PR number to changelog Signed-off-by: Trae Yelovich * rename to errorForUnsavedResource, update tests & l10n Signed-off-by: Trae Yelovich * refactor: use checkCurrentProfile in tree rename fns Signed-off-by: Trae Yelovich * chore: address changelog feedback Signed-off-by: Trae Yelovich * refactor: log error in errorForUnsavedResource Signed-off-by: Trae Yelovich --------- Signed-off-by: Trae Yelovich --- packages/zowe-explorer/CHANGELOG.md | 3 +- .../__tests__/__mocks__/mockCreators/uss.ts | 1 - .../__tests__/__mocks__/vscode.ts | 3 +- .../dataset/DatasetFSProvider.unit.test.ts | 3 +- .../trees/dataset/DatasetTree.unit.test.ts | 26 +++ .../__unit__/trees/uss/USSTree.unit.test.ts | 21 +- .../trees/uss/UssFSProvider.unit.test.ts | 20 ++ .../__unit__/utils/TreeViewUtils.unit.test.ts | 194 +++++++++++++++++- packages/zowe-explorer/l10n/bundle.l10n.json | 80 ++++---- packages/zowe-explorer/l10n/poeditor.json | 20 +- .../src/trees/dataset/DatasetFSProvider.ts | 4 +- .../src/trees/dataset/DatasetTree.ts | 4 + .../zowe-explorer/src/trees/uss/USSTree.ts | 25 +-- .../src/trees/uss/UssFSProvider.ts | 7 +- .../zowe-explorer/src/utils/TreeViewUtils.ts | 45 +++- 15 files changed, 366 insertions(+), 90 deletions(-) diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index 53edd48f8d..50fea1c71e 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -6,12 +6,12 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen ### New features and enhancements -- Updated Zowe SDKs to `8.8.2` for technical currency. [#3296](https://github.com/zowe/zowe-explorer-vscode/pull/3296) - Added expired JSON web token detection for profiles in each tree view (Data Sets, USS, Jobs). When a user performs a search on a profile, they are prompted to log in if their token expired. [#3175](https://github.com/zowe/zowe-explorer-vscode/issues/3175) - Add a data set or USS resource to a virtual workspace with the new "Add to Workspace" context menu option. [#3265](https://github.com/zowe/zowe-explorer-vscode/issues/3265) - Power users and developers can now build links to efficiently open mainframe resources in Zowe Explorer. Use the **Copy External Link** option in the context menu to get the URL for a data set or USS resource, or create a link in the format `vscode://Zowe.vscode-extension-for-zowe?`. For more information on building resource URIs, see the [FileSystemProvider wiki article](https://github.com/zowe/zowe-explorer-vscode/wiki/FileSystemProvider#file-paths-vs-uris). [#3271](https://github.com/zowe/zowe-explorer-vscode/pull/3271) - Implemented more user-friendly error messages for API or network errors within Zowe Explorer. [#3243](https://github.com/zowe/zowe-explorer-vscode/pull/3243) - Use the "Troubleshoot" option for certain errors to obtain additional context, tips, and resources for how to resolve the errors. [#3243](https://github.com/zowe/zowe-explorer-vscode/pull/3243) +- Updated Zowe SDKs to `8.8.2` for technical currency. [#3296](https://github.com/zowe/zowe-explorer-vscode/pull/3296) - Allow extenders to add context menu actions to a top level node, i.e. data sets, USS, Jobs, by encoding the profile type in the context value. [#3309](https://github.com/zowe/zowe-explorer-vscode/pull/3309) - You can now add multiple partitioned data sets or USS directories to your workspace at once using the "Add to Workspace" feature. [#3324](https://github.com/zowe/zowe-explorer-vscode/issues/3324) - Exposed read and write access to local storage keys for Zowe Explorer extenders. [#3180](https://github.com/zowe/zowe-explorer-vscode/issues/3180) @@ -22,6 +22,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen - Fixed an issue where editing a team config file or updating secrets in the OS credential vault could trigger multiple events for a single action. [#3296](https://github.com/zowe/zowe-explorer-vscode/pull/3296) - Fixed an issue where opening a PDS member after renaming an expanded PDS resulted in an error. [#3314](https://github.com/zowe/zowe-explorer-vscode/issues/3314) - Fixed issue where persistent settings defined at the workspace level were migrated into global storage rather than workspace-specific storage. [#3180](https://github.com/zowe/zowe-explorer-vscode/issues/3180) +- Fixed an issue where renaming a data set with unsaved changes did not cancel the rename operation. Now, when renaming a data set with unsaved changes, you are prompted to resolve them before continuing. [#3326](https://github.com/zowe/zowe-explorer-vscode/pull/3326) ## `3.0.3` diff --git a/packages/zowe-explorer/__tests__/__mocks__/mockCreators/uss.ts b/packages/zowe-explorer/__tests__/__mocks__/mockCreators/uss.ts index 07f19f0088..a2f5f60f0e 100644 --- a/packages/zowe-explorer/__tests__/__mocks__/mockCreators/uss.ts +++ b/packages/zowe-explorer/__tests__/__mocks__/mockCreators/uss.ts @@ -76,7 +76,6 @@ export function createUSSSessionNode(session: imperative.Session, profile: imper collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, session, profile, - parentPath: "/", }); zoweUSSNode.fullPath = "/test"; zoweUSSNode.contextValue = Constants.USS_SESSION_CONTEXT; diff --git a/packages/zowe-explorer/__tests__/__mocks__/vscode.ts b/packages/zowe-explorer/__tests__/__mocks__/vscode.ts index 5e81e6e762..cfd15d1557 100644 --- a/packages/zowe-explorer/__tests__/__mocks__/vscode.ts +++ b/packages/zowe-explorer/__tests__/__mocks__/vscode.ts @@ -918,7 +918,7 @@ export namespace l10n { return options; } options.args?.forEach((arg: string, i: number) => { - options.message = options.message.replace(`{${i}}`, arg); + options.message = options.message.replaceAll(`{${i}}`, arg); }); return options.message; } @@ -1312,7 +1312,6 @@ export enum FileSystemProviderErrorCode { * a file or folder doesn't exist, use them like so: `throw vscode.FileSystemError.FileNotFound(someUri);` */ export const { FileSystemError } = require("jest-mock-vscode").createVSCodeMock(jest); - /** * Namespace for dealing with the current workspace. A workspace is the representation * of the folder that has been opened. There is no workspace when just a file but not a diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts index ff18d56f18..e8c10345be 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetFSProvider.unit.test.ts @@ -703,7 +703,7 @@ describe("stat", () => { expect(lookupMock).toHaveBeenCalledWith(testUris.pdsMember, false); expect(lookupParentDirMock).toHaveBeenCalledWith(testUris.pdsMember); expect(allMembersMock).toHaveBeenCalledWith("USER.DATA.PDS", { attributes: true }); - expect(res).toStrictEqual({ ...fakePdsMember, mtime: dayjs("2024-08-08 12:30").unix() }); + expect(res).toStrictEqual({ ...fakePdsMember, mtime: dayjs("2024-08-08 12:30").valueOf() }); expect(fakePdsMember.wasAccessed).toBe(false); lookupMock.mockRestore(); lookupParentDirMock.mockRestore(); @@ -1126,6 +1126,7 @@ describe("rename", () => { .mockImplementation((uri): DirEntry | FileEntry => ((uri as Uri).path.includes("USER.DATA.PS2") ? (null as any) : oldPs)); const _lookupParentDirectoryMock = jest .spyOn(DatasetFSProvider.instance as any, "_lookupParentDirectory") + .mockReturnValueOnce({ ...testEntries.session }) .mockReturnValueOnce({ ...testEntries.session }); await DatasetFSProvider.instance.rename(testUris.ps, testUris.ps.with({ path: "/USER.DATA.PS2" }), { overwrite: true }); expect(mockMvsApi.renameDataSet).toHaveBeenCalledWith("USER.DATA.PS", "USER.DATA.PS2"); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetTree.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetTree.unit.test.ts index 6bb69c649b..25585eb78b 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetTree.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/DatasetTree.unit.test.ts @@ -54,6 +54,7 @@ import { Sorting } from "../../../../../zowe-explorer-api/src/tree"; import { IconUtils } from "../../../../src/icons/IconUtils"; import { SharedContext } from "../../../../src/trees/shared/SharedContext"; import { ZoweTreeProvider } from "../../../../src/trees/ZoweTreeProvider"; +import { TreeViewUtils } from "../../../../src/utils/TreeViewUtils"; jest.mock("fs"); jest.mock("util"); @@ -2280,6 +2281,31 @@ describe("Dataset Tree Unit Tests - Function rename", () => { }; } + it("returns early if errorForUnsavedResource was true", async () => { + createGlobalMocks(); + const blockMocks = createBlockMocks(); + mocked(Profiles.getInstance).mockReturnValue(blockMocks.profileInstance); + mocked(vscode.window.createTreeView).mockReturnValueOnce(blockMocks.treeView); + const testTree = new DatasetTree(); + testTree.mSessionNodes.push(blockMocks.datasetSessionNode); + const node = new ZoweDatasetNode({ + label: "HLQ.TEST.RENAME.NODE", + collapsibleState: vscode.TreeItemCollapsibleState.None, + parentNode: testTree.mSessionNodes[1], + session: blockMocks.session, + profile: testTree.mSessionNodes[1].getProfile(), + }); + blockMocks.rename.mockClear(); + const errorForUnsavedResource = jest.spyOn(TreeViewUtils, "errorForUnsavedResource").mockResolvedValueOnce(true); + await testTree.rename(node); + expect(errorForUnsavedResource).toHaveBeenCalled(); + expect(blockMocks.rename).not.toHaveBeenLastCalledWith( + { path: "/sestest/HLQ.TEST.RENAME.NODE", scheme: ZoweScheme.DS }, + { path: "/sestest/HLQ.TEST.RENAME.NODE.NEW", scheme: ZoweScheme.DS }, + { overwrite: false } + ); + }); + it("Tests that rename() renames a node", async () => { createGlobalMocks(); const blockMocks = createBlockMocks(); diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts index 8e5990c225..ca1003fb21 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/USSTree.unit.test.ts @@ -44,6 +44,8 @@ import { FilterDescriptor } from "../../../../src/management/FilterManagement"; import { AuthUtils } from "../../../../src/utils/AuthUtils"; import { Icon } from "../../../../src/icons/Icon"; import { ZoweTreeProvider } from "../../../../src/trees/ZoweTreeProvider"; +import { TreeViewUtils } from "../../../../src/utils/TreeViewUtils"; +import { SharedContext } from "../../../../src/trees/shared/SharedContext"; function createGlobalMocks() { const globalMocks = { @@ -934,6 +936,7 @@ describe("USSTree Unit Tests - Function rename", () => { globalMocks.FileSystemProvider.rename.mockClear(); const newMocks = { + errorForUnsavedResource: jest.spyOn(TreeViewUtils, "errorForUnsavedResource").mockResolvedValueOnce(false), ussFavNode, ussFavNodeParent, setAttributes: jest.spyOn(ZoweUSSNode.prototype, "setAttributes").mockImplementation(), @@ -948,6 +951,23 @@ describe("USSTree Unit Tests - Function rename", () => { getEncodingForFileMock.mockRestore(); }); + it("returns early if errorForUnsavedResource was true", async () => { + const globalMocks = createGlobalMocks(); + const blockMocks = createBlockMocks(globalMocks); + blockMocks.errorForUnsavedResource.mockReset(); + blockMocks.errorForUnsavedResource.mockResolvedValueOnce(true); + const testUSSDir = new ZoweUSSNode({ + label: "test", + collapsibleState: vscode.TreeItemCollapsibleState.Expanded, + session: globalMocks.testSession, + profile: globalMocks.testProfile, + parentPath: "/", + }); + const isFolderMock = jest.spyOn(SharedContext, "isFolder"); + await globalMocks.testTree.rename(testUSSDir); + expect(isFolderMock).not.toHaveBeenCalled(); + }); + it("Tests that USSTree.rename() shows no 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 = createGlobalMocks(); @@ -1233,7 +1253,6 @@ describe("USSTree Unit Tests - Function getChildren", () => { contextOverride: Constants.USS_SESSION_CONTEXT, session: globalMocks.testSession, profile: globalMocks.testProfile, - parentPath: "/", }), ]; diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts index 5cd2f0eaf3..741c053a92 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/uss/UssFSProvider.unit.test.ts @@ -87,6 +87,26 @@ describe("stat", () => { expect(listFilesMock).toHaveBeenCalled(); listFilesMock.mockRestore(); }); + + it("updates a file entry with new modification time and resets wasAccessed flag", async () => { + const fakeFile = Object.assign(Object.create(Object.getPrototypeOf(testEntries.file)), testEntries.file); + lookupMock.mockReturnValueOnce(fakeFile); + const newMtime = Date.now(); + const listFilesMock = jest.spyOn(UssFSProvider.instance, "listFiles").mockResolvedValueOnce({ + success: true, + apiResponse: { + items: [{ name: fakeFile.name, mtime: newMtime }], + }, + commandResponse: "", + }); + await expect(UssFSProvider.instance.stat(testUris.file)).resolves.toStrictEqual(fakeFile); + expect(lookupMock).toHaveBeenCalledWith(testUris.file, false); + expect(fakeFile.mtime).toBe(newMtime); + expect(fakeFile.wasAccessed).toBe(false); + expect(listFilesMock).toHaveBeenCalled(); + listFilesMock.mockRestore(); + }); + it("returns a file as 'read-only' when query has conflict parameter", async () => { lookupMock.mockReturnValueOnce(testEntries.file); await expect(UssFSProvider.instance.stat(testUris.conflictFile)).resolves.toStrictEqual({ diff --git a/packages/zowe-explorer/__tests__/__unit__/utils/TreeViewUtils.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/utils/TreeViewUtils.unit.test.ts index be8130c5d9..4a77bd81d9 100644 --- a/packages/zowe-explorer/__tests__/__unit__/utils/TreeViewUtils.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/utils/TreeViewUtils.unit.test.ts @@ -10,12 +10,16 @@ */ import * as vscode from "vscode"; -import { PersistenceSchemaEnum } from "@zowe/zowe-explorer-api"; +import { Gui, PersistenceSchemaEnum, ZoweScheme } from "@zowe/zowe-explorer-api"; import { createDatasetSessionNode, createDatasetTree } from "../../__mocks__/mockCreators/datasets"; import { createIProfile, createISession, createPersistentConfig, createTreeView } from "../../__mocks__/mockCreators/shared"; import { ZoweLocalStorage } from "../../../src/tools/ZoweLocalStorage"; import { TreeViewUtils } from "../../../src/utils/TreeViewUtils"; import { Constants } from "../../../src/configuration/Constants"; +import { ZoweUSSNode } from "../../../src/trees/uss/ZoweUSSNode"; +import { Profiles } from "../../../src/configuration/Profiles"; +import { createUSSSessionNode } from "../../__mocks__/mockCreators/uss"; +import { ZoweDatasetNode } from "../../../src/trees/dataset/ZoweDatasetNode"; describe("TreeViewUtils Unit Tests", () => { function createBlockMocks(): { [key: string]: any } { @@ -76,4 +80,192 @@ describe("TreeViewUtils Unit Tests", () => { await TreeViewUtils.removeSession(blockMocks.testDatasetTree, "SESTEST"); expect(blockMocks.testDatasetTree.removeFileHistory).toHaveBeenCalledTimes(1); }); + + describe("errorForUnsavedResource", () => { + function getBlockMocks() { + return { + errorMessage: jest.spyOn(Gui, "errorMessage").mockClear(), + profilesInstance: jest.spyOn(Profiles, "getInstance").mockReturnValue({ + checkCurrentProfile: jest.fn(), + } as any), + }; + } + it("prompts for an unsaved USS file", async () => { + const ussNode = new ZoweUSSNode({ + label: "testFile.txt", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.USS_TEXT_FILE_CONTEXT, + profile: createIProfile(), + parentNode: createUSSSessionNode(createISession(), createIProfile()), + }); + ussNode.resourceUri = vscode.Uri.from({ + path: "/sestest/testFile.txt", + scheme: ZoweScheme.USS, + }); + (ussNode.resourceUri as any).fsPath = ussNode.resourceUri.path; + const blockMocks = getBlockMocks(); + + const textDocumentsMock = jest.replaceProperty(vscode.workspace, "textDocuments", [ + { + fileName: ussNode.resourceUri?.fsPath as any, + uri: ussNode.resourceUri, + isDirty: true, + } as any, + ]); + + expect(await TreeViewUtils.errorForUnsavedResource(ussNode)).toBe(true); + expect(blockMocks.errorMessage).toHaveBeenCalledWith( + "Unable to rename testFile.txt because you have unsaved changes in this file. " + "Please save your work and try again.", + { vsCodeOpts: { modal: true } } + ); + textDocumentsMock.restore(); + }); + + it("prompts for an unsaved file in a USS directory", async () => { + const ussFolder = new ZoweUSSNode({ + label: "folder", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.USS_DIR_CONTEXT, + profile: createIProfile(), + parentNode: createUSSSessionNode(createISession(), createIProfile()), + }); + ussFolder.resourceUri = vscode.Uri.from({ + path: "/sestest/folder", + scheme: ZoweScheme.USS, + }); + (ussFolder.resourceUri as any).fsPath = ussFolder.resourceUri.path; + const blockMocks = getBlockMocks(); + + const ussNode = new ZoweUSSNode({ + label: "testFile.txt", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.USS_TEXT_FILE_CONTEXT, + profile: createIProfile(), + parentNode: ussFolder, + }); + ussNode.resourceUri = vscode.Uri.from({ + path: "/sestest/folder/testFile.txt", + scheme: ZoweScheme.USS, + }); + (ussNode.resourceUri as any).fsPath = ussNode.resourceUri.path; + + const textDocumentsMock = jest.replaceProperty(vscode.workspace, "textDocuments", [ + { + fileName: ussNode.resourceUri?.fsPath as any, + uri: ussNode.resourceUri, + isDirty: true, + } as any, + ]); + + expect(await TreeViewUtils.errorForUnsavedResource(ussFolder)).toBe(true); + expect(blockMocks.errorMessage).toHaveBeenCalledWith( + "Unable to rename folder because you have unsaved changes in this directory. " + "Please save your work and try again.", + { vsCodeOpts: { modal: true } } + ); + textDocumentsMock.restore(); + }); + + it("prompts for an unsaved data set", async () => { + const ps = new ZoweDatasetNode({ + label: "TEST.PS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_DS_CONTEXT, + profile: createIProfile(), + parentNode: createDatasetSessionNode(createISession(), createIProfile()), + }); + ps.resourceUri = vscode.Uri.from({ + path: "/sestest/TEST.PS", + scheme: ZoweScheme.DS, + }); + (ps.resourceUri as any).fsPath = ps.resourceUri.path; + const blockMocks = getBlockMocks(); + + const textDocumentsMock = jest.replaceProperty(vscode.workspace, "textDocuments", [ + { + fileName: ps.resourceUri?.fsPath as any, + uri: ps.resourceUri, + isDirty: true, + } as any, + ]); + + expect(await TreeViewUtils.errorForUnsavedResource(ps)).toBe(true); + expect(blockMocks.errorMessage).toHaveBeenCalledWith( + "Unable to rename TEST.PS because you have unsaved changes in this data set. " + "Please save your work and try again.", + { vsCodeOpts: { modal: true } } + ); + textDocumentsMock.restore(); + }); + + it("doesn't prompt if no resources are unsaved in editor", async () => { + const ps = new ZoweDatasetNode({ + label: "TEST.PS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_DS_CONTEXT, + profile: createIProfile(), + parentNode: createDatasetSessionNode(createISession(), createIProfile()), + }); + ps.resourceUri = vscode.Uri.from({ + path: "/sestest/TEST.PS", + scheme: ZoweScheme.DS, + }); + (ps.resourceUri as any).fsPath = ps.resourceUri.path; + const blockMocks = getBlockMocks(); + + const textDocumentsMock = jest.replaceProperty(vscode.workspace, "textDocuments", [ + { + fileName: ps.resourceUri?.fsPath as any, + uri: ps.resourceUri, + isDirty: false, + } as any, + ]); + + expect(await TreeViewUtils.errorForUnsavedResource(ps)).toBe(false); + expect(blockMocks.errorMessage).not.toHaveBeenCalled(); + textDocumentsMock.restore(); + }); + + it("prompts for an unsaved PDS member", async () => { + const pds = new ZoweDatasetNode({ + label: "TEST.PDS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_PDS_CONTEXT, + profile: createIProfile(), + parentNode: createDatasetSessionNode(createISession(), createIProfile()), + }); + pds.resourceUri = vscode.Uri.from({ + path: "/sestest/TEST.PDS", + scheme: ZoweScheme.DS, + }); + (pds.resourceUri as any).fsPath = pds.resourceUri.path; + + const pdsMember = new ZoweDatasetNode({ + label: "MEMB", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_MEMBER_CONTEXT, + profile: createIProfile(), + parentNode: createDatasetSessionNode(createISession(), createIProfile()), + }); + pdsMember.resourceUri = vscode.Uri.from({ + path: "/sestest/TEST.PDS/MEMB", + scheme: ZoweScheme.DS, + }); + (pdsMember.resourceUri as any).fsPath = pdsMember.resourceUri.path; + const blockMocks = getBlockMocks(); + + const textDocumentsMock = jest.replaceProperty(vscode.workspace, "textDocuments", [ + { + fileName: pdsMember.resourceUri?.fsPath as any, + uri: pdsMember.resourceUri, + isDirty: true, + } as any, + ]); + + expect(await TreeViewUtils.errorForUnsavedResource(pds)).toBe(true); + expect(blockMocks.errorMessage).toHaveBeenCalledWith( + "Unable to rename TEST.PDS because you have unsaved changes in this data set. " + "Please save your work and try again.", + { vsCodeOpts: { modal: true } } + ); + textDocumentsMock.restore(); + }); + }); }); diff --git a/packages/zowe-explorer/l10n/bundle.l10n.json b/packages/zowe-explorer/l10n/bundle.l10n.json index e5a1544003..754c59315a 100644 --- a/packages/zowe-explorer/l10n/bundle.l10n.json +++ b/packages/zowe-explorer/l10n/bundle.l10n.json @@ -48,6 +48,7 @@ "Submit": "Submit", "Cancel": "Cancel", "Troubleshoot Error": "Troubleshoot Error", + "rename": "rename", "Zowe Explorer profiles are being set as unsecured.": "Zowe Explorer profiles are being set as unsecured.", "Zowe Explorer profiles are being set as secured.": "Zowe Explorer profiles are being set as secured.", "Custom credential manager failed to activate": "Custom credential manager failed to activate", @@ -198,42 +199,6 @@ "Profile auth error": "Profile auth error", "Profile is not authenticated, please log in to continue": "Profile is not authenticated, please log in to continue", "Retrieving response from USS list API": "Retrieving response from USS list API", - "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", - "Failed to move {0}/File path": { - "message": "Failed to move {0}", - "comment": [ - "File path" - ] - }, - "Profile does not exist for this file.": "Profile does not exist for this file.", - "Saving USS file...": "Saving USS file...", - "Failed to rename {0}/File path": { - "message": "Failed to rename {0}", - "comment": [ - "File path" - ] - }, - "Failed to delete {0}/File name": { - "message": "Failed to delete {0}", - "comment": [ - "File name" - ] - }, - "No error details given": "No error details given", - "Error fetching destination {0} for paste action: {1}/USS pathError message": { - "message": "Error fetching destination {0} for paste action: {1}", - "comment": [ - "USS path", - "Error message" - ] - }, - "Failed to copy {0} to {1}/Source pathDestination path": { - "message": "Failed to copy {0} to {1}", - "comment": [ - "Source path", - "Destination path" - ] - }, "Downloaded: {0}/Download time": { "message": "Downloaded: {0}", "comment": [ @@ -252,13 +217,6 @@ "Confirm": "Confirm", "One or more items may be overwritten from this drop operation. Confirm or cancel?": "One or more items may be overwritten from this drop operation. Confirm or cancel?", "Moving USS files...": "Moving USS files...", - "Unable to rename {0} because you have unsaved changes in this {1}. Please save your work before renaming the {1}./Node pathNode type": { - "message": "Unable to rename {0} because you have unsaved changes in this {1}. Please save your work before renaming the {1}.", - "comment": [ - "Node path", - "Node type" - ] - }, "Enter a new name for the {0}/Node type": { "message": "Enter a new name for the {0}", "comment": [ @@ -304,6 +262,42 @@ "initializeUSSFavorites.error.buttonRemove": "initializeUSSFavorites.error.buttonRemove", "File does not exist. It may have been deleted.": "File does not exist. It may have been deleted.", "Pulling from Mainframe...": "Pulling from Mainframe...", + "The 'move' function is not implemented for this USS API.": "The 'move' function is not implemented for this USS API.", + "Failed to move {0}/File path": { + "message": "Failed to move {0}", + "comment": [ + "File path" + ] + }, + "Profile does not exist for this file.": "Profile does not exist for this file.", + "Saving USS file...": "Saving USS file...", + "Failed to rename {0}/File path": { + "message": "Failed to rename {0}", + "comment": [ + "File path" + ] + }, + "Failed to delete {0}/File name": { + "message": "Failed to delete {0}", + "comment": [ + "File name" + ] + }, + "No error details given": "No error details given", + "Error fetching destination {0} for paste action: {1}/USS pathError message": { + "message": "Error fetching destination {0} for paste action: {1}", + "comment": [ + "USS path", + "Error message" + ] + }, + "Failed to copy {0} to {1}/Source pathDestination path": { + "message": "Failed to copy {0} to {1}", + "comment": [ + "Source path", + "Destination path" + ] + }, "{0} location/Node type": { "message": "{0} location", "comment": [ diff --git a/packages/zowe-explorer/l10n/poeditor.json b/packages/zowe-explorer/l10n/poeditor.json index 70c3c1c8c9..fd9c48f5e5 100644 --- a/packages/zowe-explorer/l10n/poeditor.json +++ b/packages/zowe-explorer/l10n/poeditor.json @@ -465,6 +465,7 @@ "Submit": "", "Cancel": "", "Troubleshoot Error": "", + "rename": "", "Zowe Explorer profiles are being set as unsecured.": "", "Zowe Explorer profiles are being set as secured.": "", "Custom credential manager failed to activate": "", @@ -534,15 +535,6 @@ "Profile auth error": "", "Profile is not authenticated, please log in to continue": "", "Retrieving response from USS list API": "", - "The 'move' function is not implemented for this USS API.": "", - "Failed to move {0}": "", - "Profile does not exist for this file.": "", - "Saving USS file...": "", - "Failed to rename {0}": "", - "Failed to delete {0}": "", - "No error details given": "", - "Error fetching destination {0} for paste action: {1}": "", - "Failed to copy {0} to {1}": "", "Downloaded: {0}": "", "Encoding: {0}": "", "Binary": "", @@ -551,7 +543,6 @@ "Confirm": "", "One or more items may be overwritten from this drop operation. Confirm or cancel?": "", "Moving USS files...": "", - "Unable to rename {0} because you have unsaved changes in this {1}. Please save your work before renaming the {1}.": "", "Enter a new name for the {0}": "", "Unable to rename node:": "", "A {0} already exists with this name. Please choose a different name.": "", @@ -571,6 +562,15 @@ "initializeUSSFavorites.error.buttonRemove": "", "File does not exist. It may have been deleted.": "", "Pulling from Mainframe...": "", + "The 'move' function is not implemented for this USS API.": "", + "Failed to move {0}": "", + "Profile does not exist for this file.": "", + "Saving USS file...": "", + "Failed to rename {0}": "", + "Failed to delete {0}": "", + "No error details given": "", + "Error fetching destination {0} for paste action: {1}": "", + "Failed to copy {0} to {1}": "", "{0} location": "", "Choose a location to create the {0}": "", "Name of file or directory": "", diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts index c15105bb33..759eb43b72 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetFSProvider.ts @@ -114,12 +114,12 @@ export class DatasetFSProvider extends BaseProvider implements vscode.FileSystem const ds = isPdsMember ? items.find((it) => it.member === entry.name) : items?.[0]; if (ds != null && "m4date" in ds) { const { m4date, mtime, msec }: { m4date: string; mtime: string; msec: string } = ds; - const newTime = dayjs(`${m4date} ${mtime}:${msec}`).unix(); + const newTime = dayjs(`${m4date} ${mtime}:${msec}`).valueOf(); if (entry.mtime != newTime) { + entry.mtime = newTime; // if the modification time has changed, invalidate the previous contents to signal to `readFile` that data needs to be fetched entry.wasAccessed = false; } - return { ...entry, mtime: newTime }; } } diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts b/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts index 4fac3af363..ee4cd7960f 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetTree.ts @@ -98,6 +98,10 @@ export class DatasetTree extends ZoweTreeProvider implemen public async rename(node: IZoweDatasetTreeNode): Promise { ZoweLogger.trace("DatasetTree.rename called."); await Profiles.getInstance().checkCurrentProfile(node.getProfile()); + if (await TreeViewUtils.errorForUnsavedResource(node)) { + return; + } + if (Profiles.getInstance().validProfile === Validation.ValidationType.VALID || !SharedContext.isValidationEnabled(node)) { return SharedContext.isDsMember(node) ? this.renameDataSetMember(node) : this.renameDataSet(node); } diff --git a/packages/zowe-explorer/src/trees/uss/USSTree.ts b/packages/zowe-explorer/src/trees/uss/USSTree.ts index 1544ffcbac..21606297e6 100644 --- a/packages/zowe-explorer/src/trees/uss/USSTree.ts +++ b/packages/zowe-explorer/src/trees/uss/USSTree.ts @@ -252,29 +252,14 @@ export class USSTree extends ZoweTreeProvider implements Types */ public async rename(originalNode: IZoweUSSTreeNode): Promise { ZoweLogger.trace("USSTree.rename called."); - const currentFilePath = originalNode.resourceUri.path; // The user's complete local file path for the node - const openedTextDocuments: readonly vscode.TextDocument[] = vscode.workspace.textDocuments; // Array of all documents open in VS Code + await Profiles.getInstance().checkCurrentProfile(originalNode.getProfile()); + if (await TreeViewUtils.errorForUnsavedResource(originalNode)) { + return; + } + const nodeType = SharedContext.isFolder(originalNode) ? "folder" : "file"; const parentPath = path.dirname(originalNode.fullPath); - // If the USS node or any of its children are locally open with unsaved data, prevent rename until user saves their work. - for (const doc of openedTextDocuments) { - const docIsChild = SharedUtils.checkIfChildPath(currentFilePath, doc.fileName); - if (doc.fileName === currentFilePath || docIsChild === true) { - if (doc.isDirty === true) { - Gui.errorMessage( - vscode.l10n.t({ - message: - "Unable to rename {0} because you have unsaved changes in this {1}. Please save your work before renaming the {1}.", - args: [originalNode.fullPath, nodeType], - comment: ["Node path", "Node type"], - }), - { vsCodeOpts: { modal: true } } - ); - return; - } - } - } const loadedNodes = await this.getAllLoadedItems(); const options: vscode.InputBoxOptions = { prompt: vscode.l10n.t({ diff --git a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts index e1a917d71e..087316acf7 100644 --- a/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts +++ b/packages/zowe-explorer/src/trees/uss/UssFSProvider.ts @@ -91,15 +91,10 @@ export class UssFSProvider extends BaseProvider implements vscode.FileSystemProv const newTime = (fileResp.apiResponse?.items ?? [])?.[0]?.mtime ?? entry.mtime; if (entry.mtime != newTime) { + entry.mtime = newTime; // if the modification time has changed, invalidate the previous contents to signal to `readFile` that data needs to be fetched entry.wasAccessed = false; } - return { - ...entry, - // If there isn't a valid mtime on the API response, we cannot determine whether the resource has been updated. - // Use the last-known modification time to prevent superfluous updates. - mtime: newTime, - }; } return entry; diff --git a/packages/zowe-explorer/src/utils/TreeViewUtils.ts b/packages/zowe-explorer/src/utils/TreeViewUtils.ts index c6814ae79b..038b8d566e 100644 --- a/packages/zowe-explorer/src/utils/TreeViewUtils.ts +++ b/packages/zowe-explorer/src/utils/TreeViewUtils.ts @@ -9,13 +9,15 @@ * */ -import { Types, IZoweTree, IZoweTreeNode, PersistenceSchemaEnum } from "@zowe/zowe-explorer-api"; -import { TreeViewExpansionEvent } from "vscode"; +import { Types, IZoweTree, IZoweTreeNode, PersistenceSchemaEnum, Gui } from "@zowe/zowe-explorer-api"; +import { l10n, TextDocument, TreeViewExpansionEvent, workspace } from "vscode"; import { IconGenerator } from "../icons/IconGenerator"; import type { ZoweTreeProvider } from "../trees/ZoweTreeProvider"; import { ZoweLocalStorage } from "../tools/ZoweLocalStorage"; import { ZoweLogger } from "../tools/ZoweLogger"; import { Profiles } from "../configuration/Profiles"; +import { SharedUtils } from "../trees/shared/SharedUtils"; +import { SharedContext } from "../trees/shared/SharedContext"; export class TreeViewUtils { /** @@ -107,4 +109,43 @@ export class TreeViewUtils { } } } + + /** + * Prompts the user when a file/data set is unsaved in the editor. + * @param node The USS file or data set to check for in the editor. Also checks child paths for the node (for PDS members and inner USS files). + * @returns Whether a child or the resource itself is open with unsaved changes in the editor + */ + public static async errorForUnsavedResource(node: IZoweTreeNode, action = l10n.t("rename")): Promise { + const currentFilePath = node.resourceUri.fsPath; // The user's complete local file path for the node + await Profiles.getInstance().checkCurrentProfile(node.getProfile()); + const openedTextDocuments: readonly TextDocument[] = workspace.textDocuments; // Array of all documents open in VS Code + + const isUss = SharedContext.isUssNode(node); + let nodeType: string; + if (isUss) { + nodeType = SharedContext.isUssDirectory(node) ? "directory" : "file"; + } else { + nodeType = "data set"; + } + + for (const doc of openedTextDocuments) { + if ((doc.fileName === currentFilePath || SharedUtils.checkIfChildPath(currentFilePath, doc.fileName)) && doc.isDirty) { + ZoweLogger.error( + `TreeViewUtils.errorForUnsavedResource: detected unsaved changes in ${doc.fileName},` + + `trying to ${action} node: ${node.label as string}` + ); + Gui.errorMessage( + l10n.t({ + message: "Unable to {0} {1} because you have unsaved changes in this {2}. " + "Please save your work and try again.", + args: [action, node.label, nodeType], + comment: ["User action", "Node path", "Node type (directory, file or data set)"], + }), + { vsCodeOpts: { modal: true } } + ); + return true; + } + } + + return false; + } }