From 72cb0526c1a528a480143c09c7bc0034dfdb5639 Mon Sep 17 00:00:00 2001 From: Ed Olivares <34591886+eudoroolivares2016@users.noreply.github.com> Date: Wed, 13 Sep 2023 14:53:00 -0500 Subject: [PATCH] EDD-19: As a download app user, I want to be able to clear my download history. (#28) * EDD-19: Adds clearing download history on the downloadHistory page for the header button and individual actions --- package-lock.json | 4 +- package.json | 2 +- .../DownloadHistoryHeader.jsx | 14 ++-- .../__tests__/DownloadHistoryHeader.test.js | 3 +- .../DownloadHistoryListItem.jsx | 28 +++++++- .../__tests__/DownloadHistoryListItem.test.js | 60 +++++++++++++++++ src/main/__tests__/preload.test.ts | 4 +- .../__tests__/clearDownloadHistory.test.js | 40 +++++++++++ .../eventHandlers/clearDownloadHistory.ts | 19 ++++++ src/main/preload.ts | 2 +- .../__tests__/setupEventListeners.test.ts | 21 ++++++ src/main/utils/database/EddDatabase.ts | 44 ++++++++++++ .../database/__tests__/EddDatabase.test.ts | 67 +++++++++++++++++++ src/main/utils/setupEventListeners.ts | 9 +++ 14 files changed, 302 insertions(+), 15 deletions(-) create mode 100644 src/main/eventHandlers/__tests__/clearDownloadHistory.test.js create mode 100644 src/main/eventHandlers/clearDownloadHistory.ts diff --git a/package-lock.json b/package-lock.json index fbdbf2fc..d8bafdc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "earthdata-download", - "version": "0.0.19", + "version": "0.0.20", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "earthdata-download", - "version": "0.0.19", + "version": "0.0.20", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 17f3817d..083d65d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "earthdata-download", - "version": "0.0.19", + "version": "0.0.20", "description": "Earthdata Download is a cross-platform download manager designed to improve how users download Earth Science data. It accepts lists of files from applications like Earthdata Search (https://search.earthdata.nasa.gov/) and enables clients to offer users a streamlined experience when downloading files from their browser.", "repository": "nasa/earthdata-download", "homepage": "https://github.com/nasa/earthdata-download#readme", diff --git a/src/app/components/DownloadHistoryHeader/DownloadHistoryHeader.jsx b/src/app/components/DownloadHistoryHeader/DownloadHistoryHeader.jsx index c788b17f..cddf92e5 100644 --- a/src/app/components/DownloadHistoryHeader/DownloadHistoryHeader.jsx +++ b/src/app/components/DownloadHistoryHeader/DownloadHistoryHeader.jsx @@ -1,6 +1,7 @@ -import React from 'react' +import React, { useContext } from 'react' import { FaBan } from 'react-icons/fa' import useAppContext from '../../hooks/useAppContext' +import { ElectronApiContext } from '../../context/ElectronApiContext' import Button from '../Button/Button' @@ -19,14 +20,13 @@ const DownloadHistoryHeader = () => { const { deleteAllToastsById } = appContext - // TODO EDD-19 - // const { - // clearDownloadHistory - // } = useContext(ElectronApiContext) + + const { + clearDownloadHistory + } = useContext(ElectronApiContext) const onClearDownloadHistory = () => { - // TODO EDD-19 - // clearDownloadHistory() + clearDownloadHistory({}) deleteAllToastsById() } diff --git a/src/app/components/DownloadHistoryHeader/__tests__/DownloadHistoryHeader.test.js b/src/app/components/DownloadHistoryHeader/__tests__/DownloadHistoryHeader.test.js index 0a25516e..78e035cf 100644 --- a/src/app/components/DownloadHistoryHeader/__tests__/DownloadHistoryHeader.test.js +++ b/src/app/components/DownloadHistoryHeader/__tests__/DownloadHistoryHeader.test.js @@ -48,7 +48,8 @@ describe('DownloadHistoryHeader component', () => { const button = screen.getByRole('button', { name: 'Clear Download History' }) await userEvent.click(button) - expect(clearDownloadHistory).toHaveBeenCalledTimes(0) + expect(clearDownloadHistory).toHaveBeenCalledTimes(1) + expect(clearDownloadHistory).toHaveBeenCalledWith({}) expect(deleteAllToastsById).toHaveBeenCalledTimes(1) }) }) diff --git a/src/app/components/DownloadHistoryListItem/DownloadHistoryListItem.jsx b/src/app/components/DownloadHistoryListItem/DownloadHistoryListItem.jsx index 0d8f1890..600150f1 100644 --- a/src/app/components/DownloadHistoryListItem/DownloadHistoryListItem.jsx +++ b/src/app/components/DownloadHistoryListItem/DownloadHistoryListItem.jsx @@ -1,6 +1,7 @@ import React, { useContext } from 'react' import PropTypes from 'prop-types' import { + FaBan, FaClipboard, FaFolderOpen, FaInfoCircle @@ -42,6 +43,7 @@ const DownloadHistoryListItem = ({ deleteAllToastsById } = appContext const { + clearDownloadHistory, copyDownloadPath, openDownloadFolder, restartDownload @@ -90,13 +92,37 @@ const DownloadHistoryListItem = ({ restartDownload({ downloadId }) }, icon: FaInfoCircle + }, + { + label: 'Clear Download', + isActive: true, + isPrimary: false, + callback: () => { + deleteAllToastsById(downloadId) + clearDownloadHistory({ downloadId }) + }, + icon: FaBan } ] ] + const fetchLinksErroredActionsList = [ + [ + { + label: 'Clear Download', + isActive: true, + isPrimary: false, + callback: () => { + deleteAllToastsById(downloadId) + clearDownloadHistory({ downloadId }) + }, + icon: FaBan + } + ], []] + return ( { const deleteAllToastsById = jest.fn() + const clearDownloadHistory = jest.fn() const copyDownloadPath = jest.fn() const openDownloadFolder = jest.fn() const restartDownload = jest.fn() @@ -45,6 +60,7 @@ const setup = (overrideProps = {}) => { { return { deleteAllToastsById, + clearDownloadHistory, copyDownloadPath, openDownloadFolder, restartDownload @@ -158,4 +175,47 @@ describe('DownloadHistoryListItem component', () => { expect(deleteAllToastsById).toHaveBeenCalledWith('mock-download-id') }) }) + + describe('when clicking `Clear Download`', () => { + test('calls clearDownloadHistory', async () => { + const { clearDownloadHistory, deleteAllToastsById } = setup() + + const moreActions = screen.getByText('More Actions') + await userEvent.click(moreActions) + + const button = screen.getByText('Clear Download') + await userEvent.click(button) + + expect(clearDownloadHistory).toHaveBeenCalledTimes(1) + expect(clearDownloadHistory).toHaveBeenCalledWith({ downloadId: 'mock-download-id' }) + + expect(deleteAllToastsById).toHaveBeenCalledTimes(1) + expect(deleteAllToastsById).toHaveBeenCalledWith('mock-download-id') + }) + }) + + describe('when a `downloadHistoryListItem` is in `errorFetchingLinks` state', () => { + test(' clicking `Clear Download` clearDownloadHistory', async () => { + const { clearDownloadHistory, deleteAllToastsById } = setup( + { download: erroredFetchLinksdownload } + ) + const moreActions = screen.getByText('More Actions') + await userEvent.click(moreActions) + + // Ensure the other buttons are not rendering + expect(screen.queryAllByText('Open Folder').length).toEqual(0) + expect(screen.queryAllByText('Copy Folder Path').length).toEqual(0) + expect(screen.queryAllByText('Copy Folder Path').length).toEqual(0) + expect(screen.queryAllByText('Restart Download').length).toEqual(0) + + const Clearbutton = screen.getByText('Clear Download') + await userEvent.click(Clearbutton) + + expect(clearDownloadHistory).toHaveBeenCalledTimes(1) + expect(clearDownloadHistory).toHaveBeenCalledWith({ downloadId: 'mock-download-id' }) + + expect(deleteAllToastsById).toHaveBeenCalledTimes(1) + expect(deleteAllToastsById).toHaveBeenCalledWith('mock-download-id') + }) + }) }) diff --git a/src/main/__tests__/preload.test.ts b/src/main/__tests__/preload.test.ts index 72383420..403af138 100644 --- a/src/main/__tests__/preload.test.ts +++ b/src/main/__tests__/preload.test.ts @@ -134,10 +134,10 @@ describe('preload', () => { test('clearDownloadHistory sends the clearDownloadHistory message', async () => { await setup() - electronApi.clearDownloadHistory() + electronApi.clearDownloadHistory({ mock: 'data' }) expect(ipcRenderer.send).toHaveBeenCalledTimes(1) - expect(ipcRenderer.send).toHaveBeenCalledWith('clearDownloadHistory') + expect(ipcRenderer.send).toHaveBeenCalledWith('clearDownloadHistory', { mock: 'data' }) }) test('closeWindow sends the closeWindow message', async () => { diff --git a/src/main/eventHandlers/__tests__/clearDownloadHistory.test.js b/src/main/eventHandlers/__tests__/clearDownloadHistory.test.js new file mode 100644 index 00000000..64b2951c --- /dev/null +++ b/src/main/eventHandlers/__tests__/clearDownloadHistory.test.js @@ -0,0 +1,40 @@ +import clearDownloadHistory from '../clearDownloadHistory' + +describe('clearDownloadHistory', () => { + describe('when a downloadId is not passed in', () => { + test('ensure that downloads, files, and pauses which are not active are deleted', async () => { + const info = {} + const database = { + clearDownloadHistoryDownloads: jest.fn() + } + + await clearDownloadHistory({ + database, + info + }) + + expect(database.clearDownloadHistoryDownloads).toHaveBeenCalledTimes(1) + + expect(database.clearDownloadHistoryDownloads).toHaveBeenCalledWith(undefined) + }) + }) + + describe('when a downloadId is passed in', () => { + test('ensure only the downloads, files, and pauses associated to the downloadId are deleted', async () => { + const info = { + downloadId: 'mock-download-id' + } + const database = { + clearDownloadHistoryDownloads: jest.fn() + } + + await clearDownloadHistory({ + database, + info + }) + + expect(database.clearDownloadHistoryDownloads).toHaveBeenCalledTimes(1) + expect(database.clearDownloadHistoryDownloads).toHaveBeenCalledWith('mock-download-id') + }) + }) +}) diff --git a/src/main/eventHandlers/clearDownloadHistory.ts b/src/main/eventHandlers/clearDownloadHistory.ts new file mode 100644 index 00000000..8d358c4e --- /dev/null +++ b/src/main/eventHandlers/clearDownloadHistory.ts @@ -0,0 +1,19 @@ +// @ts-nocheck + +/** + * Removes downloads in the download history from the database + * @param {Object} params + * @param {Object} params.database `EddDatabase` instance + * @param {Object} params.info `info` parameter from ipc message + */ +const clearDownloadHistory = async ({ + database, + info +}) => { + const { downloadId } = info + + // Clear the download(s) in the history + await database.clearDownloadHistoryDownloads(downloadId) +} + +export default clearDownloadHistory diff --git a/src/main/preload.ts b/src/main/preload.ts index 472e3046..10afade4 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -18,7 +18,7 @@ contextBridge.exposeInMainWorld('electronApi', { cancelDownloadItem: (data) => ipcRenderer.send('cancelDownloadItem', data), cancelErroredDownloadItem: (data) => ipcRenderer.send('cancelErroredDownloadItem', data), clearDownload: (data) => ipcRenderer.send('clearDownload', data), - clearDownloadHistory: () => ipcRenderer.send('clearDownloadHistory'), + clearDownloadHistory: (data) => ipcRenderer.send('clearDownloadHistory', data), closeWindow: () => ipcRenderer.send('closeWindow'), copyDownloadPath: (data) => ipcRenderer.send('copyDownloadPath', data), maximizeWindow: () => ipcRenderer.send('maximizeWindow'), diff --git a/src/main/utils/__tests__/setupEventListeners.test.ts b/src/main/utils/__tests__/setupEventListeners.test.ts index e0420070..295b6dc0 100644 --- a/src/main/utils/__tests__/setupEventListeners.test.ts +++ b/src/main/utils/__tests__/setupEventListeners.test.ts @@ -13,6 +13,7 @@ import beginDownload from '../../eventHandlers/beginDownload' import cancelDownloadItem from '../../eventHandlers/cancelDownloadItem' import cancelErroredDownloadItem from '../../eventHandlers/cancelErroredDownloadItem' import clearDownload from '../../eventHandlers/clearDownload' +import clearDownloadHistory from '../../eventHandlers/clearDownloadHistory' import chooseDownloadLocation from '../../eventHandlers/chooseDownloadLocation' import copyDownloadPath from '../../eventHandlers/copyDownloadPath' import didFinishLoad from '../../eventHandlers/didFinishLoad' @@ -44,6 +45,7 @@ jest.mock('../../eventHandlers/beginDownload') jest.mock('../../eventHandlers/cancelDownloadItem') jest.mock('../../eventHandlers/cancelErroredDownloadItem') jest.mock('../../eventHandlers/clearDownload') +jest.mock('../../eventHandlers/clearDownloadHistory') jest.mock('../../eventHandlers/chooseDownloadLocation') jest.mock('../../eventHandlers/copyDownloadPath') jest.mock('../../eventHandlers/didFinishLoad') @@ -510,6 +512,25 @@ describe('setupEventListeners', () => { }) }) + describe('clearDownloadHistory', () => { + test('calls clearDownloadHistory', () => { + const { + database + } = setup() + + const event = {} + const info = { mock: 'info' } + + ipcRenderer.send('clearDownloadHistory', event, info) + + expect(clearDownloadHistory).toHaveBeenCalledTimes(1) + expect(clearDownloadHistory).toHaveBeenCalledWith({ + database, + info + }) + }) + }) + describe('pauseDownloadItem', () => { test('calls pauseDownloadItem', () => { const { diff --git a/src/main/utils/database/EddDatabase.ts b/src/main/utils/database/EddDatabase.ts index 097895b4..9a4e5750 100644 --- a/src/main/utils/database/EddDatabase.ts +++ b/src/main/utils/database/EddDatabase.ts @@ -131,6 +131,50 @@ class EddDatabase { return query } + /** + * Deletes downloads which are on the `downloadHistory` page + * @param {String} downloadId ID of download to create. + */ + async clearDownloadHistoryDownloads(downloadId) { + if (downloadId) { + await this.db('downloads') + .delete() + .where({ id: downloadId }) + + await this.db('files') + .delete() + .where({ downloadId }) + + await this.db('pauses') + .delete() + .where({ id: downloadId }) + + return + } + + await this.db('pauses') + .delete() + .whereIn('downloadId', function select() { + this + .select('id') + .from('downloads') + .where({ active: false }) + }) + + await this.db('files') + .delete() + .whereIn('downloadId', function select() { + this + .select('id') + .from('downloads') + .where({ active: false }) + }) + + await this.db('downloads') + .delete() + .where({ 'downloads.active': false }) + } + /** * Creates a new download. * @param {String} downloadId ID of download to create. diff --git a/src/main/utils/database/__tests__/EddDatabase.test.ts b/src/main/utils/database/__tests__/EddDatabase.test.ts index c2487e0a..c250b94e 100644 --- a/src/main/utils/database/__tests__/EddDatabase.test.ts +++ b/src/main/utils/database/__tests__/EddDatabase.test.ts @@ -344,6 +344,73 @@ describe('EddDatabase', () => { }) }) + describe('clearDownloadHistoryDownloads', () => { + describe('when a downloadId is provided', () => { + test('deletes the download, files and pauses', async () => { + dbTracker.on('query', (query, step) => { + if (step === 1) { + expect(query.sql).toEqual('delete from `downloads` where `id` = ?') + expect(query.bindings).toEqual(['mock-download-id']) + } + + if (step === 2) { + expect(query.sql).toEqual('delete from `files` where `downloadId` = ?') + expect(query.bindings).toEqual(['mock-download-id']) + } + + if (step === 3) { + expect(query.sql).toEqual('delete from `pauses` where `id` = ?') + expect(query.bindings).toEqual(['mock-download-id']) + } + + // We aren't returning anything from this method, the above assertions are the important part of the test + query.response([1]) + }) + + const database = new EddDatabase('./') + + await database.clearDownloadHistoryDownloads('mock-download-id') + }) + }) + + describe('when a downloadId is not provided', () => { + test('deletes all of the downloads, pauses, and files for inactive downloads', async () => { + dbTracker.on('query', (query, step) => { + if (step === 1) { + expect(query.sql).toEqual('delete from `pauses` where `downloadId` in (select `id` from `downloads` where `active` = ?)') + expect(query.bindings).toEqual([ + false + ]) + + query.response([1]) + } + + if (step === 2) { + expect(query.sql).toEqual('delete from `files` where `downloadId` in (select `id` from `downloads` where `active` = ?)') + expect(query.bindings).toEqual([ + false + ]) + + query.response([1]) + } + + if (step === 3) { + expect(query.sql).toEqual('delete from `downloads` where `downloads`.`active` = ?') + expect(query.bindings).toEqual([ + false + ]) + + query.response([1]) + } + }) + + const database = new EddDatabase('./') + + await database.clearDownloadHistoryDownloads() + }) + }) + }) + describe('createDownload', () => { test('creates a new download', async () => { dbTracker.on('query', (query) => { diff --git a/src/main/utils/setupEventListeners.ts b/src/main/utils/setupEventListeners.ts index e0fd6c56..c5439ae3 100644 --- a/src/main/utils/setupEventListeners.ts +++ b/src/main/utils/setupEventListeners.ts @@ -13,6 +13,7 @@ import cancelDownloadItem from '../eventHandlers/cancelDownloadItem' import cancelErroredDownloadItem from '../eventHandlers/cancelErroredDownloadItem' import chooseDownloadLocation from '../eventHandlers/chooseDownloadLocation' import clearDownload from '../eventHandlers/clearDownload' +import clearDownloadHistory from '../eventHandlers/clearDownloadHistory' import copyDownloadPath from '../eventHandlers/copyDownloadPath' import deleteDownload from '../eventHandlers/deleteDownload' import didFinishLoad from '../eventHandlers/didFinishLoad' @@ -199,6 +200,14 @@ const setupEventListeners = ({ }) }) + // Clear a download form the download history by deleting it + ipcMain.on('clearDownloadHistory', async (event, info) => { + await clearDownloadHistory({ + database, + info + }) + }) + // Pause a downloadItem ipcMain.on('pauseDownloadItem', async (event, info) => { await pauseDownloadItem({