refactor: handle multiselect actions directly

This commit is contained in:
thecodrr
2022-02-14 12:46:37 +05:00
parent 7ef2d60297
commit 96c351b8bb
23 changed files with 385 additions and 474 deletions

View File

@@ -268,15 +268,17 @@ export function showLoadingDialog(dialogData) {
));
}
/**
*
* @param {{title: string, subtitle?: string, action: Function}} dialogData
* @returns
*/
export function showProgressDialog(dialogData) {
const { title, message, subtitle, total, setProgress, action } = dialogData;
const { title, subtitle, action } = dialogData;
return showDialog((Dialogs, perform) => (
<Dialogs.ProgressDialog
title={title}
subtitle={subtitle}
message={message}
total={total}
setProgress={setProgress}
action={action}
onDone={(e) => perform(e)}
/>

View File

@@ -0,0 +1,61 @@
import download from "../utils/download";
import { db } from "./db";
import { TaskManager } from "./task-manager";
import { zip } from "../utils/zip";
async function exportToPDF(content: string): Promise<boolean> {
if (!content) return false;
const { default: printjs } = await import("print-js");
return new Promise(async (resolve) => {
printjs({
printable: content,
type: "raw-html",
onPrintDialogClose: () => {
resolve(false);
},
onError: () => resolve(false),
});
resolve(true);
});
}
export async function exportNotes(
format: "pdf" | "md" | "txt" | "html",
noteIds: string[]
): Promise<boolean> {
return await TaskManager.startTask({
type: "modal",
title: "Exporting notes",
subtitle: "Please wait while your notes are exported.",
action: async (report) => {
if (format === "pdf") {
const note = db.notes.note(noteIds[0]);
return await exportToPDF(await note.export("html"));
}
var files = [];
let index = 0;
for (var noteId of noteIds) {
const note = db.notes.note(noteId);
report({
current: ++index,
total: noteIds.length,
text: `Exporting "${note.title}"...`,
});
console.log("Exporting", note.title);
const content = await note.export(format).catch(() => {});
if (!content) continue;
files.push({ filename: note.title, content });
}
if (!files.length) return false;
if (files.length === 1) {
download(files[0].filename, files[0].content, format);
} else {
const zipped = zip(files, format);
download("notes", zipped, "zip");
}
return true;
},
});
}

View File

@@ -1,4 +1,3 @@
import SelectionOptions from "./selectionoptions";
import download from "../utils/download";
import {
showFeatureDialog,
@@ -32,14 +31,6 @@ export const SUBSCRIPTION_STATUS = {
PREMIUM_CANCELED: 7,
};
export const SELECTION_OPTIONS_MAP = {
notes: SelectionOptions.NotesOptions,
notebooks: SelectionOptions.NotebooksOptions,
favorites: SelectionOptions.FavoritesOptions,
trash: SelectionOptions.TrashOptions,
topics: SelectionOptions.TopicOptions,
};
export const CREATE_BUTTON_MAP = {
notes: {
title: "Make a note",

View File

@@ -0,0 +1,103 @@
import { removeStatus, updateStatus } from "../hooks/use-status";
import {
showMultiDeleteConfirmation,
showMultiPermanentDeleteConfirmation,
} from "./dialog-controller";
import { store as editorStore } from "../stores/editor-store";
import { store as appStore } from "../stores/app-store";
import { store as noteStore } from "../stores/note-store";
import { store as notebookStore } from "../stores/notebook-store";
import { db } from "./db";
import { hashNavigate } from "../navigation";
import { showToast } from "../utils/toast";
import Vault from "./vault";
import { showItemDeletedToast } from "./toasts";
import { TaskManager } from "./task-manager";
async function moveNotesToTrash(notes: any[]) {
const item = notes[0];
const isMultiselect = notes.length > 1;
if (isMultiselect) {
if (!(await showMultiDeleteConfirmation(notes.length))) return;
} else {
if (item.locked && !(await Vault.unlockNote(item.id))) return;
}
var isAnyNoteOpened = false;
const items = notes.map((item) => {
if (item.id === editorStore.get().session.id) isAnyNoteOpened = true;
if (item.locked || db.monographs.isPublished(item.id)) return 0;
return item.id;
});
if (isAnyNoteOpened) {
hashNavigate("/notes/create", { addNonce: true });
}
await TaskManager.startTask({
type: "status",
id: "deleteNotes",
action: async (report) => {
report({
text: `Deleting ${items.length} notes...`,
});
await noteStore.delete(...items);
},
});
if (isMultiselect) {
showToast("success", `${items.length} notes moved to trash`);
} else {
showItemDeletedToast(item);
}
}
async function moveNotebooksToTrash(notebooks: any[]) {
const item = notebooks[0];
const isMultiselect = notebooks.length > 1;
if (isMultiselect) {
if (!(await showMultiDeleteConfirmation(notebooks.length))) return;
} else {
if (item.locked && !(await Vault.unlockNote(item.id))) return;
}
await TaskManager.startTask({
type: "status",
id: "deleteNotebooks",
action: async (report) => {
report({
text: `Deleting ${notebooks.length} notebooks...`,
});
await notebookStore.delete(...notebooks.map((i) => i.id));
},
});
if (isMultiselect) {
showToast("success", `${notebooks.length} notebooks moved to trash`);
} else {
showItemDeletedToast(item);
}
}
async function deleteTopics(notebookId: string, topics: any[]) {
await TaskManager.startTask({
type: "status",
id: "deleteTopics",
action: async (report) => {
report({
text: `Deleting ${topics.length} topics...`,
});
await db.notebooks
.notebook(notebookId)
.topics.delete(...topics.map((t) => t.id));
notebookStore.setSelectedNotebook(notebookId);
},
});
showToast("success", `${topics.length} topics deleted`);
}
export const Multiselect = {
moveNotebooksToTrash,
moveNotesToTrash,
deleteTopics,
};

View File

@@ -1,167 +0,0 @@
import * as Icon from "../components/icons";
import { store as selectionStore } from "../stores/selection-store";
import { store as notesStore } from "../stores/note-store";
import { store as nbStore } from "../stores/notebook-store";
import { store as editorStore } from "../stores/editor-store";
import { store as appStore } from "../stores/app-store";
import { store as trashStore } from "../stores/trash-store";
import { db } from "./db";
import { showMoveNoteDialog } from "../common/dialog-controller";
import {
showMultiDeleteConfirmation,
showMultiPermanentDeleteConfirmation,
} from "../common/dialog-controller";
import { showExportDialog } from "../common/dialog-controller";
import { showToast } from "../utils/toast";
import { hashNavigate } from "../navigation";
import { removeStatus, updateStatus } from "../hooks/use-status";
function createOption(key, title, icon, onClick) {
return {
key,
icon,
title,
onClick: async () => {
await onClick.call(this, selectionStore.get());
selectionStore.toggleSelectionMode(false);
removeStatus(key);
},
};
}
function createOptions(options = []) {
return [...options, DeleteOption];
}
const DeleteOption = createOption(
"deleteOption",
"Delete",
Icon.Trash,
async function (state) {
const item = state.selectedItems[0];
const confirmDialog = item.dateDeleted
? showMultiPermanentDeleteConfirmation
: showMultiDeleteConfirmation;
if (!(await confirmDialog(state.selectedItems.length))) return;
const statusText = `${
item.dateDeleted ? `Permanently deleting` : `Deleting`
} ${state.selectedItems.length} items...`;
updateStatus({ key: "deleteOption", status: statusText });
var isAnyNoteOpened = false;
const items = state.selectedItems.map((item) => {
if (item.id === editorStore.get().session.id) isAnyNoteOpened = true;
if (item.locked || db.monographs.isPublished(item.id)) return 0;
return item.id;
});
if (item.dateDeleted) {
// we are in trash
await db.trash.delete(...items);
trashStore.refresh();
showToast("success", `${items.length} items permanently deleted!`);
return;
}
if (isAnyNoteOpened) {
hashNavigate("/notes/create", { addNonce: true });
}
if (item.type === "note") {
await db.notes.delete(...items);
} else if (item.type === "notebook") {
await db.notebooks.delete(...items);
} else if (item.type === "topic") {
await db.notebooks.notebook(item.notebookId).topics.delete(...items);
nbStore.setSelectedNotebook(item.notebookId);
}
appStore.refresh();
showToast("success", `${items.length} ${item.type}s moved to trash!`);
}
);
const UnfavoriteOption = createOption(
"unfavoriteOption",
"Unfavorite",
Icon.Star,
function (state) {
updateStatus({
key: "unfavoriteOption",
status: `Unfavoriting ${state.selectedItems.length} notes...`,
});
// we know only notes can be favorited
state.selectedItems.forEach(async (item) => {
if (!item.favorite) return;
await db.notes.note(item.id).favorite();
});
notesStore.setContext({ type: "favorites" });
}
);
const AddToNotebookOption = createOption(
"atnOption",
"Add to notebook(s)",
Icon.AddToNotebook,
async function (state) {
updateStatus({
key: "atnOption",
status: `Adding ${state.selectedItems.length} notes to notebooks...`,
});
const items = state.selectedItems.map((item) => item.id);
await showMoveNoteDialog(items);
showToast("success", `${items.length} notes moved!`);
}
);
const ExportOption = createOption(
"exportOption",
"Export",
Icon.Export,
async function (state) {
updateStatus({
key: "exportOption",
status: `Exporting ${state.selectedItems.length} notes...`,
});
const items = state.selectedItems.map((item) => item.id);
if (await showExportDialog(items)) {
await showToast("success", `${items.length} notes exported!`);
}
}
);
const RestoreOption = createOption(
"restoreOption",
"Restore",
Icon.Restore,
async function (state) {
updateStatus({
key: "restoreOption",
status: `Restoring ${state.selectedItems.length} items...`,
});
const items = state.selectedItems.map((item) => item.id);
await db.trash.restore(...items);
appStore.refresh();
showToast("success", `${items.length} items restored!`);
}
);
const NotesOptions = createOptions([AddToNotebookOption, ExportOption]);
const NotebooksOptions = createOptions();
const TopicOptions = createOptions();
const TrashOptions = createOptions([RestoreOption]);
const FavoritesOptions = createOptions([UnfavoriteOption]);
const SelectionOptions = {
NotebooksOptions,
NotesOptions,
TopicOptions,
TrashOptions,
FavoritesOptions,
};
export default SelectionOptions;

View File

@@ -0,0 +1,65 @@
import { removeStatus, updateStatus } from "../hooks/use-status";
import { showProgressDialog } from "./dialog-controller";
type TaskType = "status" | "modal";
type TaskAction<T> = (report: ProgressReportCallback) => T | Promise<T>;
type BaseTaskDefinition<TTaskType extends TaskType, TReturnType> = {
type: TTaskType;
action: TaskAction<TReturnType>;
};
type StatusTaskDefinition<TReturnType> = BaseTaskDefinition<
"status",
TReturnType
> & {
id: string;
};
type ModalTaskDefinition<TReturnType> = BaseTaskDefinition<
"modal",
TReturnType
> & {
title: string;
subtitle: string;
};
type TaskDefinition<TReturnType> =
| StatusTaskDefinition<TReturnType>
| ModalTaskDefinition<TReturnType>;
type TaskProgress = {
total?: number;
current?: number;
text: string;
};
type ProgressReportCallback = (progress: TaskProgress) => void;
export class TaskManager {
static async startTask<T>(task: TaskDefinition<T>): Promise<T> {
switch (task.type) {
case "status":
const statusTask = task;
const result = await statusTask.action((progress) => {
let percentage: number | undefined = undefined;
if (progress.current && progress.total)
percentage = Math.round((progress.current / progress.total) * 100);
updateStatus({
key: statusTask.id,
status: progress.text,
progress: percentage,
icon: null,
});
});
removeStatus(statusTask.id);
return result;
case "modal":
return await showProgressDialog({
title: task.title,
subtitle: task.subtitle,
action: task.action,
});
}
}
}

View File

@@ -57,25 +57,20 @@ function showItemDeletedToast(item) {
var toast = showToast("success", messageText, actions);
}
async function showPermanentDeleteToast(item) {
const noun = item.itemType === "note" ? "Note" : "Notebook";
const messageText = `${noun} permanently deleted!`;
const timeoutId = setTimeout(() => {
trashstore.delete(item.id, true);
trashstore.refresh();
}, 5000);
async function showUndoableToast(message, onAction, onUndo) {
const timeoutId = setTimeout(onAction, 5000);
const undoAction = async () => {
toast.hide();
trashstore.refresh();
onUndo();
clearTimeout(timeoutId);
};
let actions = [{ text: "Undo", onClick: undoAction }];
var toast = showToast("success", messageText, actions);
var toast = showToast("success", message, actions);
}
export {
showNotesMovedToast,
showUnpinnedToast,
showItemDeletedToast,
showPermanentDeleteToast,
showUndoableToast,
};

View File

@@ -1,152 +0,0 @@
import React, { useState } from "react";
import { Flex, Button, Text } from "rebass";
import * as Icon from "../icons";
import Dialog from "./dialog";
import download from "../../utils/download";
import { zip } from "../../utils/zip";
import { db } from "../../common/db";
const formats = [
{
type: "pdf",
title: "PDF",
icon: Icon.PDF,
subtitle:
"Can be opened in any PDF reader like Adobe Acrobat or Foxit Reader.",
},
{
type: "md",
title: "Markdown",
icon: Icon.Markdown,
subtitle: "Can be opened in any plain-text or markdown editor.",
},
{
type: "html",
title: "HTML",
icon: Icon.HTML,
subtitle: "Can be opened in any web browser like Google Chrome.",
},
{
type: "txt",
title: "Text",
icon: Icon.Text,
subtitle: "Can be opened in any plain-text editor.",
},
];
function ExportDialog(props) {
const { noteIds } = props;
const [progress, setProgress] = useState();
return (
<Dialog
isOpen={true}
title={props.title}
icon={props.icon}
description="You can export your notes as Markdown, HTML, PDF or Text."
onClose={props.onClose}
negativeButton={{
onClick: props.onClose,
text: "Cancel",
}}
>
<Flex my={2} flexDirection="row" justifyContent="center">
{progress ? (
<Flex>
<Text variant="body">
Processing note {progress} of {noteIds.length}
</Text>
</Flex>
) : (
<>
{formats.map(({ type, title, icon: Icon }) => {
if (type === "pdf" && noteIds.length > 1) return null;
return (
<Button
key={type}
data-test-id={`export-dialog-${type}`}
variant="tertiary"
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
mr: 2,
":last-of-type": { mr: 0 },
}}
onClick={async () => {
const format = type;
if (format === "pdf") {
const note = db.notes.note(noteIds[0]);
let result = await exportToPDF(await note.export("html"));
return result;
}
var files = [];
let index = 0;
for (var noteId of noteIds) {
setProgress(index++);
const note = db.notes.note(noteId);
const content = await note.export(format);
if (!content) continue;
files.push({ filename: note.title, content });
}
if (!files.length) return false;
if (files.length === 1) {
download(files[0].filename, files[0].content, format);
} else {
const zipped = await zip(files, format);
download("notes", zipped, "zip");
}
setProgress();
props.onDone();
}}
>
<Icon color="primary" size={48} />
<Text textAlign="center">{title}</Text>
</Button>
);
})}
</>
)}
</Flex>
</Dialog>
);
}
export default ExportDialog;
async function exportToPDF(content) {
if (!content) return false;
return new Promise((resolve) => {
return import("print-js").then(async ({ default: printjs }) => {
printjs({
printable: content,
type: "raw-html",
onPrintDialogClose: () => {
resolve();
},
});
return true;
// TODO
// const doc = new jsPDF("p", "px", "letter");
// const div = document.createElement("div");
// const { width, height } = doc.internal.pageSize;
// div.innerHTML = content;
// div.style.width = width - 80 + "px";
// div.style.height = height - 80 + "px";
// div.style.position = "absolute";
// div.style.top = 0;
// div.style.left = 0;
// div.style.margin = "40px";
// div.style.fontSize = "11px";
// document.body.appendChild(div);
// await doc.html(div, {
// callback: async (doc) => {
// div.remove();
// resolve(doc.output());
// },
// });
});
});
}

View File

@@ -2,7 +2,6 @@ import AddNotebookDialog from "./addnotebookdialog";
import BuyDialog from "./buy-dialog";
import Confirm from "./confirm";
import EmailVerificationDialog from "./emailverificationdialog";
import ExportDialog from "./exportdialog";
import ImportDialog from "./importdialog";
import LoadingDialog from "./loadingdialog";
import ProgressDialog from "./progressdialog";
@@ -22,7 +21,6 @@ const Dialogs = {
BuyDialog,
Confirm,
EmailVerificationDialog,
ExportDialog,
LoadingDialog,
MoveDialog,
PasswordDialog,

View File

@@ -3,23 +3,16 @@ import { Box, Flex, Text } from "rebass";
import Dialog from "./dialog";
function ProgressDialog(props) {
const [{ loaded, progress }, setProgress] = useState({
loaded: 0,
progress: 0,
const [{ current, total, text }, setProgress] = useState({
current: 0,
total: 1,
text: "",
});
useEffect(() => {
if (!props.setProgress) return;
const undo = props.setProgress(setProgress);
return () => {
undo && undo();
};
}, [props, setProgress]);
useEffect(() => {
(async function () {
try {
props.onDone(await props.action());
props.onDone(await props.action(setProgress));
} catch (e) {
props.onDone(e);
}
@@ -34,19 +27,23 @@ function ProgressDialog(props) {
onClose={() => {}}
>
<Flex flexDirection="column">
<Text variant="body">{props.message}</Text>
<Text variant="subBody">
{loaded} of {props.total}
</Text>
<Box
sx={{
alignSelf: "start",
my: 1,
bg: "primary",
height: "2px",
width: `${progress}%`,
}}
/>
<Text variant="body">{text}</Text>
{current > 0 && (
<>
<Text variant="subBody">
{current} of {total}
</Text>
<Box
sx={{
alignSelf: "start",
my: 1,
bg: "primary",
height: "2px",
width: `${(current / total) * 100}%`,
}}
/>
</>
)}
</Flex>
</Dialog>
);

View File

@@ -2,6 +2,7 @@ const { toTitleCase, toCamelCase } = require("../../utils/string");
var icons = undefined;
export function getIconFromAlias(alias) {
if (!alias) return;
const iconName = toTitleCase(toCamelCase(alias));
if (!icons) icons = require("./index");
return icons[iconName];

View File

@@ -5,7 +5,6 @@ import {
useStore as useSelectionStore,
} from "../../stores/selection-store";
import { useMenuTrigger } from "../../hooks/use-menu";
import { SELECTION_OPTIONS_MAP } from "../../common";
import Config from "../../utils/config";
import { db } from "../../common/db";
import * as clipboard from "clipboard-polyfill/text";
@@ -62,24 +61,20 @@ function ListItem(props) {
e.preventDefault();
let items = props.menu?.items || [];
let title = props.item.title;
let selectedItems = selectionStore.get().selectedItems.slice();
if (isSelected) {
const options = SELECTION_OPTIONS_MAP[window.currentViewType];
items = options.map((option) => {
return {
key: option.key,
title: () => option.title,
icon: option.icon,
onClick: option.onClick,
};
});
title = `${selectionStore.get().selectedItems.length} selected`;
title = `${selectedItems.length} items selected`;
items = items.filter((item) => item.multiSelect);
} else if (Config.get("debugMode", false)) {
items.push(...debugMenuItems(props.item.type));
} else {
selectedItems.push(props.item);
}
openMenu(items, {
title,
items: selectedItems,
...props.menu?.extraData,
});
}}
@@ -119,7 +114,7 @@ function ListItem(props) {
} else {
selectionStore.toggleSelectionMode(false);
selectItem(props.item);
props.onClick();
props.onClick && props.onClick();
}
}}
data-test-id={`${props.item.type}-${props.index}`}

View File

@@ -3,6 +3,7 @@ import { Flex, Text } from "rebass";
import { getPosition } from "../../hooks/use-menu";
import { FlexScrollContainer } from "../scroll-container";
import MenuItem from "./menu-item";
import { store as selectionStore } from "../../stores/selection-store";
function useMenuFocus(items, onAction) {
const [focusIndex, setFocusIndex] = useState(-1);
@@ -72,7 +73,11 @@ function Menu({ items, data, title, closeMenu }) {
(e, item) => {
e.stopPropagation();
if (closeMenu) closeMenu();
if (item.onClick) item.onClick(data, item);
if (item.onClick) {
item.onClick(data, item);
// TODO: this probably shouldn't be here.
selectionStore.toggleSelectionMode(false);
}
},
[closeMenu, data]
);
@@ -87,7 +92,7 @@ function Menu({ items, data, title, closeMenu }) {
const item = items[focusIndex];
if (!item) return;
const element = document.getElementById(item.key);
if (!element) return;
element.scrollIntoView({ behavior: "auto" });
}, [focusIndex, items]);

View File

@@ -15,6 +15,8 @@ import { showPublishView } from "../publish-view";
import Vault from "../../common/vault";
import IconTag from "../icon-tag";
import { COLORS } from "../../common";
import { exportNotes } from "../../common/export";
import { Multiselect } from "../../common/multi-select";
function Note(props) {
const { tags, notebook, item, index, context, attachments, date } = props;
@@ -94,6 +96,9 @@ function Note(props) {
data-test-id={`note-${index}-locked`}
/>
)}
{note.favorite && (
<Icon.Star color={primary} size={15} sx={{ mr: 1 }} />
)}
<TimeAgo live={true} datetime={date} locale="short" />
</>
) : (
@@ -229,22 +234,21 @@ const menuItems = [
onClick: async ({ note }) => {
await pin(note);
},
modifier: ["Alt", "P"],
},
{
key: "favorite",
title: ({ note }) => (note.favorite ? "Unfavorite" : "Favorite"),
icon: Icon.StarOutline,
onClick: ({ note }) => store.favorite(note),
modifier: ["Alt", "F"],
},
{
key: "addtonotebook",
title: "Add to notebook(s)",
icon: Icon.AddToNotebook,
onClick: async ({ note }) => {
await showMoveNoteDialog([note.id]);
onClick: async ({ items }) => {
await showMoveNoteDialog(items.map((i) => i.id));
},
multiSelect: true,
},
{
key: "colors",
@@ -264,7 +268,6 @@ const menuItems = [
},
})),
},
{
key: "publish",
disabled: ({ note }) => !db.monographs.isPublished(note.id) && note.locked,
@@ -287,10 +290,14 @@ const menuItems = [
title: format.title,
tooltip: `Export as ${format.title} - ${format.subtitle}`,
icon: format.icon,
onClick: ({ note }) => {
alert("TBI");
onClick: async ({ items }) => {
await exportNotes(
format.type,
items.map((i) => i.id)
);
},
})),
multiSelect: true,
isPro: true,
},
{
@@ -308,24 +315,20 @@ const menuItems = [
}
},
isPro: true,
modifier: ["Alt", "L"],
},
{
key: "movetotrash",
title: "Move to trash",
color: "red",
iconColor: "red",
icon: Icon.Trash,
disabled: ({ note }) => db.monographs.isPublished(note.id),
disabled: ({ items }) =>
items.length === 1 && db.monographs.isPublished(items[0].id),
disableReason: "Please unpublish this note to move it to trash",
onClick: async ({ note }) => {
if (note.locked) {
if (!(await Vault.unlockNote(note.id))) return;
}
await store.delete(note.id).then(() => showItemDeletedToast(note));
onClick: async ({ items }) => {
await Multiselect.moveNotesToTrash(items);
},
modifier: ["Delete"],
multiSelect: true,
},
];
@@ -336,13 +339,14 @@ const topicNoteMenuItems = [
title: "Remove from topic",
icon: Icon.TopicRemove,
color: "red",
onClick: async ({ note, context }) => {
iconColor: "red",
onClick: async ({ items, context }) => {
await db.notebooks
.notebook(context.value.id)
.topics.topic(context.value.topic)
.delete(note.id);
.delete(...items.map((i) => i.id));
store.refresh();
await showToast("success", "Note removed from topic!");
},
multiSelect: true,
},
];

View File

@@ -9,6 +9,7 @@ import * as Icon from "../icons";
import { hashNavigate, navigate } from "../../navigation";
import IconTag from "../icon-tag";
import { showToast } from "../../utils/toast";
import { Multiselect } from "../../common/multi-select";
function Notebook(props) {
const { item, index, totalNotes, date } = props;
@@ -124,11 +125,11 @@ const menuItems = [
{
title: "Move to trash",
color: "red",
iconColor: "red",
icon: Icon.Trash,
onClick: async ({ notebook }) => {
await store
.delete(notebook.id)
.then(() => showItemDeletedToast(notebook));
onClick: async ({ items }) => {
await Multiselect.moveNotebooksToTrash(items);
},
multiSelect: true,
},
];

View File

@@ -25,16 +25,19 @@ const menuItems = [
},
{
color: "error",
iconColor: "error",
title: "Delete",
icon: Icon.DeleteForver,
onClick: async ({ tag }) => {
if (tag.noteIds.includes(editorStore.get().session.id))
editorStore.clearSession();
await db.tags.remove(tag.id);
showToast("success", "Tag deleted!");
onClick: async ({ items }) => {
for (let tag of items) {
if (tag.noteIds.includes(editorStore.get().session.id))
await editorStore.clearSession();
await db.tags.remove(tag.id);
}
showToast("success", `${items.length} tags deleted`);
tagStore.refresh();
},
multiSelect: true,
},
];

View File

@@ -6,6 +6,7 @@ import { store as appStore } from "../../stores/app-store";
import { hashNavigate } from "../../navigation";
import { Flex, Text } from "rebass";
import * as Icon from "../icons";
import { Multiselect } from "../../common/multi-select";
function Topic({ item, index, onClick }) {
const topic = item;
@@ -32,7 +33,7 @@ function Topic({ item, index, onClick }) {
index={index}
menu={{
items: menuItems,
extraData: { topic },
extraData: { topic, notebookId: topic.notebookId },
}}
/>
);
@@ -45,16 +46,6 @@ export default React.memo(Topic, (prev, next) => {
);
});
const generalTopicMenuItems = [
{
key: "shortcut",
title: ({ topic }) =>
db.settings.isPinned(topic.id) ? "Remove shortcut" : "Create shortcut",
icon: Icon.Shortcut,
onClick: ({ topic }) => appStore.pinItemToMenu(topic),
},
];
const menuItems = [
{
key: "edit",
@@ -63,15 +54,22 @@ const menuItems = [
onClick: ({ topic }) =>
hashNavigate(`/notebooks/${topic.notebookId}/topics/${topic.id}/edit`),
},
...generalTopicMenuItems,
{
key: "shortcut",
title: ({ topic }) =>
db.settings.isPinned(topic.id) ? "Remove shortcut" : "Create shortcut",
icon: Icon.Shortcut,
onClick: ({ topic }) => appStore.pinItemToMenu(topic),
},
{
key: "delete",
title: "Delete",
icon: Icon.Trash,
color: "red",
onClick: async ({ topic }) => {
await db.notebooks.notebook(topic.notebookId).topics.delete(topic.id);
store.setSelectedNotebook(topic.notebookId);
color: "error",
iconColor: "error",
onClick: async ({ items, notebookId }) => {
await Multiselect.deleteTopics(notebookId, items);
},
multiSelect: true,
},
];

View File

@@ -1,13 +1,16 @@
import React from "react";
import ListItem from "../list-item";
import { confirm } from "../../common/dialog-controller";
import {
confirm,
showMultiPermanentDeleteConfirmation,
} from "../../common/dialog-controller";
import * as Icon from "../icons";
import { store } from "../../stores/trash-store";
import { Flex, Text } from "rebass";
import TimeAgo from "../time-ago";
import { toTitleCase } from "../../utils/string";
import { showUndoableToast } from "../../common/toasts";
import { showToast } from "../../utils/toast";
import { showPermanentDeleteToast } from "../../common/toasts";
function TrashItem({ item, index, date }) {
return (
@@ -36,44 +39,25 @@ const menuItems = [
{
title: "Restore",
icon: Icon.Restore,
onClick: ({ item }) => {
store.restore(item.id);
showToast(
"success",
`${
item.itemType === "note" ? "Note" : "Notebook"
} restored successfully!`
);
onClick: ({ items }) => {
store.restore(items.map((i) => i.id));
showToast("success", `${items.length} items restored`);
},
multiSelect: true,
},
{
title: "Delete",
icon: Icon.DeleteForver,
color: "red",
onClick: ({ item }) => {
confirm({
title: `Permanently delete ${item.itemType}`,
subtitle: `Are you sure you want to permanently delete this ${item.itemType}?`,
yesText: `Delete`,
noText: "Cancel",
message: (
<>
This action is{" "}
<Text as="span" color="error">
IRREVERSIBLE
</Text>
. You will{" "}
<Text as="span" color="error">
not be able to recover this {item.itemType}.
</Text>
</>
),
}).then(async (res) => {
if (res) {
await store.delete(item.id);
showPermanentDeleteToast(item);
}
});
onClick: async ({ items }) => {
if (!(await showMultiPermanentDeleteConfirmation(items.length))) return;
const ids = items.map((i) => i.id);
showUndoableToast(
`${items.length} items permanently deleted`,
() => store.delete(ids),
() => store.delete(ids, true)
);
},
multiSelect: true,
},
];

1
apps/web/src/react-app-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@@ -68,13 +68,15 @@ class NoteStore extends BaseStore {
});
};
delete = async (id) => {
delete = async (...ids) => {
const { session, clearSession } = editorStore.get();
if (session && session.id === id) {
await clearSession();
for (let id of ids) {
if (session && session.id === id) {
await clearSession();
}
}
await db.notes.delete(id);
await db.notes.delete(ids);
this.refresh();
appStore.refreshNavItems();

View File

@@ -27,8 +27,8 @@ class NotebookStore extends BaseStore {
this.setSelectedNotebook(this.get().selectedNotebookId);
};
delete = async (id) => {
await db.notebooks.delete(id);
delete = async (...ids) => {
await db.notebooks.delete(...ids);
this.refresh();
appStore.refreshNavItems();
noteStore.refresh();

View File

@@ -18,18 +18,20 @@ class TrashStore extends BaseStore {
);
};
delete = (id, commit = false) => {
delete = (ids, commit = false) => {
if (!commit) {
return this.set((state) => {
const index = state.trash.findIndex((item) => item.id === id);
if (index > -1) state.trash.splice(index, 1);
for (let id of ids) {
const index = state.trash.findIndex((item) => item.id === id);
if (index > -1) state.trash.splice(index, 1);
}
});
}
return db.trash.delete(id);
return db.trash.delete(...ids);
};
restore = (id) => {
return db.trash.restore(id).then(() => {
restore = (ids) => {
return db.trash.restore(...ids).then(() => {
this.refresh();
appStore.refreshNavItems();
notestore.refresh();

22
apps/web/tsconfig.json Normal file
View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"downlevelIteration": true,
"maxNodeModuleJsDepth": 1,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}