From c0622a9234890392117f050cbccb69f746c7957a Mon Sep 17 00:00:00 2001 From: Kishan Patel Date: Thu, 14 Aug 2025 14:23:31 -0400 Subject: [PATCH 01/13] feat(ContentNavigator): added more detailed path information --- .../components/ContentNavigator/ContentDataProvider.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/src/components/ContentNavigator/ContentDataProvider.ts b/client/src/components/ContentNavigator/ContentDataProvider.ts index 3febc6634..6395d813f 100644 --- a/client/src/components/ContentNavigator/ContentDataProvider.ts +++ b/client/src/components/ContentNavigator/ContentDataProvider.ts @@ -213,6 +213,15 @@ class ContentDataProvider public async getTreeItem(item: ContentItem): Promise { const isContainer = getIsContainer(item); const uri = await this.model.getUri(item, false); + let tooltip = item.name; // fallback to item name + try { + const fullPath = await this.model.getPathOfItem(item); + if (fullPath) { + tooltip = fullPath; + } + } catch { + // If getPathOfItem fails, tooltip will remain as item.name + } return { collapsibleState: isContainer @@ -230,6 +239,7 @@ class ContentDataProvider id: item.uid, label: item.name, resourceUri: uri, + tooltip: tooltip, }; } From 64965fed274ab8a7d3676a91f069dbaf168b2bee Mon Sep 17 00:00:00 2001 From: Kishan Patel Date: Thu, 14 Aug 2025 17:38:11 -0400 Subject: [PATCH 02/13] feat(ContentNavigator): added optimization with parent caching, also only fetch valid paths --- .../ContentNavigator/ContentDataProvider.ts | 46 ++++++++++++++++--- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/client/src/components/ContentNavigator/ContentDataProvider.ts b/client/src/components/ContentNavigator/ContentDataProvider.ts index 6395d813f..fdd39598f 100644 --- a/client/src/components/ContentNavigator/ContentDataProvider.ts +++ b/client/src/components/ContentNavigator/ContentDataProvider.ts @@ -79,6 +79,8 @@ class ContentDataProvider public dropMimeTypes: string[]; public dragMimeTypes: string[]; + private parentPathCache = new Map(); + get treeView(): TreeView { return this._treeView; } @@ -213,14 +215,19 @@ class ContentDataProvider public async getTreeItem(item: ContentItem): Promise { const isContainer = getIsContainer(item); const uri = await this.model.getUri(item, false); - let tooltip = item.name; // fallback to item name - try { - const fullPath = await this.model.getPathOfItem(item); - if (fullPath) { - tooltip = fullPath; + let tooltip = item.name; + + const canCopyPath = item.contextValue?.includes("copyPath"); + + if (canCopyPath) { + try { + const parentPath = await this.getCachedParentPath(item); + if (parentPath) { + tooltip = parentPath + "/" + item.name; + } + } catch { + // If getting parent path fails, tooltip will remain as item.name } - } catch { - // If getPathOfItem fails, tooltip will remain as item.name } return { @@ -745,6 +752,31 @@ class ContentDataProvider } : undefined; } + + private async getCachedParentPath(item: ContentItem): Promise { + const parentUri = item.parentFolderUri; + if (!parentUri) { + return ""; + } + + if (!this.parentPathCache.has(parentUri)) { + try { + console.log("ServerCall"); + const fullPath = await this.model.getPathOfItem(item); + if (fullPath) { + const parentPath = + fullPath.substring(0, fullPath.lastIndexOf("/")) || "/"; + this.parentPathCache.set(parentUri, parentPath); + } else { + this.parentPathCache.set(parentUri, ""); + } + } catch { + this.parentPathCache.set(parentUri, ""); + } + } + + return this.parentPathCache.get(parentUri) || ""; + } } export default ContentDataProvider; From 0eb18fc023235f86f785b38e2aafc9d6ca51a554 Mon Sep 17 00:00:00 2001 From: Kishan Patel Date: Thu, 14 Aug 2025 17:39:19 -0400 Subject: [PATCH 03/13] DCO Remediation Commit for Kishan Patel I, Kishan Patel , hereby add my Signed-off-by to this commit: c0622a9234890392117f050cbccb69f746c7957a I, Kishan Patel , hereby add my Signed-off-by to this commit: 64965fed274ab8a7d3676a91f069dbaf168b2bee Signed-off-by: Kishan Patel --- client/src/components/ContentNavigator/ContentDataProvider.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/components/ContentNavigator/ContentDataProvider.ts b/client/src/components/ContentNavigator/ContentDataProvider.ts index fdd39598f..6385bc78d 100644 --- a/client/src/components/ContentNavigator/ContentDataProvider.ts +++ b/client/src/components/ContentNavigator/ContentDataProvider.ts @@ -761,7 +761,6 @@ class ContentDataProvider if (!this.parentPathCache.has(parentUri)) { try { - console.log("ServerCall"); const fullPath = await this.model.getPathOfItem(item); if (fullPath) { const parentPath = From 05fb7f221a1de277769d677ed75205937e5b5114 Mon Sep 17 00:00:00 2001 From: Kishan Patel Date: Fri, 15 Aug 2025 14:14:54 -0400 Subject: [PATCH 04/13] feat(ContentNavigator): changed uri to have a full path to allow for more efficent path retrieval --- .../src/connection/rest/RestContentAdapter.ts | 147 +++++++++++++++--- client/src/connection/rest/util.ts | 12 +- 2 files changed, 135 insertions(+), 24 deletions(-) diff --git a/client/src/connection/rest/RestContentAdapter.ts b/client/src/connection/rest/RestContentAdapter.ts index d9c7465d7..49bbfc9bc 100644 --- a/client/src/connection/rest/RestContentAdapter.ts +++ b/client/src/connection/rest/RestContentAdapter.ts @@ -52,6 +52,8 @@ class RestContentAdapter implements ContentAdapter { [id: string]: { etag: string; lastModified: string; contentType: string }; }; private contextMenuProvider: ContextMenuProvider; + private pathCache: Map = new Map(); + private pathPromiseCache: Map> = new Map(); public constructor() { this.rootFolders = {}; @@ -118,7 +120,13 @@ class RestContentAdapter implements ContentAdapter { } const { data } = await this.connection.get(ancestorsLink.uri); if (data && data.length > 0) { - return this.enrichWithDataProviderProperties(data[0]); + const enrichedItem = this.enrichWithDataProviderProperties(data[0]); + + if (enrichedItem.contextValue?.includes("copyPath")) { + await this.updateUriWithFullPath(enrichedItem); + } + + return enrichedItem; } } @@ -142,19 +150,27 @@ class RestContentAdapter implements ContentAdapter { ? [] : await this.getChildItems(myFavoritesFolder); - const items = result.items.map( - (childItem: ContentItem, index): ContentItem => { - const favoriteUri = fetchFavoriteUri(childItem); - return { - ...childItem, - uid: `${parentItem.uid}/${index}`, - ...this.enrichWithDataProviderProperties(childItem, { - isInRecycleBin, - isInMyFavorites: parentIdIsFavoritesFolder || !!favoriteUri, - favoriteUri, - }), - }; - }, + const items = await Promise.all( + result.items.map( + async (childItem: ContentItem, index): Promise => { + const favoriteUri = fetchFavoriteUri(childItem); + const enrichedItem = { + ...childItem, + uid: `${parentItem.uid}/${index}`, + ...this.enrichWithDataProviderProperties(childItem, { + isInRecycleBin, + isInMyFavorites: parentIdIsFavoritesFolder || !!favoriteUri, + favoriteUri, + }), + }; + + if (enrichedItem.contextValue?.includes("copyPath")) { + await this.updateUriWithFullPath(enrichedItem); + } + + return enrichedItem; + }, + ), ); return items; @@ -186,21 +202,59 @@ class RestContentAdapter implements ContentAdapter { if (!item) { return ""; } + const baseKey = item.uri || item.id || item.name; + const cacheKey = `${baseKey}_${folderPathOnly || false}`; + + if (this.pathCache.has(cacheKey)) { + return this.pathCache.get(cacheKey)!; + } + if (this.pathPromiseCache.has(cacheKey)) { + return this.pathPromiseCache.get(cacheKey)!; + } + + const pathPromise = this.calculatePathOfItem(item, folderPathOnly); + this.pathPromiseCache.set(cacheKey, pathPromise); + + try { + const path = await pathPromise; + this.pathCache.set(cacheKey, path); + this.pathPromiseCache.delete(cacheKey); + + if (!folderPathOnly && path.includes("/")) { + const parentPath = path.substring(0, path.lastIndexOf("/")) || "/"; + const parentKey = `${item.parentFolderUri}_true`; + if (!this.pathCache.has(parentKey)) { + this.pathCache.set(parentKey, parentPath); + } + } + + return path; + } catch { + this.pathPromiseCache.delete(cacheKey); + this.pathCache.set(cacheKey, ""); + return ""; + } + } + + private async calculatePathOfItem( + item: ContentItem, + folderPathOnly?: boolean, + ): Promise { const filePathParts = []; let currentContentItem: Pick = item; if (!folderPathOnly) { filePathParts.push(currentContentItem.name); } + do { try { const { data: parentData } = await this.connection.get( currentContentItem.parentFolderUri, ); currentContentItem = parentData; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (e) { + } catch { return ""; } if (currentContentItem.name) { @@ -319,7 +373,13 @@ class RestContentAdapter implements ContentAdapter { const response = await this.connection.get(id); this.updateFileMetadata(id, response); - return this.enrichWithDataProviderProperties(response.data); + const enrichedItem = this.enrichWithDataProviderProperties(response.data); + + if (enrichedItem.contextValue?.includes("copyPath")) { + await this.updateUriWithFullPath(enrichedItem); + } + + return enrichedItem; } public async getItemOfUri(uri: Uri): Promise { @@ -357,7 +417,15 @@ class RestContentAdapter implements ContentAdapter { `/folders/folders?parentFolderUri=${parentFolderUri}`, { name: folderName }, ); - return this.enrichWithDataProviderProperties(createFolderResponse.data); + const enrichedItem = this.enrichWithDataProviderProperties( + createFolderResponse.data, + ); + + if (enrichedItem.contextValue?.includes("copyPath")) { + await this.updateUriWithFullPath(enrichedItem); + } + + return enrichedItem; // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { return; @@ -371,9 +439,11 @@ class RestContentAdapter implements ContentAdapter { item.flags = flags; item.permission = getPermission(item); + const contextValue = this.contextMenuProvider.availableActions(item); + return { ...item, - contextValue: this.contextMenuProvider.availableActions(item), + contextValue, fileStat: { ctime: item.creationTimeStamp, mtime: item.modifiedTimeStamp, @@ -398,6 +468,27 @@ class RestContentAdapter implements ContentAdapter { } } + private async updateUriWithFullPath(item: ContentItem): Promise { + try { + const typeName = getTypeName(item); + if (typeName === TRASH_FOLDER_TYPE || !item.parentFolderUri) { + return; + } + + const fullPath = await this.getPathOfItem(item); + if (fullPath && fullPath !== item.name) { + item.vscUri = getSasContentUri( + item, + item.flags?.isInRecycleBin || false, + fullPath, + ); + } + } catch { + // If path retrieval fails, keep the original URI + return; + } + } + public async renameItem( item: ContentItem, newName: string, @@ -443,7 +534,15 @@ class RestContentAdapter implements ContentAdapter { return await this.getItemOfId(item.uri); } - return this.enrichWithDataProviderProperties(patchResponse.data); + const enrichedItem = this.enrichWithDataProviderProperties( + patchResponse.data, + ); + + if (enrichedItem.contextValue?.includes("copyPath")) { + await this.updateUriWithFullPath(enrichedItem); + } + + return enrichedItem; // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { return; @@ -488,7 +587,13 @@ class RestContentAdapter implements ContentAdapter { return; } - return this.enrichWithDataProviderProperties(createdResource); + const enrichedItem = this.enrichWithDataProviderProperties(createdResource); + + if (enrichedItem.contextValue?.includes("copyPath")) { + await this.updateUriWithFullPath(enrichedItem); + } + + return enrichedItem; } public async addChildItem( diff --git a/client/src/connection/rest/util.ts b/client/src/connection/rest/util.ts index 824906148..96b59395d 100644 --- a/client/src/connection/rest/util.ts +++ b/client/src/connection/rest/util.ts @@ -38,12 +38,18 @@ export const getResourceIdFromItem = (item: ContentItem): string | null => { return getLink(item.links, "GET", "self")?.uri || null; }; -export const getSasContentUri = (item: ContentItem, readOnly?: boolean): Uri => +export const getSasContentUri = ( + item: ContentItem, + readOnly?: boolean, + fullPath?: string, +): Uri => Uri.parse( `${readOnly ? `${ContentSourceType.SASContent}ReadOnly` : ContentSourceType.SASContent}:/${ - item.name - ? item.name.replace(/#/g, "%23").replace(/\?/g, "%3F") + fullPath + ? fullPath.replace(/#/g, "%23").replace(/\?/g, "%3F") : item.name + ? item.name.replace(/#/g, "%23").replace(/\?/g, "%3F") + : item.name }?id=${getResourceIdFromItem(item)}`, ); From fced7a6fee14cbb6bb32f4e320162c0191dfca94 Mon Sep 17 00:00:00 2001 From: Kishan Patel Date: Fri, 15 Aug 2025 14:19:12 -0400 Subject: [PATCH 05/13] DCO Remediation Commit for Kishan Patel I, Kishan Patel , hereby add my Signed-off-by to this commit: 05fb7f221a1de277769d677ed75205937e5b5114 Signed-off-by: Kishan Patel --- client/src/components/ContentNavigator/ContentDataProvider.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/ContentNavigator/ContentDataProvider.ts b/client/src/components/ContentNavigator/ContentDataProvider.ts index 6385bc78d..c7871e776 100644 --- a/client/src/components/ContentNavigator/ContentDataProvider.ts +++ b/client/src/components/ContentNavigator/ContentDataProvider.ts @@ -221,7 +221,7 @@ class ContentDataProvider if (canCopyPath) { try { - const parentPath = await this.getCachedParentPath(item); + const parentPath = await this.getItemTooltip(item); if (parentPath) { tooltip = parentPath + "/" + item.name; } @@ -753,7 +753,7 @@ class ContentDataProvider : undefined; } - private async getCachedParentPath(item: ContentItem): Promise { + private async getItemTooltip(item: ContentItem): Promise { const parentUri = item.parentFolderUri; if (!parentUri) { return ""; From 1e5a9b67658f0c94990fffa7babd9db8ff3df022 Mon Sep 17 00:00:00 2001 From: Kishan Patel Date: Fri, 15 Aug 2025 14:32:57 -0400 Subject: [PATCH 06/13] wip(ContentNavigator): Partial working modification --- .../ContentNavigator/ContentDataProvider.ts | 53 +++++++------------ 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/client/src/components/ContentNavigator/ContentDataProvider.ts b/client/src/components/ContentNavigator/ContentDataProvider.ts index c7871e776..dc5347453 100644 --- a/client/src/components/ContentNavigator/ContentDataProvider.ts +++ b/client/src/components/ContentNavigator/ContentDataProvider.ts @@ -79,8 +79,6 @@ class ContentDataProvider public dropMimeTypes: string[]; public dragMimeTypes: string[]; - private parentPathCache = new Map(); - get treeView(): TreeView { return this._treeView; } @@ -216,17 +214,30 @@ class ContentDataProvider const isContainer = getIsContainer(item); const uri = await this.model.getUri(item, false); let tooltip = item.name; - const canCopyPath = item.contextValue?.includes("copyPath"); if (canCopyPath) { - try { - const parentPath = await this.getItemTooltip(item); - if (parentPath) { - tooltip = parentPath + "/" + item.name; + // Try to extract full path from URI first (new approach) + if (uri.path && uri.path !== `/${item.name}`) { + const fullPath = uri.path.startsWith("/") + ? uri.path.substring(1) + : uri.path; + // Only use if it looks like a proper path with more than just the filename + if (fullPath.includes("/")) { + tooltip = fullPath; + } + } + + // Fallback: if URI-based approach didn't work, try getting path directly + if (tooltip === item.name) { + try { + const fullPath = await this.model.getPathOfItem(item); + if (fullPath && fullPath !== `/${item.name}` && fullPath.includes("/")) { + tooltip = fullPath; + } + } catch { + // If path retrieval fails, tooltip remains as item.name } - } catch { - // If getting parent path fails, tooltip will remain as item.name } } @@ -752,30 +763,6 @@ class ContentDataProvider } : undefined; } - - private async getItemTooltip(item: ContentItem): Promise { - const parentUri = item.parentFolderUri; - if (!parentUri) { - return ""; - } - - if (!this.parentPathCache.has(parentUri)) { - try { - const fullPath = await this.model.getPathOfItem(item); - if (fullPath) { - const parentPath = - fullPath.substring(0, fullPath.lastIndexOf("/")) || "/"; - this.parentPathCache.set(parentUri, parentPath); - } else { - this.parentPathCache.set(parentUri, ""); - } - } catch { - this.parentPathCache.set(parentUri, ""); - } - } - - return this.parentPathCache.get(parentUri) || ""; - } } export default ContentDataProvider; From 6e8b68cdac411da7ca102b068c6c50922a368566 Mon Sep 17 00:00:00 2001 From: Kishan Patel Date: Fri, 15 Aug 2025 14:34:18 -0400 Subject: [PATCH 07/13] wip(ContentNavigator): Partial working modification --- .../ContentNavigator/ContentDataProvider.ts | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/client/src/components/ContentNavigator/ContentDataProvider.ts b/client/src/components/ContentNavigator/ContentDataProvider.ts index dc5347453..f1e8ffdaf 100644 --- a/client/src/components/ContentNavigator/ContentDataProvider.ts +++ b/client/src/components/ContentNavigator/ContentDataProvider.ts @@ -216,31 +216,12 @@ class ContentDataProvider let tooltip = item.name; const canCopyPath = item.contextValue?.includes("copyPath"); - if (canCopyPath) { - // Try to extract full path from URI first (new approach) - if (uri.path && uri.path !== `/${item.name}`) { - const fullPath = uri.path.startsWith("/") - ? uri.path.substring(1) - : uri.path; - // Only use if it looks like a proper path with more than just the filename - if (fullPath.includes("/")) { - tooltip = fullPath; - } - } - - // Fallback: if URI-based approach didn't work, try getting path directly - if (tooltip === item.name) { - try { - const fullPath = await this.model.getPathOfItem(item); - if (fullPath && fullPath !== `/${item.name}` && fullPath.includes("/")) { - tooltip = fullPath; - } - } catch { - // If path retrieval fails, tooltip remains as item.name - } - } + if (canCopyPath && uri.path && uri.path !== `/${item.name}`) { + const fullPath = uri.path.startsWith("/") + ? uri.path.substring(1) + : uri.path; + tooltip = fullPath; } - return { collapsibleState: isContainer ? TreeItemCollapsibleState.Collapsed From 7a2bd533aac0882c991df9ac221819967dae3f2a Mon Sep 17 00:00:00 2001 From: Kishan Patel Date: Fri, 22 Aug 2025 17:27:05 -0400 Subject: [PATCH 08/13] wip(ContentNavigator) refactored enrichment logic Signed-off-by: Kishan Patel --- .../src/connection/rest/RestContentAdapter.ts | 170 ++++++++++-------- 1 file changed, 92 insertions(+), 78 deletions(-) diff --git a/client/src/connection/rest/RestContentAdapter.ts b/client/src/connection/rest/RestContentAdapter.ts index 49bbfc9bc..b6f0cdcdf 100644 --- a/client/src/connection/rest/RestContentAdapter.ts +++ b/client/src/connection/rest/RestContentAdapter.ts @@ -53,7 +53,6 @@ class RestContentAdapter implements ContentAdapter { }; private contextMenuProvider: ContextMenuProvider; private pathCache: Map = new Map(); - private pathPromiseCache: Map> = new Map(); public constructor() { this.rootFolders = {}; @@ -120,12 +119,7 @@ class RestContentAdapter implements ContentAdapter { } const { data } = await this.connection.get(ancestorsLink.uri); if (data && data.length > 0) { - const enrichedItem = this.enrichWithDataProviderProperties(data[0]); - - if (enrichedItem.contextValue?.includes("copyPath")) { - await this.updateUriWithFullPath(enrichedItem); - } - + const enrichedItem = await this.enrichWithDataProviderProperties(data[0]); return enrichedItem; } } @@ -154,20 +148,16 @@ class RestContentAdapter implements ContentAdapter { result.items.map( async (childItem: ContentItem, index): Promise => { const favoriteUri = fetchFavoriteUri(childItem); - const enrichedItem = { - ...childItem, - uid: `${parentItem.uid}/${index}`, - ...this.enrichWithDataProviderProperties(childItem, { + const enrichedItem = await this.enrichWithDataProviderProperties( + childItem, + { isInRecycleBin, isInMyFavorites: parentIdIsFavoritesFolder || !!favoriteUri, favoriteUri, - }), - }; - - if (enrichedItem.contextValue?.includes("copyPath")) { - await this.updateUriWithFullPath(enrichedItem); - } + }, + ); + enrichedItem.uid = `${parentItem.uid}/${index}`; return enrichedItem; }, ), @@ -202,37 +192,16 @@ class RestContentAdapter implements ContentAdapter { if (!item) { return ""; } - const baseKey = item.uri || item.id || item.name; - const cacheKey = `${baseKey}_${folderPathOnly || false}`; - if (this.pathCache.has(cacheKey)) { - return this.pathCache.get(cacheKey)!; + if (this.pathCache.has(item.id)) { + return this.pathCache.get(item.id)!; } - if (this.pathPromiseCache.has(cacheKey)) { - return this.pathPromiseCache.get(cacheKey)!; - } - - const pathPromise = this.calculatePathOfItem(item, folderPathOnly); - this.pathPromiseCache.set(cacheKey, pathPromise); - try { - const path = await pathPromise; - this.pathCache.set(cacheKey, path); - this.pathPromiseCache.delete(cacheKey); - - if (!folderPathOnly && path.includes("/")) { - const parentPath = path.substring(0, path.lastIndexOf("/")) || "/"; - const parentKey = `${item.parentFolderUri}_true`; - if (!this.pathCache.has(parentKey)) { - this.pathCache.set(parentKey, parentPath); - } - } - + const path = await this.calculatePathOfItem(item, folderPathOnly); + this.pathCache.set(item.id, path); return path; } catch { - this.pathPromiseCache.delete(cacheKey); - this.pathCache.set(cacheKey, ""); return ""; } } @@ -273,7 +242,17 @@ class RestContentAdapter implements ContentAdapter { const updateLink = getLink(item.links, "PUT", "update"); try { const response = await this.connection.put(updateLink.uri, newItemData); - return this.enrichWithDataProviderProperties(response.data).vscUri; + const enrichedItem = await this.enrichWithDataProviderProperties( + response.data, + ); + + // Clear cache for moved item and all its children since their paths changed + await this.clearCacheForItemAndChildren(item); + + // Update cache with new path since item was moved + this.pathCache.set(item.id, await this.calculatePathOfItem(enrichedItem)); + + return enrichedItem.vscUri; // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { return; @@ -359,11 +338,11 @@ class RestContentAdapter implements ContentAdapter { ? { data: ROOT_FOLDER } : await this.connection.get(`/folders/folders/${delegateFolderName}`); - this.rootFolders[delegateFolderName] = { - ...result.data, - uid: `${index}`, - ...this.enrichWithDataProviderProperties(result.data), - }; + const enrichedItem = await this.enrichWithDataProviderProperties( + result.data, + ); + enrichedItem.uid = `${index}`; + this.rootFolders[delegateFolderName] = enrichedItem; } return this.rootFolders; @@ -373,12 +352,9 @@ class RestContentAdapter implements ContentAdapter { const response = await this.connection.get(id); this.updateFileMetadata(id, response); - const enrichedItem = this.enrichWithDataProviderProperties(response.data); - - if (enrichedItem.contextValue?.includes("copyPath")) { - await this.updateUriWithFullPath(enrichedItem); - } - + const enrichedItem = await this.enrichWithDataProviderProperties( + response.data, + ); return enrichedItem; } @@ -417,14 +393,9 @@ class RestContentAdapter implements ContentAdapter { `/folders/folders?parentFolderUri=${parentFolderUri}`, { name: folderName }, ); - const enrichedItem = this.enrichWithDataProviderProperties( + const enrichedItem = await this.enrichWithDataProviderProperties( createFolderResponse.data, ); - - if (enrichedItem.contextValue?.includes("copyPath")) { - await this.updateUriWithFullPath(enrichedItem); - } - return enrichedItem; // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { @@ -432,16 +403,16 @@ class RestContentAdapter implements ContentAdapter { } } - private enrichWithDataProviderProperties( + private async enrichWithDataProviderProperties( item: ContentItem, flags?: ContentItem["flags"], - ): ContentItem { + ): Promise { item.flags = flags; item.permission = getPermission(item); const contextValue = this.contextMenuProvider.availableActions(item); - return { + const enrichedItem = { ...item, contextValue, fileStat: { @@ -456,6 +427,13 @@ class RestContentAdapter implements ContentAdapter { typeName: getTypeName(item), }; + // Update URI with full path if the item supports copyPath context action + if (enrichedItem.contextValue?.includes("copyPath")) { + await this.updateUriWithFullPath(enrichedItem); + } + + return enrichedItem; + function getIsContainer(item: ContentItem): boolean { const typeName = getTypeName(item); if (isItemInRecycleBin(item) && isReference(item)) { @@ -534,13 +512,15 @@ class RestContentAdapter implements ContentAdapter { return await this.getItemOfId(item.uri); } - const enrichedItem = this.enrichWithDataProviderProperties( + const enrichedItem = await this.enrichWithDataProviderProperties( patchResponse.data, ); - if (enrichedItem.contextValue?.includes("copyPath")) { - await this.updateUriWithFullPath(enrichedItem); - } + // Clear cache for renamed item and all its children since their paths changed + await this.clearCacheForItemAndChildren(item); + + // Update cache with new path since item was renamed + this.pathCache.set(item.id, await this.calculatePathOfItem(enrichedItem)); return enrichedItem; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -587,12 +567,8 @@ class RestContentAdapter implements ContentAdapter { return; } - const enrichedItem = this.enrichWithDataProviderProperties(createdResource); - - if (enrichedItem.contextValue?.includes("copyPath")) { - await this.updateUriWithFullPath(enrichedItem); - } - + const enrichedItem = + await this.enrichWithDataProviderProperties(createdResource); return enrichedItem; } @@ -656,11 +632,17 @@ class RestContentAdapter implements ContentAdapter { public async deleteItem(item: ContentItem): Promise { // folder service will return 409 error if the deleting folder has non-folder item even if add recursive parameter // delete the resource or move item to recycle bin will automatically delete the favorites as well. - return await (isContainer(item) + const success = await (isContainer(item) ? this.deleteFolder(item) : this.deleteResource(item)); - } + // Clear cache since item was deleted + if (success) { + await this.clearCacheForItemAndChildren(item); + } + + return success; + } public async addItemToFavorites(item: ContentItem): Promise { return await this.addChildItem( getResourceIdFromItem(item), @@ -702,8 +684,14 @@ class RestContentAdapter implements ContentAdapter { } const success = await this.moveItem(item, recycleBinUri); - return recycleItemResponse(!!success); + // Clear cache since item path changed when moved to recycle bin + // Note: moveItem already clears cache, but being explicit here + if (success) { + await this.clearCacheForItemAndChildren(item); + } + + return recycleItemResponse(!!success); function recycleItemResponse(success: boolean) { if (!success) { return {}; @@ -721,9 +709,17 @@ class RestContentAdapter implements ContentAdapter { if (!previousParentUri) { return false; } - return !!(await this.moveItem(item, previousParentUri)); - } + const success = !!(await this.moveItem(item, previousParentUri)); + + // Clear cache since item path changed when restored + // Note: moveItem already clears cache, but being explicit here + if (success) { + await this.clearCacheForItemAndChildren(item); + } + + return success; + } private async updateAccessToken(): Promise { const session = await authentication.getSession(SASAuthProvider.id, [], { createIfNone: true, @@ -764,6 +760,7 @@ class RestContentAdapter implements ContentAdapter { try { const children = await this.getChildItems(item); await Promise.all(children.map((child) => this.deleteItem(child))); + const deleteRecursivelyLink = getLink( item.links, "DELETE", @@ -869,6 +866,23 @@ class RestContentAdapter implements ContentAdapter { return `${getResourceIdFromItem(myFavoritesFolder)}/members/${favoriteId}`; } + + private async clearCacheForItemAndChildren(item: ContentItem): Promise { + // Clear cache for the item itself + this.pathCache.delete(item.id); + + // If it's a container, clear cache for all children too + if (isContainer(item)) { + try { + const children = await this.getChildItems(item); + for (const child of children) { + await this.clearCacheForItemAndChildren(child); + } + } catch { + // If we can't get children, just continue - the cache entries will be stale but not incorrect + } + } + } } export default RestContentAdapter; From 1545dc92a436db04c0d13b336c5ff7013753741d Mon Sep 17 00:00:00 2001 From: Kishan Patel Date: Wed, 27 Aug 2025 15:07:22 -0400 Subject: [PATCH 09/13] feat(ContentNavigator): improved caching Signed-off-by: Kishan Patel --- .../src/connection/rest/RestContentAdapter.ts | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/client/src/connection/rest/RestContentAdapter.ts b/client/src/connection/rest/RestContentAdapter.ts index b6f0cdcdf..0eb558e0c 100644 --- a/client/src/connection/rest/RestContentAdapter.ts +++ b/client/src/connection/rest/RestContentAdapter.ts @@ -194,6 +194,7 @@ class RestContentAdapter implements ContentAdapter { } if (this.pathCache.has(item.id)) { + console.log(this.pathCache); return this.pathCache.get(item.id)!; } @@ -211,29 +212,55 @@ class RestContentAdapter implements ContentAdapter { folderPathOnly?: boolean, ): Promise { const filePathParts = []; - let currentContentItem: Pick = - item; + let currentContentItem: Pick< + ContentItem, + "parentFolderUri" | "name" | "id" + > = item; if (!folderPathOnly) { filePathParts.push(currentContentItem.name); } do { + if (currentContentItem.parentFolderUri) { + const cachedParentPath = this.pathCache.get( + currentContentItem.parentFolderUri, + ); + if (cachedParentPath) { + const fullPath = + cachedParentPath + "/" + filePathParts.reverse().join("/"); + return fullPath; + } + } try { const { data: parentData } = await this.connection.get( currentContentItem.parentFolderUri, ); currentContentItem = parentData; + + if (currentContentItem.name) { + filePathParts.push(currentContentItem.name); + } } catch { return ""; } - if (currentContentItem.name) { - filePathParts.push(currentContentItem.name); - } } while (currentContentItem.parentFolderUri); - return "/" + filePathParts.reverse().join("/"); - } + const fullPath = "/" + filePathParts.reverse().join("/"); + + // Cache intermediate parent paths for future efficiency + this.cacheIntermediatePaths(item, fullPath); + return fullPath; + } + private cacheIntermediatePaths(item: ContentItem, fullPath: string): void { + // Cache the parent folder URI with its path for sibling efficiency + if (item.parentFolderUri && fullPath.includes("/")) { + const parentPath = fullPath.substring(0, fullPath.lastIndexOf("/")); + if (parentPath && !this.pathCache.has(item.parentFolderUri)) { + this.pathCache.set(item.parentFolderUri, parentPath); + } + } + } public async moveItem( item: ContentItem, parentFolderUri: string, From 79f7ffdf8e6a6df399ff9fe34d29aa3666c4704f Mon Sep 17 00:00:00 2001 From: Kishan Patel Date: Fri, 5 Sep 2025 16:06:42 -0400 Subject: [PATCH 10/13] feat(ContentNavigator): modified logic to allow for server and content support Signed-off-by: Kishan Patel --- .../ContentNavigator/ContentDataProvider.ts | 16 +++++++++++----- client/src/connection/rest/RestContentAdapter.ts | 1 - 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/client/src/components/ContentNavigator/ContentDataProvider.ts b/client/src/components/ContentNavigator/ContentDataProvider.ts index f1e8ffdaf..f2e923884 100644 --- a/client/src/components/ContentNavigator/ContentDataProvider.ts +++ b/client/src/components/ContentNavigator/ContentDataProvider.ts @@ -216,11 +216,17 @@ class ContentDataProvider let tooltip = item.name; const canCopyPath = item.contextValue?.includes("copyPath"); - if (canCopyPath && uri.path && uri.path !== `/${item.name}`) { - const fullPath = uri.path.startsWith("/") - ? uri.path.substring(1) - : uri.path; - tooltip = fullPath; + if (canCopyPath) { + // Use the actual path that gets copied to clipboard, with error handling + try { + const fullPath = await this.getPathOfItem(item); + if (fullPath && fullPath !== item.name) { + tooltip = fullPath; + } + } catch { + // If getting the path fails, just use the item name + tooltip = item.name; + } } return { collapsibleState: isContainer diff --git a/client/src/connection/rest/RestContentAdapter.ts b/client/src/connection/rest/RestContentAdapter.ts index 0eb558e0c..5a12c0a22 100644 --- a/client/src/connection/rest/RestContentAdapter.ts +++ b/client/src/connection/rest/RestContentAdapter.ts @@ -194,7 +194,6 @@ class RestContentAdapter implements ContentAdapter { } if (this.pathCache.has(item.id)) { - console.log(this.pathCache); return this.pathCache.get(item.id)!; } From a66de3ce6bcf8a30c5209392b59544b49c636d80 Mon Sep 17 00:00:00 2001 From: Kishan Patel Date: Mon, 8 Sep 2025 16:51:32 -0400 Subject: [PATCH 11/13] feat(ContentNavigator): removed tooltip from ContentDataProvider Signed-off-by: Kishan Patel --- .../ContentNavigator/ContentDataProvider.ts | 17 +------ client/src/connection/itc/ItcServerAdapter.ts | 12 ++++- .../src/connection/rest/RestServerAdapter.ts | 17 ++++++- client/src/connection/rest/util.ts | 51 +++++++++++++------ 4 files changed, 63 insertions(+), 34 deletions(-) diff --git a/client/src/components/ContentNavigator/ContentDataProvider.ts b/client/src/components/ContentNavigator/ContentDataProvider.ts index f2e923884..3febc6634 100644 --- a/client/src/components/ContentNavigator/ContentDataProvider.ts +++ b/client/src/components/ContentNavigator/ContentDataProvider.ts @@ -213,21 +213,7 @@ class ContentDataProvider public async getTreeItem(item: ContentItem): Promise { const isContainer = getIsContainer(item); const uri = await this.model.getUri(item, false); - let tooltip = item.name; - const canCopyPath = item.contextValue?.includes("copyPath"); - - if (canCopyPath) { - // Use the actual path that gets copied to clipboard, with error handling - try { - const fullPath = await this.getPathOfItem(item); - if (fullPath && fullPath !== item.name) { - tooltip = fullPath; - } - } catch { - // If getting the path fails, just use the item name - tooltip = item.name; - } - } + return { collapsibleState: isContainer ? TreeItemCollapsibleState.Collapsed @@ -244,7 +230,6 @@ class ContentDataProvider id: item.uid, label: item.name, resourceUri: uri, - tooltip: tooltip, }; } diff --git a/client/src/connection/itc/ItcServerAdapter.ts b/client/src/connection/itc/ItcServerAdapter.ts index 424708ca3..f681153d7 100644 --- a/client/src/connection/itc/ItcServerAdapter.ts +++ b/client/src/connection/itc/ItcServerAdapter.ts @@ -402,11 +402,21 @@ class ItcServerAdapter implements ContentAdapter { }, }; - return { + const enrichedItem = { ...item, contextValue: this.contextMenuProvider.availableActions(item), vscUri: getSasServerUri(item, false), }; + + // Update URI with full path if the item supports copyPath context action + if (enrichedItem.contextValue?.includes("copyPath")) { + const fullPath = enrichedItem.id; // For ITC server, the id is already the full path + if (fullPath && fullPath !== enrichedItem.name) { + enrichedItem.vscUri = getSasServerUri(enrichedItem, false, fullPath); + } + } + + return enrichedItem; } private async execute(incomingCode: string, params: Record) { diff --git a/client/src/connection/rest/RestServerAdapter.ts b/client/src/connection/rest/RestServerAdapter.ts index 992d1dab7..73a95ac9f 100644 --- a/client/src/connection/rest/RestServerAdapter.ts +++ b/client/src/connection/rest/RestServerAdapter.ts @@ -551,7 +551,7 @@ class RestServerAdapter implements ContentAdapter { const typeName = getTypeName(item); - return { + const enrichedItem = { ...item, contextValue: this.contextMenuProvider.availableActions(item), fileStat: { @@ -570,6 +570,21 @@ class RestServerAdapter implements ContentAdapter { vscUri: getSasServerUri(item, flags?.isInRecycleBin || false), typeName: getTypeName(item), }; + + // Update URI with full path if the item supports copyPath context action + // For server items, the path is just the trimmed compute prefix of the id + if (enrichedItem.contextValue?.includes("copyPath")) { + const fullPath = this.trimComputePrefix(enrichedItem.id); + if (fullPath && fullPath !== enrichedItem.name) { + enrichedItem.vscUri = getSasServerUri( + enrichedItem, + flags?.isInRecycleBin || false, + fullPath, + ); + } + } + + return enrichedItem; } private trimComputePrefix(uri: string): string { diff --git a/client/src/connection/rest/util.ts b/client/src/connection/rest/util.ts index 96b59395d..16fa5eda1 100644 --- a/client/src/connection/rest/util.ts +++ b/client/src/connection/rest/util.ts @@ -42,25 +42,44 @@ export const getSasContentUri = ( item: ContentItem, readOnly?: boolean, fullPath?: string, -): Uri => - Uri.parse( - `${readOnly ? `${ContentSourceType.SASContent}ReadOnly` : ContentSourceType.SASContent}:/${ - fullPath - ? fullPath.replace(/#/g, "%23").replace(/\?/g, "%3F") - : item.name - ? item.name.replace(/#/g, "%23").replace(/\?/g, "%3F") - : item.name - }?id=${getResourceIdFromItem(item)}`, +): Uri => { + let pathToUse = fullPath || item.name || ""; + + // Remove leading slash if present since we'll add one in the URI construction + if (pathToUse.startsWith("/")) { + pathToUse = pathToUse.substring(1); + } + + // URL encode special characters + pathToUse = pathToUse.replace(/#/g, "%23").replace(/\?/g, "%3F"); + + return Uri.parse( + `${readOnly ? `${ContentSourceType.SASContent}ReadOnly` : ContentSourceType.SASContent}:/${pathToUse}?id=${getResourceIdFromItem(item)}`, ); +}; + +export const getSasServerUri = ( + item: ContentItem, + readOnly?: boolean, + fullPath?: string, +): Uri => { + let pathToUse = fullPath || item.name || ""; -export const getSasServerUri = (item: ContentItem, readOnly?: boolean): Uri => - Uri.parse( - `${readOnly ? `${ContentSourceType.SASServer}ReadOnly` : ContentSourceType.SASServer}:/${ - item.name - ? item.name.replace(/#/g, "%23").replace(/\?/g, "%3F") - : item.name - }?id=${getResourceIdFromItem(item)}`, + // Decode SAS server path encoding (~fs~ represents /) + pathToUse = pathToUse.replace(/~fs~/g, "/"); + + // Remove leading slash if present since we'll add one in the URI construction + if (pathToUse.startsWith("/")) { + pathToUse = pathToUse.substring(1); + } + + // URL encode special characters + pathToUse = pathToUse.replace(/#/g, "%23").replace(/\?/g, "%3F"); + + return Uri.parse( + `${readOnly ? `${ContentSourceType.SASServer}ReadOnly` : ContentSourceType.SASServer}:/${pathToUse}?id=${getResourceIdFromItem(item)}`, ); +}; export const getPermission = (item: ContentItem): Permission => { const itemType = getTypeName(item); From 68edf9f5987b3bc6cadb8cd8e8d137783b7f85ff Mon Sep 17 00:00:00 2001 From: Kishan Patel Date: Tue, 9 Sep 2025 10:38:05 -0400 Subject: [PATCH 12/13] feat(ContentNavigator): fixed drag and drop folder path Signed-off-by: Kishan Patel --- .../src/connection/rest/RestContentAdapter.ts | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/client/src/connection/rest/RestContentAdapter.ts b/client/src/connection/rest/RestContentAdapter.ts index 5a12c0a22..deef68ffb 100644 --- a/client/src/connection/rest/RestContentAdapter.ts +++ b/client/src/connection/rest/RestContentAdapter.ts @@ -193,13 +193,16 @@ class RestContentAdapter implements ContentAdapter { return ""; } - if (this.pathCache.has(item.id)) { - return this.pathCache.get(item.id)!; + // Use different cache keys for folder paths vs full paths + const cacheKey = folderPathOnly ? `${item.id}_folder` : item.id; + + if (this.pathCache.has(cacheKey)) { + return this.pathCache.get(cacheKey)!; } try { const path = await this.calculatePathOfItem(item, folderPathOnly); - this.pathCache.set(item.id, path); + this.pathCache.set(cacheKey, path); return path; } catch { return ""; @@ -210,14 +213,34 @@ class RestContentAdapter implements ContentAdapter { item: ContentItem, folderPathOnly?: boolean, ): Promise { + // If folderPathOnly=true, we want the path of the parent folder + if (folderPathOnly) { + if (!item.parentFolderUri) { + return ""; + } + + const cachedParentPath = this.pathCache.get(item.parentFolderUri); + if (cachedParentPath) { + return cachedParentPath; + } + + try { + const { data: parentData } = await this.connection.get( + item.parentFolderUri, + ); + const parentPath = await this.calculatePathOfItem(parentData, false); // Get parent's full path + this.pathCache.set(item.parentFolderUri, parentPath); + return parentPath; + } catch { + return ""; + } + } const filePathParts = []; let currentContentItem: Pick< ContentItem, "parentFolderUri" | "name" | "id" > = item; - if (!folderPathOnly) { - filePathParts.push(currentContentItem.name); - } + filePathParts.push(currentContentItem.name); do { if (currentContentItem.parentFolderUri) { From 2a15b080165368210566c0b8cfdd6411371bae89 Mon Sep 17 00:00:00 2001 From: Kishan Patel Date: Mon, 27 Oct 2025 12:48:41 -0600 Subject: [PATCH 13/13] feat(ContentNavigator): added process path logic Signed-off-by: Kishan Patel --- .../src/connection/rest/RestContentAdapter.ts | 5 +-- client/src/connection/rest/util.ts | 33 +++++++++---------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/client/src/connection/rest/RestContentAdapter.ts b/client/src/connection/rest/RestContentAdapter.ts index deef68ffb..f8296fe34 100644 --- a/client/src/connection/rest/RestContentAdapter.ts +++ b/client/src/connection/rest/RestContentAdapter.ts @@ -236,10 +236,7 @@ class RestContentAdapter implements ContentAdapter { } } const filePathParts = []; - let currentContentItem: Pick< - ContentItem, - "parentFolderUri" | "name" | "id" - > = item; + let currentContentItem: ContentItem = item; filePathParts.push(currentContentItem.name); do { diff --git a/client/src/connection/rest/util.ts b/client/src/connection/rest/util.ts index 16fa5eda1..80fa5e417 100644 --- a/client/src/connection/rest/util.ts +++ b/client/src/connection/rest/util.ts @@ -38,12 +38,10 @@ export const getResourceIdFromItem = (item: ContentItem): string | null => { return getLink(item.links, "GET", "self")?.uri || null; }; -export const getSasContentUri = ( - item: ContentItem, - readOnly?: boolean, - fullPath?: string, -): Uri => { - let pathToUse = fullPath || item.name || ""; +const processPath = (path: string, replacement?: RegExp): string => { + let pathToUse = path; + + pathToUse = replacement ? pathToUse.replace(replacement, "/") : pathToUse; // Remove leading slash if present since we'll add one in the URI construction if (pathToUse.startsWith("/")) { @@ -53,6 +51,16 @@ export const getSasContentUri = ( // URL encode special characters pathToUse = pathToUse.replace(/#/g, "%23").replace(/\?/g, "%3F"); + return pathToUse; +}; + +export const getSasContentUri = ( + item: ContentItem, + readOnly?: boolean, + fullPath?: string, +): Uri => { + const pathToUse = processPath(fullPath || item.name || ""); + return Uri.parse( `${readOnly ? `${ContentSourceType.SASContent}ReadOnly` : ContentSourceType.SASContent}:/${pathToUse}?id=${getResourceIdFromItem(item)}`, ); @@ -63,18 +71,7 @@ export const getSasServerUri = ( readOnly?: boolean, fullPath?: string, ): Uri => { - let pathToUse = fullPath || item.name || ""; - - // Decode SAS server path encoding (~fs~ represents /) - pathToUse = pathToUse.replace(/~fs~/g, "/"); - - // Remove leading slash if present since we'll add one in the URI construction - if (pathToUse.startsWith("/")) { - pathToUse = pathToUse.substring(1); - } - - // URL encode special characters - pathToUse = pathToUse.replace(/#/g, "%23").replace(/\?/g, "%3F"); + const pathToUse = processPath(fullPath || item.name || "", /~fs~/g); return Uri.parse( `${readOnly ? `${ContentSourceType.SASServer}ReadOnly` : ContentSourceType.SASServer}:/${pathToUse}?id=${getResourceIdFromItem(item)}`,