mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 19:49:54 +02:00
web: add support for top-level notes in notebooks
This commit is contained in:
committed by
Abdullah Atta
parent
9804c1fbb1
commit
3f55422d45
@@ -41,7 +41,7 @@ import { EVENTS } from "@notesnook/core/common";
|
||||
|
||||
export const CREATE_BUTTON_MAP = {
|
||||
notes: {
|
||||
title: "Make a note",
|
||||
title: "Add a note",
|
||||
onClick: () =>
|
||||
hashNavigate("/notes/create", { addNonce: true, replace: true })
|
||||
},
|
||||
@@ -49,6 +49,10 @@ export const CREATE_BUTTON_MAP = {
|
||||
title: "Create a notebook",
|
||||
onClick: () => hashNavigate("/notebooks/create", { replace: true })
|
||||
},
|
||||
notebook: {
|
||||
title: "Add a note",
|
||||
onClick: () => hashNavigate(`/notes/create`, { replace: true })
|
||||
},
|
||||
topics: {
|
||||
title: "Create a topic",
|
||||
onClick: () => hashNavigate(`/topics/create`, { replace: true })
|
||||
|
||||
@@ -35,7 +35,7 @@ import { usePersistentState } from "../../hooks/use-persistent-state";
|
||||
type MoveDialogProps = { onClose: Perform; noteIds: string[] };
|
||||
type NotebookReference = {
|
||||
id: string;
|
||||
topic: string;
|
||||
topic?: string;
|
||||
new: boolean;
|
||||
op: "add" | "remove";
|
||||
};
|
||||
@@ -95,6 +95,19 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const notebook of noteIds
|
||||
.map((id) => db.relations?.to({ id, type: "note" }, "notebook"))
|
||||
.flat()) {
|
||||
if (!notebook) continue;
|
||||
|
||||
selected.push({
|
||||
id: notebook.id,
|
||||
op: "add",
|
||||
new: false
|
||||
});
|
||||
}
|
||||
|
||||
setSelected(selected);
|
||||
setIsMultiselect(false);
|
||||
}, [noteIds, notebooks, setSelected, setIsMultiselect]);
|
||||
@@ -128,19 +141,13 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
|
||||
|
||||
notestore.refresh();
|
||||
|
||||
const addedTopics = selected.filter((a) => a.op === "add").length;
|
||||
const removedTopics = selected.filter(
|
||||
(a) => a.op === "remove"
|
||||
).length;
|
||||
|
||||
showToast(
|
||||
"success",
|
||||
`${pluralize(noteIds.length, "note", "notes")} added to ${pluralize(
|
||||
addedTopics,
|
||||
"topic",
|
||||
"topics"
|
||||
)} & removed from ${pluralize(removedTopics, "topic", "topics")}.`
|
||||
);
|
||||
const stringified = stringifySelected(selected);
|
||||
if (stringified) {
|
||||
showToast(
|
||||
"success",
|
||||
stringified.replace("Add", "Added").replace("remove", "removed")
|
||||
);
|
||||
}
|
||||
|
||||
onClose(true);
|
||||
}
|
||||
@@ -226,10 +233,12 @@ function NotebookItem(props: {
|
||||
}) {
|
||||
const { notebook, isSearching, onCreateItem } = props;
|
||||
|
||||
const { selected, setIsMultiselect, setSelected } = useSelectionStore();
|
||||
const { selected, setIsMultiselect, setSelected, isMultiselect } =
|
||||
useSelectionStore();
|
||||
|
||||
const [isCreatingNew, setIsCreatingNew] = useState(false);
|
||||
const isSelected = isNotebookSelected(notebook, selected);
|
||||
const index = findSelectionIndex(notebook, selected);
|
||||
const isSelected = index > -1;
|
||||
|
||||
return (
|
||||
<Box as="li" data-test-id="notebook">
|
||||
@@ -263,10 +272,14 @@ function NotebookItem(props: {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
setIsMultiselect(true);
|
||||
setSelected(
|
||||
selectNotebook(notebook, selected, isSelected || false)
|
||||
);
|
||||
const isCtrlPressed = e.ctrlKey || e.metaKey;
|
||||
if (isCtrlPressed) setIsMultiselect(true);
|
||||
|
||||
if (isMultiselect || isCtrlPressed) {
|
||||
setSelected(selectMultiple(notebook, selected));
|
||||
} else {
|
||||
setSelected(selectSingle(notebook, selected));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectedCheck size={20} selected={isSelected} />
|
||||
@@ -384,9 +397,9 @@ function TopicItem(props: { topic: Topic }) {
|
||||
if (isCtrlPressed) setIsMultiselect(true);
|
||||
|
||||
if (isMultiselect || isCtrlPressed) {
|
||||
setSelected(selectMultipleTopics(topic, selected));
|
||||
setSelected(selectMultiple(topic, selected));
|
||||
} else {
|
||||
setSelected(selectSingleTopic(topic, selected));
|
||||
setSelected(selectSingle(topic, selected));
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -545,22 +558,24 @@ function SelectedCheck({
|
||||
);
|
||||
}
|
||||
|
||||
function createSelection(topic: Topic): NotebookReference {
|
||||
function createSelection(topic: Topic | Notebook): NotebookReference {
|
||||
return {
|
||||
id: topic.notebookId,
|
||||
topic: topic.id,
|
||||
id: "notebookId" in topic ? topic.notebookId : topic.id,
|
||||
topic: "notebookId" in topic ? topic.id : undefined,
|
||||
op: "add",
|
||||
new: true
|
||||
};
|
||||
}
|
||||
|
||||
function findSelectionIndex(
|
||||
topic: Topic | NotebookReference,
|
||||
topic: Topic | NotebookReference | Notebook,
|
||||
array: NotebookReference[]
|
||||
) {
|
||||
return "op" in topic
|
||||
? array.findIndex((a) => a.id === topic.id && a.topic === topic.topic)
|
||||
: array.findIndex((a) => a.id === topic.notebookId && a.topic === topic.id);
|
||||
: "notebookId" in topic
|
||||
? array.findIndex((a) => a.id === topic.notebookId && a.topic === topic.id)
|
||||
: array.findIndex((a) => a.id === topic.id && !a.topic);
|
||||
}
|
||||
|
||||
function topicHasNotes(topic: Item, noteIds: string[]) {
|
||||
@@ -568,36 +583,10 @@ function topicHasNotes(topic: Item, noteIds: string[]) {
|
||||
return noteIds.some((id) => notes.indexOf(id) > -1);
|
||||
}
|
||||
|
||||
// There are 3 cases:
|
||||
// 1. Click on a notebook to select/deselect all its topics
|
||||
// 2. Click on a topic to select/deselect it (and deselect all other topics)
|
||||
// 3. Ctrl+click on a topic to enter multi select
|
||||
|
||||
function selectNotebook(
|
||||
notebook: Notebook,
|
||||
selected: NotebookReference[],
|
||||
isNotebookSelected: boolean
|
||||
function selectMultiple(
|
||||
topic: Topic | Notebook,
|
||||
selected: NotebookReference[]
|
||||
) {
|
||||
for (const topic of notebook.topics) {
|
||||
const index = findSelectionIndex(topic, selected);
|
||||
const item = selected[index];
|
||||
|
||||
// 1. first reset the item's selection state
|
||||
// 2. set the new state
|
||||
|
||||
if (item?.new) selected.splice(index, 1);
|
||||
else if (item && !item.new) item.op = "remove";
|
||||
|
||||
if (!isNotebookSelected) {
|
||||
if (!item || item.new) selected.push(createSelection(topic));
|
||||
else if (item && !item.new) item.op = "add";
|
||||
}
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
|
||||
function selectMultipleTopics(topic: Topic, selected: NotebookReference[]) {
|
||||
const index = findSelectionIndex(topic, selected);
|
||||
const isSelected = index > -1;
|
||||
const item = selected[index];
|
||||
@@ -613,7 +602,7 @@ function selectMultipleTopics(topic: Topic, selected: NotebookReference[]) {
|
||||
return selected;
|
||||
}
|
||||
|
||||
function selectSingleTopic(topic: Topic, array: NotebookReference[]) {
|
||||
function selectSingle(topic: Topic | Notebook, array: NotebookReference[]) {
|
||||
const selected: NotebookReference[] = array.filter((ref) => !ref.new);
|
||||
|
||||
const index = findSelectionIndex(topic, array);
|
||||
@@ -630,19 +619,6 @@ function selectSingleTopic(topic: Topic, array: NotebookReference[]) {
|
||||
return selected;
|
||||
}
|
||||
|
||||
function isNotebookSelected(notebook: Notebook, selected: NotebookReference[]) {
|
||||
const selectedTopics = notebook.topics.filter((topic) => {
|
||||
const index = findSelectionIndex(topic, selected);
|
||||
return selected[index]?.op === "add";
|
||||
});
|
||||
|
||||
return !selectedTopics.length
|
||||
? false
|
||||
: selectedTopics.length === notebook.topics.length
|
||||
? true
|
||||
: null;
|
||||
}
|
||||
|
||||
function stringifySelected(suggestion: NotebookReference[]) {
|
||||
const added = suggestion
|
||||
.filter((a) => a.op === "add")
|
||||
@@ -660,7 +636,7 @@ function stringifySelected(suggestion: NotebookReference[]) {
|
||||
|
||||
if (removed.length >= 1) {
|
||||
parts.push("remove from");
|
||||
parts.push(added[0]);
|
||||
parts.push(removed[0]);
|
||||
}
|
||||
if (removed.length > 1) parts.push(`and ${removed.length - 1} others`);
|
||||
|
||||
@@ -668,7 +644,12 @@ function stringifySelected(suggestion: NotebookReference[]) {
|
||||
}
|
||||
|
||||
function resolve(ref: NotebookReference) {
|
||||
const topic = db.notebooks?.notebook(ref.id)?.topics.topic(ref.topic);
|
||||
if (!topic) return undefined;
|
||||
return topic._topic.title;
|
||||
const notebook = db.notebooks?.notebook(ref.id);
|
||||
if (!notebook) return undefined;
|
||||
|
||||
if (ref.topic) {
|
||||
return notebook.topics.topic(ref.topic)?._topic?.title;
|
||||
} else {
|
||||
return notebook.title;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ type ListContainerProps = {
|
||||
context?: Context;
|
||||
refresh: () => void;
|
||||
header?: JSX.Element;
|
||||
placeholder: () => JSX.Element;
|
||||
placeholder: JSX.Element;
|
||||
isLoading?: boolean;
|
||||
button?: {
|
||||
onClick: () => void;
|
||||
@@ -136,7 +136,7 @@ function ListContainer(props: ListContainerProps) {
|
||||
<ListLoader />
|
||||
) : (
|
||||
<Flex variant="columnCenterFill" data-test-id="list-placeholder">
|
||||
<props.placeholder />
|
||||
{props.placeholder}
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -28,6 +28,7 @@ import { db } from "../../common/db";
|
||||
import { getTotalNotes } from "../../common";
|
||||
import Reminder from "../reminder";
|
||||
import type { Reminder as ReminderType } from "@notesnook/core/collections/reminders";
|
||||
import { useMemo } from "react";
|
||||
|
||||
const SINGLE_LINE_HEIGHT = 1.4;
|
||||
const DEFAULT_LINE_HEIGHT =
|
||||
@@ -60,19 +61,26 @@ type ItemWrapper<TItem = Item> = (
|
||||
props: ItemWrapperProps<TItem>
|
||||
) => JSX.Element;
|
||||
|
||||
const NotesProfile: ItemWrapper = ({ index, item, type, context, compact }) => (
|
||||
<Note
|
||||
compact={compact}
|
||||
index={index}
|
||||
pinnable={!context}
|
||||
item={item}
|
||||
tags={getTags(item)}
|
||||
notebook={getNotebook(item.notebooks as Item[], context?.type)}
|
||||
reminder={getReminder(item.id)}
|
||||
date={getDate(item, type)}
|
||||
context={context}
|
||||
/>
|
||||
);
|
||||
const NotesProfile: ItemWrapper = ({ index, item, type, context, compact }) => {
|
||||
const references = useMemo(
|
||||
() => getReferences(item.id, item.notebooks as Item[], context?.type),
|
||||
[item, context]
|
||||
);
|
||||
|
||||
return (
|
||||
<Note
|
||||
compact={compact}
|
||||
index={index}
|
||||
pinnable={!context}
|
||||
item={item}
|
||||
tags={getTags(item)}
|
||||
references={references}
|
||||
reminder={getReminder(item.id)}
|
||||
date={getDate(item, type)}
|
||||
context={context}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const NotebooksProfile: ItemWrapper = ({ index, item, type }) => (
|
||||
<Notebook
|
||||
@@ -129,26 +137,36 @@ function getTags(item: Item) {
|
||||
return tags || [];
|
||||
}
|
||||
|
||||
type NotebookResult =
|
||||
| {
|
||||
id: string;
|
||||
title: string;
|
||||
dateEdited: number;
|
||||
topic: { id: string; title: string };
|
||||
}
|
||||
| undefined;
|
||||
type Reference = {
|
||||
type: "topic" | "notebook";
|
||||
url: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
function getNotebook(
|
||||
function getReferences(
|
||||
noteId: string,
|
||||
notebooks: Item[],
|
||||
contextType?: string
|
||||
): NotebookResult | undefined {
|
||||
if (contextType === "topic" || !notebooks?.length) return;
|
||||
): { dateEdited: number; references: Reference[] } | undefined {
|
||||
if (["topic", "notebook"].includes(contextType || "")) return;
|
||||
|
||||
return notebooks.reduce<NotebookResult>(function (
|
||||
prev: NotebookResult,
|
||||
curr
|
||||
): NotebookResult {
|
||||
if (prev) return prev;
|
||||
const references: Reference[] = [];
|
||||
let latestDateEdited = 0;
|
||||
|
||||
db.relations
|
||||
?.to({ id: noteId, type: "note" }, "notebook")
|
||||
?.forEach((notebook: any) => {
|
||||
references.push({
|
||||
type: "notebook",
|
||||
url: `/notebooks/${notebook.id}`,
|
||||
title: notebook.title
|
||||
} as Reference);
|
||||
|
||||
if (latestDateEdited < notebook.dateEdited)
|
||||
latestDateEdited = notebook.dateEdited;
|
||||
});
|
||||
|
||||
notebooks?.forEach((curr) => {
|
||||
const topicId = (curr as NotebookReference).topics[0];
|
||||
const notebook = db.notebooks?.notebook(curr.id)?.data as NotebookType;
|
||||
if (!notebook) return;
|
||||
@@ -156,14 +174,16 @@ function getNotebook(
|
||||
const topic = notebook.topics.find((t: Item) => t.id === topicId);
|
||||
if (!topic) return;
|
||||
|
||||
return {
|
||||
id: notebook.id,
|
||||
title: notebook.title,
|
||||
dateEdited: notebook.dateEdited,
|
||||
topic: { id: topicId, title: topic.title }
|
||||
} as NotebookResult;
|
||||
},
|
||||
undefined as NotebookResult);
|
||||
references.push({
|
||||
url: `/notebooks/${curr.id}/${topicId}`,
|
||||
title: topic.title,
|
||||
type: "topic"
|
||||
});
|
||||
if (latestDateEdited < (topic.dateEdited as number))
|
||||
latestDateEdited = topic.dateEdited as number;
|
||||
});
|
||||
|
||||
return { dateEdited: latestDateEdited, references: references.slice(0, 3) };
|
||||
}
|
||||
|
||||
function getReminder(noteId: string) {
|
||||
|
||||
@@ -138,7 +138,7 @@ function ListItem(props) {
|
||||
|
||||
backgroundColor: isSelected
|
||||
? "shade"
|
||||
: isMenuTarget
|
||||
: isMenuTarget || isFocused
|
||||
? "hover"
|
||||
: background,
|
||||
|
||||
@@ -167,9 +167,11 @@ function ListItem(props) {
|
||||
}}
|
||||
data-test-id={`list-item`}
|
||||
>
|
||||
{!isCompact && props.header}
|
||||
|
||||
<Text
|
||||
data-test-id={`title`}
|
||||
variant={isSimple ? "body" : "subtitle"}
|
||||
variant={isSimple || isCompact ? "body" : "subtitle"}
|
||||
sx={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
@@ -182,8 +184,6 @@ function ListItem(props) {
|
||||
{props.title}
|
||||
</Text>
|
||||
|
||||
{!isCompact && props.header}
|
||||
|
||||
{!isSimple && !isCompact && props.body && (
|
||||
<Text
|
||||
as="p"
|
||||
|
||||
@@ -49,7 +49,7 @@ import {
|
||||
function Note(props) {
|
||||
const {
|
||||
tags,
|
||||
notebook,
|
||||
references,
|
||||
item,
|
||||
index,
|
||||
context,
|
||||
@@ -115,15 +115,16 @@ function Note(props) {
|
||||
<Flex
|
||||
sx={{ alignItems: "center", flexWrap: "wrap", gap: 1, mt: "small" }}
|
||||
>
|
||||
{notebook && (
|
||||
{references?.references?.map((reference) => (
|
||||
<IconTag
|
||||
key={reference.url}
|
||||
onClick={() => {
|
||||
navigate(`/notebooks/${notebook.id}/${notebook.topic.id}`);
|
||||
navigate(reference.url);
|
||||
}}
|
||||
text={`${notebook.title} › ${notebook.topic.title}`}
|
||||
icon={Icon.Notebook}
|
||||
text={reference.title}
|
||||
icon={reference.type === "topic" ? Icon.Topic : Icon.Notebook}
|
||||
/>
|
||||
)}
|
||||
))}
|
||||
{reminder && isReminderActive(reminder) && (
|
||||
<IconTag
|
||||
icon={Icon.Reminder}
|
||||
@@ -260,7 +261,7 @@ export default React.memo(Note, function (prevProps, nextProps) {
|
||||
prevItem.conflicted === nextItem.conflicted &&
|
||||
prevItem.color === nextItem.color &&
|
||||
prevProps.compact === nextProps.compact &&
|
||||
prevProps.notebook?.dateEdited === nextProps.notebook?.dateEdited &&
|
||||
prevProps.references?.dateEdited === nextProps.references?.dateEdited &&
|
||||
prevProps.reminder?.dateModified === nextProps.reminder?.dateModified &&
|
||||
JSON.stringify(prevProps.tags) === JSON.stringify(nextProps.tags) &&
|
||||
JSON.stringify(prevProps.context) === JSON.stringify(nextProps.context)
|
||||
@@ -543,7 +544,9 @@ function notebooksMenuItems({ note }) {
|
||||
}
|
||||
});
|
||||
|
||||
if (note && note.notebooks?.length > 0) {
|
||||
const notebooks = db.relations?.to(note, "notebook");
|
||||
|
||||
if (note && (note.notebooks?.length > 0 || notebooks?.length > 0)) {
|
||||
menuItems.push(
|
||||
{
|
||||
key: "remove-from-all-notebooks",
|
||||
@@ -556,16 +559,28 @@ function notebooksMenuItems({ note }) {
|
||||
},
|
||||
{ key: "sep", type: "separator" }
|
||||
);
|
||||
note.notebooks.forEach((ref) => {
|
||||
// if (prev) return prev;
|
||||
// const topicId = (curr as NotebookReference).topics[0];
|
||||
|
||||
notebooks?.forEach((notebook) => {
|
||||
menuItems.push({
|
||||
key: notebook.id,
|
||||
title: notebook.title,
|
||||
icon: Icon.Notebook,
|
||||
checked: true,
|
||||
tooltip: "Click to remove from this notebook",
|
||||
onClick: async () => {
|
||||
await db.notes.removeFromNotebook({ id: notebook.id }, note.id);
|
||||
store.refresh();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
note.notebooks?.forEach((ref) => {
|
||||
const notebook = db.notebooks?.notebook(ref.id);
|
||||
if (!notebook) return;
|
||||
const notebookMenuItems = [];
|
||||
for (const topicId of ref.topics) {
|
||||
if (!notebook.topics.topic(topicId)) continue;
|
||||
const topic = notebook.topics.topic(topicId)._topic;
|
||||
notebookMenuItems.push({
|
||||
menuItems.push({
|
||||
key: topicId,
|
||||
title: topic.title,
|
||||
icon: Icon.Topic,
|
||||
@@ -580,13 +595,6 @@ function notebooksMenuItems({ note }) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
key: ref.id,
|
||||
title: notebook.title,
|
||||
icon: Icon.Notebook2,
|
||||
items: notebookMenuItems
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import * as Icon from "../icons";
|
||||
import { Flex, Text } from "@theme-ui/components";
|
||||
import { useStore, store } from "../../stores/editor-store";
|
||||
@@ -86,12 +86,25 @@ function Properties(props) {
|
||||
const attachments = useAttachmentStore((store) =>
|
||||
store.attachments.filter((a) => a.noteIds.includes(session.id))
|
||||
);
|
||||
const { id: sessionId, color, notebooks, sessionType, dateCreated } = session;
|
||||
const {
|
||||
id: sessionId,
|
||||
color,
|
||||
notebooks = [],
|
||||
sessionType,
|
||||
dateCreated
|
||||
} = session;
|
||||
const isPreviewMode = sessionType === "preview";
|
||||
const reminders = db.relations.from(
|
||||
{ id: session.id, type: "note" },
|
||||
"reminder"
|
||||
);
|
||||
const allNotebooks = useMemo(
|
||||
() => [
|
||||
...notebooks.map((ref) => db.notebooks.notebook(ref.id)?.data),
|
||||
...db.relations.to({ id: sessionId, type: "note" }, "notebook")
|
||||
],
|
||||
[sessionId, notebooks]
|
||||
);
|
||||
|
||||
const changeState = useCallback(
|
||||
function changeState(prop) {
|
||||
@@ -246,22 +259,17 @@ function Properties(props) {
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
{notebooks?.length > 0 && (
|
||||
{allNotebooks?.length > 0 && (
|
||||
<Card title="Notebooks">
|
||||
{notebooks.map((ref) => {
|
||||
const notebook = db.notebooks.notebook(ref.id)?._notebook;
|
||||
if (!notebook) return null;
|
||||
|
||||
return (
|
||||
<Notebook
|
||||
key={ref.id}
|
||||
item={notebook}
|
||||
date={notebook.dateCreated}
|
||||
totalNotes={getTotalNotes(notebook)}
|
||||
simplified
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{allNotebooks.map((notebook) => (
|
||||
<Notebook
|
||||
key={notebook.id}
|
||||
item={notebook}
|
||||
date={notebook.dateCreated}
|
||||
totalNotes={getTotalNotes(notebook)}
|
||||
simplified
|
||||
/>
|
||||
))}
|
||||
</Card>
|
||||
)}
|
||||
{reminders?.length > 0 && (
|
||||
|
||||
@@ -25,12 +25,16 @@ import { hashNavigate } from "../../navigation";
|
||||
import { Flex, Text } from "@theme-ui/components";
|
||||
import * as Icon from "../icons";
|
||||
import { Multiselect } from "../../common/multi-select";
|
||||
import { pluralize } from "../../utils/string";
|
||||
import { confirm } from "../../common/dialog-controller";
|
||||
import { useStore as useNotesStore } from "../../stores/note-store";
|
||||
|
||||
function Topic({ item, index, onClick }) {
|
||||
const { id, notebookId } = item;
|
||||
const topic = item;
|
||||
const isOpened = useNotesStore(
|
||||
(store) => store.context?.value?.topic === item.id
|
||||
);
|
||||
|
||||
const totalNotes = useMemo(() => {
|
||||
return db.notebooks.notebook(notebookId)?.topics.topic(id).totalNotes;
|
||||
}, [id, notebookId]);
|
||||
@@ -38,6 +42,8 @@ function Topic({ item, index, onClick }) {
|
||||
return (
|
||||
<ListItem
|
||||
selectable
|
||||
isFocused={isOpened}
|
||||
isCompact
|
||||
item={topic}
|
||||
onClick={onClick}
|
||||
title={topic.title}
|
||||
@@ -49,9 +55,7 @@ function Topic({ item, index, onClick }) {
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Text variant="subBody">
|
||||
{pluralize(totalNotes || 0, "note", "notes")}
|
||||
</Text>
|
||||
<Text variant="subBody">{totalNotes}</Text>
|
||||
</Flex>
|
||||
}
|
||||
index={index}
|
||||
|
||||
@@ -29,7 +29,6 @@ import { navigate } from "../navigation";
|
||||
import Trash from "../views/trash";
|
||||
import { store as notestore } from "../stores/note-store";
|
||||
import { store as nbstore } from "../stores/notebook-store";
|
||||
import { showToast } from "../utils/toast";
|
||||
import Reminders from "../views/reminders";
|
||||
|
||||
const routes = {
|
||||
@@ -59,9 +58,14 @@ const routes = {
|
||||
const notebook = db.notebooks.notebook(notebookId);
|
||||
if (!notebook) return false;
|
||||
nbstore.setSelectedNotebook(notebookId);
|
||||
notestore.setContext({
|
||||
type: "notebook",
|
||||
value: { id: notebookId }
|
||||
});
|
||||
|
||||
return {
|
||||
key: "topics",
|
||||
type: "topics",
|
||||
key: "notebook",
|
||||
type: "notebook",
|
||||
component: <Topics />,
|
||||
buttons: {
|
||||
back: {
|
||||
@@ -69,7 +73,7 @@ const routes = {
|
||||
action: () => navigate("/notebooks")
|
||||
},
|
||||
search: {
|
||||
title: `Search ${notebook.title} topics`
|
||||
title: `Search ${notebook.title} notes`
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -83,16 +87,10 @@ const routes = {
|
||||
value: { id: notebookId, topic: topicId }
|
||||
});
|
||||
return {
|
||||
key: "notes",
|
||||
type: "notes",
|
||||
key: "topic",
|
||||
type: "notebook",
|
||||
title: topic.title,
|
||||
subtitle: notebook.title,
|
||||
isEditable: true,
|
||||
onChange: (title) => {
|
||||
db.notebooks.notebook(notebookId).topics.add({ ...topic, title });
|
||||
showToast("success", "Topic title updated!");
|
||||
},
|
||||
component: <Notes />,
|
||||
component: <Topics />,
|
||||
buttons: {
|
||||
back: {
|
||||
title: `Go back to ${notebook.title}`,
|
||||
|
||||
@@ -187,7 +187,8 @@ class EditorStore extends BaseStore {
|
||||
|
||||
if (currentSession.context) {
|
||||
const { type, value } = currentSession.context;
|
||||
if (type === "topic") await db.notes.addToNotebook(value, id);
|
||||
if (type === "topic" || type === "notebook")
|
||||
await db.notes.addToNotebook(value, id);
|
||||
else if (type === "color") await db.notes.note(id).color(value);
|
||||
else if (type === "tag") await db.notes.note(id).tag(value);
|
||||
// update the note.
|
||||
|
||||
@@ -205,6 +205,12 @@ function notesFromContext(context) {
|
||||
case "color":
|
||||
notes = db.notes.colored(context.value);
|
||||
break;
|
||||
case "notebook": {
|
||||
const notebook = db.notebooks.notebook(context?.value?.id);
|
||||
if (!notebook) break;
|
||||
notes = db.relations.from(notebook.data, "note");
|
||||
break;
|
||||
}
|
||||
case "topic": {
|
||||
const notebook = db.notebooks.notebook(context?.value?.id);
|
||||
if (!notebook) break;
|
||||
|
||||
@@ -27,7 +27,6 @@ import Config from "../utils/config";
|
||||
|
||||
class NotebookStore extends BaseStore {
|
||||
notebooks = [];
|
||||
selectedNotebookTopics = [];
|
||||
selectedNotebookId = 0;
|
||||
viewMode = Config.get("notebooks:viewMode", "detailed");
|
||||
|
||||
@@ -60,13 +59,7 @@ class NotebookStore extends BaseStore {
|
||||
};
|
||||
|
||||
setSelectedNotebook = (id) => {
|
||||
const topics = db.notebooks.notebook(id)?.topics?.all;
|
||||
if (!topics) return;
|
||||
this.set((state) => {
|
||||
state.selectedNotebookTopics = groupArray(
|
||||
topics,
|
||||
db.settings.getGroupOptions("topics")
|
||||
);
|
||||
state.selectedNotebookId = id;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -17,49 +17,183 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, 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";
|
||||
import { hashNavigate } from "../navigation";
|
||||
import TopicsPlaceholder from "../components/placeholders/topics-placeholder";
|
||||
import { hashNavigate, navigate } from "../navigation";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import { Edit, RemoveShortcutLink, ShortcutLink } from "../components/icons";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Edit,
|
||||
RemoveShortcutLink,
|
||||
ShortcutLink
|
||||
} 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";
|
||||
|
||||
function Notebook() {
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
function Topics() {
|
||||
const selectedNotebookTopics = useNbStore(
|
||||
(store) => store.selectedNotebookTopics
|
||||
);
|
||||
const selectedNotebookId = useNbStore((store) => store.selectedNotebookId);
|
||||
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)
|
||||
refresh(context.value.id);
|
||||
}, [selectedNotebookId, context, refresh]);
|
||||
|
||||
const selectedNotebook = useMemo(
|
||||
() => db.notebooks?.notebook(selectedNotebookId)?.data,
|
||||
[selectedNotebookId, notebooks]
|
||||
);
|
||||
|
||||
if (!context) return null;
|
||||
return (
|
||||
<>
|
||||
{context.type === "topic" && selectedNotebook ? (
|
||||
<Flex sx={{ alignItems: "center", mx: 2, mb: 1 }}>
|
||||
{[
|
||||
{ title: "Notebooks", onClick: () => navigate(`/notebooks/`) },
|
||||
{
|
||||
title: selectedNotebook.title,
|
||||
onClick: () => navigate(`/notebooks/${selectedNotebookId}`)
|
||||
}
|
||||
].map((crumb, index, array) => (
|
||||
<>
|
||||
<Button
|
||||
variant="anchor"
|
||||
sx={{
|
||||
fontSize: "subBody",
|
||||
textDecoration: "none",
|
||||
color: "fontTertiary"
|
||||
}}
|
||||
onClick={crumb.onClick}
|
||||
>
|
||||
{crumb.title}
|
||||
</Button>
|
||||
{index === array.length - 1 ? null : (
|
||||
<ChevronRight size={18} color="fontTertiary" />
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</Flex>
|
||||
) : null}
|
||||
<Allotment vertical>
|
||||
<Allotment.Pane>
|
||||
<Flex variant="columnFill" sx={{ height: "100%" }}>
|
||||
<ListContainer
|
||||
type="notes"
|
||||
groupType={"notes"}
|
||||
refresh={refreshContext}
|
||||
compact={isCompact}
|
||||
context={{ ...context, notes: undefined }}
|
||||
items={context.notes}
|
||||
placeholder={<Placeholder context="notes" />}
|
||||
header={
|
||||
context?.type === "topic" ? (
|
||||
<></>
|
||||
) : (
|
||||
<NotebookHeader notebook={selectedNotebook} />
|
||||
)
|
||||
}
|
||||
button={{
|
||||
content: "Make a new note",
|
||||
onClick: () =>
|
||||
hashNavigate("/notes/create", {
|
||||
addNonce: true,
|
||||
replace: true
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Allotment.Pane>
|
||||
<Allotment.Pane
|
||||
preferredSize={250}
|
||||
visible
|
||||
maxSize={isCollapsed ? 30 : Infinity}
|
||||
>
|
||||
<Topics
|
||||
selectedNotebook={selectedNotebook}
|
||||
isCollapsed={isCollapsed}
|
||||
setIsCollapsed={setIsCollapsed}
|
||||
/>
|
||||
</Allotment.Pane>
|
||||
</Allotment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default Notebook;
|
||||
|
||||
function Topics({ selectedNotebook, isCollapsed, setIsCollapsed }) {
|
||||
const refresh = useNbStore((store) => store.setSelectedNotebook);
|
||||
return (
|
||||
<Flex variant="columnFill" sx={{ height: "100%" }}>
|
||||
<Flex
|
||||
sx={{
|
||||
m: 1,
|
||||
ml: 2,
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
cursor: "pointer"
|
||||
}}
|
||||
onClick={() => setIsCollapsed((s) => !s)}
|
||||
>
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
{isCollapsed ? (
|
||||
<ChevronRight size={16} color="fontTertiary" />
|
||||
) : (
|
||||
<ChevronDown size={16} color="fontTertiary" />
|
||||
)}
|
||||
<Text variant="subBody" sx={{ fontSize: 11 }}>
|
||||
TOPICS
|
||||
</Text>
|
||||
</Flex>
|
||||
<Button
|
||||
variant="tool"
|
||||
sx={{
|
||||
p: "1px",
|
||||
bg: "transparent",
|
||||
visibility: isCollapsed ? "collapse" : "visible"
|
||||
}}
|
||||
onClick={(e) => {
|
||||
hashNavigate(`/topics/create`);
|
||||
}}
|
||||
>
|
||||
<Plus size={20} />
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<ListContainer
|
||||
type="topics"
|
||||
groupType="topics"
|
||||
refresh={() => refresh(selectedNotebookId)}
|
||||
items={selectedNotebookTopics}
|
||||
context={{ notebookId: selectedNotebookId }}
|
||||
placeholder={TopicsPlaceholder}
|
||||
header={
|
||||
<NotebookHeader
|
||||
notebook={db.notebooks.notebook(selectedNotebookId).data}
|
||||
/>
|
||||
}
|
||||
refresh={() => refresh(selectedNotebook.id)}
|
||||
items={selectedNotebook.topics}
|
||||
context={{
|
||||
notebookId: selectedNotebook.id
|
||||
}}
|
||||
placeholder={<Placeholder context="topics" />}
|
||||
header={<></>}
|
||||
button={{
|
||||
content: "Add a new topic",
|
||||
onClick: () => hashNavigate(`/topics/create`)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
export default Topics;
|
||||
|
||||
function NotebookHeader({ notebook }) {
|
||||
const { title, description, topics, dateEdited } = notebook;
|
||||
@@ -73,7 +207,7 @@ function NotebookHeader({ notebook }) {
|
||||
}, [shortcuts, notebook]);
|
||||
|
||||
return (
|
||||
<Flex mx={2} my={2} sx={{ flexDirection: "column" }}>
|
||||
<Flex mx={2} my={2} sx={{ flexDirection: "column", minWidth: 200 }}>
|
||||
<Text variant="subBody">{formatDate(dateEdited)}</Text>
|
||||
<Flex sx={{ alignItems: "center", justifyContent: "space-between" }}>
|
||||
<Text variant="heading">{title}</Text>
|
||||
|
||||
Reference in New Issue
Block a user