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';
/**
* @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} isReply
* @prop {VoidFunction} onToggleReplies - Callback to expand/collapse reply threads
......@@ -44,21 +46,17 @@ function Annotation({
threadIsCollapsed,
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 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 onReply = () => annotationsService.reply(annotation, userid);
......@@ -66,29 +64,40 @@ function Annotation({
return (
<article
className={classnames('Annotation', {
'Annotation--missing': !annotation,
'Annotation--reply': isReply,
'is-collapsed': threadIsCollapsed,
'is-focused': isFocused,
})}
>
<AnnotationHeader
annotation={annotation}
isEditing={isEditing}
replyCount={replyCount}
showDocumentInfo={showDocumentInfo}
threadIsCollapsed={threadIsCollapsed}
/>
{hasQuote && (
<AnnotationQuote annotation={annotation} isFocused={isFocused} />
{annotation && (
<>
<AnnotationHeader
annotation={annotation}
isEditing={isEditing}
replyCount={replyCount}
showDocumentInfo={showDocumentInfo}
threadIsCollapsed={threadIsCollapsed}
/>
{hasQuote && (
<AnnotationQuote annotation={annotation} isFocused={isFocused} />
)}
{!isCollapsedReply && !isEditing && (
<AnnotationBody annotation={annotation} />
)}
{isEditing && <AnnotationEditor annotation={annotation} />}
</>
)}
{!isCollapsedReply && !isEditing && (
<AnnotationBody annotation={annotation} />
{!annotation && !isCollapsedReply && (
<div>
<em>Message not available.</em>
</div>
)}
{isEditing && <AnnotationEditor annotation={annotation} />}
{!isCollapsedReply && (
<footer className="Annotation__footer">
<div className="Annotation__controls u-layout-row">
......@@ -116,7 +125,7 @@ function Annotation({
}
Annotation.propTypes = {
annotation: propTypes.object.isRequired,
annotation: propTypes.object,
hasAppliedFilter: propTypes.bool.isRequired,
isReply: propTypes.bool,
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';
import { countHidden, countVisible } from '../helpers/thread';
import Annotation from './Annotation';
import AnnotationMissing from './AnnotationMissing';
import Button from './Button';
import ModerationBanner from './ModerationBanner';
......@@ -28,10 +27,6 @@ import ModerationBanner from './ModerationBanner';
* @param {ThreadProps} props
*/
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
const showChildren = !thread.collapsed;
......@@ -60,11 +55,13 @@ function Thread({ showDocumentInfo = false, thread, threadsService }) {
// Memoize annotation content to avoid re-rendering an annotation when content
// in other annotations/threads change.
const annotationContent = useMemo(() => {
if (showAnnotation) {
return (
const annotationContent = useMemo(
() =>
thread.visible && (
<>
<ModerationBanner annotation={thread.annotation} />
{thread.annotation && (
<ModerationBanner annotation={thread.annotation} />
)}
<Annotation
annotation={thread.annotation}
hasAppliedFilter={hasAppliedFilter}
......@@ -75,31 +72,18 @@ function Thread({ showDocumentInfo = false, thread, threadsService }) {
threadIsCollapsed={thread.collapsed}
/>
</>
);
} else if (showMissingAnnotation) {
return (
<AnnotationMissing
hasAppliedFilter={hasAppliedFilter}
isReply={!!thread.parent}
onToggleReplies={onToggleReplies}
replyCount={thread.replyCount}
threadIsCollapsed={thread.collapsed}
/>
);
} else {
return null;
}
}, [
hasAppliedFilter,
onToggleReplies,
showAnnotation,
showMissingAnnotation,
showDocumentInfo,
thread.annotation,
thread.parent,
thread.replyCount,
thread.collapsed,
]);
),
[
hasAppliedFilter,
onToggleReplies,
showDocumentInfo,
thread.annotation,
thread.parent,
thread.replyCount,
thread.collapsed,
thread.visible,
]
);
return (
<section
......
......@@ -249,6 +249,54 @@ describe('Annotation', () => {
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(
......
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', () => {
noAnnotationThread.annotation = undefined;
});
it('does not render an annotation or a moderation banner', () => {
it('renders an annotation component', () => {
const wrapper = createComponent({ thread: noAnnotationThread });
assert.isFalse(wrapper.find('Annotation').exists());
assert.isFalse(wrapper.find('ModerationBanner').exists());
});
it('renders a missing annotation component', () => {
const wrapper = createComponent({ thread: noAnnotationThread });
const annotationMissing = wrapper.find('AnnotationMissing');
const annotation = wrapper.find('Annotation');
assert.isTrue(annotationMissing.exists());
assert.isTrue(annotation.exists());
});
});
......@@ -226,7 +219,6 @@ describe('Thread', () => {
const wrapper = createComponent({ thread: noAnnotationThread });
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