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
d6c00fd0
Commit
d6c00fd0
authored
Jul 09, 2019
by
Robert Knight
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' into move-pending-update-state-to-store
Adapt to conversion of `<top-bar>` component to Preact.
parents
343237bb
e080b8b9
Changes
48
Hide whitespace changes
Inline
Side-by-side
Showing
48 changed files
with
1453 additions
and
1109 deletions
+1453
-1109
.eslintrc
.eslintrc
+3
-9
lock.svg
src/images/icons/lock.svg
+1
-1
annotation-document-info.js
src/sidebar/components/annotation-document-info.js
+44
-0
annotation-header.js
src/sidebar/components/annotation-header.js
+76
-94
annotation-share-info.js
src/sidebar/components/annotation-share-info.js
+67
-0
annotation-user.js
src/sidebar/components/annotation-user.js
+19
-13
annotation-viewer-content.js
src/sidebar/components/annotation-viewer-content.js
+1
-10
hypothesis-app.js
src/sidebar/components/hypothesis-app.js
+0
-10
login-control.js
src/sidebar/components/login-control.js
+0
-28
moderation-banner.js
src/sidebar/components/moderation-banner.js
+78
-39
search-input.js
src/sidebar/components/search-input.js
+3
-5
stream-content.js
src/sidebar/components/stream-content.js
+5
-9
stream-search-input.js
src/sidebar/components/stream-search-input.js
+42
-0
annotation-document-info-test.js
src/sidebar/components/test/annotation-document-info-test.js
+76
-0
annotation-header-test.js
src/sidebar/components/test/annotation-header-test.js
+111
-238
annotation-share-info-test.js
src/sidebar/components/test/annotation-share-info-test.js
+138
-0
annotation-thread-test.js
src/sidebar/components/test/annotation-thread-test.js
+2
-4
hypothesis-app-test.js
src/sidebar/components/test/hypothesis-app-test.js
+0
-6
login-control-test.js
src/sidebar/components/test/login-control-test.js
+0
-84
moderation-banner-test.js
src/sidebar/components/test/moderation-banner-test.js
+78
-67
search-input-test.js
src/sidebar/components/test/search-input-test.js
+1
-1
stream-content-test.js
src/sidebar/components/test/stream-content-test.js
+9
-10
stream-search-input-test.js
src/sidebar/components/test/stream-search-input-test.js
+66
-0
top-bar-test.js
src/sidebar/components/test/top-bar-test.js
+133
-112
top-bar.js
src/sidebar/components/top-bar.js
+169
-29
index.js
src/sidebar/index.js
+10
-24
search-client.js
src/sidebar/search-client.js
+0
-5
activity.js
src/sidebar/store/modules/activity.js
+48
-0
selection.js
src/sidebar/store/modules/selection.js
+5
-0
activity-test.js
src/sidebar/store/modules/test/activity-test.js
+57
-0
annotation-header.html
src/sidebar/templates/annotation-header.html
+0
-42
annotation-thread.html
src/sidebar/templates/annotation-thread.html
+3
-2
annotation.html
src/sidebar/templates/annotation.html
+6
-6
hypothesis-app.html
src/sidebar/templates/hypothesis-app.html
+1
-2
login-control.html
src/sidebar/templates/login-control.html
+0
-10
moderation-banner.html
src/sidebar/templates/moderation-banner.html
+0
-23
top-bar.html
src/sidebar/templates/top-bar.html
+0
-72
search-client-test.js
src/sidebar/test/search-client-test.js
+27
-16
annotation-document-info.scss
src/styles/sidebar/components/annotation-document-info.scss
+14
-0
annotation-header.scss
src/styles/sidebar/components/annotation-header.scss
+25
-0
annotation-share-info.scss
src/styles/sidebar/components/annotation-share-info.scss
+26
-0
annotation-user.scss
src/styles/sidebar/components/annotation-user.scss
+11
-10
annotation.scss
src/styles/sidebar/components/annotation.scss
+1
-32
group-list.scss
src/styles/sidebar/components/group-list.scss
+5
-0
login-control.scss
src/styles/sidebar/components/login-control.scss
+0
-8
top-bar.scss
src/styles/sidebar/components/top-bar.scss
+6
-0
sidebar.scss
src/styles/sidebar/sidebar.scss
+3
-1
yarn.lock
yarn.lock
+83
-87
No files found.
.eslintrc
View file @
d6c00fd0
{
"extends": [
"hypothesis",
"plugin:react/recommended"
],
"extends": ["hypothesis", "plugin:react/recommended"],
"globals": {
"Set": false
},
...
...
@@ -10,6 +7,7 @@
"mocha/no-exclusive-tests": "error",
"no-var": "error",
"indent": "off",
"react/self-closing-comp": "error",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error"
},
...
...
@@ -19,11 +17,7 @@
"jsx": true
}
},
"plugins": [
"mocha",
"react",
"react-hooks"
],
"plugins": ["mocha", "react", "react-hooks"],
"settings": {
"react": {
"pragma": "createElement",
...
...
src/images/icons/lock.svg
View file @
d6c00fd0
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width=
"48px"
height=
"56px"
viewBox=
"0 0 48 56"
version=
"1.1"
xmlns=
"http://www.w3.org/2000/svg"
xmlns:xlink=
"http://www.w3.org/1999/xlink"
>
<!-- Generator: Sketch 3.6.1 (26313) - http://www.bohemiancoding.com/sketch -->
<title>
Group 4 Copy 3
</title>
<title>
Only me
</title>
<desc>
Created with Sketch.
</desc>
<defs></defs>
<g
id=
"Page-1"
stroke=
"none"
stroke-width=
"1"
fill=
"none"
fill-rule=
"evenodd"
>
...
...
src/sidebar/components/annotation-document-info.js
0 → 100644
View file @
d6c00fd0
'use strict'
;
const
propTypes
=
require
(
'prop-types'
);
const
{
createElement
}
=
require
(
'preact'
);
const
annotationMetadata
=
require
(
'../annotation-metadata'
);
/**
* Render some metadata about an annotation's document and link to it
* if a link is available.
*/
function
AnnotationDocumentInfo
({
annotation
})
{
const
documentInfo
=
annotationMetadata
.
domainAndTitle
(
annotation
);
// If there's no document title, nothing to do here
if
(
!
documentInfo
.
titleText
)
{
return
null
;
}
return
(
<
div
className
=
"annotation-document-info"
>
<
div
className
=
"annotation-document-info__title"
>
on
&
quot
;
{
documentInfo
.
titleLink
?
(
<
a
href
=
{
documentInfo
.
titleLink
}
>
{
documentInfo
.
titleText
}
<
/a
>
)
:
(
<
span
>
{
documentInfo
.
titleText
}
<
/span
>
)}
&
quot
;
<
/div
>
{
documentInfo
.
domain
&&
(
<
div
className
=
"annotation-document-info__domain"
>
({
documentInfo
.
domain
})
<
/div
>
)}
<
/div
>
);
}
AnnotationDocumentInfo
.
propTypes
=
{
/* Annotation for which the document metadata will be rendered */
annotation
:
propTypes
.
object
.
isRequired
,
};
module
.
exports
=
AnnotationDocumentInfo
;
src/sidebar/components/annotation-header.js
View file @
d6c00fd0
'use strict'
;
const
annotationMetadata
=
require
(
'../annotation-metadata'
);
const
memoize
=
require
(
'../util/memoize'
);
const
{
isThirdPartyUser
,
username
}
=
require
(
'../util/account-id'
);
const
propTypes
=
require
(
'prop-types'
);
const
{
createElement
}
=
require
(
'preact'
);
// @ngInject
function
AnnotationHeaderController
(
features
,
groups
,
settings
,
serviceUrl
)
{
const
self
=
this
;
this
.
user
=
function
()
{
return
self
.
annotation
.
user
;
};
this
.
displayName
=
()
=>
{
const
userInfo
=
this
.
annotation
.
user_info
;
const
isThirdPartyUser_
=
isThirdPartyUser
(
this
.
annotation
.
user
,
settings
.
authDomain
);
if
(
features
.
flagEnabled
(
'client_display_names'
)
||
isThirdPartyUser_
)
{
// userInfo is undefined if the api_render_user_info feature flag is off.
if
(
userInfo
)
{
// display_name is null if the user doesn't have a display name.
if
(
userInfo
.
display_name
)
{
return
userInfo
.
display_name
;
}
}
}
return
username
(
this
.
annotation
.
user
);
};
this
.
isThirdPartyUser
=
function
()
{
return
isThirdPartyUser
(
self
.
annotation
.
user
,
settings
.
authDomain
);
};
this
.
thirdPartyUsernameLink
=
function
()
{
return
settings
.
usernameUrl
?
settings
.
usernameUrl
+
username
(
this
.
annotation
.
user
)
:
null
;
};
this
.
serviceUrl
=
serviceUrl
;
this
.
group
=
function
()
{
return
groups
.
get
(
self
.
annotation
.
group
);
};
const
documentMeta
=
memoize
(
annotationMetadata
.
domainAndTitle
);
this
.
documentMeta
=
function
()
{
return
documentMeta
(
self
.
annotation
);
};
this
.
updated
=
function
()
{
return
self
.
annotation
.
updated
;
};
this
.
htmlLink
=
function
()
{
if
(
self
.
annotation
.
links
&&
self
.
annotation
.
links
.
html
)
{
return
self
.
annotation
.
links
.
html
;
}
return
''
;
};
}
const
AnnotationDocumentInfo
=
require
(
'./annotation-document-info'
);
const
AnnotationShareInfo
=
require
(
'./annotation-share-info'
);
const
AnnotationUser
=
require
(
'./annotation-user'
);
const
Timestamp
=
require
(
'./timestamp'
);
/**
* Header component for an annotation card.
*
* Header which displays the username, last update timestamp and other key
* metadata about an annotation.
* Render an annotation's header summary, including metadata about its user,
* sharing status, document and timestamp. It also allows the user to
* toggle sub-threads/replies in certain cases.
*/
module
.
exports
=
{
controller
:
AnnotationHeaderController
,
controllerAs
:
'vm'
,
bindings
:
{
/**
* The saved annotation
*/
annotation
:
'<'
,
/**
* True if the annotation is private or will become private when the user
* saves their changes.
*/
isPrivate
:
'<'
,
/** True if the user is currently editing the annotation. */
isEditing
:
'<'
,
/**
* True if the annotation is a highlight.
* FIXME: This should determined in AnnotationHeaderController
*/
isHighlight
:
'<'
,
onReplyCountClick
:
'&'
,
replyCount
:
'<'
,
function
AnnotationHeader
({
annotation
,
isEditing
,
isHighlight
,
isPrivate
,
onReplyCountClick
,
replyCount
,
showDocumentInfo
,
})
{
const
annotationLink
=
annotation
.
links
?
annotation
.
links
.
html
:
''
;
const
replyPluralized
=
!
replyCount
||
replyCount
>
1
?
'replies'
:
'reply'
;
return
(
<
header
className
=
"annotation-header"
>
<
div
className
=
"annotation-header__row"
>
<
AnnotationUser
annotation
=
{
annotation
}
/
>
<
div
className
=
"annotation-collapsed-replies"
>
<
a
className
=
"annotation-link"
onClick
=
{
onReplyCountClick
}
>
{
replyCount
}
{
replyPluralized
}
<
/a
>
<
/div
>
{
!
isEditing
&&
annotation
.
updated
&&
(
<
div
className
=
"annotation-header__timestamp"
>
<
Timestamp
className
=
"annotation-header__timestamp-link"
href
=
{
annotationLink
}
timestamp
=
{
annotation
.
updated
}
/
>
<
/div
>
)}
<
/div
>
<
div
className
=
"annotation-header__row"
>
<
AnnotationShareInfo
annotation
=
{
annotation
}
isPrivate
=
{
isPrivate
}
/
>
{
!
isEditing
&&
isHighlight
&&
(
<
div
className
=
"annotation-header__highlight"
>
<
i
className
=
"h-icon-border-color"
title
=
"This is a highlight. Click 'edit' to add a note or tag."
/>
<
/div
>
)}
{
showDocumentInfo
&&
<
AnnotationDocumentInfo
annotation
=
{
annotation
}
/>
}
<
/div
>
<
/header
>
);
}
/** True if document metadata should be shown. */
showDocumentInfo
:
'<'
,
},
template
:
require
(
'../templates/annotation-header.html'
),
AnnotationHeader
.
propTypes
=
{
/* The annotation */
annotation
:
propTypes
.
object
.
isRequired
,
/* Whether the annotation is actively being edited */
isEditing
:
propTypes
.
bool
,
/* Whether the annotation is a highlight */
isHighlight
:
propTypes
.
bool
,
/* Whether the annotation is an "only me" (private) annotation */
isPrivate
:
propTypes
.
bool
,
/* Callback for when the toggle-replies element is clicked */
onReplyCountClick
:
propTypes
.
func
.
isRequired
,
/* How many replies this annotation currently has */
replyCount
:
propTypes
.
number
,
/**
* Should document metadata be rendered? Hint: this is enabled for single-
* annotation and stream views
*/
showDocumentInfo
:
propTypes
.
bool
,
};
module
.
exports
=
AnnotationHeader
;
src/sidebar/components/annotation-share-info.js
0 → 100644
View file @
d6c00fd0
'use strict'
;
const
propTypes
=
require
(
'prop-types'
);
const
{
createElement
}
=
require
(
'preact'
);
const
SvgIcon
=
require
(
'./svg-icon'
);
const
useStore
=
require
(
'../store/use-store'
);
/**
* Render information about what group an annotation is in and
* whether it is private to the current user (only me)
*/
function
AnnotationShareInfo
({
annotation
,
isPrivate
})
{
const
group
=
useStore
(
store
=>
store
.
getGroup
(
annotation
.
group
));
// We may not have access to the group object beyond its ID
const
hasGroup
=
!!
group
;
// Only show the name of the group and link to it if there is a
// URL (link) returned by the API for this group. Some groups do not have links
const
linkToGroup
=
hasGroup
&&
group
.
links
&&
group
.
links
.
html
;
return
(
<
div
className
=
"annotation-share-info"
>
{
linkToGroup
&&
(
<
a
className
=
"annotation-share-info__group"
href
=
{
group
.
links
.
html
}
target
=
"_blank"
rel
=
"noopener noreferrer"
>
{
group
.
type
===
'open'
?
(
<
SvgIcon
className
=
"annotation-share-info__icon"
name
=
"public"
/>
)
:
(
<
SvgIcon
className
=
"annotation-share-info__icon"
name
=
"groups"
/>
)}
<
span
className
=
"annotation-share-info__group-info"
>
{
group
.
name
}
<
/span
>
<
/a
>
)}
{
isPrivate
&&
(
<
span
className
=
"annotation-share-info__private"
title
=
"This annotation is visible only to you."
>
{
/* Show the lock icon in all cases when the annotation is private... */
}
<
SvgIcon
className
=
"annotation-share-info__icon"
name
=
"lock"
/>
{
/* but only render the "Only Me" text if we're not showing/linking a group name */
}
{
!
linkToGroup
&&
(
<
span
className
=
"annotation-share-info__private-info"
>
Only
me
<
/span
>
)}
<
/span
>
)}
<
/div
>
);
}
AnnotationShareInfo
.
propTypes
=
{
/** The current annotation object for which sharing info will be rendered */
annotation
:
propTypes
.
object
.
isRequired
,
/** Is this an "only me" (private) annotation? */
isPrivate
:
propTypes
.
bool
.
isRequired
,
};
module
.
exports
=
AnnotationShareInfo
;
src/sidebar/components/annotation-user.js
View file @
d6c00fd0
...
...
@@ -30,22 +30,28 @@ function AnnotationUser({ annotation, features, serviceUrl, settings }) {
if
(
shouldLinkToActivity
)
{
return
(
<
a
className
=
"annotation-user"
href
=
{
isFirstPartyUser
?
serviceUrl
(
'user'
,
{
user
})
:
`
${
settings
.
usernameUrl
}${
username_
}
`
}
target
=
"_blank"
rel
=
"noopener noreferrer"
>
{
displayName
}
<
/a
>
<
div
className
=
"annotation-user"
>
<
a
className
=
"annotation-user__link"
href
=
{
isFirstPartyUser
?
serviceUrl
(
'user'
,
{
user
})
:
`
${
settings
.
usernameUrl
}${
username_
}
`
}
target
=
"_blank"
rel
=
"noopener noreferrer"
>
<
span
className
=
"annotation-user__user-name"
>
{
displayName
}
<
/span
>
<
/a
>
<
/div
>
);
}
return
<
div
className
=
"annotation-user"
>
{
displayName
}
<
/div>
;
return
(
<
div
className
=
"annotation-user"
>
<
span
className
=
"annotation-user__user-name"
>
{
displayName
}
<
/span
>
<
/div
>
);
}
AnnotationUser
.
propTypes
=
{
...
...
src/sidebar/components/annotation-viewer-content.js
View file @
d6c00fd0
...
...
@@ -28,7 +28,6 @@ function fetchThread(api, id) {
// @ngInject
function
AnnotationViewerContentController
(
$location
,
$routeParams
,
store
,
api
,
...
...
@@ -43,12 +42,6 @@ function AnnotationViewerContentController(
const
id
=
$routeParams
.
id
;
this
.
$onInit
=
()
=>
{
this
.
search
.
update
=
function
(
query
)
{
$location
.
path
(
'/stream'
).
search
(
'q'
,
query
);
};
};
store
.
subscribe
(
function
()
{
self
.
rootThread
=
rootThread
.
thread
(
store
.
getState
());
});
...
...
@@ -87,8 +80,6 @@ function AnnotationViewerContentController(
module
.
exports
=
{
controller
:
AnnotationViewerContentController
,
controllerAs
:
'vm'
,
bindings
:
{
search
:
'<'
,
},
bindings
:
{},
template
:
require
(
'../templates/annotation-viewer-content.html'
),
};
src/sidebar/components/hypothesis-app.js
View file @
d6c00fd0
...
...
@@ -35,7 +35,6 @@ function authStateFromProfile(profile) {
// @ngInject
function
HypothesisAppController
(
$document
,
$location
,
$rootScope
,
$route
,
$scope
,
...
...
@@ -183,15 +182,6 @@ function HypothesisAppController(
session
.
logout
();
};
this
.
search
=
{
query
:
function
()
{
return
store
.
getState
().
filterQuery
;
},
update
:
function
(
query
)
{
store
.
setFilterQuery
(
query
);
},
};
}
module
.
exports
=
{
...
...
src/sidebar/components/login-control.js
deleted
100644 → 0
View file @
343237bb
'use strict'
;
module
.
exports
=
{
controllerAs
:
'vm'
,
//@ngInject
controller
:
function
()
{},
bindings
:
{
/**
* An object representing the current authentication status.
*/
auth
:
'<'
,
/**
* Called when the user clicks on the "About this version" text.
*/
onLogin
:
'&'
,
/**
* Called when the user clicks on the "Sign Up" text.
*/
onSignUp
:
'&'
,
/**
* Called when the user clicks on the "Log out" text.
*/
onLogout
:
'&'
,
},
template
:
require
(
'../templates/login-control.html'
),
};
src/sidebar/components/moderation-banner.js
View file @
d6c00fd0
'use strict'
;
const
annotationMetadata
=
require
(
'../annotation-metadata'
);
// @ngInject
function
ModerationBannerController
(
store
,
flash
,
api
)
{
const
self
=
this
;
const
{
createElement
}
=
require
(
'preact'
);
const
classnames
=
require
(
'classnames'
);
const
propTypes
=
require
(
'prop-types'
);
this
.
flagCount
=
function
()
{
return
annotationMetadata
.
flagCount
(
self
.
annotation
);
}
;
const
annotationMetadata
=
require
(
'../annotation-metadata'
);
const
useStore
=
require
(
'../store/use-store'
);
const
{
withServices
}
=
require
(
'../util/service-context'
)
;
this
.
isHidden
=
function
()
{
return
self
.
annotation
.
hidden
;
};
/**
* Banner allows moderators to hide/unhide the flagged
* annotation from other users.
*/
function
ModerationBanner
({
annotation
,
api
,
flash
})
{
// actions
const
store
=
useStore
(
store
=>
({
hide
:
store
.
hideAnnotation
,
unhide
:
store
.
unhideAnnotation
,
}));
this
.
isHiddenOrFlagged
=
function
()
{
const
flagCount
=
self
.
flagCount
();
return
flagCount
!==
null
&&
(
flagCount
>
0
||
self
.
isHidden
());
};
const
flagCount
=
annotationMetadata
.
flagCount
(
annotation
);
this
.
isReply
=
function
()
{
return
annotationMetadata
.
isReply
(
self
.
annotation
);
};
const
isHiddenOrFlagged
=
flagCount
!==
null
&&
(
flagCount
>
0
||
annotation
.
hidden
);
/**
* Hide an annotation from non-moderator users.
*/
this
.
hideAnnotation
=
function
()
{
const
hideAnnotation
=
()
=>
{
api
.
annotation
.
hide
({
id
:
self
.
annotation
.
id
})
.
then
(
function
()
{
store
.
hide
Annotation
(
self
.
annotation
.
id
);
.
hide
({
id
:
annotation
.
id
})
.
then
(
()
=>
{
store
.
hide
(
annotation
.
id
);
})
.
catch
(
function
()
{
.
catch
(
()
=>
{
flash
.
error
(
'Failed to hide annotation'
);
});
};
...
...
@@ -40,28 +41,66 @@ function ModerationBannerController(store, flash, api) {
/**
* Un-hide an annotation from non-moderator users.
*/
this
.
unhideAnnotation
=
function
()
{
const
unhideAnnotation
=
()
=>
{
api
.
annotation
.
unhide
({
id
:
self
.
annotation
.
id
})
.
then
(
function
()
{
store
.
unhide
Annotation
(
self
.
annotation
.
id
);
.
unhide
({
id
:
annotation
.
id
})
.
then
(
()
=>
{
store
.
unhide
(
annotation
.
id
);
})
.
catch
(
function
()
{
.
catch
(
()
=>
{
flash
.
error
(
'Failed to unhide annotation'
);
});
};
const
toggleButtonProps
=
(()
=>
{
const
props
=
{};
if
(
annotation
.
hidden
)
{
props
.
onClick
=
unhideAnnotation
;
props
.
title
=
'Make this annotation visible to everyone'
;
}
else
{
props
.
onClick
=
hideAnnotation
;
props
.
title
=
'Hide this annotation from non-moderators'
;
}
return
props
;
})();
const
bannerClasses
=
classnames
(
'moderation-banner'
,
{
'is-flagged'
:
flagCount
>
0
,
'is-hidden'
:
annotation
.
hidden
,
'is-reply'
:
annotationMetadata
.
isReply
(
annotation
),
});
if
(
!
isHiddenOrFlagged
)
{
return
null
;
}
return
(
<
div
className
=
{
bannerClasses
}
>
{
!!
flagCount
&&
!
annotation
.
hidden
&&
(
<
span
>
Flagged
for
review
x
{
flagCount
}
<
/span
>
)}
{
annotation
.
hidden
&&
(
<
span
>
Hidden
from
users
.
Flagged
x
{
flagCount
}
<
/span
>
)}
<
span
className
=
"u-stretch"
/>
<
button
{...
toggleButtonProps
}
>
{
annotation
.
hidden
?
'Unhide'
:
'Hide'
}
<
/button
>
<
/div
>
);
}
/**
* Banner shown above flagged annotations to allow moderators to hide/unhide the
* annotation from other users.
*/
ModerationBanner
.
propTypes
=
{
/**
* The annotation object for this banner. This contains
* state about the flag count or its hidden value.
*/
annotation
:
propTypes
.
object
.
isRequired
,
module
.
exports
=
{
controller
:
ModerationBannerController
,
controllerAs
:
'vm'
,
bindings
:
{
annotation
:
'<'
,
},
template
:
require
(
'../templates/moderation-banner.html'
),
// Injected services.
api
:
propTypes
.
object
.
isRequired
,
flash
:
propTypes
.
object
.
isRequired
,
};
ModerationBanner
.
injectedProps
=
[
'api'
,
'flash'
];
module
.
exports
=
withServices
(
ModerationBanner
);
src/sidebar/components/search-input.js
View file @
d6c00fd0
...
...
@@ -30,10 +30,7 @@ function SearchInput({ alwaysExpanded, query, onSearch }) {
const
onSubmit
=
e
=>
{
e
.
preventDefault
();
// TODO - When the parent components are converted to React, the signature
// of the callback can be simplified to `onSearch(query)` rather than
// `onSearch({ $query: query })`.
onSearch
({
$query
:
input
.
current
.
value
});
onSearch
(
input
.
current
.
value
);
};
// When the active query changes outside of this component, update the input
...
...
@@ -61,9 +58,10 @@ function SearchInput({ alwaysExpanded, query, onSearch }) {
<
button
type
=
"button"
className
=
"search-input__icon top-bar__btn"
title
=
"Search"
onClick
=
{()
=>
input
.
current
.
focus
()}
>
<
i
className
=
"h-icon-search"
><
/i
>
<
i
className
=
"h-icon-search"
/
>
<
/button
>
)}
{
isLoading
&&
<
Spinner
className
=
"top-bar__btn"
title
=
"Loading…"
/>
}
...
...
src/sidebar/components/stream-content.js
View file @
d6c00fd0
...
...
@@ -3,7 +3,6 @@
// @ngInject
function
StreamContentController
(
$scope
,
$location
,
$route
,
$routeParams
,
annotationMapper
,
...
...
@@ -55,6 +54,10 @@ function StreamContentController(
}
});
// In case this route loaded after a client-side route change (eg. from
// '/a/:id'), clear any existing annotations.
store
.
clearAnnotations
();
// Perform the initial search
fetch
(
20
);
...
...
@@ -68,18 +71,11 @@ function StreamContentController(
store
.
setSortKey
(
'Newest'
);
this
.
loadMore
=
fetch
;
this
.
$onInit
=
()
=>
{
this
.
search
.
query
=
()
=>
$routeParams
.
q
||
''
;
this
.
search
.
update
=
q
=>
$location
.
search
({
q
});
};
}
module
.
exports
=
{
controller
:
StreamContentController
,
controllerAs
:
'vm'
,
bindings
:
{
search
:
'<'
,
},
bindings
:
{},
template
:
require
(
'../templates/stream-content.html'
),
};
src/sidebar/components/stream-search-input.js
0 → 100644
View file @
d6c00fd0
'use strict'
;
const
{
createElement
}
=
require
(
'preact'
);
const
{
useEffect
,
useState
}
=
require
(
'preact/hooks'
);
const
propTypes
=
require
(
'prop-types'
);
const
{
withServices
}
=
require
(
'../util/service-context'
);
const
SearchInput
=
require
(
'./search-input'
);
/**
* Search input for the single annotation view and stream.
*
* This displays and updates the "q" query param in the URL.
*/
function
StreamSearchInput
({
$location
,
$rootScope
})
{
const
[
query
,
setQuery
]
=
useState
(
$location
.
search
().
q
);
const
search
=
query
=>
{
$rootScope
.
$apply
(()
=>
{
// Re-route the user to `/stream` if they are on `/a/:id` and then set
// the search query.
$location
.
path
(
'/stream'
).
search
({
q
:
query
});
});
};
useEffect
(()
=>
{
$rootScope
.
$on
(
'$locationChangeSuccess'
,
()
=>
{
setQuery
(
$location
.
search
().
q
);
});
});
return
<
SearchInput
query
=
{
query
}
onSearch
=
{
search
}
alwaysExpanded
=
{
true
}
/>
;
}
StreamSearchInput
.
propTypes
=
{
$location
:
propTypes
.
object
,
$rootScope
:
propTypes
.
object
,
};
StreamSearchInput
.
injectedProps
=
[
'$location'
,
'$rootScope'
];
module
.
exports
=
withServices
(
StreamSearchInput
);
src/sidebar/components/test/annotation-document-info-test.js
0 → 100644
View file @
d6c00fd0
'use strict'
;
const
{
createElement
}
=
require
(
'preact'
);
const
{
shallow
}
=
require
(
'enzyme'
);
const
fixtures
=
require
(
'../../test/annotation-fixtures'
);
const
AnnotationDocumentInfo
=
require
(
'../annotation-document-info'
);
describe
(
'AnnotationDocumentInfo'
,
()
=>
{
let
fakeDomainAndTitle
;
let
fakeMetadata
;
const
createAnnotationDocumentInfo
=
props
=>
{
return
shallow
(
<
AnnotationDocumentInfo
annotation
=
{
fixtures
.
defaultAnnotation
()}
{...
props
}
/
>
);
};
beforeEach
(()
=>
{
fakeDomainAndTitle
=
sinon
.
stub
();
fakeMetadata
=
{
domainAndTitle
:
fakeDomainAndTitle
};
AnnotationDocumentInfo
.
$imports
.
$mock
({
'../annotation-metadata'
:
fakeMetadata
,
});
});
afterEach
(()
=>
{
AnnotationDocumentInfo
.
$imports
.
$restore
();
});
it
(
'should not render if there is no document title'
,
()
=>
{
fakeDomainAndTitle
.
returns
({});
const
wrapper
=
createAnnotationDocumentInfo
();
const
info
=
wrapper
.
find
(
'.annotation-document-info'
);
assert
.
notOk
(
info
.
exists
());
});
it
(
'should render the document title'
,
()
=>
{
fakeDomainAndTitle
.
returns
({
titleText
:
'I have a title'
});
const
wrapper
=
createAnnotationDocumentInfo
();
const
info
=
wrapper
.
find
(
'.annotation-document-info'
);
assert
.
isOk
(
info
.
exists
());
});
it
(
'should render a link if available'
,
()
=>
{
fakeDomainAndTitle
.
returns
({
titleText
:
'I have a title'
,
titleLink
:
'https://www.example.com'
,
});
const
wrapper
=
createAnnotationDocumentInfo
();
const
link
=
wrapper
.
find
(
'.annotation-document-info__title a'
);
assert
.
equal
(
link
.
prop
(
'href'
),
'https://www.example.com'
);
});
it
(
'should render domain if available'
,
()
=>
{
fakeDomainAndTitle
.
returns
({
titleText
:
'I have a title'
,
domain
:
'www.example.com'
,
});
const
wrapper
=
createAnnotationDocumentInfo
();
const
domain
=
wrapper
.
find
(
'.annotation-document-info__domain'
);
assert
.
equal
(
domain
.
text
(),
'(www.example.com)'
);
});
});
src/sidebar/components/test/annotation-header-test.js
View file @
d6c00fd0
'use strict'
;
const
angular
=
require
(
'angular'
);
const
{
createElement
}
=
require
(
'preact'
);
const
{
shallow
}
=
require
(
'enzyme'
);
const
unroll
=
require
(
'../../../shared/test/util'
).
unroll
;
const
fixtures
=
require
(
'../../test/annotation-fixtures'
);
const
annotationHeader
=
require
(
'../annotation-header'
);
const
fakeDocumentMeta
=
{
domain
:
'docs.io'
,
titleLink
:
'http://docs.io/doc.html'
,
titleText
:
'Dummy title'
,
};
describe
(
'sidebar.components.annotation-header'
,
function
()
{
let
$componentController
;
let
fakeFeatures
;
let
fakeGroups
;
let
fakeAccountID
;
const
fakeSettings
=
{
usernameUrl
:
'http://www.example.org/'
};
let
fakeServiceUrl
;
beforeEach
(
'Initialize fakeAccountID'
,
()
=>
{
fakeAccountID
=
{
isThirdPartyUser
:
sinon
.
stub
().
returns
(
false
),
username
:
sinon
.
stub
().
returns
(
'TEST_USERNAME'
),
};
});
beforeEach
(
'Import and register the annotationHeader component'
,
function
()
{
annotationHeader
.
$imports
.
$mock
({
'../annotation-metadata'
:
{
// eslint-disable-next-line no-unused-vars
domainAndTitle
:
function
(
ann
)
{
return
fakeDocumentMeta
;
},
},
'../util/account-id'
:
fakeAccountID
,
const
AnnotationHeader
=
require
(
'../annotation-header'
);
const
AnnotationDocumentInfo
=
require
(
'../annotation-document-info'
);
const
Timestamp
=
require
(
'../timestamp'
);
describe
(
'AnnotationHeader'
,
()
=>
{
const
createAnnotationHeader
=
props
=>
{
return
shallow
(
<
AnnotationHeader
annotation
=
{
fixtures
.
defaultAnnotation
()}
isEditing
=
{
false
}
isHighlight
=
{
false
}
isPrivate
=
{
false
}
onReplyCountClick
=
{
sinon
.
stub
()}
replyCount
=
{
0
}
showDocumentInfo
=
{
false
}
{...
props
}
/
>
);
};
describe
(
'collapsed replies'
,
()
=>
{
it
(
'should have a callback'
,
()
=>
{
const
fakeCallback
=
sinon
.
stub
();
const
wrapper
=
createAnnotationHeader
({
onReplyCountClick
:
fakeCallback
,
});
const
replyCollapseLink
=
wrapper
.
find
(
'.annotation-link'
);
assert
.
equal
(
replyCollapseLink
.
prop
(
'onClick'
),
fakeCallback
);
});
angular
.
module
(
'app'
,
[]).
component
(
'annotationHeader'
,
annotationHeader
);
});
afterEach
(()
=>
{
annotationHeader
.
$imports
.
$restore
();
unroll
(
'it should render the annotation reply count'
,
testCase
=>
{
const
wrapper
=
createAnnotationHeader
({
replyCount
:
testCase
.
replyCount
,
});
const
replyCollapseLink
=
wrapper
.
find
(
'.annotation-link'
);
assert
.
equal
(
replyCollapseLink
.
text
(),
testCase
.
expected
);
},
[
{
replyCount
:
0
,
expected
:
'0 replies'
,
},
{
replyCount
:
1
,
expected
:
'1 reply'
,
},
{
replyCount
:
2
,
expected
:
'2 replies'
,
},
]
);
});
beforeEach
(
'Initialize and register fake AngularJS dependencies'
,
function
()
{
fakeFeatures
=
{
flagEnabled
:
sinon
.
stub
().
returns
(
false
),
}
;
describe
(
'timestamp'
,
()
=>
{
it
(
'should render a timestamp if annotation has an `updated` value'
,
()
=>
{
const
wrapper
=
createAnnotationHeader
();
const
timestamp
=
wrapper
.
find
(
Timestamp
)
;
angular
.
mock
.
module
(
'app'
,
{
features
:
fakeFeatures
,
groups
:
fakeGroups
,
settings
:
fakeSettings
,
serviceUrl
:
fakeServiceUrl
,
assert
.
isTrue
(
timestamp
.
exists
());
});
angular
.
mock
.
inject
(
function
(
_$componentController_
)
{
$componentController
=
_$componentController_
;
it
(
'should not render a timestamp if annotation does not have an `updated` value'
,
()
=>
{
const
wrapper
=
createAnnotationHeader
({
annotation
:
fixtures
.
newAnnotation
(),
});
const
timestamp
=
wrapper
.
find
(
Timestamp
);
assert
.
isFalse
(
timestamp
.
exists
());
});
});
describe
(
'sidebar.components.AnnotationHeaderController'
,
function
()
{
describe
(
'#htmlLink()'
,
function
()
{
it
(
'returns the HTML link when available'
,
function
()
{
const
ann
=
fixtures
.
defaultAnnotation
();
ann
.
links
=
{
html
:
'https://annotation.service/123'
};
const
ctrl
=
$componentController
(
'annotationHeader'
,
{},
{
annotation
:
ann
,
}
);
assert
.
equal
(
ctrl
.
htmlLink
(),
ann
.
links
.
html
);
describe
(
'annotation is-highlight icon'
,
()
=>
{
it
(
'should display is-highlight icon if annotation is a highlight'
,
()
=>
{
const
wrapper
=
createAnnotationHeader
({
isEditing
:
false
,
isHighlight
:
true
,
});
const
highlightIcon
=
wrapper
.
find
(
'.annotation-header__highlight'
);
it
(
'returns an empty string when no HTML link is available'
,
function
()
{
const
ann
=
fixtures
.
defaultAnnotation
();
ann
.
links
=
{};
const
ctrl
=
$componentController
(
'annotationHeader'
,
{},
{
annotation
:
ann
,
}
);
assert
.
equal
(
ctrl
.
htmlLink
(),
''
);
});
assert
.
isTrue
(
highlightIcon
.
exists
());
});
describe
(
'#documentMeta()'
,
function
()
{
it
(
'returns the domain, title link and text for the annotation'
,
function
()
{
const
ann
=
fixtures
.
defaultAnnotation
();
const
ctrl
=
$componentController
(
'annotationHeader'
,
{},
{
annotation
:
ann
,
}
);
assert
.
deepEqual
(
ctrl
.
documentMeta
(),
fakeDocumentMeta
);
it
(
'should not display the is-highlight icon if annotation is not a highlight'
,
()
=>
{
const
wrapper
=
createAnnotationHeader
({
isEditing
:
false
,
isHighlight
:
false
,
});
const
highlightIcon
=
wrapper
.
find
(
'.annotation-header__highlight'
);
assert
.
isFalse
(
highlightIcon
.
exists
());
});
});
describe
(
'#displayName'
,
()
=>
{
[
{
context
:
'when the api_render_user_info feature flag is turned off in h'
,
it
:
'returns the username'
,
user_info
:
undefined
,
client_display_names
:
false
,
isThirdPartyUser
:
false
,
expectedResult
:
'TEST_USERNAME'
,
},
{
context
:
'when the api_render_user_info feature flag is turned off in h'
,
it
:
'returns the username even if the client_display_names feature flag is on'
,
user_info
:
undefined
,
client_display_names
:
true
,
isThirdPartyUser
:
false
,
expectedResult
:
'TEST_USERNAME'
,
},
{
context
:
'when the client_display_names feature flag is off in h'
,
it
:
'returns the username'
,
user_info
:
{
display_name
:
null
},
client_display_names
:
false
,
isThirdPartyUser
:
false
,
expectedResult
:
'TEST_USERNAME'
,
},
{
context
:
'when the client_display_names feature flag is off in h'
,
it
:
'returns the username even if the user has a display name'
,
user_info
:
{
display_name
:
'Bill Jones'
},
client_display_names
:
false
,
isThirdPartyUser
:
false
,
expectedResult
:
'TEST_USERNAME'
,
},
{
context
:
'when both feature flags api_render_user_info and '
+
'client_display_names are on'
,
it
:
'returns the username, if the user has no display_name'
,
user_info
:
{
display_name
:
null
},
client_display_names
:
true
,
isThirdPartyUser
:
false
,
expectedResult
:
'TEST_USERNAME'
,
},
{
context
:
'when both feature flags api_render_user_info and '
+
'client_display_names are on'
,
it
:
'returns the display_name, if the user has one'
,
user_info
:
{
display_name
:
'Bill Jones'
},
client_display_names
:
true
,
isThirdPartyUser
:
false
,
expectedResult
:
'Bill Jones'
,
},
{
context
:
'when the client_display_names feature flag is off but '
+
'the user is a third-party user'
,
it
:
'returns display_name even though client_display_names is off'
,
user_info
:
{
display_name
:
'Bill Jones'
},
client_display_names
:
false
,
isThirdPartyUser
:
true
,
expectedResult
:
'Bill Jones'
,
},
{
context
:
'when client_display_names is on and the user is a '
+
'third-party user'
,
it
:
'returns the display_name'
,
user_info
:
{
display_name
:
'Bill Jones'
},
client_display_names
:
true
,
isThirdPartyUser
:
true
,
expectedResult
:
'Bill Jones'
,
},
{
context
:
'when the user is a third-party user but the '
+
'api_render_user_info feature flag is turned off in h'
,
it
:
'returns the username'
,
user_info
:
undefined
,
client_display_names
:
true
,
isThirdPartyUser
:
true
,
expectedResult
:
'TEST_USERNAME'
,
},
{
context
:
"when the user is a third-party user but doesn't have a "
+
'display_name'
,
it
:
'returns the username'
,
user_info
:
{
display_name
:
null
},
client_display_names
:
true
,
isThirdPartyUser
:
true
,
expectedResult
:
'TEST_USERNAME'
,
},
].
forEach
(
test
=>
{
context
(
test
.
context
,
()
=>
{
it
(
test
.
it
,
()
=>
{
// Make features.flagEnabled('client_display_names') return true
// or false, depending on the test case.
fakeFeatures
.
flagEnabled
=
flag
=>
{
if
(
flag
===
'client_display_names'
)
{
return
test
.
client_display_names
;
}
return
false
;
};
describe
(
'annotation document info'
,
()
=>
{
it
(
'should render document info if `showDocumentInfo` is enabled'
,
()
=>
{
const
wrapper
=
createAnnotationHeader
({
showDocumentInfo
:
true
});
// Make isThirdPartyUser() return true or false,
// depending on the test case.
fakeAccountID
.
isThirdPartyUser
.
returns
(
test
.
isThirdPartyUser
);
const
documentInfo
=
wrapper
.
find
(
AnnotationDocumentInfo
);
const
ann
=
fixtures
.
defaultAnnotation
(
);
ann
.
user_info
=
test
.
user_info
;
assert
.
isTrue
(
documentInfo
.
exists
()
);
})
;
const
ctrl
=
$componentController
(
'annotationHeader'
,
{},
{
annotation
:
ann
,
}
);
it
(
'should not render document info if `showDocumentInfo` is not enabled'
,
()
=>
{
const
wrapper
=
createAnnotationHeader
({
showDocumentInfo
:
false
});
assert
.
equal
(
ctrl
.
displayName
(),
test
.
expectedResult
);
});
});
});
});
const
documentInfo
=
wrapper
.
find
(
AnnotationDocumentInfo
);
describe
(
'#thirdPartyUsernameLink'
,
()
=>
{
it
(
'returns the custom username link if set'
,
()
=>
{
let
ann
;
let
ctrl
;
assert
.
isFalse
(
documentInfo
.
exists
());
});
});
fakeSettings
.
usernameUrl
=
'http://www.example.org/'
;
ann
=
fixtures
.
defaultAnnotation
();
ctrl
=
$componentController
(
'annotationHeader'
,
{},
{
annotation
:
ann
,
}
);
assert
.
deepEqual
(
ctrl
.
thirdPartyUsernameLink
(),
'http://www.example.org/TEST_USERNAME'
);
context
(
'user is editing annotation'
,
()
=>
{
it
(
'should not display timestamp'
,
()
=>
{
const
wrapper
=
createAnnotationHeader
({
annotation
:
fixtures
.
defaultAnnotation
(),
isEditing
:
true
,
});
it
(
'returns null if no custom username link is set in the settings object'
,
()
=>
{
let
ann
;
let
ctrl
;
const
timestamp
=
wrapper
.
find
(
Timestamp
);
fakeSettings
.
usernameUrl
=
null
;
ann
=
fixtures
.
defaultAnnotation
();
ctrl
=
$componentController
(
'annotationHeader'
,
{},
{
annotation
:
ann
,
}
);
assert
.
deepEqual
(
ctrl
.
thirdPartyUsernameLink
(),
null
);
assert
.
isFalse
(
timestamp
.
exists
());
});
it
(
'should not display is-highlight icon'
,
()
=>
{
const
wrapper
=
createAnnotationHeader
({
annotation
:
fixtures
.
defaultAnnotation
(),
isEditing
:
true
,
isHighlight
:
true
,
});
const
highlight
=
wrapper
.
find
(
'.annotation-header__highlight'
);
assert
.
isFalse
(
highlight
.
exists
());
});
});
});
src/sidebar/components/test/annotation-share-info-test.js
0 → 100644
View file @
d6c00fd0
'use strict'
;
const
{
createElement
}
=
require
(
'preact'
);
const
{
shallow
}
=
require
(
'enzyme'
);
const
fixtures
=
require
(
'../../test/annotation-fixtures'
);
const
AnnotationShareInfo
=
require
(
'../annotation-share-info'
);
describe
(
'AnnotationShareInfo'
,
()
=>
{
let
fakeGroup
;
let
fakeStore
;
let
fakeGetGroup
;
const
createAnnotationShareInfo
=
props
=>
{
return
shallow
(
<
AnnotationShareInfo
annotation
=
{
fixtures
.
defaultAnnotation
()}
isPrivate
=
{
false
}
{...
props
}
/
>
);
};
beforeEach
(()
=>
{
fakeGroup
=
{
name
:
'My Group'
,
links
:
{
html
:
'https://www.example.com'
,
},
type
:
'private'
,
};
fakeGetGroup
=
sinon
.
stub
().
returns
(
fakeGroup
);
fakeStore
=
{
getGroup
:
fakeGetGroup
};
AnnotationShareInfo
.
$imports
.
$mock
({
'../store/use-store'
:
callback
=>
callback
(
fakeStore
),
});
});
afterEach
(()
=>
{
AnnotationShareInfo
.
$imports
.
$restore
();
});
describe
(
'group link'
,
()
=>
{
it
(
'should show a link to the group for extant, first-party groups'
,
()
=>
{
const
wrapper
=
createAnnotationShareInfo
();
const
groupLink
=
wrapper
.
find
(
'.annotation-share-info__group'
);
const
groupName
=
wrapper
.
find
(
'.annotation-share-info__group-info'
);
assert
.
equal
(
groupLink
.
prop
(
'href'
),
fakeGroup
.
links
.
html
);
assert
.
equal
(
groupName
.
text
(),
fakeGroup
.
name
);
});
it
(
'should display a group icon for private and restricted groups'
,
()
=>
{
const
wrapper
=
createAnnotationShareInfo
();
const
groupIcon
=
wrapper
.
find
(
'.annotation-share-info__group .annotation-share-info__icon'
);
assert
.
equal
(
groupIcon
.
prop
(
'name'
),
'groups'
);
});
it
(
'should display a public/world icon for open groups'
,
()
=>
{
fakeGroup
.
type
=
'open'
;
const
wrapper
=
createAnnotationShareInfo
();
const
groupIcon
=
wrapper
.
find
(
'.annotation-share-info__group .annotation-share-info__icon'
);
assert
.
equal
(
groupIcon
.
prop
(
'name'
),
'public'
);
});
it
(
'should not show a link to third-party groups'
,
()
=>
{
// Third-party groups have no `html` link
fakeGetGroup
.
returns
({
name
:
'A Group'
,
links
:
{}
});
const
wrapper
=
createAnnotationShareInfo
();
const
groupLink
=
wrapper
.
find
(
'.annotation-share-info__group'
);
assert
.
notOk
(
groupLink
.
exists
());
});
it
(
'should not show a link if no group available'
,
()
=>
{
fakeGetGroup
.
returns
(
undefined
);
const
wrapper
=
createAnnotationShareInfo
();
const
groupLink
=
wrapper
.
find
(
'.annotation-share-info__group'
);
assert
.
notOk
(
groupLink
.
exists
());
});
});
describe
(
'"only you" information'
,
()
=>
{
it
(
'should not show privacy information if annotation is not private'
,
()
=>
{
const
wrapper
=
createAnnotationShareInfo
({
isPrivate
:
false
});
const
privacy
=
wrapper
.
find
(
'.annotation-share-info__private'
);
assert
.
notOk
(
privacy
.
exists
());
});
context
(
'private annotation'
,
()
=>
{
it
(
'should show privacy icon'
,
()
=>
{
const
wrapper
=
createAnnotationShareInfo
({
isPrivate
:
true
});
const
privacyIcon
=
wrapper
.
find
(
'.annotation-share-info__private .annotation-share-info__icon'
);
assert
.
isOk
(
privacyIcon
.
exists
());
assert
.
equal
(
privacyIcon
.
prop
(
'name'
),
'lock'
);
});
it
(
'should not show "only me" text for first-party group'
,
()
=>
{
const
wrapper
=
createAnnotationShareInfo
({
isPrivate
:
true
});
const
privacyText
=
wrapper
.
find
(
'.annotation-share-info__private-info'
);
assert
.
notOk
(
privacyText
.
exists
());
});
it
(
'should show "only me" text for annotation in third-party group'
,
()
=>
{
fakeGetGroup
.
returns
({
name
:
'Some Name'
});
const
wrapper
=
createAnnotationShareInfo
({
isPrivate
:
true
});
const
privacyText
=
wrapper
.
find
(
'.annotation-share-info__private-info'
);
assert
.
isOk
(
privacyText
.
exists
());
assert
.
equal
(
privacyText
.
text
(),
'Only me'
);
});
});
});
});
src/sidebar/components/test/annotation-thread-test.js
View file @
d6c00fd0
...
...
@@ -238,10 +238,8 @@ describe('annotationThread', function() {
const
element
=
util
.
createDirective
(
document
,
'annotationThread'
,
{
thread
:
thread
,
});
const
moderationBanner
=
element
.
find
(
'moderation-banner'
)
.
controller
(
'moderationBanner'
);
assert
.
deepEqual
(
moderationBanner
,
{
annotation
:
ann
});
assert
.
ok
(
element
[
0
].
querySelector
(
'moderation-banner'
));
assert
.
ok
(
element
[
0
].
querySelector
(
'annotation'
));
});
it
(
'does not render the annotation or moderation banner if there is no annotation'
,
function
()
{
...
...
src/sidebar/components/test/hypothesis-app-test.js
View file @
d6c00fd0
...
...
@@ -19,7 +19,6 @@ describe('sidebar.components.hypothesis-app', function() {
let
fakeFeatures
=
null
;
let
fakeFlash
=
null
;
let
fakeFrameSync
=
null
;
let
fakeLocation
=
null
;
let
fakeParams
=
null
;
let
fakeServiceConfig
=
null
;
let
fakeSession
=
null
;
...
...
@@ -95,10 +94,6 @@ describe('sidebar.components.hypothesis-app', function() {
connect
:
sandbox
.
spy
(),
};
fakeLocation
=
{
search
:
sandbox
.
stub
().
returns
({}),
};
fakeParams
=
{
id
:
'test'
};
fakeSession
=
{
...
...
@@ -138,7 +133,6 @@ describe('sidebar.components.hypothesis-app', function() {
$provide
.
value
(
'bridge'
,
fakeBridge
);
$provide
.
value
(
'groups'
,
fakeGroups
);
$provide
.
value
(
'$route'
,
fakeRoute
);
$provide
.
value
(
'$location'
,
fakeLocation
);
$provide
.
value
(
'$routeParams'
,
fakeParams
);
$provide
.
value
(
'$window'
,
fakeWindow
);
})
...
...
src/sidebar/components/test/login-control-test.js
deleted
100644 → 0
View file @
343237bb
'use strict'
;
const
angular
=
require
(
'angular'
);
const
util
=
require
(
'../../directive/test/util'
);
const
loginControl
=
require
(
'../login-control'
);
describe
(
'loginControl'
,
function
()
{
before
(
function
()
{
angular
.
module
(
'app'
,
[]).
component
(
'loginControl'
,
loginControl
);
});
beforeEach
(
function
()
{
angular
.
mock
.
module
(
'app'
,
{});
});
describe
(
'sign up and log in links'
,
()
=>
{
it
(
'should render empty login and signup element if user auth status is unknown'
,
()
=>
{
const
el
=
util
.
createDirective
(
document
,
'loginControl'
,
{
auth
:
{
username
:
'someUsername'
,
status
:
'unknown'
,
},
newStyle
:
true
,
});
const
loginEl
=
el
.
find
(
'.login-text'
);
const
links
=
loginEl
.
find
(
'a'
);
assert
.
lengthOf
(
loginEl
,
1
);
assert
.
lengthOf
(
links
,
0
);
});
it
(
'should render login and signup links if user is logged out'
,
()
=>
{
const
el
=
util
.
createDirective
(
document
,
'loginControl'
,
{
auth
:
{
username
:
'someUsername'
,
status
:
'logged-out'
,
},
newStyle
:
true
,
});
const
loginEl
=
el
.
find
(
'.login-text'
);
const
links
=
loginEl
.
find
(
'a'
);
assert
.
lengthOf
(
loginEl
,
1
);
assert
.
lengthOf
(
links
,
2
);
});
it
(
'should not render login and signup element if user is logged in'
,
()
=>
{
const
el
=
util
.
createDirective
(
document
,
'loginControl'
,
{
auth
:
{
username
:
'someUsername'
,
status
:
'logged-in'
,
},
newStyle
:
true
,
});
const
loginEl
=
el
.
find
(
'.login-text'
);
assert
.
lengthOf
(
loginEl
,
0
);
});
});
describe
(
'user menu'
,
()
=>
{
it
(
'should render a user menu if the user is logged in'
,
()
=>
{
const
el
=
util
.
createDirective
(
document
,
'loginControl'
,
{
auth
:
{
username
:
'someUsername'
,
status
:
'logged-in'
,
},
newStyle
:
true
,
});
const
menuEl
=
el
.
find
(
'user-menu'
);
assert
.
lengthOf
(
menuEl
,
1
);
});
it
(
'should not render a user menu if user is not logged in'
,
()
=>
{
const
el
=
util
.
createDirective
(
document
,
'loginControl'
,
{
auth
:
{
username
:
'someUsername'
,
status
:
'logged-out'
,
},
newStyle
:
true
,
});
const
menuEl
=
el
.
find
(
'user-menu'
);
assert
.
lengthOf
(
menuEl
,
0
);
});
});
});
src/sidebar/components/test/moderation-banner-test.js
View file @
d6c00fd0
'use strict'
;
const
angular
=
require
(
'angular'
);
const
{
shallow
}
=
require
(
'enzyme'
);
const
{
createElement
}
=
require
(
'preact'
);
const
util
=
require
(
'../../directive/test/util
'
);
const
ModerationBanner
=
require
(
'../moderation-banner
'
);
const
fixtures
=
require
(
'../../test/annotation-fixtures'
);
const
unroll
=
require
(
'../../../shared/test/util'
).
unroll
;
const
moderatedAnnotation
=
fixtures
.
moderatedAnnotation
;
describe
(
'moderationBanner'
,
function
()
{
let
bannerEl
;
let
fakeStore
;
let
fakeFlash
;
describe
(
'ModerationBanner'
,
()
=>
{
let
fakeApi
;
let
fakeFlash
;
before
(
function
()
{
angular
.
module
(
'app'
,
[])
.
component
(
'moderationBanner'
,
require
(
'../moderation-banner'
));
});
beforeEach
(
function
()
{
fakeStore
=
{
hideAnnotation
:
sinon
.
stub
(),
unhideAnnotation
:
sinon
.
stub
(),
};
function
createComponent
(
props
)
{
return
shallow
(
<
ModerationBanner
api
=
{
fakeApi
}
flash
=
{
fakeFlash
}
{...
props
}
/
>
).
dive
();
// dive() needed because this component uses `withServices`
}
beforeEach
(()
=>
{
fakeFlash
=
{
error
:
sinon
.
stub
(),
};
...
...
@@ -37,31 +31,29 @@ describe('moderationBanner', function() {
},
};
angular
.
mock
.
module
(
'app'
,
{
store
:
fakeStore
,
api
:
fakeApi
,
flash
:
fakeFlash
,
ModerationBanner
.
$imports
.
$mock
({
'../store/use-store'
:
callback
=>
callback
({
hide
:
sinon
.
stub
(),
unhide
:
sinon
.
stub
(),
}),
});
});
afterEach
(
function
()
{
bannerEl
.
remov
e
();
afterEach
(
()
=>
{
ModerationBanner
.
$imports
.
$restor
e
();
});
function
createBanner
(
inputs
)
{
const
el
=
util
.
createDirective
(
document
,
'moderationBanner'
,
inputs
);
bannerEl
=
el
[
0
];
return
bannerEl
;
}
unroll
(
'displays if user is a moderator and annotation is hidden or flagged'
,
function
(
testCase
)
{
const
banner
=
createBanner
({
annotation
:
testCase
.
ann
});
const
wrapper
=
createComponent
({
annotation
:
testCase
.
ann
,
});
if
(
testCase
.
expectVisible
)
{
assert
.
notEqual
(
banner
.
textContent
.
trim
(),
''
);
assert
.
notEqual
(
wrapper
.
text
()
.
trim
(),
''
);
}
else
{
assert
.
equal
(
banner
.
textContent
.
trim
(),
''
);
assert
.
isFalse
(
wrapper
.
exists
()
);
}
},
[
...
...
@@ -72,9 +64,10 @@ describe('moderationBanner', function() {
},
{
// Hidden, but user is not a moderator
ann
:
Object
.
assign
(
fixtures
.
defaultAnnotation
(),
{
ann
:
{
...
fixtures
.
defaultAnnotation
(),
hidden
:
true
,
}
)
,
},
expectVisible
:
false
,
},
{
...
...
@@ -98,42 +91,62 @@ describe('moderationBanner', function() {
it
(
'displays the number of flags the annotation has received'
,
function
()
{
const
ann
=
fixtures
.
moderatedAnnotation
({
flagCount
:
10
});
const
banner
=
createBanner
({
annotation
:
ann
});
assert
.
include
(
banner
.
textContent
,
'Flagged for review x10'
);
const
wrapper
=
createComponent
({
annotation
:
ann
});
assert
.
include
(
wrapper
.
text
()
,
'Flagged for review x10'
);
});
it
(
'displays in a more compact form if the annotation is a reply'
,
function
()
{
const
ann
=
Object
.
assign
(
fixtures
.
oldReply
(),
{
moderation
:
{
flagCount
:
10
,
const
wrapper
=
createComponent
({
annotation
:
{
...
fixtures
.
oldReply
(),
moderation
:
{
flagCount
:
10
,
},
},
});
wrapper
.
exists
(
'.is-reply'
);
});
it
(
'does not display in a more compact form if the annotation is not a reply'
,
function
()
{
const
wrapper
=
createComponent
({
annotation
:
{
...
fixtures
.
moderatedAnnotation
({}),
moderation
:
{
flagCount
:
10
,
},
},
});
const
banner
=
createBanner
({
annotation
:
ann
});
assert
.
ok
(
banner
.
querySelector
(
'.is-reply'
));
assert
.
isFalse
(
wrapper
.
exists
(
'.is-reply'
));
});
it
(
'reports if the annotation was hidden'
,
function
()
{
const
ann
=
moderatedAnnotation
({
flagCount
:
1
,
hidden
:
true
,
const
wrapper
=
createComponent
({
annotation
:
fixtures
.
moderatedAnnotation
({
flagCount
:
1
,
hidden
:
true
,
}),
});
const
banner
=
createBanner
({
annotation
:
ann
});
assert
.
include
(
banner
.
textContent
,
'Hidden from users'
);
assert
.
include
(
wrapper
.
text
(),
'Hidden from users'
);
});
it
(
'hides the annotation if "Hide" is clicked'
,
function
()
{
const
ann
=
moderatedAnnotation
({
flagCount
:
10
});
const
banner
=
createBanner
({
annotation
:
ann
});
banner
.
querySelector
(
'button'
).
click
();
const
wrapper
=
createComponent
({
annotation
:
fixtures
.
moderatedAnnotation
({
flagCount
:
10
,
}),
});
wrapper
.
find
(
'button'
).
simulate
(
'click'
);
assert
.
calledWith
(
fakeApi
.
annotation
.
hide
,
{
id
:
'ann-id'
});
});
it
(
'reports an error if hiding the annotation fails'
,
function
(
done
)
{
const
ann
=
moderatedAnnotation
({
flagCount
:
10
});
const
banner
=
createBanner
({
annotation
:
ann
});
const
wrapper
=
createComponent
({
annotation
:
moderatedAnnotation
({
flagCount
:
10
,
}),
});
fakeApi
.
annotation
.
hide
.
returns
(
Promise
.
reject
(
new
Error
(
'Network Error'
)));
banner
.
querySelector
(
'button'
).
click
();
wrapper
.
find
(
'button'
).
simulate
(
'click'
);
setTimeout
(
function
()
{
assert
.
calledWith
(
fakeFlash
.
error
,
'Failed to hide annotation'
);
...
...
@@ -142,29 +155,27 @@ describe('moderationBanner', function() {
});
it
(
'unhides the annotation if "Unhide" is clicked'
,
function
()
{
const
ann
=
moderatedAnnotation
({
flagCount
:
1
,
hidden
:
true
,
const
wrapper
=
createComponent
({
annotation
:
moderatedAnnotation
({
flagCount
:
1
,
hidden
:
true
,
}),
});
const
banner
=
createBanner
({
annotation
:
ann
});
banner
.
querySelector
(
'button'
).
click
();
wrapper
.
find
(
'button'
).
simulate
(
'click'
);
assert
.
calledWith
(
fakeApi
.
annotation
.
unhide
,
{
id
:
'ann-id'
});
});
it
(
'reports an error if unhiding the annotation fails'
,
function
(
done
)
{
const
ann
=
moderatedAnnotation
({
flagCount
:
1
,
hidden
:
true
,
const
wrapper
=
createComponent
({
annotation
:
moderatedAnnotation
({
flagCount
:
1
,
hidden
:
true
,
}),
});
const
banner
=
createBanner
({
annotation
:
ann
});
fakeApi
.
annotation
.
unhide
.
returns
(
Promise
.
reject
(
new
Error
(
'Network Error'
))
);
banner
.
querySelector
(
'button'
).
click
();
wrapper
.
find
(
'button'
).
simulate
(
'click'
);
setTimeout
(
function
()
{
assert
.
calledWith
(
fakeFlash
.
error
,
'Failed to unhide annotation'
);
done
();
...
...
src/sidebar/components/test/search-input-test.js
View file @
d6c00fd0
...
...
@@ -59,7 +59,7 @@ describe('SearchInput', () => {
const
wrapper
=
createSearchInput
({
query
:
'foo'
,
onSearch
});
typeQuery
(
wrapper
,
'new-query'
);
wrapper
.
find
(
'form'
).
simulate
(
'submit'
);
assert
.
calledWith
(
onSearch
,
{
$query
:
'new-query'
}
);
assert
.
calledWith
(
onSearch
,
'new-query'
);
});
it
(
'renders loading indicator when app is in a "loading" state'
,
()
=>
{
...
...
src/sidebar/components/test/stream-content-test.js
View file @
d6c00fd0
...
...
@@ -94,18 +94,14 @@ describe('StreamContentController', function() {
});
function
createController
()
{
return
$componentController
(
'streamContent'
,
{},
{
search
:
{
query
:
sinon
.
stub
(),
update
:
sinon
.
stub
(),
},
}
);
return
$componentController
(
'streamContent'
,
{},
{});
}
it
(
'clears any existing annotations when the /stream route is loaded'
,
()
=>
{
createController
();
assert
.
calledOnce
(
fakeStore
.
clearAnnotations
);
});
it
(
'calls the search API with `_separate_replies: true`'
,
function
()
{
createController
();
assert
.
equal
(
fakeApi
.
search
.
firstCall
.
args
[
0
].
_separate_replies
,
true
);
...
...
@@ -144,7 +140,10 @@ describe('StreamContentController', function() {
it
(
'does not reload the route if the query did not change'
,
function
()
{
fakeRouteParams
.
q
=
'test query'
;
createController
();
fakeStore
.
clearAnnotations
.
resetHistory
();
$rootScope
.
$broadcast
(
'$routeUpdate'
);
assert
.
notCalled
(
fakeStore
.
clearAnnotations
);
assert
.
notCalled
(
fakeRoute
.
reload
);
});
...
...
src/sidebar/components/test/stream-search-input-test.js
0 → 100644
View file @
d6c00fd0
'use strict'
;
const
{
shallow
}
=
require
(
'enzyme'
);
const
{
createElement
}
=
require
(
'preact'
);
const
{
act
}
=
require
(
'preact/test-utils'
);
const
StreamSearchInput
=
require
(
'../stream-search-input'
);
describe
(
'StreamSearchInput'
,
()
=>
{
let
fakeLocation
;
let
fakeRootScope
;
beforeEach
(()
=>
{
fakeLocation
=
{
path
:
sinon
.
stub
().
returnsThis
(),
search
:
sinon
.
stub
().
returns
({
q
:
'the-query'
}),
};
fakeRootScope
=
{
$apply
:
callback
=>
callback
(),
$on
:
sinon
.
stub
(),
};
});
function
createSearchInput
(
props
=
{})
{
return
shallow
(
<
StreamSearchInput
$location
=
{
fakeLocation
}
$rootScope
=
{
fakeRootScope
}
{...
props
}
/
>
).
dive
();
// Dive through `withServices` wrapper.
}
it
(
'displays current "q" search param'
,
()
=>
{
const
wrapper
=
createSearchInput
();
assert
.
equal
(
wrapper
.
find
(
'SearchInput'
).
prop
(
'query'
),
'the-query'
);
});
it
(
'sets path and query when user searches'
,
()
=>
{
const
wrapper
=
createSearchInput
();
act
(()
=>
{
wrapper
.
find
(
'SearchInput'
)
.
props
()
.
onSearch
(
'new-query'
);
});
assert
.
calledWith
(
fakeLocation
.
path
,
'/stream'
);
assert
.
calledWith
(
fakeLocation
.
search
,
{
q
:
'new-query'
});
});
it
(
'updates query when changed in URL'
,
()
=>
{
fakeLocation
.
search
.
returns
({
q
:
'query-b'
});
const
wrapper
=
createSearchInput
();
assert
.
calledOnce
(
fakeRootScope
.
$on
);
assert
.
calledWith
(
fakeRootScope
.
$on
,
'$locationChangeSuccess'
);
act
(()
=>
{
fakeRootScope
.
$on
.
lastCall
.
callback
();
});
// Check that new query is displayed.
wrapper
.
update
();
assert
.
equal
(
wrapper
.
find
(
'SearchInput'
).
prop
(
'query'
),
'query-b'
);
});
});
src/sidebar/components/test/top-bar-test.js
View file @
d6c00fd0
'use strict'
;
const
angular
=
require
(
'angular'
);
const
{
createElement
}
=
require
(
'preact'
);
const
{
shallow
}
=
require
(
'enzyme'
);
const
topBar
=
require
(
'../top-bar'
);
const
util
=
require
(
'../../directive/test/util'
);
const
GroupList
=
require
(
'../group-list'
);
const
SearchInput
=
require
(
'../search-input'
);
const
StreamSearchInput
=
require
(
'../stream-search-input'
);
const
SortMenu
=
require
(
'../sort-menu'
);
const
TopBar
=
require
(
'../top-bar'
);
const
UserMenu
=
require
(
'../user-menu'
);
describe
(
'
topBar'
,
function
()
{
describe
(
'
TopBar'
,
()
=>
{
const
fakeSettings
=
{};
let
fakeStore
;
let
fakeStreamer
;
let
fakeIsThirdPartyService
;
before
(
function
()
{
angular
.
module
(
'app'
,
[])
.
component
(
'topBar'
,
topBar
)
.
component
(
'loginControl'
,
{
bindings
:
require
(
'../login-control'
).
bindings
,
})
.
component
(
'searchInput'
,
{
bindings
:
{
alwaysExpanded
:
'<'
,
query
:
'<'
,
onSearch
:
'&'
,
},
});
});
beforeEach
(()
=>
{
fakeIsThirdPartyService
=
sinon
.
stub
().
returns
(
false
);
beforeEach
(
function
()
{
fakeStore
=
{
filterQuery
:
sinon
.
stub
().
returns
(
null
),
pendingUpdateCount
:
sinon
.
stub
().
returns
(
0
),
setFilterQuery
:
sinon
.
stub
(),
};
fakeStreamer
=
{
applyPendingUpdates
:
sinon
.
stub
(),
};
angular
.
mock
.
module
(
'app'
,
{
settings
:
fakeSettings
,
store
:
fakeStore
,
streamer
:
fakeStreamer
,
});
fakeIsThirdPartyService
=
sinon
.
stub
().
returns
(
false
);
topBar
.
$imports
.
$mock
({
TopBar
.
$imports
.
$mock
({
'../store/use-store'
:
callback
=>
callback
(
fakeStore
),
'../util/is-third-party-service'
:
fakeIsThirdPartyService
,
});
});
afterEach
(()
=>
{
t
opBar
.
$imports
.
$restore
();
T
opBar
.
$imports
.
$restore
();
});
function
applyUpdateBtn
(
el
)
{
return
el
.
querySelector
(
'.top-bar__apply-update-btn'
);
function
applyUpdateBtn
(
wrapper
)
{
return
wrapper
.
find
(
'.top-bar__apply-update-btn'
);
}
function
helpBtn
(
el
)
{
return
el
.
querySelector
(
'.top-bar__help-btn'
);
function
helpBtn
(
wrapper
)
{
return
wrapper
.
find
(
'.top-bar__help-btn'
);
}
function
createTopBar
(
inputs
)
{
const
defaultInputs
=
{
isSidebar
:
true
,
};
return
util
.
createDirective
(
document
,
'topBar'
,
Object
.
assign
(
defaultInputs
,
inputs
)
);
function
createTopBar
(
props
=
{})
{
const
auth
=
{
status
:
'unknown'
};
return
shallow
(
<
TopBar
auth
=
{
auth
}
isSidebar
=
{
true
}
settings
=
{
fakeSettings
}
streamer
=
{
fakeStreamer
}
{...
props
}
/
>
).
dive
();
// Dive through `withServices` wrapper.
}
it
(
'shows the pending update count'
,
function
()
{
it
(
'shows the pending update count'
,
()
=>
{
fakeStore
.
pendingUpdateCount
.
returns
(
1
);
const
el
=
createTopBar
();
const
applyBtn
=
applyUpdateBtn
(
el
[
0
]
);
assert
.
ok
(
applyBtn
);
const
wrapper
=
createTopBar
();
const
applyBtn
=
applyUpdateBtn
(
wrapper
);
assert
.
isTrue
(
applyBtn
.
exists
()
);
});
it
(
'does not show the pending update count when there are no updates'
,
function
()
{
fakeStore
.
pendingUpdateCount
.
returns
(
0
);
const
el
=
createTopBar
();
const
applyBtn
=
applyUpdateBtn
(
el
[
0
]);
assert
.
notOk
(
applyBtn
);
it
(
'does not show the pending update count when there are no updates'
,
()
=>
{
const
wrapper
=
createTopBar
();
const
applyBtn
=
applyUpdateBtn
(
wrapper
);
assert
.
isFalse
(
applyBtn
.
exists
());
});
it
(
'applies updates when clicked'
,
function
()
{
it
(
'applies updates when clicked'
,
()
=>
{
fakeStore
.
pendingUpdateCount
.
returns
(
1
);
const
el
=
createTopBar
();
const
applyBtn
=
applyUpdateBtn
(
el
[
0
]
);
applyBtn
.
click
(
);
const
wrapper
=
createTopBar
();
const
applyBtn
=
applyUpdateBtn
(
wrapper
);
applyBtn
.
simulate
(
'click'
);
assert
.
called
(
fakeStreamer
.
applyPendingUpdates
);
});
it
(
'shows
help when help icon clicked'
,
function
()
{
it
(
'shows
Help Panel when help icon is clicked'
,
()
=>
{
const
onShowHelpPanel
=
sinon
.
stub
();
const
el
=
createTopBar
({
const
wrapper
=
createTopBar
({
onShowHelpPanel
:
onShowHelpPanel
,
});
const
help
=
helpBtn
(
el
[
0
]
);
help
.
click
(
);
const
help
=
helpBtn
(
wrapper
);
help
.
simulate
(
'click'
);
assert
.
called
(
onShowHelpPanel
);
});
it
(
'displays the login control and propagates callbacks'
,
function
()
{
const
onShowHelpPanel
=
sinon
.
stub
();
const
onLogin
=
sinon
.
stub
();
const
onLogout
=
sinon
.
stub
();
const
el
=
createTopBar
({
onShowHelpPanel
:
onShowHelpPanel
,
onLogin
:
onLogin
,
onLogout
:
onLogout
,
describe
(
'login/account actions'
,
()
=>
{
const
getLoginText
=
wrapper
=>
wrapper
.
find
(
'.top-bar__login-links'
);
it
(
'Shows ellipsis when login state is unknown'
,
()
=>
{
const
wrapper
=
createTopBar
({
auth
:
{
status
:
'unknown'
}
});
const
loginText
=
getLoginText
(
wrapper
);
assert
.
isTrue
(
loginText
.
exists
());
assert
.
equal
(
loginText
.
text
(),
'⋯'
);
});
it
(
'Shows "Log in" and "Sign up" links when user is logged out'
,
()
=>
{
const
onLogin
=
sinon
.
stub
();
const
onSignUp
=
sinon
.
stub
();
const
wrapper
=
createTopBar
({
auth
:
{
status
:
'logged-out'
},
onLogin
,
onSignUp
,
});
const
loginText
=
getLoginText
(
wrapper
);
const
links
=
loginText
.
find
(
'a'
);
assert
.
equal
(
links
.
length
,
2
);
assert
.
equal
(
links
.
at
(
0
).
text
(),
'Sign up'
);
links
.
at
(
0
).
simulate
(
'click'
);
assert
.
called
(
onSignUp
);
assert
.
equal
(
links
.
at
(
1
).
text
(),
'Log in'
);
links
.
at
(
1
).
simulate
(
'click'
);
assert
.
called
(
onLogin
);
});
const
loginControl
=
el
.
find
(
'login-control'
).
controller
(
'loginControl'
);
loginControl
.
onLogin
();
assert
.
called
(
onLogin
);
it
(
'Shows user menu when logged in'
,
()
=>
{
const
onLogout
=
sinon
.
stub
();
const
auth
=
{
status
:
'logged-in'
};
const
wrapper
=
createTopBar
({
auth
,
onLogout
});
assert
.
isFalse
(
getLoginText
(
wrapper
).
exists
());
loginControl
.
onLogout
();
assert
.
called
(
onLogout
);
const
userMenu
=
wrapper
.
find
(
UserMenu
);
assert
.
isTrue
(
userMenu
.
exists
());
assert
.
include
(
userMenu
.
props
(),
{
auth
,
onLogout
});
});
});
it
(
"checks whether we're using a third-party service"
,
function
()
{
it
(
"checks whether we're using a third-party service"
,
()
=>
{
createTopBar
();
assert
.
called
(
fakeIsThirdPartyService
);
assert
.
alwaysCalledWithExactly
(
fakeIsThirdPartyService
,
fakeSettings
);
});
context
(
'when using a first-party service'
,
function
()
{
it
(
'shows the share page button'
,
function
()
{
let
el
=
createTopBar
();
// I want the DOM element, not AngularJS's annoying angular.element
// wrapper object.
el
=
el
[
0
];
assert
.
isNotNull
(
el
.
querySelector
(
'[title="Share this page"]'
));
context
(
'when using a first-party service'
,
()
=>
{
it
(
'shows the share page button'
,
()
=>
{
const
wrapper
=
createTopBar
();
assert
.
isTrue
(
wrapper
.
exists
(
'[title="Share this page"]'
));
});
});
context
(
'when using a third-party service'
,
function
()
{
beforeEach
(
function
()
{
context
(
'when using a third-party service'
,
()
=>
{
beforeEach
(
()
=>
{
fakeIsThirdPartyService
.
returns
(
true
);
});
it
(
"doesn't show the share page button"
,
function
()
{
let
el
=
createTopBar
();
// I want the DOM element, not AngularJS's annoying angular.element
// wrapper object.
el
=
el
[
0
];
assert
.
isNull
(
el
.
querySelector
(
'[title="Share this page"]'
));
it
(
"doesn't show the share page button"
,
()
=>
{
const
wrapper
=
createTopBar
();
assert
.
isFalse
(
wrapper
.
exists
(
'[title="Share this page"]'
));
});
});
it
(
'displays the share page when "Share this page" is clicked'
,
function
()
{
it
(
'displays the share page when "Share this page" is clicked'
,
()
=>
{
const
onSharePage
=
sinon
.
stub
();
const
el
=
createTopBar
({
onSharePage
:
onSharePage
});
el
.
find
(
'[title="Share this page"]'
).
click
();
const
wrapper
=
createTopBar
({
onSharePage
});
wrapper
.
find
(
'[title="Share this page"]'
).
simulate
(
'click'
);
assert
.
called
(
onSharePage
);
});
it
(
'displays the search input and propagates query changes'
,
function
()
{
const
onSearch
=
sinon
.
stub
();
const
el
=
createTopBar
({
searchController
:
{
query
:
sinon
.
stub
().
returns
(
'query'
),
update
:
onSearch
,
},
});
const
searchInput
=
el
.
find
(
'search-input'
).
controller
(
'searchInput'
);
it
(
'displays search input in the sidebar'
,
()
=>
{
fakeStore
.
filterQuery
.
returns
(
'test-query'
);
const
wrapper
=
createTopBar
();
assert
.
equal
(
wrapper
.
find
(
SearchInput
).
prop
(
'query'
),
'test-query'
);
});
assert
.
equal
(
searchInput
.
query
,
'query'
);
it
(
'updates current filter when changing search query in the sidebar'
,
()
=>
{
const
wrapper
=
createTopBar
();
wrapper
.
find
(
'SearchInput'
).
prop
(
'onSearch'
)(
'new-query'
);
assert
.
calledWith
(
fakeStore
.
setFilterQuery
,
'new-query'
);
});
searchInput
.
onSearch
({
$query
:
'new-query'
});
assert
.
calledWith
(
onSearch
,
'new-query'
);
it
(
'displays search input in the single annotation view / stream'
,
()
=>
{
const
wrapper
=
createTopBar
({
isSidebar
:
false
});
const
searchInput
=
wrapper
.
find
(
StreamSearchInput
);
assert
.
ok
(
searchInput
.
exists
());
});
it
(
'shows the clean theme when settings contains the clean theme option'
,
function
()
{
angular
.
mock
.
module
(
'app'
,
{
settings
:
{
theme
:
'clean'
},
it
(
'shows the clean theme when settings contains the clean theme option'
,
()
=>
{
fakeSettings
.
theme
=
'clean'
;
const
wrapper
=
createTopBar
();
assert
.
isTrue
(
wrapper
.
exists
(
'.top-bar--theme-clean'
));
});
context
(
'in the stream and single annotation pages'
,
()
=>
{
it
(
'does not render the group list, sort menu or share menu'
,
()
=>
{
const
wrapper
=
createTopBar
({
isSidebar
:
false
});
assert
.
isFalse
(
wrapper
.
exists
(
GroupList
));
assert
.
isFalse
(
wrapper
.
exists
(
SortMenu
));
assert
.
isFalse
(
wrapper
.
exists
(
'button[title="Share this page"]'
));
});
const
el
=
createTopBar
();
assert
.
ok
(
el
[
0
].
querySelector
(
'.top-bar--theme-clean'
));
it
(
'does show the Help menu and user menu'
,
()
=>
{
const
wrapper
=
createTopBar
({
isSidebar
:
false
,
auth
:
{
status
:
'logged-in'
},
});
assert
.
isTrue
(
wrapper
.
exists
(
'button[title="Help"]'
));
assert
.
isTrue
(
wrapper
.
exists
(
UserMenu
));
});
});
});
src/sidebar/components/top-bar.js
View file @
d6c00fd0
'use strict'
;
const
{
Fragment
,
createElement
}
=
require
(
'preact'
);
const
classnames
=
require
(
'classnames'
);
const
propTypes
=
require
(
'prop-types'
);
const
useStore
=
require
(
'../store/use-store'
);
const
{
applyTheme
}
=
require
(
'../util/theme'
);
const
isThirdPartyService
=
require
(
'../util/is-third-party-service'
);
const
{
withServices
}
=
require
(
'../util/service-context'
);
const
GroupList
=
require
(
'./group-list'
);
const
SearchInput
=
require
(
'./search-input'
);
const
StreamSearchInput
=
require
(
'./stream-search-input'
);
const
SortMenu
=
require
(
'./sort-menu'
);
const
SvgIcon
=
require
(
'./svg-icon'
);
const
UserMenu
=
require
(
'./user-menu'
);
/**
* The toolbar which appears at the top of the sidebar providing actions
* to switch groups, view account information, sort/filter annotations etc.
*/
function
TopBar
({
auth
,
isSidebar
,
onLogin
,
onLogout
,
onSharePage
,
onShowHelpPanel
,
onSignUp
,
settings
,
streamer
,
})
{
const
useCleanTheme
=
settings
.
theme
===
'clean'
;
const
showSharePageButton
=
!
isThirdPartyService
(
settings
);
const
loginLinkStyle
=
applyTheme
([
'accentColor'
],
settings
);
const
filterQuery
=
useStore
(
store
=>
store
.
filterQuery
());
const
setFilterQuery
=
useStore
(
store
=>
store
.
setFilterQuery
);
const
pendingUpdateCount
=
useStore
(
store
=>
store
.
pendingUpdateCount
());
const
applyPendingUpdates
=
()
=>
streamer
.
applyPendingUpdates
();
const
loginControl
=
(
<
Fragment
>
{
auth
.
status
===
'unknown'
&&
(
<
span
className
=
"top-bar__login-links"
>
⋯
<
/span
>
)}
{
auth
.
status
===
'logged-out'
&&
(
<
span
className
=
"top-bar__login-links"
>
<
a
href
=
"#"
onClick
=
{
onSignUp
}
target
=
"_blank"
style
=
{
loginLinkStyle
}
>
Sign
up
<
/a>{' '
}
/
{
' '
}
<
a
href
=
"#"
onClick
=
{
onLogin
}
style
=
{
loginLinkStyle
}
>
Log
in
<
/a
>
<
/span
>
)}
{
auth
.
status
===
'logged-in'
&&
(
<
UserMenu
auth
=
{
auth
}
onLogout
=
{
onLogout
}
/
>
)}
<
/Fragment
>
);
return
(
<
div
className
=
{
classnames
(
'top-bar'
,
useCleanTheme
&&
'top-bar--theme-clean'
)}
>
{
/* Single-annotation and stream views. */
}
{
!
isSidebar
&&
(
<
div
className
=
"top-bar__inner content"
>
<
StreamSearchInput
/>
<
div
className
=
"top-bar__expander"
/>
<
button
className
=
"top-bar__btn top-bar__help-btn"
onClick
=
{
onShowHelpPanel
}
title
=
"Help"
aria
-
label
=
"Help"
>
<
SvgIcon
name
=
"help"
className
=
"top-bar__help-icon"
/>
<
/button
>
{
loginControl
}
<
/div
>
)}
{
/* Sidebar view */
}
{
isSidebar
&&
(
<
div
className
=
"top-bar__inner content"
>
<
GroupList
className
=
"GroupList"
auth
=
{
auth
}
/
>
<
div
className
=
"top-bar__expander"
/>
{
pendingUpdateCount
>
0
&&
(
<
a
className
=
"top-bar__apply-update-btn"
onClick
=
{
applyPendingUpdates
}
title
=
{
`Show
${
pendingUpdateCount
}
new/updated
${
pendingUpdateCount
===
1
?
'annotation'
:
'annotations'
}
`
}
>
<
SvgIcon
className
=
"top-bar__apply-icon"
name
=
"refresh"
/>
<
/a
>
)}
<
SearchInput
query
=
{
filterQuery
}
onSearch
=
{
setFilterQuery
}
/
>
<
SortMenu
/>
{
showSharePageButton
&&
(
<
button
className
=
"top-bar__btn"
onClick
=
{
onSharePage
}
title
=
"Share this page"
aria
-
label
=
"Share this page"
>
<
i
className
=
"h-icon-annotation-share"
/>
<
/button
>
)}
<
button
className
=
"top-bar__btn top-bar__help-btn"
onClick
=
{
onShowHelpPanel
}
title
=
"Help"
aria
-
label
=
"Help"
>
<
SvgIcon
name
=
"help"
className
=
"top-bar__help-icon"
/>
<
/button
>
{
loginControl
}
<
/div
>
)}
<
/div
>
);
}
TopBar
.
propTypes
=
{
/**
* Object containing current authentication status.
*/
auth
:
propTypes
.
shape
({
status
:
propTypes
.
string
.
isRequired
,
module
.
exports
=
{
controllerAs
:
'vm'
,
//@ngInject
controller
:
function
(
settings
,
store
,
streamer
)
{
if
(
settings
.
theme
&&
settings
.
theme
===
'clean'
)
{
this
.
isThemeClean
=
true
;
}
else
{
this
.
isThemeClean
=
false
;
}
this
.
applyPendingUpdates
=
streamer
.
applyPendingUpdates
;
this
.
pendingUpdateCount
=
store
.
pendingUpdateCount
;
this
.
showSharePageButton
=
function
()
{
return
!
isThirdPartyService
(
settings
);
};
},
bindings
:
{
auth
:
'<'
,
isSidebar
:
'<'
,
onShowHelpPanel
:
'&'
,
onLogin
:
'&'
,
onLogout
:
'&'
,
onSharePage
:
'&'
,
onSignUp
:
'&'
,
searchController
:
'<'
,
},
template
:
require
(
'../templates/top-bar.html'
),
// Additional properties when user is logged in.
displayName
:
propTypes
.
string
,
userid
:
propTypes
.
string
,
username
:
propTypes
.
string
,
}),
/**
* Flag indicating whether the app is the sidebar or a top-level page.
*/
isSidebar
:
propTypes
.
bool
,
/**
* Callback invoked when user clicks "Help" button.
*/
onShowHelpPanel
:
propTypes
.
func
,
/**
* Callback invoked when user clicks "Login" button.
*/
onLogin
:
propTypes
.
func
,
/** Callback invoked when user clicks "Logout" action in account menu. */
onLogout
:
propTypes
.
func
,
/** Callback invoked when user clicks "Share" toolbar action. */
onSharePage
:
propTypes
.
func
,
/** Callback invoked when user clicks "Sign up" button. */
onSignUp
:
propTypes
.
func
,
// Services
settings
:
propTypes
.
object
,
streamer
:
propTypes
.
object
,
};
TopBar
.
injectedProps
=
[
'settings'
,
'streamer'
];
module
.
exports
=
withServices
(
TopBar
);
src/sidebar/index.js
View file @
d6c00fd0
...
...
@@ -79,13 +79,12 @@ function configureRoutes($routeProvider) {
// The `vm.{auth,search}` properties used in these templates come from the
// `<hypothesis-app>` component which hosts the router's container element.
$routeProvider
.
when
(
'/a/:id'
,
{
template
:
'<annotation-viewer-content search="vm.search"></annotation-viewer-content>'
,
template
:
'<annotation-viewer-content></annotation-viewer-content>'
,
reloadOnSearch
:
false
,
resolve
:
resolve
,
});
$routeProvider
.
when
(
'/stream'
,
{
template
:
'<stream-content
search="vm.search"
></stream-content>'
,
template
:
'<stream-content></stream-content>'
,
reloadOnSearch
:
false
,
resolve
:
resolve
,
});
...
...
@@ -141,7 +140,10 @@ function startAngularApp(config) {
// UI components
.
component
(
'annotation'
,
require
(
'./components/annotation'
))
.
component
(
'annotationHeader'
,
require
(
'./components/annotation-header'
))
.
component
(
'annotationHeader'
,
wrapReactComponent
(
require
(
'./components/annotation-header'
))
)
.
component
(
'annotationActionButton'
,
wrapReactComponent
(
require
(
'./components/annotation-action-button'
))
...
...
@@ -164,24 +166,18 @@ function startAngularApp(config) {
require
(
'./components/annotation-viewer-content'
)
)
.
component
(
'excerpt'
,
require
(
'./components/excerpt'
))
.
component
(
'groupList'
,
wrapReactComponent
(
require
(
'./components/group-list'
))
)
.
component
(
'helpLink'
,
wrapReactComponent
(
require
(
'./components/help-link'
))
)
.
component
(
'helpPanel'
,
require
(
'./components/help-panel'
))
.
component
(
'loggedoutMessage'
,
require
(
'./components/loggedout-message'
))
.
component
(
'loginControl'
,
require
(
'./components/login-control'
))
.
component
(
'markdown'
,
require
(
'./components/markdown'
))
.
component
(
'moderationBanner'
,
require
(
'./components/moderation-banner'
))
.
component
(
'newNoteBtn'
,
require
(
'./components/new-note-btn'
))
.
component
(
'
searchInput
'
,
wrapReactComponent
(
require
(
'./components/
search-input
'
))
'
moderationBanner
'
,
wrapReactComponent
(
require
(
'./components/
moderation-banner
'
))
)
.
component
(
'newNoteBtn'
,
require
(
'./components/new-note-btn'
))
.
component
(
'searchStatusBar'
,
require
(
'./components/search-status-bar'
))
.
component
(
'selectionTabs'
,
require
(
'./components/selection-tabs'
))
.
component
(
'sidebarContent'
,
require
(
'./components/sidebar-content'
))
...
...
@@ -191,11 +187,6 @@ function startAngularApp(config) {
)
.
component
(
'sidebarTutorial'
,
require
(
'./components/sidebar-tutorial'
))
.
component
(
'shareDialog'
,
require
(
'./components/share-dialog'
))
.
component
(
'sortMenu'
,
wrapReactComponent
(
require
(
'./components/sort-menu'
))
)
.
component
(
'spinner'
,
wrapReactComponent
(
require
(
'./components/spinner'
)))
.
component
(
'streamContent'
,
require
(
'./components/stream-content'
))
.
component
(
'svgIcon'
,
wrapReactComponent
(
require
(
'./components/svg-icon'
)))
.
component
(
'tagEditor'
,
require
(
'./components/tag-editor'
))
...
...
@@ -204,12 +195,7 @@ function startAngularApp(config) {
'timestamp'
,
wrapReactComponent
(
require
(
'./components/timestamp'
))
)
.
component
(
'topBar'
,
require
(
'./components/top-bar'
))
.
component
(
'userMenu'
,
wrapReactComponent
(
require
(
'./components/user-menu'
))
)
.
component
(
'topBar'
,
wrapReactComponent
(
require
(
'./components/top-bar'
)))
.
directive
(
'hAutofocus'
,
require
(
'./directive/h-autofocus'
))
.
directive
(
'hBranding'
,
require
(
'./directive/h-branding'
))
.
directive
(
'hOnTouch'
,
require
(
'./directive/h-on-touch'
))
...
...
src/sidebar/search-client.js
View file @
d6c00fd0
...
...
@@ -74,11 +74,6 @@ class SearchClient extends EventEmitter {
return
;
}
self
.
emit
(
'error'
,
err
);
})
.
then
(
function
()
{
if
(
self
.
_canceled
)
{
return
;
}
self
.
emit
(
'end'
);
});
}
...
...
src/sidebar/store/modules/activity.js
View file @
d6c00fd0
...
...
@@ -14,6 +14,10 @@ function init() {
* The number of API requests that have started and not yet completed.
*/
activeApiRequests
:
0
,
/**
* The number of annotation fetches that have started and not yet completed.
*/
activeAnnotationFetches
:
0
,
},
};
}
...
...
@@ -44,6 +48,32 @@ const update = {
},
};
},
ANNOTATION_FETCH_STARTED
(
state
)
{
const
{
activity
}
=
state
;
return
{
activity
:
{
...
activity
,
activeAnnotationFetches
:
activity
.
activeAnnotationFetches
+
1
,
},
};
},
ANNOTATION_FETCH_FINISHED
(
state
)
{
const
{
activity
}
=
state
;
if
(
activity
.
activeAnnotationFetches
===
0
)
{
throw
new
Error
(
'ANNOTATION_FETCH_FINISHED action when no annotation fetches were active'
);
}
return
{
activity
:
{
...
activity
,
activeAnnotationFetches
:
activity
.
activeAnnotationFetches
-
1
,
},
};
},
};
const
actions
=
actionTypes
(
update
);
...
...
@@ -56,6 +86,14 @@ function apiRequestFinished() {
return
{
type
:
actions
.
API_REQUEST_FINISHED
};
}
function
annotationFetchStarted
()
{
return
{
type
:
actions
.
ANNOTATION_FETCH_STARTED
};
}
function
annotationFetchFinished
()
{
return
{
type
:
actions
.
ANNOTATION_FETCH_FINISHED
};
}
/**
* Return true when any activity is happening in the app that needs to complete
* before the UI will be idle.
...
...
@@ -64,6 +102,13 @@ function isLoading(state) {
return
state
.
activity
.
activeApiRequests
>
0
;
}
/**
* Return true when annotations are actively being fetched.
*/
function
isFetchingAnnotations
(
state
)
{
return
state
.
activity
.
activeAnnotationFetches
>
0
;
}
module
.
exports
=
{
init
,
update
,
...
...
@@ -71,9 +116,12 @@ module.exports = {
actions
:
{
apiRequestStarted
,
apiRequestFinished
,
annotationFetchStarted
,
annotationFetchFinished
,
},
selectors
:
{
isLoading
,
isFetchingAnnotations
,
},
};
src/sidebar/store/modules/selection.js
View file @
d6c00fd0
...
...
@@ -348,6 +348,10 @@ const getFirstSelectedAnnotationId = createSelector(
selected
=>
(
selected
?
Object
.
keys
(
selected
)[
0
]
:
null
)
);
function
filterQuery
(
state
)
{
return
state
.
filterQuery
;
}
module
.
exports
=
{
init
:
init
,
update
:
update
,
...
...
@@ -369,6 +373,7 @@ module.exports = {
selectors
:
{
hasSelectedAnnotations
,
filterQuery
,
isAnnotationSelected
,
getFirstSelectedAnnotationId
,
},
...
...
src/sidebar/store/modules/test/activity-test.js
View file @
d6c00fd0
...
...
@@ -33,6 +33,63 @@ describe('sidebar/store/modules/activity', () => {
});
});
describe
(
'isFetchingAnnotations'
,
()
=>
{
it
(
'returns false with the initial state'
,
()
=>
{
assert
.
equal
(
store
.
isFetchingAnnotations
(),
false
);
});
it
(
'returns true when API requests are in flight'
,
()
=>
{
store
.
annotationFetchStarted
();
assert
.
equal
(
store
.
isFetchingAnnotations
(),
true
);
});
it
(
'returns false when all requests end'
,
()
=>
{
store
.
annotationFetchStarted
();
store
.
annotationFetchStarted
();
store
.
annotationFetchFinished
();
assert
.
equal
(
store
.
isFetchingAnnotations
(),
true
);
store
.
annotationFetchFinished
();
assert
.
equal
(
store
.
isFetchingAnnotations
(),
false
);
});
});
it
(
'defaults `activeAnnotationFetches` counter to zero'
,
()
=>
{
assert
.
equal
(
store
.
getState
().
activity
.
activeAnnotationFetches
,
0
);
});
describe
(
'annotationFetchFinished'
,
()
=>
{
it
(
'triggers an error if no requests are in flight'
,
()
=>
{
assert
.
throws
(()
=>
{
store
.
annotationFetchFinished
();
});
});
it
(
'increments `activeAnnotationFetches` counter when a new annotation fetch is started'
,
()
=>
{
store
.
annotationFetchStarted
();
assert
.
equal
(
store
.
getState
().
activity
.
activeAnnotationFetches
,
1
);
});
});
describe
(
'annotationFetchStarted'
,
()
=>
{
it
(
'triggers an error if no requests are in flight'
,
()
=>
{
assert
.
throws
(()
=>
{
store
.
annotationFetchFinished
();
});
});
it
(
'decrements `activeAnnotationFetches` counter when an annotation fetch is finished'
,
()
=>
{
store
.
annotationFetchStarted
();
store
.
annotationFetchFinished
();
assert
.
equal
(
store
.
getState
().
activity
.
activeAnnotationFetches
,
0
);
});
});
describe
(
'#apiRequestFinished'
,
()
=>
{
it
(
'triggers an error if no requests are in flight'
,
()
=>
{
assert
.
throws
(()
=>
{
...
...
src/sidebar/templates/annotation-header.html
deleted
100644 → 0
View file @
343237bb
<header
class=
"annotation-header"
>
<!-- User -->
<span
ng-if=
"vm.user()"
>
<annotation-user
annotation=
"vm.annotation"
></annotation-user>
<span
class=
"annotation-collapsed-replies"
>
<a
class=
"annotation-link"
href=
""
ng-click=
"vm.onReplyCountClick()"
ng-pluralize
count=
"vm.replyCount"
when=
"{'0': '', 'one': '1 reply', 'other': '{} replies'}"
></a>
</span>
<br
/>
<span
class=
"annotation-header__share-info"
>
<a
class=
"annotation-header__group"
target=
"_blank"
ng-if=
"vm.group() && vm.group().links.html"
href=
"{{vm.group().links.html}}"
>
<i
class=
"h-icon-group"
></i><span
class=
"annotation-header__group-name"
>
{{vm.group().name}}
</span>
</a>
<span
ng-show=
"vm.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().links.html"
>
Only me
</span>
</span>
<i
class=
"h-icon-border-color"
ng-show=
"vm.isHighlight && !vm.isEditing"
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>
<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>
</span>
</span>
</span>
<span
class=
"u-flex-spacer"
></span>
<timestamp
class-name=
"'annotation-header__timestamp'"
timestamp=
"vm.updated()"
href=
"vm.htmlLink()"
ng-if=
"!vm.editing() && vm.updated()"
></timestamp>
</header>
src/sidebar/templates/annotation-thread.html
View file @
d6c00fd0
...
...
@@ -10,8 +10,9 @@
<div
class=
"annotation-thread__thread-line"
></div>
</div>
<div
class=
"annotation-thread__content"
>
<moderation-banner
annotation=
"vm.thread.annotation"
ng-if=
"vm.thread.annotation"
>
<moderation-banner
annotation=
"vm.thread.annotation"
ng-if=
"vm.thread.annotation"
>
</moderation-banner>
<annotation
ng-class=
"vm.annotationClasses()"
annotation=
"vm.thread.annotation"
...
...
src/sidebar/templates/annotation.html
View file @
d6c00fd0
...
...
@@ -5,12 +5,12 @@
<div
ng-keydown=
"vm.onKeydown($event)"
ng-if=
"vm.user()"
>
<annotation-header
annotation=
"vm.annotation"
is-editing=
"vm.editing()"
is-highlight=
"vm.isHighlight()"
is-private=
"vm.state().isPrivate"
on-reply-count-click=
"vm.onReplyCountClick()"
reply-count=
"vm.replyCount"
show-document-info=
"vm.showDocumentInfo"
>
is-editing=
"vm.editing()"
is-highlight=
"vm.isHighlight()"
is-private=
"vm.state().isPrivate"
on-reply-count-click=
"vm.onReplyCountClick()"
reply-count=
"vm.replyCount"
show-document-info=
"vm.showDocumentInfo"
>
</annotation-header>
<!-- Excerpts -->
...
...
src/sidebar/templates/hypothesis-app.html
View file @
d6c00fd0
...
...
@@ -6,8 +6,7 @@
on-logout=
"vm.logout()"
on-share-page=
"vm.share()"
on-show-help-panel=
"vm.showHelpPanel()"
is-sidebar=
"::vm.isSidebar"
search-controller=
"vm.search"
>
is-sidebar=
"::vm.isSidebar"
>
</top-bar>
<div
class=
"content"
>
...
...
src/sidebar/templates/login-control.html
deleted
100644 → 0
View file @
343237bb
<!-- New controls -->
<span
class=
"login-text"
ng-if=
"vm.auth.status === 'unknown'"
>
⋯
</span>
<span
class=
"login-text"
ng-if=
"vm.auth.status === 'logged-out'"
>
<a
href=
""
ng-click=
"vm.onSignUp()"
target=
"_blank"
h-branding=
"accentColor"
>
Sign up
</a>
/
<a
href=
""
ng-click=
"vm.onLogin()"
h-branding=
"accentColor"
>
Log in
</a>
</span>
<user-menu
auth=
"vm.auth"
on-logout=
"vm.onLogout()"
ng-if=
"vm.auth.status === 'logged-in'"
/>
src/sidebar/templates/moderation-banner.html
deleted
100644 → 0
View file @
343237bb
<div
class=
"moderation-banner"
ng-if=
"vm.isHiddenOrFlagged()"
ng-class=
"{'is-flagged': vm.flagCount() > 0,
'is-hidden': vm.isHidden(),
'is-reply': vm.isReply()}"
>
<span
ng-if=
"vm.flagCount() > 0 && !vm.isHidden()"
>
Flagged for review x{{ vm.flagCount() }}
</span>
<span
ng-if=
"vm.isHidden()"
>
Hidden from users. Flagged x{{ vm.flagCount() }}
</span>
<span
class=
"u-stretch"
></span>
<button
ng-if=
"!vm.isHidden()"
ng-click=
"vm.hideAnnotation()"
title=
"Hide this annotation from non-moderators"
>
Hide
</button>
<button
ng-if=
"vm.isHidden()"
ng-click=
"vm.unhideAnnotation()"
title=
"Make this annotation visible to everyone"
>
Unhide
</button>
</div>
src/sidebar/templates/top-bar.html
deleted
100644 → 0
View file @
343237bb
<!-- top bar for the sidebar and the stream.
!-->
<div
class=
"top-bar"
ng-class=
"{'top-bar--theme-clean' : vm.isThemeClean }"
>
<!-- Legacy design for top bar, as used in the stream !-->
<div
class=
"top-bar__inner content"
ng-if=
"::!vm.isSidebar"
>
<search-input
class=
"search-input"
query=
"vm.searchController.query()"
on-search=
"vm.searchController.update($query)"
always-expanded=
"true"
>
</search-input>
<div
class=
"top-bar__expander"
></div>
<button
class=
"top-bar__btn top-bar__help-btn"
ng-click=
"vm.onShowHelpPanel()"
title=
"Help"
aria-label=
"Help"
>
<svg-icon
name=
"'help'"
class-name=
"'top-bar__help-icon'"
></svg-icon>
</button>
<login-control
class=
"login-control"
auth=
"vm.auth"
on-login=
"vm.onLogin()"
on-logout=
"vm.onLogout()"
on-sign-up=
"vm.onSignUp()"
>
</login-control>
</div>
<!-- New design for the top bar, as used in the sidebar.
The inner div is styled with 'content' to center it in
the stream view.
!-->
<div
class=
"top-bar__inner content"
ng-if=
"::vm.isSidebar"
>
<group-list
class=
"group-list"
auth=
"vm.auth"
></group-list>
<div
class=
"top-bar__expander"
></div>
<a
class=
"top-bar__apply-update-btn"
ng-if=
"vm.pendingUpdateCount() > 0"
ng-click=
"vm.applyPendingUpdates()"
h-tooltip
tooltip-direction=
"up"
aria-label=
"Show {{vm.pendingUpdateCount()}} new/updated annotation(s)"
>
<svg-icon
class=
"top-bar__apply-icon"
name=
"'refresh'"
></svg-icon>
</a>
<search-input
class=
"search-input"
query=
"vm.searchController.query()"
on-search=
"vm.searchController.update($query)"
title=
"Filter the annotation list"
>
</search-input>
<sort-menu></sort-menu>
<button
class=
"top-bar__btn"
ng-click=
"vm.onSharePage()"
ng-if=
"vm.showSharePageButton()"
title=
"Share this page"
aria-label=
"Share this page"
>
<i
class=
"h-icon-annotation-share"
></i>
</button>
<button
class=
"top-bar__btn top-bar__help-btn"
ng-click=
"vm.onShowHelpPanel()"
title=
"Help"
aria-label=
"Help"
>
<svg-icon
name=
"'help'"
class-name=
"'top-bar__help-icon'"
></svg-icon>
</button>
<login-control
class=
"login-control"
auth=
"vm.auth"
on-login=
"vm.onLogin()"
on-logout=
"vm.onLogout()"
on-sign-up=
"vm.onSignUp()"
>
</login-control>
</div>
</div>
src/sidebar/test/search-client-test.js
View file @
d6c00fd0
...
...
@@ -8,7 +8,7 @@ function awaitEvent(emitter, event) {
});
}
describe
(
'SearchClient'
,
function
()
{
describe
(
'SearchClient'
,
()
=>
{
const
RESULTS
=
[
{
id
:
'one'
},
{
id
:
'two'
},
...
...
@@ -18,7 +18,7 @@ describe('SearchClient', function() {
let
fakeSearchFn
;
beforeEach
(
function
()
{
beforeEach
(
()
=>
{
fakeSearchFn
=
sinon
.
spy
(
function
(
params
)
{
return
Promise
.
resolve
({
rows
:
RESULTS
.
slice
(
params
.
offset
,
params
.
offset
+
params
.
limit
),
...
...
@@ -27,34 +27,45 @@ describe('SearchClient', function() {
});
});
it
(
'emits "results"'
,
function
()
{
it
(
'emits "results"'
,
()
=>
{
const
client
=
new
SearchClient
(
fakeSearchFn
);
const
onResults
=
sinon
.
stub
();
client
.
on
(
'results'
,
onResults
);
client
.
get
({
uri
:
'http://example.com'
});
return
awaitEvent
(
client
,
'end'
).
then
(
function
()
{
return
awaitEvent
(
client
,
'end'
).
then
(
()
=>
{
assert
.
calledWith
(
onResults
,
RESULTS
);
});
});
it
(
'emits "results" with chunks in incremental mode'
,
function
()
{
it
(
'emits "end" only once'
,
()
=>
{
const
client
=
new
SearchClient
(
fakeSearchFn
,
{
chunkSize
:
2
});
client
.
on
(
'results'
,
sinon
.
stub
());
let
emitEndCounter
=
0
;
client
.
on
(
'end'
,
()
=>
{
emitEndCounter
+=
1
;
assert
.
equal
(
emitEndCounter
,
1
);
});
client
.
get
({
uri
:
'http://example.com'
});
});
it
(
'emits "results" with chunks in incremental mode'
,
()
=>
{
const
client
=
new
SearchClient
(
fakeSearchFn
,
{
chunkSize
:
2
});
const
onResults
=
sinon
.
stub
();
client
.
on
(
'results'
,
onResults
);
client
.
get
({
uri
:
'http://example.com'
});
return
awaitEvent
(
client
,
'end'
).
then
(
function
()
{
return
awaitEvent
(
client
,
'end'
).
then
(
()
=>
{
assert
.
calledWith
(
onResults
,
RESULTS
.
slice
(
0
,
2
));
assert
.
calledWith
(
onResults
,
RESULTS
.
slice
(
2
,
4
));
});
});
it
(
'stops fetching chunks if the results array is empty'
,
function
()
{
it
(
'stops fetching chunks if the results array is empty'
,
()
=>
{
// Simulate a situation where the `total` count for the server is incorrect
// and we appear to have reached the end of the result list even though
// `total` implies that there should be more results available.
//
// In that case the client should stop trying to fetch additional pages.
fakeSearchFn
=
sinon
.
spy
(
function
()
{
fakeSearchFn
=
sinon
.
spy
(
()
=>
{
return
Promise
.
resolve
({
rows
:
[],
total
:
1000
,
...
...
@@ -66,13 +77,13 @@ describe('SearchClient', function() {
client
.
get
({
uri
:
'http://example.com'
});
return
awaitEvent
(
client
,
'end'
).
then
(
function
()
{
return
awaitEvent
(
client
,
'end'
).
then
(
()
=>
{
assert
.
calledWith
(
onResults
,
[]);
assert
.
calledOnce
(
fakeSearchFn
);
});
});
it
(
'emits "results" once in non-incremental mode'
,
function
()
{
it
(
'emits "results" once in non-incremental mode'
,
()
=>
{
const
client
=
new
SearchClient
(
fakeSearchFn
,
{
chunkSize
:
2
,
incremental
:
false
,
...
...
@@ -80,13 +91,13 @@ describe('SearchClient', function() {
const
onResults
=
sinon
.
stub
();
client
.
on
(
'results'
,
onResults
);
client
.
get
({
uri
:
'http://example.com'
});
return
awaitEvent
(
client
,
'end'
).
then
(
function
()
{
return
awaitEvent
(
client
,
'end'
).
then
(
()
=>
{
assert
.
calledOnce
(
onResults
);
assert
.
calledWith
(
onResults
,
RESULTS
);
});
});
it
(
'does not emit "results" if canceled'
,
function
()
{
it
(
'does not emit "results" if canceled'
,
()
=>
{
const
client
=
new
SearchClient
(
fakeSearchFn
);
const
onResults
=
sinon
.
stub
();
const
onEnd
=
sinon
.
stub
();
...
...
@@ -94,22 +105,22 @@ describe('SearchClient', function() {
client
.
on
(
'end'
,
onEnd
);
client
.
get
({
uri
:
'http://example.com'
});
client
.
cancel
();
return
Promise
.
resolve
().
then
(
function
()
{
return
Promise
.
resolve
().
then
(
()
=>
{
assert
.
notCalled
(
onResults
);
assert
.
called
(
onEnd
);
});
});
it
(
'emits "error" event if search fails'
,
function
()
{
it
(
'emits "error" event if search fails'
,
()
=>
{
const
err
=
new
Error
(
'search failed'
);
fakeSearchFn
=
function
()
{
fakeSearchFn
=
()
=>
{
return
Promise
.
reject
(
err
);
};
const
client
=
new
SearchClient
(
fakeSearchFn
);
const
onError
=
sinon
.
stub
();
client
.
on
(
'error'
,
onError
);
client
.
get
({
uri
:
'http://example.com'
});
return
awaitEvent
(
client
,
'end'
).
then
(
function
()
{
return
awaitEvent
(
client
,
'end'
).
then
(
()
=>
{
assert
.
calledWith
(
onError
,
err
);
});
});
...
...
src/styles/sidebar/components/annotation-document-info.scss
0 → 100644
View file @
d6c00fd0
.annotation-document-info
{
font-size
:
13px
;
color
:
$grey-5
;
display
:
flex
;
&
__title
{
margin-right
:
5px
;
}
&
__domain
{
margin-right
:
5px
;
font-size
:
12px
;
}
}
src/styles/sidebar/components/annotation-header.scss
0 → 100644
View file @
d6c00fd0
.annotation-header
{
@include
pie-clearfix
;
// Margin between top of x-height of username and
// top of the annotation card should be ~15px
margin-top
:
-
$layout-h-margin
+
10px
;
color
:
$grey-5
;
&
__row
{
display
:
flex
;
align-items
:
baseline
;
}
&
__timestamp
{
margin-left
:
auto
;
}
&
__timestamp-link
{
@include
font-small
;
color
:
$grey-4
;
}
&
__highlight
{
margin-right
:
5px
;
}
}
src/styles/sidebar/components/annotation-share-info.scss
0 → 100644
View file @
d6c00fd0
.annotation-share-info
{
display
:
flex
;
align-items
:
baseline
;
&
__group
,
&
__private
{
display
:
flex
;
align-items
:
baseline
;
font-size
:
$body1-font-size
;
color
:
$grey-5
;
}
&
__group-info
{
margin-right
:
5px
;
}
&
__private-info
{
margin-right
:
5px
;
}
&
__icon
{
margin-right
:
5px
;
width
:
10px
;
height
:
10px
;
}
}
src/styles/sidebar/components/annotation-user.scss
View file @
d6c00fd0
.annotation-user
,
.annotation-user
a
{
@include
font-normal
;
color
:
$grey-7
;
font-weight
:
bold
;
.annotation-user
{
&
__user-name
{
@include
font-normal
;
color
:
$grey-7
;
font-weight
:
bold
;
.is-dimmed
&
{
color
:
$grey-5
;
}
.is-dimmed
&
{
color
:
$grey-5
;
}
.is-highlighted
&
{
color
:
$grey-7
;
.is-highlighted
&
{
color
:
$grey-7
;
}
}
}
src/styles/sidebar/components/annotation.scss
View file @
d6c00fd0
...
...
@@ -3,7 +3,6 @@
// Highlight quote of annotation whenever its thread is hovered
.thread-list__card
:hover
.annotation-quote
{
border-left
:
$highlight
3px
solid
;
color
:
$grey-5
;
}
// When hovering a top-level annotation, show the footer in a hovered state.
...
...
@@ -17,7 +16,7 @@
color
:
$grey-6
;
}
.annotation-header__timestamp
{
.annotation-header__timestamp
-link
{
color
:
$grey-5
;
}
}
...
...
@@ -60,35 +59,10 @@
}
.annotation-quote-list
,
.annotation-header
,
.annotation-footer
{
@include
pie-clearfix
;
}
.annotation-header
{
display
:
flex
;
flex-direction
:
row
;
align-items
:
baseline
;
// Margin between top of x-height of username and
// top of the annotation card should be ~15px
margin-top
:
-
$layout-h-margin
+
10px
;
}
.annotation-header__share-info
{
color
:
$grey-5
;
@include
font-normal
;
}
.annotation-header__group
{
color
:
$color-gray
;
font-size
:
$body1-font-size
;
}
.annotation-header__group-name
{
display
:
inline-block
;
margin-left
:
5px
;
}
.annotation-body
{
@include
font-normal
;
color
:
$grey-6
;
...
...
@@ -164,11 +138,6 @@
color
:
$grey-5
;
}
.annotation-header__timestamp
{
@include
font-small
;
color
:
$grey-4
;
}
.annotation-actions
{
float
:
right
;
margin-top
:
0
;
...
...
src/styles/sidebar/components/group-list.scss
View file @
d6c00fd0
...
...
@@ -6,6 +6,11 @@
align-items
:
center
;
color
:
$grey-6
;
display
:
flex
;
// Prevent label from wrapping if top bar is too narrow to fit all of its
// items.
flex-shrink
:
0
;
font-size
:
$body2-font-size
;
font-weight
:
bold
;
}
...
...
src/styles/sidebar/components/login-control.scss
deleted
100644 → 0
View file @
343237bb
.login-control
{
flex-shrink
:
0
;
}
.login-text
{
font-size
:
$body2-font-size
;
padding-left
:
6px
;
}
src/styles/sidebar/components/top-bar.scss
View file @
d6c00fd0
...
...
@@ -21,6 +21,12 @@
border-bottom
:
none
;
}
.top-bar__login-links
{
flex-shrink
:
0
;
font-size
:
$body2-font-size
;
padding-left
:
6px
;
}
.top-bar__inner
{
// the edges of the top-bar's contents should be aligned
// with the edges of annotation cards displayed below
...
...
src/styles/sidebar/sidebar.scss
View file @
d6c00fd0
...
...
@@ -19,7 +19,10 @@ $base-line-height: 20px;
// Components
// ----------
@import
'./components/annotation'
;
@import
'./components/annotation-document-info'
;
@import
'./components/annotation-header'
;
@import
'./components/annotation-share-dialog'
;
@import
'./components/annotation-share-info'
;
@import
'./components/annotation-publish-control'
;
@import
'./components/annotation-thread'
;
@import
'./components/annotation-user'
;
...
...
@@ -28,7 +31,6 @@ $base-line-height: 20px;
@import
'./components/group-list-item'
;
@import
'./components/help-panel'
;
@import
'./components/loggedout-message'
;
@import
'./components/login-control'
;
@import
'./components/markdown'
;
@import
'./components/menu'
;
@import
'./components/menu-item'
;
...
...
yarn.lock
View file @
d6c00fd0
...
...
@@ -10,17 +10,17 @@
"@babel/highlight" "^7.0.0"
"@babel/core@^7.1.6":
version "7.
4.5
"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.
4.5.tgz#081f97e8ffca65a9b4b0fdc7e274e703f000c06a
"
integrity sha512-
OvjIh6aqXtlsA8ujtGKfC7LYWksYSX8yQcM8Ay3LuvVeQ63lcOKgoZWVqcpFwkd29aYU9rVx7jxhfhiEDV9MZA
==
version "7.
5.0
"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.
5.0.tgz#6ed6a2881ad48a732c5433096d96d1b0ee5eb734
"
integrity sha512-
6Isr4X98pwXqHvtigw71CKgmhL1etZjPs5A67jL/w0TkLM9eqmFR40YrnJvEc1WnMZFsskjsmid8bHZyxKEAnw
==
dependencies:
"@babel/code-frame" "^7.0.0"
"@babel/generator" "^7.
4.4
"
"@babel/helpers" "^7.
4.4
"
"@babel/parser" "^7.
4.5
"
"@babel/generator" "^7.
5.0
"
"@babel/helpers" "^7.
5.0
"
"@babel/parser" "^7.
5.0
"
"@babel/template" "^7.4.4"
"@babel/traverse" "^7.
4.5
"
"@babel/types" "^7.
4.4
"
"@babel/traverse" "^7.
5.0
"
"@babel/types" "^7.
5.0
"
convert-source-map "^1.1.0"
debug "^4.1.0"
json5 "^2.1.0"
...
...
@@ -29,12 +29,12 @@
semver "^5.4.1"
source-map "^0.5.0"
"@babel/generator@^7.4.0", "@babel/generator@^7.
4.4
":
version "7.
4.4
"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.
4.4.tgz#174a215eb843fc392c7edcaabeaa873de6e8f041
"
integrity sha512-
53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ
==
"@babel/generator@^7.4.0", "@babel/generator@^7.
5.0
":
version "7.
5.0
"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.
5.0.tgz#f20e4b7a91750ee8b63656073d843d2a736dca4a
"
integrity sha512-
1TTVrt7J9rcG5PMjvO7VEG3FrEoEJNHxumRq66GemPmzboLWtIjjcJgk8rokuAS7IiRSpgVSu5Vb9lc99iJkOA
==
dependencies:
"@babel/types" "^7.
4.4
"
"@babel/types" "^7.
5.0
"
jsesc "^2.5.1"
lodash "^4.17.11"
source-map "^0.5.0"
...
...
@@ -239,14 +239,14 @@
"@babel/traverse" "^7.1.0"
"@babel/types" "^7.0.0"
"@babel/helpers@^7.
4.4
":
version "7.
4.4
"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.
4.4.tgz#868b0ef59c1dd4e78744562d5ce1b59c89f2f2a5
"
integrity sha512-
igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2
A==
"@babel/helpers@^7.
5.0
":
version "7.
5.1
"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.
5.1.tgz#65407c741a56ddd59dd86346cd112da3de912db3
"
integrity sha512-
rVOTDv8sH8kNI72Unenusxw6u+1vEepZgLxeV+jHkhsQlYhzVhzL1EpfoWT7Ub3zpWSv2WV03V853dqsnyoQz
A==
dependencies:
"@babel/template" "^7.4.4"
"@babel/traverse" "^7.
4.4
"
"@babel/types" "^7.
4.4
"
"@babel/traverse" "^7.
5.0
"
"@babel/types" "^7.
5.0
"
"@babel/highlight@^7.0.0":
version "7.0.0"
...
...
@@ -257,10 +257,10 @@
esutils "^2.0.2"
js-tokens "^4.0.0"
"@babel/parser@^7.4.3", "@babel/parser@^7.4.4", "@babel/parser@^7.
4.5
":
version "7.
4.5
"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.
4.5.tgz#04af8d5d5a2b044a2a1bffacc1e5e6673544e872
"
integrity sha512-
9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew
==
"@babel/parser@^7.4.3", "@babel/parser@^7.4.4", "@babel/parser@^7.
5.0
":
version "7.
5.0
"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.
5.0.tgz#3e0713dff89ad6ae37faec3b29dcfc5c979770b7
"
integrity sha512-
I5nW8AhGpOXGCCNYGc+p7ExQIBxRFnS2fd/d862bNOKvmoEPjYPcfIjsfdy0ujagYOIYPczKgD9l3FsgTkAzKA
==
"@babel/plugin-proposal-async-generator-functions@^7.2.0":
version "7.2.0"
...
...
@@ -688,25 +688,25 @@
"@babel/parser" "^7.4.4"
"@babel/types" "^7.4.4"
"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.4", "@babel/traverse@^7.
4.5
":
version "7.
4.5
"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.
4.5.tgz#4e92d1728fd2f1897dafdd321efbff92156c3216
"
integrity sha512-
Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A
==
"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.4.4", "@babel/traverse@^7.
5.0
":
version "7.
5.0
"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.
5.0.tgz#4216d6586854ef5c3c4592dab56ec7eb78485485
"
integrity sha512-
SnA9aLbyOCcnnbQEGwdfBggnc142h/rbqqsXcaATj2hZcegCl903pUD/lfpsNBlBSuWow/YDfRyJuWi2EPR5cg
==
dependencies:
"@babel/code-frame" "^7.0.0"
"@babel/generator" "^7.
4.4
"
"@babel/generator" "^7.
5.0
"
"@babel/helper-function-name" "^7.1.0"
"@babel/helper-split-export-declaration" "^7.4.4"
"@babel/parser" "^7.
4.5
"
"@babel/types" "^7.
4.4
"
"@babel/parser" "^7.
5.0
"
"@babel/types" "^7.
5.0
"
debug "^4.1.0"
globals "^11.1.0"
lodash "^4.17.11"
"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4":
version "7.
4.4
"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.
4.4.tgz#8db9e9a629bb7c29370009b4b779ed93fe57d5f0
"
integrity sha512-
dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0
tQ==
"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4"
, "@babel/types@^7.5.0"
:
version "7.
5.0
"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.
5.0.tgz#e47d43840c2e7f9105bc4d3a2c371b4d0c7832ab
"
integrity sha512-
UFpDVqRABKsW01bvw7/wSUe56uy6RXM5+VJibVVAybDGxEW25jdwiFJEf7ASvSaC7sN7rbE/l3cLp2izav+C
tQ==
dependencies:
esutils "^2.0.2"
lodash "^4.17.11"
...
...
@@ -1295,17 +1295,17 @@ autofill-event@0.0.1:
integrity sha1-w4LPmJshth/0oSs1l+GUNHHTz3o=
autoprefixer@^9.4.7:
version "9.6.
0
"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.6.
0.tgz#0111c6bde2ad20c6f17995a33fad7cf6854b4c8
7"
integrity sha512-
kuip9YilBqhirhHEGHaBTZKXL//xxGnzvsD0FtBQa6z+A69qZD6s/BAX9VzDF1i9VKDquTJDQaPLSEhOnL6FvQ
==
version "9.6.
1
"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.6.
1.tgz#51967a02d2d2300bb01866c1611ec8348d355a4
7"
integrity sha512-
aVo5WxR3VyvyJxcJC3h4FKfwCQvQWb1tSI5VHNibddCVWrcD1NvlxEweg3TSgiPztMnWfjpy2FURKA2kvDE+Tw
==
dependencies:
browserslist "^4.6.
1
"
caniuse-lite "^1.0.300009
71
"
browserslist "^4.6.
3
"
caniuse-lite "^1.0.300009
80
"
chalk "^2.4.2"
normalize-range "^0.1.2"
num2fraction "^1.2.2"
postcss "^7.0.1
6
"
postcss-value-parser "^
3.3.1
"
postcss "^7.0.1
7
"
postcss-value-parser "^
4.0.0
"
aws-sdk@^2.345.0:
version "2.400.0"
...
...
@@ -1656,9 +1656,9 @@ browserify-zlib@~0.2.0:
pako "~1.0.5"
browserify@^16.1.0, browserify@^16.2.3:
version "16.
2.3
"
resolved "https://registry.yarnpkg.com/browserify/-/browserify-16.
2.3.tgz#7ee6e654ba4f92bce6ab3599c3485b1cc7a0ad0b
"
integrity sha512-
zQt/Gd1+W+IY+h/xX2NYMW4orQWhqSwyV+xsblycTtpOuB27h1fZhhNQuipJ4t79ohw4P4mMem0jp/ZkISQtjQ
==
version "16.
3.0
"
resolved "https://registry.yarnpkg.com/browserify/-/browserify-16.
3.0.tgz#4d414466e0b07492fff493a009ea883a9f2db230
"
integrity sha512-
BWaaD7alyGZVEBBwSTYx4iJF5DswIGzK17o8ai9w4iKRbYpk3EOiprRHMRRA8DCZFmFeOdx7A385w2XdFvxWmg
==
dependencies:
JSONStream "^1.0.3"
assert "^1.4.0"
...
...
@@ -1709,13 +1709,13 @@ browserify@^16.1.0, browserify@^16.2.3:
vm-browserify "^1.0.0"
xtend "^4.0.0"
browserslist@^4.6.0, browserslist@^4.6.
1
:
version "4.6.
2
"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.6.
2.tgz#574c665950915c2ac73a4594b8537a9eba26203f
"
integrity sha512-
2neU/V0giQy9h3XMPwLhEY3+Ao0uHSwHvU8Q1Ea6AgLVL1sXbX3dzPrJ8NWe5Hi4PoTkCYXOtVR9rfRLI0J/8
Q==
browserslist@^4.6.0, browserslist@^4.6.
3
:
version "4.6.
3
"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.6.
3.tgz#0530cbc6ab0c1f3fc8c819c72377ba55cf647f05
"
integrity sha512-
CNBqTCq22RKM8wKJNowcqihHJ4SkI8CGeK7KOR9tPboXUuS5Zk5lQgzzTbs4oxD8x+6HUshZUa2OyNI9lR93b
Q==
dependencies:
caniuse-lite "^1.0.3000097
4
"
electron-to-chromium "^1.3.1
50
"
caniuse-lite "^1.0.3000097
5
"
electron-to-chromium "^1.3.1
64
"
node-releases "^1.1.23"
btoa-lite@^1.0.0:
...
...
@@ -1836,10 +1836,10 @@ camelcase@^5.0.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==
caniuse-lite@^1.0.3000097
1, caniuse-lite@^1.0.30000974
:
version "1.0.300009
74
"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.300009
74.tgz#b7afe14ee004e97ce6dc73e3f878290a12928ad8
"
integrity sha512-
xc3rkNS/Zc3CmpMKuczWEdY2sZgx09BkAxfvkxlAEBTqcMHeL8QnPqhKse+5sRTi3nrw2pJwToD2WvKn1Uhvww
==
caniuse-lite@^1.0.3000097
5, caniuse-lite@^1.0.30000980
:
version "1.0.300009
81
"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.300009
81.tgz#5b6828803362363e5a1deba2eb550185cf6cec8f
"
integrity sha512-
JTByHj4DQgL2crHNMK6PibqAMrqqb/Vvh0JrsTJVSWG4VSUrT16EklkuRZofurlMjgA9e+zlCM4Y39F3kootMQ
==
caseless@~0.12.0:
version "0.12.0"
...
...
@@ -2855,10 +2855,10 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
electron-to-chromium@^1.3.1
50
:
version "1.3.1
55
"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.1
55.tgz#ebf0cc8eeaffd6151d1efad60fd9e021fb45fd3
a"
integrity sha512-
/ci/XgZG8jkLYOgOe3mpJY1onxPPTDY17y7scldhnSjjZqV6VvREG/LvwhRuV7BJbnENFfuDWZkSqlTh4x9Zj
Q==
electron-to-chromium@^1.3.1
64
:
version "1.3.1
88
"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.1
88.tgz#e28e1afe4bb229989e280bfd3b395c7ec03c8b7
a"
integrity sha512-
tEQcughYIMj8WDMc59EGEtNxdGgwal/oLLTDw+NEqJRJwGflQvH3aiyiexrWeZOETP4/ko78PVr6gwNhdozvu
Q==
elliptic@^6.0.0:
version "6.4.0"
...
...
@@ -3101,14 +3101,14 @@ eslint-plugin-mocha@^5.2.1:
ramda "^0.26.1"
eslint-plugin-react-hooks@^1.6.0:
version "1.6.
0
"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.6.
0.tgz#348efcda8fb426399ac7b8609607c7b4025a6f5f
"
integrity sha512-
lHBVRIaz5ibnIgNG07JNiAuBUeKhEf8l4etNx5vfAEwqQ5tcuK3jV9yjmopPgQDagQb7HwIuQVsE3IVcGrRnag
==
version "1.6.
1
"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.6.
1.tgz#3c66a5515ea3e0a221ffc5d4e75c971c217b1a4c
"
integrity sha512-
wHhmGJyVuijnYIJXZJHDUF2WM+rJYTjulUTqF9k61d3BTk8etydz+M4dXUVH7M76ZRS85rqBTCx0Es/lLsrjnA
==
eslint-plugin-react@^7.12.4:
version "7.14.
1
"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.14.
1.tgz#0b49bed8c18b5c2819ea4eb4fdda93e236643198
"
integrity sha512-
fQSIHJ3t0tYgctUyPbcjDPgNUTM6jNFguFKi73ctNjq+8KgqSynMMltakn60/VTtvmNSxOtju/j8Yby8mNn3bQ
==
version "7.14.
2
"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.14.
2.tgz#94c193cc77a899ac0ecbb2766fbef88685b7ecc1
"
integrity sha512-
jZdnKe3ip7FQOdjxks9XPN0pjUKZYq48OggNMd16Sk+8VXx6JOvXmlElxROCgp7tiUsTsze3jd78s/9AFJP2mA
==
dependencies:
array-includes "^3.0.3"
doctrine "^2.1.0"
...
...
@@ -3910,7 +3910,7 @@ glob-watcher@^5.0.3:
just-debounce "^1.0.0"
object.defaults "^1.1.0"
glob@7.1.3
, glob@^7.0.0, glob@^7.0.3, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1
:
glob@7.1.3:
version "7.1.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
...
...
@@ -3933,7 +3933,7 @@ glob@^6.0.4:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.
1.3
:
glob@^7.
0.0, glob@^7.0.3, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@~7.1.1
:
version "7.1.4"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==
...
...
@@ -4141,7 +4141,7 @@ gulp-util@^3.0.7:
through2 "^2.0.0"
vinyl "^0.5.0"
gulp@^4.0.0:
gulp@^4.0.0
, gulp@^4.0.2
:
version "4.0.2"
resolved "https://registry.yarnpkg.com/gulp/-/gulp-4.0.2.tgz#543651070fd0f6ab0a0650c6a3e6ff5a7cb09caa"
integrity sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==
...
...
@@ -5989,12 +5989,7 @@ mute-stream@0.0.7:
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
nan@^2.11.0:
version "2.12.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552"
integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==
nan@^2.12.1, nan@^2.13.2:
nan@^2.11.0, nan@^2.12.1, nan@^2.13.2:
version "2.13.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
...
...
@@ -6231,9 +6226,9 @@ npm-bundled@^1.0.1:
integrity sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==
npm-packlist@^1.1.12, npm-packlist@^1.1.6:
version "1.4.
1
"
resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.
1.tgz#19064cdf988da80ea3cee45533879d90192bbfbc
"
integrity sha512-
+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFc
w==
version "1.4.
4
"
resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.
4.tgz#866224233850ac534b63d1a6e76050092b5d2f44
"
integrity sha512-
zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6V
w==
dependencies:
ignore-walk "^3.0.1"
npm-bundled "^1.0.1"
...
...
@@ -6889,12 +6884,12 @@ postcss-url@^8.0.0:
postcss "^7.0.2"
xxhashjs "^0.2.1"
postcss-value-parser@^
3.3.1
:
version "
3.3.1
"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-
3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281
"
integrity sha512-
pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNy
Q==
postcss-value-parser@^
4.0.0
:
version "
4.0.0
"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-
4.0.0.tgz#99a983d365f7b2ad8d0f9b8c3094926eab4b936d
"
integrity sha512-
ESPktioptiSUchCKgggAkzdmkgzKfmp0EU8jXH+5kbIUB+unr0Y4CY9SRMvibuvYUBjNh1ACLbxqYNpdTQOte
Q==
postcss@^7.0.13, postcss@^7.0.1
6
, postcss@^7.0.2:
postcss@^7.0.13, postcss@^7.0.1
7
, postcss@^7.0.2:
version "7.0.17"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.17.tgz#4da1bdff5322d4a0acaab4d87f3e782436bad31f"
integrity sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==
...
...
@@ -8705,9 +8700,9 @@ universalify@^0.1.0:
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
unorm@^1.3.3:
version "1.
5
.0"
resolved "https://registry.yarnpkg.com/unorm/-/unorm-1.
5.0.tgz#01fa9b76f1c60f7916834605c032aa8962c3f00a
"
integrity sha512-
sMfSWoiRaXXeDZSXC+YRZ23H4xchQpwxjpw1tmfR+kgbBCaOgln4NI0LXejJIhnBuKINrB3WRn+ZI8IWssirVw
==
version "1.
6
.0"
resolved "https://registry.yarnpkg.com/unorm/-/unorm-1.
6.0.tgz#029b289661fba714f1a9af439eb51d9b16c205af
"
integrity sha512-
b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA
==
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
...
...
@@ -8929,11 +8924,12 @@ watchify@^3.7.0:
xtend "^4.0.0"
websocket@^1.0.22:
version "1.0.2
8
"
resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.2
8.tgz#9e5f6fdc8a3fe01d4422647ef93abdd8d45a78d3
"
integrity sha512-
00y/20/80P7H4bCYkzuuvvfDvh+dgtXi5kzDf3UcZwN6boTYaKvsrtZ5lIYm1Gsg48siMErd9M4zjSYfYFHTrA
==
version "1.0.2
9
"
resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.2
9.tgz#3f83e49d3279657c58b02a22d90749c806101b98
"
integrity sha512-
WhU8jKXC8sTh6ocLSqpZRlOKMNYGwUvjA5+XcIgIk/G3JCaDfkZUr0zA19sVSxJ0TEvm0i5IBzr54RZC4vzW7g
==
dependencies:
debug "^2.2.0"
gulp "^4.0.2"
nan "^2.11.0"
typedarray-to-buffer "^3.1.5"
yaeti "^0.0.6"
...
...
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