Skip to content

Commit f8c527f

Browse files
authored
[159] mass remove notes (#175)
* clean commit history * button confirmation instead of alert * saving deleted notes to file * refactor * Add countdown timer for delete confirmation button * refactor * refactor redundant method
1 parent 57265b5 commit f8c527f

File tree

2 files changed

+95
-1
lines changed

2 files changed

+95
-1
lines changed

src/components/NoteManager/components/NotesActions.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import "./NotesActions.css";
22
import { type ChangeEventHandler, useCallback, useContext, useRef, useState } from "react";
33
import Modal from "react-modal";
4+
import { Tooltip } from "react-tooltip";
45
import { type INotesContext, NotesContext } from "../../NotesProvider/NotesProvider";
56
import { RemoteSources } from "../../RemoteSources/RemoteSources";
67

@@ -26,10 +27,15 @@ export function NotesActions() {
2627
handleRedo,
2728
handleImport,
2829
handleExport,
30+
handleDeleteNotes,
2931
handleSetRemoteSources,
3032
} = useContext(NotesContext) as INotesContext;
3133

34+
const [confirmButtonTimeoutId, setConfirmButtonTimeoutId] = useState<number | null>(null);
35+
const [secondsLeft, setSecondsLeft] = useState(3);
3236
const [isModalOpen, setModalOpen] = useState(false);
37+
const [confirmDelete, setConfirmDelete] = useState(false);
38+
const confirmDeleteDisabled = confirmDelete && secondsLeft > 0;
3339

3440
const fileImport = useRef<HTMLInputElement>(null);
3541
const onImport = useCallback(() => {
@@ -62,6 +68,46 @@ export function NotesActions() {
6268
setModalOpen((x) => !x);
6369
}, []);
6470

71+
const resetDeleteState = useCallback(() => {
72+
setSecondsLeft(3);
73+
setConfirmDelete(false);
74+
if (confirmButtonTimeoutId) {
75+
window.clearTimeout(confirmButtonTimeoutId);
76+
setConfirmButtonTimeoutId(null);
77+
}
78+
}, [confirmButtonTimeoutId]);
79+
80+
const initiateDeleteCountdown = useCallback(() => {
81+
setSecondsLeft(3);
82+
setConfirmDelete(true);
83+
setConfirmButtonTimeoutId(
84+
window.setTimeout(() => {
85+
setSecondsLeft(3);
86+
setConfirmDelete(false);
87+
}, 10000),
88+
);
89+
const disabledButtonIntervalId = window.setInterval(() => {
90+
setSecondsLeft((x) => {
91+
if (x <= 1) {
92+
window.clearInterval(disabledButtonIntervalId);
93+
return 0;
94+
}
95+
return x - 1;
96+
});
97+
}, 1000);
98+
}, []);
99+
100+
const deleteNotes = useCallback(() => {
101+
if (confirmDeleteDisabled) return;
102+
103+
if (confirmDelete) {
104+
handleDeleteNotes();
105+
resetDeleteState();
106+
} else {
107+
initiateDeleteCountdown();
108+
}
109+
}, [confirmDelete, confirmDeleteDisabled, handleDeleteNotes, resetDeleteState, initiateDeleteCountdown]);
110+
65111
return (
66112
<>
67113
<div className="notes-actions">
@@ -73,8 +119,18 @@ export function NotesActions() {
73119
</button>
74120
<button onClick={onImport}>📂 import</button>
75121
<button onClick={handleExport}>💾 export</button>
122+
<button
123+
data-tooltip-id="delete-tooltip"
124+
data-tooltip-content={confirmDelete ? "Yes, delete" : "Delete all notes"}
125+
data-tooltip-place="bottom"
126+
disabled={confirmDeleteDisabled}
127+
onClick={deleteNotes}
128+
>
129+
{confirmDelete ? (secondsLeft > 0 ? `Wait (${secondsLeft})` : "❌") : "🗑️"}
130+
</button>
76131
<button onClick={toggleModal}>⚙︎</button>
77132
</div>
133+
<Tooltip id="delete-tooltip" />
78134
<input ref={fileImport} onChange={handleFileSelected} type="file" style={{ display: "none" }} />
79135
<Modal style={modalStyles} isOpen={isModalOpen} onRequestClose={toggleModal} contentLabel="Settings">
80136
<button className="settings-close" onClick={toggleModal}>

src/components/NotesProvider/NotesProvider.tsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type ReactNode, createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
22
import { type ILocationContext, LocationContext } from "../LocationProvider/LocationProvider";
3-
import { LABEL_IMPORTED } from "./consts/labels";
3+
import { LABEL_IMPORTED, LABEL_LOCAL, LABEL_REMOTE } from "./consts/labels";
44
import { NEW_REMOTE_SOURCE_ID } from "./consts/remoteSources";
55
import { useDecoratedNotes } from "./hooks/useDecoratedNotes";
66
import { type ILabel, useLabels } from "./hooks/useLabels";
@@ -33,6 +33,7 @@ export interface INotesContext {
3333
handleRedo(): void;
3434
handleImport(jsonStr: string, label: string): void;
3535
handleExport(): void;
36+
handleDeleteNotes(): void;
3637
handleToggleLabel(label: string): void;
3738
}
3839

@@ -75,6 +76,23 @@ export function NotesProvider({ children }: INotesProviderProps) {
7576
notes.saveToLocalStorage(newNotes);
7677
}, []);
7778

79+
// Filter notes by labels.
80+
const filterNotesByLabels = useCallback(
81+
(
82+
notes: IStorageNote[],
83+
labels: string[],
84+
{ includesLabel }: { includesLabel: boolean } = { includesLabel: true },
85+
): IStorageNote[] => {
86+
return notes.filter((note) => {
87+
if (note.labels.some((label) => labels.includes(label))) {
88+
return includesLabel;
89+
}
90+
return !includesLabel;
91+
});
92+
},
93+
[],
94+
);
95+
7896
// Decorate all local notes.
7997
useEffect(() => {
8098
setLocalNotesReady(false);
@@ -206,6 +224,26 @@ export function NotesProvider({ children }: INotesProviderProps) {
206224
const fileName = `graypaper-notes-${new Date().toISOString()}.json`;
207225
downloadNotesAsJson(localNotes, fileName);
208226
}, [localNotes]),
227+
handleDeleteNotes: useCallback(() => {
228+
const activeLabels = labels
229+
.filter((label) => label.isActive)
230+
.map((label) => {
231+
const parts = label.label.split("/");
232+
if (parts.length > 1) {
233+
if (parts[0] === LABEL_LOCAL || parts[0] === LABEL_REMOTE) {
234+
return parts.slice(1).join("/");
235+
}
236+
}
237+
return label.label;
238+
});
239+
240+
const fileName = `removed-graypaper-notes-${new Date().toISOString()}.json`;
241+
const deletedNotes = filterNotesByLabels(localNotes.notes, activeLabels);
242+
downloadNotesAsJson({ version: 3, notes: deletedNotes }, fileName);
243+
244+
const updatedNotes = filterNotesByLabels(localNotes.notes, activeLabels, { includesLabel: false });
245+
updateLocalNotes(localNotes, { ...localNotes, notes: updatedNotes });
246+
}, [localNotes, labels, updateLocalNotes, filterNotesByLabels]),
209247
};
210248

211249
return <NotesContext.Provider value={context}>{children}</NotesContext.Provider>;

0 commit comments

Comments
 (0)