Unverified Commit 7ab3442a authored by Lyza Gardner's avatar Lyza Gardner Committed by GitHub

Merge pull request #1813 from hypothesis/disable-unauthenticated-annotation

Disable unauthenticated annotating and highlighting
parents b1f2a088 1854fb9b
...@@ -103,6 +103,8 @@ function HypothesisAppController( ...@@ -103,6 +103,8 @@ function HypothesisAppController(
return auth return auth
.login() .login()
.then(() => { .then(() => {
// If the prompt-to-log-in sidebar panel is open, close it
store.closeSidebarPanel(uiConstants.PANEL_LOGIN_PROMPT);
store.clearGroups(); store.clearGroups();
session.reload(); session.reload();
}) })
......
import { createElement } from 'preact';
import propTypes from 'prop-types';
import useStore from '../store/use-store';
import uiConstants from '../ui-constants';
import Button from './button';
import SidebarPanel from './sidebar-panel';
/**
* A sidebar panel that prompts a user to log in (or sign up) to annotate.
*/
export default function LoginPromptPanel({ onLogin, onSignUp }) {
const isLoggedIn = useStore(store => store.isLoggedIn());
if (isLoggedIn) {
return null;
}
return (
<SidebarPanel
icon="restricted"
title="Login needed"
panelName={uiConstants.PANEL_LOGIN_PROMPT}
>
<p>Please log in to create annotations or highlights.</p>
<div className="sidebar-panel__actions">
<Button
buttonText="Sign up"
className="sidebar-panel__button"
onClick={onSignUp}
/>
<Button
buttonText="Log in"
className="sidebar-panel__button"
onClick={onLogin}
usePrimaryStyle
/>
</div>
</SidebarPanel>
);
}
LoginPromptPanel.propTypes = {
onLogin: propTypes.func.isRequired,
onSignUp: propTypes.func.isRequired,
};
...@@ -197,6 +197,7 @@ export default { ...@@ -197,6 +197,7 @@ export default {
bindings: { bindings: {
auth: '<', auth: '<',
onLogin: '&', onLogin: '&',
onSignUp: '&',
}, },
template: require('../templates/sidebar-content.html'), template: require('../templates/sidebar-content.html'),
}; };
...@@ -7,6 +7,7 @@ import useStore from '../store/use-store'; ...@@ -7,6 +7,7 @@ import useStore from '../store/use-store';
import Button from './button'; import Button from './button';
import Slider from './slider'; import Slider from './slider';
import SvgIcon from './svg-icon';
/** /**
* Base component for a sidebar panel. * Base component for a sidebar panel.
...@@ -17,6 +18,7 @@ import Slider from './slider'; ...@@ -17,6 +18,7 @@ import Slider from './slider';
*/ */
export default function SidebarPanel({ export default function SidebarPanel({
children, children,
icon = '',
panelName, panelName,
title, title,
onActiveChanged, onActiveChanged,
...@@ -48,6 +50,11 @@ export default function SidebarPanel({ ...@@ -48,6 +50,11 @@ export default function SidebarPanel({
<Slider visible={panelIsActive}> <Slider visible={panelIsActive}>
<div className="sidebar-panel" ref={panelElement}> <div className="sidebar-panel" ref={panelElement}>
<div className="sidebar-panel__header"> <div className="sidebar-panel__header">
{icon && (
<div className="sidebar-panel__header-icon">
<SvgIcon name={icon} title={title} />
</div>
)}
<div className="sidebar-panel__title u-stretch">{title}</div> <div className="sidebar-panel__title u-stretch">{title}</div>
<div> <div>
<Button <Button
...@@ -67,6 +74,11 @@ export default function SidebarPanel({ ...@@ -67,6 +74,11 @@ export default function SidebarPanel({
SidebarPanel.propTypes = { SidebarPanel.propTypes = {
children: propTypes.any, children: propTypes.any,
/**
* An optional icon name for display next to the panel's title
*/
icon: propTypes.string,
/** /**
* A string identifying this panel. Only one `panelName` may be active at * A string identifying this panel. Only one `panelName` may be active at
* any time. Multiple panels with the same `panelName` would be "in sync", * any time. Multiple panels with the same `panelName` would be "in sync",
......
...@@ -75,6 +75,7 @@ describe('sidebar.components.hypothesis-app', function() { ...@@ -75,6 +75,7 @@ describe('sidebar.components.hypothesis-app', function() {
}, },
}), }),
clearGroups: sinon.stub(), clearGroups: sinon.stub(),
closeSidebarPanel: sinon.stub(),
openSidebarPanel: sinon.stub(), openSidebarPanel: sinon.stub(),
// draft store // draft store
countDrafts: sandbox.stub().returns(0), countDrafts: sandbox.stub().returns(0),
...@@ -335,6 +336,13 @@ describe('sidebar.components.hypothesis-app', function() { ...@@ -335,6 +336,13 @@ describe('sidebar.components.hypothesis-app', function() {
}); });
}); });
it('closes the login prompt panel', () => {
const ctrl = createController();
return ctrl.login().then(() => {
assert.called(fakeStore.closeSidebarPanel);
});
});
it('reports an error if login fails', () => { it('reports an error if login fails', () => {
fakeAuth.login.returns(Promise.reject(new Error('Login failed'))); fakeAuth.login.returns(Promise.reject(new Error('Login failed')));
......
import { mount } from 'enzyme';
import { createElement } from 'preact';
import LoginPromptPanel from '../login-prompt-panel';
import { $imports } from '../login-prompt-panel';
import { checkAccessibility } from '../../../test-util/accessibility';
import mockImportedComponents from '../../../test-util/mock-imported-components';
describe('LoginPromptPanel', function() {
let fakeOnLogin;
let fakeOnSignUp;
let fakeStore;
function createComponent(props) {
return mount(
<LoginPromptPanel
onLogin={fakeOnLogin}
onSignUp={fakeOnSignUp}
{...props}
/>
);
}
beforeEach(() => {
fakeStore = {
isLoggedIn: sinon.stub().returns(false),
};
fakeOnLogin = sinon.stub();
fakeOnSignUp = sinon.stub();
$imports.$mock(mockImportedComponents());
$imports.$mock({
'../store/use-store': callback => callback(fakeStore),
});
});
afterEach(() => {
$imports.$restore();
});
it('should render if user not logged in', () => {
fakeStore.isLoggedIn.returns(false);
const wrapper = createComponent();
assert.isTrue(wrapper.find('SidebarPanel').exists());
});
it('should not render if user is logged in', () => {
fakeStore.isLoggedIn.returns(true);
const wrapper = createComponent();
assert.isFalse(wrapper.find('SidebarPanel').exists());
});
it(
'should pass a11y checks',
checkAccessibility({
content: () => createComponent(),
})
);
});
...@@ -44,6 +44,14 @@ describe('SidebarPanel', () => { ...@@ -44,6 +44,14 @@ describe('SidebarPanel', () => {
assert.equal(titleEl.text(), 'My Panel'); assert.equal(titleEl.text(), 'My Panel');
}); });
it('renders an icon if provided', () => {
const wrapper = createSidebarPanel({ icon: 'restricted' });
const icon = wrapper.find('SvgIcon').filter({ name: 'restricted' });
assert.isTrue(icon.exists());
});
it('closes the panel when close button is clicked', () => { it('closes the panel when close button is clicked', () => {
const wrapper = createSidebarPanel({ panelName: 'flibberty' }); const wrapper = createSidebarPanel({ panelName: 'flibberty' });
......
...@@ -85,7 +85,7 @@ function configureRoutes($routeProvider) { ...@@ -85,7 +85,7 @@ function configureRoutes($routeProvider) {
}); });
$routeProvider.otherwise({ $routeProvider.otherwise({
template: template:
'<sidebar-content auth="vm.auth" on-login="vm.login()"></sidebar-content>', '<sidebar-content auth="vm.auth" on-login="vm.login()" on-sign-up="vm.signUp()"></sidebar-content>',
reloadOnSearch: false, reloadOnSearch: false,
resolve: resolve, resolve: resolve,
}); });
...@@ -135,6 +135,7 @@ import AnnotationQuote from './components/annotation-quote'; ...@@ -135,6 +135,7 @@ import AnnotationQuote from './components/annotation-quote';
import FocusedModeHeader from './components/focused-mode-header'; import FocusedModeHeader from './components/focused-mode-header';
import HelpPanel from './components/help-panel'; import HelpPanel from './components/help-panel';
import LoggedOutMessage from './components/logged-out-message'; import LoggedOutMessage from './components/logged-out-message';
import LoginPromptPanel from './components/login-prompt-panel';
import ModerationBanner from './components/moderation-banner'; import ModerationBanner from './components/moderation-banner';
import SearchStatusBar from './components/search-status-bar'; import SearchStatusBar from './components/search-status-bar';
import SelectionTabs from './components/selection-tabs'; import SelectionTabs from './components/selection-tabs';
...@@ -274,6 +275,7 @@ function startAngularApp(config) { ...@@ -274,6 +275,7 @@ function startAngularApp(config) {
.component('annotationThread', annotationThread) .component('annotationThread', annotationThread)
.component('annotationViewerContent', annotationViewerContent) .component('annotationViewerContent', annotationViewerContent)
.component('helpPanel', wrapComponent(HelpPanel)) .component('helpPanel', wrapComponent(HelpPanel))
.component('loginPromptPanel', wrapComponent(LoginPromptPanel))
.component('loggedOutMessage', wrapComponent(LoggedOutMessage)) .component('loggedOutMessage', wrapComponent(LoggedOutMessage))
.component('moderationBanner', wrapComponent(ModerationBanner)) .component('moderationBanner', wrapComponent(ModerationBanner))
.component('searchStatusBar', wrapComponent(SearchStatusBar)) .component('searchStatusBar', wrapComponent(SearchStatusBar))
......
...@@ -128,8 +128,18 @@ export default function FrameSync($rootScope, $window, store, bridge) { ...@@ -128,8 +128,18 @@ export default function FrameSync($rootScope, $window, store, bridge) {
function setupSyncFromFrame() { function setupSyncFromFrame() {
// A new annotation, note or highlight was created in the frame // A new annotation, note or highlight was created in the frame
bridge.on('beforeCreateAnnotation', function(event) { bridge.on('beforeCreateAnnotation', function(event) {
inFrame.add(event.tag);
const annot = Object.assign({}, event.msg, { $tag: event.tag }); const annot = Object.assign({}, event.msg, { $tag: event.tag });
// If user is not logged in, we can't really create a meaningful highlight
// or annotation. Instead, we need to open the sidebar, show an error,
// and delete the (unsaved) annotation so it gets un-selected in the
// target document
if (!store.isLoggedIn()) {
bridge.call('showSidebar');
store.openSidebarPanel(uiConstants.PANEL_LOGIN_PROMPT);
bridge.call('deleteAnnotation', formatAnnot(annot));
return;
}
inFrame.add(event.tag);
$rootScope.$broadcast(events.BEFORE_ANNOTATION_CREATED, annot); $rootScope.$broadcast(events.BEFORE_ANNOTATION_CREATED, annot);
}); });
......
...@@ -66,6 +66,8 @@ describe('sidebar.frame-sync', function() { ...@@ -66,6 +66,8 @@ describe('sidebar.frame-sync', function() {
findIDsForTags: sinon.stub(), findIDsForTags: sinon.stub(),
focusAnnotations: sinon.stub(), focusAnnotations: sinon.stub(),
frames: sinon.stub().returns([fixtures.framesListEntry]), frames: sinon.stub().returns([fixtures.framesListEntry]),
isLoggedIn: sinon.stub().returns(false),
openSidebarPanel: sinon.stub(),
selectAnnotations: sinon.stub(), selectAnnotations: sinon.stub(),
selectTab: sinon.stub(), selectTab: sinon.stub(),
toggleSelectedAnnotations: sinon.stub(), toggleSelectedAnnotations: sinon.stub(),
...@@ -211,7 +213,9 @@ describe('sidebar.frame-sync', function() { ...@@ -211,7 +213,9 @@ describe('sidebar.frame-sync', function() {
}); });
context('when a new annotation is created in the frame', function() { context('when a new annotation is created in the frame', function() {
context('when an authenticated user is present', () => {
it('emits a BEFORE_ANNOTATION_CREATED event', function() { it('emits a BEFORE_ANNOTATION_CREATED event', function() {
fakeStore.isLoggedIn.returns(true);
const onCreated = sinon.stub(); const onCreated = sinon.stub();
const ann = { target: [] }; const ann = { target: [] };
$rootScope.$on(events.BEFORE_ANNOTATION_CREATED, onCreated); $rootScope.$on(events.BEFORE_ANNOTATION_CREATED, onCreated);
...@@ -229,6 +233,47 @@ describe('sidebar.frame-sync', function() { ...@@ -229,6 +233,47 @@ describe('sidebar.frame-sync', function() {
}); });
}); });
context('when no authenticated user is present', () => {
beforeEach(() => {
fakeStore.isLoggedIn.returns(false);
});
it('should not emit BEFORE_ANNOTATION_CREATED event', () => {
const onCreated = sinon.stub();
const ann = { target: [] };
$rootScope.$on(events.BEFORE_ANNOTATION_CREATED, onCreated);
fakeBridge.emit('beforeCreateAnnotation', { tag: 't1', msg: ann });
assert.notCalled(onCreated);
});
it('should open the sidebar', () => {
const ann = { target: [] };
fakeBridge.emit('beforeCreateAnnotation', { tag: 't1', msg: ann });
assert.calledWith(fakeBridge.call, 'showSidebar');
});
it('should open the login prompt panel', () => {
const ann = { target: [] };
fakeBridge.emit('beforeCreateAnnotation', { tag: 't1', msg: ann });
assert.calledWith(
fakeStore.openSidebarPanel,
uiConstants.PANEL_LOGIN_PROMPT
);
});
it('should send a "deleteAnnotation" message to the frame', () => {
const ann = { target: [] };
fakeBridge.emit('beforeCreateAnnotation', { tag: 't1', msg: ann });
assert.calledWith(fakeBridge.call, 'deleteAnnotation');
});
});
});
context('when anchoring completes', function() { context('when anchoring completes', function() {
let clock = sinon.stub(); let clock = sinon.stub();
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
ng-if="vm.showFocusedHeader()"> ng-if="vm.showFocusedHeader()">
</focused-mode-header> </focused-mode-header>
<login-prompt-panel on-login="vm.onLogin()" on-sign-up="vm.onSignUp()"></login-prompt-panel>
<selection-tabs <selection-tabs
ng-if="vm.showSelectedTabs()" ng-if="vm.showSelectedTabs()"
is-loading="vm.isLoading()"> is-loading="vm.isLoading()">
...@@ -27,6 +29,7 @@ ...@@ -27,6 +29,7 @@
> >
</sidebar-content-error> </sidebar-content-error>
<thread-list <thread-list
on-change-collapsed="vm.setCollapsed(id, collapsed)" on-change-collapsed="vm.setCollapsed(id, collapsed)"
on-focus="vm.focus(annotation)" on-focus="vm.focus(annotation)"
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
export default { export default {
PANEL_HELP: 'help', PANEL_HELP: 'help',
PANEL_LOGIN_PROMPT: 'loginPrompt',
PANEL_SHARE_ANNOTATIONS: 'shareGroupAnnotations', PANEL_SHARE_ANNOTATIONS: 'shareGroupAnnotations',
TAB_ANNOTATIONS: 'annotation', TAB_ANNOTATIONS: 'annotation',
TAB_NOTES: 'note', TAB_NOTES: 'note',
......
...@@ -44,6 +44,15 @@ ...@@ -44,6 +44,15 @@
margin: 1em; margin: 1em;
margin-top: 0; margin-top: 0;
} }
&__button {
margin-left: 1em;
}
&__actions {
display: flex;
justify-content: flex-end;
}
} }
/** /**
......
...@@ -5,13 +5,4 @@ ...@@ -5,13 +5,4 @@
@include panel.panel; @include panel.panel;
position: relative; position: relative;
margin-bottom: 0.75em; margin-bottom: 0.75em;
&__button {
margin-left: 1em;
}
&__actions {
display: flex;
justify-content: flex-end;
}
} }
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