mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-03 04:31:46 +02: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"]);
|
||||
}));
|
||||
|
||||
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", () =>
|
||||
noteTest({
|
||||
content: {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
months,
|
||||
getLastWeekTimestamp
|
||||
} from "../utils/date";
|
||||
import setManipulator from "../utils/set";
|
||||
|
||||
const KEYS = {
|
||||
notes: "notes",
|
||||
@@ -19,6 +20,10 @@ const KEYS = {
|
||||
user: "user"
|
||||
};
|
||||
|
||||
const TYPES = {
|
||||
note: "note"
|
||||
};
|
||||
|
||||
function checkInitialized() {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error(
|
||||
@@ -120,80 +125,56 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
async addNote(_note) {
|
||||
if (!_note || !_note.content) return;
|
||||
let timestamp = _note.dateCreated || Date.now();
|
||||
let note = { ...this.notes[timestamp], ..._note };
|
||||
async addNote(n) {
|
||||
if (!n) return;
|
||||
|
||||
if (
|
||||
!this.notes[timestamp] &&
|
||||
(note.content.text.length <= 0 || !note.content.delta) &&
|
||||
(!note.title || note.title.length <= 0)
|
||||
) {
|
||||
let timestamp = n.dateCreated || Date.now();
|
||||
let oldNote = this.notes[timestamp];
|
||||
let note = {
|
||||
...oldNote,
|
||||
...n
|
||||
};
|
||||
|
||||
if (isNoteEmpty(note)) {
|
||||
if (oldNote) await this.deleteNotes(note);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(!note.title || note.title.length <= 0) &&
|
||||
!note.locked &&
|
||||
note.content.text.length <= 0 &&
|
||||
this.notes[timestamp]
|
||||
) {
|
||||
//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,
|
||||
note = {
|
||||
type: TYPES.note,
|
||||
title: getNoteTitle(note),
|
||||
content: getNoteContent(note),
|
||||
pinned: !!note.pinned,
|
||||
tags: note.tags || [],
|
||||
locked: note.locked || false,
|
||||
locked: !!note.locked,
|
||||
notebook: note.notebook || {},
|
||||
colors: note.colors || [],
|
||||
favorite: note.favorite || false,
|
||||
headline:
|
||||
!note.locked &&
|
||||
note.content.text.substring(0, 150) +
|
||||
(note.content.text.length > 150 ? "..." : ""),
|
||||
length: note.content.text.length,
|
||||
favorite: !!note.favorite,
|
||||
headline: getNoteHeadline(note),
|
||||
dateEditted: Date.now(),
|
||||
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);
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
//TODO only send unique values here...
|
||||
async updateTags(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,
|
||||
count: this[KEYS.tags][tag].count + 1
|
||||
count: oldCount + 1
|
||||
};
|
||||
}
|
||||
await this.storage.write(KEYS.tags, this[KEYS.tags]);
|
||||
@@ -442,15 +423,15 @@ export default Database;
|
||||
|
||||
async function deleteItems(items, key) {
|
||||
if (!items || items.length <= 0 || !this[key] || this[key].length <= 0) {
|
||||
console.log(items, items.length);
|
||||
return false;
|
||||
}
|
||||
for (let item of items) {
|
||||
if (!item) continue;
|
||||
if (this[key].hasOwnProperty(item.dateCreated)) {
|
||||
//delete note from the notebook too.
|
||||
if (item.type === "note" && item.notebook.hasOwnProperty("topic")) {
|
||||
if (item.type === "note") {
|
||||
if (
|
||||
item.notebook.hasOwnProperty("topic") &&
|
||||
!(await this.deleteNoteFromTopic(
|
||||
item.notebook.notebook,
|
||||
item.notebook.topic,
|
||||
@@ -459,6 +440,15 @@ async function deleteItems(items, key) {
|
||||
) {
|
||||
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") {
|
||||
let skip = false;
|
||||
for (let topic in item.topics) {
|
||||
@@ -471,9 +461,7 @@ async function deleteItems(items, key) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (skip) {
|
||||
continue;
|
||||
}
|
||||
if (skip) continue;
|
||||
}
|
||||
//put into trash
|
||||
this[KEYS.trash][item.dateCreated] = this[key][item.dateCreated];
|
||||
@@ -567,3 +555,40 @@ async function editItem(type, id, prop) {
|
||||
function mergeDedupe(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