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
bc7085ce
Unverified
Commit
bc7085ce
authored
Jun 12, 2019
by
Robert Knight
Committed by
GitHub
Jun 12, 2019
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1148 from hypothesis/new-group-menu
Implement new groups menu design built on groups menu
parents
521b1ffd
6ad6dd58
Changes
23
Hide whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
656 additions
and
682 deletions
+656
-682
group-list-item-out-of-scope.js
src/sidebar/components/group-list-item-out-of-scope.js
+0
-113
group-list-item.js
src/sidebar/components/group-list-item.js
+97
-40
group-list-section.js
src/sidebar/components/group-list-section.js
+7
-27
group-list-v2.js
src/sidebar/components/group-list-v2.js
+124
-0
group-list.js
src/sidebar/components/group-list.js
+1
-20
menu.js
src/sidebar/components/menu.js
+12
-6
group-list-item-out-of-scope-test.js
...ebar/components/test/group-list-item-out-of-scope-test.js
+0
-101
group-list-item-test.js
src/sidebar/components/test/group-list-item-test.js
+176
-20
group-list-section-test.js
src/sidebar/components/test/group-list-section-test.js
+24
-50
group-list-test.js
src/sidebar/components/test/group-list-test.js
+0
-108
group-list-v2-test.js
src/sidebar/components/test/group-list-v2-test.js
+166
-0
menu-test.js
src/sidebar/components/test/menu-test.js
+15
-0
index.js
src/sidebar/index.js
+2
-2
group-list.html
src/sidebar/templates/group-list.html
+9
-53
common.scss
src/styles/sidebar/common.scss
+0
-11
group-list-item.scss
src/styles/sidebar/components/group-list-item.scss
+8
-67
group-list-section.scss
src/styles/sidebar/components/group-list-section.scss
+0
-14
group-list-v2.scss
src/styles/sidebar/components/group-list-v2.scss
+3
-0
group-list.scss
src/styles/sidebar/components/group-list.scss
+0
-48
menu-item.scss
src/styles/sidebar/components/menu-item.scss
+2
-0
menu.scss
src/styles/sidebar/components/menu.scss
+8
-0
sidebar.scss
src/styles/sidebar/sidebar.scss
+1
-1
variables.scss
src/styles/variables.scss
+1
-1
No files found.
src/sidebar/components/group-list-item-out-of-scope.js
deleted
100644 → 0
View file @
521b1ffd
'use strict'
;
const
classnames
=
require
(
'classnames'
);
const
{
Fragment
,
createElement
}
=
require
(
'preact'
);
const
{
useState
}
=
require
(
'preact/hooks'
);
const
propTypes
=
require
(
'prop-types'
);
const
{
orgName
,
trackViewGroupActivity
,
}
=
require
(
'../util/group-list-item-common'
);
const
{
withServices
}
=
require
(
'../util/service-context'
);
const
outOfScopeIcon
=
(
<
svg
className
=
"svg-icon group-list-item-out-of-scope__icon--unavailable"
xmlns
=
"http://www.w3.org/2000/svg"
width
=
"100%"
height
=
"100%"
viewBox
=
"0 0 24 24"
>
<
path
fill
=
"none"
d
=
"M0 0h24v24H0V0z"
/>
<
path
d
=
"M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
<
/svg
>
);
function
GroupListItemOutOfScope
({
analytics
,
group
})
{
const
[
isExpanded
,
setExpanded
]
=
useState
(
false
);
const
toggleGroupDetails
=
event
=>
{
event
.
stopPropagation
();
setExpanded
(
!
isExpanded
);
};
const
groupOrgName
=
orgName
(
group
);
const
trackViewActivity
=
event
=>
{
event
.
stopPropagation
();
trackViewGroupActivity
(
analytics
);
};
return
(
<
div
className
=
"group-list-item__item group-list-item-out-of-scope__item"
onClick
=
{
toggleGroupDetails
}
tabIndex
=
"0"
>
{
/* the group icon */
}
<
div
className
=
"group-list-item__icon-container"
>
{
group
.
logo
&&
(
<
img
className
=
"group-list-item__icon group-list-item__icon--organization"
alt
=
{
groupOrgName
}
src
=
{
group
.
logo
}
/
>
)}
<
/div
>
{
/* the group name */
}
<
div
className
=
{
classnames
({
'group-list-item-out-of-scope__details'
:
true
,
expanded
:
isExpanded
,
})}
>
{
outOfScopeIcon
}
<
a
className
=
"group-list-item__name-link"
href
=
""
title
=
"This URL cannot be annotated in this group."
>
{
group
.
name
}
<
/a
>
<
br
/>
{
/* explanation of why group is not available */
}
{
!
isExpanded
&&
(
<
p
className
=
"group-list-item-out-of-scope__details-toggle"
>
Why
is
this
group
unavailable
?
<
/p
>
)}
{
isExpanded
&&
(
<
Fragment
>
<
p
className
=
"group-list-item-out-of-scope__details-unavailable-message"
>
This
group
has
been
restricted
to
selected
URLs
by
its
administrators
.
<
/p
>
{
group
.
links
.
html
&&
(
<
p
className
=
"group-list-item-out-of-scope__details-actions"
>
<
a
className
=
"button button--text group-list-item-out-of-scope__details-group-page-link"
href
=
{
group
.
links
.
html
}
target
=
"_blank"
onClick
=
{
trackViewActivity
}
rel
=
"noopener noreferrer"
>
Go
to
group
page
<
/a
>
<
/p
>
)}
<
/Fragment
>
)}
<
/div
>
<
/div
>
);
}
GroupListItemOutOfScope
.
propTypes
=
{
group
:
propTypes
.
object
,
analytics
:
propTypes
.
object
,
};
GroupListItemOutOfScope
.
injectedProps
=
[
'analytics'
];
module
.
exports
=
withServices
(
GroupListItemOutOfScope
);
src/sidebar/components/group-list-item.js
View file @
bc7085ce
'use strict'
;
'use strict'
;
const
classnames
=
require
(
'classnames'
);
const
propTypes
=
require
(
'prop-types'
);
const
propTypes
=
require
(
'prop-types'
);
const
{
createElement
}
=
require
(
'preact'
);
const
{
Fragment
,
createElement
}
=
require
(
'preact'
);
const
{
useState
}
=
require
(
'preact/hooks'
);
const
useStore
=
require
(
'../store/use-store'
);
const
useStore
=
require
(
'../store/use-store'
);
const
{
orgName
}
=
require
(
'../util/group-list-item-common'
);
const
{
orgName
}
=
require
(
'../util/group-list-item-common'
);
const
{
withServices
}
=
require
(
'../util/service-context'
);
const
{
withServices
}
=
require
(
'../util/service-context'
);
function
GroupListItem
({
analytics
,
group
})
{
const
MenuItem
=
require
(
'./menu-item'
);
/**
* An item in the groups selection menu.
*
* The item has a primary action which selects the group, along with a set of
* secondary actions accessible via a toggle menu.
*/
function
GroupListItem
({
analytics
,
defaultSubmenuOpen
=
false
,
group
,
groups
:
groupsService
,
})
{
const
canLeaveGroup
=
group
.
type
===
'private'
;
const
activityUrl
=
group
.
links
.
html
;
const
hasActionMenu
=
activityUrl
||
canLeaveGroup
;
const
isSelectable
=
!
group
.
scopes
.
enforced
||
group
.
isScopedToUri
;
const
[
isExpanded
,
setExpanded
]
=
useState
(
hasActionMenu
?
defaultSubmenuOpen
:
undefined
);
const
focusedGroupId
=
useStore
(
store
=>
store
.
focusedGroupId
());
const
isSelected
=
group
.
id
===
focusedGroupId
;
const
actions
=
useStore
(
store
=>
({
const
actions
=
useStore
(
store
=>
({
clearDirectLinkedGroupFetchFailed
:
store
.
clearDirectLinkedGroupFetchFailed
,
clearDirectLinkedGroupFetchFailed
:
store
.
clearDirectLinkedGroupFetchFailed
,
clearDirectLinkedIds
:
store
.
clearDirectLinkedIds
,
clearDirectLinkedIds
:
store
.
clearDirectLinkedIds
,
...
@@ -22,53 +46,86 @@ function GroupListItem({ analytics, group }) {
...
@@ -22,53 +46,86 @@ function GroupListItem({ analytics, group }) {
actions
.
focusGroup
(
group
.
id
);
actions
.
focusGroup
(
group
.
id
);
};
};
const
focusedGroupId
=
useStore
(
store
=>
store
.
focusedGroupId
());
const
leaveGroup
=
()
=>
{
const
isSelected
=
group
.
id
===
focusedGroupId
;
const
message
=
`Are you sure you want to leave the group "
${
group
.
name
}
"?`
;
const
groupOrgName
=
orgName
(
group
);
if
(
window
.
confirm
(
message
))
{
analytics
.
track
(
analytics
.
events
.
GROUP_LEAVE
);
groupsService
.
leave
(
group
.
id
);
}
};
const
toggleSubmenu
=
event
=>
{
event
.
stopPropagation
();
// Prevents group items opening a new window when clicked.
// TODO - Fix this more cleanly in `MenuItem`.
event
.
preventDefault
();
setExpanded
(
!
isExpanded
);
};
// Close the submenu when any clicks happen which close the top-level menu.
const
collapseSubmenu
=
()
=>
setExpanded
(
false
);
return
(
return
(
<
div
<
Fragment
>
className
=
{
classnames
({
<
MenuItem
'group-list-item__item'
:
true
,
icon
=
{
group
.
logo
||
null
}
'is-selected'
:
isSelected
,
iconAlt
=
{
orgName
(
group
)}
})}
isDisabled
=
{
!
isSelectable
}
onClick
=
{
focusGroup
}
isExpanded
=
{
isExpanded
}
tabIndex
=
"0"
isSelected
=
{
isSelected
}
>
isSubmenuVisible
=
{
isExpanded
}
{
/* the group icon */
}
label
=
{
group
.
name
}
<
div
className
=
"group-list-item__icon-container"
>
onClick
=
{
isSelectable
?
focusGroup
:
toggleSubmenu
}
{
group
.
logo
&&
(
onToggleSubmenu
=
{
toggleSubmenu
}
<
img
/
>
className
=
"group-list-item__icon group-list-item__icon--organization"
{
isExpanded
&&
(
alt
=
{
groupOrgName
}
<
Fragment
>
src
=
{
group
.
logo
}
<
ul
onClick
=
{
collapseSubmenu
}
>
/
>
{
activityUrl
&&
(
)}
<
li
>
<
/div
>
<
MenuItem
{
/* the group name */
}
href
=
{
activityUrl
}
<
div
className
=
"group-list-item__details"
>
icon
=
"share"
<
a
isSubmenuItem
=
{
true
}
className
=
"group-list-item__name-link"
label
=
"View group activity"
href
=
""
/>
title
=
{
<
/li
>
group
.
type
===
'private'
)}
?
`Show and create annotations in
${
group
.
name
}
`
{
canLeaveGroup
&&
(
:
'Show public annotations'
<
li
>
}
<
MenuItem
>
icon
=
"leave"
{
group
.
name
}
isSubmenuItem
=
{
true
}
<
/a
>
label
=
"Leave group"
<
/div
>
onClick
=
{
leaveGroup
}
<
/div
>
/
>
<
/li
>
)}
<
/ul
>
{
!
isSelectable
&&
(
<
p
className
=
"group-list-item__footer"
>
This
group
is
restricted
to
specific
URLs
.
<
/p
>
)}
<
/Fragment
>
)}
<
/Fragment
>
);
);
}
}
GroupListItem
.
propTypes
=
{
GroupListItem
.
propTypes
=
{
group
:
propTypes
.
object
.
isRequired
,
group
:
propTypes
.
object
.
isRequired
,
/** Whether the submenu is open when the item is initially rendered. */
defaultSubmenuOpen
:
propTypes
.
bool
,
// Injected services.
analytics
:
propTypes
.
object
.
isRequired
,
analytics
:
propTypes
.
object
.
isRequired
,
groups
:
propTypes
.
object
.
isRequired
,
};
};
GroupListItem
.
injectedProps
=
[
'analytics'
];
GroupListItem
.
injectedProps
=
[
'analytics'
,
'groups'
];
module
.
exports
=
withServices
(
GroupListItem
);
module
.
exports
=
withServices
(
GroupListItem
);
src/sidebar/components/group-list-section.js
View file @
bc7085ce
'use strict'
;
'use strict'
;
const
{
Fragment
,
createElement
}
=
require
(
'preact'
);
const
{
createElement
}
=
require
(
'preact'
);
const
propTypes
=
require
(
'prop-types'
);
const
propTypes
=
require
(
'prop-types'
);
const
GroupListItem
=
require
(
'./group-list-item'
);
const
GroupListItem
=
require
(
'./group-list-item'
);
const
GroupListItemOutOfScope
=
require
(
'./group-list-item-out-of-scope
'
);
const
MenuSection
=
require
(
'./menu-section
'
);
/**
/**
* A labeled section of the groups list.
* A labeled section of the groups list.
*/
*/
function
GroupListSection
({
groups
,
heading
})
{
function
GroupListSection
({
groups
,
heading
})
{
const
isSelectable
=
groupId
=>
{
const
group
=
groups
.
find
(
g
=>
g
.
id
===
groupId
);
return
!
group
.
scopes
.
enforced
||
group
.
isScopedToUri
;
};
return
(
return
(
<
Fragment
>
<
MenuSection
heading
=
{
heading
}
>
<
h2
className
=
"group-list-section__heading"
>
{
heading
}
<
/h2
>
{
groups
.
map
(
group
=>
(
<
ul
className
=
"group-list-section__content"
>
<
GroupListItem
key
=
{
group
.
id
}
group
=
{
group
}
/
>
{
groups
.
map
(
group
=>
(
))}
<
li
<
/MenuSection
>
className
=
"dropdown-menu__row dropdown-menu__row--no-border dropdown-menu__row--unpadded"
key
=
{
group
.
id
}
>
{
isSelectable
(
group
.
id
)
?
(
<
GroupListItem
className
=
"group-list-item"
group
=
{
group
}
/
>
)
:
(
<
GroupListItemOutOfScope
className
=
"group-list-item-out-of-scope"
group
=
{
group
}
/
>
)}
<
/li
>
))}
<
/ul
>
<
/Fragment
>
);
);
}
}
...
...
src/sidebar/components/group-list-v2.js
0 → 100644
View file @
bc7085ce
'use strict'
;
const
{
createElement
}
=
require
(
'preact'
);
const
{
useMemo
}
=
require
(
'preact/hooks'
);
const
propTypes
=
require
(
'prop-types'
);
const
isThirdPartyService
=
require
(
'../util/is-third-party-service'
);
const
{
isThirdPartyUser
}
=
require
(
'../util/account-id'
);
const
groupsByOrganization
=
require
(
'../util/group-organizations'
);
const
useStore
=
require
(
'../store/use-store'
);
const
{
withServices
}
=
require
(
'../util/service-context'
);
const
serviceConfig
=
require
(
'../service-config'
);
const
Menu
=
require
(
'./menu'
);
const
MenuItem
=
require
(
'./menu-item'
);
const
GroupListSection
=
require
(
'./group-list-section'
);
/**
* Return the custom icon for the top bar configured by the publisher in
* the Hypothesis client configuration.
*/
function
publisherProvidedIcon
(
settings
)
{
const
svc
=
serviceConfig
(
settings
);
return
svc
&&
svc
.
icon
?
svc
.
icon
:
null
;
}
/**
* Menu allowing the user to select which group to show and also access
* additional actions related to groups.
*/
function
GroupList
({
serviceUrl
,
settings
})
{
const
currentGroups
=
useStore
(
store
=>
store
.
getCurrentlyViewingGroups
());
const
featuredGroups
=
useStore
(
store
=>
store
.
getFeaturedGroups
());
const
myGroups
=
useStore
(
store
=>
store
.
getMyGroups
());
const
focusedGroup
=
useStore
(
store
=>
store
.
focusedGroup
());
const
userid
=
useStore
(
store
=>
store
.
profile
().
userid
);
const
myGroupsSorted
=
useMemo
(()
=>
groupsByOrganization
(
myGroups
),
[
myGroups
,
]);
const
featuredGroupsSorted
=
useMemo
(
()
=>
groupsByOrganization
(
featuredGroups
),
[
featuredGroups
]
);
const
currentGroupsSorted
=
useMemo
(
()
=>
groupsByOrganization
(
currentGroups
),
[
currentGroups
]
);
const
{
authDomain
}
=
settings
;
const
canCreateNewGroup
=
userid
&&
!
isThirdPartyUser
(
userid
,
authDomain
);
const
newGroupLink
=
serviceUrl
(
'groups.new'
);
let
label
;
if
(
focusedGroup
)
{
const
icon
=
focusedGroup
.
organization
.
logo
;
label
=
(
<
span
>
<
img
className
=
"group-list-label__icon group-list-label__icon--organization"
src
=
{
icon
||
publisherProvidedIcon
(
settings
)}
/
>
<
span
className
=
"group-list-label__label"
>
{
focusedGroup
.
name
}
<
/span
>
<
/span
>
);
}
else
{
label
=
<
span
>
…
<
/span>
;
}
// If there is only one group and no actions available for that group,
// just show the group name as a label.
const
actionsAvailable
=
!
isThirdPartyService
(
settings
);
if
(
!
actionsAvailable
&&
currentGroups
.
length
+
featuredGroups
.
length
+
myGroups
.
length
<
2
)
{
return
label
;
}
return
(
<
Menu
align
=
"left"
contentClass
=
"group-list-v2__content"
label
=
{
label
}
title
=
"Select group"
>
{
currentGroupsSorted
.
length
>
0
&&
(
<
GroupListSection
heading
=
"Currently Viewing"
groups
=
{
currentGroupsSorted
}
/
>
)}
{
featuredGroupsSorted
.
length
>
0
&&
(
<
GroupListSection
heading
=
"Featured Groups"
groups
=
{
featuredGroupsSorted
}
/
>
)}
{
myGroupsSorted
.
length
>
0
&&
(
<
GroupListSection
heading
=
"My Groups"
groups
=
{
myGroupsSorted
}
/
>
)}
{
canCreateNewGroup
&&
(
<
MenuItem
icon
=
"add-group"
href
=
{
newGroupLink
}
label
=
"New private group"
style
=
"shaded"
/>
)}
<
/Menu
>
);
}
GroupList
.
propTypes
=
{
serviceUrl
:
propTypes
.
func
,
settings
:
propTypes
.
object
,
};
GroupList
.
injectedProps
=
[
'serviceUrl'
,
'settings'
];
module
.
exports
=
withServices
(
GroupList
);
src/sidebar/components/group-list.js
View file @
bc7085ce
...
@@ -8,12 +8,6 @@ const groupsByOrganization = require('../util/group-organizations');
...
@@ -8,12 +8,6 @@ const groupsByOrganization = require('../util/group-organizations');
const
groupOrganizations
=
memoize
(
groupsByOrganization
);
const
groupOrganizations
=
memoize
(
groupsByOrganization
);
const
myGroupOrgs
=
memoize
(
groupsByOrganization
);
const
featuredGroupOrgs
=
memoize
(
groupsByOrganization
);
const
currentlyViewingGroupOrgs
=
memoize
(
groupsByOrganization
);
// @ngInject
// @ngInject
function
GroupListController
(
function
GroupListController
(
$window
,
$window
,
...
@@ -21,8 +15,7 @@ function GroupListController(
...
@@ -21,8 +15,7 @@ function GroupListController(
features
,
features
,
groups
,
groups
,
settings
,
settings
,
serviceUrl
,
serviceUrl
store
)
{
)
{
this
.
groups
=
groups
;
this
.
groups
=
groups
;
...
@@ -66,18 +59,6 @@ function GroupListController(
...
@@ -66,18 +59,6 @@ function GroupListController(
return
groupOrganizations
(
this
.
groups
.
all
());
return
groupOrganizations
(
this
.
groups
.
all
());
};
};
this
.
currentlyViewingGroupOrganizations
=
function
()
{
return
currentlyViewingGroupOrgs
(
store
.
getCurrentlyViewingGroups
());
};
this
.
featuredGroupOrganizations
=
function
()
{
return
featuredGroupOrgs
(
store
.
getFeaturedGroups
());
};
this
.
myGroupOrganizations
=
function
()
{
return
myGroupOrgs
(
store
.
getMyGroups
());
};
this
.
viewGroupActivity
=
function
()
{
this
.
viewGroupActivity
=
function
()
{
analytics
.
track
(
analytics
.
events
.
GROUP_VIEW_ACTIVITY
);
analytics
.
track
(
analytics
.
events
.
GROUP_VIEW_ACTIVITY
);
};
};
...
...
src/sidebar/components/menu.js
View file @
bc7085ce
...
@@ -45,6 +45,7 @@ let ignoreNextClick = false;
...
@@ -45,6 +45,7 @@ let ignoreNextClick = false;
function
Menu
({
function
Menu
({
align
=
'left'
,
align
=
'left'
,
children
,
children
,
contentClass
,
defaultOpen
=
false
,
defaultOpen
=
false
,
label
,
label
,
menuIndicator
=
true
,
menuIndicator
=
true
,
...
@@ -117,7 +118,9 @@ function Menu({
...
@@ -117,7 +118,9 @@ function Menu({
>
>
{
label
}
{
label
}
{
menuIndicator
&&
(
{
menuIndicator
&&
(
<
span
className
=
"menu__toggle-arrow"
>
<
span
className
=
{
classnames
(
'menu__toggle-arrow'
,
isOpen
&&
'is-open'
)}
>
<
SvgIcon
name
=
"expand-menu"
className
=
"menu__toggle-icon"
/>
<
SvgIcon
name
=
"expand-menu"
className
=
"menu__toggle-icon"
/>
<
/span
>
<
/span
>
)}
)}
...
@@ -128,7 +131,8 @@ function Menu({
...
@@ -128,7 +131,8 @@ function Menu({
<
div
<
div
className
=
{
classnames
(
className
=
{
classnames
(
'menu__content'
,
'menu__content'
,
`menu__content--align-
${
align
}
`
`menu__content--align-
${
align
}
`
,
contentClass
)}
)}
role
=
"menu"
role
=
"menu"
>
>
...
@@ -161,10 +165,12 @@ Menu.propTypes = {
...
@@ -161,10 +165,12 @@ Menu.propTypes = {
* These are typically `MenuSection` and `MenuItem` components, but other
* These are typically `MenuSection` and `MenuItem` components, but other
* custom content is also allowed.
* custom content is also allowed.
*/
*/
children
:
propTypes
.
oneOfType
([
children
:
propTypes
.
any
,
propTypes
.
object
,
propTypes
.
arrayOf
(
propTypes
.
object
),
/**
]),
* Additional CSS classes to apply to the menu.
*/
contentClass
:
propTypes
.
string
,
/**
/**
* Whether the menu is open or closed when initially rendered.
* Whether the menu is open or closed when initially rendered.
...
...
src/sidebar/components/test/group-list-item-out-of-scope-test.js
deleted
100644 → 0
View file @
521b1ffd
'use strict'
;
const
{
mount
}
=
require
(
'enzyme'
);
const
{
createElement
}
=
require
(
'preact'
);
const
{
events
}
=
require
(
'../../services/analytics'
);
const
GroupListItemOutOfScope
=
require
(
'../group-list-item-out-of-scope'
);
describe
(
'GroupListItemOutOfScope'
,
()
=>
{
let
fakeAnalytics
;
let
fakeGroupListItemCommon
;
const
fakeGroup
=
{
id
:
'groupid'
,
links
:
{
html
:
'https://hypothes.is/groups/groupid'
,
},
logo
:
'dummy://hypothes.is/logo.svg'
,
organization
:
{
name
:
'org'
},
};
// Click on the item to expand or collapse it.
const
toggle
=
wrapper
=>
wrapper
.
find
(
'div'
)
.
first
()
.
simulate
(
'click'
);
beforeEach
(()
=>
{
fakeAnalytics
=
{
track
:
sinon
.
stub
(),
events
,
};
fakeGroupListItemCommon
=
{
orgName
:
sinon
.
stub
(),
trackViewGroupActivity
:
sinon
.
stub
(),
};
GroupListItemOutOfScope
.
$imports
.
$mock
({
'../util/group-list-item-common'
:
fakeGroupListItemCommon
,
});
});
afterEach
(()
=>
{
GroupListItemOutOfScope
.
$imports
.
$restore
();
});
const
createGroupListItemOutOfScope
=
fakeGroup
=>
{
return
mount
(
<
GroupListItemOutOfScope
analytics
=
{
fakeAnalytics
}
group
=
{
fakeGroup
}
/
>
);
};
it
(
'calls trackViewGroupActivity when "Go to group page" link is clicked'
,
()
=>
{
const
wrapper
=
createGroupListItemOutOfScope
(
fakeGroup
);
toggle
(
wrapper
);
const
link
=
wrapper
.
find
(
'a'
)
.
filterWhere
(
link
=>
link
.
text
()
===
'Go to group page'
);
link
.
simulate
(
'click'
);
assert
.
calledWith
(
fakeGroupListItemCommon
.
trackViewGroupActivity
,
fakeAnalytics
);
});
it
(
'does not show "Go to group page" link if the group has no HTML link'
,
()
=>
{
const
group
=
{
...
fakeGroup
,
links
:
{}
};
const
wrapper
=
createGroupListItemOutOfScope
(
group
);
const
link
=
wrapper
.
find
(
'a'
)
.
filterWhere
(
link
=>
link
.
text
()
===
'Go to group page'
);
assert
.
isFalse
(
link
.
exists
());
});
it
(
'sets alt text of logo'
,
()
=>
{
fakeGroupListItemCommon
.
orgName
.
withArgs
(
fakeGroup
)
.
returns
(
fakeGroup
.
organization
.
name
);
const
wrapper
=
createGroupListItemOutOfScope
(
fakeGroup
);
const
orgName
=
wrapper
.
find
(
'img'
).
props
().
alt
;
assert
.
equal
(
orgName
,
fakeGroup
.
organization
.
name
);
});
it
(
'toggles expanded state when clicked'
,
()
=>
{
const
wrapper
=
createGroupListItemOutOfScope
(
fakeGroup
);
assert
.
isFalse
(
wrapper
.
exists
(
'.expanded'
));
toggle
(
wrapper
);
assert
.
isTrue
(
wrapper
.
exists
(
'.expanded'
));
toggle
(
wrapper
);
assert
.
isFalse
(
wrapper
.
exists
(
'.expanded'
));
});
});
src/sidebar/components/test/group-list-item-test.js
View file @
bc7085ce
'use strict'
;
'use strict'
;
const
{
createElement
}
=
require
(
'preact'
);
const
{
createElement
}
=
require
(
'preact'
);
const
{
act
}
=
require
(
'preact/test-utils'
);
const
{
mount
}
=
require
(
'enzyme'
);
const
{
mount
}
=
require
(
'enzyme'
);
const
GroupListItem
=
require
(
'../group-list-item'
);
const
GroupListItem
=
require
(
'../group-list-item'
);
...
@@ -9,10 +10,24 @@ const { events } = require('../../services/analytics');
...
@@ -9,10 +10,24 @@ const { events } = require('../../services/analytics');
describe
(
'GroupListItem'
,
()
=>
{
describe
(
'GroupListItem'
,
()
=>
{
let
fakeAnalytics
;
let
fakeAnalytics
;
let
fakeGroupsService
;
let
fakeStore
;
let
fakeStore
;
let
fakeGroupListItemCommon
;
let
fakeGroupListItemCommon
;
let
fakeGroup
;
beforeEach
(()
=>
{
beforeEach
(()
=>
{
fakeGroup
=
{
id
:
'groupid'
,
name
:
'Test'
,
links
:
{
html
:
'https://annotate.com/groups/groupid'
,
},
scopes
:
{
enforced
:
false
,
},
type
:
'private'
,
};
fakeStore
=
{
fakeStore
=
{
focusGroup
:
sinon
.
stub
(),
focusGroup
:
sinon
.
stub
(),
focusedGroupId
:
sinon
.
stub
().
returns
(
'groupid'
),
focusedGroupId
:
sinon
.
stub
().
returns
(
'groupid'
),
...
@@ -29,69 +44,90 @@ describe('GroupListItem', () => {
...
@@ -29,69 +44,90 @@ describe('GroupListItem', () => {
orgName
:
sinon
.
stub
(),
orgName
:
sinon
.
stub
(),
};
};
fakeGroupsService
=
{
leave
:
sinon
.
stub
(),
};
function
FakeMenuItem
()
{
return
null
;
}
FakeMenuItem
.
displayName
=
'MenuItem'
;
GroupListItem
.
$imports
.
$mock
({
GroupListItem
.
$imports
.
$mock
({
'./menu-item'
:
FakeMenuItem
,
'../util/group-list-item-common'
:
fakeGroupListItemCommon
,
'../util/group-list-item-common'
:
fakeGroupListItemCommon
,
'../store/use-store'
:
callback
=>
callback
(
fakeStore
),
'../store/use-store'
:
callback
=>
callback
(
fakeStore
),
});
});
sinon
.
stub
(
window
,
'confirm'
).
returns
(
false
);
});
});
afterEach
(()
=>
{
afterEach
(()
=>
{
GroupListItem
.
$imports
.
$restore
();
GroupListItem
.
$imports
.
$restore
();
window
.
confirm
.
restore
();
});
});
const
createGroupListItem
=
fakeGroup
=>
{
const
createGroupListItem
=
(
fakeGroup
,
props
=
{})
=>
{
// nb. Mount rendering is used here with a manually mocked `MenuItem`
// because `GroupListItem` renders multiple top-level elements (wrapped in
// a fragment) and `wrapper.update()` cannot be used in that case when using
// shallow rendering.
return
mount
(
return
mount
(
<
GroupListItem
<
GroupListItem
group
=
{
fakeGroup
}
group
=
{
fakeGroup
}
groups
=
{
fakeGroupsService
}
analytics
=
{
fakeAnalytics
}
analytics
=
{
fakeAnalytics
}
store
=
{
fakeStore
}
{...
props
}
/
>
/
>
);
);
};
};
it
(
'changes the focused group when group is clicked'
,
()
=>
{
it
(
'changes the focused group when group is clicked'
,
()
=>
{
const
fakeGroup
=
{
id
:
'groupid'
};
const
wrapper
=
createGroupListItem
(
fakeGroup
);
const
wrapper
=
createGroupListItem
(
fakeGroup
);
wrapper
.
find
(
'.group-list-item__item'
).
simulate
(
'click'
);
wrapper
.
find
(
'MenuItem'
)
.
props
()
.
onClick
();
assert
.
calledWith
(
fakeStore
.
focusGroup
,
fakeGroup
.
id
);
assert
.
calledWith
(
fakeStore
.
focusGroup
,
fakeGroup
.
id
);
assert
.
calledWith
(
fakeAnalytics
.
track
,
fakeAnalytics
.
events
.
GROUP_SWITCH
);
assert
.
calledWith
(
fakeAnalytics
.
track
,
fakeAnalytics
.
events
.
GROUP_SWITCH
);
});
});
it
(
'clears the direct linked ids from the store when the group is clicked'
,
()
=>
{
it
(
'clears the direct linked ids from the store when the group is clicked'
,
()
=>
{
const
fakeGroup
=
{
id
:
'groupid'
};
const
wrapper
=
createGroupListItem
(
fakeGroup
);
const
wrapper
=
createGroupListItem
(
fakeGroup
);
wrapper
.
find
(
'.group-list-item__item'
).
simulate
(
'click'
);
wrapper
.
find
(
'MenuItem'
)
.
props
()
.
onClick
();
assert
.
calledOnce
(
fakeStore
.
clearDirectLinkedIds
);
assert
.
calledOnce
(
fakeStore
.
clearDirectLinkedIds
);
});
});
it
(
'clears the direct-linked group fetch failed from the store when the group is clicked'
,
()
=>
{
it
(
'clears the direct-linked group fetch failed from the store when the group is clicked'
,
()
=>
{
const
fakeGroup
=
{
id
:
'groupid'
};
const
wrapper
=
createGroupListItem
(
fakeGroup
);
const
wrapper
=
createGroupListItem
(
fakeGroup
);
wrapper
.
find
(
'.group-list-item__item'
).
simulate
(
'click'
);
wrapper
.
find
(
'MenuItem'
)
.
props
()
.
onClick
();
assert
.
calledOnce
(
fakeStore
.
clearDirectLinkedGroupFetchFailed
);
assert
.
calledOnce
(
fakeStore
.
clearDirectLinkedGroupFetchFailed
);
});
});
it
(
'sets alt text for organization logo'
,
()
=>
{
it
(
'sets alt text for organization logo'
,
()
=>
{
const
fakeG
roup
=
{
const
g
roup
=
{
id
:
'groupid'
,
...
fakeGroup
,
// Dummy scheme to avoid actually trying to load image.
// Dummy scheme to avoid actually trying to load image.
logo
:
'dummy://hypothes.is/logo.svg'
,
logo
:
'dummy://hypothes.is/logo.svg'
,
organization
:
{
name
:
'org'
},
organization
:
{
name
:
'org'
},
};
};
fakeGroupListItemCommon
.
orgName
fakeGroupListItemCommon
.
orgName
.
withArgs
(
fakeG
roup
)
.
withArgs
(
g
roup
)
.
returns
(
fakeG
roup
.
organization
.
name
);
.
returns
(
g
roup
.
organization
.
name
);
const
wrapper
=
createGroupListItem
(
fakeG
roup
);
const
wrapper
=
createGroupListItem
(
g
roup
);
const
altText
=
wrapper
.
find
(
'
img'
).
props
().
alt
;
const
altText
=
wrapper
.
find
(
'
MenuItem'
).
prop
(
'iconAlt'
)
;
assert
.
equal
(
altText
,
fakeG
roup
.
organization
.
name
);
assert
.
equal
(
altText
,
g
roup
.
organization
.
name
);
});
});
describe
(
'selected state'
,
()
=>
{
describe
(
'selected state'
,
()
=>
{
...
@@ -109,15 +145,135 @@ describe('GroupListItem', () => {
...
@@ -109,15 +145,135 @@ describe('GroupListItem', () => {
].
forEach
(({
description
,
focusedGroupId
,
expectedIsSelected
})
=>
{
].
forEach
(({
description
,
focusedGroupId
,
expectedIsSelected
})
=>
{
it
(
description
,
()
=>
{
it
(
description
,
()
=>
{
fakeStore
.
focusedGroupId
.
returns
(
focusedGroupId
);
fakeStore
.
focusedGroupId
.
returns
(
focusedGroupId
);
const
fakeGroup
=
{
id
:
'groupid'
};
const
wrapper
=
createGroupListItem
(
fakeGroup
);
const
wrapper
=
createGroupListItem
(
fakeGroup
);
assert
.
equal
(
assert
.
equal
(
wrapper
.
find
(
'
.group-list-item__item'
).
hasClass
(
'is-s
elected'
),
wrapper
.
find
(
'
MenuItem'
).
prop
(
'isS
elected'
),
expectedIsSelected
expectedIsSelected
);
);
});
});
});
});
});
});
it
(
'toggles submenu when toggle is clicked'
,
()
=>
{
const
wrapper
=
createGroupListItem
(
fakeGroup
);
const
toggleSubmenu
=
()
=>
{
const
dummyEvent
=
new
Event
();
act
(()
=>
{
wrapper
.
find
(
'MenuItem'
)
.
first
()
.
props
()
.
onToggleSubmenu
(
dummyEvent
);
});
wrapper
.
update
();
};
toggleSubmenu
();
assert
.
isTrue
(
wrapper
.
exists
(
'ul'
));
toggleSubmenu
();
assert
.
isFalse
(
wrapper
.
exists
(
'ul'
));
});
it
(
'does not show submenu toggle if there are no available actions'
,
()
=>
{
fakeGroup
.
links
.
html
=
null
;
fakeGroup
.
type
=
'open'
;
const
wrapper
=
createGroupListItem
(
fakeGroup
);
assert
.
isUndefined
(
wrapper
.
find
(
'MenuItem'
).
prop
(
'isExpanded'
));
});
it
(
'does not show link to activity page if not available'
,
()
=>
{
fakeGroup
.
links
.
html
=
null
;
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
defaultSubmenuOpen
:
true
,
});
assert
.
isFalse
(
wrapper
.
exists
(
'MenuItem[label="View group activity"]'
));
});
it
(
'shows link to activity page if available'
,
()
=>
{
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
defaultSubmenuOpen
:
true
,
});
assert
.
isTrue
(
wrapper
.
exists
(
'MenuItem[label="View group activity"]'
));
});
it
(
'does not show "Leave" action if user cannot leave'
,
()
=>
{
fakeGroup
.
type
=
'open'
;
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
defaultSubmenuOpen
:
true
,
});
assert
.
isFalse
(
wrapper
.
exists
(
'MenuItem[label="Leave group"]'
));
});
it
(
'shows "Leave" action if user can leave'
,
()
=>
{
fakeGroup
.
type
=
'private'
;
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
defaultSubmenuOpen
:
true
,
});
assert
.
isTrue
(
wrapper
.
exists
(
'MenuItem[label="Leave group"]'
));
});
it
(
'prompts to leave group if "Leave" action is clicked'
,
()
=>
{
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
defaultSubmenuOpen
:
true
,
});
act
(()
=>
{
wrapper
.
find
(
'MenuItem[label="Leave group"]'
)
.
props
()
.
onClick
();
});
assert
.
called
(
window
.
confirm
);
assert
.
notCalled
(
fakeGroupsService
.
leave
);
});
it
(
'leaves group if "Leave" is clicked and user confirms'
,
()
=>
{
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
defaultSubmenuOpen
:
true
,
});
window
.
confirm
.
returns
(
true
);
act
(()
=>
{
wrapper
.
find
(
'MenuItem[label="Leave group"]'
)
.
props
()
.
onClick
();
});
assert
.
called
(
window
.
confirm
);
assert
.
calledWith
(
fakeGroupsService
.
leave
,
fakeGroup
.
id
);
});
[
{
enforced
:
false
,
isScopedToUri
:
false
,
expectDisabled
:
false
,
},
{
enforced
:
true
,
isScopedToUri
:
false
,
expectDisabled
:
true
,
},
{
enforced
:
true
,
isScopedToUri
:
true
,
expectDisabled
:
false
,
},
].
forEach
(({
enforced
,
isScopedToUri
,
expectDisabled
})
=>
{
it
(
'disables menu item and shows note in submenu if group is not selectable'
,
()
=>
{
fakeGroup
.
scopes
.
enforced
=
enforced
;
fakeGroup
.
isScopedToUri
=
isScopedToUri
;
const
wrapper
=
createGroupListItem
(
fakeGroup
,
{
defaultSubmenuOpen
:
true
,
});
assert
.
equal
(
wrapper
.
find
(
'MenuItem'
)
.
first
()
.
prop
(
'isDisabled'
),
expectDisabled
);
assert
.
equal
(
wrapper
.
exists
(
'.group-list-item__footer'
),
expectDisabled
);
});
});
});
});
src/sidebar/components/test/group-list-section-test.js
View file @
bc7085ce
...
@@ -5,60 +5,34 @@ const { createElement } = require('preact');
...
@@ -5,60 +5,34 @@ const { createElement } = require('preact');
const
GroupListSection
=
require
(
'../group-list-section'
);
const
GroupListSection
=
require
(
'../group-list-section'
);
const
GroupListItem
=
require
(
'../group-list-item'
);
const
GroupListItem
=
require
(
'../group-list-item'
);
const
GroupListItemOutOfScope
=
require
(
'../group-list-item-out-of-scope
'
);
const
MenuSection
=
require
(
'../menu-section
'
);
describe
(
'GroupListSection'
,
()
=>
{
describe
(
'GroupListSection'
,
()
=>
{
const
createGroupListSection
=
groups
=>
{
const
testGroups
=
[
return
shallow
(
{
<
GroupListSection
groups
=
{
groups
}
analytics
=
{{}}
store
=
{{}}
/
>
id
:
'group1'
,
);
name
:
'Group 1'
,
};
},
{
id
:
'group2'
,
name
:
'Group 2'
,
},
];
describe
(
'group item types'
,
()
=>
{
const
createGroupListSection
=
({
[
groups
=
testGroups
,
{
heading
=
'Test section'
,
description
:
}
=
{})
=>
{
'renders GroupListItem if group is out of scope but scope is not enforced'
,
return
shallow
(
<
GroupListSection
groups
=
{
groups
}
heading
=
{
heading
}
/>
)
;
scopesEnforced
:
false
,
};
expectedIsSelectable
:
[
true
,
true
],
},
{
description
:
'renders GroupListItemOutOfScope if group is out of scope and scope is enforced'
,
scopesEnforced
:
true
,
expectedIsSelectable
:
[
true
,
false
],
},
].
forEach
(({
description
,
scopesEnforced
,
expectedIsSelectable
})
=>
{
it
(
description
,
()
=>
{
const
groups
=
[
{
isScopedToUri
:
true
,
scopes
:
{
enforced
:
scopesEnforced
},
id
:
0
,
},
{
isScopedToUri
:
false
,
scopes
:
{
enforced
:
scopesEnforced
},
id
:
1
,
},
];
const
wrapper
=
createGroupListSection
(
groups
);
it
(
'renders heading'
,
()
=>
{
const
wrapper
=
createGroupListSection
();
assert
.
equal
(
wrapper
.
find
(
MenuSection
).
prop
(
'heading'
),
'Test section'
);
});
// Check that the correct group item components were rendered for
it
(
'renders groups'
,
()
=>
{
// each group, depending on whether the group can be annotated in on
const
wrapper
=
createGroupListSection
();
// the current document.
assert
.
equal
(
wrapper
.
find
(
GroupListItem
).
length
,
testGroups
.
length
);
const
itemTypes
=
wrapper
.
findWhere
(
n
=>
n
.
type
()
===
GroupListItem
||
n
.
type
()
===
GroupListItemOutOfScope
)
.
map
(
item
=>
item
.
type
());
const
expectedItemTypes
=
groups
.
map
(
g
=>
expectedIsSelectable
[
g
.
id
]
?
GroupListItem
:
GroupListItemOutOfScope
);
assert
.
deepEqual
(
itemTypes
,
expectedItemTypes
);
});
});
});
});
});
});
src/sidebar/components/test/group-list-test.js
View file @
bc7085ce
...
@@ -51,7 +51,6 @@ describe('groupList', function() {
...
@@ -51,7 +51,6 @@ describe('groupList', function() {
let
fakeServiceUrl
;
let
fakeServiceUrl
;
let
fakeSettings
;
let
fakeSettings
;
let
fakeFeatures
;
let
fakeFeatures
;
let
fakeStore
;
before
(
function
()
{
before
(
function
()
{
angular
angular
...
@@ -67,12 +66,6 @@ describe('groupList', function() {
...
@@ -67,12 +66,6 @@ describe('groupList', function() {
flagEnabled
:
sinon
.
stub
().
returns
(
false
),
flagEnabled
:
sinon
.
stub
().
returns
(
false
),
};
};
fakeStore
=
{
getCurrentlyViewingGroups
:
sinon
.
stub
().
returns
([]),
getFeaturedGroups
:
sinon
.
stub
().
returns
([]),
getMyGroups
:
sinon
.
stub
().
returns
([]),
};
fakeAnalytics
=
{
fakeAnalytics
=
{
track
:
sinon
.
stub
(),
track
:
sinon
.
stub
(),
events
,
events
,
...
@@ -88,7 +81,6 @@ describe('groupList', function() {
...
@@ -88,7 +81,6 @@ describe('groupList', function() {
serviceUrl
:
fakeServiceUrl
,
serviceUrl
:
fakeServiceUrl
,
settings
:
fakeSettings
,
settings
:
fakeSettings
,
features
:
fakeFeatures
,
features
:
fakeFeatures
,
store
:
fakeStore
,
});
});
});
});
...
@@ -372,70 +364,6 @@ describe('groupList', function() {
...
@@ -372,70 +364,6 @@ describe('groupList', function() {
);
);
});
});
describe
(
'group section visibility'
,
()
=>
{
[
{
description
:
'shows Currently Viewing section when there are currently viewing groups'
,
currentlyViewingGroups
:
[
publicGroup
],
featuredGroups
:
[
restrictedGroup
],
myGroups
:
[],
expectedSections
:
[
"'Currently Viewing'"
,
"'Featured Groups'"
],
},
{
description
:
'shows Featured Groups section when there are featured groups'
,
currentlyViewingGroups
:
[],
featuredGroups
:
[
restrictedGroup
],
myGroups
:
[
publicGroup
],
expectedSections
:
[
"'Featured Groups'"
,
"'My Groups'"
],
},
{
description
:
'shows My Groups section when there are my groups'
,
currentlyViewingGroups
:
[],
featuredGroups
:
[],
myGroups
:
[
publicGroup
,
privateGroup
],
expectedSections
:
[
"'My Groups'"
],
},
].
forEach
(
({
description
,
currentlyViewingGroups
,
featuredGroups
,
myGroups
,
expectedSections
,
})
=>
{
it
(
description
,
()
=>
{
fakeFeatures
.
flagEnabled
.
withArgs
(
'community_groups'
).
returns
(
true
);
// In order to show the group drop down there must be at least two groups.
groups
=
currentlyViewingGroups
.
concat
(
featuredGroups
)
.
concat
(
myGroups
);
fakeStore
.
getCurrentlyViewingGroups
.
returns
(
currentlyViewingGroups
);
fakeStore
.
getFeaturedGroups
.
returns
(
featuredGroups
);
fakeStore
.
getMyGroups
.
returns
(
myGroups
);
const
element
=
createGroupList
();
const
showGroupsMenu
=
element
.
ctrl
.
showGroupsMenu
();
const
dropdownToggle
=
element
.
find
(
'.dropdown-toggle'
);
const
arrowIcon
=
element
.
find
(
'.h-icon-arrow-drop-down'
);
const
groupListSection
=
element
.
find
(
'group-list-section'
);
assert
.
isTrue
(
showGroupsMenu
);
assert
.
lengthOf
(
dropdownToggle
,
1
);
assert
.
lengthOf
(
arrowIcon
,
1
);
assert
.
lengthOf
(
groupListSection
,
expectedSections
.
length
);
groupListSection
.
each
(
function
()
{
assert
.
isTrue
(
expectedSections
.
includes
(
this
.
getAttribute
(
'heading'
))
);
});
});
}
);
});
describe
(
'group menu visibility'
,
()
=>
{
describe
(
'group menu visibility'
,
()
=>
{
it
(
'is hidden when third party service and only one group'
,
function
()
{
it
(
'is hidden when third party service and only one group'
,
function
()
{
// Configure third party service.
// Configure third party service.
...
@@ -506,42 +434,6 @@ describe('groupList', function() {
...
@@ -506,42 +434,6 @@ describe('groupList', function() {
assert
.
lengthOf
(
dropdownMenu
,
1
);
assert
.
lengthOf
(
dropdownMenu
,
1
);
assert
.
lengthOf
(
dropdownOptions
,
2
);
assert
.
lengthOf
(
dropdownOptions
,
2
);
});
});
it
(
'is shown when community_groups feature flag is on and there are multiple groups'
,
function
()
{
fakeFeatures
.
flagEnabled
.
withArgs
(
'community_groups'
).
returns
(
true
);
groups
=
[
publicGroup
,
restrictedGroup
];
fakeStore
.
getMyGroups
.
returns
(
groups
);
const
element
=
createGroupList
();
const
showGroupsMenu
=
element
.
ctrl
.
showGroupsMenu
();
const
dropdownToggle
=
element
.
find
(
'.dropdown-toggle'
);
const
arrowIcon
=
element
.
find
(
'.h-icon-arrow-drop-down'
);
const
groupListSection
=
element
.
find
(
'.group-list-section'
);
assert
.
isTrue
(
showGroupsMenu
);
assert
.
lengthOf
(
dropdownToggle
,
1
);
assert
.
lengthOf
(
arrowIcon
,
1
);
assert
.
lengthOf
(
groupListSection
,
1
);
});
it
(
'is not shown when community_groups feature flag is on and there is only one group'
,
function
()
{
fakeFeatures
.
flagEnabled
.
withArgs
(
'community_groups'
).
returns
(
true
);
groups
=
[
publicGroup
];
fakeStore
.
getMyGroups
.
returns
(
groups
);
const
element
=
createGroupList
();
const
showGroupsMenu
=
element
.
ctrl
.
showGroupsMenu
();
const
dropdownToggle
=
element
.
find
(
'.dropdown-toggle'
);
const
arrowIcon
=
element
.
find
(
'.h-icon-arrow-drop-down'
);
const
groupListSection
=
element
.
find
(
'.group-list-section'
);
assert
.
isFalse
(
showGroupsMenu
);
assert
.
lengthOf
(
dropdownToggle
,
0
);
assert
.
lengthOf
(
arrowIcon
,
0
);
assert
.
lengthOf
(
groupListSection
,
0
);
});
});
});
[
false
,
true
].
forEach
(
isEnabled
=>
{
[
false
,
true
].
forEach
(
isEnabled
=>
{
...
...
src/sidebar/components/test/group-list-v2-test.js
0 → 100644
View file @
bc7085ce
'use strict'
;
const
{
shallow
}
=
require
(
'enzyme'
);
const
{
createElement
}
=
require
(
'preact'
);
const
GroupList
=
require
(
'../group-list-v2'
);
describe
(
'GroupList'
,
()
=>
{
let
fakeServiceConfig
;
let
fakeServiceUrl
;
let
fakeSettings
;
let
fakeStore
;
const
testGroup
=
{
id
:
'testgroup'
,
name
:
'Test group'
,
organization
:
{
id
:
'testorg'
,
name
:
'Test Org'
},
};
function
createGroupList
()
{
return
shallow
(
<
GroupList
serviceUrl
=
{
fakeServiceUrl
}
settings
=
{
fakeSettings
}
/
>
).
dive
();
}
beforeEach
(()
=>
{
fakeServiceUrl
=
sinon
.
stub
();
fakeSettings
=
{
authDomain
:
'hypothes.is'
,
};
fakeStore
=
{
getCurrentlyViewingGroups
:
sinon
.
stub
().
returns
([]),
getFeaturedGroups
:
sinon
.
stub
().
returns
([]),
getMyGroups
:
sinon
.
stub
().
returns
([]),
focusedGroup
:
sinon
.
stub
().
returns
(
testGroup
),
profile
:
sinon
.
stub
().
returns
({
userid
:
null
}),
};
fakeServiceConfig
=
sinon
.
stub
().
returns
(
null
);
GroupList
.
$imports
.
$mock
({
'../store/use-store'
:
callback
=>
callback
(
fakeStore
),
'../service-config'
:
fakeServiceConfig
,
});
});
afterEach
(()
=>
{
GroupList
.
$imports
.
$restore
();
});
it
(
'displays no sections if there are no groups'
,
()
=>
{
const
wrapper
=
createGroupList
();
assert
.
isFalse
(
wrapper
.
exists
(
'GroupListSection'
));
});
it
(
'displays "Currently Viewing" section if there are currently viewing groups'
,
()
=>
{
fakeStore
.
getCurrentlyViewingGroups
.
returns
([
testGroup
]);
const
wrapper
=
createGroupList
();
assert
.
isTrue
(
wrapper
.
exists
(
'GroupListSection[heading="Currently Viewing"]'
)
);
});
it
(
'displays "Featured Groups" section if there are featured groups'
,
()
=>
{
fakeStore
.
getFeaturedGroups
.
returns
([
testGroup
]);
const
wrapper
=
createGroupList
();
assert
.
isTrue
(
wrapper
.
exists
(
'GroupListSection[heading="Featured Groups"]'
)
);
});
it
(
'displays "My Groups" section if user is a member of any groups'
,
()
=>
{
fakeStore
.
getMyGroups
.
returns
([
testGroup
]);
const
wrapper
=
createGroupList
();
assert
.
isTrue
(
wrapper
.
exists
(
'GroupListSection[heading="My Groups"]'
));
});
it
(
'sorts groups within each section by organization'
,
()
=>
{
const
testGroups
=
[
{
...
testGroup
,
id
:
'zzz'
,
},
{
...
testGroup
,
id
:
'aaa'
,
},
];
fakeStore
.
getMyGroups
.
returns
(
testGroups
);
fakeStore
.
getCurrentlyViewingGroups
.
returns
(
testGroups
);
fakeStore
.
getFeaturedGroups
.
returns
(
testGroups
);
const
fakeGroupOrganizations
=
groups
=>
groups
.
sort
((
a
,
b
)
=>
a
.
id
.
localeCompare
(
b
.
id
));
GroupList
.
$imports
.
$mock
({
'../util/group-organizations'
:
fakeGroupOrganizations
,
});
const
wrapper
=
createGroupList
();
const
sections
=
wrapper
.
find
(
'GroupListSection'
);
assert
.
equal
(
sections
.
length
,
3
);
sections
.
forEach
(
section
=>
{
assert
.
deepEqual
(
section
.
prop
(
'groups'
),
fakeGroupOrganizations
(
testGroups
)
);
});
});
[
{
userid
:
null
,
expectNewGroupButton
:
false
,
},
{
userid
:
'acct:john@hypothes.is'
,
expectNewGroupButton
:
true
,
},
{
userid
:
'acct:john@otherpublisher.org'
,
expectNewGroupButton
:
false
,
},
].
forEach
(({
userid
,
expectNewGroupButton
})
=>
{
it
(
'displays "New private group" button if user is logged in with first-party account'
,
()
=>
{
fakeStore
.
profile
.
returns
({
userid
});
const
wrapper
=
createGroupList
();
const
newGroupButton
=
wrapper
.
find
(
'MenuItem[label="New private group"]'
);
assert
.
equal
(
newGroupButton
.
length
,
expectNewGroupButton
?
1
:
0
);
});
});
it
(
'opens new window at correct URL when "New private group" is clicked'
,
()
=>
{
fakeServiceUrl
.
withArgs
(
'groups.new'
)
.
returns
(
'https://example.com/groups/new'
);
fakeStore
.
profile
.
returns
({
userid
:
'jsmith@hypothes.is'
});
const
wrapper
=
createGroupList
();
const
newGroupButton
=
wrapper
.
find
(
'MenuItem[label="New private group"]'
);
assert
.
equal
(
newGroupButton
.
props
().
href
,
'https://example.com/groups/new'
);
});
it
(
'displays the group name and icon as static text if there is only one group and no actions available'
,
()
=>
{
GroupList
.
$imports
.
$mock
({
'../util/is-third-party-service'
:
()
=>
true
,
});
const
wrapper
=
createGroupList
();
assert
.
equal
(
wrapper
.
text
(),
'Test group'
);
});
it
(
'renders a placeholder if groups have not loaded yet'
,
()
=>
{
fakeStore
.
focusedGroup
.
returns
(
null
);
const
wrapper
=
createGroupList
();
const
label
=
wrapper
.
find
(
'Menu'
).
prop
(
'label'
);
assert
.
equal
(
shallow
(
label
).
text
(),
'…'
);
});
it
(
'renders the publisher-provided icon in the toggle button'
,
()
=>
{
fakeServiceConfig
.
returns
({
icon
:
'test-icon'
});
const
wrapper
=
createGroupList
();
const
label
=
wrapper
.
find
(
'Menu'
).
prop
(
'label'
);
const
img
=
shallow
(
label
).
find
(
'img'
);
assert
.
equal
(
img
.
prop
(
'src'
),
'test-icon'
);
});
});
src/sidebar/components/test/menu-test.js
View file @
bc7085ce
...
@@ -72,6 +72,12 @@ describe('Menu', () => {
...
@@ -72,6 +72,12 @@ describe('Menu', () => {
assert
.
isTrue
(
wrapper
.
exists
(
TestMenuItem
));
assert
.
isTrue
(
wrapper
.
exists
(
TestMenuItem
));
});
});
it
(
'flips toggle arrow when open'
,
()
=>
{
const
wrapper
=
createMenu
({
defaultOpen
:
true
});
const
toggle
=
wrapper
.
find
(
'.menu__toggle-arrow'
);
assert
.
isTrue
(
toggle
.
hasClass
(
'is-open'
));
});
let
e
;
let
e
;
[
[
new
Event
(
'mousedown'
),
new
Event
(
'mousedown'
),
...
@@ -126,4 +132,13 @@ describe('Menu', () => {
...
@@ -126,4 +132,13 @@ describe('Menu', () => {
wrapper
.
setProps
({
align
:
'right'
});
wrapper
.
setProps
({
align
:
'right'
});
assert
.
isTrue
(
wrapper
.
exists
(
'.menu__content--align-right'
));
assert
.
isTrue
(
wrapper
.
exists
(
'.menu__content--align-right'
));
});
});
it
(
'applies custom content class'
,
()
=>
{
const
wrapper
=
createMenu
({
defaultOpen
:
true
,
contentClass
:
'special-menu'
,
});
const
content
=
wrapper
.
find
(
'.menu__content'
);
assert
.
isTrue
(
content
.
hasClass
(
'special-menu'
));
});
});
});
src/sidebar/index.js
View file @
bc7085ce
...
@@ -160,8 +160,8 @@ function startAngularApp(config) {
...
@@ -160,8 +160,8 @@ function startAngularApp(config) {
.
component
(
'excerpt'
,
require
(
'./components/excerpt'
))
.
component
(
'excerpt'
,
require
(
'./components/excerpt'
))
.
component
(
'groupList'
,
require
(
'./components/group-list'
))
.
component
(
'groupList'
,
require
(
'./components/group-list'
))
.
component
(
.
component
(
'groupList
Section
'
,
'groupList
V2
'
,
wrapReactComponent
(
require
(
'./components/group-list-
section
'
))
wrapReactComponent
(
require
(
'./components/group-list-
v2
'
))
)
)
.
component
(
'helpLink'
,
require
(
'./components/help-link'
))
.
component
(
'helpLink'
,
require
(
'./components/help-link'
))
.
component
(
'helpPanel'
,
require
(
'./components/help-panel'
))
.
component
(
'helpPanel'
,
require
(
'./components/help-panel'
))
...
...
src/sidebar/templates/group-list.html
View file @
bc7085ce
<div
class=
"pull-right"
dropdown
keyboard-nav
>
<div
class=
"pull-right"
dropdown
keyboard-nav
ng-if=
"!vm.isFeatureFlagEnabled('community_groups')"
>
<!-- Show a drop down menu if showGroupsMenu is true. -->
<!-- Show a drop down menu if showGroupsMenu is true. -->
<div
<div
class=
"dropdown-toggle"
class=
"dropdown-toggle"
...
@@ -132,56 +137,7 @@
...
@@ -132,56 +137,7 @@
</div>
</div>
</li>
</li>
</ul>
</ul>
<!-- Show new menu if community_groups feature flag is enabled. -->
<div
class=
"dropdown-menu"
role=
"menu"
ng-if=
"vm.showGroupsMenu() && vm.isFeatureFlagEnabled('community_groups')"
>
<!-- Currently Viewing -->
<group-list-section
class=
"group-list-section"
heading=
"'Currently Viewing'"
groups=
"vm.currentlyViewingGroupOrganizations()"
ng-if=
"vm.currentlyViewingGroupOrganizations().length > 0"
>
</group-list-section>
<!-- Featured Groups -->
<group-list-section
class=
"group-list-section"
heading=
"'Featured Groups'"
groups=
"vm.featuredGroupOrganizations()"
ng-if=
"vm.featuredGroupOrganizations().length > 0"
>
</group-list-section>
<!-- My Groups -->
<group-list-section
class=
"group-list-section"
heading=
"'My Groups'"
groups=
"vm.myGroupOrganizations()"
ng-if=
"vm.myGroupOrganizations().length > 0"
>
</group-list-section>
<ul
class=
"dropdown-menu__section dropdown-menu__section--no-header"
>
<li
ng-if=
"vm.auth.status === 'logged-in' && !vm.isThirdPartyUser()"
class=
"dropdown-community-groups-menu__row dropdown-menu__row--unpadded new-group-btn"
>
<div
class=
"group-item-community-groups"
ng-click=
"vm.createNewGroup()"
tabindex=
"0"
>
<div
class=
"group-icon-container"
><i
class=
"h-icon-add"
></i></div>
<div
class=
"group-details-community-groups"
>
New private group
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
<group-list-v2
ng-if=
"vm.isFeatureFlagEnabled('community_groups')"
></group-list-v2>
src/styles/sidebar/common.scss
View file @
bc7085ce
...
@@ -248,17 +248,6 @@ html {
...
@@ -248,17 +248,6 @@ html {
}
}
// Row in a dropdown menu
// Row in a dropdown menu
.dropdown-community-groups-menu__row
{
display
:
flex
;
flex-direction
:
row
;
align-items
:
center
;
padding-left
:
8px
;
padding-right
:
16px
;
min-height
:
40px
;
min-width
:
120px
;
}
.dropdown-menu__row--no-border
{
.dropdown-menu__row--no-border
{
border
:
none
;
border
:
none
;
}
}
...
...
src/styles/sidebar/components/group-list-item.scss
View file @
bc7085ce
/* The group. */
// Footer to display at the bottom of a menu item.
.group-list-item__footer
{
.group-list-item
{
background-color
:
$grey-1
;
display
:
flex
;
margin
:
0
;
flex-direction
:
row
;
padding-top
:
15px
;
flex-grow
:
1
;
}
.group-list-item__item
{
border
:
solid
1px
transparent
;
display
:
flex
;
flex-direction
:
row
;
flex-grow
:
1
;
margin
:
1px
;
padding
:
10px
;
padding
:
10px
;
cursor
:
pointer
;
&
:focus
{
outline
:
none
;
@include
focus-outline
;
}
&
:hover
{
background
:
$gray-lightest
;
}
&
.is-selected
{
background
:
$gray-lightest
;
}
}
.group-list-item__icon-container
{
margin-right
:
10px
;
width
:
15px
;
height
:
15px
;
}
// the icon indicating the type of group currently selected at
// the top of the groups list
.group-list-item__icon
{
color
:
$color-gray
;
display
:
inline-block
;
margin-right
:
4px
;
position
:
relative
;
vertical-align
:
baseline
;
// align the base of the chat-heads icon for groups
// with the baseline of the group name label
transform
:
translateY
(
1px
);
}
.group-list-item__icon--organization
{
height
:
15px
;
width
:
15px
;
top
:
2px
;
}
// the name of a group in the groups drop-down list
// and 'Post to <Group>' button for saving annotations
.group-list-item__name-link
{
white-space
:
nowrap
;
color
:
inherit
;
}
.group-list-item__details
{
// Align the left edge of the footer text with menu item labels above.
flex-grow
:
1
;
padding-left
:
35px
;
flex-shrink
:
1
;
white-space
:
normal
;
font-weight
:
500
;
}
}
src/styles/sidebar/components/group-list-section.scss
deleted
100644 → 0
View file @
521b1ffd
/* The groups section. */
.group-list-section__content
{
border-bottom
:
solid
1px
rgba
(
0
,
0
,
0
,
0
.15
);
}
.group-list-section__heading
{
color
:
$gray-light
;
font-size
:
$body1-font-size
;
line-height
:
1
;
margin
:
1px
1px
0
;
padding
:
12px
10px
0
;
text-transform
:
uppercase
;
}
src/styles/sidebar/components/group-list-v2.scss
0 → 100644
View file @
bc7085ce
.group-list-v2__content
{
min-width
:
250px
;
}
src/styles/sidebar/components/group-list.scss
View file @
bc7085ce
...
@@ -48,33 +48,6 @@ $group-list-spacing-below: 50px;
...
@@ -48,33 +48,6 @@ $group-list-spacing-below: 50px;
}
}
}
}
.group-item-community-groups
{
border
:
solid
1px
transparent
;
display
:
flex
;
flex-direction
:
row
;
flex-grow
:
1
;
margin
:
1px
;
padding
:
10px
;
cursor
:
pointer
;
&
:focus
{
outline
:
none
;
@include
focus-outline
;
}
&
:hover
{
background
:
$gray-lightest
;
}
&
.is-selected
{
.group-name-link
{
font-size
:
$body2-font-size
;
font-weight
:
600
;
}
}
}
.group-icon-container
{
.group-icon-container
{
margin-right
:
10px
;
margin-right
:
10px
;
}
}
...
@@ -97,27 +70,6 @@ $group-list-spacing-below: 50px;
...
@@ -97,27 +70,6 @@ $group-list-spacing-below: 50px;
flex-grow
:
1
;
flex-grow
:
1
;
flex-shrink
:
1
;
flex-shrink
:
1
;
}
}
.group-details-community-groups
{
flex-grow
:
1
;
flex-shrink
:
1
;
font-weight
:
500
;
.group-details__unavailable-message
,
.group-details__actions
{
display
:
none
;
}
&
.expanded
{
.group-details__toggle
{
display
:
none
;
}
.group-details__unavailable-message
,
.group-details__actions
{
display
:
block
;
}
}
}
.new-group-btn
{
.new-group-btn
{
background-color
:
$gray-lightest
;
background-color
:
$gray-lightest
;
...
...
src/styles/sidebar/components/menu-item.scss
View file @
bc7085ce
...
@@ -91,6 +91,8 @@ $menu-item-padding: 10px;
...
@@ -91,6 +91,8 @@ $menu-item-padding: 10px;
// Toggle button used to expand or collapse the submenu associated with a menu
// Toggle button used to expand or collapse the submenu associated with a menu
// item.
// item.
.menu-item__toggle
{
.menu-item__toggle
{
@include
outline-on-keyboard-focus
;
display
:
flex
;
display
:
flex
;
flex-direction
:
column
;
flex-direction
:
column
;
justify-content
:
center
;
justify-content
:
center
;
...
...
src/styles/sidebar/components/menu.scss
View file @
bc7085ce
...
@@ -4,12 +4,15 @@
...
@@ -4,12 +4,15 @@
// Toggle button that opens the menu.
// Toggle button that opens the menu.
.menu__toggle
{
.menu__toggle
{
@include
outline-on-keyboard-focus
;
appearance
:
none
;
appearance
:
none
;
border
:
none
;
border
:
none
;
background
:
none
;
background
:
none
;
padding
:
0
;
padding
:
0
;
color
:
inherit
;
color
:
inherit
;
display
:
flex
;
display
:
flex
;
align-items
:
center
;
}
}
.menu__toggle-icon
{
.menu__toggle-icon
{
...
@@ -23,6 +26,11 @@
...
@@ -23,6 +26,11 @@
width
:
10px
;
width
:
10px
;
height
:
10px
;
height
:
10px
;
margin-left
:
5px
;
margin-left
:
5px
;
&
.is-open
{
// Flip the indicator when the menu is open.
transform
:
rotateX
(
180deg
);
}
}
}
// Triangular indicator at the top of the menu that associates it with the
// Triangular indicator at the top of the menu that associates it with the
...
...
src/styles/sidebar/sidebar.scss
View file @
bc7085ce
...
@@ -24,9 +24,9 @@ $base-line-height: 20px;
...
@@ -24,9 +24,9 @@ $base-line-height: 20px;
@import
'./components/dropdown-menu-btn'
;
@import
'./components/dropdown-menu-btn'
;
@import
'./components/excerpt'
;
@import
'./components/excerpt'
;
@import
'./components/group-list'
;
@import
'./components/group-list'
;
@import
'./components/group-list-v2'
;
@import
'./components/group-list-item'
;
@import
'./components/group-list-item'
;
@import
'./components/group-list-item-out-of-scope'
;
@import
'./components/group-list-item-out-of-scope'
;
@import
'./components/group-list-section'
;
@import
'./components/help-panel'
;
@import
'./components/help-panel'
;
@import
'./components/loggedout-message'
;
@import
'./components/loggedout-message'
;
@import
'./components/login-control'
;
@import
'./components/login-control'
;
...
...
src/styles/variables.scss
View file @
bc7085ce
...
@@ -126,7 +126,7 @@ $highlight-color-second: rgba(206, 206, 60, 0.4);
...
@@ -126,7 +126,7 @@ $highlight-color-second: rgba(206, 206, 60, 0.4);
$highlight-color-third
:
rgba
(
192
,
192
,
49
,
0
.4
);
$highlight-color-third
:
rgba
(
192
,
192
,
49
,
0
.4
);
$highlight-color-focus
:
rgba
(
156
,
230
,
255
,
0
.5
);
$highlight-color-focus
:
rgba
(
156
,
230
,
255
,
0
.5
);
$top-bar-height
:
40px
;
$top-bar-height
:
40px
;
$group-list-width
:
30
0px
;
$group-list-width
:
28
0px
;
// Mixins
// Mixins
// ------
// ------
...
...
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