From 44027d358e05ae2dd3f63d095d549c4cee470417 Mon Sep 17 00:00:00 2001 From: Jack Ord <36296721+j-or@users.noreply.github.com> Date: Fri, 7 May 2021 17:56:52 +0200 Subject: [PATCH 1/4] Add delete with undo to experiments in local storage --- components/experiment.tsx | 1 - components/home.tsx | 81 ++++++++++++++++++++++++++++----- reducers/global-reducer.test.ts | 28 ++++++++---- reducers/global-reducer.ts | 9 ++++ 4 files changed, 99 insertions(+), 20 deletions(-) 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..b5c157ce 100644 --- a/components/home.tsx +++ b/components/home.tsx @@ -1,11 +1,12 @@ -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, 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"; @@ -17,7 +18,9 @@ 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 [experimentsToDelete, setExperimentsToDelete] = useState([]) const onDrop = useCallback(acceptedFiles => { const reader = new FileReader() @@ -80,6 +83,30 @@ export default function Home() { return key } + const deleteExperiment = (e: MouseEvent, id: string) => { + e.stopPropagation() + let experimentsAfterAdd: string[] = experimentsToDelete.slice() + experimentsAfterAdd.splice(experimentsToDelete.length, 0, id) + setExperimentsToDelete(experimentsAfterAdd) + setSnackbarOpen(true) + } + + const handleCloseSnackbar = () => { + setSnackbarOpen(false) + if (experimentsToDelete.length > 0) { + experimentsToDelete.forEach(id => { + dispatch({ type: 'deleteExperimentId', payload: id }) + localStorage.removeItem(id) + }) + setExperimentsToDelete([]) + } + } + + const undoDeleteExperiment = () => { + setExperimentsToDelete([]) + setSnackbarOpen(false) + } + return ( @@ -123,14 +150,25 @@ export default function Home() { {state.experimentsInLocalStorage.length > 0 ? - {state.experimentsInLocalStorage.map((k, i) => - openSavedExperiment(k)}> - - - + {state.experimentsInLocalStorage + .filter(id => experimentsToDelete.indexOf(id) === -1) + .map((id, i) => + openSavedExperiment(id)}> + + deleteExperiment(e, id)}> + + + + + + )} : @@ -144,6 +182,27 @@ export default function Home() { + + + + Experiment deleted + + + {state.experimentsInLocalStorage[experimentsToDelete.length - 1]} + + + } + 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 } From 37526849583b9191edb95a0080734067fbd7f5b8 Mon Sep 17 00:00:00 2001 From: Jack Ord <36296721+j-or@users.noreply.github.com> Date: Mon, 10 May 2021 10:14:11 +0200 Subject: [PATCH 2/4] Delete experiments set for deletion when navigating away from home --- components/home.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/home.tsx b/components/home.tsx index b5c157ce..95294a69 100644 --- a/components/home.tsx +++ b/components/home.tsx @@ -65,10 +65,12 @@ export default function Home() { } const createNewExperiment = () => { + deleteExperiments() router.push(`${paths.experiment}/${uuid()}`) } const openSavedExperiment = (key: string) => { + deleteExperiments() router.push(`${paths.experiment}/${key}`) } @@ -93,6 +95,10 @@ export default function Home() { const handleCloseSnackbar = () => { setSnackbarOpen(false) + deleteExperiments() + } + + const deleteExperiments = () => { if (experimentsToDelete.length > 0) { experimentsToDelete.forEach(id => { dispatch({ type: 'deleteExperimentId', payload: id }) From 391def24fbdf4792bd4651d2311f3a2fbc77974b Mon Sep 17 00:00:00 2001 From: Jack Ord <36296721+j-or@users.noreply.github.com> Date: Mon, 10 May 2021 10:33:05 +0200 Subject: [PATCH 3/4] Move deletion state to reducer --- components/home.tsx | 18 +++++++++--------- reducers/home-reducer.test.ts | 21 +++++++++++++++++++++ reducers/home-reducer.ts | 25 +++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 reducers/home-reducer.test.ts create mode 100644 reducers/home-reducer.ts diff --git a/components/home.tsx b/components/home.tsx index 95294a69..a634b935 100644 --- a/components/home.tsx +++ b/components/home.tsx @@ -1,5 +1,5 @@ import { Box, Button, Card, CardContent, IconButton, List, ListItem, ListItemIcon, ListItemText, Snackbar, Typography } from "@material-ui/core"; -import { MouseEvent, useCallback, useState } from "react"; +import { MouseEvent, useCallback, useReducer, useState } from "react"; import { useDropzone } from 'react-dropzone'; import Layout from "../components/layout"; import useStyles from "../styles/home.style"; @@ -13,6 +13,7 @@ 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() @@ -20,7 +21,7 @@ export default function Home() { const [uploadMessage, setUploadMessage] = useState("Drag file here") const { state, dispatch } = useGlobal() const [isSnackbarOpen, setSnackbarOpen] = useState(false) - const [experimentsToDelete, setExperimentsToDelete] = useState([]) + const [deletionState, dispatchDeletion] = useReducer(reducer, { experimentsToDelete: [] }) const onDrop = useCallback(acceptedFiles => { const reader = new FileReader() @@ -87,9 +88,7 @@ export default function Home() { const deleteExperiment = (e: MouseEvent, id: string) => { e.stopPropagation() - let experimentsAfterAdd: string[] = experimentsToDelete.slice() - experimentsAfterAdd.splice(experimentsToDelete.length, 0, id) - setExperimentsToDelete(experimentsAfterAdd) + dispatchDeletion( { type: "addExperimentForDeletion", payload: id } ) setSnackbarOpen(true) } @@ -99,17 +98,18 @@ export default function Home() { } const deleteExperiments = () => { + const experimentsToDelete: string[] = deletionState.experimentsToDelete if (experimentsToDelete.length > 0) { experimentsToDelete.forEach(id => { dispatch({ type: 'deleteExperimentId', payload: id }) localStorage.removeItem(id) }) - setExperimentsToDelete([]) + dispatchDeletion({ type: 'resetExperimentsForDeletion' }) } } const undoDeleteExperiment = () => { - setExperimentsToDelete([]) + dispatchDeletion({ type: 'resetExperimentsForDeletion' }) setSnackbarOpen(false) } @@ -157,7 +157,7 @@ export default function Home() { {state.experimentsInLocalStorage.length > 0 ? {state.experimentsInLocalStorage - .filter(id => experimentsToDelete.indexOf(id) === -1) + .filter(id => deletionState.experimentsToDelete.indexOf(id) === -1) .map((id, i) => openSavedExperiment(id)}> @@ -199,7 +199,7 @@ export default function Home() { Experiment deleted - {state.experimentsInLocalStorage[experimentsToDelete.length - 1]} + {state.experimentsInLocalStorage[deletionState.experimentsToDelete.length - 1]} } 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 From 640969caef4fbba832ab9ea4ddccc6268b6d48bc Mon Sep 17 00:00:00 2001 From: Jack Ord <36296721+j-or@users.noreply.github.com> Date: Mon, 10 May 2021 10:43:36 +0200 Subject: [PATCH 4/4] Add all experiments being deleted to undo message --- components/home.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/components/home.tsx b/components/home.tsx index a634b935..0b1e7fa5 100644 --- a/components/home.tsx +++ b/components/home.tsx @@ -195,12 +195,12 @@ export default function Home() { onClose={handleCloseSnackbar} message={ <> - - Experiment deleted - - - {state.experimentsInLocalStorage[deletionState.experimentsToDelete.length - 1]} + + {`Experiment${deletionState.experimentsToDelete.length > 1 ? 's' : ''} deleted:`} + {deletionState.experimentsToDelete.map(e => + {e} + )} } action={