diff --git a/client/src/components/ContentNavigator/ContentDataProvider.ts b/client/src/components/ContentNavigator/ContentDataProvider.ts index 3febc6634..785a7a2fb 100644 --- a/client/src/components/ContentNavigator/ContentDataProvider.ts +++ b/client/src/components/ContentNavigator/ContentDataProvider.ts @@ -36,6 +36,7 @@ import { basename, join } from "path"; import { promisify } from "util"; import { profileConfig } from "../../commands/profile"; +import { getResourceId } from "../../connection/rest/util"; import { SubscriptionProvider } from "../SubscriptionProvider"; import { ViyaProfile } from "../profile"; import { ContentModel } from "./ContentModel"; @@ -79,6 +80,8 @@ class ContentDataProvider public dropMimeTypes: string[]; public dragMimeTypes: string[]; + private uriToParentMap = new Map(); + get treeView(): TreeView { return this._treeView; } @@ -214,6 +217,11 @@ class ContentDataProvider const isContainer = getIsContainer(item); const uri = await this.model.getUri(item, false); + // Cache the URI to parent mapping + if (item.parentFolderUri) { + this.uriToParentMap.set(item.uri, item.parentFolderUri); + } + return { collapsibleState: isContainer ? TreeItemCollapsibleState.Collapsed @@ -505,6 +513,94 @@ class ContentDataProvider } } + public async checkFolderDirty(resource: ContentItem): Promise { + if (!resource.vscUri) { + return false; + } + + const targetFolderUri = resource.uri; + + const dirtyFiles = workspace.textDocuments + .filter((doc) => { + if (!doc.isDirty) { + return false; + } + + const scheme = doc.uri.scheme; + return ( + scheme === "sasContent" || + scheme === "sasServer" || + scheme === "sasContentReadOnly" || + scheme === "sasServerReadOnly" + ); + }) + .map((doc) => doc.uri); + + if (dirtyFiles.length === 0) { + return false; + } + + for (const dirtyFileUri of dirtyFiles) { + if ( + await this.isDescendantOf(getResourceId(dirtyFileUri), targetFolderUri) + ) { + return true; + } + } + + return false; + } + + private async isDescendantOf( + fileUri: string, + ancestorFolderUri: string, + ): Promise { + let currentParentUri = this.uriToParentMap.get(fileUri); + + if (!currentParentUri) { + try { + const item = await this.model.getResourceByUri(Uri.parse(fileUri)); + currentParentUri = item.parentFolderUri; + if (currentParentUri) { + this.uriToParentMap.set(fileUri, currentParentUri); + } + } catch { + return false; + } + } + + let depth = 0; + while (currentParentUri || depth >= 10) { + if (currentParentUri === ancestorFolderUri) { + return true; + } + + const nextParentUri = this.uriToParentMap.get(currentParentUri); + + if (nextParentUri) { + currentParentUri = nextParentUri; + } else { + try { + const parentItem = await this.model.getResourceByUri( + Uri.parse(currentParentUri), + ); + currentParentUri = parentItem.parentFolderUri; + if (currentParentUri) { + this.uriToParentMap.set( + currentParentUri, + parentItem.parentFolderUri, + ); + } + } catch { + break; + } + } + depth++; + } + + return false; + } + public async downloadContentItems( folderUri: Uri, selections: ContentItem[], @@ -759,6 +855,5 @@ const closeFileIfOpen = (item: ContentItem): Promise | boolean => { .catch(reject); }); } - return true; }; diff --git a/client/src/components/ContentNavigator/const.ts b/client/src/components/ContentNavigator/const.ts index f448575bf..906b40907 100644 --- a/client/src/components/ContentNavigator/const.ts +++ b/client/src/components/ContentNavigator/const.ts @@ -62,9 +62,13 @@ export const Messages = { AddFileToMyFolderSuccess: l10n.t("File added to my folder."), AddToFavoritesError: l10n.t("The item could not be added to My Favorites."), DeleteButtonLabel: l10n.t("Delete"), + MoveToRecycleBinLabel: l10n.t("Move to Recycle Bin"), DeleteWarningMessage: l10n.t( 'Are you sure you want to permanently delete the item "{name}"?', ), + RecycleDirtyFolderWarning: l10n.t( + "This folder contains unsaved files, are you sure you want to delete?", + ), EmptyRecycleBinError: l10n.t("Unable to empty the recycle bin."), EmptyRecycleBinWarningMessage: l10n.t( "Are you sure you want to permanently delete all the items? You cannot undo this action.", diff --git a/client/src/components/ContentNavigator/index.ts b/client/src/components/ContentNavigator/index.ts index 01f1b7ea6..f31adec4f 100644 --- a/client/src/components/ContentNavigator/index.ts +++ b/client/src/components/ContentNavigator/index.ts @@ -107,30 +107,47 @@ class ContentNavigator implements SubscriptionProvider { return; } const isContainer = getIsContainer(resource); + const hasUnsavedFiles = isContainer + ? await this.contentDataProvider.checkFolderDirty(resource) + : isContainer; const moveToRecycleBin = this.contentDataProvider.canRecycleResource(resource); - if ( - !moveToRecycleBin && - !(await window.showWarningMessage( - l10n.t(Messages.DeleteWarningMessage, { - name: resource.name, - }), - { modal: true }, - Messages.DeleteButtonLabel, - )) - ) { - return; - } - const deleteResult = moveToRecycleBin - ? await this.contentDataProvider.recycleResource(resource) - : await this.contentDataProvider.deleteResource(resource); - if (!deleteResult) { - window.showErrorMessage( - isContainer - ? Messages.FolderDeletionError - : Messages.FileDeletionError, - ); + if (resource.contextValue.includes("delete")) { + if ( + !moveToRecycleBin && + !(await window.showWarningMessage( + l10n.t(Messages.DeleteWarningMessage, { + name: resource.name, + }), + { modal: true }, + Messages.DeleteButtonLabel, + )) + ) { + return; + } else if (moveToRecycleBin && hasUnsavedFiles) { + if ( + !(await window.showWarningMessage( + l10n.t(Messages.RecycleDirtyFolderWarning, { + name: resource.name, + }), + { modal: true }, + Messages.MoveToRecycleBinLabel, + )) + ) { + return; + } + } + const deleteResult = moveToRecycleBin + ? await this.contentDataProvider.recycleResource(resource) + : await this.contentDataProvider.deleteResource(resource); + if (!deleteResult) { + window.showErrorMessage( + isContainer + ? Messages.FolderDeletionError + : Messages.FileDeletionError, + ); + } } }, ); @@ -523,5 +540,4 @@ class ContentNavigator implements SubscriptionProvider { } } } - export default ContentNavigator;