diff --git a/client/src/components/ContentNavigator/ContentDataProvider.ts b/client/src/components/ContentNavigator/ContentDataProvider.ts index 3febc6634..f1e8ffdaf 100644 --- a/client/src/components/ContentNavigator/ContentDataProvider.ts +++ b/client/src/components/ContentNavigator/ContentDataProvider.ts @@ -213,7 +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; + 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; + } return { collapsibleState: isContainer ? TreeItemCollapsibleState.Collapsed @@ -230,6 +238,7 @@ class ContentDataProvider id: item.uid, label: item.name, resourceUri: uri, + tooltip: tooltip, }; } 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)}`, );