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