diff --git a/packages/zowe-explorer/CHANGELOG.md b/packages/zowe-explorer/CHANGELOG.md index 2cc2dbc2cc..145e16f974 100644 --- a/packages/zowe-explorer/CHANGELOG.md +++ b/packages/zowe-explorer/CHANGELOG.md @@ -24,6 +24,9 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen - 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) +- Fixed an issue where a migrated data set is unusable after it is recalled through Zowe Explorer. [#3294](https://github.com/zowe/zowe-explorer-vscode/issues/3294) +- Fixed an issue where a recalled PDS is expandable after it is migrated through Zowe Explorer. [#3294](https://github.com/zowe/zowe-explorer-vscode/issues/3294) +- Fixed an issue where data set nodes did not update if migrated or recalled outside of Zowe Explorer. [#3294](https://github.com/zowe/zowe-explorer-vscode/issues/3294) ## `3.0.3` diff --git a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts index 0dde39981a..afcd8e8594 100644 --- a/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/trees/dataset/ZoweDatasetNode.unit.test.ts @@ -10,7 +10,7 @@ */ import * as vscode from "vscode"; -import { DsEntry, Gui, imperative, PdsEntry, Validation } from "@zowe/zowe-explorer-api"; +import { DsEntry, Gui, imperative, PdsEntry, Validation, ZoweScheme } from "@zowe/zowe-explorer-api"; import * as zosfiles from "@zowe/zos-files-for-zowe-sdk"; import { createSessCfgFromArgs, @@ -28,6 +28,8 @@ import { Profiles } from "../../../../src/configuration/Profiles"; import { ZoweLogger } from "../../../../src/tools/ZoweLogger"; import { DatasetFSProvider } from "../../../../src/trees/dataset/DatasetFSProvider"; import { ZoweDatasetNode } from "../../../../src/trees/dataset/ZoweDatasetNode"; +import { IconUtils } from "../../../../src/icons/IconUtils"; +import { IconGenerator } from "../../../../src/icons/IconGenerator"; // Missing the definition of path module, because I need the original logic for tests jest.mock("fs"); @@ -751,3 +753,148 @@ describe("ZoweDatasetNode Unit Tests - Function node.setStats", () => { createDirMock.mockRestore(); }); }); + +describe("ZoweDatasetNode Unit Tests - function datasetRecalled", () => { + it("changes the collapsible state", async () => { + const dsNode = new ZoweDatasetNode({ + label: "MIGRATED.PDS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_MIGRATED_FILE_CONTEXT, + profile: createIProfile(), + }); + await (dsNode as any).datasetRecalled(true); + expect(dsNode.collapsibleState).toBe(vscode.TreeItemCollapsibleState.Collapsed); + }); + + it("adds a resource URI", async () => { + const dsNode = new ZoweDatasetNode({ + label: "MIGRATED.PDS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_MIGRATED_FILE_CONTEXT, + parentNode: createDatasetSessionNode(createISession(), createIProfile()), + profile: createIProfile(), + }); + await (dsNode as any).datasetRecalled(true); + expect(dsNode.resourceUri).toStrictEqual( + vscode.Uri.from({ + scheme: ZoweScheme.DS, + path: "/sestest/MIGRATED.PDS", + }) + ); + }); + + it("adds a command to the node - PS", async () => { + const dsNode = new ZoweDatasetNode({ + label: "MIGRATED.PS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_MIGRATED_FILE_CONTEXT, + parentNode: createDatasetSessionNode(createISession(), createIProfile()), + profile: createIProfile(), + }); + await (dsNode as any).datasetRecalled(false); + expect(dsNode.resourceUri).toStrictEqual( + vscode.Uri.from({ + scheme: ZoweScheme.DS, + path: "/sestest/MIGRATED.PS", + }) + ); + expect(dsNode.command).toStrictEqual({ command: "vscode.open", title: "", arguments: [dsNode.resourceUri] }); + }); + + it("creates a file system entry - PDS", async () => { + const dsNode = new ZoweDatasetNode({ + label: "MIGRATED.PDS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_MIGRATED_FILE_CONTEXT, + profile: createIProfile(), + }); + const createDirMock = jest.spyOn(vscode.workspace.fs, "createDirectory").mockImplementation(); + await (dsNode as any).datasetRecalled(true); + expect(createDirMock).toHaveBeenCalledWith(dsNode.resourceUri); + }); + + it("creates a file system entry - PS", async () => { + const dsNode = new ZoweDatasetNode({ + label: "MIGRATED.PS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_MIGRATED_FILE_CONTEXT, + parentNode: createDatasetSessionNode(createISession(), createIProfile()), + profile: createIProfile(), + }); + const writeFileMock = jest.spyOn(vscode.workspace.fs, "writeFile").mockImplementation(); + await (dsNode as any).datasetRecalled(false); + expect(writeFileMock).toHaveBeenCalledWith(dsNode.resourceUri, new Uint8Array()); + }); + + it("updates the icon to folder - PDS", async () => { + const dsNode = new ZoweDatasetNode({ + label: "MIGRATED.PDS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_MIGRATED_FILE_CONTEXT, + profile: createIProfile(), + }); + await (dsNode as any).datasetRecalled(true); + expect(dsNode.iconPath).toBe(IconGenerator.getIconById(IconUtils.IconId.folder).path); + }); + + it("updates the icon to file - PS", async () => { + const dsNode = new ZoweDatasetNode({ + label: "MIGRATED.PS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_MIGRATED_FILE_CONTEXT, + profile: createIProfile(), + }); + await (dsNode as any).datasetRecalled(false); + expect(dsNode.iconPath).toBe(IconGenerator.getIconById(IconUtils.IconId.document).path); + }); +}); + +describe("ZoweDatasetNode Unit Tests - function datasetMigrated", () => { + it("changes the collapsible state", () => { + const dsNode = new ZoweDatasetNode({ + label: "SOME.PDS", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextOverride: Constants.DS_PDS_CONTEXT, + profile: createIProfile(), + }); + dsNode.datasetMigrated(); + expect(dsNode.collapsibleState).toBe(vscode.TreeItemCollapsibleState.None); + }); + + it("removes the resource URI and command", () => { + const dsNode = new ZoweDatasetNode({ + label: "SOME.PDS", + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextOverride: Constants.DS_PDS_CONTEXT, + parentNode: createDatasetSessionNode(createISession(), createIProfile()), + profile: createIProfile(), + }); + dsNode.datasetMigrated(); + expect(dsNode.resourceUri).toBeUndefined(); + expect(dsNode.command).toBeUndefined(); + }); + + it("removes the file system entry", () => { + const dsNode = new ZoweDatasetNode({ + label: "MIGRATED.PDS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_MIGRATED_FILE_CONTEXT, + profile: createIProfile(), + }); + const uri = dsNode.resourceUri; + const removeEntryMock = jest.spyOn(DatasetFSProvider.instance, "removeEntry").mockImplementation(); + dsNode.datasetMigrated(); + expect(removeEntryMock).toHaveBeenCalledWith(uri); + }); + + it("changes the icon to the migrated icon", () => { + const dsNode = new ZoweDatasetNode({ + label: "MIGRATED.PDS", + collapsibleState: vscode.TreeItemCollapsibleState.None, + contextOverride: Constants.DS_MIGRATED_FILE_CONTEXT, + profile: createIProfile(), + }); + dsNode.datasetMigrated(); + expect(dsNode.iconPath).toBe(IconGenerator.getIconById(IconUtils.IconId.migrated).path); + }); +}); diff --git a/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts b/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts index c3f315b776..3680a4c5cb 100644 --- a/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts +++ b/packages/zowe-explorer/src/trees/dataset/DatasetActions.ts @@ -1371,7 +1371,6 @@ export class DatasetActions { if (Profiles.getInstance().validProfile !== Validation.ValidationType.INVALID) { const { dataSetName } = DatasetUtils.getNodeLabels(node); try { - const response = await ZoweExplorerApiRegister.getMvsApi(node.getProfile()).hMigrateDataSet(dataSetName); Gui.showMessage( vscode.l10n.t({ message: "Migration of data set {0} requested.", @@ -1379,9 +1378,8 @@ export class DatasetActions { comment: ["Data Set name"], }) ); - node.contextValue = Constants.DS_MIGRATED_FILE_CONTEXT; - node.setIcon(IconGenerator.getIconByNode(node).path); - datasetProvider.refresh(); + const response = await ZoweExplorerApiRegister.getMvsApi(node.getProfile()).hMigrateDataSet(dataSetName); + datasetProvider.refreshElement(node.getParent()); return response; } catch (err) { ZoweLogger.error(err); @@ -1405,7 +1403,6 @@ export class DatasetActions { if (Profiles.getInstance().validProfile !== Validation.ValidationType.INVALID) { const { dataSetName } = DatasetUtils.getNodeLabels(node); try { - const response = await ZoweExplorerApiRegister.getMvsApi(node.getProfile()).hRecallDataSet(dataSetName); Gui.showMessage( vscode.l10n.t({ message: "Recall of data set {0} requested.", @@ -1413,13 +1410,8 @@ export class DatasetActions { comment: ["Data Set name"], }) ); - if (node.collapsibleState !== vscode.TreeItemCollapsibleState.None) { - node.contextValue = Constants.DS_PDS_CONTEXT; - } else { - node.contextValue = (await node.getEncoding())?.kind === "binary" ? Constants.DS_DS_BINARY_CONTEXT : Constants.DS_DS_CONTEXT; - } - node.setIcon(IconGenerator.getIconByNode(node).path); - datasetProvider.refresh(); + const response = await ZoweExplorerApiRegister.getMvsApi(node.getProfile()).hRecallDataSet(dataSetName); + datasetProvider.refreshElement(node.getParent()); return response; } catch (err) { ZoweLogger.error(err); diff --git a/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts b/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts index ffb41db3c2..70a47b01dc 100644 --- a/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts +++ b/packages/zowe-explorer/src/trees/dataset/ZoweDatasetNode.ts @@ -68,6 +68,7 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod * * @param {IZoweTreeOpts} opts */ + public constructor(opts: Definitions.IZoweDatasetTreeOpts) { super(opts.label, opts.collapsibleState, opts.parentNode, opts.session, opts.profile); if (opts.encoding != null) { @@ -101,13 +102,9 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod this.id = this.label as string; } - if (this.label !== vscode.l10n.t("Favorites")) { + if (this.label !== vscode.l10n.t("Favorites") && this.contextValue !== Constants.DS_MIGRATED_FILE_CONTEXT) { const sessionLabel = opts.profile?.name ?? SharedUtils.getSessionLabel(this); - if ( - this.contextValue === Constants.DS_DS_CONTEXT || - this.contextValue === Constants.DS_PDS_CONTEXT || - this.contextValue === Constants.DS_MIGRATED_FILE_CONTEXT - ) { + if (this.contextValue === Constants.DS_DS_CONTEXT || this.contextValue === Constants.DS_PDS_CONTEXT) { this.resourceUri = vscode.Uri.from({ scheme: ZoweScheme.DS, path: `/${sessionLabel}/${this.label as string}`, @@ -141,7 +138,6 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod } } } - public updateStats(item: any): void { if ("c4date" in item && "m4date" in item) { const { m4date, mtime, msec }: { m4date: string; mtime: string; msec: string } = item; @@ -194,6 +190,65 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod return dsEntry.stats; } + /** + * Updates this node so the recalled data set can be interacted with. + * @param isPds Whether the data set is a PDS + */ + private async datasetRecalled(isPds: boolean): Promise { + // Change context value to match dsorg, update collapsible state and assign resource URI + // Preserve favorite context and any additional context values + this.contextValue = this.contextValue.replace(Constants.DS_MIGRATED_FILE_CONTEXT, isPds ? Constants.DS_PDS_CONTEXT : Constants.DS_DS_CONTEXT); + this.collapsibleState = isPds ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None; + this.resourceUri = vscode.Uri.from({ + scheme: ZoweScheme.DS, + path: `/${SharedUtils.getSessionLabel(this)}/${this.label as string}`, + }); + + // Replace icon on existing node with new one + const icon = IconGenerator.getIconByNode(this); + if (icon) { + this.setIcon(icon.path); + } + + // Create entry in the filesystem to represent the recalled data set + if (isPds) { + await vscode.workspace.fs.createDirectory(this.resourceUri); + } else { + this.command = { command: "vscode.open", title: "", arguments: [this.resourceUri] }; + if (!DatasetFSProvider.instance.exists(this.resourceUri)) { + await vscode.workspace.fs.writeFile(this.resourceUri, new Uint8Array()); + } + } + } + + /** + * Updates this data set node so it is marked as migrated. + */ + public datasetMigrated(): void { + // Change the context value and collapsible state to represent a migrated data set + // Preserve favorite context and any additional context values + const isBinary = SharedContext.isBinary(this); + const isPds = this.collapsibleState !== vscode.TreeItemCollapsibleState.None; + let previousContext = isBinary ? Constants.DS_DS_BINARY_CONTEXT : Constants.DS_DS_CONTEXT; + if (isPds) { + previousContext = Constants.DS_PDS_CONTEXT; + } + this.contextValue = this.contextValue.replace(previousContext, Constants.DS_MIGRATED_FILE_CONTEXT); + this.collapsibleState = vscode.TreeItemCollapsibleState.None; + + // Remove the entry from the file system + DatasetFSProvider.instance.removeEntry(this.resourceUri); + + // Remove the node's resource URI and command + this.resourceUri = this.command = undefined; + + // Assign migrated icon to the data set node + const icon = IconGenerator.getIconByNode(this); + if (icon) { + this.setIcon(icon.path); + } + } + /** * Retrieves child nodes of this ZoweDatasetNode * @@ -253,7 +308,22 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod let dsNode = existingItems[item.dsname ?? item.member]; if (dsNode != null) { elementChildren[dsNode.label.toString()] = dsNode; + if (SharedContext.isMigrated(dsNode) && item.migr?.toUpperCase() !== "YES") { + await dsNode.datasetRecalled(item.dsorg === "PO" || item.dsorg === "PO-E"); + } else if (!SharedContext.isMigrated(dsNode) && item.migr?.toUpperCase() === "YES") { + dsNode.datasetMigrated(); + } // Creates a ZoweDatasetNode for a PDS + } else if (item.migr && item.migr.toUpperCase() === "YES") { + dsNode = new ZoweDatasetNode({ + label: item.dsname, + collapsibleState: vscode.TreeItemCollapsibleState.None, + parentNode: this, + contextOverride: Constants.DS_MIGRATED_FILE_CONTEXT, + profile: cachedProfile, + }); + elementChildren[dsNode.label.toString()] = dsNode; + // Creates a ZoweDatasetNode for a VSAM file } else if (item.dsorg === "PO" || item.dsorg === "PO-E") { dsNode = new ZoweDatasetNode({ label: item.dsname, @@ -275,16 +345,6 @@ export class ZoweDatasetNode extends ZoweTreeNode implements IZoweDatasetTreeNod dsNode.errorDetails = item.error; // Save imperative error to avoid extra z/OS requests elementChildren[dsNode.label.toString()] = dsNode; // Creates a ZoweDatasetNode for a migrated dataset - } else if (item.migr && item.migr.toUpperCase() === "YES") { - dsNode = new ZoweDatasetNode({ - label: item.dsname, - collapsibleState: vscode.TreeItemCollapsibleState.None, - parentNode: this, - contextOverride: Constants.DS_MIGRATED_FILE_CONTEXT, - profile: cachedProfile, - }); - elementChildren[dsNode.label.toString()] = dsNode; - // Creates a ZoweDatasetNode for a VSAM file } else if (item.dsorg === "VS") { let altLabel = item.dsname; let endPoint = altLabel.indexOf(".DATA");