Skip to content

Commit

Permalink
Hard delete of items in Trash (#9091)
Browse files Browse the repository at this point in the history
- Close enso-org/cloud-v2#698
- Requires enso-org/cloud-v2#905

# Important Notes
None
  • Loading branch information
somebody1234 authored Mar 6, 2024
1 parent 1a76f63 commit 618080b
Show file tree
Hide file tree
Showing 20 changed files with 171 additions and 85 deletions.
93 changes: 51 additions & 42 deletions app/ide-desktop/lib/dashboard/src/components/dashboard/AssetRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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: {
Expand Down
1 change: 1 addition & 0 deletions app/ide-desktop/lib/dashboard/src/events/AssetEventType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ enum AssetEventType {
cancelCut = 'cancel-cut',
move = 'move',
delete = 'delete',
deleteForever = 'delete-forever',
restore = 'restore',
download = 'download',
downloadSelected = 'download-selected',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ enum AssetListEventType {
move = 'move',
willDelete = 'will-delete',
delete = 'delete',
emptyTrash = 'empty-trash',
removeSelf = 'remove-self',
}

Expand Down
6 changes: 6 additions & 0 deletions app/ide-desktop/lib/dashboard/src/events/assetEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -134,6 +135,11 @@ export interface AssetDeleteEvent extends AssetBaseEvent<AssetEventType.delete>
readonly ids: ReadonlySet<backendModule.AssetId>
}

/** A signal to delete assets forever. */
export interface AssetDeleteForeverEvent extends AssetBaseEvent<AssetEventType.deleteForever> {
readonly ids: ReadonlySet<backendModule.AssetId>
}

/** A signal to restore assets from trash. */
export interface AssetRestoreEvent extends AssetBaseEvent<AssetEventType.restore> {
readonly ids: ReadonlySet<backendModule.AssetId>
Expand Down
4 changes: 4 additions & 0 deletions app/ide-desktop/lib/dashboard/src/events/assetListEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ interface AssetListEvents {
readonly move: AssetListMoveEvent
readonly willDelete: AssetListWillDeleteEvent
readonly delete: AssetListDeleteEvent
readonly emptyTrash: AssetListEmptyTrashEvent
readonly removeSelf: AssetListRemoveSelfEvent
}

Expand Down Expand Up @@ -127,6 +128,9 @@ interface AssetListDeleteEvent extends AssetListBaseEvent<AssetListEventType.del
readonly key: backend.AssetId
}

/** A signal to permanently delete all files in Trash. */
interface AssetListEmptyTrashEvent extends AssetListBaseEvent<AssetListEventType.emptyTrash> {}

/** A signal for a file to remove itself from the asset list, without being deleted. */
interface AssetListRemoveSelfEvent extends AssetListBaseEvent<AssetListEventType.removeSelf> {
readonly id: backend.AssetId
Expand Down
22 changes: 18 additions & 4 deletions app/ide-desktop/lib/dashboard/src/layouts/AssetContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
},
[/* should never change */ setItem]
)

return category === Category.trash ? (
!ownsThisAsset ? null : (
<ContextMenus hidden={hidden} key={asset.id} event={event}>
Expand All @@ -108,10 +109,23 @@ export default function AssetContextMenu(props: AssetContextMenuProps) {
label="Restore From Trash"
doAction={() => {
unsetModal()
dispatchAssetEvent({
type: AssetEventType.restore,
ids: new Set([asset.id]),
})
dispatchAssetEvent({ type: AssetEventType.restore, ids: new Set([asset.id]) })
}}
/>
<MenuEntry
hidden={hidden}
action="delete"
label="Delete Forever"
doAction={() => {
setModal(
<ConfirmDeleteModal
actionText={`delete the ${asset.type} '${asset.title}' forever`}
doDelete={() => {
const ids = new Set([asset.id])
dispatchAssetEvent({ type: AssetEventType.deleteForever, ids })
}}
/>
)
}}
/>
</ContextMenu>
Expand Down
17 changes: 13 additions & 4 deletions app/ide-desktop/lib/dashboard/src/layouts/AssetsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1727,11 +1727,20 @@ export default function AssetsTable(props: AssetsTableProps) {
deleteAsset(event.key)
break
}
case AssetListEventType.emptyTrash: {
if (category !== Category.trash) {
toastAndLog('Can only empty trash when in Trash')
} else if (assetTree.children != null) {
const ids = new Set(assetTree.children.map(child => child.item.id))
// This is required to prevent an infinite loop,
window.setTimeout(() => {
dispatchAssetEvent({ type: AssetEventType.deleteForever, ids })
})
}
break
}
case AssetListEventType.removeSelf: {
dispatchAssetEvent({
type: AssetEventType.removeSelf,
id: event.id,
})
dispatchAssetEvent({ type: AssetEventType.removeSelf, id: event.id })
break
}
case AssetListEventType.closeFolder: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export default function AssetsTableContextMenu(props: AssetsTableContextMenuProp
)
return selfPermission?.permission === permissions.PermissionAction.own
}).every(isOwner => isOwner))

// This is not a React component even though it contains JSX.
// eslint-disable-next-line no-restricted-syntax
const doDeleteAll = () => {
Expand All @@ -104,15 +105,7 @@ export default function AssetsTableContextMenu(props: AssetsTableContextMenuProp
)
}
}
// This is not a React component even though it contains JSX.
// eslint-disable-next-line no-restricted-syntax
const doRestoreAll = () => {
unsetModal()
dispatchAssetEvent({
type: AssetEventType.restore,
ids: selectedKeys,
})
}

if (category === Category.trash) {
return selectedKeys.size === 0 ? (
<></>
Expand All @@ -123,8 +116,29 @@ export default function AssetsTableContextMenu(props: AssetsTableContextMenuProp
hidden={hidden}
action="undelete"
label="Restore All From Trash"
doAction={doRestoreAll}
doAction={() => {
unsetModal()
dispatchAssetEvent({ type: AssetEventType.restore, ids: selectedKeys })
}}
/>
{isCloud && (
<MenuEntry
hidden={hidden}
action="delete"
label="Delete All Forever"
doAction={() => {
setModal(
<ConfirmDeleteModal
actionText={`delete ${selectedKeys.size} selected ${pluralized} forever`}
doDelete={() => {
clearSelectedKeys()
dispatchAssetEvent({ type: AssetEventType.deleteForever, ids: selectedKeys })
}}
/>
)
}}
/>
)}
</ContextMenu>
</ContextMenus>
)
Expand Down
Loading

0 comments on commit 618080b

Please sign in to comment.