diff --git a/app/ide-desktop/lib/dashboard/src/components/dashboard/AssetRow.tsx b/app/ide-desktop/lib/dashboard/src/components/dashboard/AssetRow.tsx index 61afc1b7fd36..fd824cc4e869 100644 --- a/app/ide-desktop/lib/dashboard/src/components/dashboard/AssetRow.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/dashboard/AssetRow.tsx @@ -233,51 +233,54 @@ export default function AssetRow(props: AssetRowProps) { /* should never change */ setIsAssetPanelTemporarilyVisible, ]) - const doDelete = React.useCallback(async () => { - setInsertionVisibility(Visibility.hidden) - if (asset.type === backendModule.AssetType.directory) { - dispatchAssetListEvent({ - type: AssetListEventType.closeFolder, - id: asset.id, - // This is SAFE, as this asset is already known to be a directory. - // eslint-disable-next-line no-restricted-syntax - key: item.key as backendModule.DirectoryId, - }) - } - try { - dispatchAssetListEvent({ type: AssetListEventType.willDelete, key: item.key }) - if ( - asset.type === backendModule.AssetType.project && - backend.type === backendModule.BackendType.local - ) { + const doDelete = React.useCallback( + async (forever = false) => { + setInsertionVisibility(Visibility.hidden) + if (asset.type === backendModule.AssetType.directory) { + dispatchAssetListEvent({ + type: AssetListEventType.closeFolder, + id: asset.id, + // This is SAFE, as this asset is already known to be a directory. + // eslint-disable-next-line no-restricted-syntax + key: item.key as backendModule.DirectoryId, + }) + } + try { + dispatchAssetListEvent({ type: AssetListEventType.willDelete, key: item.key }) if ( - asset.projectState.type !== backendModule.ProjectState.placeholder && - asset.projectState.type !== backendModule.ProjectState.closed + asset.type === backendModule.AssetType.project && + backend.type === backendModule.BackendType.local ) { - await backend.openProject(asset.id, null, asset.title) - } - try { - await backend.closeProject(asset.id, asset.title) - } catch { - // Ignored. The project was already closed. + if ( + asset.projectState.type !== backendModule.ProjectState.placeholder && + asset.projectState.type !== backendModule.ProjectState.closed + ) { + await backend.openProject(asset.id, null, asset.title) + } + try { + await backend.closeProject(asset.id, asset.title) + } catch { + // Ignored. The project was already closed. + } } + await backend.deleteAsset(asset.id, forever, asset.title) + dispatchAssetListEvent({ type: AssetListEventType.delete, key: item.key }) + } catch (error) { + setInsertionVisibility(Visibility.visible) + toastAndLog( + errorModule.tryGetMessage(error)?.slice(0, -1) ?? + `Could not delete ${backendModule.ASSET_TYPE_NAME[asset.type]}` + ) } - await backend.deleteAsset(asset.id, asset.title) - dispatchAssetListEvent({ type: AssetListEventType.delete, key: item.key }) - } catch (error) { - setInsertionVisibility(Visibility.visible) - toastAndLog( - errorModule.tryGetMessage(error)?.slice(0, -1) ?? - `Could not delete ${backendModule.ASSET_TYPE_NAME[asset.type]}` - ) - } - }, [ - backend, - dispatchAssetListEvent, - asset, - /* should never change */ item.key, - /* should never change */ toastAndLog, - ]) + }, + [ + backend, + dispatchAssetListEvent, + asset, + /* should never change */ item.key, + /* should never change */ toastAndLog, + ] + ) const doRestore = React.useCallback(async () => { // Visually, the asset is deleted from the Trash view. @@ -337,7 +340,13 @@ export default function AssetRow(props: AssetRowProps) { } case AssetEventType.delete: { if (event.ids.has(item.key)) { - await doDelete() + await doDelete(false) + } + break + } + case AssetEventType.deleteForever: { + if (event.ids.has(item.key)) { + await doDelete(true) } break } diff --git a/app/ide-desktop/lib/dashboard/src/components/dashboard/DataLinkNameColumn.tsx b/app/ide-desktop/lib/dashboard/src/components/dashboard/DataLinkNameColumn.tsx index 437142c4e8e8..5828d70ad747 100644 --- a/app/ide-desktop/lib/dashboard/src/components/dashboard/DataLinkNameColumn.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/dashboard/DataLinkNameColumn.tsx @@ -67,6 +67,7 @@ export default function DataLinkNameColumn(props: DataLinkNameColumnProps) { case AssetEventType.cancelCut: case AssetEventType.move: case AssetEventType.delete: + case AssetEventType.deleteForever: case AssetEventType.restore: case AssetEventType.download: case AssetEventType.downloadSelected: @@ -77,8 +78,8 @@ export default function DataLinkNameColumn(props: DataLinkNameColumnProps) { case AssetEventType.removeLabels: case AssetEventType.deleteLabel: { // Ignored. These events should all be unrelated to secrets. - // `deleteMultiple`, `restoreMultiple`, `download`, - // and `downloadSelected` are handled by `AssetRow`. + // `delete`, `deleteForever`, `restoreMultiple`, `download`, and `downloadSelected` + // are handled by `AssetRow`. break } case AssetEventType.newDataLink: { diff --git a/app/ide-desktop/lib/dashboard/src/components/dashboard/DirectoryNameColumn.tsx b/app/ide-desktop/lib/dashboard/src/components/dashboard/DirectoryNameColumn.tsx index 750345f26150..d73c39900e69 100644 --- a/app/ide-desktop/lib/dashboard/src/components/dashboard/DirectoryNameColumn.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/dashboard/DirectoryNameColumn.tsx @@ -81,6 +81,7 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) { case AssetEventType.cancelCut: case AssetEventType.move: case AssetEventType.delete: + case AssetEventType.deleteForever: case AssetEventType.restore: case AssetEventType.download: case AssetEventType.downloadSelected: @@ -91,8 +92,8 @@ export default function DirectoryNameColumn(props: DirectoryNameColumnProps) { case AssetEventType.removeLabels: case AssetEventType.deleteLabel: { // Ignored. These events should all be unrelated to directories. - // `deleteMultiple`, `restoreMultiple`, `download`, - // and `downloadSelected` are handled by `AssetRow`. + // `delete`, `deleteForever`, `restore`, `download`, and `downloadSelected` + // are handled by`AssetRow`. break } case AssetEventType.newFolder: { diff --git a/app/ide-desktop/lib/dashboard/src/components/dashboard/FileNameColumn.tsx b/app/ide-desktop/lib/dashboard/src/components/dashboard/FileNameColumn.tsx index b223fb913f0f..5fd835d49ff3 100644 --- a/app/ide-desktop/lib/dashboard/src/components/dashboard/FileNameColumn.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/dashboard/FileNameColumn.tsx @@ -66,6 +66,7 @@ export default function FileNameColumn(props: FileNameColumnProps) { case AssetEventType.cancelCut: case AssetEventType.move: case AssetEventType.delete: + case AssetEventType.deleteForever: case AssetEventType.restore: case AssetEventType.download: case AssetEventType.downloadSelected: @@ -76,7 +77,7 @@ export default function FileNameColumn(props: FileNameColumnProps) { case AssetEventType.removeLabels: case AssetEventType.deleteLabel: { // Ignored. These events should all be unrelated to projects. - // `deleteMultiple`, `restoreMultiple`, `download`, and `downloadSelected` + // `delete`, `deleteForever`, `restoreMultiple`, `download`, and `downloadSelected` // are handled by `AssetRow`. break } diff --git a/app/ide-desktop/lib/dashboard/src/components/dashboard/ProjectIcon.tsx b/app/ide-desktop/lib/dashboard/src/components/dashboard/ProjectIcon.tsx index 7339198a7031..dc38eb40d127 100644 --- a/app/ide-desktop/lib/dashboard/src/components/dashboard/ProjectIcon.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/dashboard/ProjectIcon.tsx @@ -234,6 +234,7 @@ export default function ProjectIcon(props: ProjectIconProps) { case AssetEventType.cancelCut: case AssetEventType.move: case AssetEventType.delete: + case AssetEventType.deleteForever: case AssetEventType.restore: case AssetEventType.download: case AssetEventType.downloadSelected: @@ -243,9 +244,9 @@ export default function ProjectIcon(props: ProjectIconProps) { case AssetEventType.addLabels: case AssetEventType.removeLabels: case AssetEventType.deleteLabel: { - // Ignored. Any missing project-related events should be handled by - // `ProjectNameColumn`. `deleteMultiple`, `restoreMultiple`, `download`, - // and `downloadSelected` are handled by `AssetRow`. + // Ignored. Any missing project-related events should be handled by `ProjectNameColumn`. + // `delete`, `deleteForever`, `restore`, `download`, and `downloadSelected` + // are handled by`AssetRow`. break } case AssetEventType.openProject: { diff --git a/app/ide-desktop/lib/dashboard/src/components/dashboard/ProjectNameColumn.tsx b/app/ide-desktop/lib/dashboard/src/components/dashboard/ProjectNameColumn.tsx index ed40bb6723f4..415d2e891ea3 100644 --- a/app/ide-desktop/lib/dashboard/src/components/dashboard/ProjectNameColumn.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/dashboard/ProjectNameColumn.tsx @@ -102,6 +102,7 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) { case AssetEventType.cancelCut: case AssetEventType.move: case AssetEventType.delete: + case AssetEventType.deleteForever: case AssetEventType.restore: case AssetEventType.download: case AssetEventType.downloadSelected: @@ -112,8 +113,8 @@ export default function ProjectNameColumn(props: ProjectNameColumnProps) { case AssetEventType.removeLabels: case AssetEventType.deleteLabel: { // Ignored. Any missing project-related events should be handled by `ProjectIcon`. - // `deleteMultiple`, `restoreMultiple`, `download`, and `downloadSelected` - // are handled by `AssetRow`. + // `delete`, `deleteForever`, `restore`, `download`, and `downloadSelected` + // are handled by`AssetRow`. break } case AssetEventType.newProject: { diff --git a/app/ide-desktop/lib/dashboard/src/components/dashboard/SecretNameColumn.tsx b/app/ide-desktop/lib/dashboard/src/components/dashboard/SecretNameColumn.tsx index d8dbc70be74c..a3ae12c0f552 100644 --- a/app/ide-desktop/lib/dashboard/src/components/dashboard/SecretNameColumn.tsx +++ b/app/ide-desktop/lib/dashboard/src/components/dashboard/SecretNameColumn.tsx @@ -64,6 +64,7 @@ export default function SecretNameColumn(props: SecretNameColumnProps) { case AssetEventType.cancelCut: case AssetEventType.move: case AssetEventType.delete: + case AssetEventType.deleteForever: case AssetEventType.restore: case AssetEventType.download: case AssetEventType.downloadSelected: @@ -74,8 +75,8 @@ export default function SecretNameColumn(props: SecretNameColumnProps) { case AssetEventType.removeLabels: case AssetEventType.deleteLabel: { // Ignored. These events should all be unrelated to secrets. - // `deleteMultiple`, `restoreMultiple`, `download`, - // and `downloadSelected` are handled by `AssetRow`. + // `delete`, `deleteForever`, `restore`, `download`, and `downloadSelected` + // are handled by`AssetRow`. break } case AssetEventType.newSecret: { diff --git a/app/ide-desktop/lib/dashboard/src/events/AssetEventType.ts b/app/ide-desktop/lib/dashboard/src/events/AssetEventType.ts index 96e120bc9fee..7f22c93a1c5f 100644 --- a/app/ide-desktop/lib/dashboard/src/events/AssetEventType.ts +++ b/app/ide-desktop/lib/dashboard/src/events/AssetEventType.ts @@ -19,6 +19,7 @@ enum AssetEventType { cancelCut = 'cancel-cut', move = 'move', delete = 'delete', + deleteForever = 'delete-forever', restore = 'restore', download = 'download', downloadSelected = 'download-selected', diff --git a/app/ide-desktop/lib/dashboard/src/events/AssetListEventType.ts b/app/ide-desktop/lib/dashboard/src/events/AssetListEventType.ts index c3a7764f989a..e9d0f874ab62 100644 --- a/app/ide-desktop/lib/dashboard/src/events/AssetListEventType.ts +++ b/app/ide-desktop/lib/dashboard/src/events/AssetListEventType.ts @@ -13,6 +13,7 @@ enum AssetListEventType { move = 'move', willDelete = 'will-delete', delete = 'delete', + emptyTrash = 'empty-trash', removeSelf = 'remove-self', } diff --git a/app/ide-desktop/lib/dashboard/src/events/assetEvent.ts b/app/ide-desktop/lib/dashboard/src/events/assetEvent.ts index 195a1d61a11c..24185ba8cc67 100644 --- a/app/ide-desktop/lib/dashboard/src/events/assetEvent.ts +++ b/app/ide-desktop/lib/dashboard/src/events/assetEvent.ts @@ -38,6 +38,7 @@ interface AssetEvents { readonly cancelCut: AssetCancelCutEvent readonly move: AssetMoveEvent readonly delete: AssetDeleteEvent + readonly deleteForever: AssetDeleteForeverEvent readonly restore: AssetRestoreEvent readonly download: AssetDownloadEvent readonly downloadSelected: AssetDownloadSelectedEvent @@ -134,6 +135,11 @@ export interface AssetDeleteEvent extends AssetBaseEvent readonly ids: ReadonlySet } +/** A signal to delete assets forever. */ +export interface AssetDeleteForeverEvent extends AssetBaseEvent { + readonly ids: ReadonlySet +} + /** A signal to restore assets from trash. */ export interface AssetRestoreEvent extends AssetBaseEvent { readonly ids: ReadonlySet diff --git a/app/ide-desktop/lib/dashboard/src/events/assetListEvent.ts b/app/ide-desktop/lib/dashboard/src/events/assetListEvent.ts index 41de76f11c83..4262121ff3f0 100644 --- a/app/ide-desktop/lib/dashboard/src/events/assetListEvent.ts +++ b/app/ide-desktop/lib/dashboard/src/events/assetListEvent.ts @@ -36,6 +36,7 @@ interface AssetListEvents { readonly move: AssetListMoveEvent readonly willDelete: AssetListWillDeleteEvent readonly delete: AssetListDeleteEvent + readonly emptyTrash: AssetListEmptyTrashEvent readonly removeSelf: AssetListRemoveSelfEvent } @@ -127,6 +128,9 @@ interface AssetListDeleteEvent extends AssetListBaseEvent {} + /** A signal for a file to remove itself from the asset list, without being deleted. */ interface AssetListRemoveSelfEvent extends AssetListBaseEvent { readonly id: backend.AssetId diff --git a/app/ide-desktop/lib/dashboard/src/layouts/AssetContextMenu.tsx b/app/ide-desktop/lib/dashboard/src/layouts/AssetContextMenu.tsx index 975e6c0b72b7..8cd2e9bebeff 100644 --- a/app/ide-desktop/lib/dashboard/src/layouts/AssetContextMenu.tsx +++ b/app/ide-desktop/lib/dashboard/src/layouts/AssetContextMenu.tsx @@ -98,6 +98,7 @@ export default function AssetContextMenu(props: AssetContextMenuProps) { }, [/* should never change */ setItem] ) + return category === Category.trash ? ( !ownsThisAsset ? null : ( ) diff --git a/app/ide-desktop/lib/dashboard/src/layouts/Drive.tsx b/app/ide-desktop/lib/dashboard/src/layouts/Drive.tsx index f961b7f8c5cf..c8d5afaafe91 100644 --- a/app/ide-desktop/lib/dashboard/src/layouts/Drive.tsx +++ b/app/ide-desktop/lib/dashboard/src/layouts/Drive.tsx @@ -185,6 +185,10 @@ export default function Drive(props: DriveProps) { [backend, user, rootDirectoryId, toastAndLog, /* should never change */ dispatchAssetListEvent] ) + const doEmptyTrash = React.useCallback(() => { + dispatchAssetListEvent({ type: AssetListEventType.emptyTrash }) + }, [/* should never change */ dispatchAssetListEvent]) + const doCreateProject = React.useCallback( ( templateId: string | null = null, @@ -363,6 +367,7 @@ export default function Drive(props: DriveProps) { void readonly doCreateProject: () => void readonly doCreateDirectory: () => void readonly doCreateSecret: (name: string, value: string) => void @@ -45,7 +47,7 @@ export interface DriveBarProps { /** Displays the current directory path and permissions, upload and download buttons, * and a column display mode switcher. */ export default function DriveBar(props: DriveBarProps) { - const { category, canDownloadFiles, doCreateProject, doCreateDirectory } = props + const { category, canDownloadFiles, doEmptyTrash, doCreateProject, doCreateDirectory } = props const { doCreateSecret, doCreateDataLink, doUploadFiles, dispatchAssetEvent } = props const { backend } = backendProvider.useBackend() const { setModal, unsetModal } = modalProvider.useSetModal() @@ -72,7 +74,23 @@ export default function DriveBar(props: DriveBarProps) { }) }, [backend.type, doCreateDirectory, doCreateProject, /* should never change */ inputBindings]) - return ( + return category === Category.trash ? ( +
+
+ +
+
+ ) : (