mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-24 04:00:59 +01:00
core: get rid of topics; move to sub notebooks
This commit is contained in:
@@ -166,9 +166,7 @@ describe.each([
|
||||
db.notebooks.all.every((v) => v.title != null && v.dateModified > 0)
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
db.notebooks.all.every((v) => v.topics.every((topic) => !topic.notes))
|
||||
).toBeTruthy();
|
||||
expect(db.notebooks.all.every((v) => !v.topics)).toBeTruthy();
|
||||
|
||||
expect(
|
||||
db.attachments.all.every((v) => v.dateModified > 0 && !v.dateEdited)
|
||||
|
||||
@@ -64,10 +64,3 @@ test("search notebooks", () =>
|
||||
let filtered = db.lookup.notebooks(db.notebooks.all, "Description");
|
||||
expect(filtered.length).toBeGreaterThan(0);
|
||||
}));
|
||||
|
||||
test("search topics", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
const topics = db.notebooks.topics(id).all;
|
||||
let filtered = db.lookup.topics(topics, "hello");
|
||||
expect(filtered).toHaveLength(1);
|
||||
}));
|
||||
|
||||
@@ -17,10 +17,8 @@ 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 { notebookTest, TEST_NOTEBOOK, TEST_NOTE, delay } from "./utils";
|
||||
import { makeTopic } from "../src/collections/topics";
|
||||
import { notebookTest, TEST_NOTEBOOK } from "./utils";
|
||||
import { test, expect } from "vitest";
|
||||
import qclone from "qclone";
|
||||
|
||||
test("add a notebook", () =>
|
||||
notebookTest().then(({ db, id }) => {
|
||||
@@ -58,135 +56,3 @@ test("updating notebook with empty title should throw", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
await expect(db.notebooks.add({ id, title: "" })).rejects.toThrow();
|
||||
}));
|
||||
|
||||
test("merge notebook with new topics", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
let notebook = db.notebooks.notebook(id);
|
||||
|
||||
const newNotebook = db.notebooks.merge(notebook.data, {
|
||||
...notebook.data,
|
||||
topics: [...notebook.data.topics, makeTopic({ title: "Home" }, id)],
|
||||
remote: true
|
||||
});
|
||||
|
||||
expect(
|
||||
newNotebook.topics.findIndex((t) => t.title === "Home")
|
||||
).toBeGreaterThanOrEqual(0);
|
||||
expect(
|
||||
newNotebook.topics.findIndex((t) => t.title === "hello")
|
||||
).toBeGreaterThanOrEqual(0);
|
||||
}));
|
||||
|
||||
test("merge notebook with topics removed", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
let notebook = db.notebooks.notebook(id);
|
||||
|
||||
const newNotebook = db.notebooks.merge(notebook.data, {
|
||||
...notebook.data,
|
||||
topics: [makeTopic({ title: "Home" }, id)],
|
||||
remote: true
|
||||
});
|
||||
|
||||
expect(
|
||||
newNotebook.topics.findIndex((t) => t.title === "Home")
|
||||
).toBeGreaterThanOrEqual(0);
|
||||
expect(
|
||||
newNotebook.topics.findIndex((t) => t.title === "hello")
|
||||
).toBeLessThan(0);
|
||||
}));
|
||||
|
||||
test("merge notebook with topic edited", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
let notebook = db.notebooks.notebook(id);
|
||||
|
||||
const newNotebook = db.notebooks.merge(notebook.data, {
|
||||
...notebook.data,
|
||||
topics: [{ ...notebook.data.topics[0], title: "hello (edited)" }],
|
||||
remote: true
|
||||
});
|
||||
|
||||
expect(
|
||||
newNotebook.topics.findIndex((t) => t.title === "hello (edited)")
|
||||
).toBeGreaterThanOrEqual(0);
|
||||
expect(
|
||||
newNotebook.topics.findIndex((t) => t.title === "hello")
|
||||
).toBeLessThan(0);
|
||||
}));
|
||||
|
||||
test("merge notebook when local notebook is also edited", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
let notebook = db.notebooks.notebook(id);
|
||||
|
||||
let newNotebook = { ...qclone(notebook.data), remote: true };
|
||||
newNotebook.topics[0].title = "hello (edited)";
|
||||
|
||||
await delay(500);
|
||||
|
||||
await notebook.topics.add({
|
||||
...notebook.topics.all[0],
|
||||
title: "hello (edited too)"
|
||||
});
|
||||
|
||||
newNotebook = db.notebooks.merge(
|
||||
db.notebooks.notebook(id).data,
|
||||
newNotebook,
|
||||
0
|
||||
);
|
||||
expect(
|
||||
newNotebook.topics.findIndex((t) => t.title === "hello (edited too)")
|
||||
).toBeGreaterThanOrEqual(0);
|
||||
expect(
|
||||
newNotebook.topics.findIndex((t) => t.title === "hello (edited)")
|
||||
).toBeLessThan(0);
|
||||
expect(
|
||||
newNotebook.topics.findIndex((t) => t.title === "hello")
|
||||
).toBeLessThan(0);
|
||||
}));
|
||||
|
||||
test("merging notebook when local notebook is not edited should not update remote notebook dateEdited", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
let notebook = db.notebooks.notebook(id);
|
||||
|
||||
let note = await db.notes.add(TEST_NOTE);
|
||||
await db.notes.addToNotebook(
|
||||
{ id: notebook.data.id, topic: notebook.data.topics[0].id },
|
||||
note
|
||||
);
|
||||
|
||||
const newNotebook = db.notebooks.merge(notebook.data, {
|
||||
...notebook.data,
|
||||
remote: true
|
||||
});
|
||||
|
||||
expect(db.notebooks.notebook(id).dateEdited).toBe(newNotebook.dateEdited);
|
||||
}));
|
||||
|
||||
test("merge notebook with topic removed that is edited in the local notebook", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
let notebook = db.notebooks.notebook(id);
|
||||
|
||||
let newNotebook = { ...qclone(notebook.data), remote: true };
|
||||
newNotebook.topics.splice(0, 1); // remove hello topic
|
||||
|
||||
const lastSynced = Date.now();
|
||||
|
||||
await delay(500);
|
||||
|
||||
await notebook.topics.add({
|
||||
...notebook.topics.all[1],
|
||||
title: "hello (i exist)"
|
||||
});
|
||||
|
||||
newNotebook = db.notebooks.merge(
|
||||
db.notebooks.notebook(id).data,
|
||||
newNotebook,
|
||||
lastSynced
|
||||
);
|
||||
|
||||
expect(
|
||||
newNotebook.topics.findIndex((t) => t.title === "hello (i exist)")
|
||||
).toBeGreaterThanOrEqual(0);
|
||||
expect(
|
||||
newNotebook.topics.findIndex((t) => t.title === "hello")
|
||||
).toBeLessThan(0);
|
||||
}));
|
||||
|
||||
@@ -35,35 +35,37 @@ async function createAndAddNoteToNotebook(
|
||||
noteId: string,
|
||||
options: {
|
||||
notebookTitle: string;
|
||||
topicTitle: string;
|
||||
subNotebookTitle: string;
|
||||
}
|
||||
) {
|
||||
const { notebookTitle, topicTitle } = options;
|
||||
const { notebookTitle, subNotebookTitle } = options;
|
||||
const notebookId = await db.notebooks.add({ title: notebookTitle });
|
||||
if (!notebookId) throw new Error("Could not create notebook");
|
||||
|
||||
const topics = db.notebooks.topics(notebookId);
|
||||
await topics.add({ title: topicTitle });
|
||||
const subNotebookId = await db.notebooks.add({ title: subNotebookTitle });
|
||||
if (!subNotebookId) throw new Error("Could not create sub notebook");
|
||||
|
||||
const topic = topics.topic(topicTitle);
|
||||
if (!topic) throw new Error("Could not find topic.");
|
||||
await db.notes.addToNotebook({ id: notebookId, topic: topic.id }, noteId);
|
||||
await db.relations.add(
|
||||
{ type: "notebook", id: notebookId },
|
||||
{ type: "notebook", id: subNotebookId }
|
||||
);
|
||||
|
||||
return { topic, topics, notebookId };
|
||||
await db.notes.addToNotebook(subNotebookId, noteId);
|
||||
|
||||
return { subNotebookId, notebookId };
|
||||
}
|
||||
|
||||
test("add invalid note", () =>
|
||||
databaseTest().then(async (db) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
let id = await db.notes.add();
|
||||
expect(id).toBeUndefined();
|
||||
id = await db.notes.add({});
|
||||
expect(id).toBeUndefined();
|
||||
expect(db.notes.add()).rejects.toThrow();
|
||||
|
||||
expect(db.notes.add({})).rejects.toThrow();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
id = await db.notes.add({ hello: "world" });
|
||||
expect(id).toBeUndefined();
|
||||
expect(db.notes.add({ hello: "world" })).rejects.toThrow();
|
||||
}));
|
||||
|
||||
test("add note", () =>
|
||||
@@ -84,23 +86,21 @@ test("delete note", () =>
|
||||
const notebookId = await db.notebooks.add(TEST_NOTEBOOK);
|
||||
if (!notebookId) throw new Error("Could not create notebook.");
|
||||
|
||||
const topics = db.notebooks.topics(notebookId);
|
||||
const subNotebookId = await db.notebooks.add({ title: "hello" });
|
||||
if (!notebookId) throw new Error("Could not create sub notebook.");
|
||||
|
||||
let topic = topics.topic("hello");
|
||||
if (!topic) throw new Error("Could not find topic.");
|
||||
await db.relations.add(
|
||||
{ type: "notebook", id: notebookId },
|
||||
{ type: "notebook", id: subNotebookId }
|
||||
);
|
||||
|
||||
await db.notes.addToNotebook({ id: notebookId, topic: topic.id }, id);
|
||||
await db.notes.addToNotebook(subNotebookId, id);
|
||||
|
||||
topic = topics.topic("hello");
|
||||
if (!topic) throw new Error("Could not find topic.");
|
||||
|
||||
expect(topic.all.findIndex((v) => v.id === id)).toBeGreaterThan(-1);
|
||||
await db.notes.delete(id);
|
||||
expect(db.notes.note(id)).toBeUndefined();
|
||||
expect(topic.all.findIndex((v) => v.id === id)).toBe(-1);
|
||||
|
||||
expect(db.notes.note(id)).toBeUndefined();
|
||||
expect(db.notebooks.totalNotes(notebookId)).toBe(0);
|
||||
expect(topics.topic("hello")?.totalNotes).toBe(0);
|
||||
expect(db.notebooks.totalNotes(subNotebookId)).toBe(0);
|
||||
}));
|
||||
|
||||
test("get all notes", () =>
|
||||
@@ -217,57 +217,67 @@ test("favorite note", () =>
|
||||
expect(note?.data.favorite).toBe(true);
|
||||
}));
|
||||
|
||||
test("add note to topic", () =>
|
||||
test("add note to subnotebook", () =>
|
||||
noteTest().then(async ({ db, id }) => {
|
||||
const { topic, notebookId } = await createAndAddNoteToNotebook(db, id, {
|
||||
notebookTitle: "Hello",
|
||||
topicTitle: "Home"
|
||||
});
|
||||
|
||||
expect(topic.all).toHaveLength(1);
|
||||
expect(topic.totalNotes).toBe(1);
|
||||
expect(db.notebooks.totalNotes(notebookId)).toBe(1);
|
||||
expect(db.notes.note(id)?.notebooks?.some((n) => n.id === notebookId)).toBe(
|
||||
true
|
||||
const { subNotebookId, notebookId } = await createAndAddNoteToNotebook(
|
||||
db,
|
||||
id,
|
||||
{
|
||||
notebookTitle: "Hello",
|
||||
subNotebookTitle: "Home"
|
||||
}
|
||||
);
|
||||
|
||||
expect(
|
||||
db.relations.from({ type: "notebook", id: notebookId }, "notebook")
|
||||
).toHaveLength(1);
|
||||
expect(db.notebooks.totalNotes(subNotebookId)).toBe(1);
|
||||
expect(db.notebooks.totalNotes(notebookId)).toBe(1);
|
||||
}));
|
||||
|
||||
test("duplicate note to topic should not be added", () =>
|
||||
noteTest().then(async ({ db, id }) => {
|
||||
const { topics } = await createAndAddNoteToNotebook(db, id, {
|
||||
const { subNotebookId } = await createAndAddNoteToNotebook(db, id, {
|
||||
notebookTitle: "Hello",
|
||||
topicTitle: "Home"
|
||||
subNotebookTitle: "Home"
|
||||
});
|
||||
expect(topics.topic("Home")?.all).toHaveLength(1);
|
||||
expect(db.notebooks.totalNotes(subNotebookId)).toBe(1);
|
||||
}));
|
||||
|
||||
test("add the same note to 2 notebooks", () =>
|
||||
noteTest().then(async ({ db, id }) => {
|
||||
const nb1 = await createAndAddNoteToNotebook(db, id, {
|
||||
notebookTitle: "Hello",
|
||||
topicTitle: "Home"
|
||||
subNotebookTitle: "Home"
|
||||
});
|
||||
const nb2 = await createAndAddNoteToNotebook(db, id, {
|
||||
notebookTitle: "Hello2",
|
||||
topicTitle: "Home2"
|
||||
subNotebookTitle: "Home2"
|
||||
});
|
||||
|
||||
expect(nb1.topics.topic(nb1.topic.id)?.has(id)).toBe(true);
|
||||
expect(nb2.topics.topic(nb2.topic.id)?.has(id)).toBe(true);
|
||||
expect(db.notes.note(id)?.notebooks).toHaveLength(2);
|
||||
expect(
|
||||
db.relations
|
||||
.from({ type: "notebook", id: nb1.subNotebookId }, "note")
|
||||
.has(id)
|
||||
).toBe(true);
|
||||
expect(
|
||||
db.relations
|
||||
.from({ type: "notebook", id: nb2.subNotebookId }, "note")
|
||||
.has(id)
|
||||
).toBe(true);
|
||||
expect(db.relations.to({ type: "note", id }, "notebook")).toHaveLength(2);
|
||||
}));
|
||||
|
||||
test("moving note to same notebook and topic should do nothing", () =>
|
||||
noteTest().then(async ({ db, id }) => {
|
||||
const { notebookId, topic } = await createAndAddNoteToNotebook(db, id, {
|
||||
const { subNotebookId } = await createAndAddNoteToNotebook(db, id, {
|
||||
notebookTitle: "Home",
|
||||
topicTitle: "Hello"
|
||||
subNotebookTitle: "Hello"
|
||||
});
|
||||
await db.notes.addToNotebook({ id: notebookId, topic: topic.id }, id);
|
||||
|
||||
expect(db.notes.note(id)?.notebooks?.some((n) => n.id === notebookId)).toBe(
|
||||
true
|
||||
);
|
||||
await db.notes.addToNotebook(subNotebookId, id);
|
||||
|
||||
expect(db.relations.to({ type: "note", id }, "notebook")).toHaveLength(1);
|
||||
}));
|
||||
|
||||
test("export note to html", () =>
|
||||
@@ -308,19 +318,14 @@ test("deleting a colored note should remove it from that color", () =>
|
||||
);
|
||||
|
||||
expect(
|
||||
db.relations
|
||||
.from({ id: colorId, type: "color" }, "note")
|
||||
.findIndex((r) => r.to.id === id)
|
||||
).toBe(0);
|
||||
db.relations.from({ id: colorId, type: "color" }, "note").has(id)
|
||||
).toBe(true);
|
||||
|
||||
await db.notes.delete(id);
|
||||
|
||||
expect(
|
||||
db.relations
|
||||
.from({ id: colorId, type: "color" }, "note")
|
||||
.findIndex((r) => r.to.id === id)
|
||||
).toBe(-1);
|
||||
// TODO expect(color.noteIds.indexOf(id)).toBe(-1);
|
||||
db.relations.from({ id: colorId, type: "color" }, "note").has(id)
|
||||
).toBe(false);
|
||||
}));
|
||||
|
||||
test("note's content should follow note's localOnly property", () =>
|
||||
@@ -330,7 +335,7 @@ test("note's content should follow note's localOnly property", () =>
|
||||
if (!note?.contentId) throw new Error("No content in note.");
|
||||
|
||||
expect(note?.data.localOnly).toBe(true);
|
||||
let content = await db.content.raw(note.contentId);
|
||||
let content = await db.content.get(note.contentId);
|
||||
expect(content?.localOnly).toBe(true);
|
||||
|
||||
await db.notes.note(id)?.localOnly();
|
||||
@@ -338,7 +343,7 @@ test("note's content should follow note's localOnly property", () =>
|
||||
if (!note?.contentId) throw new Error("No content in note.");
|
||||
|
||||
expect(note?.data.localOnly).toBe(false);
|
||||
content = await db.content.raw(note.contentId);
|
||||
content = await db.content.get(note.contentId);
|
||||
expect(content?.localOnly).toBe(false);
|
||||
}));
|
||||
|
||||
|
||||
@@ -43,18 +43,6 @@ test("create a duplicate shortcut of notebook", () =>
|
||||
expect(db.shortcuts.all[0].item.id).toBe(id);
|
||||
}));
|
||||
|
||||
test("create shortcut of a topic", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
const notebook = db.notebooks.notebook(id).data;
|
||||
const topic = notebook.topics[0];
|
||||
await db.shortcuts.add({
|
||||
item: { type: "topic", id: topic.id, notebookId: id }
|
||||
});
|
||||
|
||||
expect(db.shortcuts.all).toHaveLength(1);
|
||||
expect(db.shortcuts.all[0].item.id).toBe(topic.id);
|
||||
}));
|
||||
|
||||
test("pin a tag", () =>
|
||||
databaseTest().then(async (db) => {
|
||||
const tagId = await db.tags.add({ title: "HELLO!" });
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { delay, notebookTest, TEST_NOTE } from "./utils";
|
||||
import { test, expect } from "vitest";
|
||||
|
||||
test("get empty topic", () =>
|
||||
notebookTest().then(({ db, id }) => {
|
||||
let topic = db.notebooks.topics(id).topic("hello");
|
||||
expect(topic.all).toHaveLength(0);
|
||||
}));
|
||||
|
||||
test("getting invalid topic should return undefined", () =>
|
||||
notebookTest().then(({ db, id }) => {
|
||||
expect(db.notebooks.topics(id).topic("invalid")).toBeUndefined();
|
||||
}));
|
||||
|
||||
test("add topic to notebook", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
let topics = db.notebooks.topics(id);
|
||||
await topics.add({ title: "Home" });
|
||||
expect(topics.all.length).toBeGreaterThan(1);
|
||||
expect(topics.all.findIndex((v) => v.title === "Home")).toBeGreaterThan(-1);
|
||||
}));
|
||||
|
||||
test("add note to topic", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
let topics = db.notebooks.topics(id);
|
||||
let topic = topics.topic("hello");
|
||||
let noteId = await db.notes.add(TEST_NOTE);
|
||||
await db.notes.addToNotebook({ id, topic: topic.id }, noteId);
|
||||
|
||||
topic = topics.topic("hello");
|
||||
expect(topic.totalNotes).toBe(1);
|
||||
expect(db.notebooks.notebook(id).totalNotes).toBe(1);
|
||||
}));
|
||||
|
||||
test("delete note of a topic", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
let topics = db.notebooks.topics(id);
|
||||
let topic = topics.topic("hello");
|
||||
let noteId = await db.notes.add(TEST_NOTE);
|
||||
await db.notes.addToNotebook({ id, topic: topic.id }, noteId);
|
||||
|
||||
topic = topics.topic("hello");
|
||||
expect(topic.totalNotes).toBe(1);
|
||||
expect(db.notebooks.notebook(id).totalNotes).toBe(1);
|
||||
|
||||
await db.notes.removeFromNotebook({ id, topic: topic.id }, noteId);
|
||||
|
||||
topic = topics.topic("hello");
|
||||
expect(topic.totalNotes).toBe(0);
|
||||
expect(db.notebooks.notebook(id).totalNotes).toBe(0);
|
||||
}));
|
||||
|
||||
test("edit topic title", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
let topics = db.notebooks.topics(id);
|
||||
|
||||
await topics.add({ title: "Home" });
|
||||
|
||||
let topic = topics.topic("Home");
|
||||
|
||||
expect(topics.all).toHaveLength(2);
|
||||
|
||||
const oldDateEdited = topic._topic.dateEdited;
|
||||
|
||||
await delay(30);
|
||||
|
||||
await topics.add({ id: topic._topic.id, title: "Hello22" });
|
||||
|
||||
expect(topics.all).toHaveLength(2);
|
||||
expect(topics.topic(topic._topic.id)._topic.title).toBe("Hello22");
|
||||
expect(topics.topic(topic._topic.id)._topic.dateEdited).toBeGreaterThan(
|
||||
oldDateEdited
|
||||
);
|
||||
}));
|
||||
|
||||
test("get topic", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
let topics = db.notebooks.topics(id);
|
||||
await topics.add({ title: "Home" });
|
||||
let topic = topics.topic("Home");
|
||||
let noteId = await db.notes.add({
|
||||
content: TEST_NOTE.content
|
||||
});
|
||||
await db.notes.addToNotebook({ id, topic: topic.id }, noteId);
|
||||
|
||||
topic = topics.topic("Home");
|
||||
expect(await db.content.get(topic.all[0].contentId)).toBeDefined();
|
||||
expect(topic.totalNotes).toBe(1);
|
||||
}));
|
||||
|
||||
test("delete a topic", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
let topics = db.notebooks.topics(id);
|
||||
await topics.add({ title: "Home" });
|
||||
await topics.delete(topics.topic("Home").id);
|
||||
expect(topics.all.findIndex((v) => v.title === "Home")).toBe(-1);
|
||||
}));
|
||||
|
||||
test("delete note from edited topic", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
const noteId = await db.notes.add(TEST_NOTE);
|
||||
let topics = db.notebooks.topics(id);
|
||||
await topics.add({ title: "Home" });
|
||||
let topic = topics.topic("Home");
|
||||
await db.notes.addToNotebook({ id, topic: topic._topic.title }, noteId);
|
||||
await topics.add({ id: topic._topic.id, title: "Hello22" });
|
||||
await db.notes.delete(noteId);
|
||||
}));
|
||||
|
||||
test("editing one topic should not update dateEdited of all", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
let topics = db.notebooks.topics(id);
|
||||
|
||||
await topics.add({ title: "Home" });
|
||||
await topics.add("Home2");
|
||||
await topics.add("Home3");
|
||||
|
||||
let topic = topics.topic("Home");
|
||||
|
||||
const oldTopics = topics.all.filter((t) => t.title !== "Home");
|
||||
|
||||
await delay(100);
|
||||
|
||||
await topics.add({ id: topic._topic.id, title: "Hello22" });
|
||||
|
||||
const newTopics = topics.all.filter((t) => t.title !== "Hello22");
|
||||
|
||||
expect(
|
||||
newTopics.every(
|
||||
(t) =>
|
||||
oldTopics.findIndex(
|
||||
(topic) => topic.id === t.id && topic.dateEdited === t.dateEdited
|
||||
) > -1
|
||||
)
|
||||
).toBe(true);
|
||||
}));
|
||||
@@ -34,8 +34,12 @@ test("trash should be empty", () =>
|
||||
|
||||
test("permanently delete a note", () =>
|
||||
databaseTest().then(async (db) => {
|
||||
const noteId = await db.notes.add({ ...TEST_NOTE, sessionId: Date.now() });
|
||||
const noteId = await db.notes.add({
|
||||
...TEST_NOTE,
|
||||
sessionId: Date.now().toString()
|
||||
});
|
||||
const note = db.notes.note(noteId);
|
||||
if (!note) throw new Error("Could not find note.");
|
||||
|
||||
let sessions = await db.noteHistory.get(noteId);
|
||||
expect(sessions).toHaveLength(1);
|
||||
@@ -45,7 +49,7 @@ test("permanently delete a note", () =>
|
||||
expect(await note.content()).toBeDefined();
|
||||
await db.trash.delete(db.trash.all[0].id);
|
||||
expect(db.trash.all).toHaveLength(0);
|
||||
const content = await db.content.get(note.data.contentId);
|
||||
const content = note.contentId && (await db.content.get(note.contentId));
|
||||
expect(content).toBeUndefined();
|
||||
|
||||
sessions = await db.noteHistory.get(noteId);
|
||||
@@ -54,25 +58,27 @@ test("permanently delete a note", () =>
|
||||
|
||||
test("restore a deleted note that was in a notebook", () =>
|
||||
noteTest().then(async ({ db, id }) => {
|
||||
let nbId = await db.notebooks.add(TEST_NOTEBOOK);
|
||||
const topic = db.notebooks.notebook(nbId).topics.topic("hello");
|
||||
await db.notes.addToNotebook({ id: nbId, topic: topic.id }, id);
|
||||
const notebookId = await db.notebooks.add(TEST_NOTEBOOK);
|
||||
const subNotebookId = await db.notebooks.add({ title: "hello" });
|
||||
|
||||
await db.relations.add(
|
||||
{ type: "notebook", id: notebookId },
|
||||
{ type: "notebook", id: subNotebookId }
|
||||
);
|
||||
await db.notes.addToNotebook(subNotebookId, id);
|
||||
|
||||
await db.notes.delete(id);
|
||||
await db.trash.restore(db.trash.all[0].id);
|
||||
expect(db.trash.all).toHaveLength(0);
|
||||
|
||||
let note = db.notes.note(id);
|
||||
const note = db.notes.note(id);
|
||||
|
||||
expect(note).toBeDefined();
|
||||
expect(await note.content()).toBe(TEST_NOTE.content.data);
|
||||
expect(await note?.content()).toBe(TEST_NOTE.content.data);
|
||||
|
||||
const notebook = db.notebooks.notebook(nbId);
|
||||
expect(notebook.topics.topic(topic.id).has(id)).toBe(true);
|
||||
|
||||
expect(note.notebooks.some((n) => n.id === nbId)).toBe(true);
|
||||
|
||||
expect(notebook.topics.has("hello")).toBeDefined();
|
||||
expect(
|
||||
db.relations.from({ type: "notebook", id: subNotebookId }, "note").has(id)
|
||||
).toBe(true);
|
||||
}));
|
||||
|
||||
test("delete a locked note", () =>
|
||||
@@ -82,7 +88,9 @@ test("delete a locked note", () =>
|
||||
await db.vault.add(id);
|
||||
await db.notes.delete(id);
|
||||
expect(db.trash.all).toHaveLength(1);
|
||||
expect(await db.content.get(note.data.contentId)).toBeDefined();
|
||||
expect(
|
||||
note && note.contentId && (await db.content.get(note.contentId))
|
||||
).toBeDefined();
|
||||
}));
|
||||
|
||||
test("restore a deleted locked note", () =>
|
||||
@@ -92,7 +100,9 @@ test("restore a deleted locked note", () =>
|
||||
await db.vault.add(id);
|
||||
await db.notes.delete(id);
|
||||
expect(db.trash.all).toHaveLength(1);
|
||||
expect(await db.content.get(note.data.contentId)).toBeDefined();
|
||||
expect(
|
||||
note && note.contentId && (await db.content.get(note.contentId))
|
||||
).toBeDefined();
|
||||
await db.trash.restore(db.trash.all[0].id);
|
||||
expect(db.trash.all).toHaveLength(0);
|
||||
note = db.notes.note(id);
|
||||
@@ -101,69 +111,64 @@ test("restore a deleted locked note", () =>
|
||||
|
||||
test("restore a deleted note that's in a deleted notebook", () =>
|
||||
noteTest().then(async ({ db, id }) => {
|
||||
let nbId = await db.notebooks.add(TEST_NOTEBOOK);
|
||||
const topic = db.notebooks.notebook(nbId).topics.topic("hello");
|
||||
await db.notes.addToNotebook({ id: nbId, topic: topic.id }, id);
|
||||
const notebookId = await db.notebooks.add(TEST_NOTEBOOK);
|
||||
await db.notes.addToNotebook(notebookId, id);
|
||||
|
||||
await db.notes.delete(id);
|
||||
await db.notebooks.delete(nbId);
|
||||
const deletedNote = db.trash.all.find(
|
||||
(v) => v.id === id && v.itemType === "note"
|
||||
);
|
||||
await db.trash.restore(deletedNote.id);
|
||||
let note = db.notes.note(id);
|
||||
await db.notebooks.delete(notebookId);
|
||||
|
||||
await db.trash.restore(id);
|
||||
const note = db.notes.note(id);
|
||||
expect(note).toBeDefined();
|
||||
expect(db.notes.note(id).notebook).toBeUndefined();
|
||||
expect(db.relations.to({ type: "note", id }, "notebook")).toHaveLength(0);
|
||||
}));
|
||||
|
||||
test("delete a notebook", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
let noteId = await db.notes.add(TEST_NOTE);
|
||||
let notebook = db.notebooks.notebook(id);
|
||||
const topic = notebook.topics.topic("hello");
|
||||
await db.notes.addToNotebook({ id, topic: topic.id }, noteId);
|
||||
const noteId = await db.notes.add(TEST_NOTE);
|
||||
|
||||
await db.notes.addToNotebook(id, noteId);
|
||||
|
||||
await db.notebooks.delete(id);
|
||||
expect(db.notebooks.notebook(id)).toBeUndefined();
|
||||
expect(db.notes.note(noteId).notebook).toBeUndefined();
|
||||
expect(
|
||||
db.relations.to({ type: "note", id: noteId }, "notebook")
|
||||
).toHaveLength(0);
|
||||
}));
|
||||
|
||||
test("restore a deleted notebook", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
let noteId = await db.notes.add(TEST_NOTE);
|
||||
const topic = db.notebooks.notebook(id).topics.topic("hello");
|
||||
await db.notes.addToNotebook({ id, topic: topic.id }, noteId);
|
||||
const noteId = await db.notes.add(TEST_NOTE);
|
||||
await db.notes.addToNotebook(id, noteId);
|
||||
|
||||
await db.notebooks.delete(id);
|
||||
await db.trash.restore(id);
|
||||
|
||||
let notebook = db.notebooks.notebook(id);
|
||||
const notebook = db.notebooks.notebook(id);
|
||||
expect(notebook).toBeDefined();
|
||||
|
||||
let note = db.notes.note(noteId);
|
||||
const noteNotebook = note.notebooks.find((n) => n.id === id);
|
||||
expect(noteNotebook).toBeDefined();
|
||||
expect(noteNotebook.topics).toHaveLength(1);
|
||||
expect(notebook.topics.topic(noteNotebook.topics[0])).toBeDefined();
|
||||
expect(
|
||||
db.relations.to({ type: "note", id: noteId }, "notebook")
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
db.relations.to({ type: "note", id: noteId }, "notebook").has(id)
|
||||
).toBe(true);
|
||||
}));
|
||||
|
||||
test("restore a notebook that has deleted notes", () =>
|
||||
notebookTest().then(async ({ db, id }) => {
|
||||
let noteId = await db.notes.add(TEST_NOTE);
|
||||
|
||||
let notebook = db.notebooks.notebook(id);
|
||||
const topic = notebook.topics.topic("hello");
|
||||
await db.notes.addToNotebook({ id, topic: topic.id }, noteId);
|
||||
const noteId = await db.notes.add(TEST_NOTE);
|
||||
await db.notes.addToNotebook(id, noteId);
|
||||
|
||||
await db.notebooks.delete(id);
|
||||
await db.notes.delete(noteId);
|
||||
const deletedNotebook = db.trash.all.find(
|
||||
(v) => v.id === id && v.itemType === "notebook"
|
||||
);
|
||||
await db.trash.restore(deletedNotebook.id);
|
||||
notebook = db.notebooks.notebook(id);
|
||||
await db.trash.restore(id);
|
||||
|
||||
const notebook = db.notebooks.notebook(id);
|
||||
expect(notebook).toBeDefined();
|
||||
expect(notebook.topics.topic("hello").has(noteId)).toBe(false);
|
||||
expect(
|
||||
db.relations.from({ type: "notebook", id: id }, "note").has(noteId)
|
||||
).toBe(false);
|
||||
}));
|
||||
|
||||
test("permanently delete items older than 7 days", () =>
|
||||
@@ -175,16 +180,27 @@ test("permanently delete items older than 7 days", () =>
|
||||
await db.notebooks.delete(notebookId);
|
||||
await db.notes.delete(noteId);
|
||||
|
||||
const note = db.trash.all.find((t) => t.id === noteId);
|
||||
if (!note || note.itemType !== "note")
|
||||
throw new Error("Could not find note in trash.");
|
||||
|
||||
await db.notes.collection.update({
|
||||
...note,
|
||||
type: "trash",
|
||||
id: noteId,
|
||||
dateDeleted: sevenDaysEarlier
|
||||
});
|
||||
|
||||
const notebook = db.trash.all.find((t) => t.id === notebookId);
|
||||
if (!notebook || notebook.itemType !== "notebook")
|
||||
throw new Error("Could not find notebook in trash.");
|
||||
|
||||
await db.notebooks.collection.update({
|
||||
...notebook,
|
||||
type: "trash",
|
||||
id: notebookId,
|
||||
dateDeleted: sevenDaysEarlier
|
||||
dateDeleted: sevenDaysEarlier,
|
||||
itemType: "notebook"
|
||||
});
|
||||
|
||||
expect(db.trash.all).toHaveLength(2);
|
||||
@@ -211,14 +227,17 @@ test("trash cleanup should not delete items newer than 7 days", () =>
|
||||
|
||||
test("clear trash should delete note content", () =>
|
||||
databaseTest().then(async (db) => {
|
||||
const noteId = await db.notes.add({ ...TEST_NOTE, sessionId: Date.now() });
|
||||
const noteId = await db.notes.add({
|
||||
...TEST_NOTE,
|
||||
sessionId: Date.now().toString()
|
||||
});
|
||||
|
||||
const notebookId = await db.notebooks.add(TEST_NOTEBOOK);
|
||||
|
||||
let sessions = await db.noteHistory.get(noteId);
|
||||
expect(sessions).toHaveLength(1);
|
||||
|
||||
let note = { ...db.notes.note(noteId).data };
|
||||
const note = { ...db.notes.note(noteId)?.data };
|
||||
|
||||
await db.notebooks.delete(notebookId);
|
||||
await db.notes.delete(noteId);
|
||||
@@ -229,9 +248,9 @@ test("clear trash should delete note content", () =>
|
||||
|
||||
expect(db.trash.all).toHaveLength(0);
|
||||
|
||||
const content = await db.content.get(note.contentId);
|
||||
const content = note.contentId && (await db.content.get(note.contentId));
|
||||
expect(content).toBeUndefined();
|
||||
|
||||
sessions = await db.noteHistory.get(note.id);
|
||||
sessions = await db.noteHistory.get(noteId);
|
||||
expect(sessions).toHaveLength(0);
|
||||
}));
|
||||
@@ -26,23 +26,17 @@ import Compressor from "../../__mocks__/compressor.mock";
|
||||
import { expect } from "vitest";
|
||||
import { EventSourcePolyfill as EventSource } from "event-source-polyfill";
|
||||
import { randomBytes } from "../../src/utils/random";
|
||||
import { GroupOptions, Note, Notebook, Topic } from "../../src/types";
|
||||
import { GroupOptions, Note, Notebook } from "../../src/types";
|
||||
import { NoteContent } from "../../src/collections/session-content";
|
||||
|
||||
const TEST_NOTEBOOK: Partial<
|
||||
Omit<Notebook, "topics"> & { topics: Partial<Topic>[] }
|
||||
> = {
|
||||
const TEST_NOTEBOOK: Partial<Notebook> = {
|
||||
title: "Test Notebook",
|
||||
description: "Test Description",
|
||||
topics: [{ title: "hello" }]
|
||||
description: "Test Description"
|
||||
};
|
||||
|
||||
const TEST_NOTEBOOK2: Partial<
|
||||
Omit<Notebook, "topics"> & { topics: Partial<Topic>[] }
|
||||
> = {
|
||||
const TEST_NOTEBOOK2: Partial<Notebook> = {
|
||||
title: "Test Notebook 2",
|
||||
description: "Test Description 2",
|
||||
topics: [{ title: "Home2" }]
|
||||
description: "Test Description 2"
|
||||
};
|
||||
|
||||
function databaseTest() {
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
Notebook,
|
||||
Reminder,
|
||||
Tag,
|
||||
Topic,
|
||||
TrashItem,
|
||||
isDeleted
|
||||
} from "../types";
|
||||
@@ -55,16 +54,7 @@ export default class Lookup {
|
||||
}
|
||||
|
||||
notebooks(array: Notebook[], query: string) {
|
||||
return search(
|
||||
array,
|
||||
query,
|
||||
(n) =>
|
||||
`${n.title} ${n.description} ${n.topics.map((t) => t.title).join(" ")}`
|
||||
);
|
||||
}
|
||||
|
||||
topics(array: Topic[], query: string) {
|
||||
return this.byTitle(array, query);
|
||||
return search(array, query, (n) => `${n.title} ${n.description}}`);
|
||||
}
|
||||
|
||||
tags(array: GroupedItems<Tag>, query: string) {
|
||||
|
||||
@@ -335,8 +335,6 @@ class Sync {
|
||||
}
|
||||
|
||||
async stop(lastSynced: number) {
|
||||
// refresh topic references
|
||||
this.db.notes.topicReferences.rebuild();
|
||||
// refresh monographs on sync completed
|
||||
await this.db.monographs.init();
|
||||
|
||||
@@ -373,9 +371,6 @@ class Sync {
|
||||
* @private
|
||||
*/
|
||||
async onPushCompleted(lastSynced: number) {
|
||||
// refresh topic references
|
||||
this.db.notes.topicReferences.rebuild();
|
||||
|
||||
this.db.eventManager.publish(
|
||||
EVENTS.databaseSyncRequested,
|
||||
false,
|
||||
|
||||
@@ -85,7 +85,7 @@ class Merger {
|
||||
ItemMap[TType] | TrashOrItem<Note> | TrashOrItem<Notebook>
|
||||
>,
|
||||
type: TType,
|
||||
lastSynced: number
|
||||
_lastSynced: number
|
||||
) {
|
||||
switch (type) {
|
||||
case "shortcut":
|
||||
@@ -93,7 +93,8 @@ class Merger {
|
||||
case "tag":
|
||||
case "color":
|
||||
case "note":
|
||||
case "relation": {
|
||||
case "relation":
|
||||
case "notebook": {
|
||||
const localItem = this.db[SYNC_COLLECTIONS_MAP[type]].collection.getRaw(
|
||||
remoteItem.id
|
||||
);
|
||||
@@ -102,31 +103,6 @@ class Merger {
|
||||
}
|
||||
break;
|
||||
}
|
||||
// case "note": {
|
||||
// const localItem = this.db.notes.collection.getRaw(remoteItem.id);
|
||||
// if (!localItem || remoteItem.dateModified > localItem.dateModified) {
|
||||
// return this.db.notes.merge(
|
||||
// localItem,
|
||||
// remoteItem as MaybeDeletedItem<TrashOrItem<Note>>
|
||||
// );
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
case "notebook": {
|
||||
const THRESHOLD = 1000;
|
||||
const localItem = this.db.notebooks.collection.getRaw(remoteItem.id);
|
||||
if (
|
||||
!localItem ||
|
||||
this.isConflicted(localItem, remoteItem, lastSynced, THRESHOLD)
|
||||
) {
|
||||
return this.db.notebooks.merge(
|
||||
localItem,
|
||||
remoteItem as MaybeDeletedItem<TrashOrItem<Notebook>>,
|
||||
lastSynced
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,18 +19,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { createNotebookModel } from "../models/notebook";
|
||||
import { getId } from "../utils/id";
|
||||
import { CHECK_IDS, checkIsUserPremium } from "../common";
|
||||
import { CachedCollection } from "../database/cached-collection";
|
||||
import Topics from "./topics";
|
||||
import Database from "../api";
|
||||
import {
|
||||
MaybeDeletedItem,
|
||||
Notebook,
|
||||
Topic,
|
||||
TrashOrItem,
|
||||
isDeleted,
|
||||
isTrashItem
|
||||
} from "../types";
|
||||
import { BaseTrashItem, Notebook, TrashOrItem, isTrashItem } from "../types";
|
||||
import { ICollection } from "./collection";
|
||||
|
||||
export class Notebooks implements ICollection {
|
||||
@@ -51,63 +42,7 @@ export class Notebooks implements ICollection {
|
||||
return this.collection.init();
|
||||
}
|
||||
|
||||
merge(
|
||||
localNotebook: MaybeDeletedItem<TrashOrItem<Notebook>> | undefined,
|
||||
remoteNotebook: MaybeDeletedItem<TrashOrItem<Notebook>>,
|
||||
lastSyncedTimestamp: number
|
||||
) {
|
||||
if (isDeleted(remoteNotebook) || isTrashItem(remoteNotebook))
|
||||
return remoteNotebook;
|
||||
|
||||
if (
|
||||
localNotebook &&
|
||||
(isTrashItem(localNotebook) || isDeleted(localNotebook))
|
||||
) {
|
||||
if (localNotebook.dateModified > remoteNotebook.dateModified) return;
|
||||
return remoteNotebook;
|
||||
}
|
||||
|
||||
if (localNotebook && localNotebook.topics?.length) {
|
||||
let isChanged = false;
|
||||
// merge new and old topics
|
||||
for (const oldTopic of localNotebook.topics) {
|
||||
const newTopicIndex = remoteNotebook.topics.findIndex(
|
||||
(t) => t.id === oldTopic.id
|
||||
);
|
||||
const newTopic = remoteNotebook.topics[newTopicIndex];
|
||||
|
||||
// CASE 1: if topic exists in old notebook but not in new notebook, it's deleted.
|
||||
// However, if the dateEdited of topic in the old notebook is > lastSyncedTimestamp
|
||||
// it was newly added or edited so add it to the new notebook.
|
||||
if (!newTopic && oldTopic.dateEdited > lastSyncedTimestamp) {
|
||||
remoteNotebook.topics.push({ ...oldTopic, dateEdited: Date.now() });
|
||||
isChanged = true;
|
||||
}
|
||||
|
||||
// CASE 2: if topic exists in new notebook but not in old notebook, it's new.
|
||||
// This case will be automatically handled as the new notebook is our source of truth.
|
||||
|
||||
// CASE 3: if topic exists in both notebooks:
|
||||
// if oldTopic.dateEdited > newTopic.dateEdited: we keep oldTopic
|
||||
// and merge the notes of both topics.
|
||||
else if (newTopic && oldTopic.dateEdited > newTopic.dateEdited) {
|
||||
remoteNotebook.topics[newTopicIndex] = {
|
||||
...oldTopic,
|
||||
dateEdited: Date.now()
|
||||
};
|
||||
isChanged = true;
|
||||
}
|
||||
}
|
||||
remoteNotebook.remote = !isChanged;
|
||||
}
|
||||
return remoteNotebook;
|
||||
}
|
||||
|
||||
async add(
|
||||
notebookArg: Partial<
|
||||
Omit<Notebook, "topics"> & { topics: Partial<Topic>[] }
|
||||
>
|
||||
) {
|
||||
async add(notebookArg: Partial<Notebook>) {
|
||||
if (!notebookArg) throw new Error("Notebook cannot be undefined or null.");
|
||||
if (notebookArg.remote)
|
||||
throw new Error(
|
||||
@@ -121,17 +56,9 @@ export class Notebooks implements ICollection {
|
||||
if (oldNotebook && isTrashItem(oldNotebook))
|
||||
throw new Error("Cannot modify trashed notebooks.");
|
||||
|
||||
if (
|
||||
!oldNotebook &&
|
||||
this.all.length >= 3 &&
|
||||
!(await checkIsUserPremium(CHECK_IDS.notebookAdd))
|
||||
)
|
||||
return;
|
||||
|
||||
const mergedNotebook: Partial<Notebook> = {
|
||||
...oldNotebook,
|
||||
...notebookArg,
|
||||
topics: oldNotebook?.topics || []
|
||||
...notebookArg
|
||||
};
|
||||
|
||||
if (!mergedNotebook.title)
|
||||
@@ -143,7 +70,6 @@ export class Notebooks implements ICollection {
|
||||
title: mergedNotebook.title,
|
||||
description: mergedNotebook.description,
|
||||
pinned: !!mergedNotebook.pinned,
|
||||
topics: mergedNotebook.topics || [],
|
||||
|
||||
dateCreated: mergedNotebook.dateCreated || Date.now(),
|
||||
dateModified: mergedNotebook.dateModified || Date.now(),
|
||||
@@ -151,10 +77,6 @@ export class Notebooks implements ICollection {
|
||||
};
|
||||
|
||||
await this.collection.add(notebook);
|
||||
|
||||
if (!oldNotebook && notebookArg.topics) {
|
||||
await this.topics(id).add(...notebookArg.topics);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -175,7 +97,7 @@ export class Notebooks implements ICollection {
|
||||
get trashed() {
|
||||
return this.raw.filter((item) =>
|
||||
isTrashItem(item)
|
||||
) as TrashOrItem<Notebook>[];
|
||||
) as BaseTrashItem<Notebook>[];
|
||||
}
|
||||
|
||||
async pin(...ids: string[]) {
|
||||
@@ -192,18 +114,17 @@ export class Notebooks implements ICollection {
|
||||
}
|
||||
}
|
||||
|
||||
topics(id: string) {
|
||||
return new Topics(id, this.db);
|
||||
}
|
||||
|
||||
totalNotes(id: string) {
|
||||
const notebook = this.collection.get(id);
|
||||
if (!notebook || isTrashItem(notebook)) return 0;
|
||||
let count = 0;
|
||||
for (const topic of notebook.topics) {
|
||||
count += this.db.notes.topicReferences.count(topic.id);
|
||||
const subNotebooks = this.db.relations.from(
|
||||
{ type: "notebook", id },
|
||||
"notebook"
|
||||
);
|
||||
for (const notebook of subNotebooks) {
|
||||
count += this.totalNotes(notebook.to.id);
|
||||
}
|
||||
return count + this.db.relations.from(notebook, "note").resolved().length;
|
||||
count += this.db.relations.from({ type: "notebook", id }, "note").length;
|
||||
return count;
|
||||
}
|
||||
|
||||
notebook(idOrNotebook: string | Notebook) {
|
||||
|
||||
@@ -20,20 +20,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { createNoteModel } from "../models/note";
|
||||
import { getId } from "../utils/id";
|
||||
import { getContentFromData } from "../content-types";
|
||||
import { deleteItem, findById } from "../utils/array";
|
||||
import { NEWLINE_STRIP_REGEX, formatTitle } from "../utils/title-format";
|
||||
import { clone } from "../utils/clone";
|
||||
import { Tiptap } from "../content-types/tiptap";
|
||||
import { EMPTY_CONTENT, isUnencryptedContent } from "./content";
|
||||
import { CHECK_IDS, checkIsUserPremium } from "../common";
|
||||
import { buildFromTemplate } from "../utils/templates";
|
||||
import { Note, TrashOrItem, isTrashItem, isDeleted } from "../types";
|
||||
import {
|
||||
Note,
|
||||
TrashOrItem,
|
||||
isTrashItem,
|
||||
isDeleted,
|
||||
BaseTrashItem
|
||||
} from "../types";
|
||||
import Database from "../api";
|
||||
import { CachedCollection } from "../database/cached-collection";
|
||||
import { ICollection } from "./collection";
|
||||
import { NoteContent } from "./session-content";
|
||||
|
||||
type NotebookReference = { id: string; topic?: string; rebuildCache?: boolean };
|
||||
type ExportOptions = {
|
||||
format: "html" | "md" | "txt" | "md-frontmatter";
|
||||
contentItem?: NoteContent<false>;
|
||||
@@ -43,7 +47,6 @@ type ExportOptions = {
|
||||
|
||||
export class Notes implements ICollection {
|
||||
name = "notes";
|
||||
topicReferences = new NoteIdCache(this);
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -58,13 +61,11 @@ export class Notes implements ICollection {
|
||||
|
||||
async init() {
|
||||
await this.collection.init();
|
||||
this.topicReferences.rebuild();
|
||||
}
|
||||
|
||||
async add(
|
||||
item: Partial<Note & { content: NoteContent<false>; sessionId: string }>
|
||||
): Promise<string | undefined> {
|
||||
if (!item) return;
|
||||
): Promise<string> {
|
||||
if (item.remote)
|
||||
throw new Error("Please use db.notes.merge to merge remote notes.");
|
||||
|
||||
@@ -80,7 +81,8 @@ export class Notes implements ICollection {
|
||||
|
||||
if (oldNote) note.contentId = oldNote.contentId;
|
||||
|
||||
if (!oldNote && !item.content && !item.contentId && !item.title) return;
|
||||
if (!oldNote && !item.content && !item.contentId && !item.title)
|
||||
throw new Error("Note must have a title or content.");
|
||||
|
||||
if (item.content && item.content.data && item.content.type) {
|
||||
const { type, data } = item.content;
|
||||
@@ -160,7 +162,9 @@ export class Notes implements ICollection {
|
||||
}
|
||||
|
||||
get trashed() {
|
||||
return this.raw.filter((item) => isTrashItem(item)) as TrashOrItem<Note>[];
|
||||
return this.raw.filter((item) =>
|
||||
isTrashItem(item)
|
||||
) as BaseTrashItem<Note>[];
|
||||
}
|
||||
|
||||
get pinned() {
|
||||
@@ -289,142 +293,45 @@ export class Notes implements ICollection {
|
||||
if (!item) continue;
|
||||
const itemData = clone(item);
|
||||
|
||||
if (itemData.notebooks && !moveToTrash) {
|
||||
for (const notebook of itemData.notebooks) {
|
||||
for (const topicId of notebook.topics) {
|
||||
await this.removeFromNotebook(
|
||||
{ id: notebook.id, topic: topicId, rebuildCache: false },
|
||||
id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.db.relations.unlinkAll(item, "tag");
|
||||
await this.db.relations.unlinkAll(item, "color");
|
||||
await this.db.relations.unlinkAll(item, "attachment");
|
||||
|
||||
if (moveToTrash && !isTrashItem(itemData))
|
||||
await this.db.trash.add(itemData);
|
||||
else {
|
||||
await this.db.relations.unlinkAll(item, "tag");
|
||||
await this.db.relations.unlinkAll(item, "color");
|
||||
await this.db.relations.unlinkAll(item, "attachment");
|
||||
await this.db.relations.unlinkAll(item, "notebook");
|
||||
|
||||
await this.collection.remove(id);
|
||||
if (itemData.contentId)
|
||||
await this.db.content.remove(itemData.contentId);
|
||||
}
|
||||
}
|
||||
this.topicReferences.rebuild();
|
||||
}
|
||||
|
||||
async addToNotebook(to: NotebookReference, ...noteIds: string[]) {
|
||||
if (!to) throw new Error("The destination notebook cannot be undefined.");
|
||||
if (!to.id) throw new Error("The destination notebook must contain id.");
|
||||
|
||||
const { id: notebookId, topic: topicId } = to;
|
||||
|
||||
async addToNotebook(notebookId: string, ...noteIds: string[]) {
|
||||
for (const noteId of noteIds) {
|
||||
const note = this.collection.get(noteId);
|
||||
if (!note || isTrashItem(note)) continue;
|
||||
|
||||
if (topicId) {
|
||||
const notebooks = note.notebooks || [];
|
||||
|
||||
const noteNotebook = notebooks.find((nb) => nb.id === notebookId);
|
||||
const noteHasNotebook = !!noteNotebook;
|
||||
const noteHasTopic =
|
||||
noteHasNotebook && noteNotebook.topics.indexOf(topicId) > -1;
|
||||
if (noteHasNotebook && !noteHasTopic) {
|
||||
// 1 note can be inside multiple topics
|
||||
noteNotebook.topics.push(topicId);
|
||||
} else if (!noteHasNotebook) {
|
||||
notebooks.push({
|
||||
id: notebookId,
|
||||
topics: [topicId]
|
||||
});
|
||||
}
|
||||
|
||||
if (!noteHasNotebook || !noteHasTopic) {
|
||||
await this.db.notes.add({
|
||||
id: noteId,
|
||||
notebooks
|
||||
});
|
||||
this.topicReferences.add(topicId, noteId);
|
||||
}
|
||||
} else {
|
||||
await this.db.relations.add({ id: notebookId, type: "notebook" }, note);
|
||||
}
|
||||
await this.db.relations.add(
|
||||
{ id: notebookId, type: "notebook" },
|
||||
{ type: "note", id: noteId }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async removeFromNotebook(to: NotebookReference, ...noteIds: string[]) {
|
||||
if (!to) throw new Error("The destination notebook cannot be undefined.");
|
||||
if (!to.id) throw new Error("The destination notebook must contain id.");
|
||||
|
||||
const { id: notebookId, topic: topicId, rebuildCache = true } = to;
|
||||
|
||||
async removeFromNotebook(notebookId: string, ...noteIds: string[]) {
|
||||
for (const noteId of noteIds) {
|
||||
const note = this.collection.get(noteId);
|
||||
if (!note || isTrashItem(note)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (topicId) {
|
||||
if (!note.notebooks) continue;
|
||||
const { notebooks } = note;
|
||||
|
||||
const notebook = findById(notebooks, notebookId);
|
||||
if (!notebook) continue;
|
||||
|
||||
const { topics } = notebook;
|
||||
if (!deleteItem(topics, topicId)) continue;
|
||||
|
||||
if (topics.length <= 0) deleteItem(notebooks, notebook);
|
||||
|
||||
await this.db.notes.add({
|
||||
id: noteId,
|
||||
notebooks
|
||||
});
|
||||
} else {
|
||||
await this.db.relations.unlink(
|
||||
{ id: notebookId, type: "notebook" },
|
||||
note
|
||||
);
|
||||
}
|
||||
await this.db.relations.unlink(
|
||||
{ id: notebookId, type: "notebook" },
|
||||
{ type: "note", id: noteId }
|
||||
);
|
||||
}
|
||||
if (rebuildCache) this.topicReferences.rebuild();
|
||||
}
|
||||
|
||||
async removeFromAllNotebooks(...noteIds: string[]) {
|
||||
for (const noteId of noteIds) {
|
||||
const note = this.collection.get(noteId);
|
||||
if (!note || isTrashItem(note)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.db.notes.add({
|
||||
id: noteId,
|
||||
notebooks: []
|
||||
});
|
||||
await this.db.relations.unlinkAll(note, "notebook");
|
||||
}
|
||||
this.topicReferences.rebuild();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async _clearAllNotebookReferences(notebookId: string) {
|
||||
const notes = this.db.notes.all;
|
||||
|
||||
for (const note of notes) {
|
||||
const { notebooks } = note;
|
||||
if (!notebooks) continue;
|
||||
|
||||
for (const notebook of notebooks) {
|
||||
if (notebook.id !== notebookId) continue;
|
||||
if (!deleteItem(notebooks, notebook)) continue;
|
||||
}
|
||||
|
||||
await this.collection.update(note);
|
||||
await this.db.relations.unlinkAll(
|
||||
{ type: "note", id: noteId },
|
||||
"notebook"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,48 +360,3 @@ export class Notes implements ICollection {
|
||||
function getNoteHeadline(content: Tiptap) {
|
||||
return content.toHeadline();
|
||||
}
|
||||
|
||||
class NoteIdCache {
|
||||
private cache = new Map<string, string[]>();
|
||||
constructor(private readonly notes: Notes) {}
|
||||
|
||||
rebuild() {
|
||||
this.cache = new Map();
|
||||
const notes = this.notes.all;
|
||||
|
||||
for (const note of notes) {
|
||||
const { notebooks } = note;
|
||||
if (!notebooks) return;
|
||||
|
||||
for (const notebook of notebooks) {
|
||||
for (const topic of notebook.topics) {
|
||||
this.add(topic, note.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add(topicId: string, noteId: string) {
|
||||
let noteIds = this.cache.get(topicId);
|
||||
if (!noteIds) noteIds = [];
|
||||
if (noteIds.includes(noteId)) return;
|
||||
noteIds.push(noteId);
|
||||
this.cache.set(topicId, noteIds);
|
||||
}
|
||||
|
||||
has(topicId: string, noteId: string) {
|
||||
const noteIds = this.cache.get(topicId);
|
||||
if (!noteIds) return false;
|
||||
return noteIds.includes(noteId);
|
||||
}
|
||||
|
||||
count(topicId: string) {
|
||||
const noteIds = this.cache.get(topicId);
|
||||
if (!noteIds) return 0;
|
||||
return noteIds.length;
|
||||
}
|
||||
|
||||
get(topicId: string) {
|
||||
return this.cache.get(topicId) || [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import Database from "../api";
|
||||
|
||||
type RelationsArray<TType extends keyof ItemMap> = Relation[] & {
|
||||
resolved: (limit?: number) => ItemMap[TType][];
|
||||
has: (id: string) => boolean;
|
||||
};
|
||||
|
||||
export class Relations implements ICollection {
|
||||
@@ -71,9 +72,18 @@ export class Relations implements ICollection {
|
||||
reference: ItemReference,
|
||||
type: TType
|
||||
): RelationsArray<TType> {
|
||||
const relations = this.all.filter(
|
||||
(a) => compareItemReference(a.from, reference) && a.to.type === type
|
||||
);
|
||||
const relations =
|
||||
type === "note" || type === "notebook"
|
||||
? this.all.filter(
|
||||
(a) =>
|
||||
compareItemReference(a.from, reference) &&
|
||||
a.to.type === type &&
|
||||
!this.db.trash.exists(a.to.id)
|
||||
)
|
||||
: this.all.filter(
|
||||
(a) => compareItemReference(a.from, reference) && a.to.type === type
|
||||
);
|
||||
|
||||
Object.defineProperties(relations, {
|
||||
resolved: {
|
||||
writable: false,
|
||||
@@ -81,6 +91,12 @@ export class Relations implements ICollection {
|
||||
configurable: false,
|
||||
value: (limit?: number) =>
|
||||
this.resolve(limit ? relations.slice(0, limit) : relations, "to")
|
||||
},
|
||||
has: {
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
value: (id: string) => relations.some((rel) => rel.to.id === id)
|
||||
}
|
||||
});
|
||||
return relations as RelationsArray<TType>;
|
||||
@@ -90,9 +106,17 @@ export class Relations implements ICollection {
|
||||
reference: ItemReference,
|
||||
type: TType
|
||||
): RelationsArray<TType> {
|
||||
const relations = this.all.filter(
|
||||
(a) => compareItemReference(a.to, reference) && a.from.type === type
|
||||
);
|
||||
const relations =
|
||||
type === "note" || type === "notebook"
|
||||
? this.all.filter(
|
||||
(a) =>
|
||||
compareItemReference(a.to, reference) &&
|
||||
a.from.type === type &&
|
||||
!this.db.trash.exists(a.from.id)
|
||||
)
|
||||
: this.all.filter(
|
||||
(a) => compareItemReference(a.to, reference) && a.from.type === type
|
||||
);
|
||||
Object.defineProperties(relations, {
|
||||
resolved: {
|
||||
writable: false,
|
||||
@@ -100,6 +124,12 @@ export class Relations implements ICollection {
|
||||
configurable: false,
|
||||
value: (limit?: number) =>
|
||||
this.resolve(limit ? relations.slice(0, limit) : relations, "from")
|
||||
},
|
||||
has: {
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
value: (id: string) => relations.some((rel) => rel.from.id === id)
|
||||
}
|
||||
});
|
||||
return relations as RelationsArray<TType>;
|
||||
@@ -133,9 +163,9 @@ export class Relations implements ICollection {
|
||||
await this.remove(relation.id);
|
||||
}
|
||||
|
||||
async unlinkAll(to: ItemReference, type: keyof ItemMap) {
|
||||
async unlinkAll(to: ItemReference, type?: keyof ItemMap) {
|
||||
for (const relation of this.all.filter(
|
||||
(a) => compareItemReference(a.to, to) && a.from.type === type
|
||||
(a) => compareItemReference(a.to, to) && (!type || a.from.type === type)
|
||||
)) {
|
||||
await this.remove(relation.id);
|
||||
}
|
||||
|
||||
@@ -96,13 +96,6 @@ export class Shortcuts implements ICollection {
|
||||
item = notebook ? notebook.data : null;
|
||||
break;
|
||||
}
|
||||
case "topic": {
|
||||
const topic = this.db.notebooks
|
||||
.topics(shortcut.item.notebookId)
|
||||
.topic(id);
|
||||
if (topic) item = topic._topic;
|
||||
break;
|
||||
}
|
||||
case "tag":
|
||||
item = this.db.tags.tag(id);
|
||||
break;
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { createTopicModel } from "../models/topic";
|
||||
import { getId } from "../utils/id";
|
||||
import Database from "../api";
|
||||
import { clone } from "../utils/clone";
|
||||
import { Topic } from "../types";
|
||||
|
||||
export default class Topics {
|
||||
constructor(
|
||||
private readonly notebookId: string,
|
||||
private readonly db: Database
|
||||
) {}
|
||||
|
||||
has(topic: string) {
|
||||
return this.all.findIndex((v) => v.id === topic || v.title === topic) > -1;
|
||||
}
|
||||
|
||||
async add(...topics: Partial<Topic>[]) {
|
||||
const notebook = clone(this.db.notebooks.notebook(this.notebookId)?.data);
|
||||
if (!notebook) return;
|
||||
const allTopics = [...notebook.topics, ...topics];
|
||||
|
||||
notebook.topics = [];
|
||||
for (const t of allTopics) {
|
||||
const topic = makeTopic(t, this.notebookId);
|
||||
if (!topic) continue;
|
||||
|
||||
if (topics.findIndex((t) => t.id === topic.id) > -1)
|
||||
topic.dateEdited = Date.now();
|
||||
|
||||
const index = notebook.topics.findIndex((t) => t.id === topic.id);
|
||||
if (index > -1) {
|
||||
notebook.topics[index] = {
|
||||
...notebook.topics[index],
|
||||
...topic
|
||||
};
|
||||
} else {
|
||||
notebook.topics.push(topic);
|
||||
}
|
||||
}
|
||||
return this.db.notebooks.collection.update(notebook);
|
||||
}
|
||||
|
||||
get all() {
|
||||
return this.db.notebooks.notebook(this.notebookId)?.data.topics || [];
|
||||
}
|
||||
|
||||
topic(idOrTitleOrTopic: string | Topic) {
|
||||
const topic =
|
||||
typeof idOrTitleOrTopic === "string"
|
||||
? this.all.find(
|
||||
(t) => t.id === idOrTitleOrTopic || t.title === idOrTitleOrTopic
|
||||
)
|
||||
: idOrTitleOrTopic;
|
||||
if (!topic) return;
|
||||
return createTopicModel(topic, this.notebookId, this.db);
|
||||
}
|
||||
|
||||
async delete(...topicIds: string[]) {
|
||||
const notebook = clone(this.db.notebooks.notebook(this.notebookId)?.data);
|
||||
if (!notebook) return;
|
||||
|
||||
const allTopics = notebook.topics;
|
||||
for (const topicId of topicIds) {
|
||||
const topic = this.topic(topicId);
|
||||
if (!topic) continue;
|
||||
|
||||
await topic.clear();
|
||||
await this.db.shortcuts.remove(topicId);
|
||||
|
||||
const topicIndex = allTopics.findIndex(
|
||||
(t) => t.id === topicId || t.title === topicId
|
||||
);
|
||||
allTopics.splice(topicIndex, 1);
|
||||
}
|
||||
|
||||
return this.db.notebooks.collection.update(notebook);
|
||||
}
|
||||
}
|
||||
|
||||
// we export this for testing.
|
||||
export function makeTopic(
|
||||
topic: Partial<Topic>,
|
||||
notebookId: string
|
||||
): Topic | undefined {
|
||||
if (!topic.title) return;
|
||||
return {
|
||||
type: "topic",
|
||||
id: topic.id || getId(),
|
||||
notebookId: topic.notebookId || notebookId,
|
||||
title: topic.title.trim(),
|
||||
dateCreated: topic.dateCreated || Date.now(),
|
||||
dateEdited: topic.dateEdited || Date.now(),
|
||||
dateModified: Date.now()
|
||||
};
|
||||
}
|
||||
@@ -19,7 +19,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import Database from "../api";
|
||||
import { BaseTrashItem, Note, Notebook, isTrashItem } from "../types";
|
||||
import {
|
||||
BaseTrashItem,
|
||||
Note,
|
||||
Notebook,
|
||||
TrashItem,
|
||||
isTrashItem
|
||||
} from "../types";
|
||||
|
||||
function toTrashItem<T extends Note | Notebook>(item: T): BaseTrashItem<T> {
|
||||
return {
|
||||
@@ -33,10 +39,12 @@ function toTrashItem<T extends Note | Notebook>(item: T): BaseTrashItem<T> {
|
||||
|
||||
export default class Trash {
|
||||
collections = ["notes", "notebooks"] as const;
|
||||
cache: string[] = [];
|
||||
constructor(private readonly db: Database) {}
|
||||
|
||||
async init() {
|
||||
await this.cleanup();
|
||||
this.cache = this.all.map((t) => t.id);
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
@@ -54,8 +62,8 @@ export default class Trash {
|
||||
}
|
||||
}
|
||||
|
||||
get all() {
|
||||
const trashItems = [];
|
||||
get all(): TrashItem[] {
|
||||
const trashItems: TrashItem[] = [];
|
||||
for (const key of this.collections) {
|
||||
const collection = this.db[key];
|
||||
trashItems.push(...collection.trashed);
|
||||
@@ -78,22 +86,22 @@ export default class Trash {
|
||||
} else if (item.type === "notebook") {
|
||||
await this.db.notebooks.collection.update(toTrashItem(item));
|
||||
}
|
||||
this.cache.push(item.id);
|
||||
}
|
||||
|
||||
async delete(...ids: string[]) {
|
||||
for (const id of ids) {
|
||||
if (!id) continue;
|
||||
const [item, collection] = this.getItem(id);
|
||||
if (!item || !collection) continue;
|
||||
if (item.itemType === "note") {
|
||||
if (item.contentId) await this.db.content.remove(item.contentId);
|
||||
await this.db.noteHistory.clearSessions(id);
|
||||
} else if (item.itemType === "notebook") {
|
||||
await this.db.notes._clearAllNotebookReferences(item.id);
|
||||
await this.db.relations.unlinkAll({ type: "notebook", id: item.id });
|
||||
}
|
||||
await collection.remove(id);
|
||||
this.cache.splice(this.cache.indexOf(id), 1);
|
||||
}
|
||||
this.db.notes.topicReferences.rebuild();
|
||||
}
|
||||
|
||||
async restore(...ids: string[]) {
|
||||
@@ -108,14 +116,15 @@ export default class Trash {
|
||||
type: "notebook"
|
||||
});
|
||||
}
|
||||
this.cache.splice(this.cache.indexOf(id), 1);
|
||||
}
|
||||
this.db.notes.topicReferences.rebuild();
|
||||
}
|
||||
|
||||
async clear() {
|
||||
for (const item of this.all) {
|
||||
await this.delete(item.id);
|
||||
}
|
||||
this.cache = [];
|
||||
}
|
||||
|
||||
synced(id: string) {
|
||||
@@ -131,6 +140,6 @@ export default class Trash {
|
||||
* @param {string} id
|
||||
*/
|
||||
exists(id: string) {
|
||||
return this.all.findIndex((item) => item.id === id) > -1;
|
||||
return this.cache.includes(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,6 +246,15 @@ const migrations: Migration[] = [
|
||||
await db.colors.delete(oldColorId);
|
||||
}
|
||||
}
|
||||
|
||||
if (item.notebooks) {
|
||||
for (const notebook of item.notebooks) {
|
||||
for (const topic of notebook.topics) {
|
||||
await db.relations.add({ type: "notebook", id: topic }, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delete item.tags;
|
||||
delete item.color;
|
||||
return true;
|
||||
@@ -259,6 +268,27 @@ const migrations: Migration[] = [
|
||||
}
|
||||
delete item.noteIds;
|
||||
return true;
|
||||
},
|
||||
notebook: async (item, db) => {
|
||||
for (const topic of item.topics || []) {
|
||||
const subNotebookId = await db.notebooks.add({
|
||||
id: topic.id,
|
||||
title: topic.title,
|
||||
dateCreated: topic.dateCreated,
|
||||
dateEdited: topic.dateEdited,
|
||||
dateModified: topic.dateModified
|
||||
});
|
||||
if (!subNotebookId) continue;
|
||||
await db.relations.add(item, { id: subNotebookId, type: "notebook" });
|
||||
}
|
||||
delete item.topics;
|
||||
return true;
|
||||
},
|
||||
shortcut: (item) => {
|
||||
if (item.item.type === "topic") {
|
||||
item.item = { type: "notebook", id: item.item.id };
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Database from "../api";
|
||||
import Topics from "../collections/topics";
|
||||
import { Notebook } from "../types";
|
||||
|
||||
export function createNotebookModel(notebook: Notebook, db: Database) {
|
||||
@@ -34,10 +33,6 @@ export function createNotebookModel(notebook: Notebook, db: Database) {
|
||||
totalNotes: (function () {
|
||||
return db.notebooks.totalNotes(notebook.id);
|
||||
})(),
|
||||
/**
|
||||
* @deprecated please use `db.notebooks.topics()` instead
|
||||
*/
|
||||
topics: new Topics(notebook.id, db),
|
||||
/**
|
||||
* @deprecated please use `db.notebooks.pin()` & `db.notebooks.unpin()` instead.
|
||||
*/
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Database from "../api";
|
||||
import { Note, Topic } from "../types";
|
||||
import { clone } from "../utils/clone";
|
||||
|
||||
type TopicModel = Topic & {
|
||||
_topic: Topic;
|
||||
totalNotes: number;
|
||||
has: (noteId: string) => boolean;
|
||||
all: Note[];
|
||||
clear: () => Promise<void>;
|
||||
};
|
||||
export function createTopicModel(
|
||||
topic: Topic,
|
||||
notebookId: string,
|
||||
db: Database
|
||||
): TopicModel {
|
||||
return Object.defineProperties(clone(topic), {
|
||||
_topic: {
|
||||
get: () => topic
|
||||
},
|
||||
totalNotes: {
|
||||
get: () => db.notes?.topicReferences.count(topic.id)
|
||||
},
|
||||
has: {
|
||||
value: (noteId: string) => {
|
||||
return db.notes.topicReferences.has(topic.id, noteId);
|
||||
}
|
||||
},
|
||||
all: {
|
||||
get: () => getAllNotes(db, topic.id)
|
||||
},
|
||||
clear: {
|
||||
value: () => {
|
||||
const noteIds = db.notes?.topicReferences.get(topic.id);
|
||||
if (!noteIds.length) return;
|
||||
|
||||
return db.notes.removeFromNotebook(
|
||||
{
|
||||
topic: topic.id,
|
||||
id: notebookId,
|
||||
rebuildCache: true
|
||||
},
|
||||
...noteIds
|
||||
);
|
||||
}
|
||||
}
|
||||
}) as TopicModel;
|
||||
}
|
||||
|
||||
function getAllNotes(db: Database, topicId: string) {
|
||||
const noteIds = db.notes.topicReferences.get(topicId);
|
||||
return noteIds.reduce<Note[]>((arr, noteId) => {
|
||||
const note = db.notes.note(noteId);
|
||||
if (note) arr.push(note.data);
|
||||
return arr;
|
||||
}, []);
|
||||
}
|
||||
@@ -139,6 +139,9 @@ export interface Note extends BaseItem<"note"> {
|
||||
* @deprecated only kept here for migration purposes.
|
||||
*/
|
||||
color?: string;
|
||||
/**
|
||||
* @deprecated only kept here for migration purposes.
|
||||
*/
|
||||
notebooks?: NotebookReference[];
|
||||
|
||||
pinned: boolean;
|
||||
@@ -156,9 +159,15 @@ export interface Notebook extends BaseItem<"notebook"> {
|
||||
description?: string;
|
||||
dateEdited: number;
|
||||
pinned: boolean;
|
||||
topics: Topic[];
|
||||
/**
|
||||
* @deprecated only kept here for migration purposes.
|
||||
*/
|
||||
topics?: Topic[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated only kept here for migration purposes.
|
||||
*/
|
||||
export interface Topic extends BaseItem<"topic"> {
|
||||
title: string;
|
||||
notebookId: string;
|
||||
@@ -235,6 +244,9 @@ type TagNotebookShortcutReference = BaseShortcutReference & {
|
||||
type: "tag" | "notebook";
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated only kept here for migration purposes
|
||||
*/
|
||||
type TopicShortcutReference = BaseShortcutReference & {
|
||||
type: "topic";
|
||||
notebookId: string;
|
||||
|
||||
Reference in New Issue
Block a user