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
64cb045a
Unverified
Commit
64cb045a
authored
Jul 19, 2019
by
Hannah Stepanek
Committed by
GitHub
Jul 19, 2019
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1211 from hypothesis/move-load-anns-to-service
Move load annotations to service
parents
11ab71ea
3c9115cd
Changes
5
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
402 additions
and
434 deletions
+402
-434
sidebar-content.js
src/sidebar/components/sidebar-content.js
+19
-130
sidebar-content-test.js
src/sidebar/components/test/sidebar-content-test.js
+51
-304
index.js
src/sidebar/index.js
+1
-0
annotations.js
src/sidebar/services/annotations.js
+63
-0
annotations-test.js
src/sidebar/services/test/annotations-test.js
+268
-0
No files found.
src/sidebar/components/sidebar-content.js
View file @
64cb045a
'use strict'
;
const
SearchClient
=
require
(
'../search-client'
);
const
events
=
require
(
'../events'
);
const
isThirdPartyService
=
require
(
'../util/is-third-party-service'
);
const
tabs
=
require
(
'../tabs'
);
/**
* Returns the group ID of the first annotation in `results` whose
* ID is `annId`.
*/
function
getGroupID
(
annId
,
results
)
{
const
annot
=
results
.
find
(
function
(
annot
)
{
return
annot
.
id
===
annId
;
});
if
(
!
annot
)
{
return
null
;
}
return
annot
.
group
;
}
// @ngInject
function
SidebarContentController
(
$scope
,
analytics
,
annotations
,
store
,
annotationMapper
,
api
,
features
,
frameSync
,
groups
,
rootThread
,
settings
,
streamer
,
streamFilter
streamer
)
{
const
self
=
this
;
...
...
@@ -90,61 +71,7 @@ function SidebarContentController(
}
}
const
searchClients
=
[];
function
_resetAnnotations
()
{
annotationMapper
.
unloadAnnotations
(
store
.
savedAnnotations
());
}
function
_loadAnnotationsFor
(
uris
,
group
)
{
const
searchClient
=
new
SearchClient
(
api
.
search
,
{
// If no group is specified, we are fetching annotations from
// all groups in order to find out which group contains the selected
// annotation, therefore we need to load all chunks before processing
// the results
incremental
:
!!
group
,
});
searchClients
.
push
(
searchClient
);
searchClient
.
on
(
'results'
,
function
(
results
)
{
if
(
store
.
hasSelectedAnnotations
())
{
// Focus the group containing the selected annotation and filter
// annotations to those from this group
let
groupID
=
getGroupID
(
store
.
getFirstSelectedAnnotationId
(),
results
);
if
(
!
groupID
)
{
// If the selected annotation is not available, fall back to
// loading annotations for the currently focused group
groupID
=
groups
.
focused
().
id
;
}
results
=
results
.
filter
(
function
(
result
)
{
return
result
.
group
===
groupID
;
});
groups
.
focus
(
groupID
);
}
if
(
results
.
length
)
{
annotationMapper
.
loadAnnotations
(
results
);
}
});
searchClient
.
on
(
'end'
,
function
()
{
// Remove client from list of active search clients.
//
// $evalAsync is required here because search results are emitted
// asynchronously. A better solution would be that the loading state is
// tracked as part of the app state.
$scope
.
$evalAsync
(
function
()
{
searchClients
.
splice
(
searchClients
.
indexOf
(
searchClient
),
1
);
});
store
.
frames
().
forEach
(
function
(
frame
)
{
if
(
0
<=
uris
.
indexOf
(
frame
.
uri
))
{
store
.
updateFrameAnnotationFetchStatus
(
frame
.
uri
,
true
);
}
});
});
searchClient
.
get
({
uri
:
uris
,
group
:
group
});
}
this
.
isLoading
=
function
()
{
this
.
isLoading
=
()
=>
{
if
(
!
store
.
frames
().
some
(
function
(
frame
)
{
return
frame
.
uri
;
...
...
@@ -153,47 +80,9 @@ function SidebarContentController(
// The document's URL isn't known so the document must still be loading.
return
true
;
}
if
(
searchClients
.
length
>
0
)
{
// We're still waiting for annotation search results from the API.
return
true
;
}
return
false
;
return
store
.
isFetchingAnnotations
();
};
/**
* Load annotations for all URLs associated with `frames`.
*/
function
loadAnnotations
()
{
_resetAnnotations
();
searchClients
.
forEach
(
function
(
client
)
{
client
.
cancel
();
});
// If there is no selection, load annotations only for the focused group.
//
// If there is a selection, we load annotations for all groups, find out
// which group the first selected annotation is in and then filter the
// results on the client by that group.
//
// In the common case where the total number of annotations on
// a page that are visible to the user is not greater than
// the batch size, this saves an extra roundtrip to the server
// to fetch the selected annotation in order to determine which group
// it is in before fetching the remaining annotations.
const
group
=
store
.
hasSelectedAnnotations
()
?
null
:
groups
.
focused
().
id
;
const
searchUris
=
store
.
searchUris
();
if
(
searchUris
.
length
>
0
)
{
_loadAnnotationsFor
(
searchUris
,
group
);
streamFilter
.
resetFilter
().
addClause
(
'/uri'
,
'one_of'
,
searchUris
);
streamer
.
setConfig
(
'filter'
,
{
filter
:
streamFilter
.
getFilter
()
});
}
}
$scope
.
$on
(
'sidebarOpened'
,
function
()
{
analytics
.
track
(
analytics
.
events
.
SIDEBAR_OPENED
);
...
...
@@ -233,27 +122,25 @@ function SidebarContentController(
// Re-fetch annotations when focused group, logged-in user or connected frames
// change.
$scope
.
$watch
(
()
=>
[
groups
.
focused
(),
store
.
profile
().
userid
,
...
store
.
searchUris
()],
([
currentGroup
],
[
prevGroup
])
=>
{
if
(
!
currentGroup
)
{
// When switching accounts, groups are cleared and so the focused group
()
=>
[
store
.
focusedGroupId
(),
store
.
profile
().
userid
,
...
store
.
searchUris
(),
],
([
currentGroupId
],
[
prevGroupId
])
=>
{
if
(
!
currentGroupId
)
{
// When switching accounts, groups are cleared and so the focused group id
// will be null for a brief period of time.
store
.
clearSelectedAnnotations
();
return
;
}
if
(
!
prevGroup
||
currentGroup
.
id
!==
prevGroup
.
id
)
{
// The focused group may be changed during loading annotations as a result
// of switching to the group containing a direct-linked annotation.
//
// In that case, we don't want to trigger reloading annotations again.
if
(
this
.
isLoading
())
{
return
;
}
if
(
!
prevGroupId
||
currentGroupId
!==
prevGroupId
)
{
store
.
clearSelectedAnnotations
();
}
loadAnnotations
();
const
searchUris
=
store
.
searchUris
();
annotations
.
load
(
searchUris
,
currentGroupId
);
},
true
);
...
...
@@ -277,7 +164,7 @@ function SidebarContentController(
this
.
scrollTo
=
scrollToAnnotation
;
this
.
selectedGroupUnavailable
=
function
()
{
return
!
this
.
isLoading
()
&&
store
.
getState
().
directLinkedGroupFetchFailed
;
return
store
.
getState
().
directLinkedGroupFetchFailed
;
};
this
.
selectedAnnotationUnavailable
=
function
()
{
...
...
@@ -310,7 +197,9 @@ function SidebarContentController(
// selection is available to the user, show the CTA.
const
selectedID
=
store
.
getFirstSelectedAnnotationId
();
return
(
!
this
.
isLoading
()
&&
!!
selectedID
&&
store
.
annotationExists
(
selectedID
)
!
store
.
isFetchingAnnotations
()
&&
!!
selectedID
&&
store
.
annotationExists
(
selectedID
)
);
};
}
...
...
src/sidebar/components/test/sidebar-content-test.js
View file @
64cb045a
This diff is collapsed.
Click to expand it.
src/sidebar/index.js
View file @
64cb045a
...
...
@@ -207,6 +207,7 @@ function startAngularApp(config) {
.
service
(
'analytics'
,
require
(
'./services/analytics'
))
.
service
(
'annotationMapper'
,
require
(
'./services/annotation-mapper'
))
.
service
(
'annotations'
,
require
(
'./services/annotations'
))
.
service
(
'api'
,
require
(
'./services/api'
))
.
service
(
'apiRoutes'
,
require
(
'./services/api-routes'
))
.
service
(
'auth'
,
require
(
'./services/oauth-auth'
))
...
...
src/sidebar/services/annotations.js
0 → 100644
View file @
64cb045a
'use strict'
;
const
SearchClient
=
require
(
'../search-client'
);
// @ngInject
function
annotations
(
annotationMapper
,
api
,
store
,
streamer
,
streamFilter
)
{
let
searchClient
=
null
;
/**
* Load annotations for all URIs and groupId.
*
* @param {string[]} uris
* @param {string} groupId
*/
function
load
(
uris
,
groupId
)
{
annotationMapper
.
unloadAnnotations
(
store
.
savedAnnotations
());
// Cancel previously running search client.
if
(
searchClient
)
{
searchClient
.
cancel
();
}
if
(
uris
.
length
>
0
)
{
searchAndLoad
(
uris
,
groupId
);
streamFilter
.
resetFilter
().
addClause
(
'/uri'
,
'one_of'
,
uris
);
streamer
.
setConfig
(
'filter'
,
{
filter
:
streamFilter
.
getFilter
()
});
}
}
function
searchAndLoad
(
uris
,
groupId
)
{
searchClient
=
new
SearchClient
(
api
.
search
,
{
incremental
:
true
,
});
searchClient
.
on
(
'results'
,
results
=>
{
if
(
results
.
length
)
{
annotationMapper
.
loadAnnotations
(
results
);
}
});
searchClient
.
on
(
'error'
,
error
=>
{
console
.
error
(
error
);
});
searchClient
.
on
(
'end'
,
()
=>
{
// Remove client as it's no longer active.
searchClient
=
null
;
store
.
frames
().
forEach
(
function
(
frame
)
{
if
(
0
<=
uris
.
indexOf
(
frame
.
uri
))
{
store
.
updateFrameAnnotationFetchStatus
(
frame
.
uri
,
true
);
}
});
store
.
annotationFetchFinished
();
});
store
.
annotationFetchStarted
();
searchClient
.
get
({
uri
:
uris
,
group
:
groupId
});
}
return
{
load
,
};
}
module
.
exports
=
annotations
;
src/sidebar/services/test/annotations-test.js
0 → 100644
View file @
64cb045a
'use strict'
;
const
annotations
=
require
(
'../annotations'
);
const
EventEmitter
=
require
(
'tiny-emitter'
);
let
searchClients
;
let
longRunningSearchClient
=
false
;
class
FakeSearchClient
extends
EventEmitter
{
constructor
(
searchFn
,
opts
)
{
super
();
assert
.
ok
(
searchFn
);
searchClients
.
push
(
this
);
this
.
cancel
=
sinon
.
stub
();
this
.
incremental
=
!!
opts
.
incremental
;
this
.
get
=
sinon
.
spy
(
query
=>
{
assert
.
ok
(
query
.
uri
);
for
(
let
i
=
0
;
i
<
query
.
uri
.
length
;
i
++
)
{
const
uri
=
query
.
uri
[
i
];
this
.
emit
(
'results'
,
[{
id
:
uri
+
'123'
,
group
:
'__world__'
}]);
this
.
emit
(
'results'
,
[{
id
:
uri
+
'456'
,
group
:
'private-group'
}]);
}
if
(
!
longRunningSearchClient
)
{
this
.
emit
(
'end'
);
}
});
}
}
describe
(
'annotations'
,
()
=>
{
let
fakeStore
;
let
fakeApi
;
let
fakeAnnotationMapper
;
let
fakeStreamer
;
let
fakeStreamFilter
;
let
fakeUris
;
let
fakeGroupId
;
beforeEach
(()
=>
{
sinon
.
stub
(
console
,
'error'
);
searchClients
=
[];
longRunningSearchClient
=
false
;
fakeAnnotationMapper
=
{
loadAnnotations
:
sinon
.
stub
(),
unloadAnnotations
:
sinon
.
stub
(),
};
fakeApi
=
{
search
:
sinon
.
stub
(),
};
fakeStore
=
{
getState
:
sinon
.
stub
(),
frames
:
sinon
.
stub
(),
searchUris
:
sinon
.
stub
(),
savedAnnotations
:
sinon
.
stub
(),
hasSelectedAnnotations
:
sinon
.
stub
(),
updateFrameAnnotationFetchStatus
:
sinon
.
stub
(),
annotationFetchStarted
:
sinon
.
stub
(),
annotationFetchFinished
:
sinon
.
stub
(),
};
fakeStreamer
=
{
setConfig
:
sinon
.
stub
(),
connect
:
sinon
.
stub
(),
reconnect
:
sinon
.
stub
(),
};
fakeStreamFilter
=
{
resetFilter
:
sinon
.
stub
().
returns
({
addClause
:
sinon
.
stub
(),
}),
getFilter
:
sinon
.
stub
().
returns
({}),
};
fakeUris
=
[
'http://example.com'
];
fakeGroupId
=
'group-id'
;
annotations
.
$imports
.
$mock
({
'../search-client'
:
FakeSearchClient
,
});
});
afterEach
(()
=>
{
console
.
error
.
restore
();
annotations
.
$imports
.
$restore
();
});
function
service
()
{
fakeStore
.
frames
.
returns
(
fakeUris
.
map
(
uri
=>
{
return
{
uri
:
uri
};
})
);
return
annotations
(
fakeAnnotationMapper
,
fakeApi
,
fakeStore
,
fakeStreamer
,
fakeStreamFilter
);
}
describe
(
'load'
,
()
=>
{
it
(
'unloads any existing annotations'
,
()
=>
{
// When new clients connect, all existing annotations should be unloaded
// before reloading annotations for each currently-connected client.
fakeStore
.
savedAnnotations
.
returns
([
{
id
:
fakeUris
[
0
]
+
'123'
},
{
id
:
fakeUris
[
0
]
+
'456'
},
]);
const
svc
=
service
();
svc
.
load
(
fakeUris
,
fakeGroupId
);
assert
.
calledWith
(
fakeAnnotationMapper
.
unloadAnnotations
,
[
sinon
.
match
({
id
:
fakeUris
[
0
]
+
'123'
}),
sinon
.
match
({
id
:
fakeUris
[
0
]
+
'456'
}),
]);
});
it
(
'loads all annotations for a URI'
,
()
=>
{
const
svc
=
service
();
svc
.
load
(
fakeUris
,
fakeGroupId
);
assert
.
calledWith
(
fakeAnnotationMapper
.
loadAnnotations
,
[
sinon
.
match
({
id
:
fakeUris
[
0
]
+
'123'
}),
]);
assert
.
calledWith
(
fakeAnnotationMapper
.
loadAnnotations
,
[
sinon
.
match
({
id
:
fakeUris
[
0
]
+
'456'
}),
]);
});
it
(
'loads all annotations for a frame with multiple URIs'
,
()
=>
{
const
uri
=
'http://example.com/test.pdf'
;
const
fingerprint
=
'urn:x-pdf:fingerprint'
;
fakeUris
=
[
uri
,
fingerprint
];
const
svc
=
service
();
// Override the default frames set by the service call above.
fakeStore
.
frames
.
returns
([
{
uri
:
uri
,
metadata
:
{
documentFingerprint
:
'fingerprint'
,
link
:
[
{
href
:
fingerprint
,
},
{
href
:
uri
,
},
],
},
},
]);
svc
.
load
(
fakeUris
,
fakeGroupId
);
assert
.
calledWith
(
fakeAnnotationMapper
.
loadAnnotations
,
[
sinon
.
match
({
id
:
uri
+
'123'
}),
]);
assert
.
calledWith
(
fakeAnnotationMapper
.
loadAnnotations
,
[
sinon
.
match
({
id
:
fingerprint
+
'123'
}),
]);
assert
.
calledWith
(
fakeAnnotationMapper
.
loadAnnotations
,
[
sinon
.
match
({
id
:
uri
+
'456'
}),
]);
assert
.
calledWith
(
fakeAnnotationMapper
.
loadAnnotations
,
[
sinon
.
match
({
id
:
fingerprint
+
'456'
}),
]);
});
it
(
'loads all annotations for all URIs'
,
()
=>
{
fakeUris
=
[
'http://example.com'
,
'http://foobar.com'
];
const
svc
=
service
();
svc
.
load
(
fakeUris
,
fakeGroupId
);
[
fakeUris
[
0
]
+
'123'
,
fakeUris
[
0
]
+
'456'
,
fakeUris
[
1
]
+
'123'
,
fakeUris
[
1
]
+
'456'
,
].
forEach
(
uri
=>
{
assert
.
calledWith
(
fakeAnnotationMapper
.
loadAnnotations
,
[
sinon
.
match
({
id
:
uri
}),
]);
});
});
it
(
'updates annotation fetch status for all frames'
,
()
=>
{
fakeUris
=
[
'http://example.com'
,
'http://foobar.com'
];
const
svc
=
service
();
svc
.
load
(
fakeUris
,
fakeGroupId
);
assert
.
calledWith
(
fakeStore
.
updateFrameAnnotationFetchStatus
,
fakeUris
[
0
],
true
);
assert
.
calledWith
(
fakeStore
.
updateFrameAnnotationFetchStatus
,
fakeUris
[
1
],
true
);
});
it
(
'fetches annotations for the specified group'
,
()
=>
{
const
svc
=
service
();
svc
.
load
(
fakeUris
,
fakeGroupId
);
assert
.
calledWith
(
searchClients
[
0
].
get
,
{
uri
:
fakeUris
,
group
:
fakeGroupId
,
});
});
it
(
'loads annotations in batches'
,
()
=>
{
const
svc
=
service
();
svc
.
load
(
fakeUris
,
fakeGroupId
);
assert
.
ok
(
searchClients
[
0
].
incremental
);
});
it
(
"cancels previously search client if it's still running"
,
()
=>
{
const
svc
=
service
();
// Issue a long running load annotations request.
longRunningSearchClient
=
true
;
svc
.
load
(
fakeUris
,
fakeGroupId
);
// Issue another load annotations request while the
// previous annotation load is still running.
svc
.
load
(
fakeUris
,
fakeGroupId
);
assert
.
calledOnce
(
searchClients
[
0
].
cancel
);
});
it
(
'does not load annotations if URIs list is empty'
,
()
=>
{
fakeUris
=
[];
const
svc
=
service
();
svc
.
load
(
fakeUris
,
fakeGroupId
);
assert
.
notCalled
(
fakeAnnotationMapper
.
loadAnnotations
);
});
it
(
'calls annotationFetchStarted when it starts searching for annotations'
,
()
=>
{
const
svc
=
service
();
svc
.
load
(
fakeUris
,
fakeGroupId
);
assert
.
calledOnce
(
fakeStore
.
annotationFetchStarted
);
});
it
(
'calls annotationFetchFinished when all annotations have been found'
,
()
=>
{
const
svc
=
service
();
svc
.
load
(
fakeUris
,
fakeGroupId
);
assert
.
calledOnce
(
fakeStore
.
annotationFetchFinished
);
});
it
(
'logs an error to the console if the search client runs into an error'
,
()
=>
{
const
svc
=
service
();
const
error
=
new
Error
(
'search for annotations failed'
);
svc
.
load
(
fakeUris
,
fakeGroupId
);
searchClients
[
0
].
emit
(
'error'
,
error
);
assert
.
calledWith
(
console
.
error
,
error
);
});
});
});
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