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(
return auth
.login()
.then(() => {
// If the prompt-to-log-in sidebar panel is open, close it
store.closeSidebarPanel(uiConstants.PANEL_LOGIN_PROMPT);
store.clearGroups();
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 {
bindings: {
auth: '<',
onLogin: '&',
onSignUp: '&',
},
template: require('../templates/sidebar-content.html'),
};
......@@ -7,6 +7,7 @@ import useStore from '../store/use-store';
import Button from './button';
import Slider from './slider';
import SvgIcon from './svg-icon';
/**
* Base component for a sidebar panel.
......@@ -17,6 +18,7 @@ import Slider from './slider';
*/
export default function SidebarPanel({
children,
icon = '',
panelName,
title,
onActiveChanged,
......@@ -48,6 +50,11 @@ export default function SidebarPanel({
<Slider visible={panelIsActive}>
<div className="sidebar-panel" ref={panelElement}>
<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>
<Button
......@@ -67,6 +74,11 @@ export default function SidebarPanel({
SidebarPanel.propTypes = {
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
* any time. Multiple panels with the same `panelName` would be "in sync",
......
......@@ -75,6 +75,7 @@ describe('sidebar.components.hypothesis-app', function() {
},
}),
clearGroups: sinon.stub(),
closeSidebarPanel: sinon.stub(),
openSidebarPanel: sinon.stub(),
// draft store
countDrafts: sandbox.stub().returns(0),
......@@ -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', () => {
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', () => {
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', () => {
const wrapper = createSidebarPanel({ panelName: 'flibberty' });
......
......@@ -85,7 +85,7 @@ function configureRoutes($routeProvider) {
});
$routeProvider.otherwise({
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,
resolve: resolve,
});
......@@ -135,6 +135,7 @@ import AnnotationQuote from './components/annotation-quote';
import FocusedModeHeader from './components/focused-mode-header';
import HelpPanel from './components/help-panel';
import LoggedOutMessage from './components/logged-out-message';
import LoginPromptPanel from './components/login-prompt-panel';
import ModerationBanner from './components/moderation-banner';
import SearchStatusBar from './components/search-status-bar';
import SelectionTabs from './components/selection-tabs';
......@@ -274,6 +275,7 @@ function startAngularApp(config) {
.component('annotationThread', annotationThread)
.component('annotationViewerContent', annotationViewerContent)
.component('helpPanel', wrapComponent(HelpPanel))
.component('loginPromptPanel', wrapComponent(LoginPromptPanel))
.component('loggedOutMessage', wrapComponent(LoggedOutMessage))
.component('moderationBanner', wrapComponent(ModerationBanner))
.component('searchStatusBar', wrapComponent(SearchStatusBar))
......
......@@ -128,8 +128,18 @@ export default function FrameSync($rootScope, $window, store, bridge) {
function setupSyncFromFrame() {
// A new annotation, note or highlight was created in the frame
bridge.on('beforeCreateAnnotation', function(event) {
inFrame.add(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);
});
......
......@@ -66,6 +66,8 @@ describe('sidebar.frame-sync', function() {
findIDsForTags: sinon.stub(),
focusAnnotations: sinon.stub(),
frames: sinon.stub().returns([fixtures.framesListEntry]),
isLoggedIn: sinon.stub().returns(false),
openSidebarPanel: sinon.stub(),
selectAnnotations: sinon.stub(),
selectTab: sinon.stub(),
toggleSelectedAnnotations: sinon.stub(),
......@@ -211,21 +213,64 @@ describe('sidebar.frame-sync', function() {
});
context('when a new annotation is created in the frame', function() {
it('emits a BEFORE_ANNOTATION_CREATED event', function() {
const onCreated = sinon.stub();
const ann = { target: [] };
$rootScope.$on(events.BEFORE_ANNOTATION_CREATED, onCreated);
context('when an authenticated user is present', () => {
it('emits a BEFORE_ANNOTATION_CREATED event', function() {
fakeStore.isLoggedIn.returns(true);
const onCreated = sinon.stub();
const ann = { target: [] };
$rootScope.$on(events.BEFORE_ANNOTATION_CREATED, onCreated);
fakeBridge.emit('beforeCreateAnnotation', { tag: 't1', msg: ann });
assert.calledWithMatch(
onCreated,
sinon.match.any,
sinon.match({
$tag: 't1',
target: [],
})
);
});
});
fakeBridge.emit('beforeCreateAnnotation', { tag: 't1', msg: ann });
context('when no authenticated user is present', () => {
beforeEach(() => {
fakeStore.isLoggedIn.returns(false);
});
assert.calledWithMatch(
onCreated,
sinon.match.any,
sinon.match({
$tag: 't1',
target: [],
})
);
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');
});
});
});
......
......@@ -2,6 +2,8 @@
ng-if="vm.showFocusedHeader()">
</focused-mode-header>
<login-prompt-panel on-login="vm.onLogin()" on-sign-up="vm.onSignUp()"></login-prompt-panel>
<selection-tabs
ng-if="vm.showSelectedTabs()"
is-loading="vm.isLoading()">
......@@ -27,6 +29,7 @@
>
</sidebar-content-error>
<thread-list
on-change-collapsed="vm.setCollapsed(id, collapsed)"
on-focus="vm.focus(annotation)"
......
......@@ -4,6 +4,7 @@
export default {
PANEL_HELP: 'help',
PANEL_LOGIN_PROMPT: 'loginPrompt',
PANEL_SHARE_ANNOTATIONS: 'shareGroupAnnotations',
TAB_ANNOTATIONS: 'annotation',
TAB_NOTES: 'note',
......
......@@ -44,6 +44,15 @@
margin: 1em;
margin-top: 0;
}
&__button {
margin-left: 1em;
}
&__actions {
display: flex;
justify-content: flex-end;
}
}
/**
......
......@@ -5,13 +5,4 @@
@include panel.panel;
position: relative;
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