Unverified Commit d77ca41f authored by Lyza Gardner's avatar Lyza Gardner Committed by GitHub

Merge pull request #1708 from hypothesis/annotation-omega-footer

Add some initial footer components to `AnnotationOmega`
parents 052d6e86 f319e86f
......@@ -2,10 +2,14 @@ import { createElement } from 'preact';
import propTypes from 'prop-types';
import useStore from '../store/use-store';
import { quote } from '../util/annotation-metadata';
import { isNew, isReply, quote } from '../util/annotation-metadata';
import { isShared } from '../util/permissions';
import AnnotationActionBar from './annotation-action-bar';
import AnnotationBody from './annotation-body';
import AnnotationHeader from './annotation-header';
import AnnotationLicense from './annotation-license';
import AnnotationPublishControl from './annotation-publish-control';
import AnnotationQuote from './annotation-quote';
import TagEditor from './tag-editor';
import TagList from './tag-list';
......@@ -20,16 +24,24 @@ function AnnotationOmega({
showDocumentInfo,
}) {
const createDraft = useStore(store => store.createDraft);
const setDefault = useStore(store => store.setDefault);
// An annotation will have a draft if it is being edited
const draft = useStore(store => store.getDraft(annotation));
const group = useStore(store => store.getGroup(annotation.group));
const isPrivate = draft ? draft.isPrivate : !isShared(annotation.permissions);
const tags = draft ? draft.tags : annotation.tags;
const text = draft ? draft.text : annotation.text;
const hasQuote = !!quote(annotation);
const isEmpty = !text && !tags.length;
const isSaving = false;
const isEditing = !!draft && !isSaving;
const shouldShowActions = !isEditing && !isNew(annotation);
const shouldShowLicense = isEditing && !isPrivate && group.type !== 'private';
const onEditTags = ({ tags }) => {
createDraft(annotation, { ...draft, tags });
};
......@@ -38,6 +50,20 @@ function AnnotationOmega({
createDraft(annotation, { ...draft, text });
};
const onSetPrivacy = ({ level }) => {
createDraft(annotation, { ...draft, isPrivate: level === 'private' });
// Persist this as privacy default for future annotations unless this is a reply
if (!isReply(annotation)) {
setDefault('annotationPrivacy', level);
}
};
// TODO
const fakeOnEdit = () => alert('Enter edit mode: TBD');
const fakeOnReply = () => alert('Reply: TBD');
const fakeOnRevert = () => alert('Revert changes: TBD');
const fakeOnSave = () => alert('Save changes: TBD');
return (
<div className="annotation-omega">
<AnnotationHeader
......@@ -56,6 +82,28 @@ function AnnotationOmega({
/>
{isEditing && <TagEditor onEditTags={onEditTags} tagList={tags} />}
{!isEditing && <TagList annotation={annotation} tags={tags} />}
<footer className="annotation-footer">
{isEditing && (
<AnnotationPublishControl
group={group}
isDisabled={isEmpty}
isShared={!isPrivate}
onCancel={fakeOnRevert}
onSave={fakeOnSave}
onSetPrivacy={onSetPrivacy}
/>
)}
{shouldShowLicense && <AnnotationLicense />}
{shouldShowActions && (
<div className="annotation-actions">
<AnnotationActionBar
annotation={annotation}
onEdit={fakeOnEdit}
onReply={fakeOnReply}
/>
</div>
)}
</footer>
</div>
);
}
......
......@@ -15,9 +15,19 @@ describe('AnnotationOmega', () => {
let fakeOnReplyCountClick;
// Dependency Mocks
let fakeQuote;
let fakeMetadata;
let fakePermissions;
let fakeStore;
const setEditingMode = (isEditing = true) => {
// The presence of a draft will make `isEditing` `true`
if (isEditing) {
fakeStore.getDraft.returns(fixtures.defaultDraft());
} else {
fakeStore.getDraft.returns(null);
}
};
const createComponent = props => {
return mount(
<Annotation
......@@ -33,17 +43,29 @@ describe('AnnotationOmega', () => {
beforeEach(() => {
fakeOnReplyCountClick = sinon.stub();
fakeQuote = sinon.stub();
fakeMetadata = {
isNew: sinon.stub(),
isReply: sinon.stub().returns(false),
quote: sinon.stub(),
};
fakePermissions = {
isShared: sinon.stub().returns(true),
};
fakeStore = {
createDraft: sinon.stub(),
getDraft: sinon.stub().returns(null),
getGroup: sinon.stub().returns({
type: 'private',
}),
setDefault: sinon.stub(),
};
$imports.$mock(mockImportedComponents());
$imports.$mock({
'../util/annotation-metadata': {
quote: fakeQuote,
},
'../util/annotation-metadata': fakeMetadata,
'../util/permissions': fakePermissions,
'../store/use-store': callback => callback(fakeStore),
});
});
......@@ -54,7 +76,7 @@ describe('AnnotationOmega', () => {
describe('annotation quote', () => {
it('renders quote if annotation has a quote', () => {
fakeQuote.returns('quote');
fakeMetadata.quote.returns('quote');
const wrapper = createComponent();
const quote = wrapper.find('AnnotationQuote');
......@@ -62,7 +84,7 @@ describe('AnnotationOmega', () => {
});
it('does not render quote if annotation does not have a quote', () => {
fakeQuote.returns(null);
fakeMetadata.quote.returns(null);
const wrapper = createComponent();
......@@ -72,7 +94,7 @@ describe('AnnotationOmega', () => {
});
describe('annotation body and excerpt', () => {
it('updates annotation state when text edited', () => {
it('updates annotation draft when text edited', () => {
const wrapper = createComponent();
const body = wrapper.find('AnnotationBody');
......@@ -80,23 +102,24 @@ describe('AnnotationOmega', () => {
body.props().onEditText({ text: 'updated text' });
});
const call = fakeStore.createDraft.getCall(0);
assert.calledOnce(fakeStore.createDraft);
assert.equal(call.args[1].text, 'updated text');
});
});
describe('tags', () => {
it('renders tag editor if `isEditing', () => {
// The presence of a draft will make `isEditing` `true`
fakeStore.getDraft.returns(fixtures.defaultDraft());
setEditingMode(true);
const wrapper = createComponent();
assert.isOk(wrapper.find('TagEditor').exists());
assert.notOk(wrapper.find('TagList').exists());
assert.isTrue(wrapper.find('TagEditor').exists());
assert.isFalse(wrapper.find('TagList').exists());
});
it('updates annotation state if tags changed', () => {
fakeStore.getDraft.returns(fixtures.defaultDraft());
it('updates annotation draft if tags changed', () => {
setEditingMode(true);
const wrapper = createComponent();
wrapper
......@@ -104,14 +127,183 @@ describe('AnnotationOmega', () => {
.props()
.onEditTags({ tags: ['uno', 'dos'] });
const call = fakeStore.createDraft.getCall(0);
assert.calledOnce(fakeStore.createDraft);
assert.sameMembers(call.args[1].tags, ['uno', 'dos']);
});
it('renders tag list if not `isEditing', () => {
const wrapper = createComponent();
assert.isOk(wrapper.find('TagList').exists());
assert.notOk(wrapper.find('TagEditor').exists());
assert.isTrue(wrapper.find('TagList').exists());
assert.isFalse(wrapper.find('TagEditor').exists());
});
});
describe('publish control', () => {
it('should show the publish control if in edit mode', () => {
setEditingMode(true);
const wrapper = createComponent();
assert.isTrue(wrapper.find('AnnotationPublishControl').exists());
});
it('should not show the publish control if not in edit mode', () => {
setEditingMode(false);
const wrapper = createComponent();
assert.isFalse(wrapper.find('AnnotationPublishControl').exists());
});
it('should set the publish control to disabled if annotation is empty', () => {
const draft = fixtures.defaultDraft();
draft.tags = [];
draft.text = '';
fakeStore.getDraft.returns(draft);
const wrapper = createComponent();
assert.isTrue(
wrapper.find('AnnotationPublishControl').props().isDisabled
);
});
it('should set `isShared` to `false` if annotation is private', () => {
const draft = fixtures.defaultDraft();
draft.isPrivate = true;
fakeStore.getDraft.returns(draft);
const wrapper = createComponent();
assert.isFalse(wrapper.find('AnnotationPublishControl').props().isShared);
});
it('should set `isShared` to `true` if annotation is shared', () => {
const draft = fixtures.defaultDraft();
draft.isPrivate = false;
fakeStore.getDraft.returns(draft);
const wrapper = createComponent();
assert.isTrue(wrapper.find('AnnotationPublishControl').props().isShared);
});
it('should update annotation privacy when changed by publish control', () => {
setEditingMode(true);
const wrapper = createComponent();
act(() => {
wrapper
.find('AnnotationPublishControl')
.props()
.onSetPrivacy({ level: 'private' });
});
const call = fakeStore.createDraft.getCall(0);
assert.calledOnce(fakeStore.createDraft);
assert.isTrue(call.args[1].isPrivate);
});
it('should update annotation privacy default on change', () => {
setEditingMode(true);
const wrapper = createComponent();
act(() => {
wrapper
.find('AnnotationPublishControl')
.props()
.onSetPrivacy({ level: 'private' });
});
assert.calledOnce(fakeStore.setDefault);
assert.calledWith(fakeStore.setDefault, 'annotationPrivacy', 'private');
});
it('should not update annotation privacy default on change if annotation is reply', () => {
fakeMetadata.isReply.returns(true);
setEditingMode(true);
const wrapper = createComponent();
act(() => {
wrapper
.find('AnnotationPublishControl')
.props()
.onSetPrivacy({ level: 'private' });
});
assert.equal(fakeStore.setDefault.callCount, 0);
});
});
describe('license information', () => {
it('should show license information when editing shared annotations in public groups', () => {
fakeStore.getGroup.returns({ type: 'open' });
setEditingMode(true);
const wrapper = createComponent();
assert.isTrue(wrapper.find('AnnotationLicense').exists());
});
it('should not show license information when not editing', () => {
fakeStore.getGroup.returns({ type: 'open' });
setEditingMode(false);
const wrapper = createComponent();
assert.isFalse(wrapper.find('AnnotationLicense').exists());
});
it('should not show license information for annotations in private groups', () => {
fakeStore.getGroup.returns({ type: 'private' });
setEditingMode(true);
const wrapper = createComponent();
assert.isFalse(wrapper.find('AnnotationLicense').exists());
});
it('should not show license information for private annotations', () => {
const draft = fixtures.defaultDraft();
draft.isPrivate = true;
fakeStore.getGroup.returns({ type: 'open' });
fakeStore.getDraft.returns(draft);
const wrapper = createComponent();
assert.isFalse(wrapper.find('AnnotationLicense').exists());
});
});
describe('annotation actions', () => {
it('should show annotation actions', () => {
const wrapper = createComponent();
assert.isTrue(wrapper.find('AnnotationActionBar').exists());
});
it('should not show annotation actions when editing', () => {
setEditingMode(true);
const wrapper = createComponent();
assert.isFalse(wrapper.find('AnnotationActionBar').exists());
});
it('should not show annotation actions for new annotation', () => {
fakeMetadata.isNew.returns(true);
const wrapper = createComponent();
assert.isFalse(wrapper.find('AnnotationActionBar').exists());
});
});
});
......@@ -156,8 +156,11 @@ describe('sidebar.frame-sync', function() {
});
it('sends a "publicAnnotationCountChanged" message to the frame when there are only private annotations', function() {
const annot = annotationFixtures.defaultAnnotation();
delete annot.permissions;
fakeStore.setState({
annotations: { annotations: [annotationFixtures.defaultAnnotation()] },
annotations: { annotations: [annot] },
});
assert.calledWithMatch(
fakeBridge.call,
......
......@@ -8,7 +8,12 @@ export function defaultAnnotation() {
document: {
title: 'A special document',
},
permissions: {
read: ['group:__world__'],
},
tags: [],
target: [{ source: 'source', selector: [] }],
text: '',
uri: 'http://example.com',
user: 'acct:bill@localhost',
updated: '2015-05-10T20:18:56.613388+00:00',
......
......@@ -371,7 +371,9 @@ describe('annotation-metadata', function() {
});
it('returns false if an annotation is missing permissions', function() {
assert.isFalse(annotationMetadata.isPublic(fixtures.defaultAnnotation()));
const annot = fixtures.defaultAnnotation();
delete annot.permissions;
assert.isFalse(annotationMetadata.isPublic(annot));
});
});
......
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