From 41c1344d6f9a05e06cf9fd9eb61234e2e80b863e Mon Sep 17 00:00:00 2001 From: thecodrr Date: Wed, 5 Feb 2020 00:17:12 +0500 Subject: [PATCH] feat: implement topics and move note --- packages/core/__tests__/notebooks.test.js | 53 +----------- packages/core/__tests__/notes.test.js | 99 ++++++++++++----------- packages/core/__tests__/topics.test.js | 48 +++++++++++ packages/core/__tests__/utils/index.js | 76 +++++++++++++++++ packages/core/api/index.js | 4 +- packages/core/collections/notebooks.js | 16 ++-- packages/core/collections/notes.js | 39 ++++++++- packages/core/collections/tags.js | 4 + packages/core/collections/topics.js | 97 +++++++++++++++++++--- 9 files changed, 318 insertions(+), 118 deletions(-) create mode 100644 packages/core/__tests__/topics.test.js create mode 100644 packages/core/__tests__/utils/index.js diff --git a/packages/core/__tests__/notebooks.test.js b/packages/core/__tests__/notebooks.test.js index 4008192a9..bc8c8ad95 100644 --- a/packages/core/__tests__/notebooks.test.js +++ b/packages/core/__tests__/notebooks.test.js @@ -1,27 +1,4 @@ -import DB from "../api"; -import StorageInterface from "../__mocks__/storage.mock"; - -const TEST_NOTEBOOK = { - title: "Test Notebook", - description: "Test Description", - topics: ["hello", "hello", " "] -}; -const TEST_NOTEBOOK2 = { - title: "Test Notebook 2", - description: "Test Description 2", - topics: ["Home2"] -}; - -function databaseTest() { - let db = new DB(StorageInterface); - return db.init().then(() => db); -} - -const notebookTest = (notebook = TEST_NOTEBOOK) => - databaseTest().then(async db => { - let id = await db.notebooks.add(notebook); - return { db, id }; - }); +import { StorageInterface, notebookTest, TEST_NOTEBOOK } from "./utils"; beforeEach(async () => { StorageInterface.clear(); @@ -77,31 +54,3 @@ test("unfavorite a notebook", () => let notebook = db.notebooks.get(id); expect(notebook.favorite).toBe(false); })); - -test("get empty topic", () => - notebookTest().then(({ db, id }) => { - let notes = db.notebooks.topics(id).get("General"); - expect(notes.length).toBe(0); - })); - -test("getting invalid topic should throw", () => - notebookTest().then(({ db, id }) => { - expect(() => db.notebooks.topics(id).get("invalid")).toThrow(); - })); - -test("add topic to notebook", () => - notebookTest().then(async ({ db, id }) => { - let topics = db.notebooks.topics(id); - await topics.add("Home"); - expect(topics.all.length).toBeGreaterThan(1); - expect(topics.all.findIndex(v => v.title === "Home")).toBeGreaterThan(-1); - })); - -test("duplicate topic to notebook should not be added", () => - notebookTest().then(async ({ db, id }) => { - let topics = db.notebooks.topics(id); - await topics.add("Home"); - let len = topics.all.length; - await topics.add("Home"); - expect(topics.all.length).toBe(len); - })); diff --git a/packages/core/__tests__/notes.test.js b/packages/core/__tests__/notes.test.js index 6d91c4cb3..456a201b3 100644 --- a/packages/core/__tests__/notes.test.js +++ b/packages/core/__tests__/notes.test.js @@ -1,49 +1,11 @@ -import DB from "../api"; -import StorageInterface from "../__mocks__/storage.mock"; -import { getLastWeekTimestamp } from "../utils/date"; - -var TEST_NOTE = { - content: { delta: "I am a delta", text: "I am a text" } -}; - -const LONG_TEXT = - "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."; - -function databaseTest() { - let db = new DB(StorageInterface); - return db.init().then(() => db); -} - -const noteTest = (note = TEST_NOTE) => - databaseTest().then(async db => { - let id = await db.notes.add(note); - return { db, id }; - }); - -const groupedTest = (type, special = false) => - noteTest().then(async ({ db }) => { - await db.notes.add({ ...TEST_NOTE, title: "HELLO WHAT!" }); - await db.notes.add({ - ...TEST_NOTE, - title: "Some title", - dateCreated: getLastWeekTimestamp() - 604800000 - }); - await db.notes.add({ - ...TEST_NOTE, - title: "Some title and title title", - dateCreated: getLastWeekTimestamp() - 604800000 * 2 - }); - let grouped = db.notes.group(type, special); - if (special) { - expect(grouped.items.length).toBeGreaterThan(0); - expect(grouped.groups.length).toBeGreaterThan(0); - expect(grouped.groupCounts.length).toBeGreaterThan(0); - return; - } - expect(grouped.length).toBeGreaterThan(0); - expect(grouped[0].data.length).toBeGreaterThan(0); - expect(grouped[0].title.length).toBeGreaterThan(0); - }); +import { + StorageInterface, + databaseTest, + noteTest, + groupedTest, + LONG_TEXT, + TEST_NOTE +} from "./utils"; beforeEach(async () => { StorageInterface.clear(); @@ -247,3 +209,48 @@ test("lock and unlock note", () => note = db.notes.get(id); expect(note.locked).toBe(false); })); + +test("add note to topic", () => + noteTest().then(async ({ db, id }) => { + let notebookId = await db.notebooks.add({ title: "Hello" }); + let topics = db.notebooks.topics(notebookId); + let topic = await topics.add("Home"); + await topic.add(id); + expect(topic.all.length).toBe(1); + let note = db.notes.get(id); + expect(note.notebook.id).toBe(notebookId); + })); + +test("duplicate note to topic should not be added", () => + noteTest().then(async ({ db, id }) => { + let notebookId = await db.notebooks.add({ title: "Hello" }); + let topics = db.notebooks.topics(notebookId); + let topic = await topics.add("Home"); + await topic.add(id); + expect(topic.all.length).toBe(1); + })); + +test("move note", () => + noteTest().then(async ({ db, id }) => { + let notebookId = await db.notebooks.add({ title: "Hello" }); + let topics = db.notebooks.topics(notebookId); + let topic = await topics.add("Home"); + await topic.add(id); + + let notebookId2 = await db.notebooks.add({ title: "Hello2" }); + await db.notebooks.topics(notebookId2).add("Home2"); + await db.notes.move({ id: notebookId2, topic: "Home2" }, id); + let note = db.notes.get(id); + expect(note.notebook.id).toBe(notebookId2); + })); + +test("moving note to same notebook and topic should do nothing", () => + noteTest().then(async ({ db, id }) => { + let notebookId = await db.notebooks.add({ title: "Hello" }); + let topics = db.notebooks.topics(notebookId); + let topic = await topics.add("Home"); + await topic.add(id); + await db.notes.move({ id: notebookId, topic: "Home" }, id); + let note = db.notes.get(id); + expect(note.notebook.id).toBe(notebookId); + })); diff --git a/packages/core/__tests__/topics.test.js b/packages/core/__tests__/topics.test.js new file mode 100644 index 000000000..3b3538c04 --- /dev/null +++ b/packages/core/__tests__/topics.test.js @@ -0,0 +1,48 @@ +import { notebookTest, StorageInterface } from "./utils"; + +beforeEach(() => StorageInterface.clear()); + +test("get empty topic", () => + notebookTest().then(({ db, id }) => { + let topic = db.notebooks.topics(id).topic("General"); + expect(topic.all.length).toBe(0); + })); + +test("getting invalid topic should throw", () => + notebookTest().then(({ db, id }) => { + expect(() => db.notebooks.topics(id).get("invalid")).toThrow(); + })); + +test("add topic to notebook", () => + notebookTest().then(async ({ db, id }) => { + let topics = db.notebooks.topics(id); + await topics.add("Home"); + expect(topics.all.length).toBeGreaterThan(1); + expect(topics.all.findIndex(v => v.title === "Home")).toBeGreaterThan(-1); + })); + +test("duplicate topic to notebook should not be added", () => + notebookTest().then(async ({ db, id }) => { + let topics = db.notebooks.topics(id); + await topics.add("Home"); + let len = topics.all.length; + await topics.add("Home"); + expect(topics.all.length).toBe(len); + })); + +test("get topic", () => + notebookTest().then(async ({ db, id }) => { + let topics = db.notebooks.topics(id); + let topic = await topics.add("Home"); + let noteId = await db.notes.add({ content: { text: "Hello", delta: [] } }); + await topic.add(noteId); + expect(topic.all[0].content.text).toBe("Hello"); + })); + +test("delete a topic", () => + notebookTest().then(async ({ db, id }) => { + let topics = db.notebooks.topics(id); + await topics.add("Home"); + await topics.delete("Home"); + expect(topics.all.findIndex(v => v.title === "Home")).toBe(-1); + })); diff --git a/packages/core/__tests__/utils/index.js b/packages/core/__tests__/utils/index.js new file mode 100644 index 000000000..20cb92207 --- /dev/null +++ b/packages/core/__tests__/utils/index.js @@ -0,0 +1,76 @@ +import DB from "../../api"; +import StorageInterface from "../../__mocks__/storage.mock"; +import { getLastWeekTimestamp } from "../../utils/date"; + +const TEST_NOTEBOOK = { + title: "Test Notebook", + description: "Test Description", + topics: ["hello", "hello", " "] +}; + +const TEST_NOTEBOOK2 = { + title: "Test Notebook 2", + description: "Test Description 2", + topics: ["Home2"] +}; + +function databaseTest() { + let db = new DB(StorageInterface); + return db.init().then(() => db); +} + +const notebookTest = (notebook = TEST_NOTEBOOK) => + databaseTest().then(async db => { + let id = await db.notebooks.add(notebook); + return { db, id }; + }); + +var TEST_NOTE = { + content: { delta: "I am a delta", text: "I am a text" } +}; + +const LONG_TEXT = + "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."; + +const noteTest = (note = TEST_NOTE) => + databaseTest().then(async db => { + let id = await db.notes.add(note); + return { db, id }; + }); + +const groupedTest = (type, special = false) => + noteTest().then(async ({ db }) => { + await db.notes.add({ ...TEST_NOTE, title: "HELLO WHAT!" }); + await db.notes.add({ + ...TEST_NOTE, + title: "Some title", + dateCreated: getLastWeekTimestamp() - 604800000 + }); + await db.notes.add({ + ...TEST_NOTE, + title: "Some title and title title", + dateCreated: getLastWeekTimestamp() - 604800000 * 2 + }); + let grouped = db.notes.group(type, special); + if (special) { + expect(grouped.items.length).toBeGreaterThan(0); + expect(grouped.groups.length).toBeGreaterThan(0); + expect(grouped.groupCounts.length).toBeGreaterThan(0); + return; + } + expect(grouped.length).toBeGreaterThan(0); + expect(grouped[0].data.length).toBeGreaterThan(0); + expect(grouped[0].title.length).toBeGreaterThan(0); + }); + +export { + databaseTest, + notebookTest, + noteTest, + groupedTest, + StorageInterface, + TEST_NOTEBOOK, + TEST_NOTEBOOK2, + TEST_NOTE, + LONG_TEXT +}; diff --git a/packages/core/api/index.js b/packages/core/api/index.js index 565f25720..914d0e39b 100644 --- a/packages/core/api/index.js +++ b/packages/core/api/index.js @@ -6,8 +6,10 @@ class DB { this.context = context; } async init() { - this.notes = new Notes(this.context); this.notebooks = new Notebooks(this.context); + this.notes = new Notes(this.context); + await this.notes.init(this.notebooks); + await this.notebooks.init(this.notes); } } diff --git a/packages/core/collections/notebooks.js b/packages/core/collections/notebooks.js index 6ce217f03..52a5e4501 100644 --- a/packages/core/collections/notebooks.js +++ b/packages/core/collections/notebooks.js @@ -9,6 +9,12 @@ export default class Notebooks { constructor(context) { this.context = context; this.collection = new CachedCollection(context, "notebooks"); + this.notes = undefined; + } + + init(notes) { + this.notes = notes; + return this.collection.init(); } async add(notebookArg) { @@ -24,11 +30,11 @@ export default class Notebooks { ...oldNotebook, ...notebookArg }; - if (oldNotebook && oldNotebook.topics) { - notebook.topics = [...oldNotebook.topics]; - } - if (notebookArg.topics) { + /* if (notebookArg.topics) { notebook.topics = [...notebook.topics, ...notebookArg.topics]; + } */ + if (oldNotebook && oldNotebook.topics) { + notebook.topics = [...notebook.topics, ...oldNotebook.topics]; } let topics = notebook.topics || []; @@ -90,7 +96,7 @@ export default class Notebooks { } topics(id) { - return new Topics(this, id); + return new Topics(this, this.notes, id); } pin(id) { diff --git a/packages/core/collections/notes.js b/packages/core/collections/notes.js index 22aa06c4c..b66b4aa1f 100644 --- a/packages/core/collections/notes.js +++ b/packages/core/collections/notes.js @@ -9,6 +9,7 @@ import { getLastWeekTimestamp } from "../utils/date"; import Storage from "../database/storage"; +import Notebooks from "./notebooks"; var tfun = require("transfun/transfun.js").tfun; if (!tfun) { tfun = global.tfun; @@ -21,8 +22,14 @@ export default class Notes { this.tagsCollection = new Tags(context); } - async init() { + /** + * + * @param {Notebooks} notebooks + */ + async init(notebooks) { await this.collection.init(); + this.notebooks = notebooks; + await this.tagsCollection.init(); } async add(noteArg) { @@ -203,6 +210,36 @@ export default class Notes { await this.collection.addItem(note); } + async move(to, ...noteIds) { + if (!to) throw new Error("The destination notebook cannot be undefined."); + if (!to.id || !to.topic) + throw new Error( + "The destination notebook must contain notebookId and topic." + ); + let topic = this.notebooks.topics(to.id).topic(to.topic); + if (!topic) throw new Error("No such topic exists."); + await topic.transaction(async () => { + for (let id of noteIds) { + let note = this.get(id); + if (!note) continue; + /* if (note.notebook && note.notebook.id & note.notebook.topic) { + if ( + note.notebook.id === notebook.id && + note.notebook.topic === topic.title + ) { + continue; + } + let topic = this.notebooks + .topics(note.notebook.id) + .topic(note.notebook.topic); + if (!topic) continue; + await topic.delete(id); + } */ + await topic.add(id); + } + }); + } + async favorite(id) { await this.add({ id, favorite: true }); } diff --git a/packages/core/collections/tags.js b/packages/core/collections/tags.js index b3f02df31..97e53f3d8 100644 --- a/packages/core/collections/tags.js +++ b/packages/core/collections/tags.js @@ -5,6 +5,10 @@ export default class Tags { this.collection = new CachedCollection(context, "tags"); } + init() { + return this.collection.init(); + } + async add(id) { if (!id || id.trim().length <= 0) return; let tag = (await this.collection.getItem(id)) || { diff --git a/packages/core/collections/topics.js b/packages/core/collections/topics.js index a5083b096..e507dcedb 100644 --- a/packages/core/collections/topics.js +++ b/packages/core/collections/topics.js @@ -5,41 +5,42 @@ export default class Topics { /** * * @param {Notebooks} notebooks + * @param {Notes} notes * @param {string} notebookId */ - constructor(notebooks, notebookId) { + constructor(notebooks, notes, notebookId) { this.notebooks = notebooks; this.notebookId = notebookId; - this.notes = new Notes(this.notebooks.context); + this.notes = notes; } - add(topic) { - return this.notebooks.add({ + async add(topic) { + await this.notebooks.add({ id: this.notebookId, topics: [topic] }); + return this.topic(topic); } get all() { return this.notebooks.get(this.notebookId).topics; } - get(topic) { - let notebook = this.notebooks.get(this.notebookId); + topic(topic) { if (typeof topic === "string") { - topic = notebook.topics.find(v => v.title === topic); + topic = this.all.find(t => t.title === topic); } - if (!topic) - throw new Error("topics.get: Topic cannot be undefined or null."); - if (!topic.notes) - throw new Error("topics.get: Topic must contain an array of note ids."); - return topic.notes.map(note => this.notes.get(note)); + if (!topic) return; + return new Topic(this, topic); } async delete(...topics) { let notebook = this.notebooks.get(this.notebookId); for (let topic of topics) { - let index = notebook.topics.findIndex(t => (t.title = topic.title)); + if (!topic) continue; + let index = notebook.topics.findIndex( + t => t.title === topic.title || topic + ); if (index <= -1) continue; notebook.topics.splice(index, 1); } @@ -49,3 +50,73 @@ export default class Topics { }); } } + +class Topic { + /** + * + * @param {Topics} topics + * @param {Object} topic + */ + constructor(topics, topic) { + this.topic = topic; + this.topics = topics; + this.transactionOpen = false; + } + + transaction(ops) { + this.transactionOpen = true; + return ops().then(() => { + this.transactionOpen = false; + }); + } + + has(noteId) { + return this.topic.notes.findIndex(n => n === noteId) > -1; + } + + async add(noteId) { + let note = this.topics.notes.get(noteId); + if (this.has(noteId) || !note) return this; + + this.topic.notes.push(noteId); + + if (note.notebook && note.notebook.id && note.notebook.topic) { + if ( + note.notebook.id === this.topics.notebookId && + note.notebook.topic === this.topic.title + ) + return this; + await this.topics.notebooks + .topics(note.notebook.id) + .topic(note.notebook.topic) + .delete(note.id); + } + + await this.topics.notes.add({ + id: noteId, + notebook: { id: this.topics.notebookId, topic: this.topic.title } + }); + + return await this.save(); + } + + async delete(noteId) { + if (!this.has(noteId)) return this; + let index = this.topic.notes.findIndex(n => n === noteId); + this.topic.notes.splice(index, 1); + await this.topics.notes.add({ + id: noteId, + notebook: {} + }); + return await this.save(); + } + + save() { + if (this.transactionOpen) return this; + return this.topics.add(this.topic); + } + + get all() { + return this.topic.notes.map(note => this.topics.notes.get(note)); + } +}