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
644e5daa
Commit
644e5daa
authored
Jan 26, 2016
by
Nick Stenning
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #2887 from hypothesis/optimistic-save
Prevent double-posting of new annotations
parents
e1009e6b
f407141a
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
88 additions
and
171 deletions
+88
-171
annotation.js
h/static/scripts/directive/annotation.js
+38
-69
annotation-test.js
h/static/scripts/directive/test/annotation-test.js
+43
-100
annotation.html
h/templates/client/annotation.html
+7
-2
No files found.
h/static/scripts/directive/annotation.js
View file @
644e5daa
...
@@ -27,11 +27,11 @@ function domainModelTagsFromViewModelTags(viewModelTags) {
...
@@ -27,11 +27,11 @@ function domainModelTagsFromViewModelTags(viewModelTags) {
*/
*/
function
errorMessage
(
reason
)
{
function
errorMessage
(
reason
)
{
var
message
;
var
message
;
if
(
reason
.
status
==
=
0
)
{
if
(
reason
.
status
<
=
0
)
{
message
=
'Service unreachable.'
;
message
=
'Service unreachable.'
;
}
else
{
}
else
{
message
=
reason
.
status
+
' '
+
reason
.
statusText
;
message
=
reason
.
status
+
' '
+
reason
.
statusText
;
if
(
reason
.
data
.
reason
)
{
if
(
reason
.
data
&&
reason
.
data
.
reason
)
{
message
=
message
+
': '
+
reason
.
data
.
reason
;
message
=
message
+
': '
+
reason
.
data
.
reason
;
}
}
}
}
...
@@ -193,43 +193,6 @@ function updateViewModel($scope, time, domainModel, vm, permissions) {
...
@@ -193,43 +193,6 @@ function updateViewModel($scope, time, domainModel, vm, permissions) {
}
}
}
}
/** Return truthy if the given annotation is valid, falsy otherwise.
*
* A public annotation has to have some text and/or some tags to be valid,
* public annotations with no text or tags (i.e. public highlights) are not
* valid.
*
* Non-public annotations just need to have a target to be valid, non-public
* highlights are valid.
*
* @param {object} annotation The annotation to be validated
*
*/
function
validate
(
domainModel
)
{
if
(
!
angular
.
isObject
(
domainModel
))
{
return
;
}
var
permissions
=
domainModel
.
permissions
||
{};
var
readPermissions
=
permissions
.
read
||
[];
var
targets
=
domainModel
.
target
||
[];
if
(
domainModel
.
tags
&&
domainModel
.
tags
.
length
)
{
return
domainModel
.
tags
.
length
;
}
if
(
domainModel
.
text
&&
domainModel
.
text
.
length
)
{
return
domainModel
.
text
.
length
;
}
var
worldReadable
=
false
;
if
(
readPermissions
.
indexOf
(
'group:__world__'
)
!==
-
1
)
{
worldReadable
=
true
;
}
return
(
targets
.
length
&&
!
worldReadable
);
}
/** Return a vm tags array from the given domainModel tags array.
/** Return a vm tags array from the given domainModel tags array.
*
*
* domainModel.tags and vm.form.tags use different formats. This
* domainModel.tags and vm.form.tags use different formats. This
...
@@ -318,6 +281,9 @@ function AnnotationController(
...
@@ -318,6 +281,9 @@ function AnnotationController(
/** Determines whether the annotation body should be collapsed. */
/** Determines whether the annotation body should be collapsed. */
vm
.
collapseBody
=
true
;
vm
.
collapseBody
=
true
;
/** True if the annotation is currently being saved. */
vm
.
isSaving
=
false
;
/** The domain model, contains the currently saved version of the
/** The domain model, contains the currently saved version of the
* annotation from the server (or in the case of new annotations that
* 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
* haven't been saved yet - the data that will be saved to the server when
...
@@ -661,7 +627,13 @@ function AnnotationController(
...
@@ -661,7 +627,13 @@ function AnnotationController(
*/
*/
vm
.
save
=
function
()
{
vm
.
save
=
function
()
{
if
(
!
domainModel
.
user
)
{
if
(
!
domainModel
.
user
)
{
return
flash
.
info
(
'Please sign in to save your annotations.'
);
flash
.
info
(
'Please sign in to save your annotations.'
);
return
Promise
.
resolve
();
}
if
((
vm
.
action
===
'create'
||
vm
.
action
===
'edit'
)
&&
!
vm
.
hasContent
()
&&
vm
.
isShared
())
{
flash
.
info
(
'Please add text or a tag before publishing.'
);
return
Promise
.
resolve
();
}
}
// Update stored tags with the new tags of this annotation.
// Update stored tags with the new tags of this annotation.
...
@@ -671,47 +643,45 @@ function AnnotationController(
...
@@ -671,47 +643,45 @@ function AnnotationController(
});
});
tags
.
store
(
newTags
);
tags
.
store
(
newTags
);
var
saved
;
switch
(
vm
.
action
)
{
switch
(
vm
.
action
)
{
case
'create'
:
case
'create'
:
updateDomainModel
(
domainModel
,
vm
,
permissions
,
groups
);
updateDomainModel
(
domainModel
,
vm
,
permissions
,
groups
);
saved
=
domainModel
.
$create
().
then
(
function
()
{
if
(
!
validate
(
domainModel
))
{
return
flash
.
info
(
'Please add text or a tag before publishing.'
);
}
var
onFulfilled
=
function
()
{
$rootScope
.
$emit
(
'annotationCreated'
,
domainModel
);
$rootScope
.
$emit
(
'annotationCreated'
,
domainModel
);
updateView
(
domainModel
);
updateView
(
domainModel
);
view
();
drafts
.
remove
(
domainModel
);
drafts
.
remove
(
domainModel
);
};
});
var
onRejected
=
function
(
reason
)
{
break
;
flash
.
error
(
errorMessage
(
reason
),
'Saving annotation failed'
);
};
return
domainModel
.
$create
().
then
(
onFulfilled
,
onRejected
);
case
'edit'
:
case
'edit'
:
var
updatedModel
=
angular
.
copy
(
domainModel
);
var
updatedModel
=
angular
.
copy
(
domainModel
);
updateDomainModel
(
updatedModel
,
vm
,
permissions
,
groups
);
updateDomainModel
(
updatedModel
,
vm
,
permissions
,
groups
);
saved
=
updatedModel
.
$update
({
if
(
!
validate
(
updatedModel
))
{
id
:
updatedModel
.
id
return
flash
.
info
(
'Please add text or a tag before publishing.'
);
}).
then
(
function
()
{
}
onFulfilled
=
function
()
{
drafts
.
remove
(
domainModel
);
drafts
.
remove
(
domainModel
);
$rootScope
.
$emit
(
'annotationUpdated'
,
updatedModel
);
$rootScope
.
$emit
(
'annotationUpdated'
,
updatedModel
);
view
();
});
};
break
;
onRejected
=
function
(
reason
)
{
flash
.
error
(
default
:
errorMessage
(
reason
),
'Saving annotation failed'
);
throw
new
Error
(
'Tried to save an annotation that is not being edited'
);
};
return
updatedModel
.
$update
({
id
:
updatedModel
.
id
}).
then
(
onFulfilled
,
onRejected
);
}
}
// optimistically switch back to view mode and display the saving
// indicator
vm
.
isSaving
=
true
;
view
();
return
saved
.
then
(
function
()
{
vm
.
isSaving
=
false
;
}).
catch
(
function
(
reason
)
{
vm
.
isSaving
=
false
;
vm
.
edit
();
flash
.
error
(
errorMessage
(
reason
),
'Saving annotation failed'
);
});
};
};
/**
/**
...
@@ -863,7 +833,6 @@ module.exports = {
...
@@ -863,7 +833,6 @@ module.exports = {
extractDocumentMetadata
:
extractDocumentMetadata
,
extractDocumentMetadata
:
extractDocumentMetadata
,
link
:
link
,
link
:
link
,
updateDomainModel
:
updateDomainModel
,
updateDomainModel
:
updateDomainModel
,
validate
:
validate
,
// These are meant to be the public API of this module.
// These are meant to be the public API of this module.
directive
:
annotation
,
directive
:
annotation
,
...
...
h/static/scripts/directive/test/annotation-test.js
View file @
644e5daa
...
@@ -262,99 +262,6 @@ describe('annotation', function() {
...
@@ -262,99 +262,6 @@ describe('annotation', function() {
});
});
});
});
describe
(
'validate()'
,
function
()
{
var
validate
=
require
(
'../annotation'
).
validate
;
it
(
'returns undefined if value is not an object'
,
function
()
{
var
i
;
var
values
=
[
2
,
'foo'
,
true
,
null
];
for
(
i
=
0
;
i
<
values
.
length
;
i
++
)
{
assert
.
equal
(
validate
(
values
[
i
]));
}
});
it
(
'returns the length if the value contains a non-empty tags array'
,
function
()
{
assert
.
equal
(
validate
({
tags
:
[
'foo'
,
'bar'
],
permissions
:
{
read
:
[
'group:__world__'
]
},
target
:
[
1
,
2
,
3
]
}),
2
);
}
);
it
(
'returns the length if the value contains a non-empty text string'
,
function
()
{
assert
.
equal
(
validate
({
text
:
'foobar'
,
permissions
:
{
read
:
[
'group:__world__'
]
},
target
:
[
1
,
2
,
3
]
}),
6
);
}
);
it
(
'returns true for private highlights'
,
function
()
{
assert
.
equal
(
validate
({
permissions
:
{
read
:
[
'acct:seanh@hypothes.is'
]
},
target
:
[
1
,
2
,
3
]
}),
true
);
});
it
(
'returns true for group highlights'
,
function
()
{
assert
.
equal
(
validate
({
permissions
:
{
read
:
[
'group:foo'
]
},
target
:
[
1
,
2
,
3
]
}),
true
);
});
it
(
'returns false for public highlights'
,
function
()
{
assert
.
equal
(
validate
(
{
text
:
undefined
,
tags
:
undefined
,
target
:
[
1
,
2
,
3
],
permissions
:
{
read
:
[
'group:__world__'
]}}
),
false
);
});
it
(
'handles values with no permissions'
,
function
()
{
assert
.
equal
(
validate
({
permissions
:
void
0
,
target
:
[
1
,
2
,
3
]
}),
true
);
});
it
(
'handles permissions objects with no read'
,
function
()
{
assert
.
equal
(
validate
({
permissions
:
{
read
:
void
0
},
target
:
[
1
,
2
,
3
]
}),
true
);
});
});
describe
(
'link'
,
function
()
{
describe
(
'link'
,
function
()
{
var
link
=
require
(
'../annotation'
).
link
;
var
link
=
require
(
'../annotation'
).
link
;
...
@@ -1120,6 +1027,7 @@ describe('annotation', function() {
...
@@ -1120,6 +1027,7 @@ describe('annotation', function() {
parts
.
controller
.
isPrivate
=
true
;
parts
.
controller
.
isPrivate
=
true
;
parts
.
annotation
.
$update
=
sinon
.
stub
().
returns
(
Promise
.
resolve
());
parts
.
annotation
.
$update
=
sinon
.
stub
().
returns
(
Promise
.
resolve
());
parts
.
controller
.
edit
();
parts
.
controller
.
edit
();
parts
.
controller
.
form
.
text
=
'test'
;
parts
.
controller
.
setPrivacy
(
'shared'
);
parts
.
controller
.
setPrivacy
(
'shared'
);
return
parts
.
controller
.
save
().
then
(
function
()
{
return
parts
.
controller
.
save
().
then
(
function
()
{
assert
.
equal
(
parts
.
controller
.
isPrivate
,
false
);
assert
.
equal
(
parts
.
controller
.
isPrivate
,
false
);
...
@@ -1131,6 +1039,7 @@ describe('annotation', function() {
...
@@ -1131,6 +1039,7 @@ describe('annotation', function() {
parts
.
annotation
.
$update
=
sinon
.
stub
().
returns
(
Promise
.
resolve
());
parts
.
annotation
.
$update
=
sinon
.
stub
().
returns
(
Promise
.
resolve
());
parts
.
controller
.
edit
();
parts
.
controller
.
edit
();
parts
.
controller
.
setPrivacy
(
'shared'
);
parts
.
controller
.
setPrivacy
(
'shared'
);
parts
.
controller
.
form
.
text
=
'test'
;
return
parts
.
controller
.
save
().
then
(
function
()
{
return
parts
.
controller
.
save
().
then
(
function
()
{
assert
(
fakePermissions
.
setDefault
.
calledWithExactly
(
'shared'
));
assert
(
fakePermissions
.
setDefault
.
calledWithExactly
(
'shared'
));
});
});
...
@@ -1245,6 +1154,7 @@ describe('annotation', function() {
...
@@ -1245,6 +1154,7 @@ describe('annotation', function() {
};
};
var
controller
=
createDirective
(
annotation
).
controller
;
var
controller
=
createDirective
(
annotation
).
controller
;
controller
.
action
=
'create'
;
controller
.
action
=
'create'
;
controller
.
form
.
text
=
'test'
;
return
controller
.
save
().
then
(
function
()
{
return
controller
.
save
().
then
(
function
()
{
assert
.
equal
(
controller
.
relativeTimestamp
,
'a while ago'
);
assert
.
equal
(
controller
.
relativeTimestamp
,
'a while ago'
);
});
});
...
@@ -1270,6 +1180,7 @@ describe('annotation', function() {
...
@@ -1270,6 +1180,7 @@ describe('annotation', function() {
var
controller
=
createDirective
(
annotation
).
controller
;
var
controller
=
createDirective
(
annotation
).
controller
;
assert
.
equal
(
controller
.
relativeTimestamp
,
'ages ago'
);
assert
.
equal
(
controller
.
relativeTimestamp
,
'ages ago'
);
controller
.
edit
();
controller
.
edit
();
controller
.
form
.
text
=
'test'
;
clock
.
restore
();
clock
.
restore
();
return
controller
.
save
().
then
(
function
()
{
return
controller
.
save
().
then
(
function
()
{
assert
.
equal
(
controller
.
relativeTimestamp
,
'just now'
);
assert
.
equal
(
controller
.
relativeTimestamp
,
'just now'
);
...
@@ -1414,6 +1325,7 @@ describe('annotation', function() {
...
@@ -1414,6 +1325,7 @@ describe('annotation', function() {
function
controllerWithActionCreate
()
{
function
controllerWithActionCreate
()
{
var
controller
=
createDirective
(
annotation
).
controller
;
var
controller
=
createDirective
(
annotation
).
controller
;
controller
.
action
=
'create'
;
controller
.
action
=
'create'
;
controller
.
form
.
text
=
'new annotation'
;
return
controller
;
return
controller
;
}
}
...
@@ -1471,6 +1383,36 @@ describe('annotation', function() {
...
@@ -1471,6 +1383,36 @@ describe('annotation', function() {
assert
(
fakeFlash
.
error
.
notCalled
);
assert
(
fakeFlash
.
error
.
notCalled
);
}
}
);
);
it
(
'shows a saving indicator when saving an annotation'
,
function
()
{
var
controller
=
controllerWithActionCreate
();
var
create
;
annotation
.
$create
.
returns
(
new
Promise
(
function
(
resolve
)
{
create
=
resolve
;
}));
var
saved
=
controller
.
save
();
assert
.
equal
(
controller
.
isSaving
,
true
);
assert
.
equal
(
controller
.
action
,
'view'
);
create
();
return
saved
.
then
(
function
()
{
assert
.
equal
(
controller
.
isSaving
,
false
);
});
});
it
(
'reverts to edit mode if saving fails'
,
function
()
{
var
controller
=
controllerWithActionCreate
();
var
failCreation
;
annotation
.
$create
.
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
());
});
});
});
});
describe
(
'saving an edited an annotation'
,
function
()
{
describe
(
'saving an edited an annotation'
,
function
()
{
...
@@ -1485,37 +1427,36 @@ describe('annotation', function() {
...
@@ -1485,37 +1427,36 @@ describe('annotation', function() {
function
controllerWithActionEdit
()
{
function
controllerWithActionEdit
()
{
var
controller
=
createDirective
(
annotation
).
controller
;
var
controller
=
createDirective
(
annotation
).
controller
;
controller
.
action
=
'edit'
;
controller
.
action
=
'edit'
;
controller
.
form
.
text
=
'updated text'
;
return
controller
;
return
controller
;
}
}
it
(
it
(
'flashes a generic error if the server cannot be reached'
,
'flashes a generic error if the server cannot be reached'
,
function
(
done
)
{
function
()
{
var
controller
=
controllerWithActionEdit
();
var
controller
=
controllerWithActionEdit
();
annotation
.
$update
.
returns
(
Promise
.
reject
({
annotation
.
$update
.
returns
(
Promise
.
reject
({
status
:
0
status
:
-
1
}));
}));
controller
.
save
().
then
(
function
()
{
return
controller
.
save
().
then
(
function
()
{
assert
(
fakeFlash
.
error
.
calledWith
(
assert
(
fakeFlash
.
error
.
calledWith
(
'Service unreachable.'
,
'Saving annotation failed'
));
'Service unreachable.'
,
'Saving annotation failed'
));
done
();
});
});
}
}
);
);
it
(
it
(
'flashes an error if saving the annotation fails on the server'
,
'flashes an error if saving the annotation fails on the server'
,
function
(
done
)
{
function
()
{
var
controller
=
controllerWithActionEdit
();
var
controller
=
controllerWithActionEdit
();
annotation
.
$update
.
returns
(
Promise
.
reject
({
annotation
.
$update
.
returns
(
Promise
.
reject
({
status
:
500
,
status
:
500
,
statusText
:
'Server Error'
,
statusText
:
'Server Error'
,
data
:
{}
data
:
{}
}));
}));
controller
.
save
().
then
(
function
()
{
return
controller
.
save
().
then
(
function
()
{
assert
(
fakeFlash
.
error
.
calledWith
(
assert
(
fakeFlash
.
error
.
calledWith
(
'500 Server Error'
,
'Saving annotation failed'
));
'500 Server Error'
,
'Saving annotation failed'
));
done
();
});
});
}
}
);
);
...
@@ -1525,6 +1466,7 @@ describe('annotation', function() {
...
@@ -1525,6 +1466,7 @@ describe('annotation', function() {
function
()
{
function
()
{
var
controller
=
controllerWithActionEdit
();
var
controller
=
controllerWithActionEdit
();
annotation
.
$update
.
returns
(
Promise
.
resolve
());
annotation
.
$update
.
returns
(
Promise
.
resolve
());
controller
.
form
.
text
=
'updated text'
;
controller
.
save
();
controller
.
save
();
assert
(
fakeFlash
.
error
.
notCalled
);
assert
(
fakeFlash
.
error
.
notCalled
);
}
}
...
@@ -1571,6 +1513,7 @@ describe('annotation', function() {
...
@@ -1571,6 +1513,7 @@ describe('annotation', function() {
annotation
.
$update
=
sandbox
.
stub
().
returns
(
Promise
.
resolve
());
annotation
.
$update
=
sandbox
.
stub
().
returns
(
Promise
.
resolve
());
var
controller
=
createDirective
(
annotation
).
controller
;
var
controller
=
createDirective
(
annotation
).
controller
;
controller
.
edit
();
controller
.
edit
();
controller
.
form
.
text
=
'test annotation'
;
return
controller
.
save
().
then
(
function
()
{
return
controller
.
save
().
then
(
function
()
{
assert
.
calledWith
(
fakeDrafts
.
remove
,
annotation
);
assert
.
calledWith
(
fakeDrafts
.
remove
,
annotation
);
});
});
...
...
h/templates/client/annotation.html
View file @
644e5daa
...
@@ -149,7 +149,12 @@
...
@@ -149,7 +149,12 @@
when=
"{'0': '', 'one': '1 reply', 'other': '{} replies'}"
></a>
when=
"{'0': '', 'one': '1 reply', 'other': '{} replies'}"
></a>
</div>
</div>
<div
class=
"annotation-actions"
ng-if=
"!vm.editing() && vm.id()"
>
<div
class=
"annotation-actions"
ng-if=
"vm.isSaving"
>
Saving...
</div>
<div
class=
"annotation-actions"
ng-if=
"!vm.isSaving && !vm.editing() && vm.id()"
>
<div
ng-show=
"vm.isSaving"
>
Saving…
</div>
<button
class=
"small btn btn-clean"
<button
class=
"small btn btn-clean"
ng-click=
"vm.reply()"
ng-click=
"vm.reply()"
><i
class=
"h-icon-reply btn-icon"
></i>
Reply
</button>
><i
class=
"h-icon-reply btn-icon"
></i>
Reply
</button>
...
@@ -166,7 +171,7 @@
...
@@ -166,7 +171,7 @@
</span>
</span>
</span>
</span>
<button
class=
"small btn btn-clean"
<button
class=
"small btn btn-clean"
ng-show=
"vm.authorize('update')"
ng-show=
"vm.authorize('update')
&& !vm.isSaving
"
ng-click=
"vm.edit()"
ng-click=
"vm.edit()"
><i
class=
"h-icon-edit btn-icon"
></i>
Edit
</button>
><i
class=
"h-icon-edit btn-icon"
></i>
Edit
</button>
<button
class=
"small btn btn-clean"
<button
class=
"small btn btn-clean"
...
...
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