diff --git a/components/experiment.tsx b/components/experiment.tsx index 3c754ff5..190e497f 100644 --- a/components/experiment.tsx +++ b/components/experiment.tsx @@ -150,7 +150,6 @@ export default function Experiment() { diff --git a/components/home.tsx b/components/home.tsx index 74591cb4..0b1e7fa5 100644 --- a/components/home.tsx +++ b/components/home.tsx @@ -1,23 +1,27 @@ -import { Box, Card, CardContent, List, ListItem, ListItemText, Typography } from "@material-ui/core"; -import { useCallback, useState } from "react"; +import { Box, Button, Card, CardContent, IconButton, List, ListItem, ListItemIcon, ListItemText, Snackbar, Typography } from "@material-ui/core"; +import { MouseEvent, useCallback, useReducer, useState } from "react"; import { useDropzone } from 'react-dropzone'; import Layout from "../components/layout"; import useStyles from "../styles/home.style"; import { NextRouter, useRouter } from 'next/router' import SystemUpdateAltIcon from '@material-ui/icons/SystemUpdateAlt'; import ChevronRightIcon from '@material-ui/icons/ChevronRight'; +import DeleteIcon from '@material-ui/icons/Delete'; import { paths } from "../paths"; import { ExperimentType } from "../types/common"; import { useGlobal } from "../context/global-context"; import { saveExperiment } from '../context/experiment-context'; import { v4 as uuid } from 'uuid'; import { isEmpty } from "../utility/string-util"; +import { reducer } from "../reducers/home-reducer"; export default function Home() { const classes = useStyles() const router: NextRouter = useRouter() const [uploadMessage, setUploadMessage] = useState("Drag file here") - const { state } = useGlobal() + const { state, dispatch } = useGlobal() + const [isSnackbarOpen, setSnackbarOpen] = useState(false) + const [deletionState, dispatchDeletion] = useReducer(reducer, { experimentsToDelete: [] }) const onDrop = useCallback(acceptedFiles => { const reader = new FileReader() @@ -62,10 +66,12 @@ export default function Home() { } const createNewExperiment = () => { + deleteExperiments() router.push(`${paths.experiment}/${uuid()}`) } const openSavedExperiment = (key: string) => { + deleteExperiments() router.push(`${paths.experiment}/${key}`) } @@ -80,6 +86,33 @@ export default function Home() { return key } + const deleteExperiment = (e: MouseEvent, id: string) => { + e.stopPropagation() + dispatchDeletion( { type: "addExperimentForDeletion", payload: id } ) + setSnackbarOpen(true) + } + + const handleCloseSnackbar = () => { + setSnackbarOpen(false) + deleteExperiments() + } + + const deleteExperiments = () => { + const experimentsToDelete: string[] = deletionState.experimentsToDelete + if (experimentsToDelete.length > 0) { + experimentsToDelete.forEach(id => { + dispatch({ type: 'deleteExperimentId', payload: id }) + localStorage.removeItem(id) + }) + dispatchDeletion({ type: 'resetExperimentsForDeletion' }) + } + } + + const undoDeleteExperiment = () => { + dispatchDeletion({ type: 'resetExperimentsForDeletion' }) + setSnackbarOpen(false) + } + return ( @@ -123,14 +156,25 @@ export default function Home() { {state.experimentsInLocalStorage.length > 0 ? - {state.experimentsInLocalStorage.map((k, i) => - openSavedExperiment(k)}> - - - + {state.experimentsInLocalStorage + .filter(id => deletionState.experimentsToDelete.indexOf(id) === -1) + .map((id, i) => + openSavedExperiment(id)}> + + deleteExperiment(e, id)}> + + + + + + )} : @@ -144,6 +188,27 @@ export default function Home() { + + + + {`Experiment${deletionState.experimentsToDelete.length > 1 ? 's' : ''} deleted:`} + + {deletionState.experimentsToDelete.map(e => + {e} + )} + + } + action={ + + }/> ); } \ No newline at end of file diff --git a/reducers/global-reducer.test.ts b/reducers/global-reducer.test.ts index 9ae2ef20..56101a4c 100644 --- a/reducers/global-reducer.test.ts +++ b/reducers/global-reducer.test.ts @@ -1,12 +1,12 @@ import { reducer, State } from "./global-reducer" -describe("storeExperimentId", () => { - const initState: State = { - debug: false, - useLocalStorage: true, - experimentsInLocalStorage: [] - } +const initState: State = { + debug: false, + useLocalStorage: true, + experimentsInLocalStorage: [] +} +describe("storeExperimentId", () => { it("should store id", async () => { const payload = '1234' expect(reducer(initState, { type: 'storeExperimentId', payload })).toEqual({...initState, experimentsInLocalStorage: [payload]}) @@ -16,7 +16,19 @@ describe("storeExperimentId", () => { const payload = '1234' expect( reducer({...initState, experimentsInLocalStorage: [payload]}, { type: 'storeExperimentId', payload })) - .toEqual({...initState, experimentsInLocalStorage: [payload]} - ) + .toEqual({...initState, experimentsInLocalStorage: [payload]}) + }) +}) + +describe("deleteExperimentId", () => { + it("should delete id", async () => { + expect( + reducer({...initState, experimentsInLocalStorage: ['1234', '5678'] }, { type: 'deleteExperimentId', payload: '1234' })) + .toEqual({...initState, experimentsInLocalStorage: ['5678']}) + }) + it("should return empty array when no ids to delete", async () => { + expect( + reducer(initState, { type: 'deleteExperimentId', payload: '1234' })) + .toEqual(initState) }) }) \ No newline at end of file diff --git a/reducers/global-reducer.ts b/reducers/global-reducer.ts index 0ab5d892..ebddd61b 100644 --- a/reducers/global-reducer.ts +++ b/reducers/global-reducer.ts @@ -16,6 +16,10 @@ export type Action = { type:'storeExperimentId' payload: string } +| { + type:'deleteExperimentId' + payload: string +} export type Dispatch = (action: Action) => void @@ -41,6 +45,11 @@ export const reducer = (state: State, action: Action) => { } else { return state } + case 'deleteExperimentId': + let idsAfterDelete: string[] = state.experimentsInLocalStorage.slice() + let indexOfDelete = state.experimentsInLocalStorage.indexOf(action.payload) + idsAfterDelete.splice(indexOfDelete, 1) + return {...state, experimentsInLocalStorage: idsAfterDelete } default: return state } diff --git a/reducers/home-reducer.test.ts b/reducers/home-reducer.test.ts new file mode 100644 index 00000000..474f8d71 --- /dev/null +++ b/reducers/home-reducer.test.ts @@ -0,0 +1,21 @@ +import { reducer, State } from "./home-reducer" + +const initState: State = { + experimentsToDelete: [] +} + +describe("addExperimentForDeletion", () => { + it("should add experiment", async () => { + expect( + reducer(initState, { type: 'addExperimentForDeletion', payload: '1234' } )) + .toEqual({ ...initState, experimentsToDelete: ['1234'] }) + }) +}) + +describe("resetExperimentsForDeletion", () => { + it("should reset experiments", async () => { + expect( + reducer({ ...initState, experimentsToDelete: ['1234', '5678'] }, { type: 'resetExperimentsForDeletion' } )) + .toEqual({ ...initState }) + }) +}) \ No newline at end of file diff --git a/reducers/home-reducer.ts b/reducers/home-reducer.ts new file mode 100644 index 00000000..bba5b2cd --- /dev/null +++ b/reducers/home-reducer.ts @@ -0,0 +1,25 @@ +export type Action = { + type: 'addExperimentForDeletion' + payload: string +} +| { + type: 'resetExperimentsForDeletion' +} + +export type State = { + experimentsToDelete: string[] +} + +export const reducer = (state: State, action: Action) => { + switch (action.type) { + case 'addExperimentForDeletion': + const experimentsToDelete: string[] = state.experimentsToDelete + let experimentsAfterAdd: string[] = experimentsToDelete.slice() + experimentsAfterAdd.splice(experimentsToDelete.length, 0, action.payload) + return { ...state, experimentsToDelete: experimentsAfterAdd } + case 'resetExperimentsForDeletion': + return { ...state, experimentsToDelete: [] } + default: + return state + } +} \ No newline at end of file