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
8142037e
Unverified
Commit
8142037e
authored
Mar 04, 2020
by
Lyza Gardner
Committed by
GitHub
Mar 04, 2020
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1869 from hypothesis/create-annotation-service
Move `createAnnotation` to a service
parents
ec5d1b01
5ee1e30d
Changes
13
Show whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
321 additions
and
426 deletions
+321
-426
annotation-omega.js
src/sidebar/components/annotation-omega.js
+3
-27
annotation.js
src/sidebar/components/annotation.js
+0
-9
new-note-btn.js
src/sidebar/components/new-note-btn.js
+4
-4
annotation-omega-test.js
src/sidebar/components/test/annotation-omega-test.js
+0
-17
annotation-test.js
src/sidebar/components/test/annotation-test.js
+0
-22
new-note-btn-test.js
src/sidebar/components/test/new-note-btn-test.js
+11
-2
annotations.js
src/sidebar/services/annotations.js
+94
-5
root-thread.js
src/sidebar/services/root-thread.js
+2
-1
annotations-test.js
src/sidebar/services/test/annotations-test.js
+196
-24
root-thread-test.js
src/sidebar/services/test/root-thread-test.js
+7
-2
annotations.js
src/sidebar/store/modules/annotations.js
+2
-71
annotations-test.js
src/sidebar/store/modules/test/annotations-test.js
+1
-242
threading-test.js
src/sidebar/test/integration/threading-test.js
+1
-0
No files found.
src/sidebar/components/annotation-omega.js
View file @
8142037e
import
classnames
from
'classnames'
;
import
{
createElement
}
from
'preact'
;
import
{
use
Effect
,
use
State
}
from
'preact/hooks'
;
import
{
useState
}
from
'preact/hooks'
;
import
propTypes
from
'prop-types'
;
import
useStore
from
'../store/use-store'
;
import
{
isHighlight
,
isNew
,
isReply
,
quote
,
}
from
'../util/annotation-metadata'
;
import
{
isNew
,
isReply
,
quote
}
from
'../util/annotation-metadata'
;
import
{
isShared
}
from
'../util/permissions'
;
import
{
withServices
}
from
'../util/service-context'
;
...
...
@@ -56,18 +51,6 @@ function AnnotationOmega({
const
toggleAction
=
threadIsCollapsed
?
'Show replies'
:
'Hide replies'
;
const
toggleText
=
`
${
toggleAction
}
(
${
replyCount
}
)`
;
useEffect
(()
=>
{
// TEMPORARY. Create a new draft for new (non-highlight) annotations
// to put the component in "edit mode."
if
(
!
isSaving
&&
!
draft
&&
isNew
(
annotation
)
&&
!
isHighlight
(
annotation
))
{
createDraft
(
annotation
,
{
tags
:
annotation
.
tags
,
text
:
annotation
.
text
,
isPrivate
:
!
isShared
(
annotation
.
permissions
),
});
}
},
[
annotation
,
draft
,
createDraft
,
isSaving
]);
const
shouldShowActions
=
!
isSaving
&&
!
isEditing
&&
!
isNew
(
annotation
);
const
shouldShowLicense
=
isEditing
&&
!
isPrivate
&&
group
.
type
!==
'private'
;
const
shouldShowReplyToggle
=
replyCount
>
0
&&
!
isReply
(
annotation
);
...
...
@@ -80,14 +63,7 @@ function AnnotationOmega({
createDraft
(
annotation
,
{
...
draft
,
text
});
};
const
onReply
=
()
=>
{
// TODO: Re-evaluate error handling here
if
(
!
userid
)
{
flash
.
error
(
'Please log in to reply to annotations'
);
}
else
{
annotationsService
.
reply
(
annotation
,
userid
);
}
};
const
onReply
=
()
=>
annotationsService
.
reply
(
annotation
,
userid
);
const
onSave
=
async
()
=>
{
setIsSaving
(
true
);
...
...
src/sidebar/components/annotation.js
View file @
8142037e
...
...
@@ -82,15 +82,6 @@ function AnnotationController(
* new client-side).
*/
newlyCreatedByHighlightButton
=
self
.
annotation
.
$highlight
||
false
;
// 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
(
self
.
annotation
)
||
store
.
getDraft
(
self
.
annotation
))
{
self
.
edit
();
}
}
};
/**
...
...
src/sidebar/components/new-note-btn.js
View file @
8142037e
...
...
@@ -8,11 +8,10 @@ import { applyTheme } from '../util/theme';
import
Button
from
'./button'
;
function
NewNoteButton
({
settings
})
{
function
NewNoteButton
({
annotationsService
,
settings
})
{
const
topLevelFrame
=
useStore
(
store
=>
store
.
mainFrame
());
const
isLoggedIn
=
useStore
(
store
=>
store
.
isLoggedIn
());
const
createAnnotation
=
useStore
(
store
=>
store
.
createAnnotation
);
const
openSidebarPanel
=
useStore
(
store
=>
store
.
openSidebarPanel
);
const
onNewNoteBtnClick
=
function
()
{
...
...
@@ -24,7 +23,7 @@ function NewNoteButton({ settings }) {
target
:
[],
uri
:
topLevelFrame
.
uri
,
};
createAnnotation
(
annot
);
annotationsService
.
create
(
annot
);
};
return
(
...
...
@@ -42,9 +41,10 @@ function NewNoteButton({ settings }) {
}
NewNoteButton
.
propTypes
=
{
// Injected services.
annotationsService
:
propTypes
.
object
.
isRequired
,
settings
:
propTypes
.
object
.
isRequired
,
};
NewNoteButton
.
injectedProps
=
[
'settings'
];
NewNoteButton
.
injectedProps
=
[
'
annotationsService'
,
'
settings'
];
export
default
withServices
(
NewNoteButton
);
src/sidebar/components/test/annotation-omega-test.js
View file @
8142037e
...
...
@@ -445,23 +445,6 @@ describe('AnnotationOmega', () => {
describe
(
'annotation actions'
,
()
=>
{
describe
(
'replying to an annotation'
,
()
=>
{
// nb: There's no reason this logic needs to stay within `AnnotationOmega`
// once we've migrated to it; it could happily move to `AnnotationActionBar`
it
(
'should show a flash alert if user not logged in'
,
()
=>
{
// No logged-in user...
fakeStore
.
profile
.
returns
({});
const
wrapper
=
createComponent
();
wrapper
.
find
(
'AnnotationActionBar'
)
.
props
()
.
onReply
();
assert
.
calledOnce
(
fakeFlash
.
error
);
assert
.
notCalled
(
fakeAnnotationsService
.
reply
);
});
it
(
'should create a reply'
,
()
=>
{
const
theAnnot
=
fixtures
.
defaultAnnotation
();
const
wrapper
=
createComponent
({
annotation
:
theAnnot
});
...
...
src/sidebar/components/test/annotation-test.js
View file @
8142037e
...
...
@@ -235,28 +235,6 @@ describe('annotation', function() {
sandbox
.
restore
();
});
describe
(
'initialization'
,
function
()
{
it
(
'creates drafts for new annotations on initialization'
,
function
()
{
const
annotation
=
fixtures
.
newAnnotation
();
createDirective
(
annotation
);
assert
.
calledWith
(
fakeStore
.
createDraft
,
annotation
,
{
isPrivate
:
false
,
tags
:
annotation
.
tags
,
text
:
annotation
.
text
,
});
});
it
(
'edits annotations with drafts on initialization'
,
function
()
{
const
annotation
=
fixtures
.
oldAnnotation
();
// The drafts store has some draft changes for this annotation.
fakeStore
.
getDraft
.
returns
({
text
:
'foo'
,
tags
:
[]
});
const
controller
=
createDirective
(
annotation
).
controller
;
assert
.
isTrue
(
controller
.
editing
());
});
});
describe
(
'#editing()'
,
function
()
{
it
(
'returns false if the annotation does not have a draft'
,
function
()
{
const
controller
=
createDirective
().
controller
;
...
...
src/sidebar/components/test/new-note-btn-test.js
View file @
8142037e
...
...
@@ -11,13 +11,22 @@ import mockImportedComponents from '../../../test-util/mock-imported-components'
describe
(
'NewNoteButton'
,
function
()
{
let
fakeStore
;
let
fakeAnnotationsService
;
let
fakeSettings
;
function
createComponent
()
{
return
mount
(
<
NewNoteButton
settings
=
{
fakeSettings
}
/>
)
;
return
mount
(
<
NewNoteButton
annotationsService
=
{
fakeAnnotationsService
}
settings
=
{
fakeSettings
}
/
>
);
}
beforeEach
(
function
()
{
fakeAnnotationsService
=
{
create
:
sinon
.
stub
(),
};
fakeSettings
=
{
branding
:
{
ctaBackgroundColor
:
'#00f'
,
...
...
@@ -77,7 +86,7 @@ describe('NewNoteButton', function() {
.
onClick
();
});
assert
.
calledWith
(
fake
Store
.
createAnnotation
,
{
assert
.
calledWith
(
fake
AnnotationsService
.
create
,
{
target
:
[],
uri
:
'thisFrame'
,
});
...
...
src/sidebar/services/annotations.js
View file @
8142037e
import
SearchClient
from
'../search-client'
;
import
{
isNew
,
isPublic
}
from
'../util/annotation-metadata'
;
import
{
privatePermissions
,
sharedPermissions
}
from
'../util/permissions'
;
import
*
as
metadata
from
'../util/annotation-metadata'
;
import
{
defaultPermissions
,
privatePermissions
,
sharedPermissions
,
}
from
'../util/permissions'
;
import
{
generateHexString
}
from
'../util/random'
;
import
uiConstants
from
'../ui-constants'
;
// @ngInject
export
default
function
annotationsService
(
...
...
@@ -12,6 +18,88 @@ export default function annotationsService(
)
{
let
searchClient
=
null
;
/**
* Extend new annotation objects with defaults and permissions.
*/
function
initialize
(
annotationData
,
now
=
new
Date
())
{
const
defaultPrivacy
=
store
.
getDefault
(
'annotationPrivacy'
);
const
groupid
=
store
.
focusedGroupId
();
const
profile
=
store
.
profile
();
const
userid
=
profile
.
userid
;
const
userInfo
=
profile
.
user_info
;
// We need a unique local/app identifier for this new annotation such
// that we might look it up later in the store. It won't have an ID yet,
// as it has not been persisted to the service.
const
$tag
=
generateHexString
(
8
);
let
permissions
=
defaultPermissions
(
userid
,
groupid
,
defaultPrivacy
);
// Highlights are peculiar in that they always have private permissions
if
(
metadata
.
isHighlight
(
annotationData
))
{
permissions
=
privatePermissions
(
userid
);
}
return
Object
.
assign
(
{
created
:
now
.
toISOString
(),
group
:
groupid
,
permissions
,
tags
:
[],
text
:
''
,
updated
:
now
.
toISOString
(),
user
:
userid
,
user_info
:
userInfo
,
$tag
:
$tag
,
},
annotationData
);
}
/**
* Populate a new annotation object from `annotation` and add it to the store.
* Create a draft for it unless it's a highlight and clear other empty
* drafts out of the way.
*
* @param {Object} annotationData
* @param {Date} now
*/
function
create
(
annotationData
,
now
=
new
Date
())
{
const
annotation
=
initialize
(
annotationData
,
now
);
store
.
addAnnotations
([
annotation
]);
// Remove other drafts that are in the way, and their annotations (if new)
store
.
deleteNewAndEmptyDrafts
();
// Create a draft unless it's a highlight
if
(
!
metadata
.
isHighlight
(
annotation
))
{
store
.
createDraft
(
annotation
,
{
tags
:
annotation
.
tags
,
text
:
annotation
.
text
,
isPrivate
:
!
metadata
.
isPublic
(
annotation
),
});
}
// NB: It may make sense to move the following code at some point to
// the UI layer
// Select the correct tab
// If the annotation is of type note or annotation, make sure
// the appropriate tab is selected. If it is of type reply, user
// stays in the selected tab.
if
(
metadata
.
isPageNote
(
annotation
))
{
store
.
selectTab
(
uiConstants
.
TAB_NOTES
);
}
else
if
(
metadata
.
isAnnotation
(
annotation
))
{
store
.
selectTab
(
uiConstants
.
TAB_ANNOTATIONS
);
}
(
annotation
.
references
||
[]).
forEach
(
parent
=>
{
// Expand any parents of this annotation.
store
.
setCollapsed
(
parent
,
false
);
});
}
/**
* Load annotations for all URIs and groupId.
*
...
...
@@ -90,14 +178,14 @@ export default function annotationsService(
function
reply
(
annotation
,
userid
)
{
const
replyAnnotation
=
{
group
:
annotation
.
group
,
permissions
:
isPublic
(
annotation
)
permissions
:
metadata
.
isPublic
(
annotation
)
?
sharedPermissions
(
userid
,
annotation
.
group
)
:
privatePermissions
(
userid
),
references
:
(
annotation
.
references
||
[]).
concat
(
annotation
.
id
),
target
:
[{
source
:
annotation
.
target
[
0
].
source
}],
uri
:
annotation
.
uri
,
};
store
.
createAnnotation
(
replyAnnotation
);
create
(
replyAnnotation
);
}
/**
...
...
@@ -110,7 +198,7 @@ export default function annotationsService(
const
annotationWithChanges
=
applyDraftChanges
(
annotation
);
if
(
isNew
(
annotation
))
{
if
(
metadata
.
isNew
(
annotation
))
{
saved
=
api
.
annotation
.
create
({},
annotationWithChanges
);
}
else
{
saved
=
api
.
annotation
.
update
(
...
...
@@ -136,6 +224,7 @@ export default function annotationsService(
}
return
{
create
,
load
,
reply
,
save
,
...
...
src/sidebar/services/root-thread.js
View file @
8142037e
...
...
@@ -39,6 +39,7 @@ const sortFns = {
// @ngInject
export
default
function
RootThread
(
$rootScope
,
annotationsService
,
store
,
searchFilter
,
viewFilter
...
...
@@ -114,7 +115,7 @@ export default function RootThread(
});
$rootScope
.
$on
(
events
.
BEFORE_ANNOTATION_CREATED
,
function
(
event
,
ann
)
{
store
.
createAnnotation
(
ann
);
annotationsService
.
create
(
ann
);
});
// Remove any annotations that are deleted or unloaded
...
...
src/sidebar/services/test/annotations-test.js
View file @
8142037e
import
EventEmitter
from
'tiny-emitter'
;
import
*
as
fixtures
from
'../../test/annotation-fixtures'
;
import
uiConstants
from
'../../ui-constants'
;
import
annotationsService
from
'../annotations'
;
import
{
$imports
}
from
'../annotations'
;
...
...
@@ -38,11 +39,11 @@ describe('annotationService', () => {
let
fakeStreamer
;
let
fakeStreamFilter
;
let
fakeMetadata
;
let
fakeUris
;
let
fakeGroupId
;
let
fakeIsNew
;
let
fakeIsPublic
;
let
fakeDefaultPermissions
;
let
fakePrivatePermissions
;
let
fakeSharedPermissions
;
...
...
@@ -64,8 +65,7 @@ describe('annotationService', () => {
search
:
sinon
.
stub
(),
};
fakeIsNew
=
sinon
.
stub
().
returns
(
true
);
fakeIsPublic
=
sinon
.
stub
().
returns
(
true
);
fakeDefaultPermissions
=
sinon
.
stub
();
fakePrivatePermissions
=
sinon
.
stub
().
returns
({
read
:
[
'acct:foo@bar.com'
],
...
...
@@ -75,20 +75,33 @@ describe('annotationService', () => {
fakeSharedPermissions
=
sinon
.
stub
().
returns
({
read
:
[
'group:__world__'
],
});
fakeMetadata
=
{
isAnnotation
:
sinon
.
stub
(),
isHighlight
:
sinon
.
stub
(),
isNew
:
sinon
.
stub
(),
isPageNote
:
sinon
.
stub
(),
isPublic
:
sinon
.
stub
(),
};
fakeStore
=
{
addAnnotations
:
sinon
.
stub
(),
annotationFetchFinished
:
sinon
.
stub
(),
annotationFetchStarted
:
sinon
.
stub
(),
createAnnotation
:
sinon
.
stub
(),
createDraft
:
sinon
.
stub
(),
deleteNewAndEmptyDrafts
:
sinon
.
stub
(),
focusedGroupId
:
sinon
.
stub
(),
frames
:
sinon
.
stub
(),
getDefault
:
sinon
.
stub
(),
getDraft
:
sinon
.
stub
().
returns
(
null
),
getState
:
sinon
.
stub
(),
hasSelectedAnnotations
:
sinon
.
stub
(),
profile
:
sinon
.
stub
().
returns
({}),
removeDraft
:
sinon
.
stub
(),
searchUris
:
sinon
.
stub
(),
savedAnnotations
:
sinon
.
stub
(),
selectTab
:
sinon
.
stub
(),
setCollapsed
:
sinon
.
stub
(),
updateFrameAnnotationFetchStatus
:
sinon
.
stub
(),
};
fakeStreamer
=
{
setConfig
:
sinon
.
stub
(),
connect
:
sinon
.
stub
(),
...
...
@@ -105,11 +118,9 @@ describe('annotationService', () => {
$imports
.
$mock
({
'../search-client'
:
FakeSearchClient
,
'../util/annotation-metadata'
:
{
isNew
:
fakeIsNew
,
isPublic
:
fakeIsPublic
,
},
'../util/annotation-metadata'
:
fakeMetadata
,
'../util/permissions'
:
{
defaultPermissions
:
fakeDefaultPermissions
,
privatePermissions
:
fakePrivatePermissions
,
sharedPermissions
:
fakeSharedPermissions
,
},
...
...
@@ -136,6 +147,166 @@ describe('annotationService', () => {
);
}
describe
(
'create'
,
()
=>
{
let
now
;
let
svc
;
const
getLastAddedAnnotation
=
()
=>
{
if
(
fakeStore
.
addAnnotations
.
callCount
<=
0
)
{
return
null
;
}
const
callCount
=
fakeStore
.
addAnnotations
.
callCount
;
return
fakeStore
.
addAnnotations
.
getCall
(
callCount
-
1
).
args
[
0
][
0
];
};
beforeEach
(()
=>
{
now
=
new
Date
();
svc
=
service
();
fakeStore
.
focusedGroupId
.
returns
(
'mygroup'
);
fakeStore
.
profile
.
returns
({
userid
:
'acct:foo@bar.com'
,
user_info
:
{},
});
});
it
(
'extends the provided annotation object with defaults'
,
()
=>
{
fakeStore
.
focusedGroupId
.
returns
(
'mygroup'
);
svc
.
create
({},
now
);
const
annotation
=
getLastAddedAnnotation
();
assert
.
equal
(
annotation
.
created
,
now
.
toISOString
());
assert
.
equal
(
annotation
.
group
,
'mygroup'
);
assert
.
isArray
(
annotation
.
tags
);
assert
.
isEmpty
(
annotation
.
tags
);
assert
.
isString
(
annotation
.
text
);
assert
.
isEmpty
(
annotation
.
text
);
assert
.
equal
(
annotation
.
updated
,
now
.
toISOString
());
assert
.
equal
(
annotation
.
user
,
'acct:foo@bar.com'
);
assert
.
isOk
(
annotation
.
$tag
);
assert
.
isString
(
annotation
.
$tag
);
});
describe
(
'annotation permissions'
,
()
=>
{
it
(
'sets private permissions if default privacy level is "private"'
,
()
=>
{
fakeStore
.
getDefault
.
returns
(
'private'
);
fakeDefaultPermissions
.
returns
(
'private-permissions'
);
svc
.
create
({},
now
);
const
annotation
=
getLastAddedAnnotation
();
assert
.
calledOnce
(
fakeDefaultPermissions
);
assert
.
calledWith
(
fakeDefaultPermissions
,
'acct:foo@bar.com'
,
'mygroup'
,
'private'
);
assert
.
equal
(
annotation
.
permissions
,
'private-permissions'
);
});
it
(
'sets shared permissions if default privacy level is "shared"'
,
()
=>
{
fakeStore
.
getDefault
.
returns
(
'shared'
);
fakeDefaultPermissions
.
returns
(
'default permissions'
);
svc
.
create
({},
now
);
const
annotation
=
getLastAddedAnnotation
();
assert
.
calledOnce
(
fakeDefaultPermissions
);
assert
.
calledWith
(
fakeDefaultPermissions
,
'acct:foo@bar.com'
,
'mygroup'
,
'shared'
);
assert
.
equal
(
annotation
.
permissions
,
'default permissions'
);
});
it
(
'sets private permissions if annotation is a highlight'
,
()
=>
{
fakeMetadata
.
isHighlight
.
returns
(
true
);
fakePrivatePermissions
.
returns
(
'private permissions'
);
fakeDefaultPermissions
.
returns
(
'default permissions'
);
svc
.
create
({},
now
);
const
annotation
=
getLastAddedAnnotation
();
assert
.
calledOnce
(
fakePrivatePermissions
);
assert
.
equal
(
annotation
.
permissions
,
'private permissions'
);
});
});
it
(
'creates a draft for the new annotation'
,
()
=>
{
fakeMetadata
.
isHighlight
.
returns
(
false
);
svc
.
create
(
fixtures
.
newAnnotation
(),
now
);
assert
.
calledOnce
(
fakeStore
.
createDraft
);
});
it
(
'adds the annotation to the store'
,
()
=>
{
svc
.
create
(
fixtures
.
newAnnotation
(),
now
);
assert
.
calledOnce
(
fakeStore
.
addAnnotations
);
});
it
(
'deletes other empty drafts for new annotations'
,
()
=>
{
svc
.
create
(
fixtures
.
newAnnotation
(),
now
);
assert
.
calledOnce
(
fakeStore
.
deleteNewAndEmptyDrafts
);
});
it
(
'does not create a draft if the annotation is a highlight'
,
()
=>
{
fakeMetadata
.
isHighlight
.
returns
(
true
);
svc
.
create
(
fixtures
.
newAnnotation
(),
now
);
assert
.
notCalled
(
fakeStore
.
createDraft
);
});
describe
(
'automatic tab selection'
,
()
=>
{
it
(
'sets the active tab to "Page Notes" if the annotation is a Page Note'
,
()
=>
{
fakeMetadata
.
isPageNote
.
returns
(
true
);
svc
.
create
(
fixtures
.
newAnnotation
(),
now
);
assert
.
calledOnce
(
fakeStore
.
selectTab
);
assert
.
calledWith
(
fakeStore
.
selectTab
,
uiConstants
.
TAB_NOTES
);
});
it
(
'sets the active tab to "Annotations" if the annotation is an annotation'
,
()
=>
{
fakeMetadata
.
isAnnotation
.
returns
(
true
);
svc
.
create
(
fixtures
.
newAnnotation
(),
now
);
assert
.
calledOnce
(
fakeStore
.
selectTab
);
assert
.
calledWith
(
fakeStore
.
selectTab
,
uiConstants
.
TAB_ANNOTATIONS
);
});
it
(
'does nothing if the annotation is neither an annotation nor a page note (e.g. reply)'
,
()
=>
{
fakeMetadata
.
isAnnotation
.
returns
(
false
);
fakeMetadata
.
isPageNote
.
returns
(
false
);
svc
.
create
(
fixtures
.
newAnnotation
(),
now
);
assert
.
notCalled
(
fakeStore
.
selectTab
);
});
});
it
(
"expands all of the new annotation's parents"
,
()
=>
{
const
annot
=
fixtures
.
newAnnotation
();
annot
.
references
=
[
'aparent'
,
'anotherparent'
,
'yetanotherancestor'
];
svc
.
create
(
annot
,
now
);
assert
.
equal
(
fakeStore
.
setCollapsed
.
callCount
,
3
);
assert
.
calledWith
(
fakeStore
.
setCollapsed
,
'aparent'
,
false
);
assert
.
calledWith
(
fakeStore
.
setCollapsed
,
'anotherparent'
,
false
);
assert
.
calledWith
(
fakeStore
.
setCollapsed
,
'yetanotherancestor'
,
false
);
});
});
describe
(
'load'
,
()
=>
{
it
(
'unloads any existing annotations'
,
()
=>
{
// When new clients connect, all existing annotations should be unloaded
...
...
@@ -323,7 +494,7 @@ describe('annotationService', () => {
svc
.
reply
(
annotation
,
'acct:foo@bar.com'
);
assert
.
calledOnce
(
fakeStore
.
createAnnotation
);
assert
.
calledOnce
(
fakeStore
.
addAnnotations
);
});
it
(
'associates the reply with the annotation'
,
()
=>
{
...
...
@@ -331,7 +502,7 @@ describe('annotationService', () => {
svc
.
reply
(
annotation
,
'acct:foo@bar.com'
);
const
reply
=
fakeStore
.
createAnnotation
.
getCall
(
0
).
args
[
0
];
const
reply
=
fakeStore
.
addAnnotations
.
getCall
(
0
).
args
[
0
]
[
0
];
assert
.
equal
(
reply
.
references
[
reply
.
references
.
length
-
1
],
...
...
@@ -343,26 +514,26 @@ describe('annotationService', () => {
});
it
(
'uses public permissions if annotation is public'
,
()
=>
{
fake
I
sPublic
.
returns
(
true
);
fake
Metadata
.
i
sPublic
.
returns
(
true
);
fakeSharedPermissions
.
returns
(
'public'
);
const
annotation
=
filledAnnotation
();
svc
.
reply
(
annotation
,
'acct:foo@bar.com'
);
const
reply
=
fakeStore
.
createAnnotation
.
getCall
(
0
).
args
[
0
];
const
reply
=
fakeStore
.
addAnnotations
.
getCall
(
0
).
args
[
0
]
[
0
];
assert
.
equal
(
reply
.
permissions
,
'public'
);
});
it
(
'uses private permissions if annotation is private'
,
()
=>
{
fake
I
sPublic
.
returns
(
false
);
fake
Metadata
.
i
sPublic
.
returns
(
false
);
fakePrivatePermissions
.
returns
(
'private'
);
const
annotation
=
filledAnnotation
();
svc
.
reply
(
annotation
,
'acct:foo@bar.com'
);
const
reply
=
fakeStore
.
createAnnotation
.
getCall
(
0
).
args
[
0
];
const
reply
=
fakeStore
.
addAnnotations
.
getCall
(
0
).
args
[
0
]
[
0
];
assert
.
equal
(
reply
.
permissions
,
'private'
);
});
});
...
...
@@ -375,7 +546,7 @@ describe('annotationService', () => {
});
it
(
'calls the `create` API service for new annotations'
,
()
=>
{
fake
I
sNew
.
returns
(
true
);
fake
Metadata
.
i
sNew
.
returns
(
true
);
// Using the new-annotation fixture has no bearing on which API method
// will get called because `isNew` is mocked, but it has representative
// properties
...
...
@@ -387,7 +558,7 @@ describe('annotationService', () => {
});
it
(
'calls the `update` API service for pre-existing annotations'
,
()
=>
{
fake
I
sNew
.
returns
(
false
);
fake
Metadata
.
i
sNew
.
returns
(
false
);
const
annotation
=
fixtures
.
defaultAnnotation
();
return
svc
.
save
(
annotation
).
then
(()
=>
{
...
...
@@ -397,6 +568,7 @@ describe('annotationService', () => {
});
it
(
'calls the relevant API service with an object that has any draft changes integrated'
,
()
=>
{
fakeMetadata
.
isNew
.
returns
(
true
);
fakePrivatePermissions
.
returns
({
read
:
[
'foo'
]
});
const
annotation
=
fixtures
.
defaultAnnotation
();
annotation
.
text
=
'not this'
;
...
...
@@ -424,7 +596,7 @@ describe('annotationService', () => {
context
(
'successful save'
,
()
=>
{
it
(
'copies over internal app-specific keys to the annotation object'
,
()
=>
{
fake
I
sNew
.
returns
(
false
);
fake
Metadata
.
i
sNew
.
returns
(
false
);
const
annotation
=
fixtures
.
defaultAnnotation
();
annotation
.
$tag
=
'mytag'
;
annotation
.
$foo
=
'bar'
;
...
...
@@ -450,7 +622,7 @@ describe('annotationService', () => {
it
(
'adds the updated annotation to the store'
,
()
=>
{
const
annotation
=
fixtures
.
defaultAnnotation
();
fake
I
sNew
.
returns
(
false
);
fake
Metadata
.
i
sNew
.
returns
(
false
);
fakeApi
.
annotation
.
update
.
resolves
(
annotation
);
return
svc
.
save
(
annotation
).
then
(()
=>
{
...
...
@@ -462,7 +634,7 @@ describe('annotationService', () => {
context
(
'error on save'
,
()
=>
{
it
(
'does not remove the annotation draft'
,
()
=>
{
fakeApi
.
annotation
.
update
.
rejects
();
fake
I
sNew
.
returns
(
false
);
fake
Metadata
.
i
sNew
.
returns
(
false
);
return
svc
.
save
(
fixtures
.
defaultAnnotation
()).
catch
(()
=>
{
assert
.
notCalled
(
fakeStore
.
removeDraft
);
...
...
@@ -471,7 +643,7 @@ describe('annotationService', () => {
it
(
'does not add the annotation to the store'
,
()
=>
{
fakeApi
.
annotation
.
update
.
rejects
();
fake
I
sNew
.
returns
(
false
);
fake
Metadata
.
i
sNew
.
returns
(
false
);
return
svc
.
save
(
fixtures
.
defaultAnnotation
()).
catch
(()
=>
{
assert
.
notCalled
(
fakeStore
.
addAnnotations
);
...
...
src/sidebar/services/test/root-thread-test.js
View file @
8142037e
...
...
@@ -20,6 +20,7 @@ const fixtures = immutable({
});
describe
(
'rootThread'
,
function
()
{
let
fakeAnnotationsService
;
let
fakeStore
;
let
fakeBuildThread
;
let
fakeSearchFilter
;
...
...
@@ -31,6 +32,9 @@ describe('rootThread', function() {
let
rootThread
;
beforeEach
(
function
()
{
fakeAnnotationsService
=
{
create
:
sinon
.
stub
(),
};
fakeStore
=
{
state
:
{
annotations
:
{
...
...
@@ -83,6 +87,7 @@ describe('rootThread', function() {
angular
.
module
(
'app'
,
[])
.
value
(
'annotationsService'
,
fakeAnnotationsService
)
.
value
(
'store'
,
fakeStore
)
.
value
(
'searchFilter'
,
fakeSearchFilter
)
.
value
(
'settings'
,
fakeSettings
)
...
...
@@ -351,10 +356,10 @@ describe('rootThread', function() {
context
(
'when annotation events occur'
,
function
()
{
const
annot
=
annotationFixtures
.
defaultAnnotation
();
it
(
'creates a new annotation
in the store
when BEFORE_ANNOTATION_CREATED event occurs'
,
function
()
{
it
(
'creates a new annotation when BEFORE_ANNOTATION_CREATED event occurs'
,
function
()
{
$rootScope
.
$broadcast
(
events
.
BEFORE_ANNOTATION_CREATED
,
annot
);
assert
.
notCalled
(
fakeStore
.
removeAnnotations
);
assert
.
calledWith
(
fake
Store
.
createAnnotation
,
sinon
.
match
(
annot
));
assert
.
calledWith
(
fake
AnnotationsService
.
create
,
sinon
.
match
(
annot
));
});
[
...
...
src/sidebar/store/modules/annotations.js
View file @
8142037e
...
...
@@ -5,15 +5,10 @@
import
{
createSelector
}
from
'reselect'
;
import
uiConstants
from
'../../ui-constants'
;
import
*
as
metadata
from
'../../util/annotation-metadata'
;
import
*
as
arrayUtil
from
'../../util/array'
;
import
{
defaultPermissions
,
privatePermissions
}
from
'../../util/permissions'
;
import
*
as
util
from
'../util'
;
import
drafts
from
'./drafts'
;
import
selection
from
'./selection'
;
/**
* Return a copy of `current` with all matching annotations in `annotations`
* removed.
...
...
@@ -241,6 +236,8 @@ function addAnnotations(annotations) {
});
// If we're not in the sidebar, we're done here.
// FIXME Split the annotation-adding from the anchoring code; possibly
// move into service
if
(
!
getState
().
viewer
.
isSidebar
)
{
return
;
}
...
...
@@ -320,71 +317,6 @@ function hideAnnotation(id) {
};
}
/**
* Create a new annotation (as-yet unpersisted)
*
* This method has several responsibilities:
* 1. Set some default data attributes on the annotation
* 2. Remove any existing, empty drafts
* 3. Add the annotation to the current collection of annotations
* 4. Change focused tab to the applicable one for the new annotation's meta-type
* 5. Expand all of the new annotation's parents
*
*/
function
createAnnotation
(
ann
,
now
=
new
Date
())
{
return
(
dispatch
,
getState
)
=>
{
/**
* Extend the new, unsaved annotation object with defaults for some
* required data fields and default permissions.
*
* Note: the `created` and `updated` values will be ignored and superseded
* by the service when the annotation is persisted, but they are used
* app-side for annotation card sorting until then.
*/
const
groupid
=
getState
().
groups
.
focusedGroupId
;
const
userid
=
getState
().
session
.
userid
;
ann
=
Object
.
assign
(
{
created
:
now
.
toISOString
(),
group
:
groupid
,
permissions
:
defaultPermissions
(
userid
,
groupid
,
getState
().
defaults
.
annotationPrivacy
),
tags
:
[],
text
:
''
,
updated
:
now
.
toISOString
(),
user
:
userid
,
user_info
:
getState
().
session
.
user_info
,
},
ann
);
// Highlights are peculiar in that they always have private permissions
if
(
metadata
.
isHighlight
(
ann
))
{
ann
.
permissions
=
privatePermissions
(
userid
);
}
// When a new annotation is created, remove any existing annotations
// that are empty.
dispatch
(
drafts
.
actions
.
deleteNewAndEmptyDrafts
([
ann
]));
dispatch
(
addAnnotations
([
ann
]));
// If the annotation is of type note or annotation, make sure
// the appropriate tab is selected. If it is of type reply, user
// stays in the selected tab.
if
(
metadata
.
isPageNote
(
ann
))
{
dispatch
(
selection
.
actions
.
selectTab
(
uiConstants
.
TAB_NOTES
));
}
else
if
(
metadata
.
isAnnotation
(
ann
))
{
dispatch
(
selection
.
actions
.
selectTab
(
uiConstants
.
TAB_ANNOTATIONS
));
}
(
ann
.
references
||
[]).
forEach
(
parent
=>
{
// Expand any parents of this annotation.
dispatch
(
selection
.
actions
.
setCollapsed
(
parent
,
false
));
});
};
}
/**
* Update the local hidden state of an annotation.
*
...
...
@@ -491,7 +423,6 @@ export default {
actions
:
{
addAnnotations
,
clearAnnotations
,
createAnnotation
,
hideAnnotation
,
removeAnnotations
,
updateAnchorStatus
,
...
...
src/sidebar/store/modules/test/annotations-test.js
View file @
8142037e
import
*
as
fixtures
from
'../../../test/annotation-fixtures'
;
import
uiConstants
from
'../../../ui-constants'
;
import
*
as
metadata
from
'../../../util/annotation-metadata'
;
import
createStore
from
'../../create-store'
;
import
annotations
from
'../annotations'
;
import
{
$imports
}
from
'../annotations'
;
import
defaults
from
'../defaults'
;
import
drafts
from
'../drafts'
;
import
groups
from
'../groups'
;
import
selection
from
'../selection'
;
import
session
from
'../session'
;
import
viewer
from
'../viewer'
;
const
{
actions
,
selectors
}
=
annotations
;
function
createTestStore
()
{
return
createStore
(
[
annotations
,
selection
,
defaults
,
drafts
,
groups
,
session
,
viewer
],
[{}]
);
return
createStore
([
annotations
,
viewer
],
[{}]);
}
// Tests for most of the functionality in reducers/annotations.js are currently
// in the tests for the whole Redux store
describe
(
'sidebar/store/modules/annotations'
,
function
()
{
let
fakeDefaultPermissions
;
let
fakePrivatePermissions
;
beforeEach
(()
=>
{
fakeDefaultPermissions
=
sinon
.
stub
();
fakePrivatePermissions
=
sinon
.
stub
();
$imports
.
$mock
({
'../../util/permissions'
:
{
defaultPermissions
:
fakeDefaultPermissions
,
privatePermissions
:
fakePrivatePermissions
,
},
});
});
afterEach
(()
=>
{
$imports
.
$restore
();
});
describe
(
'#addAnnotations()'
,
function
()
{
const
ANCHOR_TIME_LIMIT
=
1000
;
let
clock
;
...
...
@@ -72,36 +44,6 @@ describe('sidebar/store/modules/annotations', function() {
]);
});
it
(
'does not change `selectedTab` state if annotations are already loaded'
,
function
()
{
const
annot
=
fixtures
.
defaultAnnotation
();
store
.
addAnnotations
([
annot
]);
const
page
=
fixtures
.
oldPageNote
();
store
.
addAnnotations
([
page
]);
assert
.
equal
(
store
.
getState
().
selection
.
selectedTab
,
uiConstants
.
TAB_ANNOTATIONS
);
});
it
(
'sets `selectedTab` to "note" if only page notes are present'
,
function
()
{
const
page
=
fixtures
.
oldPageNote
();
store
.
addAnnotations
([
page
]);
assert
.
equal
(
store
.
getState
().
selection
.
selectedTab
,
uiConstants
.
TAB_NOTES
);
});
it
(
'leaves `selectedTab` as "annotation" if annotations and/or page notes are present'
,
function
()
{
const
page
=
fixtures
.
oldPageNote
();
const
annot
=
fixtures
.
defaultAnnotation
();
store
.
addAnnotations
([
annot
,
page
]);
assert
.
equal
(
store
.
getState
().
selection
.
selectedTab
,
uiConstants
.
TAB_ANNOTATIONS
);
});
it
(
'assigns a local tag to annotations'
,
function
()
{
const
annotA
=
Object
.
assign
(
fixtures
.
defaultAnnotation
(),
{
id
:
'a1'
});
const
annotB
=
Object
.
assign
(
fixtures
.
defaultAnnotation
(),
{
id
:
'a2'
});
...
...
@@ -444,187 +386,4 @@ describe('sidebar/store/modules/annotations', function() {
});
});
});
describe
(
'#createAnnotation'
,
function
()
{
let
clock
;
let
now
;
let
store
;
beforeEach
(()
=>
{
// Stop the clock to keep the current date from advancing
clock
=
sinon
.
useFakeTimers
();
now
=
new
Date
();
store
=
createTestStore
();
});
afterEach
(()
=>
{
clock
.
restore
();
});
it
(
'should create an annotation'
,
function
()
{
const
ann
=
fixtures
.
oldAnnotation
();
store
.
dispatch
(
actions
.
createAnnotation
(
ann
));
assert
.
equal
(
selectors
.
findAnnotationByID
(
store
.
getState
(),
ann
.
id
).
id
,
ann
.
id
);
});
it
(
'should set basic default properties on a new/empty annotation'
,
()
=>
{
store
.
dispatch
(
actions
.
createAnnotation
({
id
:
'myID'
},
now
));
const
createdAnnotation
=
selectors
.
findAnnotationByID
(
store
.
getState
(),
'myID'
);
assert
.
include
(
createdAnnotation
,
{
created
:
now
.
toISOString
(),
updated
:
now
.
toISOString
(),
text
:
''
,
});
assert
.
isArray
(
createdAnnotation
.
tags
);
});
it
(
'should set user properties on a new/empty annotation'
,
()
=>
{
store
.
dispatch
(
actions
.
createAnnotation
({
id
:
'myID'
},
now
));
const
createdAnnotation
=
selectors
.
findAnnotationByID
(
store
.
getState
(),
'myID'
);
assert
.
equal
(
createdAnnotation
.
user
,
store
.
getState
().
session
.
userid
);
assert
.
equal
(
createdAnnotation
.
user_info
,
store
.
getState
().
session
.
user_info
);
});
it
(
'should set default permissions on a new annotation'
,
()
=>
{
fakeDefaultPermissions
.
returns
(
'somePermissions'
);
store
.
dispatch
(
actions
.
createAnnotation
({
id
:
'myID'
},
now
));
const
createdAnnotation
=
selectors
.
findAnnotationByID
(
store
.
getState
(),
'myID'
);
assert
.
equal
(
createdAnnotation
.
permissions
,
'somePermissions'
);
});
it
(
'should always assign private permissions to highlights'
,
()
=>
{
fakePrivatePermissions
.
returns
(
'private'
);
store
.
dispatch
(
actions
.
createAnnotation
({
id
:
'myID'
,
$highlight
:
true
},
now
)
);
const
createdAnnotation
=
selectors
.
findAnnotationByID
(
store
.
getState
(),
'myID'
);
assert
.
equal
(
createdAnnotation
.
permissions
,
'private'
);
});
it
(
'should set group to currently-focused group if not set on annotation'
,
()
=>
{
store
.
dispatch
(
actions
.
createAnnotation
({
id
:
'myID'
},
now
));
const
createdAnnotation
=
selectors
.
findAnnotationByID
(
store
.
getState
(),
'myID'
);
assert
.
equal
(
createdAnnotation
.
group
,
store
.
getState
().
groups
.
focusedGroupId
);
});
it
(
'should set not overwrite properties if present'
,
()
=>
{
store
.
dispatch
(
actions
.
createAnnotation
(
{
id
:
'myID'
,
created
:
'when'
,
updated
:
'then'
,
text
:
'my annotation'
,
tags
:
[
'foo'
,
'bar'
],
group
:
'fzzy'
,
permissions
:
[
'whatever'
],
user
:
'acct:foo@bar.com'
,
user_info
:
{
display_name
:
'Herbivore Fandango'
,
},
},
now
)
);
const
createdAnnotation
=
selectors
.
findAnnotationByID
(
store
.
getState
(),
'myID'
);
assert
.
include
(
createdAnnotation
,
{
created
:
'when'
,
updated
:
'then'
,
text
:
'my annotation'
,
group
:
'fzzy'
,
user
:
'acct:foo@bar.com'
,
});
assert
.
include
(
createdAnnotation
.
tags
,
'foo'
,
'bar'
);
assert
.
include
(
createdAnnotation
.
permissions
,
'whatever'
);
assert
.
equal
(
createdAnnotation
.
user_info
.
display_name
,
'Herbivore Fandango'
);
});
it
(
'should change tab focus to TAB_ANNOTATIONS when a new annotation is created'
,
function
()
{
store
.
dispatch
(
actions
.
createAnnotation
(
fixtures
.
oldAnnotation
()));
assert
.
equal
(
store
.
getState
().
selection
.
selectedTab
,
uiConstants
.
TAB_ANNOTATIONS
);
});
it
(
'should change tab focus to TAB_NOTES when a new note annotation is created'
,
function
()
{
store
.
dispatch
(
actions
.
createAnnotation
(
fixtures
.
oldPageNote
()));
assert
.
equal
(
store
.
getState
().
selection
.
selectedTab
,
uiConstants
.
TAB_NOTES
);
});
it
(
'should expand parent of created annotation'
,
function
()
{
const
store
=
createTestStore
();
store
.
dispatch
(
actions
.
addAnnotations
([
{
id
:
'annotation_id'
,
$highlight
:
undefined
,
target
:
[{
source
:
'source'
,
selector
:
[]
}],
references
:
[],
text
:
'This is my annotation'
,
tags
:
[
'tag_1'
,
'tag_2'
],
},
])
);
// Collapse the parent.
store
.
dispatch
(
selection
.
actions
.
setCollapsed
(
'annotation_id'
,
true
));
// Creating a new child annotation should expand its parent.
store
.
dispatch
(
actions
.
createAnnotation
({
highlight
:
undefined
,
target
:
[{
source
:
'http://example.org'
}],
references
:
[
'annotation_id'
],
text
:
''
,
tags
:
[],
})
);
assert
.
isTrue
(
store
.
getState
().
selection
.
expanded
.
annotation_id
);
});
});
});
src/sidebar/test/integration/threading-test.js
View file @
8142037e
...
...
@@ -57,6 +57,7 @@ describe('annotation threading', function() {
.
service
(
'store'
,
storeFactory
)
.
service
(
'rootThread'
,
rootThreadFactory
)
.
service
(
'searchFilter'
,
searchFilterFactory
)
.
service
(
'annotationsService'
,
()
=>
{})
.
service
(
'viewFilter'
,
viewFilterFactory
)
.
value
(
'features'
,
fakeFeatures
)
.
value
(
'settings'
,
{})
...
...
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