>({} as any);
+
+ return (
+
+ );
+}
+
+export default ConfirmDialog;
diff --git a/apps/web/src/components/dialogs/issue-dialog.tsx b/apps/web/src/components/dialogs/issue-dialog.tsx
index 7eddd259f..6c6a44d13 100644
--- a/apps/web/src/components/dialogs/issue-dialog.tsx
+++ b/apps/web/src/components/dialogs/issue-dialog.tsx
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-import { Flex, Link, Text } from "@theme-ui/components";
+import { Flex, Text } from "@theme-ui/components";
import { appVersion } from "../../utils/version";
import Field from "../field";
import Dialog from "./dialog";
@@ -152,33 +152,14 @@ export default IssueDialog;
function showIssueReportedDialog({ url }: { url: string }) {
return confirm({
title: "Thank you for reporting!",
- yesAction: () => clipboard.writeText(url),
- yesText: "Copy link",
- message: (
- <>
-
- You can track your bug report at{" "}
-
- {url}
-
- .
-
-
- Please note that we will respond to your bug report on the link above.{" "}
-
- We recommended that you save the above link for later reference.
-
-
-
- If your issue is critical (e.g. notes not syncing, crashes etc.),
- please{" "}
-
- join our Discord community
- {" "}
- for one-to-one support.
-
- >
- )
+ positiveButtonText: "Copy link",
+ message: `You can track your bug report at [${url}](${url}).
+
+ Please note that we will respond to your bug report on the link above. **We recommended that you save the above link for later reference.**
+
+ If your issue is critical (e.g. notes not syncing, crashes etc.), please [join our Discord community](https://discord.com/invite/zQBK97EE22) for one-to-one support.`
+ }).then((result) => {
+ result && clipboard.writeText(url);
});
}
diff --git a/apps/web/src/components/dialogs/move-note-dialog.tsx b/apps/web/src/components/dialogs/move-note-dialog.tsx
index 46b715df6..440fbd05c 100644
--- a/apps/web/src/components/dialogs/move-note-dialog.tsx
+++ b/apps/web/src/components/dialogs/move-note-dialog.tsx
@@ -145,7 +145,9 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
if (stringified) {
showToast(
"success",
- stringified.replace("Add", "Added").replace("remove", "removed")
+ `${pluralize(noteIds.length, "note", "notes")} ${stringified
+ .replace("Add", "added")
+ .replace("remove", "removed")}`
);
}
@@ -635,12 +637,12 @@ function stringifySelected(suggestion: NotebookReference[]) {
if (added.length > 1) parts.push(`and ${added.length - 1} others`);
if (removed.length >= 1) {
- parts.push("remove from");
+ parts.push("& remove from");
parts.push(removed[0]);
}
if (removed.length > 1) parts.push(`and ${removed.length - 1} others`);
- return parts.join(" ");
+ return parts.join(" ") + ".";
}
function resolve(ref: NotebookReference) {
diff --git a/apps/web/src/components/group-header/index.js b/apps/web/src/components/group-header/index.js
index e4d124390..3c113d99e 100644
--- a/apps/web/src/components/group-header/index.js
+++ b/apps/web/src/components/group-header/index.js
@@ -21,7 +21,7 @@ import * as Icon from "../icons";
import { useEffect, useMemo, useRef, useState } from "react";
import { Button, Flex, Text } from "@theme-ui/components";
import { db } from "../../common/db";
-import { useMenuTrigger } from "../../hooks/use-menu";
+import { Menu, useMenuTrigger } from "../../hooks/use-menu";
import { useStore as useNoteStore } from "../../stores/note-store";
import { useStore as useNotebookStore } from "../../stores/notebook-store";
import useMobile from "../../hooks/use-mobile";
@@ -36,84 +36,97 @@ const groupByToTitleMap = {
month: "Month"
};
-const menuItems = [
- {
- key: "sortDirection",
- title: "Order by",
- icon: ({ groupOptions }) =>
- groupOptions.sortDirection === "asc"
- ? groupOptions.sortBy === "title"
- ? Icon.OrderAtoZ
- : Icon.OrderOldestNewest
- : groupOptions.sortBy === "title"
- ? Icon.OrderZtoA
- : Icon.OrderNewestOldest,
- items: map([
- {
- key: "asc",
- title: ({ groupOptions }) =>
- groupOptions.sortBy === "title" ? "A - Z" : "Oldest - newest"
- },
- {
- key: "desc",
- title: ({ groupOptions }) =>
- groupOptions.sortBy === "title" ? "Z - A" : "Newest - oldest"
+const groupByMenu = {
+ key: "groupBy",
+ title: "Group by",
+ icon: Icon.GroupBy,
+ items: map([
+ { key: "none", title: "None" },
+ { key: "default", title: "Default" },
+ { key: "year", title: "Year" },
+ { key: "month", title: "Month" },
+ { key: "week", title: "Week" },
+ { key: "abc", title: "A - Z" }
+ ])
+};
+
+const orderByMenu = {
+ key: "sortDirection",
+ title: "Order by",
+ icon: ({ groupOptions }) =>
+ groupOptions.sortDirection === "asc"
+ ? groupOptions.sortBy === "title"
+ ? Icon.OrderAtoZ
+ : Icon.OrderOldestNewest
+ : groupOptions.sortBy === "title"
+ ? Icon.OrderZtoA
+ : Icon.OrderNewestOldest,
+ items: map([
+ {
+ key: "asc",
+ title: ({ groupOptions }) =>
+ groupOptions.sortBy === "title" ? "A - Z" : "Oldest - newest"
+ },
+ {
+ key: "desc",
+ title: ({ groupOptions }) =>
+ groupOptions.sortBy === "title" ? "Z - A" : "Newest - oldest"
+ }
+ ])
+};
+
+const sortByMenu = {
+ key: "sortBy",
+ title: "Sort by",
+ icon: Icon.SortBy,
+ items: map([
+ {
+ key: "dateCreated",
+ title: "Date created",
+ hidden: ({ type }) => type === "trash"
+ },
+ {
+ key: "dateEdited",
+ title: "Date edited",
+ hidden: ({ type }) => type === "trash" || type === "tags"
+ },
+ {
+ key: "dateDeleted",
+ title: "Date deleted",
+ hidden: ({ type }) => type !== "trash"
+ },
+ {
+ key: "dateModified",
+ title: "Date modified",
+ hidden: ({ type }) => type !== "tags"
+ },
+ {
+ key: "title",
+ title: "Title",
+ hidden: ({ groupOptions, parent, isUngrouped }, item) => {
+ if (isUngrouped) return false;
+
+ return (
+ parent?.key === "sortBy" &&
+ item.key === "title" &&
+ groupOptions.groupBy !== "abc" &&
+ groupOptions.groupBy !== "none"
+ );
}
- ])
- },
- {
- key: "sortBy",
- title: "Sort by",
- icon: Icon.SortBy,
- items: map([
- {
- key: "dateCreated",
- title: "Date created",
- hidden: ({ type }) => type === "trash"
- },
- {
- key: "dateEdited",
- title: "Date edited",
- hidden: ({ type }) => type === "trash" || type === "tags"
- },
- {
- key: "dateDeleted",
- title: "Date deleted",
- hidden: ({ type }) => type !== "trash"
- },
- {
- key: "dateModified",
- title: "Date modified",
- hidden: ({ type }) => type !== "tags"
- },
- {
- key: "title",
- title: "Title",
- hidden: ({ groupOptions, parent }, item) => {
- return (
- parent?.key === "sortBy" &&
- item.key === "title" &&
- groupOptions.groupBy !== "abc" &&
- groupOptions.groupBy !== "none"
- );
- }
- }
- ])
- },
- {
- key: "groupBy",
- title: "Group by",
- icon: Icon.GroupBy,
- items: map([
- { key: "none", title: "None" },
- { key: "default", title: "Default" },
- { key: "year", title: "Year" },
- { key: "month", title: "Month" },
- { key: "week", title: "Week" },
- { key: "abc", title: "A - Z" }
- ])
- }
-];
+ }
+ ])
+};
+
+export function showSortMenu(type, refresh) {
+ const groupOptions = db.settings.getGroupOptions(type);
+ Menu.openMenu([orderByMenu, sortByMenu], {
+ title: "Sort",
+ groupOptions,
+ refresh,
+ type,
+ isUngrouped: true
+ });
+}
function changeGroupOptions({ groupOptions, type, refresh, parent }, item) {
if (!parent) return false;
@@ -132,19 +145,10 @@ function isChecked({ groupOptions, parent }, item) {
return groupOptions[parent.key] === item.key;
}
-function isDisabled({ groupOptions, parent }, item) {
- return (
- parent?.key === "sortBy" &&
- item.key === "title" &&
- groupOptions.groupBy === "abc"
- );
-}
-
function map(items) {
return items.map((item) => {
item.checked = isChecked;
item.onClick = changeGroupOptions;
- item.disabled = isDisabled;
return item;
}, []);
}
@@ -248,7 +252,7 @@ function GroupHeader(props) {
{type && (
{
- const phrase = items.length > 1 ? "this notebook" : "these notebooks";
- const shouldDeleteNotes = await confirm({
- title: `Delete notes in ${phrase}?`,
- message: `These notes will be moved to trash and permanently deleted after 7 days.`,
- yesText: `Yes`,
- noText: "No"
- });
-
- if (shouldDeleteNotes) {
- const notes = [];
- for (const item of items) {
- const topics = db.notebooks.notebook(item.id).topics;
- for (const topic of topics.all) {
- notes.push(...topics.topic(topic.id).all);
+ const result = await confirm({
+ title: `Delete ${pluralize(items.length, "notebook", "notebooks")}?`,
+ positiveButtonText: `Yes`,
+ negativeButtonText: "No",
+ checks: {
+ deleteContainingNotes: {
+ text: `Delete all containing notes`
}
}
- await Multiselect.moveNotesToTrash(notes, false);
+ });
+
+ if (result) {
+ if (result.deleteContainingNotes) {
+ const notes = [];
+ for (const item of items) {
+ notes.push(...db.relations.from(item, "note"));
+ const topics = db.notebooks.notebook(item.id).topics;
+ for (const topic of topics.all) {
+ notes.push(...topics.topic(topic.id).all);
+ }
+ }
+ await Multiselect.moveNotesToTrash(notes, false);
+ }
+ await Multiselect.moveNotebooksToTrash(items);
}
- await Multiselect.moveNotebooksToTrash(items);
},
multiSelect: true
}
diff --git a/apps/web/src/components/reminder/index.tsx b/apps/web/src/components/reminder/index.tsx
index 7f483fc77..f44a2b9aa 100644
--- a/apps/web/src/components/reminder/index.tsx
+++ b/apps/web/src/components/reminder/index.tsx
@@ -19,7 +19,7 @@ along with this program. If not, see .
import React from "react";
import ListItem from "../list-item";
-import { Flex, Text } from "@theme-ui/components";
+import { Flex } from "@theme-ui/components";
import * as Icon from "../icons";
import IconTag from "../icon-tag";
import {
@@ -164,15 +164,11 @@ const menuItems: MenuItem[] = [
onClick: async ({ items }) => {
confirm({
title: `Delete ${pluralize(items.length, "reminder", "reminders")}`,
- message: (
-
- Are you sure you want to proceed?
- This action is IRREVERSIBLE.
-
- ),
- yesText: "Yes",
- noText: "No",
- yesAction: () => Multiselect.moveRemindersToTrash(items)
+ message: `Are you sure you want to proceed? **This action is IRREVERSIBLE**.`,
+ positiveButtonText: "Yes",
+ negativeButtonText: "No"
+ }).then((result) => {
+ result && Multiselect.moveRemindersToTrash(items);
});
},
multiSelect: true
diff --git a/apps/web/src/components/topic/index.js b/apps/web/src/components/topic/index.js
index 4de13cbd7..41f4e06b3 100644
--- a/apps/web/src/components/topic/index.js
+++ b/apps/web/src/components/topic/index.js
@@ -27,6 +27,7 @@ import * as Icon from "../icons";
import { Multiselect } from "../../common/multi-select";
import { confirm } from "../../common/dialog-controller";
import { useStore as useNotesStore } from "../../stores/note-store";
+import { pluralize } from "../../utils/string";
function Topic({ item, index, onClick }) {
const { id, notebookId } = item;
@@ -36,7 +37,7 @@ function Topic({ item, index, onClick }) {
);
const totalNotes = useMemo(() => {
- return db.notebooks.notebook(notebookId)?.topics.topic(id).totalNotes;
+ return db.notebooks.notebook(notebookId)?.topics.topic(id)?.totalNotes || 0;
}, [id, notebookId]);
return (
@@ -94,23 +95,30 @@ const menuItems = [
color: "error",
iconColor: "error",
onClick: async ({ items, notebookId }) => {
- const phrase = items.length > 1 ? "this topic" : "these topics";
- const shouldDeleteNotes = await confirm({
- title: `Delete notes in ${phrase}?`,
- message: `These notes will be moved to trash and permanently deleted after 7 days.`,
- yesText: `Yes`,
- noText: "No"
+ const result = await confirm({
+ title: `Delete ${pluralize(items.length, "topic", "topics")}?`,
+ positiveButtonText: `Yes`,
+ negativeButtonText: "No",
+ checks: {
+ deleteContainingNotes: {
+ text: `Delete all containing notes`
+ }
+ }
});
- if (shouldDeleteNotes) {
- const notes = [];
- for (const item of items) {
- const topic = db.notebooks.notebook(notebookId).topics.topic(item.id);
- notes.push(...topic.all);
+ if (result) {
+ if (result.deleteContainingNotes) {
+ const notes = [];
+ for (const item of items) {
+ const topic = db.notebooks
+ .notebook(notebookId)
+ .topics.topic(item.id);
+ notes.push(...topic.all);
+ }
+ await Multiselect.moveNotesToTrash(notes, false);
}
- await Multiselect.moveNotesToTrash(notes, false);
+ await Multiselect.deleteTopics(notebookId, items);
}
- await Multiselect.deleteTopics(notebookId, items);
},
multiSelect: true
}
diff --git a/apps/web/src/hooks/use-menu.js b/apps/web/src/hooks/use-menu.js
index ff5ca44e0..f2e357280 100644
--- a/apps/web/src/hooks/use-menu.js
+++ b/apps/web/src/hooks/use-menu.js
@@ -62,6 +62,13 @@ export function useMenuTrigger() {
};
}
+export const Menu = {
+ openMenu: (items, data) => useMenuStore.getState().open(items, data),
+ closeMenu: () => useMenuStore.getState().close(),
+ isOpen: () => useMenuStore.getState().isOpen,
+ target: () => useMenuStore.getState().target
+};
+
export function useMenu() {
const [items, data] = useMenuStore((store) => [store.items, store.data]);
return { items, data };
diff --git a/apps/web/src/navigation/routes.js b/apps/web/src/navigation/routes.js
index 88ab6f727..7bc738637 100644
--- a/apps/web/src/navigation/routes.js
+++ b/apps/web/src/navigation/routes.js
@@ -87,7 +87,7 @@ const routes = {
value: { id: notebookId, topic: topicId }
});
return {
- key: "topic",
+ key: "notebook",
type: "notebook",
title: topic.title,
component: ,
diff --git a/apps/web/src/stores/notebook-store.js b/apps/web/src/stores/notebook-store.js
index 2b26e4e01..254dd99c5 100644
--- a/apps/web/src/stores/notebook-store.js
+++ b/apps/web/src/stores/notebook-store.js
@@ -27,7 +27,8 @@ import Config from "../utils/config";
class NotebookStore extends BaseStore {
notebooks = [];
- selectedNotebookId = 0;
+ selectedNotebook = undefined;
+ selectedNotebookTopics = [];
viewMode = Config.get("notebooks:viewMode", "detailed");
setViewMode = (viewMode) => {
@@ -42,7 +43,7 @@ class NotebookStore extends BaseStore {
db.settings.getGroupOptions("notebooks")
);
});
- this.setSelectedNotebook(this.get().selectedNotebookId);
+ this.setSelectedNotebook(this.get().selectedNotebook?.id);
};
delete = async (...ids) => {
@@ -59,8 +60,16 @@ class NotebookStore extends BaseStore {
};
setSelectedNotebook = (id) => {
+ if (!id) return;
+ const notebook = db.notebooks?.notebook(id)?.data;
+ if (!notebook) return;
+
this.set((state) => {
- state.selectedNotebookId = id;
+ state.selectedNotebook = notebook;
+ state.selectedNotebookTopics = groupArray(
+ notebook.topics,
+ db.settings.getGroupOptions("topics")
+ );
});
};
}
diff --git a/apps/web/src/utils/changelog.ts b/apps/web/src/utils/changelog.ts
index 8ab313aba..ebc935c89 100644
--- a/apps/web/src/utils/changelog.ts
+++ b/apps/web/src/utils/changelog.ts
@@ -17,38 +17,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-import { marked } from "marked";
-
-const emoji: marked.TokenizerExtension & marked.RendererExtension = {
- name: "emoji",
- level: "inline",
- start(src) {
- return src.indexOf(":");
- },
- tokenizer(src, _tokens) {
- const rule = /^:(\w+):/;
- const match = rule.exec(src);
- if (match) {
- return {
- type: "emoji",
- raw: match[0],
- emoji: match[1]
- };
- }
- },
- renderer(token) {
- return ``;
- }
-};
-
-const renderer = new marked.Renderer();
-renderer.link = function (href, title, text) {
- return `${text}`;
-};
-marked.use({ extensions: [emoji] });
-
export async function getChangelog(tag: string) {
try {
if (!tag) return "No changelog found.";
@@ -63,7 +31,7 @@ export async function getChangelog(tag: string) {
if (!release || !release.body) return "No changelog found.";
const { body } = release;
- return await marked.parse(body, { async: true, renderer, gfm: true });
+ return body;
} catch (e) {
console.error(e);
return "No changelog found.";
diff --git a/apps/web/src/utils/md.ts b/apps/web/src/utils/md.ts
new file mode 100644
index 000000000..b5894ba1a
--- /dev/null
+++ b/apps/web/src/utils/md.ts
@@ -0,0 +1,54 @@
+/*
+This file is part of the Notesnook project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+import { marked } from "marked";
+
+const emoji: marked.TokenizerExtension & marked.RendererExtension = {
+ name: "emoji",
+ level: "inline",
+ start(src) {
+ return src.indexOf(":");
+ },
+ tokenizer(src, _tokens) {
+ const rule = /^:(\w+):/;
+ const match = rule.exec(src);
+ if (match) {
+ return {
+ type: "emoji",
+ raw: match[0],
+ emoji: match[1]
+ };
+ }
+ },
+ renderer(token) {
+ return ``;
+ }
+};
+
+const renderer = new marked.Renderer();
+renderer.link = function (href, title, text) {
+ return `${text}`;
+};
+marked.use({ extensions: [emoji] });
+
+export function mdToHtml(markdown: string) {
+ return marked.parse(markdown, { async: false, renderer, gfm: true });
+}
diff --git a/apps/web/src/views/auth.tsx b/apps/web/src/views/auth.tsx
index 1534de29a..9a3a83d2b 100644
--- a/apps/web/src/views/auth.tsx
+++ b/apps/web/src/views/auth.tsx
@@ -755,6 +755,7 @@ function MFASelector(props: BaseAuthComponentProps<"mfa:select">) {
(method, index) =>
isValidMethod(method.type) && (
.
*/
-import { useEffect, useMemo, useState } from "react";
+import { useEffect, useState } from "react";
import ListContainer from "../components/list-container";
import { useStore as useNbStore } from "../stores/notebook-store";
import { useStore as useAppStore } from "../stores/app-store";
@@ -28,37 +28,37 @@ import {
ChevronRight,
Edit,
RemoveShortcutLink,
- ShortcutLink
+ ShortcutLink,
+ SortAsc
} from "../components/icons";
import { getTotalNotes } from "../common";
import { formatDate } from "@notesnook/core/utils/date";
-import { db } from "../common/db";
import { pluralize } from "../utils/string";
import { Allotment } from "allotment";
import { Plus } from "../components/icons";
import { useStore as useNotesStore } from "../stores/note-store";
import Placeholder from "../components/placeholders";
+import { showSortMenu } from "../components/group-header";
function Notebook() {
const [isCollapsed, setIsCollapsed] = useState(false);
- const selectedNotebookId = useNbStore((store) => store.selectedNotebookId);
+ const selectedNotebook = useNbStore((store) => store.selectedNotebook);
const refresh = useNbStore((store) => store.setSelectedNotebook);
- const notebooks = useNbStore((store) => store.notebooks);
const context = useNotesStore((store) => store.context);
const refreshContext = useNotesStore((store) => store.refreshContext);
const isCompact = useNotesStore((store) => store.viewMode === "compact");
useEffect(() => {
- if (context && context.value && selectedNotebookId !== context.value.id)
+ if (
+ context &&
+ context.value &&
+ selectedNotebook &&
+ selectedNotebook.id !== context.value.id
+ )
refresh(context.value.id);
- }, [selectedNotebookId, context, refresh]);
-
- const selectedNotebook = useMemo(
- () => db.notebooks?.notebook(selectedNotebookId)?.data,
- [selectedNotebookId, notebooks]
- );
+ }, [selectedNotebook, context, refresh]);
if (!context) return null;
return (
@@ -69,7 +69,7 @@ function Notebook() {
{ title: "Notebooks", onClick: () => navigate(`/notebooks/`) },
{
title: selectedNotebook.title,
- onClick: () => navigate(`/notebooks/${selectedNotebookId}`)
+ onClick: () => navigate(`/notebooks/${selectedNotebook.id}`)
}
].map((crumb, index, array) => (
<>
@@ -139,8 +139,11 @@ export default Notebook;
function Topics({ selectedNotebook, isCollapsed, setIsCollapsed }) {
const refresh = useNbStore((store) => store.setSelectedNotebook);
+ const topics = useNbStore((store) => store.selectedNotebookTopics);
+
+ if (!selectedNotebook) return null;
return (
-
+
-
+
+
+
+
refresh(selectedNotebook.id)}
- items={selectedNotebook.topics}
+ items={topics}
context={{
notebookId: selectedNotebook.id
}}
diff --git a/apps/web/src/views/trash.js b/apps/web/src/views/trash.js
index 7cd9d7f4a..5a2d6c1e8 100644
--- a/apps/web/src/views/trash.js
+++ b/apps/web/src/views/trash.js
@@ -51,20 +51,9 @@ function Trash() {
confirm({
title: "Clear Trash",
subtitle: "Are you sure you want to clear all the trash?",
- yesText: "Clear trash",
- noText: "Cancel",
- message: (
- <>
- This action is{" "}
-
- IRREVERSIBLE
-
- . You will{" "}
-
- not be able to recover any of these items.
-
- >
- )
+ positiveButtonText: "Clear trash",
+ negativeButtonText: "Cancel",
+ message: `Are you sure you want to proceed? **This action is IRREVERSIBLE**.`
}).then(async (res) => {
if (res) {
try {