feat: implement tags & improve add note function

This commit is contained in:
thecodrr
2020-01-31 00:24:21 +05:00
parent f45da1f24c
commit 0bd3680f89
3 changed files with 249 additions and 62 deletions

View File

@@ -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: {

View File

@@ -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
View 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;