Commit 9051fefd authored by Sean Hammond's avatar Sean Hammond Committed by GitHub

Merge pull request #431 from hypothesis/tags-decaf

Convert tags service to JS
parents 93632029 515a59e1
module.exports = ['localStorage', (localStorage) ->
TAGS_LIST_KEY = 'hypothesis.user.tags.list'
TAGS_MAP_KEY = 'hypothesis.user.tags.map'
filter: (query) ->
savedTags = localStorage.getObject TAGS_LIST_KEY
savedTags ?= []
# Only show tags having query as a substring
filterFn = (e) ->
e.toLowerCase().indexOf(query.toLowerCase()) > -1
savedTags.filter(filterFn)
# Add newly added tags from an annotation to the stored ones and refresh
# timestamp for every tags used.
store: (tags) ->
savedTags = localStorage.getObject TAGS_MAP_KEY
savedTags ?= {}
for tag in tags
if savedTags[tag.text]?
# Update counter and timestamp
savedTags[tag.text].count += 1
savedTags[tag.text].updated = Date.now()
else
# Brand new tag, create an entry for it
savedTags[tag.text] = {
text: tag.text
count: 1
updated: Date.now()
}
localStorage.setObject TAGS_MAP_KEY, savedTags
tagsList = []
for tag of savedTags
tagsList[tagsList.length] = tag
# Now produce TAGS_LIST, ordered by (count desc, lexical asc)
compareFn = (t1, t2) ->
if savedTags[t1].count != savedTags[t2].count
return savedTags[t2].count - savedTags[t1].count
else
return -1 if t1 < t2
return 1 if t1 > t2
return 0
tagsList = tagsList.sort(compareFn)
localStorage.setObject TAGS_LIST_KEY, tagsList
]
'use strict';
/**
* @typedef Tag
* @property {string} text - The label of the tag
* @property {number} count - The number of times this tag has been used.
* @property {number} updated - The timestamp when this tag was last used.
*/
/**
* Service for fetching tag suggestions and storing data to generate them.
*
* The `tags` service stores metadata about recently used tags to local storage
* and provides a `filter` method to fetch tags matching a query, ranked based
* on frequency of usage.
*/
// @ngInject
function tags(localStorage) {
var TAGS_LIST_KEY = 'hypothesis.user.tags.list';
var TAGS_MAP_KEY = 'hypothesis.user.tags.map';
/**
* Return a list of tag suggestions matching `query`.
*
* @param {string} query
* @return {Tag[]} List of matching tags
*/
function filter(query) {
var savedTags = localStorage.getObject(TAGS_LIST_KEY) || [];
return savedTags.filter((e) => {
return e.toLowerCase().indexOf(query.toLowerCase()) !== -1;
});
}
/**
* Update the list of stored tag suggestions based on the tags that a user has
* entered for a given annotation.
*
* @param {Tag} tags - List of tags.
*/
function store(tags) {
// Update the stored (tag, frequency) map.
var savedTags = localStorage.getObject(TAGS_MAP_KEY) || {};
tags.forEach((tag) => {
if (savedTags[tag.text]) {
savedTags[tag.text].count += 1;
savedTags[tag.text].updated = Date.now();
} else {
savedTags[tag.text] = {
text: tag.text,
count: 1,
updated: Date.now(),
};
}
});
localStorage.setObject(TAGS_MAP_KEY, savedTags);
// Sort tag suggestions by frequency.
var tagsList = Object.keys(savedTags).sort((t1, t2) => {
if (savedTags[t1].count !== savedTags[t2].count) {
return savedTags[t2].count - savedTags[t1].count;
}
return t1.localeCompare(t2);
});
localStorage.setObject(TAGS_LIST_KEY, tagsList);
}
return {
filter,
store,
};
}
module.exports = tags;
{module, inject} = angular.mock
describe 'tags', ->
TAGS_LIST_KEY = 'hypothesis.user.tags.list'
TAGS_MAP_KEY = 'hypothesis.user.tags.map'
fakeLocalStorage = null
sandbox = null
savedTagsMap = null
savedTagsList = null
tags = null
before ->
angular.module('h', []).service('tags', require('../tags'))
beforeEach module('h')
beforeEach module ($provide) ->
sandbox = sinon.sandbox.create()
fakeStorage = {}
fakeLocalStorage = {
getObject: sandbox.spy (key) -> fakeStorage[key]
setObject: sandbox.spy (key, value) -> fakeStorage[key] = value
wipe: -> fakeStorage = {}
}
$provide.value 'localStorage', fakeLocalStorage
return
beforeEach inject (_tags_) ->
tags = _tags_
afterEach ->
sandbox.restore()
beforeEach ->
fakeLocalStorage.wipe()
stamp = Date.now()
savedTagsMap =
foo:
text: 'foo'
count: 1
updated: stamp
bar:
text: 'bar'
count: 5
updated: stamp
future:
text: 'future'
count: 2
updated: stamp
argon:
text: 'argon'
count: 1
updated: stamp
savedTagsList = ['bar', 'future', 'argon', 'foo']
fakeLocalStorage.setObject TAGS_MAP_KEY, savedTagsMap
fakeLocalStorage.setObject TAGS_LIST_KEY, savedTagsList
describe 'filter()', ->
it 'returns tags having the query as a substring', ->
assert.deepEqual(tags.filter('a'), ['bar', 'argon'])
it 'is case insensitive', ->
assert.deepEqual(tags.filter('Ar'), ['bar', 'argon'])
describe 'store()', ->
it 'saves new tags to storage', ->
tags.store([{text: 'new'}])
storedTagsList = fakeLocalStorage.getObject TAGS_LIST_KEY
assert.deepEqual(storedTagsList, ['bar', 'future', 'argon', 'foo', 'new'])
storedTagsMap = fakeLocalStorage.getObject TAGS_MAP_KEY
assert.isTrue(storedTagsMap.new?)
assert.equal(storedTagsMap.new.count, 1)
assert.equal(storedTagsMap.new.text, 'new')
it 'increases the count for a tag already stored', ->
tags.store([{text: 'bar'}])
storedTagsMap = fakeLocalStorage.getObject TAGS_MAP_KEY
assert.equal(storedTagsMap.bar.count, 6)
it 'list is ordered by count desc, lexical asc', ->
# Will increase from 1 to 6 (as future)
tags.store([{text: 'foo'}])
tags.store([{text: 'foo'}])
tags.store([{text: 'foo'}])
tags.store([{text: 'foo'}])
tags.store([{text: 'foo'}])
storedTagsList = fakeLocalStorage.getObject TAGS_LIST_KEY
assert.deepEqual(storedTagsList, ['foo', 'bar', 'future', 'argon'])
it 'gets/sets its objects from the localstore', ->
tags.store([{text: 'foo'}])
assert.called(fakeLocalStorage.getObject)
assert.called(fakeLocalStorage.setObject)
'use strict';
var angular = require('angular');
var TAGS_LIST_KEY = 'hypothesis.user.tags.list';
var TAGS_MAP_KEY = 'hypothesis.user.tags.map';
class FakeStorage {
constructor() {
this._storage = {};
}
getObject(key) {
return this._storage[key];
}
setObject(key, value) {
this._storage[key] = value;
}
}
describe('sidebar.tags', () => {
var fakeLocalStorage;
var tags;
before(() => {
angular.module('h', [])
.service('tags', require('../tags'));
});
beforeEach(() => {
fakeLocalStorage = new FakeStorage();
var stamp = Date.now();
var savedTagsMap = {
foo: {
text: 'foo',
count: 1,
updated: stamp,
},
bar: {
text: 'bar',
count: 5,
updated: stamp,
},
future: {
text: 'future',
count: 2,
updated: stamp,
},
argon: {
text: 'argon',
count: 1,
updated: stamp,
},
};
var savedTagsList = Object.keys(savedTagsMap);
fakeLocalStorage.setObject(TAGS_MAP_KEY, savedTagsMap);
fakeLocalStorage.setObject(TAGS_LIST_KEY, savedTagsList);
angular.mock.module('h', {
localStorage: fakeLocalStorage,
});
angular.mock.inject((_tags_) => {
tags = _tags_;
});
});
describe('#filter', () => {
it('returns tags having the query as a substring', () => {
assert.deepEqual(tags.filter('a'), ['bar', 'argon']);
});
it('is case insensitive', () => {
assert.deepEqual(tags.filter('Ar'), ['bar', 'argon']);
});
});
describe('#store', () => {
it('saves new tags to storage', () => {
tags.store([{text: 'new'}]);
var storedTagsList = fakeLocalStorage.getObject(TAGS_LIST_KEY);
assert.include(storedTagsList, 'new');
var storedTagsMap = fakeLocalStorage.getObject(TAGS_MAP_KEY);
assert.match(storedTagsMap.new, sinon.match({
count: 1,
text: 'new',
updated: sinon.match.number,
}));
});
it('increases the count for a tag already stored', () => {
tags.store([{text: 'bar'}]);
var storedTagsMap = fakeLocalStorage.getObject(TAGS_MAP_KEY);
assert.equal(storedTagsMap.bar.count, 6);
});
it('orders list by count descending, lexical ascending', () => {
for (var i = 0; i < 6; i++) {
tags.store([{text: 'foo'}]);
}
var storedTagsList = fakeLocalStorage.getObject(TAGS_LIST_KEY);
assert.deepEqual(storedTagsList, ['foo', 'bar', 'future', 'argon']);
});
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment