Skip to content

Commit

Permalink
Use autocomplete feature for generating tags
Browse files Browse the repository at this point in the history
Introduce tagsService which will handle the necessary filtering of tags
and saving them into the local storage in an appropriate format.
  • Loading branch information
gergely-ujvari committed Mar 16, 2015
1 parent ada5012 commit 90c782d
Show file tree
Hide file tree
Showing 10 changed files with 397 additions and 76 deletions.
26 changes: 22 additions & 4 deletions h/static/scripts/directives/annotation.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,13 @@ validate = (value) ->
# {@link annotationMapper AnnotationMapper service} for persistence.
###
AnnotationController = [
'$scope', '$timeout', '$rootScope', '$document',
'auth', 'drafts', 'flash', 'permissions',
'$scope', '$timeout', '$q', '$rootScope', '$document',
'auth', 'drafts', 'flash', 'permissions', 'tagHelpers',
'timeHelpers', 'annotationUI', 'annotationMapper'
($scope, $timeout, $rootScope, $document,
auth, drafts, flash, permissions,
($scope, $timeout, $q, $rootScope, $document,
auth, drafts, flash, permissions, tagHelpers,
timeHelpers, annotationUI, annotationMapper) ->

@annotation = {}
@action = 'view'
@document = null
Expand All @@ -50,6 +51,20 @@ AnnotationController = [
original = null
vm = this

###*
# @ngdoc method
# @name annotation.AnnotationController#tagsAutoComplete.
# @returns {Promise} immediately resolved to {string[]} -
# the tags to show in autocomplete.
###
this.tagsAutoComplete = (query) ->
deferred = $q.defer()

filteredTags = tagHelpers.filterTags(query)
deferred.resolve filteredTags

return deferred.promise

###*
# @ngdoc method
# @name annotation.AnnotationController#isComment.
Expand Down Expand Up @@ -145,6 +160,9 @@ AnnotationController = [
unless validate(@annotation)
return flash 'info', 'Please add text or a tag before publishing.'

# Update stored tags with the new tags of this annotation
tagHelpers.refreshTags(model.tags, @annotation.tags)

angular.extend model, @annotation,
tags: (tag.text for tag in @annotation.tags)

Expand Down
9 changes: 8 additions & 1 deletion h/static/scripts/directives/test/annotation-test.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ describe 'h.directives.annotation', ->
fakePermissions = null
fakePersonaFilter = null
fakeStore = null
fakeTagHelpers = null
fakeTimeHelpers = null
fakeUrlEncodeFilter = null
sandbox = null

before ->
angular.module('h', [])
angular.module('h', ['ng'])
require('../annotation')

beforeEach module('h')
Expand All @@ -48,6 +49,7 @@ describe 'h.directives.annotation', ->
remove: sandbox.stub()
}
fakeFlash = sandbox.stub()

fakeMomentFilter = sandbox.stub().returns('ages ago')
fakePermissions = {
isPublic: sandbox.stub().returns(true)
Expand All @@ -57,6 +59,10 @@ describe 'h.directives.annotation', ->
private: sandbox.stub().returns({read: ['justme']})
}
fakePersonaFilter = sandbox.stub().returnsArg(0)
fakeTagsHelpers = {
filterTags: sandbox.stub().returns('a while ago')
refreshTags: sandbox.stub().returns(30)
}
fakeTimeHelpers = {
toFuzzyString: sandbox.stub().returns('a while ago')
nextFuzzyUpdate: sandbox.stub().returns(30)
Expand All @@ -72,6 +78,7 @@ describe 'h.directives.annotation', ->
$provide.value 'permissions', fakePermissions
$provide.value 'personaFilter', fakePersonaFilter
$provide.value 'store', fakeStore
$provide.value 'tagHelpers', fakeTagHelpers
$provide.value 'timeHelpers', fakeTimeHelpers
$provide.value 'urlencodeFilter', fakeUrlEncodeFilter
return
Expand Down
53 changes: 17 additions & 36 deletions h/static/scripts/directives/test/privacy-test.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe 'h.directives.privacy', ->
$window = null
fakeAuth = null
fakePermissions = null
fakeLocalStorage = null
sandbox = null

before ->
Expand All @@ -28,6 +29,13 @@ describe 'h.directives.privacy', ->
user: 'acct:angry.joe@texas.com'
}

storage = {}
fakeLocalStorage = {
getItem: sandbox.spy (key) -> storage[key]
setItem: sandbox.spy (key, value) -> storage[key] = value
removeItem: sandbox.spy (key) -> delete storage[key]
}

fakePermissions = {
isPublic: sandbox.stub().returns(true)
isPrivate: sandbox.stub().returns(false)
Expand All @@ -37,6 +45,7 @@ describe 'h.directives.privacy', ->
}

$provide.value 'auth', fakeAuth
$provide.value 'localstorage', fakeLocalStorage
$provide.value 'permissions', fakePermissions
return

Expand All @@ -48,31 +57,7 @@ describe 'h.directives.privacy', ->
afterEach ->
sandbox.restore()

describe 'memory fallback', ->
$scope2 = null

beforeEach inject (_$rootScope_) ->
$scope2 = _$rootScope_.$new()

$window.localStorage = null

it 'stores the default visibility level when it changes', ->
$scope.permissions = {read: ['acct:user@example.com']}
$element = $compile('<privacy ng-model="permissions">')($scope)
$scope.$digest()
$isolateScope = $element.isolateScope()
$isolateScope.setLevel(name: VISIBILITY_PUBLIC)

$scope2.permissions = {read: []}
$element = $compile('<privacy ng-model="permissions">')($scope2)
$scope2.$digest()

# Roundabout way: the storage works because the directive
# could read out the privacy level
readPermissions = $scope2.permissions.read[0]
assert.equal readPermissions, 'everybody'

describe 'has localStorage', ->
describe 'saves visibility level', ->

it 'stores the default visibility level when it changes', ->
$scope.permissions = {read: ['acct:user@example.com']}
Expand All @@ -82,19 +67,15 @@ describe 'h.directives.privacy', ->
$isolateScope.setLevel(name: VISIBILITY_PUBLIC)

expected = VISIBILITY_PUBLIC
stored = $window.localStorage.getItem VISIBILITY_KEY
stored = fakeLocalStorage.getItem VISIBILITY_KEY
assert.equal stored, expected

describe 'setting permissions', ->
$element = null
store = null

beforeEach ->
store = $window.localStorage

describe 'when no setting is stored', ->
beforeEach ->
store.removeItem VISIBILITY_KEY
fakeLocalStorage.removeItem VISIBILITY_KEY

it 'defaults to public', ->
$scope.permissions = {read: []}
Expand All @@ -105,7 +86,7 @@ describe 'h.directives.privacy', ->

describe 'when permissions.read is empty', ->
beforeEach ->
store.setItem VISIBILITY_KEY, VISIBILITY_PUBLIC
fakeLocalStorage.setItem VISIBILITY_KEY, VISIBILITY_PUBLIC

$scope.permissions = {read: []}
$element = $compile('<privacy ng-model="permissions">')($scope)
Expand All @@ -115,14 +96,14 @@ describe 'h.directives.privacy', ->
assert.equal $element.isolateScope().level.name, VISIBILITY_PUBLIC

it 'does not alter the level on subsequent renderings', ->
store.setItem VISIBILITY_KEY, VISIBILITY_PRIVATE
fakeLocalStorage.setItem VISIBILITY_KEY, VISIBILITY_PRIVATE
$scope.permissions.read = ['acct:user@example.com']
$scope.$digest()
assert.equal $element.isolateScope().level.name, VISIBILITY_PUBLIC

describe 'when permissions.read is filled', ->
it 'does not alter the level', ->
store.setItem VISIBILITY_KEY, VISIBILITY_PRIVATE
fakeLocalStorage.setItem VISIBILITY_KEY, VISIBILITY_PRIVATE

$scope.permissions = {read: ['group:__world__']}
$element = $compile('<privacy ng-model="permissions">')($scope)
Expand All @@ -135,14 +116,14 @@ describe 'h.directives.privacy', ->
$scope.permissions = {read: []}

it 'fills the permissions fields with the auth.user name', ->
store.setItem VISIBILITY_KEY, VISIBILITY_PRIVATE
fakeLocalStorage.setItem VISIBILITY_KEY, VISIBILITY_PRIVATE
$element = $compile('<privacy ng-model="permissions">')($scope)
$scope.$digest()

assert.deepEqual $scope.permissions, fakePermissions.private()

it 'puts group_world into the read permissions for public visibility', ->
store.setItem VISIBILITY_KEY, VISIBILITY_PUBLIC
fakeLocalStorage.setItem VISIBILITY_KEY, VISIBILITY_PUBLIC
$element = $compile('<privacy ng-model="permissions">')($scope)
$scope.$digest()

Expand Down
1 change: 1 addition & 0 deletions h/static/scripts/helpers/helpers.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ angular.module('h.helpers', ['bootstrap'])

require('./form-helpers')
require('./string-helpers')
require('./tag-helpers')
require('./time-helpers')
require('./ui-helpers')
require('./xsrf-service')
58 changes: 58 additions & 0 deletions h/static/scripts/helpers/tag-helpers.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
createTagHelpers = ['localstorage', (localstorage) ->
TAGS_LIST_KEY = 'hypothesis.user.tags.list'
TAGS_MAP_KEY = 'hypothesis.user.tags.map'

filterTags: (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.
refreshTags: (existingTags, allTags) ->
savedTags = localstorage.getObject TAGS_MAP_KEY
savedTags ?= {}

for tag in allTags
if existingTags? and (tag.text in existingTags) and savedTags[tag.text]?
# We've already counted this tag, just update the timestamp
savedTags[tag.text].updated = Date.now()
else
# Update the tag counter too
if savedTags[tag.text]?
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
]

angular.module('h.helpers')
.service('tagHelpers', createTagHelpers)

0 comments on commit 90c782d

Please sign in to comment.