mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-22 14:39:34 +01:00
feat: add notebook merging
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
|||||||
TEST_NOTEBOOK,
|
TEST_NOTEBOOK,
|
||||||
TEST_NOTE,
|
TEST_NOTE,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
import { makeTopic } from "../collections/topics";
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
StorageInterface.clear();
|
StorageInterface.clear();
|
||||||
@@ -43,6 +44,92 @@ test("unpin a notebook", () =>
|
|||||||
|
|
||||||
test("updating notebook with empty title should throw", () =>
|
test("updating notebook with empty title should throw", () =>
|
||||||
notebookTest().then(async ({ db, id }) => {
|
notebookTest().then(async ({ db, id }) => {
|
||||||
expect(id).toBeDefined();
|
|
||||||
await expect(db.notebooks.add({ id, title: "" })).rejects.toThrow();
|
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 = { ...notebook.data, remote: true };
|
||||||
|
newNotebook.topics.push(makeTopic("Home", id));
|
||||||
|
|
||||||
|
await expect(db.notebooks.merge(newNotebook)).resolves.not.toThrow();
|
||||||
|
|
||||||
|
expect(notebook.topics.has("Home")).toBe(true);
|
||||||
|
expect(notebook.topics.has("General")).toBe(true);
|
||||||
|
expect(notebook.topics.has("hello")).toBe(true);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("merge notebook with topics removed", () =>
|
||||||
|
notebookTest().then(async ({ db, id }) => {
|
||||||
|
let notebook = db.notebooks.notebook(id);
|
||||||
|
|
||||||
|
const newNotebook = { ...notebook.data, remote: true };
|
||||||
|
newNotebook.topics.splice(1, 1); // remove hello topic
|
||||||
|
newNotebook.topics.push(makeTopic("Home", id));
|
||||||
|
|
||||||
|
await expect(db.notebooks.merge(newNotebook)).resolves.not.toThrow();
|
||||||
|
|
||||||
|
expect(notebook.topics.has("Home")).toBe(true);
|
||||||
|
expect(notebook.topics.has("General")).toBe(true);
|
||||||
|
expect(notebook.topics.has("hello")).toBe(false);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("merge notebook with topic edited", () =>
|
||||||
|
notebookTest().then(async ({ db, id }) => {
|
||||||
|
let notebook = db.notebooks.notebook(id);
|
||||||
|
|
||||||
|
const newNotebook = { ...notebook.data, remote: true };
|
||||||
|
newNotebook.topics[1].title = "hello (edited)";
|
||||||
|
|
||||||
|
await expect(db.notebooks.merge(newNotebook)).resolves.not.toThrow();
|
||||||
|
|
||||||
|
expect(notebook.topics.has("hello (edited)")).toBe(true);
|
||||||
|
expect(notebook.topics.has("hello")).toBe(false);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("merge notebook when local notebook is also edited", () =>
|
||||||
|
notebookTest().then(async ({ db, id }) => {
|
||||||
|
let notebook = db.notebooks.notebook(id);
|
||||||
|
|
||||||
|
const newNotebook = { ...notebook.data, remote: true };
|
||||||
|
newNotebook.topics[1].title = "hello (edited)";
|
||||||
|
|
||||||
|
await delay(500);
|
||||||
|
|
||||||
|
await notebook.topics.add({
|
||||||
|
...notebook.topics.all[1],
|
||||||
|
title: "hello (edited too)",
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(db.notebooks.merge(newNotebook)).resolves.not.toThrow();
|
||||||
|
|
||||||
|
expect(notebook.topics.has("hello (edited too)")).toBe(true);
|
||||||
|
expect(notebook.topics.has("hello (edited)")).toBe(false);
|
||||||
|
expect(notebook.topics.has("hello")).toBe(false);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("merge notebook with topic removed that is edited in the local notebook", () =>
|
||||||
|
notebookTest().then(async ({ db, id }) => {
|
||||||
|
let notebook = db.notebooks.notebook(id);
|
||||||
|
|
||||||
|
const newNotebook = { ...notebook.data, remote: true };
|
||||||
|
newNotebook.topics.splice(1, 1); // remove hello topic
|
||||||
|
|
||||||
|
await delay(500);
|
||||||
|
|
||||||
|
await notebook.topics.add({
|
||||||
|
...notebook.topics.all[1],
|
||||||
|
title: "hello (i exist)",
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(db.notebooks.merge(newNotebook)).resolves.not.toThrow();
|
||||||
|
|
||||||
|
expect(notebook.topics.has("hello (i exist)")).toBe(true);
|
||||||
|
expect(notebook.topics.has("hello")).toBe(false);
|
||||||
|
}));
|
||||||
|
|
||||||
|
function delay(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ class Merger {
|
|||||||
await this._mergeArray(
|
await this._mergeArray(
|
||||||
notebooks,
|
notebooks,
|
||||||
(id) => this._db.notebooks.notebook(id),
|
(id) => this._db.notebooks.notebook(id),
|
||||||
(item) => this._db.notebooks.add(item)
|
(item) => this._db.notebooks.merge(item)
|
||||||
);
|
);
|
||||||
|
|
||||||
await this._mergeArrayWithConflicts(
|
await this._mergeArrayWithConflicts(
|
||||||
|
|||||||
@@ -6,12 +6,46 @@ import { CHECK_IDS, sendCheckUserStatusEvent } from "../common";
|
|||||||
import { qclone } from "qclone";
|
import { qclone } from "qclone";
|
||||||
|
|
||||||
export default class Notebooks extends Collection {
|
export default class Notebooks extends Collection {
|
||||||
|
async merge(remoteNotebook) {
|
||||||
|
const id = remoteNotebook.id || getId();
|
||||||
|
let localNotebook = this._collection.getItem(id);
|
||||||
|
|
||||||
|
if (localNotebook && localNotebook.topics?.length) {
|
||||||
|
// merge new and old topics
|
||||||
|
|
||||||
|
// We need to handle 3 cases:
|
||||||
|
for (let 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 > the dateEdited of newNotebook
|
||||||
|
// it was newly added or edited so add it to the new notebook.
|
||||||
|
if (!newTopic && oldTopic.dateEdited > remoteNotebook.dateEdited) {
|
||||||
|
remoteNotebook.topics.push(oldTopic);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
if (newTopic && oldTopic.dateEdited > newTopic.dateEdited) {
|
||||||
|
remoteNotebook.topics[newTopicIndex] = oldTopic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await this._collection.addItem(remoteNotebook);
|
||||||
|
}
|
||||||
|
|
||||||
async add(notebookArg) {
|
async add(notebookArg) {
|
||||||
if (!notebookArg) throw new Error("Notebook cannot be undefined or null.");
|
if (!notebookArg) throw new Error("Notebook cannot be undefined or null.");
|
||||||
|
if (notebookArg.remote)
|
||||||
if (notebookArg.remote) {
|
throw new Error(
|
||||||
return await this._collection.addItem(notebookArg);
|
"Please use db.notebooks.merge to merge remote notebooks"
|
||||||
}
|
);
|
||||||
|
|
||||||
//TODO reliably and efficiently check for duplicates.
|
//TODO reliably and efficiently check for duplicates.
|
||||||
const id = notebookArg.id || getId();
|
const id = notebookArg.id || getId();
|
||||||
|
|||||||
@@ -111,7 +111,8 @@ export default class Topics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeTopic(topic, notebookId) {
|
// we export this for testing.
|
||||||
|
export function makeTopic(topic, notebookId) {
|
||||||
if (typeof topic !== "string") return topic;
|
if (typeof topic !== "string") return topic;
|
||||||
return {
|
return {
|
||||||
type: "topic",
|
type: "topic",
|
||||||
|
|||||||
Reference in New Issue
Block a user