Unverified Commit 90bcfb11 authored by Robert Knight's avatar Robert Knight Committed by GitHub

Merge pull request #1018 from hypothesis/refactor-group-sections

Refactor new groups menu
parents 3f1f9c33 92b9aae3
'use strict';
const {
orgName,
trackViewGroupActivity,
} = require('../util/group-list-item-common');
// @ngInject
function GroupListItemOutOfScopeController(analytics) {
// Track whether the group details are expanded.
this.isDetailsExpanded = false;
/**
* Toggle the expanded setting on un-selectable groups.
*/
this.toggleGroupDetails = function(event) {
event.stopPropagation();
this.isDetailsExpanded = !this.isDetailsExpanded;
};
this.orgName = function() {
return orgName(this.group);
};
this.trackViewGroupActivity = function() {
trackViewGroupActivity(analytics);
};
}
module.exports = {
controller: GroupListItemOutOfScopeController,
controllerAs: 'vm',
bindings: {
group: '<',
},
template: require('../templates/group-list-item-out-of-scope.html'),
};
'use strict';
const {
orgName,
trackViewGroupActivity,
} = require('../util/group-list-item-common');
// @ngInject
function GroupListItemController(analytics, store) {
this.focusGroup = function() {
analytics.track(analytics.events.GROUP_SWITCH);
store.focusGroup(this.group.id);
};
this.isSelected = function() {
return this.group.id === store.focusedGroupId();
};
this.orgName = function() {
return orgName(this.group);
};
this.trackViewGroupActivity = function() {
trackViewGroupActivity(analytics);
};
}
module.exports = {
controller: GroupListItemController,
controllerAs: 'vm',
bindings: {
group: '<',
},
template: require('../templates/group-list-item.html'),
};
'use strict';
// @ngInject
function GroupListSectionController() {
this.isSelectable = function(groupId) {
const group = this.sectionGroups.find(g => g.id === groupId);
return !this.disableOosGroupSelection || group.isScopedToUri;
};
}
module.exports = {
controller: GroupListSectionController,
controllerAs: 'vm',
bindings: {
/* The list of groups to be displayed in the group list section. */
sectionGroups: '<',
/* The string name of the group list section. */
heading: '<',
/* A boolean indicating whether out of scope group selection should be disabled. */
disableOosGroupSelection: '<',
},
template: require('../templates/group-list-section.html'),
};
......@@ -26,9 +26,6 @@ function GroupListController(
) {
this.groups = groups;
// Track which non-selectable groups have their group details expanded.
this.groupDetailsExpanded = {};
this.createNewGroup = function() {
$window.open(serviceUrl('groups.new'), '_blank');
};
......@@ -90,23 +87,6 @@ function GroupListController(
groups.focus(groupId);
};
this.isGroupDetailsExpanded = function(groupId) {
if (!(groupId in this.groupDetailsExpanded)) {
this.groupDetailsExpanded[groupId] = false;
}
return this.groupDetailsExpanded[groupId];
};
/**
* Toggle the expanded setting on un-selectable groups.
*/
this.toggleGroupDetails = function(event, groupId) {
event.stopPropagation();
// Call the isGroupDetailsExpanded method so that if the groupId doesn't exist,
// it gets added before toggling it.
this.groupDetailsExpanded[groupId] = !this.isGroupDetailsExpanded(groupId);
};
/**
* Show the share link for the group if it is not a third-party group
* AND if the URL needed is present in the group object. We should be able
......
'use strict';
const angular = require('angular');
const proxyquire = require('proxyquire');
const util = require('../../directive/test/util');
const { events } = require('../../services/analytics');
describe('groupListItemOutOfScope', () => {
let fakeAnalytics;
let fakeGroupListItemCommon;
before(() => {
fakeGroupListItemCommon = {
orgName: sinon.stub(),
trackViewGroupActivity: sinon.stub(),
};
// Return groupListItemOutOfScope with groupListItemCommon stubbed out.
const groupListItemOutOfScope = proxyquire(
'../group-list-item-out-of-scope',
{
'../util/group-list-item-common': fakeGroupListItemCommon,
'@noCallThru': true,
}
);
angular
.module('app', [])
.component('groupListItemOutOfScope', groupListItemOutOfScope);
});
beforeEach(() => {
fakeAnalytics = {
track: sinon.stub(),
events,
};
angular.mock.module('app', { analytics: fakeAnalytics });
});
const createGroupListItemOutOfScope = fakeGroup => {
return util.createDirective(document, 'groupListItemOutOfScope', {
group: fakeGroup,
});
};
it('calls groupListItemCommon.trackViewGroupActivity when trackViewGroupActivity is called', () => {
const fakeGroup = { id: 'groupid' };
const element = createGroupListItemOutOfScope(fakeGroup);
element.ctrl.trackViewGroupActivity();
assert.calledWith(
fakeGroupListItemCommon.trackViewGroupActivity,
fakeAnalytics
);
});
it('returns groupListItemCommon.orgName when orgName is called', () => {
const fakeGroup = { id: 'groupid', organization: { name: 'org' } };
fakeGroupListItemCommon.orgName
.withArgs(fakeGroup)
.returns(fakeGroup.organization.name);
const element = createGroupListItemOutOfScope(fakeGroup);
const orgName = element.ctrl.orgName();
assert.calledWith(fakeGroupListItemCommon.orgName, fakeGroup);
assert.equal(orgName, fakeGroup.organization.name);
});
describe('toggleGroupDetails', () => {
it('sets the default expanded value to false', () => {
const fakeGroup = { id: 'groupid' };
const element = createGroupListItemOutOfScope(fakeGroup);
assert.isFalse(element.ctrl.isDetailsExpanded);
});
it('toggles the expanded value', () => {
const fakeGroup = { id: 'groupid' };
const element = createGroupListItemOutOfScope(fakeGroup);
const fakeEvent = { stopPropagation: sinon.stub() };
element.ctrl.toggleGroupDetails(fakeEvent);
assert.isTrue(element.ctrl.isDetailsExpanded);
element.ctrl.toggleGroupDetails(fakeEvent);
assert.isFalse(element.ctrl.isDetailsExpanded);
});
it('stops the event from propagating when toggling', () => {
const fakeGroup = { id: 'groupid' };
const element = createGroupListItemOutOfScope(fakeGroup);
const fakeEvent = { stopPropagation: sinon.spy() };
element.ctrl.toggleGroupDetails(fakeEvent);
sinon.assert.called(fakeEvent.stopPropagation);
});
});
});
'use strict';
const angular = require('angular');
const proxyquire = require('proxyquire');
const util = require('../../directive/test/util');
const { events } = require('../../services/analytics');
describe('groupListItem', () => {
let fakeAnalytics;
let fakeStore;
let fakeGroupListItemCommon;
before(() => {
fakeGroupListItemCommon = {
orgName: sinon.stub(),
trackViewGroupActivity: sinon.stub(),
};
// Return groupListItem with groupListItemCommon stubbed out.
const groupListItem = proxyquire('../group-list-item', {
'../util/group-list-item-common': fakeGroupListItemCommon,
'@noCallThru': true,
});
angular.module('app', []).component('groupListItem', groupListItem);
});
beforeEach(() => {
fakeStore = {
focusGroup: sinon.stub(),
focusedGroupId: sinon.stub().returns('groupid'),
};
fakeAnalytics = {
track: sinon.stub(),
events,
};
angular.mock.module('app', {
analytics: fakeAnalytics,
store: fakeStore,
});
});
const createGroupListItem = fakeGroup => {
return util.createDirective(document, 'groupListItem', {
group: fakeGroup,
});
};
it('changes the focused group when group is clicked', () => {
const fakeGroup = { id: 'groupid' };
const element = createGroupListItem(fakeGroup);
const group = element.find('.group-list-item__item');
group[0].click();
assert.calledWith(fakeStore.focusGroup, fakeGroup.id);
assert.calledWith(fakeAnalytics.track, fakeAnalytics.events.GROUP_SWITCH);
});
it('calls groupListItemCommon.trackViewGroupActivity when trackViewGroupActivity is called', () => {
const fakeGroup = { id: 'groupid' };
const element = createGroupListItem(fakeGroup);
element.ctrl.trackViewGroupActivity();
assert.calledWith(
fakeGroupListItemCommon.trackViewGroupActivity,
fakeAnalytics
);
});
it('returns groupListItemCommon.orgName when orgName is called', () => {
const fakeGroup = { id: 'groupid', organization: { name: 'org' } };
fakeGroupListItemCommon.orgName
.withArgs(fakeGroup)
.returns(fakeGroup.organization.name);
const element = createGroupListItem(fakeGroup);
const orgName = element.ctrl.orgName();
assert.equal(orgName, fakeGroup.organization.name);
});
describe('isSelected', () => {
[
{
description: 'returns true if group is the focused group',
focusedGroupId: 'groupid',
expectedIsSelected: true,
},
{
description: 'returns false if group is not the focused group',
focusedGroupId: 'other',
expectedIsSelected: false,
},
].forEach(({ description, focusedGroupId, expectedIsSelected }) => {
it(description, () => {
fakeStore.focusedGroupId.returns(focusedGroupId);
const fakeGroup = { id: 'groupid' };
const element = createGroupListItem(fakeGroup);
assert.equal(element.ctrl.isSelected(), expectedIsSelected);
});
});
});
});
'use strict';
const angular = require('angular');
const groupListSection = require('../group-list-section');
const util = require('../../directive/test/util');
describe('groupListSection', () => {
before(() => {
angular.module('app', []).component('groupListSection', groupListSection);
});
beforeEach(() => {
angular.mock.module('app', {});
});
const createGroupListSection = (
fakeSectionGroups,
fakeDisableOosGroupSelection
) => {
const config = {
sectionGroups: fakeSectionGroups,
};
if (fakeDisableOosGroupSelection !== undefined) {
config.disableOosGroupSelection = fakeDisableOosGroupSelection;
}
return util.createDirective(document, 'groupListSection', config);
};
describe('isSelectable', () => {
[
{
description: 'always returns true if disableOosGroupSelection is false',
fakeDisableOosGroupSelection: false,
expectedIsSelectable: [true, true],
},
{
description:
'always returns true if disableOosGroupSelection is undefined',
fakeDisableOosGroupSelection: undefined,
expectedIsSelectable: [true, true],
},
{
description:
'returns false if disableOosGroupSelection is true and group is out of scope',
fakeDisableOosGroupSelection: true,
expectedIsSelectable: [true, false],
},
].forEach(
({ description, fakeDisableOosGroupSelection, expectedIsSelectable }) => {
it(description, () => {
const fakeSectionGroups = [
{ isScopedToUri: true, id: 0 },
{ isScopedToUri: false, id: 1 },
];
const element = createGroupListSection(
fakeSectionGroups,
fakeDisableOosGroupSelection
);
fakeSectionGroups.forEach(g =>
assert.equal(
element.ctrl.isSelectable(g.id),
expectedIsSelectable[g.id]
)
);
});
}
);
});
});
......@@ -3,6 +3,7 @@
const angular = require('angular');
const immutable = require('seamless-immutable');
const { events } = require('../../services/analytics');
const groupList = require('../group-list');
const util = require('../../directive/test/util');
......@@ -33,17 +34,6 @@ describe('groupList', function() {
isScopedToUri: true,
});
const restrictedOutOfScopeGroup = immutable({
id: 'restrictedoos',
links: {
html: 'https://hypothes.is/groups/restricto',
},
name: 'Restricted',
organization: groupFixtures.defaultOrganization(),
type: 'restricted',
isScopedToUri: false,
});
const publicGroup = immutable({
id: '__world__',
links: {
......@@ -85,11 +75,7 @@ describe('groupList', function() {
fakeAnalytics = {
track: sinon.stub(),
events: {
GROUP_LEAVE: 'groupLeave',
GROUP_SWITCH: 'groupSwitch',
GROUP_VIEW_ACTIVITY: 'groupViewActivity',
},
events,
};
fakeServiceUrl = sinon.stub();
......@@ -385,6 +371,7 @@ describe('groupList', function() {
'_blank'
);
});
describe('group section visibility', () => {
[
{
......@@ -393,7 +380,7 @@ describe('groupList', function() {
currentlyViewingGroups: [publicGroup],
featuredGroups: [restrictedGroup],
myGroups: [],
expectedSections: ['Currently Viewing', 'Featured Groups'],
expectedSections: ["'Currently Viewing'", "'Featured Groups'"],
},
{
description:
......@@ -401,14 +388,14 @@ describe('groupList', function() {
currentlyViewingGroups: [],
featuredGroups: [restrictedGroup],
myGroups: [publicGroup],
expectedSections: ['Featured Groups', 'My Groups'],
expectedSections: ["'Featured Groups'", "'My Groups'"],
},
{
description: 'shows My Groups section when there are my groups',
currentlyViewingGroups: [],
featuredGroups: [],
myGroups: [publicGroup, privateGroup],
expectedSections: ['My Groups'],
expectedSections: ["'My Groups'"],
},
].forEach(
({
......@@ -433,76 +420,21 @@ describe('groupList', function() {
const showGroupsMenu = element.ctrl.showGroupsMenu();
const dropdownToggle = element.find('.dropdown-toggle');
const arrowIcon = element.find('.h-icon-arrow-drop-down');
const sectionHeader = element.find('.dropdown-menu__section-heading');
const section = element.find('.dropdown-menu__section');
const dropdownOptions = element.find(
'.dropdown-community-groups-menu__row'
);
const groupListSection = element.find('group-list-section');
assert.isTrue(showGroupsMenu);
assert.lengthOf(dropdownToggle, 1);
assert.lengthOf(arrowIcon, 1);
sectionHeader.each(function() {
assert.isTrue(expectedSections.includes(this.textContent));
assert.lengthOf(groupListSection, expectedSections.length);
groupListSection.each(function() {
assert.isTrue(
expectedSections.includes(this.getAttribute('heading'))
);
});
// Plus one for the create private group section.
assert.lengthOf(section, expectedSections.length + 1);
assert.lengthOf(dropdownOptions, 3);
});
}
);
});
describe('group details expanded on out of scope groups', () => {
it('sets the default for the given groupid to false and returns it', () => {
const element = createGroupList();
const expanded = element.ctrl.isGroupDetailsExpanded('groupid');
assert.isFalse(expanded);
assert.isFalse(element.ctrl.groupDetailsExpanded.groupid);
});
it('gets expanded value for the given groupid if already present', () => {
const element = createGroupList();
element.ctrl.groupDetailsExpanded = { groupid: true };
const expanded = element.ctrl.isGroupDetailsExpanded('groupid');
assert.isTrue(expanded);
});
it('toggles the expanded value for the given groupid', () => {
const element = createGroupList();
let fakeEvent = { stopPropagation: sinon.stub() };
element.ctrl.toggleGroupDetails(fakeEvent, 'groupid');
assert.isTrue(element.ctrl.groupDetailsExpanded.groupid);
element.ctrl.toggleGroupDetails(fakeEvent, 'groupid');
assert.isFalse(element.ctrl.groupDetailsExpanded.groupid);
});
it('stops the event from propogating when toggling', () => {
const element = createGroupList();
let fakeEvent = { stopPropagation: sinon.spy() };
element.ctrl.toggleGroupDetails(fakeEvent, 'groupid');
sinon.assert.called(fakeEvent.stopPropagation);
});
});
it('displays out of scope groups as non-selectable', () => {
fakeFeatures.flagEnabled.withArgs('community_groups').returns(true);
// In order to show the group drop down there must be at least two groups.
groups = [publicGroup, restrictedOutOfScopeGroup];
fakeStore.getMyGroups.returns(groups);
const element = createGroupList();
const notSelectable = element.find('.group-item--out-of-scope');
assert.lengthOf(notSelectable, 1);
});
describe('group menu visibility', () => {
it('is hidden when third party service and only one group', function() {
......@@ -585,14 +517,12 @@ describe('groupList', function() {
const showGroupsMenu = element.ctrl.showGroupsMenu();
const dropdownToggle = element.find('.dropdown-toggle');
const arrowIcon = element.find('.h-icon-arrow-drop-down');
const dropdownOptions = element.find(
'.dropdown-community-groups-menu__row'
);
const groupListSection = element.find('.group-list-section');
assert.isTrue(showGroupsMenu);
assert.lengthOf(dropdownToggle, 1);
assert.lengthOf(arrowIcon, 1);
assert.lengthOf(dropdownOptions, 3);
assert.lengthOf(groupListSection, 1);
});
it('is not shown when community_groups feature flag is on and there is only one group', function() {
......@@ -605,14 +535,12 @@ describe('groupList', function() {
const showGroupsMenu = element.ctrl.showGroupsMenu();
const dropdownToggle = element.find('.dropdown-toggle');
const arrowIcon = element.find('.h-icon-arrow-drop-down');
const dropdownOptions = element.find(
'.dropdown-community-groups-menu__row'
);
const groupListSection = element.find('.group-list-section');
assert.isFalse(showGroupsMenu);
assert.lengthOf(dropdownToggle, 0);
assert.lengthOf(arrowIcon, 0);
assert.lengthOf(dropdownOptions, 0);
assert.lengthOf(groupListSection, 0);
});
});
......
......@@ -151,6 +151,12 @@ function startAngularApp(config) {
.component('dropdownMenuBtn', require('./components/dropdown-menu-btn'))
.component('excerpt', require('./components/excerpt'))
.component('groupList', require('./components/group-list'))
.component('groupListItem', require('./components/group-list-item'))
.component(
'groupListItemOutOfScope',
require('./components/group-list-item-out-of-scope')
)
.component('groupListSection', require('./components/group-list-section'))
.component('helpLink', require('./components/help-link'))
.component('helpPanel', require('./components/help-panel'))
.component('loggedoutMessage', require('./components/loggedout-message'))
......
<div
class="group-list-item-out-of-scope__item"
ng-class="{'group-list-item__item': true, 'is-selected': false}"
ng-click="vm.toggleGroupDetails($event)"
tabindex="0"
>
<!-- the group icon !-->
<div class="group-list-item__icon-container">
<img
class="group-list-item__icon group-list-item__icon--organization"
alt="{{ vm.orgName() }}"
ng-src="{{ vm.group.logo }}"
ng-if="vm.group.logo"
/>
</div>
<!-- the group name and share link !-->
<div
ng-class="{'group-list-item-out-of-scope__details': true, expanded: vm.isDetailsExpanded}"
>
<svg
class="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>
<a
class="group-list-item__name-link"
href=""
title="Group not annotatable on this domain."
>
{{vm.group.name}} </a
><br />
<p
class="group-list-item-out-of-scope__details-toggle"
ng-if="!vm.isDetailsExpanded"
>
Why is this group unavailable?
</p>
<p
class="group-list-item-out-of-scope__details-unavailable-message"
ng-if="vm.isDetailsExpanded"
>
This group has been restricted to selected URLs by its administrators.
</p>
<p
class="group-list-item-out-of-scope__details-actions"
ng-if="vm.isDetailsExpanded"
>
<a
class="button button--text group-list-item-out-of-scope__details-group-page-link"
href="{{vm.group.links.html}}"
target="_blank"
ng-click="vm.trackViewGroupActivity()"
>Go to group page</a
>
</p>
</div>
</div>
<div
ng-class="{'group-list-item__item': true, 'is-selected': vm.isSelected()}"
ng-click="vm.focusGroup()"
tabindex="0"
>
<!-- the group icon !-->
<div class="group-list-item__icon-container">
<img
class="group-list-item__icon group-list-item__icon--organization"
alt="{{ vm.orgName() }}"
ng-src="{{ vm.group.logo }}"
ng-if="vm.group.logo"
/>
</div>
<!-- the group name and share link -->
<div class="group-list-item__details">
<a
class="group-list-item__name-link"
href=""
title="{{ vm.group.type === 'private' ? 'Show and create annotations in ' + vm.group.name : 'Show public annotations' }}"
>
{{vm.group.name}}
</a>
</div>
</div>
<h2 class="group-list-section__heading">
{{ vm.heading }}
</h2>
<ul class="group-list-section__content">
<li
class="dropdown-menu__row dropdown-menu__row--no-border dropdown-menu__row--unpadded"
ng-repeat="group in vm.sectionGroups track by group.id"
>
<group-list-item
class="group-list-item"
group="group"
ng-if="vm.isSelectable(group.id)"
>
</group-list-item>
<group-list-item-out-of-scope
class="group-list-item-out-of-scope"
group="group"
ng-if="!vm.isSelectable(group.id)"
>
</group-list-item-out-of-scope>
</li>
</ul>
<div class="pull-right"
dropdown
keyboard-nav>
<div class="pull-right" dropdown keyboard-nav>
<!-- Show a drop down menu if showGroupsMenu is true. -->
<div class="dropdown-toggle"
dropdown-toggle
data-toggle="dropdown"
role="button"
tabindex="0"
ng-if="vm.showGroupsMenu()">
<img class="group-list-label__icon group-list-label__icon--organization"
ng-src="{{ vm.focusedIcon() }}"
alt="{{ vm.orgName(vm.groups.focused().id)}}"
ng-if="vm.focusedIcon()">
<i class="group-list-label__icon h-icon-{{ vm.focusedIconClass() }}"
ng-if="!vm.focusedIcon()"></i><!-- nospace
!--><span class="group-list-label__label"><a class="group-list-label__toggle">{{vm.groups.focused().name}}</a><!-- nospace
!--><i class="h-icon-arrow-drop-down"></i></span>
<div
class="dropdown-toggle"
dropdown-toggle
data-toggle="dropdown"
role="button"
tabindex="0"
ng-if="vm.showGroupsMenu()"
>
<img
class="group-list-label__icon group-list-label__icon--organization"
ng-src="{{ vm.focusedIcon() }}"
alt="{{ vm.orgName(vm.groups.focused().id)}}"
ng-if="vm.focusedIcon()"
/>
<i
class="group-list-label__icon h-icon-{{ vm.focusedIconClass() }}"
ng-if="!vm.focusedIcon()"
></i
><!-- nospace
!--><span class="group-list-label__label"
><a class="group-list-label__toggle">{{vm.groups.focused().name}}</a
><!-- nospace
!--><i class="h-icon-arrow-drop-down"></i
></span>
</div>
<!-- Do not show a drop down menu if showGroupsMenu is false. -->
<div ng-if="!vm.showGroupsMenu()">
<img class="group-list-label__icon group-list-label__icon--organization"
ng-src="{{ vm.focusedIcon() }}"
alt="{{ vm.orgName(vm.groups.focused().id)}}"
ng-if="vm.focusedIcon()">
<i class="group-list-label__icon h-icon-{{ vm.focusedIconClass() }}"
ng-if="!vm.focusedIcon()"></i><!-- nospace
!--><span class="group-list-label__label">{{vm.groups.focused().name}}</span>
<img
class="group-list-label__icon group-list-label__icon--organization"
ng-src="{{ vm.focusedIcon() }}"
alt="{{ vm.orgName(vm.groups.focused().id)}}"
ng-if="vm.focusedIcon()"
/>
<i
class="group-list-label__icon h-icon-{{ vm.focusedIconClass() }}"
ng-if="!vm.focusedIcon()"
></i
><!-- nospace
!--><span class="group-list-label__label"
>{{vm.groups.focused().name}}</span
>
</div>
<!-- Only build the drop down menu if showGroupsMenu is true. -->
<!-- Original groups menu. -->
<div class="dropdown-menu__top-arrow" ng-if="vm.showGroupsMenu() && !vm.isFeatureFlagEnabled('community_groups')"></div>
<ul class="dropdown-menu pull-none" role="menu" ng-if="vm.showGroupsMenu() && !vm.isFeatureFlagEnabled('community_groups')">
<li class="dropdown-menu__row dropdown-menu__row--unpadded "
ng-repeat="group in vm.groupOrganizations() track by group.id">
<div ng-class="{'group-item': true, selected: group.id == vm.groups.focused().id}"
ng-click="vm.focusGroup(group.id)">
<div
class="dropdown-menu__top-arrow"
ng-if="vm.showGroupsMenu() && !vm.isFeatureFlagEnabled('community_groups')"
></div>
<ul
class="dropdown-menu pull-none"
role="menu"
ng-if="vm.showGroupsMenu() && !vm.isFeatureFlagEnabled('community_groups')"
>
<li
class="dropdown-menu__row dropdown-menu__row--unpadded "
ng-repeat="group in vm.groupOrganizations() track by group.id"
>
<div
ng-class="{'group-item': true, selected: group.id == vm.groups.focused().id}"
ng-click="vm.focusGroup(group.id)"
>
<!-- the group icon !-->
<div class="group-menu-icon-container">
<img class="group-list-label__icon group-list-label__icon--organization"
<img
class="group-list-label__icon group-list-label__icon--organization"
alt="{{ vm.orgName(group.id) }}"
ng-src="{{ group.logo }}"
ng-if="group.logo">
ng-if="group.logo"
/>
</div>
<!-- the group name and share link !-->
<div class="group-details">
<div class="group-name-container">
<a class="group-name-link"
href=""
title="{{ group.type === 'private' ? 'Show and create annotations in ' + group.name : 'Show public annotations' }}">
{{group.name}}
<a
class="group-name-link"
href=""
title="{{ group.type === 'private' ? 'Show and create annotations in ' + group.name : 'Show public annotations' }}"
>
{{group.name}}
</a>
</div>
<div class="share-link-container" ng-click="$event.stopPropagation()" ng-if="vm.shouldShowActivityLink(group.id)">
<a class="share-link"
href="{{group.links.html}}"
target="_blank"
ng-click="vm.viewGroupActivity()">
<div
class="share-link-container"
ng-click="$event.stopPropagation()"
ng-if="vm.shouldShowActivityLink(group.id)"
>
<a
class="share-link"
href="{{group.links.html}}"
target="_blank"
ng-click="vm.viewGroupActivity()"
>
View group activity
<span ng-if="group.type === 'private'">
and invite others
......@@ -65,19 +101,31 @@
</div>
</div>
<!-- the 'Leave group' icon !-->
<div class="group-cancel-icon-container" ng-click="$event.stopPropagation()">
<i class="h-icon-cancel-outline btn--cancel"
ng-if="group.type === 'private'"
ng-click="vm.leaveGroup(group.id)"
title="Leave '{{group.name}}'"></i>
<div
class="group-cancel-icon-container"
ng-click="$event.stopPropagation()"
>
<i
class="h-icon-cancel-outline btn--cancel"
ng-if="group.type === 'private'"
ng-click="vm.leaveGroup(group.id)"
title="Leave '{{group.name}}'"
></i>
</div>
</div>
</li>
<li ng-if="vm.auth.status === 'logged-in' && !vm.isThirdPartyUser()" class="dropdown-menu__row dropdown-menu__row--unpadded new-group-btn">
<li
ng-if="vm.auth.status === 'logged-in' && !vm.isThirdPartyUser()"
class="dropdown-menu__row dropdown-menu__row--unpadded new-group-btn"
>
<div class="group-item" ng-click="vm.createNewGroup()">
<div class="group-icon-container"><i class="h-icon-add"></i></div>
<div class="group-details">
<a href="" class="group-name-link" title="Create a new group to share annotations">
<a
href=""
class="group-name-link"
title="Create a new group to share annotations"
>
New private group
</a>
</div>
......@@ -86,124 +134,49 @@
</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')">
<!-- Display Currently Viewing. -->
<h2 class="dropdown-menu__section-heading" ng-if="vm.currentlyViewingGroupOrganizations().length > 0">Currently Viewing</h2>
<ul class="dropdown-menu__section" ng-if="vm.currentlyViewingGroupOrganizations().length > 0">
<li class="dropdown-community-groups-menu__row dropdown-menu__row--unpadded"
ng-repeat="group in vm.currentlyViewingGroupOrganizations() track by group.id">
<div ng-class="{'group-item-community-groups': true, 'is-selected': group.id == vm.groups.focused().id}"
ng-click="vm.focusGroup(group.id)" tabindex="0">
<!-- the group icon !-->
<div class="group-menu-icon-container">
<img class="group-list-label__icon group-list-label__icon--organization"
alt="{{ vm.orgName(group.id) }}"
ng-src="{{ group.logo }}"
ng-if="group.logo">
</div>
<!-- the group name and share link !-->
<div class="group-details-community-groups">
<a class="group-name-link"
href=""
title="{{ group.type === 'private' ? 'Show and create annotations in ' + group.name : 'Show public annotations' }}">
{{group.name}}
</a>
</div>
</li>
</ul>
<!-- Display Featured groups section. -->
<h2 class="dropdown-menu__section-heading" ng-if="vm.featuredGroupOrganizations().length > 0">Featured Groups</h2>
<ul class="dropdown-menu__section" ng-if="vm.featuredGroupOrganizations().length > 0">
<li class="dropdown-community-groups-menu__row dropdown-menu__row--unpadded"
ng-repeat="group in vm.featuredGroupOrganizations() track by group.id">
<div ng-class="{'group-item-community-groups': true, 'is-selected': group.id == vm.groups.focused().id}"
ng-click="vm.focusGroup(group.id)" tabindex="0">
<!-- the group icon !-->
<div class="group-menu-icon-container">
<img class="group-list-label__icon group-list-label__icon--organization"
alt="{{ vm.orgName(group.id) }}"
ng-src="{{ group.logo }}"
ng-if="group.logo">
</div>
<!-- the group name and share link !-->
<div class="group-details-community-groups">
<a class="group-name-link"
href=""
title="{{ group.type === 'private' ? 'Show and create annotations in ' + group.name : 'Show public annotations' }}">
{{group.name}}
</a>
</div>
</li>
</ul>
<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'"
section-groups="vm.currentlyViewingGroupOrganizations()"
ng-if="vm.currentlyViewingGroupOrganizations().length > 0"
>
</group-list-section>
<!-- Featured Groups -->
<group-list-section
class="group-list-section"
heading="'Featured Groups'"
section-groups="vm.featuredGroupOrganizations()"
ng-if="vm.featuredGroupOrganizations().length > 0"
>
</group-list-section>
<!-- Display My Groups groups section if user is logged in. -->
<h2 class="dropdown-menu__section-heading" ng-if="vm.myGroupOrganizations().length > 0">My Groups</h2>
<ul class="dropdown-menu__section" ng-if="vm.myGroupOrganizations().length > 0">
<li class="dropdown-community-groups-menu__row dropdown-menu__row--unpadded"
ng-repeat="group in vm.myGroupOrganizations() track by group.id">
<!-- Show the group like normal if it is scoped to this page -->
<div ng-class="{'group-item-community-groups': true, 'is-selected': group.id == vm.groups.focused().id}"
ng-click="vm.focusGroup(group.id)" tabindex="0" ng-if="group.isScopedToUri">
<!-- the group icon !-->
<div class="group-menu-icon-container">
<img class="group-list-label__icon group-list-label__icon--organization"
alt="{{ vm.orgName(group.id) }}"
ng-src="{{ group.logo }}"
ng-if="group.logo">
</div>
<!-- the group name and share link -->
<div class="group-details-community-groups">
<a class="group-name-link"
href=""
title="{{ group.type === 'private' ? 'Show and create annotations in ' + group.name : 'Show public annotations' }}">
{{group.name}}
</a>
</div>
</div>
<!-- Show the group as not selectable if it is not scoped to this page !-->
<div class="group-item--out-of-scope"
ng-class="{'group-item-community-groups': true, 'is-selected': group.id == vm.groups.focused().id}"
ng-click="vm.toggleGroupDetails($event, group.id)" tabindex="0" ng-if="!group.isScopedToUri">
<!-- the group icon !-->
<div class="group-menu-icon-container">
<img class="group-list-label__icon group-list-label__icon--organization"
alt="{{ vm.orgName(group.id) }}"
ng-src="{{ group.logo }}"
ng-if="group.logo">
</div>
<!-- the group name and share link !-->
<div ng-class="{'group-details-community-groups': true, expanded: vm.isGroupDetailsExpanded(group.id)}">
<svg class="svg-icon group__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>
<a class="group-name-link"
href=""
title="Group not annotatable on this domain.">
{{group.name}}
</a><br>
<p class="group-details__toggle">Why is this group unavailable?</p>
<p class="group-details__unavailable-message">
This group has been restricted to selected domains by its administrators.
</p>
<p class="group-details__actions">
<a class="button button--text group-details__group-page-link" href="{{group.links.html}}"
target="_blank"
ng-click="vm.viewGroupActivity()">Go to group page</a>
</p>
</div>
</div>
</li>
</ul>
<!-- My Groups -->
<group-list-section
class="group-list-section"
heading="'My Groups'"
section-groups="vm.myGroupOrganizations()"
disable-oos-group-selection="true"
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">
<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
......
'use strict';
function orgName(group) {
return group.organization && group.organization.name;
}
function trackViewGroupActivity(analytics) {
analytics.track(analytics.events.GROUP_VIEW_ACTIVITY);
}
module.exports = {
orgName,
trackViewGroupActivity,
};
'use strict';
const groupListItemCommon = require('../group-list-item-common');
const { events } = require('../../services/analytics');
describe('sidebar/util/groupListItemCommon', () => {
describe('trackViewGroupActivity', () => {
it('triggers the GROUP_VIEW_ACTIVITY event when called', () => {
const fakeAnalytics = {
track: sinon.stub(),
events,
};
groupListItemCommon.trackViewGroupActivity(fakeAnalytics);
assert.calledWith(
fakeAnalytics.track,
fakeAnalytics.events.GROUP_VIEW_ACTIVITY
);
});
});
describe('orgName', () => {
it('returns the organization name if it exists', () => {
const fakeGroup = { id: 'groupid', organization: { name: 'org' } };
const organizationName = groupListItemCommon.orgName(fakeGroup);
assert.equal(organizationName, fakeGroup.organization.name);
});
it('returns undefined if group has no organization', () => {
const fakeGroup = { id: 'groupid' };
assert.isUndefined(groupListItemCommon.orgName(fakeGroup));
});
});
});
......@@ -260,6 +260,10 @@ html {
min-width: 120px;
}
.dropdown-menu__row--no-border{
border: none;
}
.dropdown-menu__row--unpadded {
padding-left: 0px;
padding-right: 0px;
......
/* The group-list out of scope item. */
.group-list-item-out-of-scope {
display: flex;
flex-direction: row;
flex-grow: 1;
}
.group-list-item-out-of-scope__item {
background-color: $gray-lightest;
color: $gray-light;
}
.group-list-item-out-of-scope__icon--unavailable {
fill: $gray-light;
float: right;
height: 20px;
width: auto;
}
.group-list-item-out-of-scope__details {
flex-grow: 1;
flex-shrink: 1;
font-weight: 500;
}
.group-list-item-out-of-scope__details-toggle {
font-size: $body1-font-size;
font-style: italic;
margin: 0;
text-decoration: underline;
}
.group-list-item-out-of-scope__details-unavailable-message {
font-size: $body1-font-size;
line-height: 1.5;
white-space: normal;
width: $group-list-width - 60px;
}
.group-list-item-out-of-scope__details-actions {
text-align: right;
}
.group-list-item-out-of-scope__details-group-page-link {
color: inherit;
font-size: $body1-font-size;
text-decoration: underline;
text-transform: uppercase;
}
/* The group. */
.group-list-item {
display: flex;
flex-direction: row;
flex-grow: 1;
}
.group-list-item__item {
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 {
.name-link {
font-size: $body2-font-size;
font-weight: 600;
}
}
}
.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 {
flex-grow: 1;
flex-shrink: 1;
font-weight: 500;
}
/* 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;
}
/* The groups dropdown list. */
$group-list-width: 300px;
$group-list-spacing-below: 50px;
.group-list {
......@@ -10,7 +9,7 @@ $group-list-spacing-below: 50px;
.dropdown-menu {
width: $group-list-width;
max-height: 500px; // fallback for browsers lacking support for vh/calc
max-height: 500px; // fallback for browsers lacking support for vh/calc
max-height: calc(100vh - #{$top-bar-height} - #{$group-list-spacing-below});
overflow-y: auto;
......@@ -22,26 +21,11 @@ $group-list-spacing-below: 50px;
}
.dropdown-menu__section--no-header {
border-top: solid 1px rgba(0, 0, 0, 0.15);
.group-details {
font-weight: 400;
}
}
.dropdown-menu__section-heading {
color: $gray-light;
font-size: 12px;
line-height: 1;
margin: 1px 1px 0;
padding: 12px 10px 0;
text-transform: uppercase;
&:not(:first-child) {
border-top: solid 1px rgba(0, 0, 0, 0.15);
}
}
.group-item {
display: flex;
flex-direction: row;
......@@ -91,11 +75,6 @@ $group-list-spacing-below: 50px;
}
}
.group-item--out-of-scope {
background-color: $gray-lightest;
color: $gray-light;
}
.group-icon-container {
margin-right: 10px;
}
......@@ -114,13 +93,6 @@ $group-list-spacing-below: 50px;
margin-right: 2px;
}
.group__icon--unavailable {
fill: $gray-light;
float: right;
height: 20px;
width: auto;
}
.group-details {
flex-grow: 1;
flex-shrink: 1;
......@@ -147,31 +119,6 @@ $group-list-spacing-below: 50px;
}
}
.group-details__toggle {
font-size: $body1-font-size;
font-style: italic;
margin: 0;
text-decoration: underline;
}
.group-details__unavailable-message {
font-size: $body1-font-size;
line-height: 1.5;
white-space: normal;
width: $group-list-width - 60px;
}
.group-details__actions {
text-align: right;
}
.group-details__group-page-link {
color: inherit;
font-size: $body1-font-size;
text-decoration: underline;
text-transform: uppercase;
}
.new-group-btn {
background-color: $gray-lightest;
......@@ -210,7 +157,7 @@ $group-list-spacing-below: 50px;
// the drop-down list when clicked
.group-list-label__label {
font-size: $body2-font-size;
font-weight:bold;
font-weight: bold;
display: inline-block;
}
......@@ -226,7 +173,6 @@ $group-list-spacing-below: 50px;
color: inherit;
}
.open {
& > .group-list__toggle {
background: $gray-lighter;
......
......@@ -15,6 +15,9 @@ $base-line-height: 20px;
@import './components/dropdown-menu-btn';
@import './components/excerpt';
@import './components/group-list';
@import './components/group-list-item';
@import './components/group-list-item-out-of-scope';
@import './components/group-list-section';
@import './components/help-panel';
@import './components/loggedout-message';
@import './components/login-control';
......
......@@ -126,6 +126,7 @@ $highlight-color-second: rgba(206, 206, 60, 0.4);
$highlight-color-third: rgba(192, 192, 49, 0.4);
$highlight-color-focus: rgba(156, 230, 255, 0.5);
$top-bar-height: 40px;
$group-list-width: 300px;
// Mixins
// ------
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment