mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-23 19:49:56 +01:00
refactor: handle multiselect actions directly
This commit is contained in:
@@ -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)}
|
||||
/>
|
||||
|
||||
61
apps/web/src/common/export.ts
Normal file
61
apps/web/src/common/export.ts
Normal 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;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
103
apps/web/src/common/multi-select.ts
Normal file
103
apps/web/src/common/multi-select.ts
Normal 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,
|
||||
};
|
||||
@@ -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;
|
||||
65
apps/web/src/common/task-manager.ts
Normal file
65
apps/web/src/common/task-manager.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
// },
|
||||
// });
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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
1
apps/web/src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
22
apps/web/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user