Commit 66d4cc02 authored by Robert Knight's avatar Robert Knight

Merge branch 'master' into new-group-menu

parents abe75126 521b1ffd
......@@ -89,7 +89,7 @@
"postcss": "^7.0.13",
"postcss-url": "^8.0.0",
"preact": "10.0.0-beta.2",
"prettier": "1.17.1",
"prettier": "1.18.2",
"query-string": "^3.0.1",
"raf": "^3.1.0",
"raven-js": "^3.7.0",
......
......@@ -36,9 +36,7 @@ function httpRequest(opts) {
/** Create a release in Sentry. Returns a Promise. */
function createRelease(opts, project, release) {
return httpRequest({
uri: `${SENTRY_API_ROOT}/projects/${
opts.organization
}/${project}/releases/`,
uri: `${SENTRY_API_ROOT}/projects/${opts.organization}/${project}/releases/`,
method: 'POST',
auth: {
user: opts.key,
......@@ -66,9 +64,7 @@ function createRelease(opts, project, release) {
/** Upload a named file to a release in Sentry. Returns a Promise. */
function uploadReleaseFile(opts, project, release, file) {
return httpRequest({
uri: `${SENTRY_API_ROOT}/projects/${
opts.organization
}/${project}/releases/${release}/files/`,
uri: `${SENTRY_API_ROOT}/projects/${opts.organization}/${project}/releases/${release}/files/`,
method: 'POST',
auth: {
user: opts.key,
......
......@@ -5,9 +5,9 @@
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="Artboard-1-Copy" sketch:type="MSArtboardGroup" fill="#000000">
<g id="Artboard-1-Copy" sketch:type="MSArtboardGroup" fill="currentColor">
<circle id="Oval-1" sketch:type="MSShapeGroup" cx="8" cy="4" r="3"></circle>
<path d="M8,15 C11,15 14,14.4329966 14,12 C14,10.5670034 10,8 8,8 C6,8 2,10.5670034 2,12 C2,14.4329966 5,15 8,15 Z" id="Oval-1-Copy" sketch:type="MSShapeGroup"></path>
</g>
</g>
</svg>
\ No newline at end of file
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="currentColor" fill-rule="nonzero" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-.5-5.805c.496 0 .768-.29.873-.788.091-.623.3-.943 1.23-1.515.987-.616 1.497-1.381 1.497-2.503C11.1 3.66 9.765 2.5 7.779 2.5c-1.503 0-2.622.631-3.07 1.612-.14.282-.209.564-.209.884 0 .564.343.928.895.928.426 0 .741-.208.916-.683.224-.661.685-1.018 1.37-1.018.77 0 1.3.505 1.3 1.233 0 .683-.272 1.055-1.174 1.627-.825.512-1.251 1.092-1.251 1.968v.104c0 .609.35 1.04.943 1.04zm.013 3.305c.637 0 1.147-.512 1.147-1.166 0-.654-.51-1.166-1.147-1.166-.629 0-1.14.512-1.14 1.166 0 .654.511 1.166 1.14 1.166z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#000" fill-rule="nonzero" d="M16 1.656v3.5c0 .587-.721.874-1.138.464l-.992-.977-6.764 6.66a.674.674 0 0 1-.943 0l-.629-.62a.649.649 0 0 1 0-.927l6.765-6.66-.992-.976c-.419-.412-.122-1.12.471-1.12h3.555c.369 0 .667.294.667.656zm-4.694 6.749c.42-.414 1.138-.121 1.138.464v4.819c0 .724-.597 1.312-1.333 1.312H1.333A1.323 1.323 0 0 1 0 13.687V4.063C0 3.338.597 2.75 1.333 2.75h8.223c.593 0 .89.707.47 1.12l-.444.438a.772.772 0 0 1-.47.192H1.777v8.75h8.889V9.306c0-.15.087-.358.195-.464l.444-.437z"/>
</svg>
\ No newline at end of file
<path fill="currentColor" fill-rule="nonzero" d="M16 1.656v3.5c0 .587-.721.874-1.138.464l-.992-.977-6.764 6.66a.674.674 0 0 1-.943 0l-.629-.62a.649.649 0 0 1 0-.927l6.765-6.66-.992-.976c-.419-.412-.122-1.12.471-1.12h3.555c.369 0 .667.294.667.656zm-4.694 6.749c.42-.414 1.138-.121 1.138.464v4.819c0 .724-.597 1.312-1.333 1.312H1.333A1.323 1.323 0 0 1 0 13.687V4.063C0 3.338.597 2.75 1.333 2.75h8.223c.593 0 .89.707.47 1.12l-.444.438a.772.772 0 0 1-.47.192H1.777v8.75h8.889V9.306c0-.15.087-.358.195-.464l.444-.437z"/>
</svg>
......@@ -21,21 +21,21 @@ function visibleCount(thread) {
}
function showAllChildren(thread, showFn) {
thread.children.forEach(function(child) {
showFn({ thread: child });
thread.children.forEach(child => {
showFn(child);
showAllChildren(child, showFn);
});
}
function showAllParents(thread, showFn) {
while (thread.parent && thread.parent.annotation) {
showFn({ thread: thread.parent });
showFn(thread.parent);
thread = thread.parent;
}
}
// @ngInject
function AnnotationThreadController() {
function AnnotationThreadController(store) {
// Flag that tracks whether the content of the annotation is hovered,
// excluding any replies.
this.annotationHovered = false;
......@@ -78,7 +78,7 @@ function AnnotationThreadController() {
*/
this.showThreadAndReplies = function() {
showAllParents(this.thread, this.onForceVisible);
this.onForceVisible({ thread: this.thread });
this.onForceVisible(this.thread);
showAllChildren(this.thread, this.onForceVisible);
};
......@@ -98,6 +98,13 @@ function AnnotationThreadController() {
this.shouldShowReply = function(child) {
return visibleCount(child) > 0;
};
this.onForceVisible = function(thread) {
store.setForceVisible(thread.id, true);
if (thread.parent) {
store.setCollapsed(thread.parent.id, false);
}
};
}
/**
......@@ -116,11 +123,6 @@ module.exports = {
showDocumentInfo: '<',
/** Called when the user clicks on the expand/collapse replies toggle. */
onChangeCollapsed: '&',
/**
* Called when the user clicks the button to show this thread or
* one of its replies.
*/
onForceVisible: '&',
},
template: require('../templates/annotation-thread.html'),
};
......@@ -53,6 +53,7 @@ function AnnotationController(
store,
annotationMapper,
api,
bridge,
drafts,
flash,
groups,
......@@ -197,6 +198,11 @@ function AnnotationController(
return;
}
if (!self.annotation.user) {
// Open sidebar to display error message about needing to login to create highlights.
bridge.call('showSidebar');
}
if (!self.isHighlight()) {
// Not a highlight,
return;
......
'use strict';
const bridgeEvents = require('../../shared/bridge-events');
const { isThirdPartyUser } = require('../util/account-id');
const serviceConfig = require('../service-config');
module.exports = {
controllerAs: 'vm',
//@ngInject
controller: function(bridge, serviceUrl, settings, $window) {
this.serviceUrl = serviceUrl;
this.isThirdPartyUser = function() {
return isThirdPartyUser(this.auth.userid, settings.authDomain);
};
this.shouldShowLogOutButton = function() {
if (this.auth.status !== 'logged-in') {
return false;
}
const service = serviceConfig(settings);
if (service && !service.onLogoutRequestProvided) {
return false;
}
return true;
};
this.shouldEnableProfileButton = function() {
const service = serviceConfig(settings);
if (service) {
return service.onProfileRequestProvided;
}
return true;
};
this.showProfile = function() {
if (this.isThirdPartyUser()) {
bridge.call(bridgeEvents.PROFILE_REQUESTED);
return;
}
$window.open(this.serviceUrl('user', { user: this.auth.username }));
};
},
controller: function() {},
bindings: {
/**
......@@ -51,10 +14,6 @@ module.exports = {
/**
* Called when the user clicks on the "About this version" text.
*/
onShowHelpPanel: '&',
/**
* Called when the user clicks on the "Log in" text.
*/
onLogin: '&',
/**
* Called when the user clicks on the "Sign Up" text.
......@@ -64,12 +23,6 @@ module.exports = {
* Called when the user clicks on the "Log out" text.
*/
onLogout: '&',
/**
* Whether or not to use the new design for the control.
*
* FIXME: should be removed when the old design is deprecated.
*/
newStyle: '<',
},
template: require('../templates/login-control.html'),
};
......@@ -53,7 +53,7 @@ function MenuItem({
'is-selected': isSelected,
})}
role="menuitem"
{...onClick && onActivate('menuitem', onClick)}
{...(onClick && onActivate('menuitem', onClick))}
>
{icon !== undefined && (
<div className="menu-item__icon-container">
......
......@@ -150,7 +150,10 @@ Menu.propTypes = {
/**
* Label element for the toggle button that hides and shows the menu.
*/
label: propTypes.object.isRequired,
label: propTypes.oneOfType([
propTypes.object.isRequired,
propTypes.string.isRequired,
]),
/**
* Menu items and sections to display in the content area of the menu.
......
......@@ -276,13 +276,6 @@ function SidebarContentController(
store.setCollapsed(id, collapsed);
};
this.forceVisible = function(thread) {
store.setForceVisible(thread.id, true);
if (thread.parent) {
store.setCollapsed(thread.parent.id, false);
}
};
this.focus = focusAnnotation;
this.scrollTo = scrollToAnnotation;
......
......@@ -59,9 +59,6 @@ function StreamContentController(
fetch(20);
this.setCollapsed = store.setCollapsed;
this.forceVisible = function(id) {
store.setForceVisible(id, true);
};
store.subscribe(function() {
self.rootThread = rootThread.thread(store.getState());
......
......@@ -11,6 +11,7 @@ const icons = {
'expand-menu': require('../../images/icons/expand-menu.svg'),
copy: require('../../images/icons/copy.svg'),
cursor: require('../../images/icons/cursor.svg'),
help: require('../../images/icons/help.svg'),
leave: require('../../images/icons/leave.svg'),
refresh: require('../../images/icons/refresh.svg'),
share: require('../../images/icons/share.svg'),
......@@ -40,7 +41,9 @@ function SvgIcon({ name, className = '' }) {
markup,
]);
return <span dangerouslySetInnerHTML={markup} ref={element} />;
return (
<span className="svg-icon" dangerouslySetInnerHTML={markup} ref={element} />
);
}
SvgIcon.propTypes = {
......
......@@ -115,6 +115,7 @@ describe('annotation', function() {
let fakeSession;
let fakeSettings;
let fakeApi;
let fakeBridge;
let fakeStreamer;
let sandbox;
......@@ -248,6 +249,10 @@ describe('annotation', function() {
},
};
fakeBridge = {
call: sinon.stub(),
};
fakeStreamer = {
hasPendingDeletion: sinon.stub(),
};
......@@ -256,6 +261,7 @@ describe('annotation', function() {
$provide.value('annotationMapper', fakeAnnotationMapper);
$provide.value('store', fakeStore);
$provide.value('api', fakeApi);
$provide.value('bridge', fakeBridge);
$provide.value('drafts', fakeDrafts);
$provide.value('flash', fakeFlash);
$provide.value('groups', fakeGroups);
......@@ -373,6 +379,18 @@ describe('annotation', function() {
assert.called(fakeDrafts.update);
});
it('opens the sidebar when trying to save highlights while logged out', () => {
// The sidebar is opened in order to draw the user's attention to
// the `You must be logged in to create annotations and highlights` message.
const annotation = fixtures.newHighlight();
// The user is not logged-in.
annotation.user = fakeSession.state.userid = undefined;
createDirective(annotation);
assert.calledWith(fakeBridge.call, 'showSidebar');
});
it('does not save new annotations on initialization', function() {
const annotation = fixtures.newAnnotation();
......
......@@ -36,8 +36,16 @@ describe('annotationThread', function() {
});
});
let fakeStore;
beforeEach(function() {
angular.mock.module('app');
fakeStore = {
setForceVisible: sinon.stub(),
setCollapsed: sinon.stub(),
getState: sinon.stub(),
};
angular.mock.module('app', { store: fakeStore });
});
it('renders the tree structure of parent and child annotations', function() {
......@@ -75,6 +83,33 @@ describe('annotationThread', function() {
assert.isTrue(pageObject.isHidden(pageObject.annotations()[0]));
});
describe('onForceVisible', () => {
it('shows the thread', () => {
const thread = {
id: '1',
children: [],
};
const element = util.createDirective(document, 'annotationThread', {
thread: thread,
});
element.ctrl.onForceVisible(thread);
assert.calledWith(fakeStore.setForceVisible, thread.id, true);
});
it('uncollapses the parent', () => {
const thread = {
id: '2',
children: [],
parent: { id: '3' },
};
const element = util.createDirective(document, 'annotationThread', {
thread: thread,
});
element.ctrl.onForceVisible(thread);
assert.calledWith(fakeStore.setCollapsed, thread.parent.id, false);
});
});
it('shows replies if not collapsed', function() {
const element = util.createDirective(document, 'annotationThread', {
thread: {
......@@ -166,7 +201,6 @@ describe('annotationThread', function() {
describe('#showThreadAndReplies', function() {
it('reveals all parents and replies', function() {
const onForceVisible = sinon.stub();
const thread = {
id: '123',
annotation: { id: '123' },
......@@ -184,15 +218,12 @@ describe('annotationThread', function() {
};
const element = util.createDirective(document, 'annotationThread', {
thread: thread,
onForceVisible: {
args: ['thread'],
callback: onForceVisible,
},
});
element.ctrl.showThreadAndReplies();
assert.calledWith(onForceVisible, thread.parent);
assert.calledWith(onForceVisible, thread);
assert.calledWith(onForceVisible, thread.children[0]);
assert.calledWith(fakeStore.setForceVisible, thread.parent.id, true);
assert.calledWith(fakeStore.setForceVisible, thread.id, true);
assert.calledWith(fakeStore.setForceVisible, thread.children[0].id, true);
assert.calledWith(fakeStore.setCollapsed, thread.parent.id, false);
});
});
......
......@@ -78,9 +78,7 @@ describe('Menu', () => {
new Event('click'),
((e = new Event('keypress')), (e.key = 'Escape'), e),
].forEach(event => {
it(`closes when the user clicks or presses the mouse outside (${
event.type
})`, () => {
it(`closes when the user clicks or presses the mouse outside (${event.type})`, () => {
const wrapper = createMenu({ defaultOpen: true });
act(() => {
......
......@@ -717,24 +717,6 @@ describe('sidebar.components.sidebar-content', function() {
});
});
describe('#forceVisible', function() {
it('shows the thread', function() {
const thread = { id: '1' };
ctrl.forceVisible(thread);
assert.deepEqual(store.getState().forceVisible, { 1: true });
});
it('uncollapses the parent', function() {
const thread = {
id: '2',
parent: { id: '3' },
};
assert.equal(store.getState().expanded[thread.parent.id], undefined);
ctrl.forceVisible(thread);
assert.equal(store.getState().expanded[thread.parent.id], true);
});
});
describe('#visibleCount', function() {
it('returns the total number of visible annotations or replies', function() {
fakeRootThread.thread.returns({
......
......@@ -41,6 +41,10 @@ describe('topBar', function() {
return el.querySelector('.top-bar__apply-update-btn');
}
function helpBtn(el) {
return el.querySelector('.top-bar__help-btn');
}
function createTopBar(inputs) {
const defaultInputs = {
isSidebar: true,
......@@ -79,6 +83,16 @@ describe('topBar', function() {
assert.called(onApplyPendingUpdates);
});
it('shows help when help icon clicked', function() {
const onShowHelpPanel = sinon.stub();
const el = createTopBar({
onShowHelpPanel: onShowHelpPanel,
});
const help = helpBtn(el[0]);
help.click();
assert.called(onShowHelpPanel);
});
it('displays the login control and propagates callbacks', function() {
const onShowHelpPanel = sinon.stub();
const onLogin = sinon.stub();
......@@ -90,9 +104,6 @@ describe('topBar', function() {
});
const loginControl = el.find('login-control').controller('loginControl');
loginControl.onShowHelpPanel();
assert.called(onShowHelpPanel);
loginControl.onLogin();
assert.called(onLogin);
......
'use strict';
const { createElement } = require('preact');
const { shallow } = require('enzyme');
const UserMenu = require('../user-menu');
const MenuItem = require('../menu-item');
describe('UserMenu', () => {
let fakeAuth;
let fakeBridge;
let fakeIsThirdPartyUser;
let fakeOnLogout;
let fakeProfileBridgeEvent;
let fakeServiceConfig;
let fakeServiceUrl;
let fakeSettings;
const createUserMenu = () => {
return shallow(
<UserMenu
auth={fakeAuth}
bridge={fakeBridge}
onLogout={fakeOnLogout}
serviceUrl={fakeServiceUrl}
settings={fakeSettings}
/>
).dive(); // Dive needed because this component uses `withServices`
};
const findMenuItem = (wrapper, labelText) => {
return wrapper
.find(MenuItem)
.filterWhere(n => n.prop('label') === labelText);
};
beforeEach(() => {
fakeAuth = {
displayName: 'Eleanor Fishtail',
status: 'logged-in',
userid: 'acct:eleanorFishtail@hypothes.is',
username: 'eleanorFishy',
};
fakeBridge = { call: sinon.stub() };
fakeIsThirdPartyUser = sinon.stub();
fakeOnLogout = sinon.stub();
fakeProfileBridgeEvent = 'profile-requested';
fakeServiceConfig = sinon.stub();
fakeServiceUrl = sinon.stub();
fakeSettings = {
authDomain: 'hypothes.is',
};
UserMenu.$imports.$mock({
'../util/account-id': {
isThirdPartyUser: fakeIsThirdPartyUser,
},
'../service-config': fakeServiceConfig,
'../../shared/bridge-events': {
PROFILE_REQUESTED: fakeProfileBridgeEvent,
},
});
});
afterEach(() => {
UserMenu.$imports.$restore();
});
describe('profile menu item', () => {
context('first-party user', () => {
beforeEach(() => {
fakeIsThirdPartyUser.returns(false);
fakeServiceUrl.returns('profile-link');
});
it('should be enabled', () => {
const wrapper = createUserMenu();
const profileMenuItem = findMenuItem(wrapper, fakeAuth.displayName);
assert.notOk(profileMenuItem.prop('isDisabled'));
});
it('should have a link (href)', () => {
const wrapper = createUserMenu();
const profileMenuItem = findMenuItem(wrapper, fakeAuth.displayName);
assert.equal(profileMenuItem.prop('href'), 'profile-link');
});
it('should have a callback', () => {
const wrapper = createUserMenu();
const profileMenuItem = findMenuItem(wrapper, fakeAuth.displayName);
assert.isFunction(profileMenuItem.prop('onClick'));
});
});
context('third-party user', () => {
beforeEach(() => {
fakeIsThirdPartyUser.returns(true);
});
it('should be disabled if no service configured', () => {
fakeServiceConfig.returns(null);
const wrapper = createUserMenu();
const profileMenuItem = findMenuItem(wrapper, fakeAuth.displayName);
assert.isTrue(profileMenuItem.prop('isDisabled'));
});
it('should be disabled if service feature not supported', () => {
fakeServiceConfig.returns({ onProfileRequestProvided: false });
const wrapper = createUserMenu();
const profileMenuItem = findMenuItem(wrapper, fakeAuth.displayName);
assert.isTrue(profileMenuItem.prop('isDisabled'));
});
it('should be enabled if service feature support', () => {
fakeServiceConfig.returns({ onProfileRequestProvided: true });
const wrapper = createUserMenu();
const profileMenuItem = findMenuItem(wrapper, fakeAuth.displayName);
assert.notOk(profileMenuItem.prop('isDisabled'));
});
it('should have a callback if enabled', () => {
fakeServiceConfig.returns({ onProfileRequestProvided: true });
const wrapper = createUserMenu();
const profileMenuItem = findMenuItem(wrapper, fakeAuth.displayName);
assert.isFunction(profileMenuItem.prop('onClick'));
});
});
describe('profile-selected callback', () => {
it('should fire profile event for third-party user', () => {
fakeServiceConfig.returns({ onProfileRequestProvided: true });
fakeIsThirdPartyUser.returns(true);
const wrapper = createUserMenu();
const profileMenuItem = findMenuItem(wrapper, fakeAuth.displayName);
const onProfileSelected = profileMenuItem.prop('onClick');
onProfileSelected();
assert.equal(fakeBridge.call.callCount, 1);
assert.calledWith(fakeBridge.call, fakeProfileBridgeEvent);
});
it('should not fire profile event for first-party user', () => {
fakeIsThirdPartyUser.returns(false);
const wrapper = createUserMenu();
const profileMenuItem = findMenuItem(wrapper, fakeAuth.displayName);
const onProfileSelected = profileMenuItem.prop('onClick');
onProfileSelected();
assert.equal(fakeBridge.call.callCount, 0);
});
});
});
describe('account settings menu item', () => {
it('should be present if first-party user', () => {
fakeIsThirdPartyUser.returns(false);
const wrapper = createUserMenu();
const accountMenuItem = findMenuItem(wrapper, 'Account settings');
assert.isTrue(accountMenuItem.exists());
assert.calledWith(fakeServiceUrl, 'account.settings');
});
it('should not be present if third-party user', () => {
fakeIsThirdPartyUser.returns(true);
const wrapper = createUserMenu();
const accountMenuItem = findMenuItem(wrapper, 'Account settings');
assert.isFalse(accountMenuItem.exists());
});
});
describe('log out menu item', () => {
const tests = [
{
it: 'should be present for first-party user if no service configured',
isThirdParty: false,
serviceConfigReturns: null,
expected: true,
},
{
it:
'should be present for first-party user if service supports `onLogoutRequest`',
isThirdParty: false,
serviceConfigReturns: { onLogoutRequestProvided: true },
expected: true,
},
{
it:
'should be present for first-party user if service does not support `onLogoutRequest`',
isThirdParty: false,
serviceConfigReturns: { onLogoutRequestProvided: false },
expected: true,
},
{
it: 'should be absent for third-party user if no service configured',
isThirdParty: true,
serviceConfigReturns: null,
expected: false,
},
{
it:
'should be present for third-party user if service supports `onLogoutRequest`',
isThirdParty: true,
serviceConfigReturns: { onLogoutRequestProvided: true },
expected: true,
},
{
it:
'should be absent for third-party user if `onLogoutRequest` not supported',
isThirdParty: true,
serviceConfigReturns: { onLogoutRequestProvided: false },
expected: false,
},
];
tests.forEach(test => {
it(test.it, () => {
fakeIsThirdPartyUser.returns(test.isThirdParty);
fakeServiceConfig.returns(test.serviceConfigReturns);
const wrapper = createUserMenu();
const logOutMenuItem = findMenuItem(wrapper, 'Log out');
assert.equal(logOutMenuItem.exists(), test.expected);
if (test.expected) {
assert.equal(logOutMenuItem.prop('onClick'), fakeOnLogout);
}
});
});
});
});
......@@ -185,11 +185,6 @@ module.exports = {
thread: '<',
showDocumentInfo: '<',
/**
* Called when the user clicks a link to show an annotation that does not
* match the current filter.
*/
onForceVisible: '&',
/** Called when the user focuses an annotation by hovering it. */
onFocus: '&',
/** Called when a user selects an annotation. */
......
'use strict';
const { createElement } = require('preact');
const propTypes = require('prop-types');
const bridgeEvents = require('../../shared/bridge-events');
const { isThirdPartyUser } = require('../util/account-id');
const serviceConfig = require('../service-config');
const { withServices } = require('../util/service-context');
const Menu = require('./menu');
const MenuSection = require('./menu-section');
const MenuItem = require('./menu-item');
/**
* A menu with user and account links.
*
* This menu will contain different items depending on service configuration,
* context and whether the user is first- or third-party.
*/
function UserMenu({ auth, bridge, onLogout, serviceUrl, settings }) {
const isThirdParty = isThirdPartyUser(auth.userid, settings.authDomain);
const service = serviceConfig(settings);
const serviceSupports = feature => service && !!service[feature];
const isSelectableProfile =
!isThirdParty || serviceSupports('onProfileRequestProvided');
const isLogoutEnabled =
!isThirdParty || serviceSupports('onLogoutRequestProvided');
const onProfileSelected = () =>
isThirdParty && bridge.call(bridgeEvents.PROFILE_REQUESTED);
// Generate dynamic props for the profile <MenuItem> component
const profileItemProps = (() => {
const props = {};
if (isSelectableProfile) {
if (!isThirdParty) {
props.href = serviceUrl('user', { user: auth.username });
}
props.onClick = onProfileSelected;
}
return props;
})();
const menuLabel = <i className="h-icon-account top-bar__btn" />;
return (
<div className="user-menu">
<Menu label={menuLabel} title={auth.displayName} align="right">
<MenuSection>
<MenuItem
label={auth.displayName}
isDisabled={!isSelectableProfile}
{...profileItemProps}
/>
{!isThirdParty && (
<MenuItem
label="Account settings"
href={serviceUrl('account.settings')}
/>
)}
</MenuSection>
{isLogoutEnabled && (
<MenuSection>
<MenuItem label="Log out" onClick={onLogout} />
</MenuSection>
)}
</Menu>
</div>
);
}
UserMenu.propTypes = {
/* object representing authenticated user and auth status */
auth: propTypes.object.isRequired,
/* onClick callback for the "log out" button */
onLogout: propTypes.func.isRequired,
/* services */
bridge: propTypes.object.isRequired,
serviceUrl: propTypes.func.isRequired,
settings: propTypes.object.isRequired,
};
UserMenu.injectedProps = ['bridge', 'serviceUrl', 'settings'];
module.exports = withServices(UserMenu);
......@@ -198,6 +198,10 @@ function startAngularApp(config) {
wrapReactComponent(require('./components/timestamp'))
)
.component('topBar', require('./components/top-bar'))
.component(
'userMenu',
wrapReactComponent(require('./components/user-menu'))
)
.directive('hAutofocus', require('./directive/h-autofocus'))
.directive('hBranding', require('./directive/h-branding'))
......
......@@ -50,7 +50,7 @@
show-document-info="false"
thread="child"
on-change-collapsed="vm.onChangeCollapsed({id:id, collapsed:collapsed})"
on-force-visible="vm.onForceVisible({thread:thread})">
on-force-visible="vm.onForceVisible(thread)">
</annotation-thread>
</li>
</ul>
......
<header class="annotation-header" ng-if="!vm.user()">
<strong>You must be logged in to create annotations.</strong>
<strong>You must be logged in to create annotations and highlights.</strong>
</header>
<div ng-keydown="vm.onKeydown($event)" ng-if="vm.user()">
......
<!-- New controls -->
<span class="login-text"
ng-if="vm.newStyle && vm.auth.status === 'unknown'"></span>
ng-if="vm.auth.status === 'unknown'"></span>
<span class="login-text"
ng-if="vm.newStyle && vm.auth.status === 'logged-out'">
ng-if="vm.auth.status === 'logged-out'">
<a href="" ng-click="vm.onSignUp()" target="_blank" h-branding="accentColor">Sign up</a>
/ <a href="" ng-click="vm.onLogin()" h-branding="accentColor">Log in</a>
</span>
<div ng-if="vm.newStyle"
class="pull-right login-control-menu"
dropdown
keyboard-nav>
<a role="button"
class="top-bar__btn"
data-toggle="dropdown"
dropdown-toggle
title="{{vm.auth.username}}">
<i class="h-icon-account" ng-if="vm.auth.status === 'logged-in'"></i><!--
!--><i class="h-icon-arrow-drop-down top-bar__dropdown-arrow"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li class="dropdown-menu__row" ng-if="vm.auth.status === 'logged-in'">
<span ng-if="!vm.shouldEnableProfileButton()"
class="dropdown-menu__link dropdown-menu__link--disabled js-user-profile-btn is-disabled">
{{vm.auth.displayName}}</span>
<a ng-if="vm.shouldEnableProfileButton()"
ng-click="vm.showProfile()"
class="dropdown-menu__link js-user-profile-btn is-enabled"
title="View all your annotations"
target="_blank">{{vm.auth.displayName}}</a>
</li>
<li class="dropdown-menu__row" ng-if="vm.auth.status === 'logged-in' && !vm.isThirdPartyUser()">
<a class="dropdown-menu__link js-account-settings-btn" href="{{vm.serviceUrl('account.settings')}}" target="_blank">Account settings</a>
</li>
<li class="dropdown-menu__row">
<a class="dropdown-menu__link js-help-btn" ng-click="vm.onShowHelpPanel()">Help</a>
</li>
<li class="dropdown-menu__row" ng-if="vm.shouldShowLogOutButton()">
<a class="dropdown-menu__link dropdown-menu__link--subtle js-log-out-btn"
href="" ng-click="vm.onLogout()">Log out</a>
</li>
</ul>
</div>
<!-- Old controls -->
<span ng-if="!vm.newStyle && vm.auth.status === 'unknown'"></span>
<span ng-if="!vm.newStyle && vm.auth.status === 'logged-out'">
<a href="" ng-click="vm.onLogin()">Log in</a>
</span>
<div ng-if="!vm.newStyle"
class="pull-right login-control-menu"
dropdown
keyboard-nav>
<span role="button" data-toggle="dropdown" dropdown-toggle>
{{vm.auth.username}}<!--
--><span class="provider"
ng-if="vm.auth.provider">/{{vm.auth.provider}}</span><!--
--><i class="h-icon-arrow-drop-down"></i>
</span>
<ul class="dropdown-menu pull-right" role="menu">
<li class="dropdown-menu__row" ng-if="vm.auth.status === 'logged-in'">
<a class="dropdown-menu__link" href="{{vm.serviceUrl('account.settings')}}" target="_blank">Account</a>
</li>
<li class="dropdown-menu__row" >
<a class="dropdown-menu__link" ng-click="vm.onShowHelpPanel()">Help</a>
</li>
<li class="dropdown-menu__row" ng-if="vm.auth.status === 'logged-in'">
<a class="dropdown-menu__link" href="{{vm.serviceUrl('user',{user: vm.auth.username})}}"
target="_blank">My Annotations</a>
</li>
<li class="dropdown-menu__row" ng-if="vm.auth.status === 'logged-in'">
<a class="dropdown-menu__link" href="" ng-click="vm.onLogout()">Log out</a>
</li>
</ul>
</div>
<user-menu auth="vm.auth" on-logout="vm.onLogout()" ng-if="vm.auth.status === 'logged-in'"/>
......@@ -47,7 +47,6 @@
on-change-collapsed="vm.setCollapsed(id, collapsed)"
on-clear-selection="vm.clearSelection()"
on-focus="vm.focus(annotation)"
on-force-visible="vm.forceVisible(thread)"
on-select="vm.scrollTo(annotation)"
show-document-info="false"
ng-if="!vm.selectedGroupUnavailable()"
......
<span window-scroll="vm.loadMore(20)">
<thread-list
on-change-collapsed="vm.setCollapsed(id, collapsed)"
on-force-visible="vm.forceVisible(thread)"
show-document-info="true"
thread="vm.rootThread">
</thread-list>
......
......@@ -11,8 +11,7 @@
<annotation-thread
thread="child"
show-document-info="vm.showDocumentInfo"
on-change-collapsed="vm.onChangeCollapsed({id: id, collapsed: collapsed})"
on-force-visible="vm.onForceVisible({thread: thread})">
on-change-collapsed="vm.onChangeCollapsed({id: id, collapsed: collapsed})">
</annotation-thread>
</div>
<hr ng-if="vm.isThemeClean"
......
......@@ -11,12 +11,18 @@
always-expanded="true">
</search-input>
<div class="top-bar__expander"></div>
<button class="top-bar__btn top-bar__help-btn"
ng-click="vm.onShowHelpPanel()"
title="Help"
aria-label="Help">
<svg-icon name="'help'" class-name="'top-bar__help-icon'"></svg-icon>
</button>
<login-control
class="login-control"
auth="vm.auth"
new-style="false"
on-show-help-panel="vm.onShowHelpPanel()"
on-login="vm.onLogin()"
on-logout="vm.onLogout()">
on-logout="vm.onLogout()"
on-sign-up="vm.onSignUp()">
</login-control>
</div>
<!-- New design for the top bar, as used in the sidebar.
......@@ -49,11 +55,15 @@
aria-label="Share this page">
<i class="h-icon-annotation-share"></i>
</button>
<button class="top-bar__btn top-bar__help-btn"
ng-click="vm.onShowHelpPanel()"
title="Help"
aria-label="Help">
<svg-icon name="'help'" class-name="'top-bar__help-icon'"></svg-icon>
</button>
<login-control
class="login-control"
auth="vm.auth"
new-style="true"
on-show-help-panel="vm.onShowHelpPanel()"
on-login="vm.onLogin()"
on-logout="vm.onLogout()"
on-sign-up="vm.onSignUp()">
......
......@@ -112,9 +112,7 @@ class ReactController {
function wrapReactComponent(type) {
if (!type.propTypes) {
throw new Error(
`React component ${
type.name
} does not specify its inputs using "propTypes"`
`React component ${type.name} does not specify its inputs using "propTypes"`
);
}
......
.login-control{
.login-control {
flex-shrink: 0;
}
......@@ -6,22 +6,3 @@
font-size: $body2-font-size;
padding-left: 6px;
}
/* The user account dropdown menu */
.login-control-menu {
.dropdown-toggle {
.provider {
color: $gray-light;
display: none;
}
&:hover {
.provider {
display: inline;
}
}
}
.dropdown.open .provider {
display: inline;
}
}
/* Make the wrapper element's size match the contained `svg` element */
.svg-icon {
display: flex;
}
......@@ -14,7 +14,7 @@
// Force top-bar onto a new compositor layer so that it does not judder when
// the window is scrolled.
transform: translate3d(0,0,0);
transform: translate3d(0, 0, 0);
}
.top-bar--theme-clean {
......@@ -93,6 +93,12 @@
user-select: none;
}
/* FIXME: Temporary fix to make 'help' icon align with other top-bar icons */
.top-bar__help-icon {
width: 15px;
height: 15px;
}
.top-bar__apply-icon {
display: inline-block;
line-height: 12px;
......
......@@ -42,6 +42,7 @@ $base-line-height: 20px;
@import './components/share-link';
@import './components/sidebar-tutorial';
@import './components/simple-search';
@import './components/svg-icon';
@import './components/spinner';
@import './components/tags-input';
@import './components/thread-list';
......@@ -50,7 +51,8 @@ $base-line-height: 20px;
// Top-level styles
// ----------------
html, body {
html,
body {
height: 100%;
}
......@@ -104,7 +106,7 @@ hypothesis-app {
border-radius: 2px;
font-family: $sans-font-family;
font-weight: 300;
margin-bottom: .72em;
margin-bottom: 0.72em;
padding: 1em;
position: relative;
background-color: $body-background;
......@@ -113,7 +115,9 @@ hypothesis-app {
border: 1px none $gray-lighter;
border-bottom-style: solid;
padding: 0 0 1.1em;
li a { padding-bottom: .231em }
li a {
padding-bottom: 0.231em;
}
}
.close {
......
......@@ -763,9 +763,9 @@
universal-user-agent "^2.1.0"
"@octokit/rest@^16.9.0":
version "16.27.3"
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.27.3.tgz#20ad5d0c7043364d1e1f72fa74f825c181771fc0"
integrity sha512-WWH/SHF4kus6FG+EAfX7/JYH70EjgFYa4AAd2Lf1hgmgzodhrsoxpXPSZliZ5BdJruZPMP7ZYaPoTrYCCKYzmQ==
version "16.28.0"
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.28.0.tgz#0bc39402cc8894519d8438101cae1fbe0e542452"
integrity sha512-9S9h/5tiu5vdrhHHyjXZrq826zaQcfci0O21+KRYL82Y6m8T64dZbYUAFiAlDOsQMtlWmgKmoGIJqWLlgySDdQ==
dependencies:
"@octokit/request" "^4.0.1"
"@octokit/request-error" "^1.0.2"
......@@ -1295,15 +1295,16 @@ autofill-event@0.0.1:
integrity sha1-w4LPmJshth/0oSs1l+GUNHHTz3o=
autoprefixer@^9.4.7:
version "9.5.1"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.5.1.tgz#243b1267b67e7e947f28919d786b50d3bb0fb357"
integrity sha512-KJSzkStUl3wP0D5sdMlP82Q52JLy5+atf2MHAre48+ckWkXgixmfHyWmA77wFDy6jTHU6mIgXv6hAQ2mf1PjJQ==
version "9.6.0"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.6.0.tgz#0111c6bde2ad20c6f17995a33fad7cf6854b4c87"
integrity sha512-kuip9YilBqhirhHEGHaBTZKXL//xxGnzvsD0FtBQa6z+A69qZD6s/BAX9VzDF1i9VKDquTJDQaPLSEhOnL6FvQ==
dependencies:
browserslist "^4.5.4"
caniuse-lite "^1.0.30000957"
browserslist "^4.6.1"
caniuse-lite "^1.0.30000971"
chalk "^2.4.2"
normalize-range "^0.1.2"
num2fraction "^1.2.2"
postcss "^7.0.14"
postcss "^7.0.16"
postcss-value-parser "^3.3.1"
aws-sdk@^2.345.0:
......@@ -1708,14 +1709,14 @@ browserify@^16.1.0, browserify@^16.2.3:
vm-browserify "^1.0.0"
xtend "^4.0.0"
browserslist@^4.5.4, browserslist@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.6.0.tgz#5274028c26f4d933d5b1323307c1d1da5084c9ff"
integrity sha512-Jk0YFwXBuMOOol8n6FhgkDzn3mY9PYLYGk29zybF05SbRTsMgPqmTNeQQhOghCxq5oFqAXE3u4sYddr4C0uRhg==
browserslist@^4.6.0, browserslist@^4.6.1:
version "4.6.2"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.6.2.tgz#574c665950915c2ac73a4594b8537a9eba26203f"
integrity sha512-2neU/V0giQy9h3XMPwLhEY3+Ao0uHSwHvU8Q1Ea6AgLVL1sXbX3dzPrJ8NWe5Hi4PoTkCYXOtVR9rfRLI0J/8Q==
dependencies:
caniuse-lite "^1.0.30000967"
electron-to-chromium "^1.3.133"
node-releases "^1.1.19"
caniuse-lite "^1.0.30000974"
electron-to-chromium "^1.3.150"
node-releases "^1.1.23"
btoa-lite@^1.0.0:
version "1.0.0"
......@@ -1835,15 +1836,10 @@ camelcase@^5.0.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
integrity sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==
caniuse-lite@^1.0.30000957:
version "1.0.30000957"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000957.tgz#fb1026bf184d7d62c685205358c3b24b9e29f7b3"
integrity sha512-8wxNrjAzyiHcLXN/iunskqQnJquQQ6VX8JHfW5kLgAPRSiSuKZiNfmIkP5j7jgyXqAQBSoXyJxfnbCFS0ThSiQ==
caniuse-lite@^1.0.30000967:
version "1.0.30000971"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000971.tgz#d1000e4546486a6977756547352bc96a4cfd2b13"
integrity sha512-TQFYFhRS0O5rdsmSbF1Wn+16latXYsQJat66f7S7lizXW1PVpWJeZw9wqqVLIjuxDRz7s7xRUj13QCfd8hKn6g==
caniuse-lite@^1.0.30000971, caniuse-lite@^1.0.30000974:
version "1.0.30000974"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000974.tgz#b7afe14ee004e97ce6dc73e3f878290a12928ad8"
integrity sha512-xc3rkNS/Zc3CmpMKuczWEdY2sZgx09BkAxfvkxlAEBTqcMHeL8QnPqhKse+5sRTi3nrw2pJwToD2WvKn1Uhvww==
caseless@~0.12.0:
version "0.12.0"
......@@ -2859,10 +2855,10 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
electron-to-chromium@^1.3.133:
version "1.3.137"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.137.tgz#ba7c88024984c038a5c5c434529aabcea7b42944"
integrity sha512-kGi32g42a8vS/WnYE7ELJyejRT7hbr3UeOOu0WeuYuQ29gCpg9Lrf6RdcTQVXSt/v0bjCfnlb/EWOOsiKpTmkw==
electron-to-chromium@^1.3.150:
version "1.3.155"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.155.tgz#ebf0cc8eeaffd6151d1efad60fd9e021fb45fd3a"
integrity sha512-/ci/XgZG8jkLYOgOe3mpJY1onxPPTDY17y7scldhnSjjZqV6VvREG/LvwhRuV7BJbnENFfuDWZkSqlTh4x9ZjQ==
elliptic@^6.0.0:
version "6.4.0"
......@@ -2945,17 +2941,17 @@ entities@^1.1.1, entities@~1.1.1:
integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
enzyme-adapter-preact-pure@^1.9.0:
version "1.13.2"
resolved "https://registry.yarnpkg.com/enzyme-adapter-preact-pure/-/enzyme-adapter-preact-pure-1.13.2.tgz#2a1fa77ec50401c647455e20e7c21c7618fcee0f"
integrity sha512-ebJynMPYKHxnxA2owEgjQOZljoiYIj13M4TcE77pND2Rs9p87zxLAQOj0j6kPE6+Er+xUnsHIxzPTWyXrDhfMg==
version "1.13.4"
resolved "https://registry.yarnpkg.com/enzyme-adapter-preact-pure/-/enzyme-adapter-preact-pure-1.13.4.tgz#6243e1eb38145b33b90333cdd1db792cb425eb7c"
integrity sha512-Q3Py/Y13DcA6+UrnqgJhamTUPhj92CLuFxfBa60+9MEI5NbtOzl+m3qnu590i95fnJZ6yPQChh1qi1wfe+5Mgg==
dependencies:
array.prototype.flatmap "^1.2.1"
preact-render-to-string "^4.1.0"
enzyme@^3.8.0:
version "3.9.0"
resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.9.0.tgz#2b491f06ca966eb56b6510068c7894a7e0be3909"
integrity sha512-JqxI2BRFHbmiP7/UFqvsjxTirWoM1HfeaJrmVSZ9a1EADKkZgdPcAuISPMpoUiHlac9J4dYt81MC5BBIrbJGMg==
version "3.10.0"
resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.10.0.tgz#7218e347c4a7746e133f8e964aada4a3523452f6"
integrity sha512-p2yy9Y7t/PFbPoTvrWde7JIYB2ZyGC+NgTNbVEGvZ5/EyoYSr9aG/2rSbVvyNvMHEhw9/dmGUJHWtfQIEiX9pg==
dependencies:
array.prototype.flat "^1.2.1"
cheerio "^1.0.0-rc.2"
......@@ -6136,10 +6132,10 @@ node-pre-gyp@^0.12.0:
semver "^5.3.0"
tar "^4"
node-releases@^1.1.19:
version "1.1.21"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.21.tgz#46c86f9adaceae4d63c75d3c2f2e6eee618e55f3"
integrity sha512-TwnURTCjc8a+ElJUjmDqU6+12jhli1Q61xOQmdZ7ECZVBZuQpN/1UnembiIHDM1wCcfLvh5wrWXUF5H6ufX64Q==
node-releases@^1.1.23:
version "1.1.23"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.23.tgz#de7409f72de044a2fa59c097f436ba89c39997f0"
integrity sha512-uq1iL79YjfYC0WXoHbC/z28q/9pOl8kSHaXdWmAAc8No+bDwqkZbzIJz55g/MUsPgSGm9LZ7QSUbzTcH5tz47w==
dependencies:
semver "^5.3.0"
......@@ -6896,10 +6892,10 @@ postcss-value-parser@^3.3.1:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
postcss@^7.0.13, postcss@^7.0.14, postcss@^7.0.2:
version "7.0.16"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.16.tgz#48f64f1b4b558cb8b52c88987724359acb010da2"
integrity sha512-MOo8zNSlIqh22Uaa3drkdIAgUGEL+AD1ESiSdmElLUmE2uVDo1QloiT/IfW9qRw8Gw+Y/w69UVMGwbufMSftxA==
postcss@^7.0.13, postcss@^7.0.16, postcss@^7.0.2:
version "7.0.17"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.17.tgz#4da1bdff5322d4a0acaab4d87f3e782436bad31f"
integrity sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==
dependencies:
chalk "^2.4.2"
source-map "^0.6.1"
......@@ -6922,10 +6918,10 @@ prelude-ls@~1.1.2:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
prettier@1.17.1:
version "1.17.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.17.1.tgz#ed64b4e93e370cb8a25b9ef7fef3e4fd1c0995db"
integrity sha512-TzGRNvuUSmPgwivDqkZ9tM/qTGW9hqDKWOE9YHiyQdixlKbv7kvEqsmDPrcHJTKwthU774TQwZXVtaQ/mMsvjg==
prettier@1.18.2:
version "1.18.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea"
integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==
pretty-format@^3.8.0:
version "3.8.0"
......@@ -7123,9 +7119,9 @@ range-parser@^1.2.0, range-parser@~1.2.1:
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raven-js@^3.7.0:
version "3.27.1"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.27.1.tgz#e187a12982061908ccbf935af0640c9043d7d666"
integrity sha512-r/9CwSbaGfBFjo4hGR45DAmrukUKkQ4HdMu80PlVLDY1t8f9b4aaZzTsFegaafu7EGhEYougWDJ9/IcTdYdLXQ==
version "3.27.2"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.27.2.tgz#6c33df952026cd73820aa999122b7b7737a66775"
integrity sha512-mFWQcXnhRFEQe5HeFroPaEghlnqy7F5E2J3Fsab189ondqUzcjwSVi7el7F36cr6PvQYXoZ1P2F5CSF2/azeMQ==
raw-body@2.4.0:
version "2.4.0"
......
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