Commit b164ff16 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Convert AnnotationHeader and its subcomponents to Tailwind

Convert AnnotationHeader and sub-components to use Tailwind utility
classes. Lightly refactor props for AnnotationShareInfo for simplicity.
parent 41f123a2
...@@ -15,7 +15,7 @@ import { Link } from '@hypothesis/frontend-shared'; ...@@ -15,7 +15,7 @@ import { Link } from '@hypothesis/frontend-shared';
*/ */
export default function AnnotationDocumentInfo({ domain, link, title }) { export default function AnnotationDocumentInfo({ domain, link, title }) {
return ( return (
<div className="hyp-u-layout-row hyp-u-horizontal-spacing--2"> <div className="flex gap-x-1">
<div className="text-color-text-light"> <div className="text-color-text-light">
on &quot; on &quot;
{link ? ( {link ? (
......
import { Icon, LinkButton } from '@hypothesis/frontend-shared'; import { Icon, LinkButton } from '@hypothesis/frontend-shared';
import { useMemo } from 'preact/hooks'; import { useMemo } from 'preact/hooks';
import { withServices } from '../../service-context';
import { withServices } from '../../service-context';
import { useStoreProxy } from '../../store/use-store'; import { useStoreProxy } from '../../store/use-store';
import { isThirdPartyUser, username } from '../../helpers/account-id'; import { isThirdPartyUser, username } from '../../helpers/account-id';
import { import {
...@@ -23,6 +23,15 @@ import AnnotationUser from './AnnotationUser'; ...@@ -23,6 +23,15 @@ import AnnotationUser from './AnnotationUser';
* @typedef {import('../../../types/config').SidebarSettings} SidebarSettings * @typedef {import('../../../types/config').SidebarSettings} SidebarSettings
*/ */
/** @param {{ children: import("preact").ComponentChildren}} props */
function HeaderRow({ children }) {
return (
<div className="flex gap-x-1 items-baseline flex-wrap-reverse">
{children}
</div>
);
}
/** /**
* @typedef AnnotationHeaderProps * @typedef AnnotationHeaderProps
* @prop {Annotation} annotation * @prop {Annotation} annotation
...@@ -72,23 +81,15 @@ function AnnotationHeader({ ...@@ -72,23 +81,15 @@ function AnnotationHeader({
const isCollapsedReply = isReply(annotation) && threadIsCollapsed; const isCollapsedReply = isReply(annotation) && threadIsCollapsed;
const annotationIsPrivate = isPrivate(annotation.permissions);
// Link (URL) to single-annotation view for this annotation, if it has // Link (URL) to single-annotation view for this annotation, if it has
// been provided by the service. Note: this property is not currently // been provided by the service. Note: this property is not currently
// present on third-party annotations. // present on third-party annotations.
const annotationUrl = annotation.links?.html || ''; const annotationUrl = annotation.links?.html || '';
const showTimestamps = !isEditing && annotation.created;
const showEditedTimestamp = useMemo(() => { const showEditedTimestamp = useMemo(() => {
return hasBeenEdited(annotation) && !isCollapsedReply; return hasBeenEdited(annotation) && !isCollapsedReply;
}, [annotation, isCollapsedReply]); }, [annotation, isCollapsedReply]);
const replyPluralized = replyCount > 1 ? 'replies' : 'reply';
const replyButtonText = `${replyCount} ${replyPluralized}`;
const showReplyButton = replyCount > 0 && isCollapsedReply;
const showExtendedInfo = !isReply(annotation);
// Pull together some document metadata related to this annotation // Pull together some document metadata related to this annotation
const documentInfo = domainAndTitle(annotation); const documentInfo = domainAndTitle(annotation);
// There are some cases at present in which linking directly to an // There are some cases at present in which linking directly to an
...@@ -113,10 +114,12 @@ function AnnotationHeader({ ...@@ -113,10 +114,12 @@ function AnnotationHeader({
// an ID. // an ID.
store.setExpanded(/** @type {string} */ (annotation.id), true); store.setExpanded(/** @type {string} */ (annotation.id), true);
const group = store.getGroup(annotation.group);
return ( return (
<header> <header>
<div className="hyp-u-layout-row--align-baseline hyp-u-horizontal-spacing--2 flex-wrap-reverse"> <HeaderRow>
{annotationIsPrivate && !isEditing && ( {isPrivate(annotation.permissions) && !isEditing && (
<Icon <Icon
classes="text-tiny" classes="text-tiny"
name="lock" name="lock"
...@@ -127,14 +130,14 @@ function AnnotationHeader({ ...@@ -127,14 +130,14 @@ function AnnotationHeader({
authorLink={authorLink} authorLink={authorLink}
displayName={authorDisplayName} displayName={authorDisplayName}
/> />
{showReplyButton && ( {replyCount > 0 && isCollapsedReply && (
<LinkButton onClick={onReplyCountClick} title="Expand replies"> <LinkButton onClick={onReplyCountClick} title="Expand replies">
{replyButtonText} {`${replyCount} ${replyCount > 1 ? 'replies' : 'reply'}`}
</LinkButton> </LinkButton>
)} )}
{showTimestamps && ( {!isEditing && annotation.created && (
<div className="hyp-u-layout-row--justify-right hyp-u-stretch"> <div className="flex justify-end grow">
<AnnotationTimestamps <AnnotationTimestamps
annotationCreated={annotation.created} annotationCreated={annotation.created}
annotationUpdated={annotation.updated} annotationUpdated={annotation.updated}
...@@ -143,11 +146,16 @@ function AnnotationHeader({ ...@@ -143,11 +146,16 @@ function AnnotationHeader({
/> />
</div> </div>
)} )}
</div> </HeaderRow>
{showExtendedInfo && ( {!isReply(annotation) && (
<div className="hyp-u-layout-row--align-baseline hyp-u-horizontal-spacing--2 flex-wrap-reverse"> <HeaderRow>
<AnnotationShareInfo annotation={annotation} /> {group && (
<AnnotationShareInfo
group={group}
isPrivate={isPrivate(annotation.permissions)}
/>
)}
{!isEditing && isHighlight(annotation) && ( {!isEditing && isHighlight(annotation) && (
<Icon <Icon
name="highlight" name="highlight"
...@@ -162,7 +170,7 @@ function AnnotationHeader({ ...@@ -162,7 +170,7 @@ function AnnotationHeader({
title={documentInfo.titleText} title={documentInfo.titleText}
/> />
)} )}
</div> </HeaderRow>
)} )}
</header> </header>
); );
......
import { Icon, Link } from '@hypothesis/frontend-shared'; import { Icon, Link } from '@hypothesis/frontend-shared';
import classnames from 'classnames';
import { useStoreProxy } from '../../store/use-store';
import { isPrivate } from '../../helpers/permissions';
/** /**
* @typedef {import("../../../types/api").Annotation} Annotation * @typedef {import("../../../types/api").Group} Group
*/ */
/** /**
* @typedef AnnotationShareInfoProps * @typedef AnnotationShareInfoProps
* @prop {Annotation} annotation * @prop {Group} group - Group to which the annotation belongs
* @prop {boolean} isPrivate
*/ */
/** /**
...@@ -18,38 +17,35 @@ import { isPrivate } from '../../helpers/permissions'; ...@@ -18,38 +17,35 @@ import { isPrivate } from '../../helpers/permissions';
* *
* @param {AnnotationShareInfoProps} props * @param {AnnotationShareInfoProps} props
*/ */
function AnnotationShareInfo({ annotation }) { function AnnotationShareInfo({ group, isPrivate }) {
const store = useStoreProxy();
const group = store.getGroup(annotation.group);
// Only show the name of the group and link to it if there is a // 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 // URL (link) returned by the API for this group. Some groups do not have links
const linkToGroup = group?.links.html; const linkToGroup = group?.links.html;
const annotationIsPrivate = isPrivate(annotation.permissions);
return ( return (
<div className="hyp-u-layout-row--align-baseline"> <>
{group && linkToGroup && ( {group && linkToGroup && (
<Link <Link
classes="hyp-u-layout-row--align-baseline hyp-u-horizontal-spacing--2 p-link--muted p-link--hover-muted" classes={classnames(
'flex items-baseline gap-x-1',
'text-color-text-light hover:text-color-text-light hover:underline'
)}
href={group.links.html} href={group.links.html}
target="_blank" target="_blank"
> >
{group.type === 'open' ? ( <Icon
<Icon classes="text-tiny" name="public" /> classes="text-tiny"
) : ( name={group.type === 'open' ? 'public' : 'groups'}
<Icon classes="text-tiny" name="groups" /> />
)}
<span>{group.name}</span> <span>{group.name}</span>
</Link> </Link>
)} )}
{annotationIsPrivate && !linkToGroup && ( {isPrivate && !linkToGroup && (
<span className="hyp-u-layout-row--align-baseline text-color-text-light"> <div className="text-color-text-light" data-testid="private-info">
<span data-testid="private-info">Only me</span> Only me
</span>
)}
</div> </div>
)}
</>
); );
} }
......
...@@ -90,7 +90,7 @@ export default function AnnotationTimestamps({ ...@@ -90,7 +90,7 @@ export default function AnnotationTimestamps({
)} )}
{annotationUrl ? ( {annotationUrl ? (
<Link <Link
classes="p-link--muted p-link--hover-muted" classes="text-color-text-light hover:text-color-text-light hover:underline"
target="_blank" target="_blank"
title={created.absolute} title={created.absolute}
href={annotationUrl} href={annotationUrl}
......
...@@ -11,6 +11,7 @@ describe('AnnotationHeader', () => { ...@@ -11,6 +11,7 @@ describe('AnnotationHeader', () => {
let fakeAccountId; let fakeAccountId;
let fakeAnnotationDisplayName; let fakeAnnotationDisplayName;
let fakeDomainAndTitle; let fakeDomainAndTitle;
let fakeGroup;
let fakeIsHighlight; let fakeIsHighlight;
let fakeIsReply; let fakeIsReply;
let fakeHasBeenEdited; let fakeHasBeenEdited;
...@@ -33,6 +34,13 @@ describe('AnnotationHeader', () => { ...@@ -33,6 +34,13 @@ describe('AnnotationHeader', () => {
beforeEach(() => { beforeEach(() => {
fakeDomainAndTitle = sinon.stub().returns({}); fakeDomainAndTitle = sinon.stub().returns({});
fakeGroup = {
name: 'My Group',
links: {
html: 'https://www.example.com',
},
type: 'private',
};
fakeIsHighlight = sinon.stub().returns(false); fakeIsHighlight = sinon.stub().returns(false);
fakeIsReply = sinon.stub().returns(false); fakeIsReply = sinon.stub().returns(false);
fakeHasBeenEdited = sinon.stub().returns(false); fakeHasBeenEdited = sinon.stub().returns(false);
...@@ -49,6 +57,7 @@ describe('AnnotationHeader', () => { ...@@ -49,6 +57,7 @@ describe('AnnotationHeader', () => {
fakeStore = { fakeStore = {
defaultAuthority: sinon.stub().returns('foo.com'), defaultAuthority: sinon.stub().returns('foo.com'),
getGroup: sinon.stub().returns(fakeGroup),
getLink: sinon.stub().returns('http://example.com'), getLink: sinon.stub().returns('http://example.com'),
isFeatureEnabled: sinon.stub().returns(false), isFeatureEnabled: sinon.stub().returns(false),
route: sinon.stub().returns('sidebar'), route: sinon.stub().returns('sidebar'),
...@@ -282,14 +291,21 @@ describe('AnnotationHeader', () => { ...@@ -282,14 +291,21 @@ describe('AnnotationHeader', () => {
}); });
describe('extended header information', () => { describe('extended header information', () => {
it('should render extended header information if annotation is not a reply', () => {
fakeIsReply.returns(false);
const wrapper = createAnnotationHeader();
// Extended header information is rendered in a second (flex) row
assert.equal(wrapper.find('HeaderRow').length, 2);
});
it('should not render extended header information if annotation is reply', () => { it('should not render extended header information if annotation is reply', () => {
fakeIsReply.returns(true); fakeIsReply.returns(true);
const wrapper = createAnnotationHeader({ const wrapper = createAnnotationHeader({
showDocumentInfo: true, showDocumentInfo: true,
}); });
assert.isFalse(wrapper.find('AnnotationShareInfo').exists()); assert.equal(wrapper.find('HeaderRow').length, 1);
assert.isFalse(wrapper.find('AnnotationDocumentInfo').exists());
}); });
describe('annotation is-highlight icon', () => { describe('annotation is-highlight icon', () => {
...@@ -314,6 +330,21 @@ describe('AnnotationHeader', () => { ...@@ -314,6 +330,21 @@ describe('AnnotationHeader', () => {
}); });
}); });
describe('Annotation share info', () => {
it('should render annotation share/group information if group is available', () => {
const wrapper = createAnnotationHeader();
assert.isTrue(wrapper.find('AnnotationShareInfo').exists());
});
it('should not render annotation share/group information if group is unavailable', () => {
fakeStore.getGroup.returns(undefined);
const wrapper = createAnnotationHeader();
assert.isFalse(wrapper.find('AnnotationShareInfo').exists());
});
});
describe('annotation document info', () => { describe('annotation document info', () => {
const fakeDocumentInfo = { const fakeDocumentInfo = {
titleText: 'This document', titleText: 'This document',
......
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import * as fixtures from '../../../test/annotation-fixtures';
import { checkAccessibility } from '../../../../test-util/accessibility'; import { checkAccessibility } from '../../../../test-util/accessibility';
import { mockImportedComponents } from '../../../../test-util/mock-imported-components'; import { mockImportedComponents } from '../../../../test-util/mock-imported-components';
...@@ -9,17 +7,9 @@ import AnnotationShareInfo, { $imports } from '../AnnotationShareInfo'; ...@@ -9,17 +7,9 @@ import AnnotationShareInfo, { $imports } from '../AnnotationShareInfo';
describe('AnnotationShareInfo', () => { describe('AnnotationShareInfo', () => {
let fakeGroup; let fakeGroup;
let fakeStore;
let fakeGetGroup;
let fakeIsPrivate;
const createAnnotationShareInfo = props => { const createAnnotationShareInfo = props => {
return mount( return mount(<AnnotationShareInfo group={fakeGroup} {...props} />);
<AnnotationShareInfo
annotation={fixtures.defaultAnnotation()}
{...props}
/>
);
}; };
beforeEach(() => { beforeEach(() => {
...@@ -30,15 +20,8 @@ describe('AnnotationShareInfo', () => { ...@@ -30,15 +20,8 @@ describe('AnnotationShareInfo', () => {
}, },
type: 'private', type: 'private',
}; };
fakeGetGroup = sinon.stub().returns(fakeGroup);
fakeStore = { getGroup: fakeGetGroup };
fakeIsPrivate = sinon.stub().returns(false);
$imports.$mock(mockImportedComponents()); $imports.$mock(mockImportedComponents());
$imports.$mock({
'../../store/use-store': { useStoreProxy: () => fakeStore },
'../../helpers/permissions': { isPrivate: fakeIsPrivate },
});
}); });
afterEach(() => { afterEach(() => {
...@@ -74,18 +57,9 @@ describe('AnnotationShareInfo', () => { ...@@ -74,18 +57,9 @@ describe('AnnotationShareInfo', () => {
it('should not show a link to third-party groups', () => { it('should not show a link to third-party groups', () => {
// Third-party groups have no `html` link // Third-party groups have no `html` link
fakeGetGroup.returns({ name: 'A Group', links: {} }); const wrapper = createAnnotationShareInfo({
group: { name: 'A Group', links: {} },
const wrapper = createAnnotationShareInfo();
const groupLink = wrapper.find('.AnnotationShareInfo__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('.AnnotationShareInfo__group'); const groupLink = wrapper.find('.AnnotationShareInfo__group');
assert.notOk(groupLink.exists()); assert.notOk(groupLink.exists());
...@@ -102,13 +76,11 @@ describe('AnnotationShareInfo', () => { ...@@ -102,13 +76,11 @@ describe('AnnotationShareInfo', () => {
}); });
context('private annotation', () => { context('private annotation', () => {
beforeEach(() => {
fakeIsPrivate.returns(true);
});
it('should show "only me" text for annotation in third-party group', () => { it('should show "only me" text for annotation in third-party group', () => {
fakeGetGroup.returns({ name: 'Some Name', links: {} }); const wrapper = createAnnotationShareInfo({
const wrapper = createAnnotationShareInfo(); group: { name: 'Some Name', links: {} },
isPrivate: true,
});
const privacyText = wrapper.find('[data-testid="private-info"]'); const privacyText = wrapper.find('[data-testid="private-info"]');
......
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