Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
coopwire-hypothesis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
孙灵跃 Leon Sun
coopwire-hypothesis
Commits
41904b1b
Commit
41904b1b
authored
Jul 04, 2016
by
Sean Hammond
Committed by
GitHub
Jul 04, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #8 from hypothesis/simplify-annot-cmp
Simplify annotation component by removing duplicated state
parents
9d6f56bc
8db47b83
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
322 additions
and
653 deletions
+322
-653
annotation.js
h/static/scripts/directive/annotation.js
+127
-234
annotation-test.js
h/static/scripts/directive/test/annotation-test.js
+181
-405
annotation.html
h/templates/client/annotation.html
+14
-14
No files found.
h/static/scripts/directive/annotation.js
View file @
41904b1b
/* jshint node: true */
'use strict'
;
var
angular
=
require
(
'angular'
);
var
annotationMetadata
=
require
(
'../annotation-metadata'
);
var
events
=
require
(
'../events'
);
var
memoize
=
require
(
'../util/memoize'
);
var
persona
=
require
(
'../filter/persona'
);
var
isNew
=
annotationMetadata
.
isNew
;
...
...
@@ -31,95 +30,22 @@ function errorMessage(reason) {
return
message
;
}
/** Restore unsaved changes to this annotation from the drafts service.
*
* If there are no draft changes to this annotation, does nothing.
*
*/
function
restoreFromDrafts
(
drafts
,
domainModel
,
vm
)
{
var
draft
=
drafts
.
get
(
domainModel
);
if
(
draft
)
{
vm
.
isPrivate
=
draft
.
isPrivate
;
vm
.
form
.
tags
=
draft
.
tags
;
vm
.
form
.
text
=
draft
.
text
;
}
}
/**
* Save the given annotation to the drafts service.
*
* Any existing drafts for this annotation will be overwritten.
*
* @param {object} drafts - The drafts service
* @param {object} domainModel - The full domainModel object of the
* annotation to be saved. This full domainModel model is not retrieved
* again from drafts, it's only used to identify the annotation's draft in
* order to retrieve the fields below.
* @param {object} vm - The view model object containing the user's unsaved
* changes to the annotation.
*
*/
function
saveToDrafts
(
drafts
,
domainModel
,
vm
)
{
drafts
.
update
(
domainModel
,
{
isPrivate
:
vm
.
isPrivate
,
tags
:
vm
.
form
.
tags
,
text
:
vm
.
form
.
text
,
});
}
/** Update domainModel from vm.
*
* Copy any properties from vm that might have been modified by the user into
* domainModel, overwriting any existing properties in domainModel.
*
* @param {object} domainModel The object to copy properties to
* @param {object} vm The object to copy properties from
*
* Return a copy of `annotation` with changes made in the editor applied.
*/
function
updateDomainModel
(
domainModel
,
vm
,
permissions
)
{
domainModel
.
text
=
vm
.
form
.
text
;
domainModel
.
tags
=
vm
.
form
.
tags
;
if
(
vm
.
isPrivate
)
{
domainModel
.
permissions
=
permissions
.
private
();
}
else
{
domainModel
.
permissions
=
permissions
.
shared
(
domainModel
.
group
);
}
}
/** Update the view model from the domain model changes. */
function
updateViewModel
(
$scope
,
domainModel
,
vm
,
permissions
)
{
vm
.
form
=
{
text
:
domainModel
.
text
,
tags
:
domainModel
.
tags
,
};
if
(
domainModel
.
links
)
{
vm
.
linkInContext
=
domainModel
.
links
.
incontext
||
domainModel
.
links
.
html
||
''
;
vm
.
linkHTML
=
domainModel
.
links
.
html
||
''
;
}
else
{
vm
.
linkInContext
=
''
;
vm
.
linkHTML
=
''
;
}
vm
.
isPrivate
=
permissions
.
isPrivate
(
domainModel
.
permissions
,
domainModel
.
user
);
vm
.
documentMeta
=
annotationMetadata
.
domainAndTitle
(
domainModel
);
function
updateModel
(
annotation
,
changes
,
permissions
)
{
return
Object
.
assign
({},
annotation
,
{
// Explicitly copy across the non-enumerable local tag for the annotation
$
$tag
:
annotation
.
$$tag
,
// Apply changes from the draft
tags
:
changes
.
tags
,
text
:
changes
.
text
,
permissions
:
changes
.
isPrivate
?
permissions
.
private
()
:
permissions
.
shared
(
annotation
.
group
),
});
}
/**
* @ngdoc type
* @name annotation.AnnotationController
*
*/
// @ngInject
function
AnnotationController
(
$document
,
$q
,
$rootScope
,
$scope
,
$timeout
,
$window
,
annotationUI
,
...
...
@@ -127,7 +53,6 @@ function AnnotationController(
settings
,
store
)
{
var
vm
=
this
;
var
domainModel
;
var
newlyCreatedByHighlightButton
;
/** Save an annotation to the server. */
...
...
@@ -164,13 +89,6 @@ function AnnotationController(
* can call the methods.
*/
function
init
()
{
/** The currently active action - 'view', 'create' or 'edit'. */
vm
.
action
=
'view'
;
/** vm.form is the read-write part of vm for the templates: it contains
* the variables that the templates will write changes to via ng-model. */
vm
.
form
=
{};
// The remaining properties on vm are read-only properties for the
// templates.
...
...
@@ -180,9 +98,6 @@ function AnnotationController(
/** Give the template access to the feature flags. */
vm
.
feature
=
features
.
flagEnabled
;
/** Whether or not this annotation is private. */
vm
.
isPrivate
=
false
;
/** Determines whether controls to expand/collapse the annotation body
* are displayed adjacent to the tags field.
*/
...
...
@@ -197,13 +112,6 @@ function AnnotationController(
/** True if the 'Share' dialog for this annotation is currently open. */
vm
.
showShareDialog
=
false
;
/** The domain model, contains the currently saved version of the
* annotation from the server (or in the case of new annotations that
* haven't been saved yet - the data that will be saved to the server when
* they are saved).
*/
domainModel
=
vm
.
annotation
;
/**
* `true` if this AnnotationController instance was created as a result of
* the highlight button being clicked.
...
...
@@ -212,35 +120,34 @@ function AnnotationController(
* or annotation that was fetched from the server (as opposed to created
* new client-side).
*/
newlyCreatedByHighlightButton
=
domainModel
.
$highlight
||
false
;
// Call `onAnnotationUpdated()` whenever the "annotationUpdated" event is
// emitted. This event is emitted after changes to the annotation are
// successfully saved to the server, and also when changes to the
// annotation made by another client are received by this client from the
// server.
$rootScope
.
$on
(
events
.
ANNOTATION_UPDATED
,
onAnnotationUpdated
);
newlyCreatedByHighlightButton
=
vm
.
annotation
.
$highlight
||
false
;
// When a new annotation is created, remove any existing annotations that
// are empty
$rootScope
.
$on
(
events
.
BEFORE_ANNOTATION_CREATED
,
deleteIfNewAndEmpty
);
// are empty.
//
// This event is currently emitted with $emit rather than $broadcast so
// we have to listen for it on the $rootScope and manually de-register
// on destruction.
var
removeNewAnnotListener
=
$rootScope
.
$on
(
events
.
BEFORE_ANNOTATION_CREATED
,
deleteIfNewAndEmpty
);
// Call `onDestroy()` when the component is destroyed.
$scope
.
$on
(
'$destroy'
,
onDestroy
);
vm
.
$onDestroy
=
function
()
{
removeNewAnnotListener
();
};
// Call `onGroupFocused()` whenever the currently-focused group changes.
$scope
.
$on
(
events
.
GROUP_FOCUSED
,
onGroupFocused
);
// New annotations (just created locally by the client, rather then
// received from the server) have some fields missing. Add them.
domainModel
.
user
=
domainModel
.
user
||
session
.
state
.
userid
;
domainModel
.
group
=
domainModel
.
group
||
groups
.
focused
().
id
;
if
(
!
domainModel
.
permissions
)
{
domainModel
.
permissions
=
permissions
[
'default'
](
domainModel
.
group
);
vm
.
annotation
.
user
=
vm
.
annotation
.
user
||
session
.
state
.
userid
;
vm
.
annotation
.
group
=
vm
.
annotation
.
group
||
groups
.
focused
().
id
;
if
(
!
vm
.
annotation
.
permissions
)
{
vm
.
annotation
.
permissions
=
permissions
[
'default'
](
vm
.
annotation
.
group
);
}
domainModel
.
text
=
domainModel
.
text
||
''
;
if
(
!
Array
.
isArray
(
domainModel
.
tags
))
{
domainModel
.
tags
=
[];
vm
.
annotation
.
text
=
vm
.
annotation
.
text
||
''
;
if
(
!
Array
.
isArray
(
vm
.
annotation
.
tags
))
{
vm
.
annotation
.
tags
=
[];
}
// Automatically save new highlights to the server when they're created.
...
...
@@ -250,51 +157,26 @@ function AnnotationController(
// log in.
saveNewHighlight
();
updateView
(
domainModel
);
// If this annotation is not a highlight and if it's new (has just been
// created by the annotate button) or it has edits not yet saved to the
// server - then open the editor on AnnotationController instantiation.
if
(
!
newlyCreatedByHighlightButton
)
{
if
(
isNew
(
domainModel
)
||
drafts
.
get
(
domainModel
))
{
if
(
isNew
(
vm
.
annotation
)
||
drafts
.
get
(
vm
.
annotation
))
{
vm
.
edit
();
}
}
}
function
updateView
(
domainModel
)
{
updateViewModel
(
$scope
,
domainModel
,
vm
,
permissions
);
}
function
onAnnotationUpdated
(
event
,
updatedDomainModel
)
{
if
(
updatedDomainModel
.
id
===
domainModel
.
id
)
{
domainModel
=
updatedDomainModel
;
updateView
(
updatedDomainModel
);
}
}
function
deleteIfNewAndEmpty
()
{
if
(
isNew
(
domainModel
)
&&
!
vm
.
form
.
text
&&
vm
.
form
.
tags
.
length
===
0
)
{
if
(
isNew
(
vm
.
annotation
)
&&
!
vm
.
state
().
text
&&
vm
.
state
()
.
tags
.
length
===
0
)
{
vm
.
revert
();
}
}
function
onDestroy
()
{
// If the annotation component is destroyed whilst the annotation is being
// edited, persist temporary state so that we can restore it if the
// annotation editor is later recreated.
//
// The annotation component may be destroyed when switching accounts,
// when switching groups or when the component is scrolled off-screen.
if
(
vm
.
editing
())
{
saveToDrafts
(
drafts
,
domainModel
,
vm
);
}
}
function
onGroupFocused
()
{
// New annotations move to the new group, when a new group is focused.
if
(
isNew
(
domainModel
))
{
domainModel
.
group
=
groups
.
focused
().
id
;
if
(
isNew
(
vm
.
annotation
))
{
vm
.
annotation
.
group
=
groups
.
focused
().
id
;
}
}
...
...
@@ -308,7 +190,7 @@ function AnnotationController(
*
*/
function
saveNewHighlight
()
{
if
(
!
isNew
(
domainModel
))
{
if
(
!
isNew
(
vm
.
annotation
))
{
// Already saved.
return
;
}
...
...
@@ -318,29 +200,20 @@ function AnnotationController(
return
;
}
if
(
domainModel
.
user
)
{
if
(
vm
.
annotation
.
user
)
{
// User is logged in, save to server.
// Highlights are always private.
domainModel
.
permissions
=
permissions
.
private
();
save
(
domainModel
).
then
(
function
(
model
)
{
domainModel
=
model
;
vm
.
annotation
.
permissions
=
permissions
.
private
();
save
(
vm
.
annotation
).
then
(
function
(
model
)
{
model
.
$$tag
=
vm
.
annotation
.
$$tag
;
$rootScope
.
$emit
(
events
.
ANNOTATION_CREATED
,
model
);
updateView
(
domainModel
);
});
}
else
{
// User isn't logged in, save to drafts.
saveToDrafts
(
drafts
,
domainModel
,
vm
);
drafts
.
update
(
vm
.
annotation
,
vm
.
state
()
);
}
}
/** Switches the view to a viewer, closing the editor controls if they're
* open.
* @name annotation.AnnotationController#view
*/
function
view
()
{
vm
.
action
=
'view'
;
}
/**
* @ngdoc method
* @name annotation.AnnotationController#authorize
...
...
@@ -355,7 +228,7 @@ function AnnotationController(
// performance bottleneck and we would need to get the id token into the
// session, which we should probably do anyway (and move to opaque bearer
// tokens for the access token).
return
permissions
.
permits
(
action
,
domainModel
,
session
.
state
.
userid
);
return
permissions
.
permits
(
action
,
vm
.
annotation
,
session
.
state
.
userid
);
};
/**
...
...
@@ -372,7 +245,7 @@ function AnnotationController(
errorMessage
(
reason
),
'Deleting annotation failed'
);
};
$scope
.
$apply
(
function
()
{
annotationMapper
.
deleteAnnotation
(
domainModel
).
then
(
annotationMapper
.
deleteAnnotation
(
vm
.
annotation
).
then
(
null
,
onRejected
);
});
}
...
...
@@ -385,8 +258,9 @@ function AnnotationController(
* @description Switches the view to an editor.
*/
vm
.
edit
=
function
()
{
restoreFromDrafts
(
drafts
,
domainModel
,
vm
);
vm
.
action
=
isNew
(
domainModel
)
?
'create'
:
'edit'
;
if
(
!
drafts
.
get
(
vm
.
annotation
))
{
drafts
.
update
(
vm
.
annotation
,
vm
.
state
());
}
};
/**
...
...
@@ -396,11 +270,7 @@ function AnnotationController(
* (i.e. the annotation editor form should be open), `false` otherwise.
*/
vm
.
editing
=
function
()
{
if
(
vm
.
action
===
'create'
||
vm
.
action
===
'edit'
)
{
return
true
;
}
else
{
return
false
;
}
return
drafts
.
get
(
vm
.
annotation
)
&&
!
vm
.
isSaving
;
};
/**
...
...
@@ -409,7 +279,7 @@ function AnnotationController(
* @returns {Object} The full group object associated with the annotation.
*/
vm
.
group
=
function
()
{
return
groups
.
get
(
domainModel
.
group
);
return
groups
.
get
(
vm
.
annotation
.
group
);
};
/**
...
...
@@ -419,14 +289,14 @@ function AnnotationController(
* otherwise.
*/
vm
.
hasContent
=
function
()
{
return
vm
.
form
.
text
.
length
>
0
||
vm
.
form
.
tags
.
length
>
0
;
return
vm
.
state
().
text
.
length
>
0
||
vm
.
state
()
.
tags
.
length
>
0
;
};
/**
* @returns {boolean} True if this annotation has quotes
*/
vm
.
hasQuotes
=
function
()
{
return
domainModel
.
target
.
some
(
function
(
target
)
{
return
vm
.
annotation
.
target
.
some
(
function
(
target
)
{
return
target
.
selector
&&
target
.
selector
.
some
(
function
(
selector
)
{
return
selector
.
type
===
'TextQuoteSelector'
;
});
...
...
@@ -434,7 +304,7 @@ function AnnotationController(
};
vm
.
id
=
function
()
{
return
domainModel
.
id
;
return
vm
.
annotation
.
id
;
};
/**
...
...
@@ -445,16 +315,16 @@ function AnnotationController(
vm
.
isHighlight
=
function
()
{
if
(
newlyCreatedByHighlightButton
)
{
return
true
;
}
else
if
(
isNew
(
domainModel
))
{
}
else
if
(
isNew
(
vm
.
annotation
))
{
return
false
;
}
else
{
// Once an annotation has been saved to the server there's no longer a
// simple property that says whether it's a highlight or not. For
// example there's no
domainModel
.highlight: true. Instead a highlight is
// example there's no
vm.annotation
.highlight: true. Instead a highlight is
// defined as an annotation that isn't a page note or a reply and that
// has no text or tags.
var
isPageNote
=
(
domainModel
.
target
||
[]).
length
===
0
;
return
(
!
isPageNote
&&
!
isReply
(
domainModel
)
&&
!
vm
.
hasContent
());
var
isPageNote
=
(
vm
.
annotation
.
target
||
[]).
length
===
0
;
return
(
!
isPageNote
&&
!
isReply
(
vm
.
annotation
)
&&
!
vm
.
hasContent
());
}
};
...
...
@@ -465,7 +335,7 @@ function AnnotationController(
* current group or with everyone).
*/
vm
.
isShared
=
function
()
{
return
!
vm
.
isPrivate
;
return
!
vm
.
state
().
isPrivate
;
};
// Save on Meta + Enter or Ctrl + Enter.
...
...
@@ -488,15 +358,15 @@ function AnnotationController(
* Creates a new message in reply to this annotation.
*/
vm
.
reply
=
function
()
{
var
references
=
(
domainModel
.
references
||
[]).
concat
(
domainModel
.
id
);
var
references
=
(
vm
.
annotation
.
references
||
[]).
concat
(
vm
.
annotation
.
id
);
var
reply
=
annotationMapper
.
createAnnotation
({
references
:
references
,
uri
:
domainModel
.
uri
uri
:
vm
.
annotation
.
uri
});
reply
.
group
=
domainModel
.
group
;
reply
.
group
=
vm
.
annotation
.
group
;
if
(
session
.
state
.
userid
)
{
if
(
vm
.
isPrivate
)
{
if
(
vm
.
state
().
isPrivate
)
{
reply
.
permissions
=
permissions
.
private
();
}
else
{
reply
.
permissions
=
permissions
.
shared
(
reply
.
group
);
...
...
@@ -510,56 +380,38 @@ function AnnotationController(
* @description Reverts an edit in progress and returns to the viewer.
*/
vm
.
revert
=
function
()
{
drafts
.
remove
(
domainModel
);
if
(
vm
.
action
===
'create'
)
{
$rootScope
.
$emit
(
events
.
ANNOTATION_DELETED
,
domainModel
);
}
else
{
updateView
(
domainModel
);
view
();
drafts
.
remove
(
vm
.
annotation
);
if
(
isNew
(
vm
.
annotation
))
{
$rootScope
.
$emit
(
events
.
ANNOTATION_DELETED
,
vm
.
annotation
);
}
};
/**
* @ngdoc method
* @name annotation.AnnotationController#save
* @description Saves any edits and returns to the viewer.
*/
vm
.
save
=
function
()
{
if
(
!
domainModel
.
user
)
{
if
(
!
vm
.
annotation
.
user
)
{
flash
.
info
(
'Please sign in to save your annotations.'
);
return
Promise
.
resolve
();
}
if
((
vm
.
action
===
'create'
||
vm
.
action
===
'edit'
)
&&
!
vm
.
hasContent
()
&&
vm
.
isShared
())
{
if
(
!
vm
.
hasContent
()
&&
vm
.
isShared
())
{
flash
.
info
(
'Please add text or a tag before publishing.'
);
return
Promise
.
resolve
();
}
var
updatedModel
=
angular
.
copy
(
domainModel
);
// Copy across the non-enumerable local tag for the annotation
updatedModel
.
$$tag
=
domainModel
.
$$tag
;
var
updatedModel
=
updateModel
(
vm
.
annotation
,
vm
.
state
(),
permissions
);
updateDomainModel
(
updatedModel
,
vm
,
permissions
);
var
saved
=
save
(
updatedModel
).
then
(
function
(
model
)
{
var
isNew
=
!
domainModel
.
id
;
drafts
.
remove
(
domainModel
);
domainModel
=
model
;
if
(
isNew
)
{
$rootScope
.
$emit
(
events
.
ANNOTATION_CREATED
,
domainModel
);
}
else
{
$rootScope
.
$emit
(
events
.
ANNOTATION_UPDATED
,
domainModel
);
}
updateView
(
domainModel
);
});
// optimistically switch back to view mode and display the saving
// Optimistically switch back to view mode and display the saving
// indicator
vm
.
isSaving
=
true
;
view
();
return
saved
.
then
(
function
()
{
return
save
(
updatedModel
).
then
(
function
(
model
)
{
Object
.
assign
(
updatedModel
,
model
);
vm
.
isSaving
=
false
;
var
event
=
isNew
(
vm
.
annotation
)
?
events
.
ANNOTATION_CREATED
:
events
.
ANNOTATION_UPDATED
;
drafts
.
remove
(
vm
.
annotation
);
$rootScope
.
$emit
(
event
,
updatedModel
);
}).
catch
(
function
(
reason
)
{
vm
.
isSaving
=
false
;
vm
.
edit
();
...
...
@@ -584,10 +436,14 @@ function AnnotationController(
// creating or editing, we cache that and use the same privacy level the
// next time they create an annotation.
// But _don't_ cache it when they change the privacy level of a reply.
if
(
!
isReply
(
domainModel
))
{
if
(
!
isReply
(
vm
.
annotation
))
{
permissions
.
setDefault
(
privacy
);
}
vm
.
isPrivate
=
(
privacy
===
'private'
);
drafts
.
update
(
vm
.
annotation
,
{
tags
:
vm
.
state
().
tags
,
text
:
vm
.
state
().
text
,
isPrivate
:
privacy
===
'private'
});
};
vm
.
tagStreamURL
=
function
(
tag
)
{
...
...
@@ -595,23 +451,34 @@ function AnnotationController(
};
vm
.
target
=
function
()
{
return
domainModel
.
target
;
return
vm
.
annotation
.
target
;
};
vm
.
updated
=
function
()
{
return
domainModel
.
updated
;
return
vm
.
annotation
.
updated
;
};
vm
.
user
=
function
()
{
return
domainModel
.
user
;
return
vm
.
annotation
.
user
;
};
vm
.
username
=
function
()
{
return
persona
.
username
(
domainModel
.
user
);
return
persona
.
username
(
vm
.
annotation
.
user
);
};
vm
.
isReply
=
function
()
{
return
isReply
(
domainModel
);
return
isReply
(
vm
.
annotation
);
};
vm
.
links
=
function
()
{
if
(
vm
.
annotation
.
links
)
{
return
{
incontext
:
vm
.
annotation
.
links
.
incontext
||
vm
.
annotation
.
links
.
html
||
''
,
html
:
vm
.
annotation
.
links
.
html
};
}
else
{
return
{
incontext
:
''
,
html
:
''
};
}
};
/**
...
...
@@ -630,11 +497,37 @@ function AnnotationController(
};
vm
.
setText
=
function
(
text
)
{
vm
.
form
.
text
=
text
;
drafts
.
update
(
vm
.
annotation
,
{
isPrivate
:
vm
.
state
().
isPrivate
,
tags
:
vm
.
state
().
tags
,
text
:
text
,
});
};
vm
.
setTags
=
function
(
tags
)
{
vm
.
form
.
tags
=
tags
;
drafts
.
update
(
vm
.
annotation
,
{
isPrivate
:
vm
.
state
().
isPrivate
,
tags
:
tags
,
text
:
vm
.
state
().
text
,
});
};
vm
.
state
=
function
()
{
var
draft
=
drafts
.
get
(
vm
.
annotation
);
if
(
draft
)
{
return
draft
;
}
return
{
tags
:
vm
.
annotation
.
tags
,
text
:
vm
.
annotation
.
text
,
isPrivate
:
permissions
.
isPrivate
(
vm
.
annotation
.
permissions
,
vm
.
annotation
.
user
),
};
};
var
documentMeta
=
memoize
(
annotationMetadata
.
domainAndTitle
);
vm
.
documentMeta
=
function
()
{
return
documentMeta
(
vm
.
annotation
);
};
init
();
...
...
@@ -664,7 +557,7 @@ module.exports = {
// to be unit tested.
// FIXME: The code should be refactored to enable unit testing without having
// to do this.
update
DomainModel
:
updateDomain
Model
,
update
Model
:
update
Model
,
// These are meant to be the public API of this module.
directive
:
annotation
,
...
...
h/static/scripts/directive/test/annotation-test.js
View file @
41904b1b
...
...
@@ -11,6 +11,12 @@ var util = require('./util');
var
inject
=
angular
.
mock
.
inject
;
var
fakeDocumentMeta
=
{
domain
:
'docs.io'
,
titleLink
:
'http://docs.io/doc.html'
,
titleText
:
'Dummy title'
,
};
/**
* Returns the annotation directive with helpers stubbed out.
*/
...
...
@@ -19,10 +25,13 @@ function annotationDirective() {
var
annotation
=
proxyquire
(
'../annotation'
,
{
angular
:
testUtil
.
noCallThru
(
angular
),
'../filter/document-domain'
:
noop
,
'../filter/document-title'
:
noop
,
'../filter/persona'
:
{
username
:
noop
,
},
'../annotation-metadata'
:
{
domainAndTitle
:
function
(
annot
)
{
return
fakeDocumentMeta
;
},
}
});
...
...
@@ -30,8 +39,8 @@ function annotationDirective() {
}
describe
(
'annotation'
,
function
()
{
describe
(
'update
Domain
Model()'
,
function
()
{
var
update
DomainModel
=
require
(
'../annotation'
).
updateDomain
Model
;
describe
(
'updateModel()'
,
function
()
{
var
update
Model
=
require
(
'../annotation'
).
update
Model
;
function
fakePermissions
()
{
return
{
...
...
@@ -40,89 +49,30 @@ describe('annotation', function() {
};
}
function
fakeGroups
()
{
return
{
focused
:
function
()
{
return
{};},
};
}
it
(
'copies text from viewModel into domainModel'
,
function
()
{
var
domainModel
=
{};
var
viewModel
=
{
form
:
{
text
:
'bar'
,
tags
:
[]}};
updateDomainModel
(
domainModel
,
viewModel
,
fakePermissions
(),
fakeGroups
());
assert
.
equal
(
domainModel
.
text
,
viewModel
.
form
.
text
);
it
(
'copies tags and text into the new model'
,
function
()
{
var
changes
=
{
text
:
'bar'
,
tags
:
[
'foo'
,
'bar'
]};
var
newModel
=
updateModel
(
fixtures
.
defaultAnnotation
(),
changes
,
fakePermissions
());
assert
.
deepEqual
(
newModel
.
tags
,
changes
.
tags
);
assert
.
equal
(
newModel
.
text
,
changes
.
text
);
});
it
(
'overwrites text in domainModel'
,
function
()
{
var
domainModel
=
{
text
:
'foo'
};
var
viewModel
=
{
form
:
{
text
:
'bar'
,
tags
:
[]}};
updateDomainModel
(
domainModel
,
viewModel
,
fakePermissions
(),
fakeGroups
());
assert
.
equal
(
domainModel
.
text
,
viewModel
.
form
.
text
);
});
it
(
'doesn
\'
t touch other properties in domainModel'
,
function
()
{
var
domainModel
=
{
foo
:
'foo'
,
bar
:
'bar'
};
var
viewModel
=
{
form
:
{
foo
:
'FOO'
,
tags
:
[]}};
updateDomainModel
(
domainModel
,
viewModel
,
fakePermissions
(),
fakeGroups
());
assert
.
equal
(
domainModel
.
bar
,
'bar'
,
'updateDomainModel() should not touch properties of domainModel'
+
'that don
\'
t exist in viewModel'
);
});
it
(
'copies tag texts from viewModel into domainModel'
,
function
()
{
var
domainModel
=
{};
var
viewModel
=
{
form
:
{
tags
:
[
'foo'
,
'bar'
],
}
};
updateDomainModel
(
domainModel
,
viewModel
,
fakePermissions
(),
fakeGroups
());
assert
.
deepEqual
(
domainModel
.
tags
,
[
'foo'
,
'bar'
]);
});
it
(
'sets domainModel.permissions to private if vm.isPrivate'
,
function
()
{
var
domainModel
=
{};
var
viewModel
=
{
isPrivate
:
true
,
form
:
{
text
:
'foo'
,
},
};
it
(
'sets permissions to private if the draft is private'
,
function
()
{
var
changes
=
{
isPrivate
:
true
,
text
:
'bar'
,
tags
:
[
'foo'
,
'bar'
]};
var
annot
=
fixtures
.
defaultAnnotation
();
var
permissions
=
fakePermissions
();
permissions
.
private
=
sinon
.
stub
().
returns
(
'private permissions'
);
updateDomainModel
(
domainModel
,
viewModel
,
permissions
,
fakeGroups
());
assert
.
equal
(
domainModel
.
permissions
,
'private permissions'
);
var
newModel
=
updateModel
(
annot
,
changes
,
permissions
);
assert
.
equal
(
newModel
.
permissions
,
'private permissions'
);
});
it
(
'sets domainModel.permissions to shared if !vm.isPrivate'
,
function
()
{
var
domainModel
=
{};
var
viewModel
=
{
isPrivate
:
false
,
form
:
{
text
:
'foo'
,
},
};
it
(
'sets permissions to shared if the draft is shared'
,
function
()
{
var
changes
=
{
isPrivate
:
false
,
text
:
'bar'
,
tags
:
[
'foo'
,
'bar'
]};
var
annot
=
fixtures
.
defaultAnnotation
();
var
permissions
=
fakePermissions
();
permissions
.
shared
=
sinon
.
stub
().
returns
(
'shared permissions'
);
updateDomainModel
(
domainModel
,
viewModel
,
permissions
,
fakeGroups
());
assert
.
equal
(
domainModel
.
permissions
,
'shared permissions'
);
var
newModel
=
updateModel
(
annot
,
changes
,
permissions
);
assert
.
equal
(
newModel
.
permissions
,
'shared permissions'
);
});
});
...
...
@@ -160,8 +110,6 @@ describe('annotation', function() {
};
}
before
(
function
()
{
angular
.
module
(
'h'
,
[])
.
directive
(
'annotation'
,
annotationDirective
());
...
...
@@ -248,11 +196,11 @@ describe('annotation', function() {
$provide
.
value
(
'drafts'
,
fakeDrafts
);
$provide
.
value
(
'features'
,
fakeFeatures
);
$provide
.
value
(
'flash'
,
fakeFlash
);
$provide
.
value
(
'groups'
,
fakeGroups
);
$provide
.
value
(
'permissions'
,
fakePermissions
);
$provide
.
value
(
'session'
,
fakeSession
);
$provide
.
value
(
'settings'
,
fakeSettings
);
$provide
.
value
(
'store'
,
fakeStore
);
$provide
.
value
(
'groups'
,
fakeGroups
);
}));
beforeEach
(
...
...
@@ -380,12 +328,22 @@ describe('annotation', function() {
assert
.
notCalled
(
fakeStore
.
annotation
.
create
);
});
it
(
'
edits
new annotations on initialization'
,
function
()
{
it
(
'
creates drafts for
new annotations on initialization'
,
function
()
{
var
annotation
=
fixtures
.
newAnnotation
();
createDirective
(
annotation
);
assert
.
calledWith
(
fakeDrafts
.
update
,
annotation
,
{
isPrivate
:
false
,
tags
:
annotation
.
tags
,
text
:
annotation
.
text
,
});
});
it
(
'does not create drafts for new highlights on initialization'
,
function
()
{
var
annotation
=
fixtures
.
newHighlight
();
var
controller
=
createDirective
(
annotation
).
controller
;
assert
.
isTrue
(
controller
.
editing
());
assert
.
notOk
(
controller
.
editing
());
assert
.
notCalled
(
fakeDrafts
.
update
);
});
it
(
'edits annotations with drafts on initialization'
,
function
()
{
...
...
@@ -397,51 +355,29 @@ describe('annotation', function() {
assert
.
isTrue
(
controller
.
editing
());
});
it
(
'does not edit new highlights on initialization'
,
function
()
{
var
annotation
=
fixtures
.
newHighlight
();
var
controller
=
createDirective
(
annotation
).
controller
;
assert
.
isFalse
(
controller
.
editing
());
});
it
(
'edits highlights with drafts on initialization'
,
function
()
{
var
annotation
=
fixtures
.
oldHighlight
();
// You can edit a highlight, enter some text or tags, and save it (the
// highlight then becomes an annotation). You can also edit a highlight
// and then change focus to another group and back without saving the
// highlight, in which case the highlight will have draft edits.
// This highlight has draft edits.
fakeDrafts
.
get
.
returns
({
text
:
''
,
tags
:
[]});
var
controller
=
createDirective
(
annotation
).
controller
;
assert
.
isTrue
(
controller
.
editing
());
});
});
describe
(
'
.
editing()'
,
function
()
{
it
(
'returns
true if action is "create"'
,
function
()
{
describe
(
'
#
editing()'
,
function
()
{
it
(
'returns
false if the annotation does not have a draft'
,
function
()
{
var
controller
=
createDirective
().
controller
;
controller
.
action
=
'create'
;
assert
.
equal
(
controller
.
editing
(),
true
);
assert
.
notOk
(
controller
.
editing
());
});
it
(
'returns true if
action is "edit"'
,
function
()
{
it
(
'returns true if
the annotation has a draft'
,
function
()
{
var
controller
=
createDirective
().
controller
;
controller
.
action
=
'edit'
;
assert
.
equal
(
controller
.
editing
(),
true
);
fakeDrafts
.
get
.
returns
({
tags
:
[],
text
:
''
,
isPrivate
:
false
})
;
assert
.
isTrue
(
controller
.
editing
()
);
});
it
(
'returns false if
action is "view"'
,
function
()
{
it
(
'returns false if
the annotation has a draft but is being saved'
,
function
()
{
var
controller
=
createDirective
().
controller
;
controller
.
action
=
'view'
;
assert
.
equal
(
controller
.
editing
(),
false
);
fakeDrafts
.
get
.
returns
({
tags
:
[],
text
:
''
,
isPrivate
:
false
});
controller
.
isSaving
=
true
;
assert
.
isFalse
(
controller
.
editing
());
});
});
describe
(
'
.
isHighlight()'
,
function
()
{
describe
(
'
#
isHighlight()'
,
function
()
{
it
(
'returns true for new highlights'
,
function
()
{
var
annotation
=
fixtures
.
newHighlight
();
...
...
@@ -582,7 +518,7 @@ describe('annotation', function() {
'does not add the world readable principal if the parent is private'
,
function
()
{
var
controller
=
createDirective
(
annotation
).
controller
;
controller
.
isPrivate
=
true
;
fakePermissions
.
isPrivate
.
returns
(
true
)
;
var
reply
=
{};
fakeAnnotationMapper
.
createAnnotation
.
returns
(
reply
);
controller
.
reply
();
...
...
@@ -606,78 +542,49 @@ describe('annotation', function() {
describe
(
'#setPrivacy'
,
function
()
{
it
(
'makes the annotation private when level is "private"'
,
function
()
{
var
parts
=
createDirective
();
// Make this annotation shared.
parts
.
controller
.
isPrivate
=
false
;
fakePermissions
.
isPrivate
.
returns
(
false
);
// Edit the annotation and make it private.
parts
.
controller
.
edit
();
parts
.
controller
.
setPrivacy
(
'private'
);
fakePermissions
.
isPrivate
.
returns
(
true
);
return
parts
.
controller
.
save
().
then
(
function
()
{
// Verify that the permissions are updated once the annotation
// is saved.
assert
.
equal
(
parts
.
controller
.
isPrivate
,
true
);
});
assert
.
calledWith
(
fakeDrafts
.
update
,
parts
.
controller
.
annotation
,
sinon
.
match
({
isPrivate
:
true
,
}));
});
it
(
'makes the annotation shared when level is "shared"'
,
function
()
{
var
parts
=
createDirective
();
parts
.
controller
.
isPrivate
=
true
;
parts
.
controller
.
edit
();
parts
.
controller
.
form
.
text
=
'test'
;
parts
.
controller
.
setPrivacy
(
'shared'
);
return
parts
.
controller
.
save
().
then
(
function
()
{
assert
.
equal
(
parts
.
controller
.
isPrivate
,
false
);
});
assert
.
calledWith
(
fakeDrafts
.
update
,
parts
.
controller
.
annotation
,
sinon
.
match
(
{
isPrivate
:
false
,
})
)
;
});
it
(
's
aves the "shared" visibility level to localStorage
'
,
function
()
{
it
(
's
ets the default visibility level if "shared"
'
,
function
()
{
var
parts
=
createDirective
();
parts
.
controller
.
edit
();
parts
.
controller
.
setPrivacy
(
'shared'
);
parts
.
controller
.
form
.
text
=
'test'
;
return
parts
.
controller
.
save
().
then
(
function
()
{
assert
.
calledWith
(
fakePermissions
.
setDefault
,
'shared'
);
});
assert
.
calledWith
(
fakePermissions
.
setDefault
,
'shared'
);
});
it
(
's
aves the "private" visibility level to localStorage
'
,
function
()
{
it
(
's
ets the default visibility if "private"
'
,
function
()
{
var
parts
=
createDirective
();
parts
.
controller
.
edit
();
parts
.
controller
.
setPrivacy
(
'private'
);
return
parts
.
controller
.
save
().
then
(
function
()
{
assert
.
calledWith
(
fakePermissions
.
setDefault
,
'private'
);
});
assert
.
calledWith
(
fakePermissions
.
setDefault
,
'private'
);
});
it
(
'doesn
\'
t save the visibility if the annotation is a reply'
,
function
()
{
var
parts
=
createDirective
();
parts
.
annotation
.
references
=
[
'parent id'
];
parts
.
controller
.
edit
();
var
parts
=
createDirective
(
fixtures
.
oldReply
());
parts
.
controller
.
setPrivacy
(
'private'
);
return
parts
.
controller
.
save
().
then
(
function
()
{
assert
.
notCalled
(
fakePermissions
.
setDefault
);
});
assert
.
notCalled
(
fakePermissions
.
setDefault
);
});
});
describe
(
'#hasContent'
,
function
()
{
it
(
'returns false if the annotation has no tags or text'
,
function
()
{
var
controller
=
createDirective
().
controller
;
controller
.
form
.
text
=
''
;
controller
.
form
.
tags
=
[];
var
controller
=
createDirective
(
fixtures
.
oldHighlight
()).
controller
;
assert
.
ok
(
!
controller
.
hasContent
());
});
it
(
'returns true if the annotation has tags or text'
,
function
()
{
var
controller
=
createDirective
().
controller
;
controller
.
form
.
text
=
'bar'
;
assert
.
ok
(
controller
.
hasContent
());
controller
.
form
.
text
=
''
;
controller
.
form
.
tags
=
[
'foo'
];
var
controller
=
createDirective
(
fixtures
.
oldAnnotation
()).
controller
;
assert
.
ok
(
controller
.
hasContent
());
});
});
...
...
@@ -786,6 +693,14 @@ describe('annotation', function() {
});
});
describe
(
'#documentMeta()'
,
function
()
{
it
(
'returns the domain, title link and text for the annotation'
,
function
()
{
var
annot
=
fixtures
.
defaultAnnotation
();
var
controller
=
createDirective
(
annot
).
controller
;
assert
.
deepEqual
(
controller
.
documentMeta
(),
fakeDocumentMeta
);
});
});
describe
(
'saving a new annotation'
,
function
()
{
var
annotation
;
...
...
@@ -793,26 +708,27 @@ describe('annotation', function() {
annotation
=
fixtures
.
newAnnotation
();
});
function
controllerWithActionCreate
()
{
var
controller
=
createDirective
(
annotation
).
controller
;
controller
.
action
=
'create'
;
controller
.
form
.
text
=
'new annotation'
;
return
controller
;
function
createController
()
{
return
createDirective
(
annotation
).
controller
;
}
it
(
'emits annotationCreated when saving an annotation succeeds'
,
function
()
{
var
controller
=
controllerWithActionCreate
();
sandbox
.
spy
(
$rootScope
,
'$emit'
);
return
controller
.
save
().
then
(
function
()
{
assert
.
calledWith
(
$rootScope
.
$emit
,
events
.
ANNOTATION_CREATED
);
});
}
);
it
(
'removes the draft when saving an annotation succeeds'
,
function
()
{
var
controller
=
createController
();
return
controller
.
save
().
then
(
function
()
{
assert
.
calledWith
(
fakeDrafts
.
remove
,
annotation
);
});
});
it
(
'emits annotationCreated when saving an annotation succeeds'
,
function
()
{
var
controller
=
createController
();
sandbox
.
spy
(
$rootScope
,
'$emit'
);
return
controller
.
save
().
then
(
function
()
{
assert
.
calledWith
(
$rootScope
.
$emit
,
events
.
ANNOTATION_CREATED
);
});
});
it
(
'flashes a generic error if the server can
\'
t be reached'
,
function
()
{
var
controller
=
c
ontrollerWithActionCreate
();
var
controller
=
c
reateController
();
fakeStore
.
annotation
.
create
=
sinon
.
stub
().
returns
(
Promise
.
reject
({
status
:
0
}));
...
...
@@ -823,7 +739,7 @@ describe('annotation', function() {
});
it
(
'flashes an error if saving the annotation fails on the server'
,
function
()
{
var
controller
=
c
ontrollerWithActionCreate
();
var
controller
=
c
reateController
();
fakeStore
.
annotation
.
create
=
sinon
.
stub
().
returns
(
Promise
.
reject
({
status
:
500
,
statusText
:
'Server Error'
,
...
...
@@ -836,59 +752,41 @@ describe('annotation', function() {
});
it
(
'doesn
\'
t flash an error when saving an annotation succeeds'
,
function
()
{
var
controller
=
controllerWithActionCreate
();
controller
.
save
();
assert
.
notCalled
(
fakeFlash
.
error
);
var
controller
=
createController
();
return
controller
.
save
().
then
(
function
()
{
assert
.
notCalled
(
fakeFlash
.
error
);
});
});
it
(
'shows a saving indicator when saving an annotation'
,
function
()
{
var
controller
=
c
ontrollerWithActionCreate
();
var
controller
=
c
reateController
();
var
create
;
fakeStore
.
annotation
.
create
=
sinon
.
stub
().
returns
(
new
Promise
(
function
(
resolve
)
{
create
=
resolve
;
}));
var
saved
=
controller
.
save
();
assert
.
equal
(
controller
.
isSaving
,
true
);
assert
.
equal
(
controller
.
action
,
'view'
);
create
();
create
(
Object
.
assign
({},
controller
.
annotation
,
{
id
:
'new-id'
}));
return
saved
.
then
(
function
()
{
assert
.
equal
(
controller
.
isSaving
,
false
);
});
});
it
(
'reverts to edit mode if saving fails'
,
function
()
{
var
controller
=
controllerWithActionCreate
();
var
failCreation
;
fakeStore
.
annotation
.
create
=
sinon
.
stub
().
returns
(
new
Promise
(
function
(
resolve
,
reject
)
{
failCreation
=
reject
;
}));
var
saved
=
controller
.
save
();
assert
.
equal
(
controller
.
isSaving
,
true
);
failCreation
({
status
:
-
1
});
return
saved
.
then
(
function
()
{
assert
.
equal
(
controller
.
isSaving
,
false
);
assert
.
ok
(
controller
.
editing
());
it
(
'does not remove the draft if saving fails'
,
function
()
{
var
controller
=
createController
();
fakeStore
.
annotation
.
create
=
sinon
.
stub
().
returns
(
Promise
.
reject
({
status
:
-
1
}));
return
controller
.
save
().
then
(
function
()
{
assert
.
notCalled
(
fakeDrafts
.
remove
);
});
});
it
(
'Passes group:<id> to the server when saving a new annotation'
,
function
()
{
fakeGroups
.
focused
=
function
()
{
return
{
id
:
'test-id'
};
};
var
annotation
=
{
user
:
'acct:fred@hypothes.is'
,
text
:
'foo'
,
};
var
controller
=
createDirective
(
annotation
).
controller
;
controller
.
action
=
'create'
;
return
controller
.
save
().
then
(
function
()
{
assert
.
calledWith
(
fakeStore
.
annotation
.
create
,
sinon
.
match
({}),
sinon
.
match
({
group
:
'test-id'
}));
});
}
);
it
(
'sets the annotation
\'
s group to the focused group'
,
function
()
{
fakeGroups
.
focused
=
function
()
{
return
{
id
:
'test-id'
};
};
var
controller
=
createDirective
(
fixtures
.
newAnnotation
()).
controller
;
assert
.
equal
(
controller
.
annotation
.
group
,
'test-id'
);
});
});
describe
(
'saving an edited an annotation'
,
function
()
{
...
...
@@ -896,54 +794,43 @@ describe('annotation', function() {
beforeEach
(
function
()
{
annotation
=
fixtures
.
defaultAnnotation
();
fakeDrafts
.
get
.
returns
({
text
:
'unsaved change'
});
});
function
controllerWithActionEdit
()
{
var
controller
=
createDirective
(
annotation
).
controller
;
controller
.
action
=
'edit'
;
controller
.
form
.
text
=
'updated text'
;
return
controller
;
function
createController
()
{
return
createDirective
(
annotation
).
controller
;
}
it
(
'flashes a generic error if the server cannot be reached'
,
function
()
{
var
controller
=
controllerWithActionEdit
();
fakeStore
.
annotation
.
update
=
sinon
.
stub
().
returns
(
Promise
.
reject
({
status
:
-
1
}));
return
controller
.
save
().
then
(
function
()
{
assert
.
calledWith
(
fakeFlash
.
error
,
'Service unreachable.'
,
'Saving annotation failed'
);
});
}
);
it
(
'flashes a generic error if the server cannot be reached'
,
function
()
{
var
controller
=
createController
();
fakeStore
.
annotation
.
update
=
sinon
.
stub
().
returns
(
Promise
.
reject
({
status
:
-
1
}));
return
controller
.
save
().
then
(
function
()
{
assert
.
calledWith
(
fakeFlash
.
error
,
'Service unreachable.'
,
'Saving annotation failed'
);
});
});
it
(
'flashes an error if saving the annotation fails on the server'
,
function
()
{
var
controller
=
controllerWithActionEdit
();
fakeStore
.
annotation
.
update
=
sinon
.
stub
().
returns
(
Promise
.
reject
({
status
:
500
,
statusText
:
'Server Error'
,
data
:
{}
}));
return
controller
.
save
().
then
(
function
()
{
assert
.
calledWith
(
fakeFlash
.
error
,
'500 Server Error'
,
'Saving annotation failed'
);
});
}
);
it
(
'flashes an error if saving the annotation fails on the server'
,
function
()
{
var
controller
=
createController
();
fakeStore
.
annotation
.
update
=
sinon
.
stub
().
returns
(
Promise
.
reject
({
status
:
500
,
statusText
:
'Server Error'
,
data
:
{}
}));
return
controller
.
save
().
then
(
function
()
{
assert
.
calledWith
(
fakeFlash
.
error
,
'500 Server Error'
,
'Saving annotation failed'
);
});
});
it
(
'doesn
\'
t flash an error if saving the annotation succeeds'
,
function
()
{
var
controller
=
controllerWithActionEdit
();
controller
.
form
.
text
=
'updated text'
;
controller
.
save
();
it
(
'doesn
\'
t flash an error if saving the annotation succeeds'
,
function
()
{
var
controller
=
createController
();
return
controller
.
save
().
then
(
function
()
{
assert
.
notCalled
(
fakeFlash
.
error
);
}
);
}
);
}
);
});
describe
(
'drafts'
,
function
()
{
...
...
@@ -962,8 +849,8 @@ describe('annotation', function() {
text
:
'unsaved-text'
});
var
controller
=
createDirective
().
controller
;
assert
.
deepEqual
(
controller
.
form
.
tags
,
[
'unsaved-tag'
]);
assert
.
equal
(
controller
.
form
.
text
,
'unsaved-text'
);
assert
.
deepEqual
(
controller
.
state
()
.
tags
,
[
'unsaved-tag'
]);
assert
.
equal
(
controller
.
state
()
.
text
,
'unsaved-text'
);
});
it
(
'removes the draft when changes are discarded'
,
function
()
{
...
...
@@ -976,41 +863,13 @@ describe('annotation', function() {
it
(
'removes the draft when changes are saved'
,
function
()
{
var
annotation
=
fixtures
.
defaultAnnotation
();
var
controller
=
createDirective
(
annotation
).
controller
;
controller
.
edit
();
controller
.
form
.
text
=
'test annotation'
;
fakeDrafts
.
get
.
returns
({
text
:
'unsaved changes'
});
return
controller
.
save
().
then
(
function
()
{
assert
.
calledWith
(
fakeDrafts
.
remove
,
annotation
);
});
});
});
describe
(
'onAnnotationUpdated()'
,
function
()
{
it
(
'updates vm.form.text'
,
function
()
{
var
parts
=
createDirective
();
var
updatedModel
=
{
id
:
parts
.
annotation
.
id
,
text
:
'new text'
,
};
$rootScope
.
$emit
(
events
.
ANNOTATION_UPDATED
,
updatedModel
);
assert
.
equal
(
parts
.
controller
.
form
.
text
,
'new text'
);
});
it
(
'doesn
\'
t update if a different annotation was updated'
,
function
()
{
var
parts
=
createDirective
();
parts
.
controller
.
form
.
text
=
'original text'
;
var
updatedModel
=
{
id
:
'different annotation id'
,
text
:
'new text'
,
};
$rootScope
.
$emit
(
events
.
ANNOTATION_UPDATED
,
updatedModel
);
assert
.
equal
(
parts
.
controller
.
form
.
text
,
'original text'
);
});
});
describe
(
'when another new annotation is created'
,
function
()
{
it
(
'removes the current annotation if empty'
,
function
()
{
var
annotation
=
fixtures
.
newEmptyAnnotation
();
...
...
@@ -1021,65 +880,43 @@ describe('annotation', function() {
});
it
(
'does not remove the current annotation if is is not new'
,
function
()
{
var
parts
=
createDirective
(
fixtures
.
defaultAnnotation
());
parts
.
controller
.
form
.
text
=
''
;
parts
.
controller
.
form
.
tags
=
[];
createDirective
(
fixtures
.
defaultAnnotation
());
fakeDrafts
.
get
.
returns
({
text
:
''
,
tags
:
[]});
$rootScope
.
$emit
(
events
.
BEFORE_ANNOTATION_CREATED
,
fixtures
.
newAnnotation
());
assert
.
notCalled
(
fakeDrafts
.
remove
);
});
it
(
'does not remove the current annotation if
it has text
'
,
function
()
{
var
annotation
=
fixtures
.
newAnnotation
();
it
(
'does not remove the current annotation if
the scope was destroyed
'
,
function
()
{
var
annotation
=
fixtures
.
new
Empty
Annotation
();
var
parts
=
createDirective
(
annotation
);
parts
.
controller
.
form
.
text
=
'An incomplete thought'
;
parts
.
scope
.
$destroy
()
;
$rootScope
.
$emit
(
events
.
BEFORE_ANNOTATION_CREATED
,
fixtures
.
newAnnotation
());
assert
.
notCalled
(
fakeDrafts
.
remove
);
});
it
(
'does not remove the current annotation if it has t
ags
'
,
function
()
{
it
(
'does not remove the current annotation if it has t
ext
'
,
function
()
{
var
annotation
=
fixtures
.
newAnnotation
();
var
parts
=
createDirective
(
annotation
);
parts
.
controller
.
form
.
tags
=
[
'a-tag'
]
;
createDirective
(
annotation
);
fakeDrafts
.
get
.
returns
({
text
:
'An incomplete thought'
})
;
$rootScope
.
$emit
(
events
.
BEFORE_ANNOTATION_CREATED
,
fixtures
.
newAnnotation
());
assert
.
notCalled
(
fakeDrafts
.
remove
);
});
});
describe
(
'when component is destroyed'
,
function
()
{
it
(
'if the annotation is being edited it updates drafts'
,
function
()
{
var
parts
=
createDirective
();
parts
.
controller
.
isPrivate
=
true
;
parts
.
controller
.
edit
();
parts
.
controller
.
form
.
text
=
'unsaved-text'
;
parts
.
controller
.
form
.
tags
=
[];
fakeDrafts
.
get
=
sinon
.
stub
().
returns
({
text
:
'old-draft'
});
fakeDrafts
.
update
=
sinon
.
stub
();
parts
.
scope
.
$broadcast
(
'$destroy'
);
assert
.
calledWith
(
fakeDrafts
.
update
,
parts
.
annotation
,
{
isPrivate
:
true
,
tags
:[],
text
:
'unsaved-text'
});
it
(
'does not remove the current annotation if it has tags'
,
function
()
{
var
annotation
=
fixtures
.
newAnnotation
();
createDirective
(
annotation
);
fakeDrafts
.
get
.
returns
({
tags
:
[
'a-tag'
]});
$rootScope
.
$emit
(
events
.
BEFORE_ANNOTATION_CREATED
,
fixtures
.
newAnnotation
());
assert
.
notCalled
(
fakeDrafts
.
remove
);
});
it
(
'if the annotation isn
\'
t being edited it doesn
\'
t update drafts'
,
function
()
{
var
parts
=
createDirective
();
parts
.
controller
.
isPrivate
=
true
;
fakeDrafts
.
update
=
sinon
.
stub
();
parts
.
scope
.
$broadcast
(
'$destroy'
);
assert
.
notCalled
(
fakeDrafts
.
update
);
});
});
describe
(
'
onGroupFocused()
'
,
function
()
{
it
(
'
updates domainModel.group if the annotation is new
'
,
function
()
{
describe
(
'
when the focused group changes
'
,
function
()
{
it
(
'
moves new annotations to the focused group
'
,
function
()
{
var
annotation
=
fixtures
.
newAnnotation
();
annotation
.
group
=
'old-group-id'
;
createDirective
(
annotation
);
...
...
@@ -1090,7 +927,7 @@ describe('annotation', function() {
assert
.
equal
(
annotation
.
group
,
'new-group-id'
);
});
it
(
'does not
update domainModel.group if the annotation is not new
'
,
it
(
'does not
modify the group of saved annotations
'
,
function
()
{
var
annotation
=
fixtures
.
oldAnnotation
();
annotation
.
group
=
'old-group-id'
;
...
...
@@ -1104,80 +941,24 @@ describe('annotation', function() {
);
});
describe
(
'reverting edits'
,
function
()
{
// Simulate what happens when the user edits an annotation,
// clicks Save, gets an error because the server fails to save the
// annotation, then clicks Cancel - in the frontend the annotation should
// be restored to its original value, the edits lost.
it
(
'restores the original text'
,
function
()
{
var
controller
=
createDirective
({
id
:
'test-annotation-id'
,
user
:
'acct:bill@localhost'
,
text
:
'Initial annotation body text'
,
}).
controller
;
fakeStore
.
annotation
.
update
=
function
()
{
return
Promise
.
reject
({
status
:
500
,
statusText
:
'Server Error'
,
data
:
{}
});
};
var
originalText
=
controller
.
form
.
text
;
// Simulate the user clicking the Edit button on the annotation.
it
(
'removes the current draft'
,
function
()
{
var
controller
=
createDirective
(
fixtures
.
defaultAnnotation
()).
controller
;
controller
.
edit
();
// Simulate the user typing some text into the annotation editor textarea.
controller
.
form
.
text
=
'changed by test code'
;
// Simulate the user hitting the Save button and wait for the
// (unsuccessful) response from the server.
controller
.
save
();
// At this point the annotation editor controls are still open, and the
// annotation's text is still the modified (unsaved) text.
assert
.
equal
(
controller
.
form
.
text
,
'changed by test code'
);
// Simulate the user clicking the Cancel button.
controller
.
revert
();
assert
.
equal
(
controller
.
form
.
text
,
originalText
);
assert
.
calledWith
(
fakeDrafts
.
remove
,
controller
.
annotation
);
});
// Test that editing reverting changes to an annotation with
// no text resets the text to be empty.
it
(
'clears the text if the text was originally empty'
,
function
()
{
var
controller
=
createDirective
({
id
:
'test-annotation-id'
,
user
:
'acct:bill@localhost'
,
}).
controller
;
controller
.
edit
();
assert
.
equal
(
controller
.
action
,
'edit'
);
controller
.
form
.
text
=
'this should be reverted'
;
it
(
'deletes the annotation if it was new'
,
function
()
{
var
controller
=
createDirective
(
fixtures
.
newAnnotation
()).
controller
;
sandbox
.
spy
(
$rootScope
,
'$emit'
);
controller
.
revert
();
assert
.
equal
(
controller
.
form
.
text
,
''
);
});
it
(
'reverts to the most recently saved version'
,
function
()
{
fakeStore
.
annotation
.
update
=
function
(
params
,
ann
)
{
return
Promise
.
resolve
(
Object
.
assign
({},
ann
));
};
var
controller
=
createDirective
({
id
:
'new-annot'
,
user
:
'acct:bill@localhost'
,
}).
controller
;
controller
.
edit
();
controller
.
form
.
text
=
'New annotation text'
;
return
controller
.
save
().
then
(
function
()
{
controller
.
edit
();
controller
.
form
.
text
=
'Updated annotation text'
;
return
controller
.
save
();
}).
then
(
function
()
{
controller
.
edit
();
controller
.
revert
();
assert
.
equal
(
controller
.
form
.
text
,
'Updated annotation text'
);
});
assert
.
calledWith
(
$rootScope
.
$emit
,
events
.
ANNOTATION_DELETED
);
});
});
describe
(
'tag display'
,
function
()
{
it
(
'displays
annotation tags
'
,
function
()
{
it
(
'displays
links to tags on the stream
'
,
function
()
{
var
directive
=
createDirective
({
id
:
'1234'
,
tags
:
[
'atag'
]
...
...
@@ -1193,7 +974,7 @@ describe('annotation', function() {
});
describe
(
'annotation links'
,
function
()
{
it
(
'
linkInContext
uses the in-context links when available'
,
function
()
{
it
(
'uses the in-context links when available'
,
function
()
{
var
annotation
=
Object
.
assign
({},
fixtures
.
defaultAnnotation
(),
{
links
:
{
html
:
'https://test.hypothes.is/a/deadbeef'
,
...
...
@@ -1201,22 +982,20 @@ describe('annotation', function() {
},
});
var
controller
=
createDirective
(
annotation
).
controller
;
assert
.
equal
(
controller
.
linkInContext
,
annotation
.
links
.
incontext
);
assert
.
equal
(
controller
.
links
().
incontext
,
annotation
.
links
.
incontext
);
});
it
(
'
linkInContext
falls back to the HTML link when in-context links are missing'
,
function
()
{
it
(
'falls back to the HTML link when in-context links are missing'
,
function
()
{
var
annotation
=
Object
.
assign
({},
fixtures
.
defaultAnnotation
(),
{
links
:
{
html
:
'https://test.hypothes.is/a/deadbeef'
,
},
});
var
controller
=
createDirective
(
annotation
).
controller
;
assert
.
equal
(
controller
.
linkInContext
,
annotation
.
links
.
html
);
assert
.
equal
(
controller
.
links
().
html
,
annotation
.
links
.
html
);
});
it
(
'
linkHTML
uses the HTML link when available'
,
function
()
{
it
(
'uses the HTML link when available'
,
function
()
{
var
annotation
=
Object
.
assign
({},
fixtures
.
defaultAnnotation
(),
{
links
:
{
html
:
'https://test.hypothes.is/a/deadbeef'
,
...
...
@@ -1224,22 +1003,19 @@ describe('annotation', function() {
},
});
var
controller
=
createDirective
(
annotation
).
controller
;
assert
.
equal
(
controller
.
linkHTML
,
annotation
.
links
.
html
);
assert
.
equal
(
controller
.
links
().
html
,
annotation
.
links
.
html
);
});
it
(
'
linkInContext
is blank when unknown'
,
function
()
{
it
(
'
in-context link
is blank when unknown'
,
function
()
{
var
annotation
=
fixtures
.
defaultAnnotation
();
var
controller
=
createDirective
(
annotation
).
controller
;
assert
.
equal
(
controller
.
linkInContext
,
''
);
assert
.
equal
(
controller
.
links
().
incontext
,
''
);
});
it
(
'
link
HTML is blank when unknown'
,
function
()
{
it
(
'HTML is blank when unknown'
,
function
()
{
var
annotation
=
fixtures
.
defaultAnnotation
();
var
controller
=
createDirective
(
annotation
).
controller
;
assert
.
equal
(
controller
.
linkHTML
,
''
);
assert
.
equal
(
controller
.
links
().
html
,
''
);
});
});
});
...
...
h/templates/client/annotation.html
View file @
41904b1b
...
...
@@ -22,20 +22,20 @@
target=
"_blank"
ng-if=
"vm.group() && vm.group().url"
href=
"{{vm.group().url}}"
>
<i
class=
"h-icon-group"
></i><span
class=
"annotation-header__group-name"
>
{{vm.group().name}}
</span>
</a>
<span
ng-show=
"vm.isPrivate"
<span
ng-show=
"vm.
state().
isPrivate"
title=
"This annotation is visible only to you."
>
<i
class=
"h-icon-lock"
></i><span
class=
"annotation-header__group-name"
ng-show=
"!vm.group().url"
>
Only me
</span>
</span>
<i
class=
"h-icon-border-color"
ng-show=
"vm.isHighlight() && !vm.editing()"
title=
"This is a highlight. Click 'edit' to add a note or tag."
></i>
<span
ng-if=
"::vm.showDocumentInfo"
>
<span
class=
"annotation-citation"
ng-if=
"vm.documentMeta.titleLink"
>
on "
<a
ng-href=
"{{vm.documentMeta
.titleLink}}"
>
{{vm.documentMeta
.titleText}}
</a>
"
<span
class=
"annotation-citation"
ng-if=
"vm.documentMeta
()
.titleLink"
>
on "
<a
ng-href=
"{{vm.documentMeta
().titleLink}}"
>
{{vm.documentMeta()
.titleText}}
</a>
"
</span>
<span
class=
"annotation-citation"
ng-if=
"!vm.documentMeta.titleLink"
>
on "{{vm.documentMeta.titleText}}"
<span
class=
"annotation-citation"
ng-if=
"!vm.documentMeta
()
.titleLink"
>
on "{{vm.documentMeta
()
.titleText}}"
</span>
<span
class=
"annotation-citation-domain"
ng-if=
"vm.documentMeta
.domain"
>
({{vm.documentMeta
.domain}})
</span>
ng-if=
"vm.documentMeta
().domain"
>
({{vm.documentMeta()
.domain}})
</span>
</span>
</span>
</span>
...
...
@@ -45,7 +45,7 @@
<timestamp
class-name=
"'annotation-header__timestamp'"
timestamp=
"vm.updated()"
href=
"vm.link
HTML
"
href=
"vm.link
s().html
"
ng-if=
"!vm.editing() && vm.updated()"
></timestamp>
</header>
...
...
@@ -75,8 +75,8 @@
collapse=
"vm.collapseBody"
collapsed-height=
"400"
overflow-hysteresis=
"20"
content-data=
"vm.
form
.text"
>
<markdown
text=
"vm.
form
.text"
content-data=
"vm.
state()
.text"
>
<markdown
text=
"vm.
state()
.text"
on-edit-text=
"vm.setText(text)"
read-only=
"!vm.editing()"
>
</markdown>
...
...
@@ -86,14 +86,14 @@
<!-- Tags -->
<div
class=
"annotation-body form-field"
ng-if=
"vm.editing()"
>
<tag-editor
tags=
"vm.
form
.tags"
<tag-editor
tags=
"vm.
state()
.tags"
on-edit-tags=
"vm.setTags(tags)"
></tag-editor>
</div>
<div
class=
"annotation-body u-layout-row tags tags-read-only"
ng-if=
"(vm.canCollapseBody || vm.
form
.tags.length) && !vm.editing()"
>
ng-if=
"(vm.canCollapseBody || vm.
state()
.tags.length) && !vm.editing()"
>
<ul
class=
"tag-list"
>
<li
class=
"tag-item"
ng-repeat=
"tag in vm.
form
.tags"
>
<li
class=
"tag-item"
ng-repeat=
"tag in vm.
state()
.tags"
>
<a
ng-href=
"{{vm.tagStreamURL(tag)}}"
target=
"_blank"
>
{{tag}}
</a>
</li>
</ul>
...
...
@@ -170,8 +170,8 @@
</button>
<annotation-share-dialog
group=
"vm.group()"
uri=
"vm.link
InC
ontext"
is-private=
"vm.isPrivate"
uri=
"vm.link
s().inc
ontext"
is-private=
"vm.
state().
isPrivate"
is-open=
"vm.showShareDialog"
on-close=
"vm.showShareDialog = false"
>
</annotation-share-dialog>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment