Skip to content
Open
1 change: 1 addition & 0 deletions packages/zowe-explorer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen
- Fixed an issue where secure credentials and headers were being logged to the Zowe logger and VSCode output channel. [#3848](https://github.com/zowe/zowe-explorer-vscode/pull/3848)
- Fixed an error encountered when deleting members in a PDS. [#3874](https://github.com/zowe/zowe-explorer-vscode/issues/3874)
- Fixed regression where PDS member attributes were no longer listed when using the "Show Attributes" feature from the data set context menu. [#3856](https://github.com/zowe/zowe-explorer-vscode/issues/3856)
- Prevent drag-and-drop between profiles pointing to the same DASD/DSN by creating a check in the `DatasetTree.handleDrop` function. The operation is now blocked and an error message is shown instead; users should refresh the target profile to see any changes. [#3827](https://github.com/zowe/zowe-explorer-vscode/pull/3827)

## `3.3.0`

Expand Down
66 changes: 43 additions & 23 deletions packages/zowe-explorer/src/trees/dataset/DatasetTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,8 @@ export class DatasetTree extends ZoweTreeProvider<IZoweDatasetTreeNode> implemen
try {
const encodingInfo = await sourceNode.getEncoding();
// If the encoding is binary, we need to force upload it as binary
const queryString = `forceUpload=true${
encodingInfo?.kind === "binary" ? "&encoding=binary" : encodingInfo?.kind === "other" ? "&encoding=" + encodingInfo?.codepage : ""
}`;
const queryString = `forceUpload=true${encodingInfo?.kind === "binary" ? "&encoding=binary" : encodingInfo?.kind === "other" ? "&encoding=" + encodingInfo?.codepage : ""
}`;
await DatasetFSProvider.instance.writeFile(
destUri.with({
query: queryString,
Expand Down Expand Up @@ -227,9 +226,30 @@ export class DatasetTree extends ZoweTreeProvider<IZoweDatasetTreeNode> implemen
}

let target = targetNode;

if (target) {
for (const item of droppedItems.value) {
const node = this.draggedNodes[item.uri.path];
const srcDsn = typeof node.label === "string" ? node.label : node.label.label;
const tgtDsn = typeof target.label === "string" ? target.label : target.label.label;

// skip drop if source and target are the same dataset on the same DASD
if (
node.getProfile && target.getProfile &&
await SharedUtils.sameDatasetSameDasd(
node.getProfile(),
target.getProfile(),
srcDsn,
tgtDsn
)
) {
Gui.errorMessage(
vscode.l10n.t(
"Cannot move: The source and target are the same. You are using a different profile to view the target. Refresh to view changes."
)
);
return;
}

if (SharedContext.isPds(target) || SharedContext.isDsMember(target)) {
if (SharedContext.isPds(node) || SharedContext.isDs(node)) {
Expand Down Expand Up @@ -1759,15 +1779,15 @@ export class DatasetTree extends ZoweTreeProvider<IZoweDatasetTreeNode> implemen
// Adapt menus to user based on the node that was interacted with
const specifier = isSession
? vscode.l10n.t({
message: "all PDS members in {0}",
args: [node.label as string],
comment: ["Node label"],
})
message: "all PDS members in {0}",
args: [node.label as string],
comment: ["Node label"],
})
: vscode.l10n.t({
message: "the PDS members in {0}",
args: [node.label as string],
comment: ["Node label"],
});
message: "the PDS members in {0}",
args: [node.label as string],
comment: ["Node label"],
});
const selection = await Gui.showQuickPick(
DatasetUtils.DATASET_SORT_OPTS.map((opt, i) => ({
label: sortOpts.method === i ? `${opt} $(check)` : opt,
Expand Down Expand Up @@ -1832,10 +1852,10 @@ export class DatasetTree extends ZoweTreeProvider<IZoweDatasetTreeNode> implemen
node.filter = newFilter;
node.description = newFilter
? vscode.l10n.t({
message: "Filter: {0}",
args: [newFilter.value],
comment: ["Filter value"],
})
message: "Filter: {0}",
args: [newFilter.value],
comment: ["Filter value"],
})
: null;
this.nodeDataChanged(node);

Expand Down Expand Up @@ -1896,15 +1916,15 @@ export class DatasetTree extends ZoweTreeProvider<IZoweDatasetTreeNode> implemen
// Adapt menus to user based on the node that was interacted with
const specifier = isSession
? vscode.l10n.t({
message: "all PDS members in {0}",
args: [node.label as string],
comment: ["Node label"],
})
message: "all PDS members in {0}",
args: [node.label as string],
comment: ["Node label"],
})
: vscode.l10n.t({
message: "the PDS members in {0}",
args: [node.label as string],
comment: ["Node label"],
});
message: "the PDS members in {0}",
args: [node.label as string],
comment: ["Node label"],
});
const clearFilter = isSession
? `$(clear-all) ${vscode.l10n.t("Clear filter for profile")}`
: `$(clear-all) ${vscode.l10n.t("Clear filter for PDS")}`;
Expand Down
88 changes: 83 additions & 5 deletions packages/zowe-explorer/src/trees/shared/SharedUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { ZoweLogger } from "../../tools/ZoweLogger";
import { SharedContext } from "./SharedContext";
import { Definitions } from "../../configuration/Definitions";
import { SettingsConfig } from "../../configuration/SettingsConfig";
import { ZoweExplorerApiRegister } from "../../extending/ZoweExplorerApiRegister";

export class SharedUtils {
public static async copyExternalLink(this: void, context: vscode.ExtensionContext, node: IZoweTreeNode): Promise<void> {
Expand Down Expand Up @@ -380,11 +381,11 @@ export class SharedUtils {
comment: ["Node label"],
}),
currentEncoding &&
vscode.l10n.t({
message: "Current encoding is {0}",
args: [currentEncoding],
comment: ["Encoding name"],
})
vscode.l10n.t({
message: "Current encoding is {0}",
args: [currentEncoding],
comment: ["Encoding name"],
})
);

return SharedUtils.processEncodingResponse(response, node.label as string);
Expand Down Expand Up @@ -580,4 +581,81 @@ export class SharedUtils {
}
}
}

/**
* Checks if two datasets refer to the same physical object by comparing
* their names and DASD volumes across both profiles
*
* @param srcProfile - source profile representing the dataset origin
* @param dstProfile - target profile representing the dataset destination
* @param srcDsn - source profile dataset name
* @param dstDsn - target profile dataset name
* @returns Promise resolves to true if both dataset names are equal and on the same DASD volume. false otherwise
*/
public static async sameDatasetSameDasd(
srcProfile: imperative.IProfileLoaded,
dstProfile: imperative.IProfileLoaded,
srcDsn: string,
dstDsn: string
): Promise<boolean> {
if (!srcDsn || !dstDsn) return false;
if (srcDsn.toUpperCase() !== dstDsn.toUpperCase()) return false;

try {
const mvsSrc = ZoweExplorerApiRegister.getMvsApi(srcProfile);
const mvsDst = ZoweExplorerApiRegister.getMvsApi(dstProfile);

const [srcAttr, dstAttr] = await Promise.all([
mvsSrc.dataSet(srcDsn, { attributes: true }),
mvsDst.dataSet(dstDsn, { attributes: true }),
]);

const s = srcAttr?.apiResponse?.items?.[0];
const d = dstAttr?.apiResponse?.items?.[0];
if (!s?.vols || !d?.vols) return false;

// normalize
const sVols = (Array.isArray(s.vols) ? s.vols : [s.vols]) as string[];
const dVols = (Array.isArray(d.vols) ? d.vols : [d.vols]) as string[];

// check they match exactly (same count, same order)
return (
sVols.length === dVols.length &&
sVols.every((vol, i) => vol === dVols[i])
);
} catch {
return false;
}
}

/**
* Checks if a USS file or directory is likely the same actual object as another
* by comparing the normalized paths (ignoring profile) and verifying existence
*
* @param sourceNode - source USS tree node being moved
* @param targetParent - target USS tree node parent receiving the drop
* @param droppedLabel - name of the dropped item
* @returns Promise resolves to true if the normalized paths match and the target path exists. false otherwise
*/
public static async isLikelySameUssObjectByUris(
sourceNode: IZoweUSSTreeNode,
targetParent: IZoweUSSTreeNode,
droppedLabel: string
): Promise<boolean> {
// drop the profile prefix so just comparing real paths
const stripProfile = (p: string) => "/" + p.split("/").slice(2).join("/");

const srcPathFull = stripProfile(sourceNode.resourceUri.path);
const dstUri = targetParent.resourceUri.with({
path: path.posix.join(targetParent.resourceUri.path, droppedLabel),
});
const dstPathFull = stripProfile(dstUri.path);

if (path.posix.normalize(srcPathFull) !== path.posix.normalize(dstPathFull)) {
return false;
}

// if same path, double check target actually exists
return UssFSProvider.instance.exists(dstUri);
}
}
12 changes: 12 additions & 0 deletions packages/zowe-explorer/src/trees/uss/USSTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,18 @@ export class USSTree extends ZoweTreeProvider<IZoweUSSTreeNode> implements Types
continue;
}

// skip drop if source and target are possibly same-object
if (await SharedUtils.isLikelySameUssObjectByUris(node, target, typeof item.label === "string" ? item.label : item.label.label)) {
Gui.errorMessage(
vscode.l10n.t(
"Cannot move: The source and target are possibly the same. You are using a different profile to view the target. Refresh to view changes."
)
);
movingMsg.dispose();
this.draggedNodes = {};
return;
}

const newUriForNode = vscode.Uri.from({
scheme: ZoweScheme.USS,
path: path.posix.join("/", target.getProfile().name, target.fullPath, item.label as string),
Expand Down