Commit 66cf493a authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Merge `AnnotationMissing` into `Annotation`

parent 963f5961
...@@ -19,7 +19,9 @@ import AnnotationReplyToggle from './AnnotationReplyToggle'; ...@@ -19,7 +19,9 @@ import AnnotationReplyToggle from './AnnotationReplyToggle';
/** /**
* @typedef AnnotationProps * @typedef AnnotationProps
* @prop {Annotation} annotation * @prop {Annotation} [annotation] - The annotation to render. If undefined,
* this Annotation will render as a "missing annotation" and will stand in
* as an Annotation for threads that lack an annotation.
* @prop {boolean} hasAppliedFilter - Is any filter applied currently? * @prop {boolean} hasAppliedFilter - Is any filter applied currently?
* @prop {boolean} isReply * @prop {boolean} isReply
* @prop {VoidFunction} onToggleReplies - Callback to expand/collapse reply threads * @prop {VoidFunction} onToggleReplies - Callback to expand/collapse reply threads
...@@ -44,21 +46,17 @@ function Annotation({ ...@@ -44,21 +46,17 @@ function Annotation({
threadIsCollapsed, threadIsCollapsed,
annotationsService, annotationsService,
}) { }) {
const store = useStoreProxy();
const isFocused = store.isAnnotationFocused(annotation.$tag);
// An annotation will have a draft if it is being edited
const draft = store.getDraft(annotation);
const userid = store.profile().userid;
const isSaving = store.isSavingAnnotation(annotation);
const isCollapsedReply = isReply && threadIsCollapsed; const isCollapsedReply = isReply && threadIsCollapsed;
const hasQuote = !!quote(annotation); const store = useStoreProxy();
const isEditing = !!draft && !isSaving; const hasQuote = annotation && !!quote(annotation);
const isFocused = annotation && store.isAnnotationFocused(annotation.$tag);
const isSaving = annotation && store.isSavingAnnotation(annotation);
const isEditing = annotation && !!store.getDraft(annotation) && !isSaving;
const showActions = !isSaving && !isEditing; const userid = store.profile().userid;
const showActions = annotation && !isSaving && !isEditing;
const showReplyToggle = !isReply && !hasAppliedFilter && replyCount > 0; const showReplyToggle = !isReply && !hasAppliedFilter && replyCount > 0;
const onReply = () => annotationsService.reply(annotation, userid); const onReply = () => annotationsService.reply(annotation, userid);
...@@ -66,29 +64,40 @@ function Annotation({ ...@@ -66,29 +64,40 @@ function Annotation({
return ( return (
<article <article
className={classnames('Annotation', { className={classnames('Annotation', {
'Annotation--missing': !annotation,
'Annotation--reply': isReply, 'Annotation--reply': isReply,
'is-collapsed': threadIsCollapsed, 'is-collapsed': threadIsCollapsed,
'is-focused': isFocused, 'is-focused': isFocused,
})} })}
> >
<AnnotationHeader {annotation && (
annotation={annotation} <>
isEditing={isEditing} <AnnotationHeader
replyCount={replyCount} annotation={annotation}
showDocumentInfo={showDocumentInfo} isEditing={isEditing}
threadIsCollapsed={threadIsCollapsed} replyCount={replyCount}
/> showDocumentInfo={showDocumentInfo}
threadIsCollapsed={threadIsCollapsed}
{hasQuote && ( />
<AnnotationQuote annotation={annotation} isFocused={isFocused} />
{hasQuote && (
<AnnotationQuote annotation={annotation} isFocused={isFocused} />
)}
{!isCollapsedReply && !isEditing && (
<AnnotationBody annotation={annotation} />
)}
{isEditing && <AnnotationEditor annotation={annotation} />}
</>
)} )}
{!isCollapsedReply && !isEditing && ( {!annotation && !isCollapsedReply && (
<AnnotationBody annotation={annotation} /> <div>
<em>Message not available.</em>
</div>
)} )}
{isEditing && <AnnotationEditor annotation={annotation} />}
{!isCollapsedReply && ( {!isCollapsedReply && (
<footer className="Annotation__footer"> <footer className="Annotation__footer">
<div className="Annotation__controls u-layout-row"> <div className="Annotation__controls u-layout-row">
...@@ -116,7 +125,7 @@ function Annotation({ ...@@ -116,7 +125,7 @@ function Annotation({
} }
Annotation.propTypes = { Annotation.propTypes = {
annotation: propTypes.object.isRequired, annotation: propTypes.object,
hasAppliedFilter: propTypes.bool.isRequired, hasAppliedFilter: propTypes.bool.isRequired,
isReply: propTypes.bool, isReply: propTypes.bool,
onToggleReplies: propTypes.func, onToggleReplies: propTypes.func,
......
import classnames from 'classnames';
import propTypes from 'prop-types';
import AnnotationReplyToggle from './AnnotationReplyToggle';
/**
* @typedef {import('./Annotation').AnnotationProps} AnnotationProps
* @typedef {Omit<AnnotationProps, 'annotation'|'showDocumentInfo'|'annotationsService'>} AnnotationMissingProps
*/
/**
* Renders in place of an annotation if a thread's annotation is missing.
*
* @param {AnnotationMissingProps} props
*/
function AnnotationMissing({
hasAppliedFilter,
isReply,
onToggleReplies,
replyCount,
threadIsCollapsed,
}) {
const showReplyToggle = !isReply && !hasAppliedFilter && replyCount > 0;
const isCollapsedReply = isReply && threadIsCollapsed;
return (
<article
className={classnames('Annotation', 'Annotation--missing', {
'is-collapsed': threadIsCollapsed,
})}
>
{!isCollapsedReply && (
<div>
<em>Message not available.</em>
</div>
)}
{!isCollapsedReply && (
<footer className="Annotation__footer">
<div className="Annotation__controls u-layout-row">
{showReplyToggle && (
<AnnotationReplyToggle
onToggleReplies={onToggleReplies}
replyCount={replyCount}
threadIsCollapsed={threadIsCollapsed}
/>
)}
</div>
</footer>
)}
</article>
);
}
AnnotationMissing.propTypes = {
hasAppliedFilter: propTypes.bool,
isReply: propTypes.bool.isRequired,
onToggleReplies: propTypes.func,
replyCount: propTypes.number.isRequired,
threadIsCollapsed: propTypes.bool,
};
export default AnnotationMissing;
...@@ -7,7 +7,6 @@ import { withServices } from '../service-context'; ...@@ -7,7 +7,6 @@ import { withServices } from '../service-context';
import { countHidden, countVisible } from '../helpers/thread'; import { countHidden, countVisible } from '../helpers/thread';
import Annotation from './Annotation'; import Annotation from './Annotation';
import AnnotationMissing from './AnnotationMissing';
import Button from './Button'; import Button from './Button';
import ModerationBanner from './ModerationBanner'; import ModerationBanner from './ModerationBanner';
...@@ -28,10 +27,6 @@ import ModerationBanner from './ModerationBanner'; ...@@ -28,10 +27,6 @@ import ModerationBanner from './ModerationBanner';
* @param {ThreadProps} props * @param {ThreadProps} props
*/ */
function Thread({ showDocumentInfo = false, thread, threadsService }) { function Thread({ showDocumentInfo = false, thread, threadsService }) {
// Only render this thread's annotation if it exists and the thread is `visible`
const showAnnotation = thread.annotation && thread.visible;
const showMissingAnnotation = thread.visible && !thread.annotation;
// Render this thread's replies only if the thread is expanded // Render this thread's replies only if the thread is expanded
const showChildren = !thread.collapsed; const showChildren = !thread.collapsed;
...@@ -60,11 +55,13 @@ function Thread({ showDocumentInfo = false, thread, threadsService }) { ...@@ -60,11 +55,13 @@ function Thread({ showDocumentInfo = false, thread, threadsService }) {
// Memoize annotation content to avoid re-rendering an annotation when content // Memoize annotation content to avoid re-rendering an annotation when content
// in other annotations/threads change. // in other annotations/threads change.
const annotationContent = useMemo(() => { const annotationContent = useMemo(
if (showAnnotation) { () =>
return ( thread.visible && (
<> <>
<ModerationBanner annotation={thread.annotation} /> {thread.annotation && (
<ModerationBanner annotation={thread.annotation} />
)}
<Annotation <Annotation
annotation={thread.annotation} annotation={thread.annotation}
hasAppliedFilter={hasAppliedFilter} hasAppliedFilter={hasAppliedFilter}
...@@ -75,31 +72,18 @@ function Thread({ showDocumentInfo = false, thread, threadsService }) { ...@@ -75,31 +72,18 @@ function Thread({ showDocumentInfo = false, thread, threadsService }) {
threadIsCollapsed={thread.collapsed} threadIsCollapsed={thread.collapsed}
/> />
</> </>
); ),
} else if (showMissingAnnotation) { [
return ( hasAppliedFilter,
<AnnotationMissing onToggleReplies,
hasAppliedFilter={hasAppliedFilter} showDocumentInfo,
isReply={!!thread.parent} thread.annotation,
onToggleReplies={onToggleReplies} thread.parent,
replyCount={thread.replyCount} thread.replyCount,
threadIsCollapsed={thread.collapsed} thread.collapsed,
/> thread.visible,
); ]
} else { );
return null;
}
}, [
hasAppliedFilter,
onToggleReplies,
showAnnotation,
showMissingAnnotation,
showDocumentInfo,
thread.annotation,
thread.parent,
thread.replyCount,
thread.collapsed,
]);
return ( return (
<section <section
......
...@@ -249,6 +249,54 @@ describe('Annotation', () => { ...@@ -249,6 +249,54 @@ describe('Annotation', () => {
assert.isTrue(wrapper.find('footer').exists()); assert.isTrue(wrapper.find('footer').exists());
}); });
}); });
context('missing annotation', () => {
it('should render a message about annotation unavailability', () => {
const wrapper = createComponent({ annotation: undefined });
assert.equal(wrapper.text(), 'Message not available.');
});
it('should not render a message if collapsed reply', () => {
const wrapper = createComponent({
annotation: undefined,
isReply: true,
threadIsCollapsed: true,
});
assert.equal(wrapper.text(), '');
});
it('should render reply toggle controls if there are replies', () => {
const wrapper = createComponent({
annotation: undefined,
replyCount: 5,
threadIsCollapsed: true,
});
const toggle = wrapper.find('AnnotationReplyToggle');
assert.isTrue(toggle.exists());
assert.equal(toggle.props().onToggleReplies, fakeOnToggleReplies);
assert.equal(toggle.props().replyCount, 5);
assert.equal(toggle.props().threadIsCollapsed, true);
});
it('should not render reply toggle controls if collapsed reply', () => {
const wrapper = createComponent({
annotation: undefined,
isReply: true,
replyCount: 5,
threadIsCollapsed: true,
});
const toggle = wrapper.find('AnnotationReplyToggle');
assert.isFalse(toggle.exists());
});
it('should not render other annotation sub-components');
});
}); });
it( it(
......
import { mount } from 'enzyme';
import AnnotationMissing, { $imports } from '../AnnotationMissing';
import { checkAccessibility } from '../../../test-util/accessibility';
import mockImportedComponents from '../../../test-util/mock-imported-components';
describe('AnnotationMissing', () => {
let fakeOnToggleReplies;
function createComponent(props = {}) {
return mount(
<AnnotationMissing
hasAppliedFilter={false}
isReply={false}
onToggleReplies={fakeOnToggleReplies}
replyCount={5}
threadIsCollapsed={true}
{...props}
/>
);
}
beforeEach(() => {
fakeOnToggleReplies = sinon.stub();
$imports.$mock(mockImportedComponents());
});
afterEach(() => {
$imports.$restore();
});
context('collapsed reply', () => {
it('does not show message-unavailable text', () => {
const wrapper = createComponent({
isReply: true,
threadIsCollapsed: true,
});
assert.equal(wrapper.text(), '');
});
it('does not render a reply toggle', () => {
const wrapper = createComponent({
isReply: true,
threadIsCollapsed: true,
});
assert.isFalse(wrapper.find('AnnotationReplyToggle').exists());
});
});
context('collapsed thread, not a reply', () => {
it('shows message-unavailable text', () => {
const wrapper = createComponent({
isReply: false,
threadIsCollapsed: true,
});
assert.match(wrapper.text(), /Message not available/);
});
it('renders a reply toggle control', () => {
const wrapper = createComponent({
isReply: false,
threadIsCollapsed: true,
});
const toggle = wrapper.find('AnnotationReplyToggle');
assert.equal(toggle.props().onToggleReplies, fakeOnToggleReplies);
});
});
context('expanded thread, not a reply', () => {
it('shows message-unavailable text', () => {
const wrapper = createComponent({
isReply: false,
threadIsCollapsed: false,
});
assert.match(wrapper.text(), /Message not available/);
});
it('renders a reply toggle control', () => {
const wrapper = createComponent({
isReply: false,
threadIsCollapsed: false,
});
const toggle = wrapper.find('AnnotationReplyToggle');
assert.equal(toggle.props().onToggleReplies, fakeOnToggleReplies);
});
});
it(
'should pass a11y checks',
checkAccessibility({
content: () => createComponent(),
})
);
});
...@@ -197,19 +197,12 @@ describe('Thread', () => { ...@@ -197,19 +197,12 @@ describe('Thread', () => {
noAnnotationThread.annotation = undefined; noAnnotationThread.annotation = undefined;
}); });
it('does not render an annotation or a moderation banner', () => { it('renders an annotation component', () => {
const wrapper = createComponent({ thread: noAnnotationThread }); const wrapper = createComponent({ thread: noAnnotationThread });
assert.isFalse(wrapper.find('Annotation').exists()); const annotation = wrapper.find('Annotation');
assert.isFalse(wrapper.find('ModerationBanner').exists());
});
it('renders a missing annotation component', () => {
const wrapper = createComponent({ thread: noAnnotationThread });
const annotationMissing = wrapper.find('AnnotationMissing');
assert.isTrue(annotationMissing.exists()); assert.isTrue(annotation.exists());
}); });
}); });
...@@ -226,7 +219,6 @@ describe('Thread', () => { ...@@ -226,7 +219,6 @@ describe('Thread', () => {
const wrapper = createComponent({ thread: noAnnotationThread }); const wrapper = createComponent({ thread: noAnnotationThread });
assert.isFalse(wrapper.find('Annotation').exists()); assert.isFalse(wrapper.find('Annotation').exists());
assert.isFalse(wrapper.find('AnnotationMissing').exists());
}); });
}); });
......
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