Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hard delete of items in Trash #9091

Merged
merged 7 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Loading