mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 23:19:40 +01:00
feat: implement tags & improve add note function
This commit is contained in:
@@ -141,6 +141,57 @@ test("update note", () =>
|
|||||||
expect(note.colors).toStrictEqual(["red", "blue"]);
|
expect(note.colors).toStrictEqual(["red", "blue"]);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
test("updating empty note should delete it", () =>
|
||||||
|
noteTest().then(async ({ db, timestamp }) => {
|
||||||
|
let updateTimestamp = await db.addNote({
|
||||||
|
dateCreated: timestamp,
|
||||||
|
title: "\n\n",
|
||||||
|
content: {
|
||||||
|
text: "",
|
||||||
|
delta: []
|
||||||
|
},
|
||||||
|
pinned: true,
|
||||||
|
favorite: true,
|
||||||
|
colors: ["red", "blue"]
|
||||||
|
});
|
||||||
|
expect(updateTimestamp).toBeUndefined();
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("updating note with duplicate colors", () =>
|
||||||
|
noteTest({
|
||||||
|
...TEST_NOTE,
|
||||||
|
colors: ["red", "blue"]
|
||||||
|
}).then(async ({ db, timestamp }) => {
|
||||||
|
let updateTimestamp = await db.addNote({
|
||||||
|
dateCreated: timestamp,
|
||||||
|
colors: ["red", "red", "blue", "blue"]
|
||||||
|
});
|
||||||
|
expect(updateTimestamp).toBe(timestamp);
|
||||||
|
let note = db.getNote(updateTimestamp);
|
||||||
|
expect(note.colors).toStrictEqual(["red", "blue"]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
test("updating note with new tags", () =>
|
||||||
|
noteTest({
|
||||||
|
...TEST_NOTE,
|
||||||
|
tags: ["hello", "world"]
|
||||||
|
}).then(async ({ db, timestamp }) => {
|
||||||
|
let updateTimestamp = await db.addNote({
|
||||||
|
dateCreated: timestamp,
|
||||||
|
tags: ["new", "tag", "goes", "here"]
|
||||||
|
});
|
||||||
|
expect(updateTimestamp).toBe(timestamp);
|
||||||
|
let note = db.getNote(updateTimestamp);
|
||||||
|
expect(note.tags).toStrictEqual([
|
||||||
|
"hello",
|
||||||
|
"world",
|
||||||
|
"new",
|
||||||
|
"tag",
|
||||||
|
"goes",
|
||||||
|
"here"
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
test("note with text longer than 150 characters should have ... in the headline", () =>
|
test("note with text longer than 150 characters should have ... in the headline", () =>
|
||||||
noteTest({
|
noteTest({
|
||||||
content: {
|
content: {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
months,
|
months,
|
||||||
getLastWeekTimestamp
|
getLastWeekTimestamp
|
||||||
} from "../utils/date";
|
} from "../utils/date";
|
||||||
|
import setManipulator from "../utils/set";
|
||||||
|
|
||||||
const KEYS = {
|
const KEYS = {
|
||||||
notes: "notes",
|
notes: "notes",
|
||||||
@@ -19,6 +20,10 @@ const KEYS = {
|
|||||||
user: "user"
|
user: "user"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TYPES = {
|
||||||
|
note: "note"
|
||||||
|
};
|
||||||
|
|
||||||
function checkInitialized() {
|
function checkInitialized() {
|
||||||
if (!this.isInitialized) {
|
if (!this.isInitialized) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -120,80 +125,56 @@ class Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async addNote(_note) {
|
async addNote(n) {
|
||||||
if (!_note || !_note.content) return;
|
if (!n) return;
|
||||||
let timestamp = _note.dateCreated || Date.now();
|
|
||||||
let note = { ...this.notes[timestamp], ..._note };
|
|
||||||
|
|
||||||
if (
|
let timestamp = n.dateCreated || Date.now();
|
||||||
!this.notes[timestamp] &&
|
let oldNote = this.notes[timestamp];
|
||||||
(note.content.text.length <= 0 || !note.content.delta) &&
|
let note = {
|
||||||
(!note.title || note.title.length <= 0)
|
...oldNote,
|
||||||
) {
|
...n
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isNoteEmpty(note)) {
|
||||||
|
if (oldNote) await this.deleteNotes(note);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
note = {
|
||||||
(!note.title || note.title.length <= 0) &&
|
type: TYPES.note,
|
||||||
!note.locked &&
|
title: getNoteTitle(note),
|
||||||
note.content.text.length <= 0 &&
|
content: getNoteContent(note),
|
||||||
this.notes[timestamp]
|
pinned: !!note.pinned,
|
||||||
) {
|
|
||||||
//delete the note
|
|
||||||
await this.deleteNotes(note);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//add or update a note into the database
|
|
||||||
let title =
|
|
||||||
note.title ||
|
|
||||||
note.content.text
|
|
||||||
.split(" ")
|
|
||||||
.slice(0, 3)
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
//if note exists
|
|
||||||
if (this.notes[timestamp] !== undefined) {
|
|
||||||
let oldNote = this.notes[timestamp];
|
|
||||||
//if we are having new colors
|
|
||||||
if (oldNote.colors !== note.colors && note.colors) {
|
|
||||||
note.colors = mergeDedupe([oldNote.colors, note.colors]);
|
|
||||||
}
|
|
||||||
//if we are having new tags
|
|
||||||
//TODO add new tags to the tags collection...
|
|
||||||
if (oldNote.tags !== note.tags && note.tags) {
|
|
||||||
note.tags = mergeDedupe([oldNote.tags, note.tags]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.notes[timestamp] = {
|
|
||||||
type: "note",
|
|
||||||
title,
|
|
||||||
content: note.content,
|
|
||||||
pinned: note.pinned || false,
|
|
||||||
tags: note.tags || [],
|
tags: note.tags || [],
|
||||||
locked: note.locked || false,
|
locked: !!note.locked,
|
||||||
notebook: note.notebook || {},
|
notebook: note.notebook || {},
|
||||||
colors: note.colors || [],
|
colors: note.colors || [],
|
||||||
favorite: note.favorite || false,
|
favorite: !!note.favorite,
|
||||||
headline:
|
headline: getNoteHeadline(note),
|
||||||
!note.locked &&
|
|
||||||
note.content.text.substring(0, 150) +
|
|
||||||
(note.content.text.length > 150 ? "..." : ""),
|
|
||||||
length: note.content.text.length,
|
|
||||||
dateEditted: Date.now(),
|
dateEditted: Date.now(),
|
||||||
dateCreated: timestamp
|
dateCreated: timestamp
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (oldNote) {
|
||||||
|
note.colors = setManipulator.union(oldNote.colors, note.colors);
|
||||||
|
await this.updateTags(setManipulator.complement(note.tags, oldNote.tags));
|
||||||
|
note.tags = setManipulator.union(oldNote.tags, note.tags);
|
||||||
|
} else {
|
||||||
|
await this.updateTags(note.tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notes[timestamp] = note;
|
||||||
await this.storage.write(KEYS.notes, this.notes);
|
await this.storage.write(KEYS.notes, this.notes);
|
||||||
return timestamp;
|
return timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO only send unique values here...
|
|
||||||
async updateTags(tags) {
|
async updateTags(tags) {
|
||||||
for (let tag of tags) {
|
for (let tag of tags) {
|
||||||
this[KEYS.tags][tag] = {
|
if (!tag || tag.trim().length <= 0) continue;
|
||||||
|
let oldCount = this.tags[tag] ? this.tags[tag].count : 0;
|
||||||
|
this.tags[tag] = {
|
||||||
title: tag,
|
title: tag,
|
||||||
count: this[KEYS.tags][tag].count + 1
|
count: oldCount + 1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
await this.storage.write(KEYS.tags, this[KEYS.tags]);
|
await this.storage.write(KEYS.tags, this[KEYS.tags]);
|
||||||
@@ -442,15 +423,15 @@ export default Database;
|
|||||||
|
|
||||||
async function deleteItems(items, key) {
|
async function deleteItems(items, key) {
|
||||||
if (!items || items.length <= 0 || !this[key] || this[key].length <= 0) {
|
if (!items || items.length <= 0 || !this[key] || this[key].length <= 0) {
|
||||||
console.log(items, items.length);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (let item of items) {
|
for (let item of items) {
|
||||||
if (!item) continue;
|
if (!item) continue;
|
||||||
if (this[key].hasOwnProperty(item.dateCreated)) {
|
if (this[key].hasOwnProperty(item.dateCreated)) {
|
||||||
//delete note from the notebook too.
|
//delete note from the notebook too.
|
||||||
if (item.type === "note" && item.notebook.hasOwnProperty("topic")) {
|
if (item.type === "note") {
|
||||||
if (
|
if (
|
||||||
|
item.notebook.hasOwnProperty("topic") &&
|
||||||
!(await this.deleteNoteFromTopic(
|
!(await this.deleteNoteFromTopic(
|
||||||
item.notebook.notebook,
|
item.notebook.notebook,
|
||||||
item.notebook.topic,
|
item.notebook.topic,
|
||||||
@@ -459,6 +440,15 @@ async function deleteItems(items, key) {
|
|||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
//TODO test
|
||||||
|
if (item.tags.length > 0) {
|
||||||
|
for (let tag of item.tags) {
|
||||||
|
this.tags[tag] = {
|
||||||
|
...this.tags[tag],
|
||||||
|
count: this.tags[tag].count - 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (item.type === "notebook") {
|
} else if (item.type === "notebook") {
|
||||||
let skip = false;
|
let skip = false;
|
||||||
for (let topic in item.topics) {
|
for (let topic in item.topics) {
|
||||||
@@ -471,9 +461,7 @@ async function deleteItems(items, key) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (skip) {
|
if (skip) continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//put into trash
|
//put into trash
|
||||||
this[KEYS.trash][item.dateCreated] = this[key][item.dateCreated];
|
this[KEYS.trash][item.dateCreated] = this[key][item.dateCreated];
|
||||||
@@ -567,3 +555,40 @@ async function editItem(type, id, prop) {
|
|||||||
function mergeDedupe(arr) {
|
function mergeDedupe(arr) {
|
||||||
return [...new Set([].concat(...arr))];
|
return [...new Set([].concat(...arr))];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isNoteEmpty(note) {
|
||||||
|
return (
|
||||||
|
!note.content ||
|
||||||
|
!note.content.delta ||
|
||||||
|
(!note.locked &&
|
||||||
|
(!note.title || note.title.trim().length <= 0) &&
|
||||||
|
(!note.content.text || note.content.text.trim().length <= 0))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNoteHeadline(note) {
|
||||||
|
if (note.locked) return "";
|
||||||
|
return (
|
||||||
|
note.content.text.substring(0, 150) +
|
||||||
|
(note.content.text.length > 150 ? "..." : "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNoteTitle(note) {
|
||||||
|
if (note.title && note.title.length > 0) return note.title.trim();
|
||||||
|
return note.content.text
|
||||||
|
.split(" ")
|
||||||
|
.slice(0, 3)
|
||||||
|
.join(" ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNoteContent(note) {
|
||||||
|
if (note.locked) {
|
||||||
|
return note.content;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: note.content.text.trim(),
|
||||||
|
delta: note.content.delta
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
111
packages/core/utils/set.js
Normal file
111
packages/core/utils/set.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
// SetManipulator MIT Licence © 2016 Edwin Monk-Fromont http://github.com/edmofro
|
||||||
|
// Based on setOps.js MIT License © 2014 James Abney http://github.com/jabney
|
||||||
|
|
||||||
|
// Set operations union, intersection, symmetric difference,
|
||||||
|
// relative complement, equals. Set operations are fast.
|
||||||
|
class SetManipulator {
|
||||||
|
constructor(identityExtractor) {
|
||||||
|
// Create and push the uid identity method.
|
||||||
|
identityExtractor = identityExtractor || (identity => identity);
|
||||||
|
this.uidList = [identityExtractor];
|
||||||
|
this.uid = identityExtractor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push a new uid method onto the stack. Call this and
|
||||||
|
// supply a unique key generator for sets of objects.
|
||||||
|
pushIdentityExtractor(method) {
|
||||||
|
this.uidList.push(method);
|
||||||
|
this.uid = method;
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop the previously pushed uid method off the stack and
|
||||||
|
// assign top of stack to uid. Return the previous method.
|
||||||
|
popIdentityExtractor() {
|
||||||
|
let prev;
|
||||||
|
if (this.uidList.length > 1) prev = this.uidList.pop();
|
||||||
|
this.uid = this.uidList[this.uidList.length - 1];
|
||||||
|
return prev || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processes a histogram consructed from two arrays, 'a' and 'b'.
|
||||||
|
// This function is used generically by the below set operation
|
||||||
|
// methods, a.k.a, 'evaluators', to return some subset of
|
||||||
|
// a set union, based on frequencies in the histogram.
|
||||||
|
process(a, b, evaluator, identityExtractor) {
|
||||||
|
// If identity extractor passed in, push it on the stack
|
||||||
|
if (identityExtractor) this.pushIdentityExtractor(identityExtractor);
|
||||||
|
// Create a histogram of 'a'.
|
||||||
|
const hist = {};
|
||||||
|
const out = [];
|
||||||
|
let ukey;
|
||||||
|
a.forEach(value => {
|
||||||
|
ukey = this.uid(value);
|
||||||
|
if (!hist[ukey]) {
|
||||||
|
hist[ukey] = { value: value, freq: 1 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Merge 'b' into the histogram.
|
||||||
|
b.forEach(value => {
|
||||||
|
ukey = this.uid(value);
|
||||||
|
if (hist[ukey]) {
|
||||||
|
if (hist[ukey].freq === 1) hist[ukey].freq = 3;
|
||||||
|
} else hist[ukey] = { value: value, freq: 2 };
|
||||||
|
});
|
||||||
|
// Pop any new identity extractor
|
||||||
|
if (identityExtractor) this.popIdentityExtractor(identityExtractor);
|
||||||
|
// Call the given evaluator.
|
||||||
|
if (evaluator) {
|
||||||
|
for (const key in hist) {
|
||||||
|
if (!hist.hasOwnProperty(key)) continue; // Property from object prototype, skip
|
||||||
|
if (evaluator(hist[key].freq)) out.push(hist[key].value);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return hist;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join two sets together.
|
||||||
|
// Set.union([1, 2, 2], [2, 3]) => [1, 2, 3]
|
||||||
|
union(a, b, identityExtractor) {
|
||||||
|
return this.process(a, b, () => true, identityExtractor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return items common to both sets.
|
||||||
|
// Set.intersection([1, 1, 2], [2, 2, 3]) => [2]
|
||||||
|
intersection(a, b, identityExtractor) {
|
||||||
|
return this.process(a, b, freq => freq === 3, identityExtractor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Symmetric difference. Items from either set that
|
||||||
|
// are not in both sets.
|
||||||
|
// Set.difference([1, 1, 2], [2, 3, 3]) => [1, 3]
|
||||||
|
difference(a, b, identityExtractor) {
|
||||||
|
return this.process(a, b, freq => freq < 3, identityExtractor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relative complement. Items from 'a' which are
|
||||||
|
// not also in 'b'.
|
||||||
|
// Set.complement([1, 2, 2], [2, 2, 3]) => [3]
|
||||||
|
complement(a, b, identityExtractor) {
|
||||||
|
return this.process(a, b, freq => freq === 1, identityExtractor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if both sets are equivalent, false otherwise.
|
||||||
|
// Set.equals([1, 1, 2], [1, 2, 2]) => true
|
||||||
|
// Set.equals([1, 1, 2], [1, 2, 3]) => false
|
||||||
|
equals(a, b, identityExtractor) {
|
||||||
|
let max = 0;
|
||||||
|
let min = Math.pow(2, 53);
|
||||||
|
const hist = this.process(a, b, identityExtractor);
|
||||||
|
for (const key in hist) {
|
||||||
|
if (!hist.hasOwnProperty(key)) continue; // Property from object prototype, skip
|
||||||
|
max = Math.max(max, hist[key].freq);
|
||||||
|
min = Math.min(min, hist[key].freq);
|
||||||
|
}
|
||||||
|
return min === 3 && max === 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setManipulator = new SetManipulator();
|
||||||
|
export default setManipulator;
|
||||||
Reference in New Issue
Block a user