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
32b086a8
Unverified
Commit
32b086a8
authored
Apr 23, 2020
by
Robert Knight
Committed by
GitHub
Apr 23, 2020
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #2074 from hypothesis/convert-annotation-viewer-content
Convert single annotation page content to Preact
parents
8a0c0852
4feb9fee
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
244 additions
and
177 deletions
+244
-177
annotation-viewer-content.js
src/sidebar/components/annotation-viewer-content.js
+116
-61
annotation-viewer-content-test.js
...sidebar/components/test/annotation-viewer-content-test.js
+123
-108
index.js
src/sidebar/index.js
+5
-2
annotation-viewer-content.html
src/sidebar/templates/annotation-viewer-content.html
+0
-6
No files found.
src/sidebar/components/annotation-viewer-content.js
View file @
32b086a8
import
{
createElement
}
from
'preact'
;
import
{
useEffect
}
from
'preact/hooks'
;
import
propTypes
from
'prop-types'
;
import
useStore
from
'../store/use-store'
;
import
{
withServices
}
from
'../util/service-context'
;
import
ThreadList
from
'./thread-list'
;
/**
/**
* Fetch all annotations in the same thread as `id`.
* The main content for the single annotation page (aka. https://hypothes.is/a/<annotation ID>)
*
* @return Promise<Array<Annotation>>
*/
*/
function
fetchThread
(
api
,
id
)
{
function
AnnotationViewerContent
({
let
annot
;
return
api
.
annotation
.
get
({
id
:
id
})
.
then
(
function
(
annot
)
{
if
(
annot
.
references
&&
annot
.
references
.
length
)
{
// This is a reply, fetch the top-level annotation
return
api
.
annotation
.
get
({
id
:
annot
.
references
[
0
]
});
}
else
{
return
annot
;
}
})
.
then
(
function
(
annot_
)
{
annot
=
annot_
;
return
api
.
search
({
references
:
annot
.
id
});
})
.
then
(
function
(
searchResult
)
{
return
[
annot
].
concat
(
searchResult
.
rows
);
});
}
// @ngInject
function
AnnotationViewerContentController
(
store
,
api
,
api
,
rootThread
,
rootThread
:
rootThreadService
,
streamer
,
streamer
,
streamFilter
streamFilter
,
)
{
})
{
store
.
clearAnnotations
();
const
addAnnotations
=
useStore
(
store
=>
store
.
addAnnotations
);
const
annotationId
=
useStore
(
store
=>
store
.
routeParams
().
id
);
const
annotationId
=
store
.
routeParams
().
id
;
const
clearAnnotations
=
useStore
(
store
=>
store
.
clearAnnotations
);
const
highlightAnnotations
=
useStore
(
store
=>
store
.
highlightAnnotations
);
const
rootThread
=
useStore
(
store
=>
rootThreadService
.
thread
(
store
.
getState
())
);
const
setCollapsed
=
useStore
(
store
=>
store
.
setCollapsed
);
this
.
rootThread
=
()
=>
rootThread
.
thread
(
store
.
getState
());
useEffect
(()
=>
{
clearAnnotations
();
this
.
setCollapsed
=
function
(
id
,
collapsed
)
{
// TODO - Handle exceptions during the `fetchThread` call.
store
.
setCollapsed
(
id
,
collapsed
);
fetchThread
(
api
,
annotationId
).
then
(
annots
=>
{
}
;
addAnnotations
(
annots
)
;
this
.
ready
=
fetchThread
(
api
,
annotationId
).
then
(
function
(
annots
)
{
// Find the top-level annotation in the thread that `annotationId` is
store
.
addAnnotations
(
annots
);
// part of. This will be different to `annotationId` if `annotationId`
// is a reply.
const
topLevelAnnot
=
annots
.
filter
(
ann
=>
(
ann
.
references
||
[]).
length
===
0
)[
0
];
const
topLevelAnnot
=
annots
.
filter
(
function
(
annot
)
{
if
(
!
topLevelAnnot
)
{
return
(
annot
.
references
||
[]).
length
===
0
;
// We were able to fetch annotations in the thread that `annotationId`
})[
0
];
// is part of (note that `annotationId` may refer to a reply) but
// couldn't find a top-level (non-reply) annotation in that thread.
//
// This might happen if the top-level annotation was deleted or
// moderated or had its permissions changed.
//
// We need to decide what what be the most useful behavior in this case
// and implement it.
/* istanbul ignore next */
return
;
}
if
(
!
topLevelAnnot
)
{
// Configure the connection to the real-time update service to send us
return
;
// updates to any of the annotations in the thread.
}
streamFilter
.
addClause
(
'/references'
,
'one_of'
,
topLevelAnnot
.
id
,
true
)
.
addClause
(
'/id'
,
'equals'
,
topLevelAnnot
.
id
,
true
);
streamer
.
setConfig
(
'filter'
,
{
filter
:
streamFilter
.
getFilter
()
});
streamer
.
connect
();
streamFilter
// Make the full thread of annotations visible. By default replies are
.
addClause
(
'/references'
,
'one_of'
,
topLevelAnnot
.
id
,
true
)
// not shown until the user expands the thread.
.
addClause
(
'/id'
,
'equals'
,
topLevelAnnot
.
id
,
true
);
annots
.
forEach
(
annot
=>
setCollapsed
(
annot
.
id
,
false
));
streamer
.
setConfig
(
'filter'
,
{
filter
:
streamFilter
.
getFilter
()
});
streamer
.
connect
();
annots
.
forEach
(
function
(
annot
)
{
// FIXME - This should show a visual indication of which reply the
store
.
setCollapsed
(
annot
.
id
,
false
);
// annotation ID in the URL refers to. That isn't currently working.
if
(
topLevelAnnot
.
id
!==
annotationId
)
{
highlightAnnotations
([
annotationId
]);
}
});
});
},
[
annotationId
,
// Static dependencies.
addAnnotations
,
api
,
clearAnnotations
,
highlightAnnotations
,
setCollapsed
,
streamFilter
,
streamer
,
]);
if
(
topLevelAnnot
.
id
!==
annotationId
)
{
return
<
ThreadList
thread
=
{
rootThread
}
/>
;
store
.
highlightAnnotations
([
annotationId
]);
}
});
}
}
export
default
{
AnnotationViewerContent
.
propTypes
=
{
controller
:
AnnotationViewerContentController
,
// Injected.
controllerAs
:
'vm'
,
api
:
propTypes
.
object
,
bindings
:
{},
rootThread
:
propTypes
.
object
,
template
:
require
(
'../templates/annotation-viewer-content.html'
),
streamer
:
propTypes
.
object
,
streamFilter
:
propTypes
.
object
,
};
};
AnnotationViewerContent
.
injectedProps
=
[
'api'
,
'rootThread'
,
'streamer'
,
'streamFilter'
,
];
// NOTE: The function below is intentionally at the bottom of the file.
//
// Putting it at the top resulted in an issue where the `createElement` import
// wasn't correctly referenced in the body of `AnnotationViewerContent` in
// the compiled JS, causing a runtime error.
/**
* Fetch all annotations in the same thread as `id`.
*
* @param {Object} api - API client
* @param {string} id - Annotation ID. This may be an annotation or a reply.
* @return Promise<Annotation[]> - The annotation, followed by any replies.
*/
async
function
fetchThread
(
api
,
id
)
{
let
annot
=
await
api
.
annotation
.
get
({
id
});
if
(
annot
.
references
&&
annot
.
references
.
length
)
{
// This is a reply, fetch the top-level annotation
annot
=
await
api
.
annotation
.
get
({
id
:
annot
.
references
[
0
]
});
}
// Fetch all replies to the top-level annotation.
const
replySearchResult
=
await
api
.
search
({
references
:
annot
.
id
});
return
[
annot
,
...
replySearchResult
.
rows
];
}
export
default
withServices
(
AnnotationViewerContent
);
src/sidebar/components/test/annotation-viewer-content-test.js
View file @
32b086a8
import
angular
from
'angular'
;
import
{
createElement
}
from
'preact'
;
import
{
mount
}
from
'enzyme'
;
import
annotationViewerContent
from
'../annotation-viewer-content'
;
import
{
waitFor
}
from
'../../../test-util/wait'
;
// Fake implementation of the API for fetching annotations and replies to
import
mockImportedComponents
from
'../../../test-util/mock-imported-components'
;
// annotations.
function
FakeApi
(
annots
)
{
import
AnnotationViewerContent
,
{
this
.
annots
=
annots
;
$imports
,
}
from
'../annotation-viewer-content'
;
this
.
annotation
=
{
get
:
function
(
query
)
{
/**
let
result
;
* Fake implementation of the `api` service.
if
(
query
.
id
)
{
*/
result
=
annots
.
find
(
function
(
a
)
{
class
FakeApi
{
return
a
.
id
===
query
.
id
;
constructor
(
annots
)
{
});
this
.
annotations
=
annots
;
}
return
Promise
.
resolve
(
result
);
this
.
annotation
=
{
},
get
:
async
query
=>
this
.
annotations
.
find
(
a
=>
a
.
id
===
query
.
id
),
};
};
}
this
.
search
=
function
(
query
)
{
let
result
;
async
search
(
query
)
{
let
matches
=
[];
if
(
query
.
references
)
{
if
(
query
.
references
)
{
result
=
annots
.
filter
(
function
(
a
)
{
matches
=
this
.
annotations
.
filter
(
return
a
.
references
&&
a
.
references
.
indexOf
(
query
.
references
)
!==
-
1
;
a
=>
a
.
references
&&
a
.
references
.
includes
(
query
.
references
)
}
);
);
}
}
return
Promise
.
resolve
({
rows
:
result
})
;
return
{
rows
:
matches
}
;
}
;
}
}
}
describe
(
'annotationViewerContent'
,
function
()
{
describe
(
'AnnotationViewerContent'
,
()
=>
{
before
(
function
()
{
let
fakeStore
;
angular
let
fakeRootThread
;
.
module
(
'h'
,
[])
let
fakeStreamer
;
.
component
(
'annotationViewerContent'
,
annotationViewerContent
);
let
fakeStreamFilter
;
});
beforeEach
(
angular
.
mock
.
module
(
'h'
));
beforeEach
(()
=>
{
fakeStore
=
{
function
createController
(
opts
)
{
addAnnotations
:
sinon
.
stub
(),
const
locals
=
{
clearAnnotations
:
sinon
.
stub
(),
store
:
{
getState
:
sinon
.
stub
().
returns
({}),
addAnnotations
:
sinon
.
stub
(),
highlightAnnotations
:
sinon
.
stub
(),
clearAnnotations
:
sinon
.
stub
(),
routeParams
:
sinon
.
stub
().
returns
({
id
:
'test_annotation_id'
}),
setCollapsed
:
sinon
.
stub
(),
setCollapsed
:
sinon
.
stub
(),
highlightAnnotations
:
sinon
.
stub
(),
};
routeParams
:
sinon
.
stub
().
returns
({
id
:
'test_annotation_id'
}),
subscribe
:
sinon
.
stub
(),
fakeRootThread
=
{
thread
:
sinon
.
stub
().
returns
({})
};
},
api
:
opts
.
api
,
fakeStreamer
=
{
rootThread
:
{
thread
:
sinon
.
stub
()
},
setConfig
:
()
=>
{},
streamer
:
{
connect
:
()
=>
{},
setConfig
:
function
()
{},
};
connect
:
function
()
{},
},
fakeStreamFilter
=
{
streamFilter
:
{
addClause
:
()
=>
{
addClause
:
function
()
{
return
{
return
{
addClause
:
()
=>
{},
addClause
:
function
()
{},
};
};
},
getFilter
:
function
()
{},
},
},
getFilter
:
()
=>
{},
};
};
let
$componentController
;
$imports
.
$mock
(
mockImportedComponents
());
angular
.
mock
.
inject
(
function
(
_$componentController_
)
{
$imports
.
$mock
({
$componentController
=
_$componentController_
;
'../store/use-store'
:
callback
=>
callback
(
fakeStore
),
});
locals
.
ctrl
=
$componentController
(
'annotationViewerContent'
,
locals
,
{
search
:
{},
});
});
return
locals
;
});
afterEach
(()
=>
{
$imports
.
$restore
();
});
function
createComponent
({
api
})
{
return
mount
(
<
AnnotationViewerContent
api
=
{
api
}
rootThread
=
{
fakeRootThread
}
streamer
=
{
fakeStreamer
}
streamFilter
=
{
fakeStreamFilter
}
/
>
);
}
}
describe
(
'the standalone view for a top-level annotation'
,
function
()
{
function
waitForAnnotationsToLoad
()
{
it
(
'loads the annotation and all replies'
,
function
()
{
return
waitFor
(()
=>
fakeStore
.
addAnnotations
.
called
);
}
describe
(
'the standalone view for a top-level annotation'
,
()
=>
{
it
(
'loads the annotation and all replies'
,
async
()
=>
{
const
fakeApi
=
new
FakeApi
([
const
fakeApi
=
new
FakeApi
([
{
id
:
'test_annotation_id'
},
{
id
:
'test_annotation_id'
},
{
id
:
'test_reply_id'
,
references
:
[
'test_annotation_id'
]
},
{
id
:
'test_reply_id'
,
references
:
[
'test_annotation_id'
]
},
]);
]);
const
controller
=
createController
({
api
:
fakeApi
});
createComponent
({
api
:
fakeApi
});
return
controller
.
ctrl
.
ready
.
then
(
function
()
{
assert
.
calledOnce
(
controller
.
store
.
addAnnotations
);
await
waitForAnnotationsToLoad
();
assert
.
calledWith
(
controller
.
store
.
addAnnotations
,
assert
.
calledOnce
(
fakeStore
.
addAnnotations
);
sinon
.
match
(
fakeApi
.
annots
)
assert
.
calledWith
(
);
fakeStore
.
addAnnotations
,
});
sinon
.
match
(
fakeApi
.
annotations
)
);
});
});
it
(
'does not highlight any annotations'
,
function
()
{
it
(
'does not highlight any annotations'
,
async
()
=>
{
const
fakeApi
=
new
FakeApi
([
const
fakeApi
=
new
FakeApi
([
{
id
:
'test_annotation_id'
},
{
id
:
'test_annotation_id'
},
{
id
:
'test_reply_id'
,
references
:
[
'test_annotation_id'
]
},
{
id
:
'test_reply_id'
,
references
:
[
'test_annotation_id'
]
},
]);
]);
const
controller
=
createController
({
api
:
fakeApi
});
createComponent
({
api
:
fakeApi
});
return
controller
.
ctrl
.
ready
.
then
(
function
()
{
assert
.
notCalled
(
controller
.
store
.
highlightAnnotations
);
await
waitForAnnotationsToLoad
();
});
assert
.
notCalled
(
fakeStore
.
highlightAnnotations
);
});
});
});
});
describe
(
'the standalone view for a reply'
,
function
()
{
describe
(
'the standalone view for a reply'
,
()
=>
{
it
(
'loads the top-level annotation and all replies'
,
function
()
{
it
(
'loads the top-level annotation and all replies'
,
async
()
=>
{
const
fakeApi
=
new
FakeApi
([
const
fakeApi
=
new
FakeApi
([
{
id
:
'parent_id'
},
{
id
:
'parent_id'
},
{
id
:
'test_annotation_id'
,
references
:
[
'parent_id'
]
},
{
id
:
'test_annotation_id'
,
references
:
[
'parent_id'
]
},
]);
]);
const
controller
=
createController
({
api
:
fakeApi
});
createComponent
({
api
:
fakeApi
});
return
controller
.
ctrl
.
ready
.
then
(
function
()
{
assert
.
calledWith
(
await
waitForAnnotationsToLoad
();
controller
.
store
.
addAnnotations
,
sinon
.
match
(
fakeApi
.
annots
)
assert
.
calledWith
(
);
fakeStore
.
addAnnotations
,
});
sinon
.
match
(
fakeApi
.
annotations
)
);
});
});
it
(
'expands the thread'
,
function
()
{
it
(
'expands the thread'
,
async
()
=>
{
const
fakeApi
=
new
FakeApi
([
const
fakeApi
=
new
FakeApi
([
{
id
:
'parent_id'
},
{
id
:
'parent_id'
},
{
id
:
'test_annotation_id'
,
references
:
[
'parent_id'
]
},
{
id
:
'test_annotation_id'
,
references
:
[
'parent_id'
]
},
]);
]);
const
controller
=
createController
({
api
:
fakeApi
});
return
controller
.
ctrl
.
ready
.
then
(
function
()
{
createComponent
({
api
:
fakeApi
});
assert
.
calledWith
(
controller
.
store
.
setCollapsed
,
'parent_id'
,
false
);
assert
.
calledWith
(
await
waitForAnnotationsToLoad
();
controller
.
store
.
setCollapsed
,
'test_annotation_id'
,
assert
.
calledWith
(
fakeStore
.
setCollapsed
,
'parent_id'
,
false
);
false
assert
.
calledWith
(
fakeStore
.
setCollapsed
,
'test_annotation_id'
,
false
);
);
});
});
});
it
(
'highlights the reply'
,
function
()
{
it
(
'highlights the reply'
,
async
()
=>
{
const
fakeApi
=
new
FakeApi
([
const
fakeApi
=
new
FakeApi
([
{
id
:
'parent_id'
},
{
id
:
'parent_id'
},
{
id
:
'test_annotation_id'
,
references
:
[
'parent_id'
]
},
{
id
:
'test_annotation_id'
,
references
:
[
'parent_id'
]
},
]);
]);
const
controller
=
createController
({
api
:
fakeApi
});
createComponent
({
api
:
fakeApi
});
return
controller
.
ctrl
.
ready
.
then
(
function
()
{
assert
.
calledWith
(
await
waitForAnnotationsToLoad
();
controller
.
store
.
highlightAnnotations
,
sinon
.
match
([
'test_annotation_id'
])
assert
.
calledWith
(
);
fakeStore
.
highlightAnnotations
,
});
sinon
.
match
([
'test_annotation_id'
])
);
});
});
});
});
});
});
src/sidebar/index.js
View file @
32b086a8
...
@@ -119,6 +119,7 @@ registerIcons(iconSet);
...
@@ -119,6 +119,7 @@ registerIcons(iconSet);
// Preact UI components that are wrapped for use within Angular templates.
// Preact UI components that are wrapped for use within Angular templates.
import
Annotation
from
'./components/annotation'
;
import
Annotation
from
'./components/annotation'
;
import
AnnotationViewerContent
from
'./components/annotation-viewer-content'
;
import
FocusedModeHeader
from
'./components/focused-mode-header'
;
import
FocusedModeHeader
from
'./components/focused-mode-header'
;
import
HelpPanel
from
'./components/help-panel'
;
import
HelpPanel
from
'./components/help-panel'
;
import
LoggedOutMessage
from
'./components/logged-out-message'
;
import
LoggedOutMessage
from
'./components/logged-out-message'
;
...
@@ -136,7 +137,6 @@ import TopBar from './components/top-bar';
...
@@ -136,7 +137,6 @@ import TopBar from './components/top-bar';
// Remaining UI components that are still built with Angular.
// Remaining UI components that are still built with Angular.
import
annotationViewerContent
from
'./components/annotation-viewer-content'
;
import
hypothesisApp
from
'./components/hypothesis-app'
;
import
hypothesisApp
from
'./components/hypothesis-app'
;
import
sidebarContent
from
'./components/sidebar-content'
;
import
sidebarContent
from
'./components/sidebar-content'
;
import
streamContent
from
'./components/stream-content'
;
import
streamContent
from
'./components/stream-content'
;
...
@@ -257,7 +257,10 @@ function startAngularApp(config) {
...
@@ -257,7 +257,10 @@ function startAngularApp(config) {
// UI components
// UI components
.
component
(
'annotation'
,
wrapComponent
(
Annotation
))
.
component
(
'annotation'
,
wrapComponent
(
Annotation
))
.
component
(
'annotationViewerContent'
,
annotationViewerContent
)
.
component
(
'annotationViewerContent'
,
wrapComponent
(
AnnotationViewerContent
)
)
.
component
(
'helpPanel'
,
wrapComponent
(
HelpPanel
))
.
component
(
'helpPanel'
,
wrapComponent
(
HelpPanel
))
.
component
(
'loginPromptPanel'
,
wrapComponent
(
LoginPromptPanel
))
.
component
(
'loginPromptPanel'
,
wrapComponent
(
LoginPromptPanel
))
.
component
(
'loggedOutMessage'
,
wrapComponent
(
LoggedOutMessage
))
.
component
(
'loggedOutMessage'
,
wrapComponent
(
LoggedOutMessage
))
...
...
src/sidebar/templates/annotation-viewer-content.html
deleted
100644 → 0
View file @
8a0c0852
<thread-list
on-change-collapsed=
"vm.setCollapsed(id, collapsed)"
on-force-visible=
"vm.forceVisible(thread)"
show-document-info=
"true"
thread=
"vm.rootThread()"
>
</thread-list>
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