core: add new collection for shortcuts

This commit is contained in:
Abdullah Atta
2022-09-07 12:47:02 +05:00
parent ef552f8b38
commit 1e99d7732c
9 changed files with 251 additions and 1 deletions

View File

@@ -0,0 +1,81 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2022 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 { databaseTest, notebookTest, StorageInterface } from "./utils";
beforeEach(() => {
StorageInterface.clear();
});
test("create a shortcut of an invalid item should throw", () =>
databaseTest().then(async (db) => {
await expect(() =>
db.shorcuts.add({ item: { type: "HELLO!" } })
).rejects.toThrow(/cannot create a shortcut/i);
}));
test("create a shortcut of notebook", () =>
notebookTest().then(async ({ db, id }) => {
await db.shorcuts.add({ item: { type: "notebook", id } });
expect(db.shorcuts.exists(id)).toBe(true);
expect(db.shorcuts.all[0].item.id).toBe(id);
}));
test("create a duplicate shortcut of notebook", () =>
notebookTest().then(async ({ db, id }) => {
await db.shorcuts.add({ item: { type: "notebook", id } });
await db.shorcuts.add({ item: { type: "notebook", id } });
expect(db.shorcuts.all).toHaveLength(1);
expect(db.shorcuts.all[0].item.id).toBe(id);
}));
test("create shortcut of a topic", () =>
notebookTest().then(async ({ db, id }) => {
const notebook = db.notebooks.notebook(id)._notebook;
const topic = notebook.topics[0];
await db.shorcuts.add({
item: { type: "topic", id: topic.id, notebookId: id }
});
expect(db.shorcuts.all).toHaveLength(1);
expect(db.shorcuts.all[0].item.id).toBe(topic.id);
}));
test("pin a tag", () =>
databaseTest().then(async (db) => {
const tag = await db.tags.add("HELLO!");
await db.shorcuts.add({ item: { type: "tag", id: tag.id } });
expect(db.shorcuts.all).toHaveLength(1);
expect(db.shorcuts.all[0].item.id).toBe(tag.id);
}));
test("remove shortcut", () =>
databaseTest().then(async (db) => {
const tag = await db.tags.add("HELLO!");
const shortcutId = await db.shorcuts.add({
item: { type: "tag", id: tag.id }
});
expect(db.shorcuts.all).toHaveLength(1);
await db.shorcuts.remove(shortcutId);
expect(db.shorcuts.all).toHaveLength(0);
}));

View File

@@ -46,6 +46,7 @@ import MFAManager from "./mfa-manager";
import EventManager from "../utils/event-manager"; import EventManager from "../utils/event-manager";
import Pricing from "./pricing"; import Pricing from "./pricing";
import { logger } from "../logger"; import { logger } from "../logger";
import Shortcuts from "../collections/shortcuts";
/** /**
* @type {EventSource} * @type {EventSource}
@@ -139,6 +140,8 @@ class Database {
this.attachments = await Attachments.new(this, "attachments"); this.attachments = await Attachments.new(this, "attachments");
/**@type {NoteHistory} */ /**@type {NoteHistory} */
this.noteHistory = await NoteHistory.new(this, "notehistory", false); this.noteHistory = await NoteHistory.new(this, "notehistory", false);
/**@type {Shortcuts} */
this.shorcuts = await Shortcuts.new(this, "shorcuts");
this.trash = new Trash(this); this.trash = new Trash(this);

View File

@@ -75,6 +75,10 @@ class Migrations {
dbCollection: this._db.settings, dbCollection: this._db.settings,
type: "settings" type: "settings"
}, },
{
index: this._db.shorcuts.raw,
dbCollection: this._db.shorcuts
},
{ {
index: this._db.notes.raw, index: this._db.notes.raw,
dbCollection: this._db.notes dbCollection: this._db.notes

View File

@@ -23,7 +23,7 @@ import { logger } from "../../logger";
class Collector { class Collector {
/** /**
* *
* @param {Database} db * @param {import("../index").default} db
*/ */
constructor(db) { constructor(db) {
this._db = db; this._db = db;
@@ -38,6 +38,7 @@ class Collector {
const items = [ const items = [
...this._collect("note", this._db.notes.raw, isForceSync), ...this._collect("note", this._db.notes.raw, isForceSync),
...this._collect("shortcut", this._db.shorcuts.raw, isForceSync),
...this._collect("notebook", this._db.notebooks.raw, isForceSync), ...this._collect("notebook", this._db.notebooks.raw, isForceSync),
...this._collect("content", await this._db.content.all(), isForceSync), ...this._collect("content", await this._db.content.all(), isForceSync),
...this._collect( ...this._collect(

View File

@@ -43,6 +43,10 @@ class Merger {
get: (id) => this._db.notes.note(id), get: (id) => this._db.notes.note(id),
set: (item) => this._db.notes.merge(item) set: (item) => this._db.notes.merge(item)
}, },
shortcut: {
get: (id) => this._db.shorcuts.shortcut(id),
set: (item) => this._db.shorcuts.merge(item)
},
notebook: { notebook: {
threshold: 1000, threshold: 1000,
get: (id) => this._db.notebooks.notebook(id), get: (id) => this._db.notebooks.notebook(id),

View File

@@ -0,0 +1,148 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2022 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 Collection from "./collection";
import getId from "../utils/id";
/**
* @typedef {{
* id: string,
* type: "tag" | "notebook" | "topic",
* notebookId?: string
* }} ShortcutRef
*
* @typedef {{
* id: string,
* type: "shortcut",
* item: ShortcutRef,
* dateCreated: number,
* dateModified: number,
* sortIndex: number
* }} Shortcut
*
*/
const ALLOWED_SHORTCUT_TYPES = ["notebook", "topic", "tag"];
export default class Shortcuts extends Collection {
shortcut(id) {
return this._collection.getItem(id);
}
async merge(shortcut) {
if (!shortcut) return;
await this._collection.addItem(shortcut);
}
/**
*
* @param {Partial<Shortcut>} shortcut
* @returns
*/
async add(shortcut) {
if (!shortcut) return;
if (shortcut.remote)
throw new Error("Please use db.shortcuts.merge to merge remote notes.");
if (!ALLOWED_SHORTCUT_TYPES.includes(shortcut.item.type))
throw new Error("Cannot create a shortcut for this type of item.");
let oldShortcut = shortcut.item
? this.find(shortcut.item.id)
: shortcut.id
? this._collection.getItem(shortcut.id)
: null;
const id = shortcut.id || (oldShortcut && oldShortcut.id) || getId();
shortcut = {
...oldShortcut,
...shortcut
};
shortcut = {
id,
type: "shortcut",
item: {
type: shortcut.item.type,
id: shortcut.item.id,
notebookId: shortcut.item.notebookId
},
dateCreated: shortcut.dateCreated,
dateModified: shortcut.dateModified,
sortIndex: this._collection.count()
};
await this._collection.addItem(shortcut);
return shortcut.id;
}
get raw() {
return this._collection.getRaw();
}
/**
* @return {Shortcut[]}
*/
get all() {
return this._collection.getItems();
}
get resolved() {
return this.all.reduce((prev, shortcut) => {
const {
item: { id, type, notebookId }
} = shortcut;
let item = null;
switch (type) {
case "notebook": {
const notebook = this._db.notebooks.notebook(id);
item = notebook ? notebook.data : null;
break;
}
case "topic": {
const notebook = this._db.notebooks.notebook(notebookId);
if (notebook) {
const topic = notebook.topics.topic(id);
if (topic) item = topic._topic;
}
break;
}
case "tag":
item = this._db.tags.tag(id);
break;
}
if (item) prev.push(item);
return prev;
}, []);
}
exists(itemId) {
return !!this.find(itemId);
}
find(itemId) {
return this.all.find((shortcut) => shortcut.item.id === itemId);
}
async remove(...shortcutIds) {
for (const id of shortcutIds) {
await this._collection.removeItem(id);
}
}
}

View File

@@ -169,6 +169,10 @@ export default class Backup {
index: data["content"], index: data["content"],
dbCollection: this._db.content dbCollection: this._db.content
}, },
{
index: data["shortcuts"],
dbCollection: this._db.shorcuts
},
{ {
index: data["notes"], index: data["notes"],
dbCollection: this._db.notes dbCollection: this._db.notes

View File

@@ -70,6 +70,10 @@ export default class CachedCollection extends IndexedCollection {
return this.map.has(id); return this.map.has(id);
} }
count() {
return this.map.size;
}
getItem(id) { getItem(id) {
return this.map.get(id); return this.map.get(id);
} }

View File

@@ -57,6 +57,7 @@ export const migrations = {
5.5: {}, 5.5: {},
5.6: { 5.6: {
note: false, note: false,
shortcut: false,
notebook: false, notebook: false,
tag: false, tag: false,
attachment: false, attachment: false,