Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(v2): Show error for unsaved data set rename #3328

Merged
merged 7 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"devDependencies": {
"@babel/core": "^7.24.9",
"@babel/preset-env": "^7.24.8",
"@types/jest": "^29.2.3",
"@types/jest": "^29.4.0",
"@types/mocha": "^10.0.1",
"@types/node": "^14.18.12",
"@types/vscode": "^1.53.2",
Expand All @@ -33,7 +33,7 @@
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^6.0.0",
"jest": "^29.3.1",
"jest": "^29.4.0",
"jest-html-reporter": "^3.10.2",
"jest-junit": "^15.0.0",
"jest-stare": "^2.5.2",
Expand Down
1 change: 1 addition & 0 deletions packages/zowe-explorer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen
- Fixed an issue where the location prompt for the `Create Directory` and `Create File` USS features would appear even when a path is already set for the profile or parent folder. [#3183](https://github.com/zowe/zowe-explorer-vscode/pull/3183)
- Fixed an issue where the `Create Directory` and `Create File` features would continue processing when the first prompt was dismissed, causing an incorrect path to be generated. [#3183](https://github.com/zowe/zowe-explorer-vscode/pull/3183)
- Fixed an issue where the `Create Directory` and `Create File` features would incorrectly handle user-specified locations with trailing slashes. [#3183](https://github.com/zowe/zowe-explorer-vscode/pull/3183)
- 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. [#3328](https://github.com/zowe/zowe-explorer-vscode/pull/3328)

## `2.18.0`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { join } from "path";
import { mocked } from "../../../__mocks__/mockUtils";
import * as sharedUtils from "../../../src/shared/utils";
import { LocalFileManagement } from "../../../src/utils/LocalFileManagement";
import { TreeViewUtils } from "../../../src/utils/TreeViewUtils";

jest.mock("fs");
jest.mock("util");
Expand Down Expand Up @@ -2456,6 +2457,27 @@ describe("Dataset Tree Unit Tests - Function rename", () => {
expect(renameDataSetSpy).toHaveBeenLastCalledWith("HLQ.TEST.RENAME.NODE", "HLQ.TEST.RENAME.NODE.NEW");
});

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(),
});
const renameDataSet = jest.spyOn(testTree as any, "renameDataSet");
const promptedForUnsavedResource = jest.spyOn(TreeViewUtils, "errorForUnsavedResource").mockResolvedValueOnce(true);
await testTree.rename(node);
expect(promptedForUnsavedResource).toHaveBeenCalled();
expect(renameDataSet).not.toHaveBeenCalled();
});

it("Checking function with PS Dataset using Unverified profile", async () => {
globals.defineGlobals("");
createGlobalMocks();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@

import { TreeViewUtils } from "../../../src/utils/TreeViewUtils";
import * as globals from "../../../src/globals";
import * as vscode from "vscode";
import { Gui } from "@zowe/zowe-explorer-api";
import { Profiles } from "../../../src/Profiles";
import { ZoweUSSNode } from "../../../src/uss/ZoweUSSNode";
import { createIProfile, createISession } from "../../../__mocks__/mockCreators/shared";
import { createUSSSessionNode } from "../../../__mocks__/mockCreators/uss";
import { createDatasetSessionNode } from "../../../__mocks__/mockCreators/datasets";
import { ZoweDatasetNode } from "../../../src/dataset/ZoweDatasetNode";

describe("TreeViewUtils Unit Tests", () => {
it("refreshIconOnCollapse - generated listener function works as intended", () => {
Expand All @@ -23,4 +31,155 @@ describe("TreeViewUtils Unit Tests", () => {
listenerFn({ element });
expect(testTreeProvider.mOnDidChangeTreeData.fire).toHaveBeenCalledWith(element);
});

describe("errorForUnsavedResource", () => {
function getBlockMocks(): Record<string, jest.SpyInstance> {
return {
errorMessage: jest.spyOn(Gui, "errorMessage").mockClear(),
profilesInstance: jest.spyOn(Profiles, "getInstance").mockReturnValue({
loadNamedProfile: jest.fn().mockReturnValue(createIProfile()),
checkCurrentProfile: jest.fn(),
} as any),
};
}
it("prompts for an unsaved USS file", async () => {
const ussNode = new ZoweUSSNode({
label: "testFile.txt",
collapsibleState: vscode.TreeItemCollapsibleState.None,
profile: createIProfile(),
parentNode: createUSSSessionNode(createISession(), createIProfile()),
});
const blockMocks = getBlockMocks();

const textDocumentsMock = jest.replaceProperty(vscode.workspace, "textDocuments", [
{
fileName: ussNode.getUSSDocumentFilePath() as any,
uri: vscode.Uri.file(ussNode.getUSSDocumentFilePath()),
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.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an extra space between the period and the quotation marks in the first message? Not sure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No extra spaces here - the text was simply split into two sections to avoid the "max length" limit that we have set for lines of code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The + sign seems unnecessary here since the text isn't split across multiple lines:

Suggested change
"Unable to rename testFile.txt because you have unsaved changes in this file. " + "Please save your work and try again.",
"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.Collapsed,
profile: createIProfile(),
parentNode: createUSSSessionNode(createISession(), createIProfile()),
});
const blockMocks = getBlockMocks();

const ussNode = new ZoweUSSNode({
label: "testFile.txt",
collapsibleState: vscode.TreeItemCollapsibleState.None,
profile: createIProfile(),
parentNode: ussFolder,
});

const textDocumentsMock = jest.replaceProperty(vscode.workspace, "textDocuments", [
{
fileName: ussNode.getUSSDocumentFilePath(),
uri: vscode.Uri.file(ussNode.getUSSDocumentFilePath()),
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.",
traeok marked this conversation as resolved.
Show resolved Hide resolved
{ 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: globals.DS_DS_CONTEXT,
profile: createIProfile(),
parentNode: createDatasetSessionNode(createISession(), createIProfile()),
});
const blockMocks = getBlockMocks();

const textDocumentsMock = jest.replaceProperty(vscode.workspace, "textDocuments", [
{
fileName: ps.getDsDocumentFilePath(),
uri: vscode.Uri.file(ps.getDsDocumentFilePath()),
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.",
traeok marked this conversation as resolved.
Show resolved Hide resolved
{ 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: globals.DS_DS_CONTEXT,
profile: createIProfile(),
parentNode: createDatasetSessionNode(createISession(), createIProfile()),
});
const blockMocks = getBlockMocks();

const textDocumentsMock = jest.replaceProperty(vscode.workspace, "textDocuments", [
{
fileName: ps.getDsDocumentFilePath(),
uri: vscode.Uri.file(ps.getDsDocumentFilePath()),
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.JCL",
collapsibleState: vscode.TreeItemCollapsibleState.None,
contextOverride: globals.DS_PDS_CONTEXT,
profile: createIProfile(),
parentNode: createDatasetSessionNode(createISession(), createIProfile()),
});

const pdsMember = new ZoweDatasetNode({
label: "MEMB",
collapsibleState: vscode.TreeItemCollapsibleState.None,
contextOverride: globals.DS_MEMBER_CONTEXT,
profile: createIProfile(),
parentNode: pds,
});
const blockMocks = getBlockMocks();

const textDocumentsMock = jest.replaceProperty(vscode.workspace, "textDocuments", [
{
fileName: pdsMember.getDsDocumentFilePath(),
uri: vscode.Uri.file(pdsMember.getDsDocumentFilePath()),
isDirty: true,
} as any,
]);

expect(await TreeViewUtils.errorForUnsavedResource(pds)).toBe(true);
expect(blockMocks.errorMessage).toHaveBeenCalledWith(
"Unable to rename TEST.PDS.JCL because you have unsaved changes in this data set. " + "Please save your work and try again.",
traeok marked this conversation as resolved.
Show resolved Hide resolved
{ vsCodeOpts: { modal: true } }
);
textDocumentsMock.restore();
});
});
});
4 changes: 4 additions & 0 deletions packages/zowe-explorer/src/dataset/DatasetTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@
public async rename(node: IZoweDatasetTreeNode): Promise<void> {
ZoweLogger.trace("DatasetTree.rename called.");
await Profiles.getInstance().checkCurrentProfile(node.getProfile());
if (await TreeViewUtils.errorForUnsavedResource(node)) {
return;
}

if (Profiles.getInstance().validProfile === ValidProfileEnum.VALID || !contextually.isValidationEnabled(node)) {
return contextually.isDsMember(node) ? this.renameDataSetMember(node) : this.renameDataSet(node);
}
Expand Down Expand Up @@ -927,7 +931,7 @@
return loadedItems;
}

public async datasetFilterPrompt(node: IZoweDatasetTreeNode): Promise<void> {

Check warning on line 934 in packages/zowe-explorer/src/dataset/DatasetTree.ts

View workflow job for this annotation

GitHub Actions / lint

Async method 'datasetFilterPrompt' has a complexity of 42. Maximum allowed is 15
ZoweLogger.trace("DatasetTree.datasetFilterPrompt called.");
ZoweLogger.debug(localize("enterPattern.log.debug.prompt", "Prompting the user for a data set pattern"));
let pattern: string;
Expand Down Expand Up @@ -1134,7 +1138,7 @@
await TreeViewUtils.expandNode(nonFaveNode, this);
}

public checkFilterPattern(dsName: string, itemName: string): boolean {

Check warning on line 1141 in packages/zowe-explorer/src/dataset/DatasetTree.ts

View workflow job for this annotation

GitHub Actions / lint

Method 'checkFilterPattern' has a complexity of 19. Maximum allowed is 15
ZoweLogger.trace("DatasetTree.checkFilterPattern called.");
let existing: boolean;
if (!/(\*?)(\w+)(\*)(\w+)(\*?)/.test(itemName)) {
Expand Down
21 changes: 2 additions & 19 deletions packages/zowe-explorer/src/uss/USSTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,6 @@ export class USSTree extends ZoweTreeProvider implements IZoweTree<IZoweUSSTreeN
*/
public async rename(originalNode: IZoweUSSTreeNode): Promise<void> {
ZoweLogger.trace("USSTree.rename called.");
const currentFilePath = originalNode.getUSSDocumentFilePath(); // 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
const nodeType = contextually.isFolder(originalNode) ? "folder" : "file";
const parentPath = path.dirname(originalNode.fullPath);
let originalNodeInFavorites: boolean = false;
Expand All @@ -129,23 +127,8 @@ export class USSTree extends ZoweTreeProvider implements IZoweTree<IZoweUSSTreeN
oldFavorite = this.findFavoritedNode(originalNode);
}

// 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 = checkIfChildPath(currentFilePath, doc.fileName);
if (doc.fileName === currentFilePath || docIsChild === true) {
if (doc.isDirty === true) {
Gui.errorMessage(
localize(
"renameUSS.unsavedWork",
"Unable to rename {0} because you have unsaved changes in this {1}. Please save your work before renaming the {1}.",
originalNode.fullPath,
nodeType
),
{ vsCodeOpts: { modal: true } }
);
return;
}
}
if (await TreeViewUtils.errorForUnsavedResource(originalNode)) {
return;
}
const loadedNodes = await this.getAllLoadedItems();
const options: vscode.InputBoxOptions = {
Expand Down
67 changes: 65 additions & 2 deletions packages/zowe-explorer/src/utils/TreeViewUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,22 @@
*
*/

import { IZoweNodeType, IZoweTree, IZoweTreeNode } from "@zowe/zowe-explorer-api";
import { Gui, IZoweDatasetTreeNode, IZoweNodeType, IZoweTree, IZoweTreeNode, IZoweUSSTreeNode } from "@zowe/zowe-explorer-api";
import { ZoweLogger } from "./LoggerUtils";
import { TreeViewExpansionEvent } from "vscode";
import { TextDocument, TreeViewExpansionEvent, workspace } from "vscode";
import { getIconByNode } from "../generators/icons";
import { ZoweTreeProvider } from "../abstract/ZoweTreeProvider";
import { Profiles } from "../Profiles";
import { checkIfChildPath, isZoweDatasetTreeNode } from "../shared/utils";
import * as path from "path";

import * as nls from "vscode-nls";
import { isUssDirectory } from "../shared/context";
nls.config({
messageFormat: nls.MessageFormat.bundle,
bundleFormat: nls.BundleFormat.standalone,
})();
const localize: nls.LocalizeFunc = nls.loadMessageBundle();

export class TreeViewUtils {
/**
Expand Down Expand Up @@ -64,4 +74,57 @@ export class TreeViewUtils {
}
}
}

/**
* Shows a modal error message to the user when a file/data set is detected as 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: IZoweDatasetTreeNode | IZoweUSSTreeNode,
action: string = localize("uss.renameNode", "Rename").toLocaleLowerCase()
): Promise<boolean> {
const isDataset = isZoweDatasetTreeNode(node);
// The user's complete local file path for the node
const currentFilePath = isDataset ? node.getDsDocumentFilePath() : node.getUSSDocumentFilePath();
await Profiles.getInstance().checkCurrentProfile(node.getProfile());
const openedTextDocuments: readonly TextDocument[] = workspace.textDocuments; // Array of all documents open in VS Code

let nodeType: string;
if (isDataset) {
nodeType = "data set";
} else {
nodeType = isUssDirectory(node) ? "directory" : "file";
}

for (const doc of openedTextDocuments) {
if (
(doc.fileName === currentFilePath ||
// check for USS child paths
checkIfChildPath(currentFilePath, doc.fileName) ||
// check if doc is a PDS member - basename starts with `${PDS name}(`
(path.dirname(doc.fileName) === path.dirname(currentFilePath) &&
path.basename(doc.fileName).startsWith(`${node.label as string}(`))) &&
doc.isDirty
) {
ZoweLogger.error(
`TreeViewUtils.errorForUnsavedResource: detected unsaved changes in ${doc.fileName},` +
`trying to ${action} node: ${node.label as string}`
);
Gui.errorMessage(
localize(
"unsavedChanges.errorMsg",
"Unable to {0} {1} because you have unsaved changes in this {2}. Please save your work and try again.",
action,
node.label as string,
nodeType
),
{ vsCodeOpts: { modal: true } }
);
return true;
}
}

return false;
}
}
Loading
Loading