diff --git a/packages/zowe-explorer-api/CHANGELOG.md b/packages/zowe-explorer-api/CHANGELOG.md index f3d025e88f..5e1d59f8c7 100644 --- a/packages/zowe-explorer-api/CHANGELOG.md +++ b/packages/zowe-explorer-api/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to the "zowe-explorer-api" extension will be documented in t ### New features and enhancements +- Added optional `getTag` function to `ZoweExplorerAPI.IUss` for getting the tag of a file on USS. + ### Bug fixes ## `2.11.0` diff --git a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ZoweExplorerZosmfApi.unit.test.ts b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ZoweExplorerZosmfApi.unit.test.ts index ab4e76d44d..f700d6df52 100644 --- a/packages/zowe-explorer-api/__tests__/__unit__/profiles/ZoweExplorerZosmfApi.unit.test.ts +++ b/packages/zowe-explorer-api/__tests__/__unit__/profiles/ZoweExplorerZosmfApi.unit.test.ts @@ -260,6 +260,30 @@ describe("ZosmfUssApi", () => { expect(logoutSpy).toHaveBeenCalledWith(fakeSession); }); + it("should retrieve the tag of a file", async () => { + const zosmfApi = new ZosmfUssApi(); + jest.spyOn(JSON, "parse").mockReturnValue({ + stdout: ["-t UTF-8 tesfile.txt"], + }); + + Object.defineProperty(zowe.Utilities, "putUSSPayload", { + value: () => Buffer.from(""), + configurable: true, + }); + await expect(zosmfApi.getTag("testfile.txt")).resolves.toEqual("UTF-8"); + }); + + it("should update the tag attribute when passed in", async () => { + const zosmfApi = new ZosmfUssApi(); + const changeTagSpy = jest.fn(); + Object.defineProperty(zowe.Utilities, "putUSSPayload", { + value: changeTagSpy, + configurable: true, + }); + await expect(zosmfApi.updateAttributes("/test/path", { tag: "utf-8" })).resolves.not.toThrow(); + expect(changeTagSpy).toBeCalledTimes(1); + }); + const ussApis: ITestApi[] = [ { name: "isFileTagBinOrAscii", diff --git a/packages/zowe-explorer-api/src/profiles/ZoweExplorerApi.ts b/packages/zowe-explorer-api/src/profiles/ZoweExplorerApi.ts index 905722b894..d79c99ca37 100644 --- a/packages/zowe-explorer-api/src/profiles/ZoweExplorerApi.ts +++ b/packages/zowe-explorer-api/src/profiles/ZoweExplorerApi.ts @@ -205,6 +205,14 @@ export namespace ZoweExplorerApi { * @returns {Promise} */ rename(currentUssPath: string, newUssPath: string): Promise; + + /** + * Get the tag of a USS file + * + * @param {string} currentUssPath + * @returns {Promise} + */ + getTag?(ussPath: string): Promise; } /** diff --git a/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts b/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts index c20cda5208..1075472f49 100644 --- a/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts +++ b/packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts @@ -147,6 +147,14 @@ export class ZosmfUssApi extends ZosmfApiCommon implements ZoweExplorerApi.IUss public async updateAttributes(ussPath: string, attributes: Partial): Promise { try { + if (attributes.tag) { + await zowe.Utilities.putUSSPayload(this.getSession(), ussPath, { + request: "chtag", + action: "set", + type: "text", + codeset: attributes.tag !== null ? attributes.tag.toString() : attributes.tag, + }); + } if ((attributes.group || attributes.gid) && (attributes.owner || attributes.uid)) { await zowe.Utilities.putUSSPayload(this.getSession(), ussPath, { request: "chown", @@ -161,7 +169,6 @@ export class ZosmfUssApi extends ZosmfApiCommon implements ZoweExplorerApi.IUss recursive: true, }); } - if (attributes.perms) { await zowe.Utilities.putUSSPayload(this.getSession(), ussPath, { request: "chmod", @@ -205,6 +212,14 @@ export class ZosmfUssApi extends ZosmfApiCommon implements ZoweExplorerApi.IUss apiResponse: result, }; } + + public async getTag(ussPath: string): Promise { + const response = await zowe.Utilities.putUSSPayload(this.getSession(), ussPath, { + request: "chtag", + action: "list", + }); + return JSON.parse(response.toString()).stdout[0].split(" ")[1] as string; + } } /** diff --git a/packages/zowe-explorer-api/src/utils/files.ts b/packages/zowe-explorer-api/src/utils/files.ts index 76b1026ff2..cb95337463 100644 --- a/packages/zowe-explorer-api/src/utils/files.ts +++ b/packages/zowe-explorer-api/src/utils/files.ts @@ -21,6 +21,7 @@ export type FileAttributes = { owner: string; uid: number; perms: string; + tag?: string; }; export function permStringToOctal(perms: string): number { diff --git a/packages/zowe-explorer/__tests__/__unit__/api/ZoweExplorerZosmfApi.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/api/ZoweExplorerZosmfApi.unit.test.ts index 21fdff381b..40449b9978 100644 --- a/packages/zowe-explorer/__tests__/__unit__/api/ZoweExplorerZosmfApi.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/api/ZoweExplorerZosmfApi.unit.test.ts @@ -120,4 +120,31 @@ describe("Zosmf API tests", () => { Promise.resolve(zowe.Download.ussFile(api.getSession(), "/some/input/path", {})) ); }); + + it("should update the tag attribute of a USS file if a new change is made", async () => { + const api = new ZosmfUssApi(); + const changeTagSpy = jest.fn(); + Object.defineProperty(zowe, "Utilities", { + value: { + putUSSPayload: changeTagSpy, + }, + configurable: true, + }); + await expect(api.updateAttributes("/test/path", { tag: "utf-8" })).resolves.not.toThrow(); + expect(changeTagSpy).toBeCalledTimes(1); + }); + + it("should get the tag of a file successfully", async () => { + const api = new ZosmfUssApi(); + jest.spyOn(JSON, "parse").mockReturnValue({ + stdout: ["-t UTF-8 tesfile.txt"], + }); + Object.defineProperty(zowe, "Utilities", { + value: { + putUSSPayload: () => Buffer.from(""), + }, + configurable: true, + }); + await expect(api.getTag("testfile.txt")).resolves.toEqual("UTF-8"); + }); }); diff --git a/packages/zowe-explorer/__tests__/__unit__/uss/AttributeView.unit.test.ts b/packages/zowe-explorer/__tests__/__unit__/uss/AttributeView.unit.test.ts index ce554ede95..d26119d6d1 100644 --- a/packages/zowe-explorer/__tests__/__unit__/uss/AttributeView.unit.test.ts +++ b/packages/zowe-explorer/__tests__/__unit__/uss/AttributeView.unit.test.ts @@ -13,6 +13,7 @@ import { ExtensionContext } from "vscode"; import { AttributeView } from "../../../src/uss/AttributeView"; import { IZoweTree, IZoweUSSTreeNode, ZoweExplorerApi } from "@zowe/zowe-explorer-api"; import { ZoweExplorerApiRegister } from "../../../src/ZoweExplorerApiRegister"; +import * as contextually from "../../../src/shared/context"; describe("AttributeView unit tests", () => { let view: AttributeView; @@ -21,6 +22,7 @@ describe("AttributeView unit tests", () => { const node = { attributes: { perms: "----------", + tag: undefined, }, label: "example node", fullPath: "/z/some/path", @@ -33,7 +35,9 @@ describe("AttributeView unit tests", () => { beforeAll(() => { jest.spyOn(ZoweExplorerApiRegister, "getUssApi").mockReturnValue({ updateAttributes: updateAttributesMock, + getTag: () => Promise.resolve("UTF-8"), } as unknown as ZoweExplorerApi.IUss); + jest.spyOn(contextually, "isUssDirectory").mockReturnValue(false); view = new AttributeView(context, treeProvider, node); }); @@ -41,21 +45,21 @@ describe("AttributeView unit tests", () => { node.onUpdate = jest.fn(); }); - it("refreshes properly when webview sends 'refresh' command", () => { + it("refreshes properly when webview sends 'refresh' command", async () => { // case 1: node is a root node - (view as any).onDidReceiveMessage({ command: "refresh" }); + await (view as any).onDidReceiveMessage({ command: "refresh" }); expect(treeProvider.refresh).toHaveBeenCalled(); // case 2: node is a child node node.getParent = jest.fn().mockReturnValueOnce({ label: "parent node" } as IZoweUSSTreeNode); - (view as any).onDidReceiveMessage({ command: "refresh" }); + await (view as any).onDidReceiveMessage({ command: "refresh" }); expect(treeProvider.refreshElement).toHaveBeenCalled(); expect(node.onUpdate).toHaveBeenCalledTimes(2); }); - it("dispatches node data to webview when 'ready' command is received", () => { - (view as any).onDidReceiveMessage({ command: "ready" }); + it("dispatches node data to webview when 'ready' command is received", async () => { + await (view as any).onDidReceiveMessage({ command: "ready" }); expect(view.panel.webview.postMessage).toHaveBeenCalledWith({ attributes: node.attributes, name: node.fullPath, @@ -65,7 +69,7 @@ describe("AttributeView unit tests", () => { it("updates attributes when 'update-attributes' command is received", async () => { // case 1: no attributes provided from webview (sanity check) - (view as any).onDidReceiveMessage({ command: "update-attributes" }); + await (view as any).onDidReceiveMessage({ command: "update-attributes" }); expect(updateAttributesMock).not.toHaveBeenCalled(); const attributes = { diff --git a/packages/zowe-explorer/src/uss/AttributeView.ts b/packages/zowe-explorer/src/uss/AttributeView.ts index baaac36f3a..5a880dbccc 100644 --- a/packages/zowe-explorer/src/uss/AttributeView.ts +++ b/packages/zowe-explorer/src/uss/AttributeView.ts @@ -12,6 +12,7 @@ import { FileAttributes, Gui, IZoweTree, IZoweUSSTreeNode, WebView, ZoweExplorerApi } from "@zowe/zowe-explorer-api"; import { Disposable, ExtensionContext } from "vscode"; import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister"; +import * as contextually from "../shared/context"; export class AttributeView extends WebView { private treeProvider: IZoweTree; @@ -30,11 +31,18 @@ export class AttributeView extends WebView { this.ussApi = ZoweExplorerApiRegister.getUssApi(this.ussNode.getProfile()); } + private async attachTag(node: IZoweUSSTreeNode): Promise { + if (this.ussApi.getTag && !contextually.isUssDirectory(node)) { + node.attributes.tag = await this.ussApi.getTag(node.fullPath); + } + } + protected async onDidReceiveMessage(message: any): Promise { switch (message.command) { case "refresh": if (this.canUpdate) { this.onUpdateDisposable = this.ussNode.onUpdate(async (node) => { + await this.attachTag(node); await this.panel.webview.postMessage({ attributes: node.attributes, name: node.fullPath, @@ -51,6 +59,7 @@ export class AttributeView extends WebView { } break; case "ready": + await this.attachTag(this.ussNode); await this.panel.webview.postMessage({ attributes: this.ussNode.attributes, name: this.ussNode.fullPath, @@ -104,6 +113,10 @@ export class AttributeView extends WebView { newAttrs.perms = attrs.perms; } + if (this.ussNode.attributes.tag !== attrs.tag && this.ussApi.getTag) { + newAttrs.tag = attrs.tag; + } + await this.ussApi.updateAttributes(this.ussNode.fullPath, newAttrs); this.ussNode.attributes = { ...(this.ussNode.attributes ?? {}), ...newAttrs } as FileAttributes; diff --git a/packages/zowe-explorer/webviews/edit-attributes/src/App.tsx b/packages/zowe-explorer/webviews/edit-attributes/src/App.tsx index b15dcfd43e..0d44c55652 100644 --- a/packages/zowe-explorer/webviews/edit-attributes/src/App.tsx +++ b/packages/zowe-explorer/webviews/edit-attributes/src/App.tsx @@ -15,6 +15,7 @@ import isEqual from "lodash.isequal"; const vscodeApi = acquireVsCodeApi(); export function App() { + const notSupported = "NOT SUPPORTED"; const [readonly, setReadonly] = useState(false); const [allowUpdate, setAllowUpdate] = useState(false); const [attributes, setAttributes] = useState>({ @@ -111,6 +112,7 @@ export function App() { }; return all; }, {}), + tag: attributes.tag ?? notSupported, }; setAttributes({ @@ -155,6 +157,17 @@ export function App() {
{attributes.current.name}
+ {attributes.initial?.directory ?? false ? null : ( +
+ updateFileAttributes("tag", e.target.value)} + > + Tag + +
+ )}
diff --git a/packages/zowe-explorer/webviews/edit-attributes/src/types.ts b/packages/zowe-explorer/webviews/edit-attributes/src/types.ts index f1dba099f4..72efca076a 100644 --- a/packages/zowe-explorer/webviews/edit-attributes/src/types.ts +++ b/packages/zowe-explorer/webviews/edit-attributes/src/types.ts @@ -15,4 +15,5 @@ export type FileAttributes = { directory: boolean; group: string; perms: FilePermissions; + tag?: string; };