Unverified Commit 2ed42cfb authored by Lyza Gardner's avatar Lyza Gardner Committed by GitHub

Merge pull request #1219 from hypothesis/annotation-header-preact

Convert `AnnotationHeader` and its allies to preact
parents d8efffcf c673c916
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="48px" height="56px" viewBox="0 0 48 56" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="48px" height="56px" viewBox="0 0 48 56" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.6.1 (26313) - http://www.bohemiancoding.com/sketch --> <!-- Generator: Sketch 3.6.1 (26313) - http://www.bohemiancoding.com/sketch -->
<title>Group 4 Copy 3</title> <title>Only me</title>
<desc>Created with Sketch.</desc> <desc>Created with Sketch.</desc>
<defs></defs> <defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
......
'use strict';
const propTypes = require('prop-types');
const { createElement } = require('preact');
const annotationMetadata = require('../annotation-metadata');
/**
* Render some metadata about an annotation's document and link to it
* if a link is available.
*/
function AnnotationDocumentInfo({ annotation }) {
const documentInfo = annotationMetadata.domainAndTitle(annotation);
// If there's no document title, nothing to do here
if (!documentInfo.titleText) {
return null;
}
return (
<div className="annotation-document-info">
<div className="annotation-document-info__title">
on &quot;
{documentInfo.titleLink ? (
<a href={documentInfo.titleLink}>{documentInfo.titleText}</a>
) : (
<span>{documentInfo.titleText}</span>
)}
&quot;
</div>
{documentInfo.domain && (
<div className="annotation-document-info__domain">
({documentInfo.domain})
</div>
)}
</div>
);
}
AnnotationDocumentInfo.propTypes = {
/* Annotation for which the document metadata will be rendered */
annotation: propTypes.object.isRequired,
};
module.exports = AnnotationDocumentInfo;
'use strict'; 'use strict';
const annotationMetadata = require('../annotation-metadata'); const propTypes = require('prop-types');
const memoize = require('../util/memoize'); const { createElement } = require('preact');
const { isThirdPartyUser, username } = require('../util/account-id');
// @ngInject const AnnotationDocumentInfo = require('./annotation-document-info');
function AnnotationHeaderController(features, groups, settings, serviceUrl) { const AnnotationShareInfo = require('./annotation-share-info');
const self = this; const AnnotationUser = require('./annotation-user');
const Timestamp = require('./timestamp');
this.user = function() {
return self.annotation.user;
};
this.displayName = () => {
const userInfo = this.annotation.user_info;
const isThirdPartyUser_ = isThirdPartyUser(
this.annotation.user,
settings.authDomain
);
if (features.flagEnabled('client_display_names') || isThirdPartyUser_) {
// userInfo is undefined if the api_render_user_info feature flag is off.
if (userInfo) {
// display_name is null if the user doesn't have a display name.
if (userInfo.display_name) {
return userInfo.display_name;
}
}
}
return username(this.annotation.user);
};
this.isThirdPartyUser = function() {
return isThirdPartyUser(self.annotation.user, settings.authDomain);
};
this.thirdPartyUsernameLink = function() {
return settings.usernameUrl
? settings.usernameUrl + username(this.annotation.user)
: null;
};
this.serviceUrl = serviceUrl;
this.group = function() {
return groups.get(self.annotation.group);
};
const documentMeta = memoize(annotationMetadata.domainAndTitle);
this.documentMeta = function() {
return documentMeta(self.annotation);
};
this.updated = function() {
return self.annotation.updated;
};
this.htmlLink = function() {
if (self.annotation.links && self.annotation.links.html) {
return self.annotation.links.html;
}
return '';
};
}
/** /**
* Header component for an annotation card. * Render an annotation's header summary, including metadata about its user,
* * sharing status, document and timestamp. It also allows the user to
* Header which displays the username, last update timestamp and other key * toggle sub-threads/replies in certain cases.
* metadata about an annotation.
*/ */
module.exports = { function AnnotationHeader({
controller: AnnotationHeaderController, annotation,
controllerAs: 'vm', isEditing,
bindings: { isHighlight,
/** isPrivate,
* The saved annotation onReplyCountClick,
*/ replyCount,
annotation: '<', showDocumentInfo,
}) {
/** const annotationLink = annotation.links ? annotation.links.html : '';
* True if the annotation is private or will become private when the user const replyPluralized = !replyCount || replyCount > 1 ? 'replies' : 'reply';
* saves their changes.
*/ return (
isPrivate: '<', <header className="annotation-header">
<div className="annotation-header__row">
/** True if the user is currently editing the annotation. */ <AnnotationUser annotation={annotation} />
isEditing: '<', <div className="annotation-collapsed-replies">
<a className="annotation-link" onClick={onReplyCountClick}>
/** {replyCount} {replyPluralized}
* True if the annotation is a highlight. </a>
* FIXME: This should determined in AnnotationHeaderController </div>
*/ {!isEditing && annotation.updated && (
isHighlight: '<', <div className="annotation-header__timestamp">
onReplyCountClick: '&', <Timestamp
replyCount: '<', className="annotation-header__timestamp-link"
href={annotationLink}
timestamp={annotation.updated}
/>
</div>
)}
</div>
<div className="annotation-header__row">
<AnnotationShareInfo annotation={annotation} isPrivate={isPrivate} />
{!isEditing && isHighlight && (
<div className="annotation-header__highlight">
<i
className="h-icon-border-color"
title="This is a highlight. Click 'edit' to add a note or tag."
/>
</div>
)}
{showDocumentInfo && <AnnotationDocumentInfo annotation={annotation} />}
</div>
</header>
);
}
/** True if document metadata should be shown. */ AnnotationHeader.propTypes = {
showDocumentInfo: '<', /* The annotation */
}, annotation: propTypes.object.isRequired,
template: require('../templates/annotation-header.html'), /* Whether the annotation is actively being edited */
isEditing: propTypes.bool,
/* Whether the annotation is a highlight */
isHighlight: propTypes.bool,
/* Whether the annotation is an "only me" (private) annotation */
isPrivate: propTypes.bool,
/* Callback for when the toggle-replies element is clicked */
onReplyCountClick: propTypes.func.isRequired,
/* How many replies this annotation currently has */
replyCount: propTypes.number,
/**
* Should document metadata be rendered? Hint: this is enabled for single-
* annotation and stream views
*/
showDocumentInfo: propTypes.bool,
}; };
module.exports = AnnotationHeader;
'use strict';
const propTypes = require('prop-types');
const { createElement } = require('preact');
const SvgIcon = require('./svg-icon');
const useStore = require('../store/use-store');
/**
* Render information about what group an annotation is in and
* whether it is private to the current user (only me)
*/
function AnnotationShareInfo({ annotation, isPrivate }) {
const group = useStore(store => store.getGroup(annotation.group));
// We may not have access to the group object beyond its ID
const hasGroup = !!group;
// Only show the name of the group and link to it if there is a
// URL (link) returned by the API for this group. Some groups do not have links
const linkToGroup = hasGroup && group.links && group.links.html;
return (
<div className="annotation-share-info">
{linkToGroup && (
<a
className="annotation-share-info__group"
href={group.links.html}
target="_blank"
rel="noopener noreferrer"
>
{group.type === 'open' ? (
<SvgIcon className="annotation-share-info__icon" name="public" />
) : (
<SvgIcon className="annotation-share-info__icon" name="groups" />
)}
<span className="annotation-share-info__group-info">
{group.name}
</span>
</a>
)}
{isPrivate && (
<span
className="annotation-share-info__private"
title="This annotation is visible only to you."
>
{/* Show the lock icon in all cases when the annotation is private... */}
<SvgIcon className="annotation-share-info__icon" name="lock" />
{/* but only render the "Only Me" text if we're not showing/linking a group name */}
{!linkToGroup && (
<span className="annotation-share-info__private-info">Only me</span>
)}
</span>
)}
</div>
);
}
AnnotationShareInfo.propTypes = {
/** The current annotation object for which sharing info will be rendered */
annotation: propTypes.object.isRequired,
/** Is this an "only me" (private) annotation? */
isPrivate: propTypes.bool.isRequired,
};
module.exports = AnnotationShareInfo;
...@@ -30,22 +30,28 @@ function AnnotationUser({ annotation, features, serviceUrl, settings }) { ...@@ -30,22 +30,28 @@ function AnnotationUser({ annotation, features, serviceUrl, settings }) {
if (shouldLinkToActivity) { if (shouldLinkToActivity) {
return ( return (
<a <div className="annotation-user">
className="annotation-user" <a
href={ className="annotation-user__link"
isFirstPartyUser href={
? serviceUrl('user', { user }) isFirstPartyUser
: `${settings.usernameUrl}${username_}` ? serviceUrl('user', { user })
} : `${settings.usernameUrl}${username_}`
target="_blank" }
rel="noopener noreferrer" target="_blank"
> rel="noopener noreferrer"
{displayName} >
</a> <span className="annotation-user__user-name">{displayName}</span>
</a>
</div>
); );
} }
return <div className="annotation-user">{displayName}</div>; return (
<div className="annotation-user">
<span className="annotation-user__user-name">{displayName}</span>
</div>
);
} }
AnnotationUser.propTypes = { AnnotationUser.propTypes = {
......
'use strict';
const { createElement } = require('preact');
const { shallow } = require('enzyme');
const fixtures = require('../../test/annotation-fixtures');
const AnnotationDocumentInfo = require('../annotation-document-info');
describe('AnnotationDocumentInfo', () => {
let fakeDomainAndTitle;
let fakeMetadata;
const createAnnotationDocumentInfo = props => {
return shallow(
<AnnotationDocumentInfo
annotation={fixtures.defaultAnnotation()}
{...props}
/>
);
};
beforeEach(() => {
fakeDomainAndTitle = sinon.stub();
fakeMetadata = { domainAndTitle: fakeDomainAndTitle };
AnnotationDocumentInfo.$imports.$mock({
'../annotation-metadata': fakeMetadata,
});
});
afterEach(() => {
AnnotationDocumentInfo.$imports.$restore();
});
it('should not render if there is no document title', () => {
fakeDomainAndTitle.returns({});
const wrapper = createAnnotationDocumentInfo();
const info = wrapper.find('.annotation-document-info');
assert.notOk(info.exists());
});
it('should render the document title', () => {
fakeDomainAndTitle.returns({ titleText: 'I have a title' });
const wrapper = createAnnotationDocumentInfo();
const info = wrapper.find('.annotation-document-info');
assert.isOk(info.exists());
});
it('should render a link if available', () => {
fakeDomainAndTitle.returns({
titleText: 'I have a title',
titleLink: 'https://www.example.com',
});
const wrapper = createAnnotationDocumentInfo();
const link = wrapper.find('.annotation-document-info__title a');
assert.equal(link.prop('href'), 'https://www.example.com');
});
it('should render domain if available', () => {
fakeDomainAndTitle.returns({
titleText: 'I have a title',
domain: 'www.example.com',
});
const wrapper = createAnnotationDocumentInfo();
const domain = wrapper.find('.annotation-document-info__domain');
assert.equal(domain.text(), '(www.example.com)');
});
});
'use strict'; 'use strict';
const angular = require('angular'); const { createElement } = require('preact');
const { shallow } = require('enzyme');
const unroll = require('../../../shared/test/util').unroll;
const fixtures = require('../../test/annotation-fixtures'); const fixtures = require('../../test/annotation-fixtures');
const annotationHeader = require('../annotation-header');
const fakeDocumentMeta = { const AnnotationHeader = require('../annotation-header');
domain: 'docs.io', const AnnotationDocumentInfo = require('../annotation-document-info');
titleLink: 'http://docs.io/doc.html', const Timestamp = require('../timestamp');
titleText: 'Dummy title',
}; describe('AnnotationHeader', () => {
const createAnnotationHeader = props => {
describe('sidebar.components.annotation-header', function() { return shallow(
let $componentController; <AnnotationHeader
let fakeFeatures; annotation={fixtures.defaultAnnotation()}
let fakeGroups; isEditing={false}
let fakeAccountID; isHighlight={false}
const fakeSettings = { usernameUrl: 'http://www.example.org/' }; isPrivate={false}
let fakeServiceUrl; onReplyCountClick={sinon.stub()}
replyCount={0}
beforeEach('Initialize fakeAccountID', () => { showDocumentInfo={false}
fakeAccountID = { {...props}
isThirdPartyUser: sinon.stub().returns(false), />
username: sinon.stub().returns('TEST_USERNAME'), );
}; };
});
describe('collapsed replies', () => {
beforeEach('Import and register the annotationHeader component', function() { it('should have a callback', () => {
annotationHeader.$imports.$mock({ const fakeCallback = sinon.stub();
'../annotation-metadata': { const wrapper = createAnnotationHeader({
// eslint-disable-next-line no-unused-vars onReplyCountClick: fakeCallback,
domainAndTitle: function(ann) { });
return fakeDocumentMeta; const replyCollapseLink = wrapper.find('.annotation-link');
}, assert.equal(replyCollapseLink.prop('onClick'), fakeCallback);
},
'../util/account-id': fakeAccountID,
}); });
angular.module('app', []).component('annotationHeader', annotationHeader);
});
afterEach(() => { unroll(
annotationHeader.$imports.$restore(); 'it should render the annotation reply count',
testCase => {
const wrapper = createAnnotationHeader({
replyCount: testCase.replyCount,
});
const replyCollapseLink = wrapper.find('.annotation-link');
assert.equal(replyCollapseLink.text(), testCase.expected);
},
[
{
replyCount: 0,
expected: '0 replies',
},
{
replyCount: 1,
expected: '1 reply',
},
{
replyCount: 2,
expected: '2 replies',
},
]
);
}); });
beforeEach('Initialize and register fake AngularJS dependencies', function() { describe('timestamp', () => {
fakeFeatures = { it('should render a timestamp if annotation has an `updated` value', () => {
flagEnabled: sinon.stub().returns(false), const wrapper = createAnnotationHeader();
}; const timestamp = wrapper.find(Timestamp);
angular.mock.module('app', { assert.isTrue(timestamp.exists());
features: fakeFeatures,
groups: fakeGroups,
settings: fakeSettings,
serviceUrl: fakeServiceUrl,
}); });
angular.mock.inject(function(_$componentController_) { it('should not render a timestamp if annotation does not have an `updated` value', () => {
$componentController = _$componentController_; const wrapper = createAnnotationHeader({
annotation: fixtures.newAnnotation(),
});
const timestamp = wrapper.find(Timestamp);
assert.isFalse(timestamp.exists());
}); });
}); });
describe('sidebar.components.AnnotationHeaderController', function() { describe('annotation is-highlight icon', () => {
describe('#htmlLink()', function() { it('should display is-highlight icon if annotation is a highlight', () => {
it('returns the HTML link when available', function() { const wrapper = createAnnotationHeader({
const ann = fixtures.defaultAnnotation(); isEditing: false,
ann.links = { html: 'https://annotation.service/123' }; isHighlight: true,
const ctrl = $componentController(
'annotationHeader',
{},
{
annotation: ann,
}
);
assert.equal(ctrl.htmlLink(), ann.links.html);
}); });
const highlightIcon = wrapper.find('.annotation-header__highlight');
it('returns an empty string when no HTML link is available', function() { assert.isTrue(highlightIcon.exists());
const ann = fixtures.defaultAnnotation();
ann.links = {};
const ctrl = $componentController(
'annotationHeader',
{},
{
annotation: ann,
}
);
assert.equal(ctrl.htmlLink(), '');
});
}); });
describe('#documentMeta()', function() { it('should not display the is-highlight icon if annotation is not a highlight', () => {
it('returns the domain, title link and text for the annotation', function() { const wrapper = createAnnotationHeader({
const ann = fixtures.defaultAnnotation(); isEditing: false,
const ctrl = $componentController( isHighlight: false,
'annotationHeader',
{},
{
annotation: ann,
}
);
assert.deepEqual(ctrl.documentMeta(), fakeDocumentMeta);
}); });
const highlightIcon = wrapper.find('.annotation-header__highlight');
assert.isFalse(highlightIcon.exists());
}); });
});
describe('#displayName', () => { describe('annotation document info', () => {
[ it('should render document info if `showDocumentInfo` is enabled', () => {
{ const wrapper = createAnnotationHeader({ showDocumentInfo: true });
context:
'when the api_render_user_info feature flag is turned off in h',
it: 'returns the username',
user_info: undefined,
client_display_names: false,
isThirdPartyUser: false,
expectedResult: 'TEST_USERNAME',
},
{
context:
'when the api_render_user_info feature flag is turned off in h',
it:
'returns the username even if the client_display_names feature flag is on',
user_info: undefined,
client_display_names: true,
isThirdPartyUser: false,
expectedResult: 'TEST_USERNAME',
},
{
context: 'when the client_display_names feature flag is off in h',
it: 'returns the username',
user_info: { display_name: null },
client_display_names: false,
isThirdPartyUser: false,
expectedResult: 'TEST_USERNAME',
},
{
context: 'when the client_display_names feature flag is off in h',
it: 'returns the username even if the user has a display name',
user_info: { display_name: 'Bill Jones' },
client_display_names: false,
isThirdPartyUser: false,
expectedResult: 'TEST_USERNAME',
},
{
context:
'when both feature flags api_render_user_info and ' +
'client_display_names are on',
it: 'returns the username, if the user has no display_name',
user_info: { display_name: null },
client_display_names: true,
isThirdPartyUser: false,
expectedResult: 'TEST_USERNAME',
},
{
context:
'when both feature flags api_render_user_info and ' +
'client_display_names are on',
it: 'returns the display_name, if the user has one',
user_info: { display_name: 'Bill Jones' },
client_display_names: true,
isThirdPartyUser: false,
expectedResult: 'Bill Jones',
},
{
context:
'when the client_display_names feature flag is off but ' +
'the user is a third-party user',
it: 'returns display_name even though client_display_names is off',
user_info: { display_name: 'Bill Jones' },
client_display_names: false,
isThirdPartyUser: true,
expectedResult: 'Bill Jones',
},
{
context:
'when client_display_names is on and the user is a ' +
'third-party user',
it: 'returns the display_name',
user_info: { display_name: 'Bill Jones' },
client_display_names: true,
isThirdPartyUser: true,
expectedResult: 'Bill Jones',
},
{
context:
'when the user is a third-party user but the ' +
'api_render_user_info feature flag is turned off in h',
it: 'returns the username',
user_info: undefined,
client_display_names: true,
isThirdPartyUser: true,
expectedResult: 'TEST_USERNAME',
},
{
context:
"when the user is a third-party user but doesn't have a " +
'display_name',
it: 'returns the username',
user_info: { display_name: null },
client_display_names: true,
isThirdPartyUser: true,
expectedResult: 'TEST_USERNAME',
},
].forEach(test => {
context(test.context, () => {
it(test.it, () => {
// Make features.flagEnabled('client_display_names') return true
// or false, depending on the test case.
fakeFeatures.flagEnabled = flag => {
if (flag === 'client_display_names') {
return test.client_display_names;
}
return false;
};
// Make isThirdPartyUser() return true or false, const documentInfo = wrapper.find(AnnotationDocumentInfo);
// depending on the test case.
fakeAccountID.isThirdPartyUser.returns(test.isThirdPartyUser);
const ann = fixtures.defaultAnnotation(); assert.isTrue(documentInfo.exists());
ann.user_info = test.user_info; });
const ctrl = $componentController( it('should not render document info if `showDocumentInfo` is not enabled', () => {
'annotationHeader', const wrapper = createAnnotationHeader({ showDocumentInfo: false });
{},
{
annotation: ann,
}
);
assert.equal(ctrl.displayName(), test.expectedResult); const documentInfo = wrapper.find(AnnotationDocumentInfo);
});
});
});
});
describe('#thirdPartyUsernameLink', () => { assert.isFalse(documentInfo.exists());
it('returns the custom username link if set', () => { });
let ann; });
let ctrl;
fakeSettings.usernameUrl = 'http://www.example.org/'; context('user is editing annotation', () => {
ann = fixtures.defaultAnnotation(); it('should not display timestamp', () => {
ctrl = $componentController( const wrapper = createAnnotationHeader({
'annotationHeader', annotation: fixtures.defaultAnnotation(),
{}, isEditing: true,
{
annotation: ann,
}
);
assert.deepEqual(
ctrl.thirdPartyUsernameLink(),
'http://www.example.org/TEST_USERNAME'
);
}); });
it('returns null if no custom username link is set in the settings object', () => { const timestamp = wrapper.find(Timestamp);
let ann;
let ctrl;
fakeSettings.usernameUrl = null; assert.isFalse(timestamp.exists());
ann = fixtures.defaultAnnotation(); });
ctrl = $componentController(
'annotationHeader', it('should not display is-highlight icon', () => {
{}, const wrapper = createAnnotationHeader({
{ annotation: fixtures.defaultAnnotation(),
annotation: ann, isEditing: true,
} isHighlight: true,
);
assert.deepEqual(ctrl.thirdPartyUsernameLink(), null);
}); });
const highlight = wrapper.find('.annotation-header__highlight');
assert.isFalse(highlight.exists());
}); });
}); });
}); });
'use strict';
const { createElement } = require('preact');
const { shallow } = require('enzyme');
const fixtures = require('../../test/annotation-fixtures');
const AnnotationShareInfo = require('../annotation-share-info');
describe('AnnotationShareInfo', () => {
let fakeGroup;
let fakeStore;
let fakeGetGroup;
const createAnnotationShareInfo = props => {
return shallow(
<AnnotationShareInfo
annotation={fixtures.defaultAnnotation()}
isPrivate={false}
{...props}
/>
);
};
beforeEach(() => {
fakeGroup = {
name: 'My Group',
links: {
html: 'https://www.example.com',
},
type: 'private',
};
fakeGetGroup = sinon.stub().returns(fakeGroup);
fakeStore = { getGroup: fakeGetGroup };
AnnotationShareInfo.$imports.$mock({
'../store/use-store': callback => callback(fakeStore),
});
});
afterEach(() => {
AnnotationShareInfo.$imports.$restore();
});
describe('group link', () => {
it('should show a link to the group for extant, first-party groups', () => {
const wrapper = createAnnotationShareInfo();
const groupLink = wrapper.find('.annotation-share-info__group');
const groupName = wrapper.find('.annotation-share-info__group-info');
assert.equal(groupLink.prop('href'), fakeGroup.links.html);
assert.equal(groupName.text(), fakeGroup.name);
});
it('should display a group icon for private and restricted groups', () => {
const wrapper = createAnnotationShareInfo();
const groupIcon = wrapper.find(
'.annotation-share-info__group .annotation-share-info__icon'
);
assert.equal(groupIcon.prop('name'), 'groups');
});
it('should display a public/world icon for open groups', () => {
fakeGroup.type = 'open';
const wrapper = createAnnotationShareInfo();
const groupIcon = wrapper.find(
'.annotation-share-info__group .annotation-share-info__icon'
);
assert.equal(groupIcon.prop('name'), 'public');
});
it('should not show a link to third-party groups', () => {
// Third-party groups have no `html` link
fakeGetGroup.returns({ name: 'A Group', links: {} });
const wrapper = createAnnotationShareInfo();
const groupLink = wrapper.find('.annotation-share-info__group');
assert.notOk(groupLink.exists());
});
it('should not show a link if no group available', () => {
fakeGetGroup.returns(undefined);
const wrapper = createAnnotationShareInfo();
const groupLink = wrapper.find('.annotation-share-info__group');
assert.notOk(groupLink.exists());
});
});
describe('"only you" information', () => {
it('should not show privacy information if annotation is not private', () => {
const wrapper = createAnnotationShareInfo({ isPrivate: false });
const privacy = wrapper.find('.annotation-share-info__private');
assert.notOk(privacy.exists());
});
context('private annotation', () => {
it('should show privacy icon', () => {
const wrapper = createAnnotationShareInfo({ isPrivate: true });
const privacyIcon = wrapper.find(
'.annotation-share-info__private .annotation-share-info__icon'
);
assert.isOk(privacyIcon.exists());
assert.equal(privacyIcon.prop('name'), 'lock');
});
it('should not show "only me" text for first-party group', () => {
const wrapper = createAnnotationShareInfo({ isPrivate: true });
const privacyText = wrapper.find(
'.annotation-share-info__private-info'
);
assert.notOk(privacyText.exists());
});
it('should show "only me" text for annotation in third-party group', () => {
fakeGetGroup.returns({ name: 'Some Name' });
const wrapper = createAnnotationShareInfo({ isPrivate: true });
const privacyText = wrapper.find(
'.annotation-share-info__private-info'
);
assert.isOk(privacyText.exists());
assert.equal(privacyText.text(), 'Only me');
});
});
});
});
...@@ -141,7 +141,10 @@ function startAngularApp(config) { ...@@ -141,7 +141,10 @@ function startAngularApp(config) {
// UI components // UI components
.component('annotation', require('./components/annotation')) .component('annotation', require('./components/annotation'))
.component('annotationHeader', require('./components/annotation-header')) .component(
'annotationHeader',
wrapReactComponent(require('./components/annotation-header'))
)
.component( .component(
'annotationActionButton', 'annotationActionButton',
wrapReactComponent(require('./components/annotation-action-button')) wrapReactComponent(require('./components/annotation-action-button'))
......
<header class="annotation-header">
<!-- User -->
<span ng-if="vm.user()">
<annotation-user annotation="vm.annotation"></annotation-user>
<span class="annotation-collapsed-replies">
<a class="annotation-link" href=""
ng-click="vm.onReplyCountClick()"
ng-pluralize count="vm.replyCount"
when="{'0': '', 'one': '1 reply', 'other': '{} replies'}"></a>
</span>
<br />
<span class="annotation-header__share-info">
<a class="annotation-header__group"
target="_blank" ng-if="vm.group() && vm.group().links.html" href="{{vm.group().links.html}}">
<i class="h-icon-group"></i><span class="annotation-header__group-name">{{vm.group().name}}</span>
</a>
<span ng-show="vm.isPrivate"
title="This annotation is visible only to you.">
<i class="h-icon-lock"></i><span class="annotation-header__group-name" ng-show="!vm.group().links.html">Only me</span>
</span>
<i class="h-icon-border-color" ng-show="vm.isHighlight && !vm.isEditing" title="This is a highlight. Click 'edit' to add a note or tag."></i>
<span ng-if="::vm.showDocumentInfo">
<span class="annotation-citation" ng-if="vm.documentMeta().titleLink">
on "<a ng-href="{{vm.documentMeta().titleLink}}">{{vm.documentMeta().titleText}}</a>"
</span>
<span class="annotation-citation" ng-if="!vm.documentMeta().titleLink">
on "{{vm.documentMeta().titleText}}"
</span>
<span class="annotation-citation-domain"
ng-if="vm.documentMeta().domain">({{vm.documentMeta().domain}})</span>
</span>
</span>
</span>
<span class="u-flex-spacer"></span>
<timestamp
class-name="'annotation-header__timestamp'"
timestamp="vm.updated()"
href="vm.htmlLink()"
ng-if="!vm.editing() && vm.updated()"></timestamp>
</header>
...@@ -5,12 +5,12 @@ ...@@ -5,12 +5,12 @@
<div ng-keydown="vm.onKeydown($event)" ng-if="vm.user()"> <div ng-keydown="vm.onKeydown($event)" ng-if="vm.user()">
<annotation-header annotation="vm.annotation" <annotation-header annotation="vm.annotation"
is-editing="vm.editing()" is-editing="vm.editing()"
is-highlight="vm.isHighlight()" is-highlight="vm.isHighlight()"
is-private="vm.state().isPrivate" is-private="vm.state().isPrivate"
on-reply-count-click="vm.onReplyCountClick()" on-reply-count-click="vm.onReplyCountClick()"
reply-count="vm.replyCount" reply-count="vm.replyCount"
show-document-info="vm.showDocumentInfo"> show-document-info="vm.showDocumentInfo">
</annotation-header> </annotation-header>
<!-- Excerpts --> <!-- Excerpts -->
......
.annotation-document-info {
font-size: 13px;
color: $grey-5;
display: flex;
&__title {
margin-right: 5px;
}
&__domain {
margin-right: 5px;
font-size: 12px;
}
}
.annotation-header {
@include pie-clearfix;
// Margin between top of x-height of username and
// top of the annotation card should be ~15px
margin-top: -$layout-h-margin + 10px;
color: $grey-5;
&__row {
display: flex;
align-items: baseline;
}
&__timestamp {
margin-left: auto;
}
&__timestamp-link {
@include font-small;
color: $grey-4;
}
&__highlight {
margin-right: 5px;
}
}
.annotation-share-info {
display: flex;
align-items: baseline;
&__group,
&__private {
display: flex;
align-items: baseline;
font-size: $body1-font-size;
color: $grey-5;
}
&__group-info {
margin-right: 5px;
}
&__private-info {
margin-right: 5px;
}
&__icon {
margin-right: 5px;
width: 10px;
height: 10px;
}
}
.annotation-user, .annotation-user {
.annotation-user a { &__user-name {
@include font-normal; @include font-normal;
color: $grey-7; color: $grey-7;
font-weight: bold; font-weight: bold;
.is-dimmed & { .is-dimmed & {
color: $grey-5; color: $grey-5;
} }
.is-highlighted & { .is-highlighted & {
color: $grey-7; color: $grey-7;
}
} }
} }
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
// Highlight quote of annotation whenever its thread is hovered // Highlight quote of annotation whenever its thread is hovered
.thread-list__card:hover .annotation-quote { .thread-list__card:hover .annotation-quote {
border-left: $highlight 3px solid; border-left: $highlight 3px solid;
color: $grey-5;
} }
// When hovering a top-level annotation, show the footer in a hovered state. // When hovering a top-level annotation, show the footer in a hovered state.
...@@ -17,7 +16,7 @@ ...@@ -17,7 +16,7 @@
color: $grey-6; color: $grey-6;
} }
.annotation-header__timestamp { .annotation-header__timestamp-link {
color: $grey-5; color: $grey-5;
} }
} }
...@@ -60,35 +59,10 @@ ...@@ -60,35 +59,10 @@
} }
.annotation-quote-list, .annotation-quote-list,
.annotation-header,
.annotation-footer { .annotation-footer {
@include pie-clearfix; @include pie-clearfix;
} }
.annotation-header {
display: flex;
flex-direction: row;
align-items: baseline;
// Margin between top of x-height of username and
// top of the annotation card should be ~15px
margin-top: -$layout-h-margin + 10px;
}
.annotation-header__share-info {
color: $grey-5;
@include font-normal;
}
.annotation-header__group {
color: $color-gray;
font-size: $body1-font-size;
}
.annotation-header__group-name {
display: inline-block;
margin-left: 5px;
}
.annotation-body { .annotation-body {
@include font-normal; @include font-normal;
color: $grey-6; color: $grey-6;
...@@ -164,11 +138,6 @@ ...@@ -164,11 +138,6 @@
color: $grey-5; color: $grey-5;
} }
.annotation-header__timestamp {
@include font-small;
color: $grey-4;
}
.annotation-actions { .annotation-actions {
float: right; float: right;
margin-top: 0; margin-top: 0;
......
...@@ -19,7 +19,10 @@ $base-line-height: 20px; ...@@ -19,7 +19,10 @@ $base-line-height: 20px;
// Components // Components
// ---------- // ----------
@import './components/annotation'; @import './components/annotation';
@import './components/annotation-document-info';
@import './components/annotation-header';
@import './components/annotation-share-dialog'; @import './components/annotation-share-dialog';
@import './components/annotation-share-info';
@import './components/annotation-publish-control'; @import './components/annotation-publish-control';
@import './components/annotation-thread'; @import './components/annotation-thread';
@import './components/annotation-user'; @import './components/annotation-user';
......
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