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'; ...@@ -2,10 +2,14 @@ import { createElement } from 'preact';
import propTypes from 'prop-types'; import propTypes from 'prop-types';
import useStore from '../store/use-store'; 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 AnnotationBody from './annotation-body';
import AnnotationHeader from './annotation-header'; import AnnotationHeader from './annotation-header';
import AnnotationLicense from './annotation-license';
import AnnotationPublishControl from './annotation-publish-control';
import AnnotationQuote from './annotation-quote'; import AnnotationQuote from './annotation-quote';
import TagEditor from './tag-editor'; import TagEditor from './tag-editor';
import TagList from './tag-list'; import TagList from './tag-list';
...@@ -20,16 +24,24 @@ function AnnotationOmega({ ...@@ -20,16 +24,24 @@ function AnnotationOmega({
showDocumentInfo, showDocumentInfo,
}) { }) {
const createDraft = useStore(store => store.createDraft); const createDraft = useStore(store => store.createDraft);
const setDefault = useStore(store => store.setDefault);
// An annotation will have a draft if it is being edited // An annotation will have a draft if it is being edited
const draft = useStore(store => store.getDraft(annotation)); 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 tags = draft ? draft.tags : annotation.tags;
const text = draft ? draft.text : annotation.text; const text = draft ? draft.text : annotation.text;
const hasQuote = !!quote(annotation); const hasQuote = !!quote(annotation);
const isEmpty = !text && !tags.length;
const isSaving = false; const isSaving = false;
const isEditing = !!draft && !isSaving; const isEditing = !!draft && !isSaving;
const shouldShowActions = !isEditing && !isNew(annotation);
const shouldShowLicense = isEditing && !isPrivate && group.type !== 'private';
const onEditTags = ({ tags }) => { const onEditTags = ({ tags }) => {
createDraft(annotation, { ...draft, tags }); createDraft(annotation, { ...draft, tags });
}; };
...@@ -38,6 +50,20 @@ function AnnotationOmega({ ...@@ -38,6 +50,20 @@ function AnnotationOmega({
createDraft(annotation, { ...draft, text }); 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 ( return (
<div className="annotation-omega"> <div className="annotation-omega">
<AnnotationHeader <AnnotationHeader
...@@ -56,6 +82,28 @@ function AnnotationOmega({ ...@@ -56,6 +82,28 @@ function AnnotationOmega({
/> />
{isEditing && <TagEditor onEditTags={onEditTags} tagList={tags} />} {isEditing && <TagEditor onEditTags={onEditTags} tagList={tags} />}
{!isEditing && <TagList annotation={annotation} tags={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> </div>
); );
} }
......
...@@ -15,9 +15,19 @@ describe('AnnotationOmega', () => { ...@@ -15,9 +15,19 @@ describe('AnnotationOmega', () => {
let fakeOnReplyCountClick; let fakeOnReplyCountClick;
// Dependency Mocks // Dependency Mocks
let fakeQuote; let fakeMetadata;
let fakePermissions;
let fakeStore; 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 => { const createComponent = props => {
return mount( return mount(
<Annotation <Annotation
...@@ -33,17 +43,29 @@ describe('AnnotationOmega', () => { ...@@ -33,17 +43,29 @@ describe('AnnotationOmega', () => {
beforeEach(() => { beforeEach(() => {
fakeOnReplyCountClick = sinon.stub(); 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 = { fakeStore = {
createDraft: sinon.stub(), createDraft: sinon.stub(),
getDraft: sinon.stub().returns(null), getDraft: sinon.stub().returns(null),
getGroup: sinon.stub().returns({
type: 'private',
}),
setDefault: sinon.stub(),
}; };
$imports.$mock(mockImportedComponents()); $imports.$mock(mockImportedComponents());
$imports.$mock({ $imports.$mock({
'../util/annotation-metadata': { '../util/annotation-metadata': fakeMetadata,
quote: fakeQuote, '../util/permissions': fakePermissions,
},
'../store/use-store': callback => callback(fakeStore), '../store/use-store': callback => callback(fakeStore),
}); });
}); });
...@@ -54,7 +76,7 @@ describe('AnnotationOmega', () => { ...@@ -54,7 +76,7 @@ describe('AnnotationOmega', () => {
describe('annotation quote', () => { describe('annotation quote', () => {
it('renders quote if annotation has a quote', () => { it('renders quote if annotation has a quote', () => {
fakeQuote.returns('quote'); fakeMetadata.quote.returns('quote');
const wrapper = createComponent(); const wrapper = createComponent();
const quote = wrapper.find('AnnotationQuote'); const quote = wrapper.find('AnnotationQuote');
...@@ -62,7 +84,7 @@ describe('AnnotationOmega', () => { ...@@ -62,7 +84,7 @@ describe('AnnotationOmega', () => {
}); });
it('does not render quote if annotation does not have a quote', () => { it('does not render quote if annotation does not have a quote', () => {
fakeQuote.returns(null); fakeMetadata.quote.returns(null);
const wrapper = createComponent(); const wrapper = createComponent();
...@@ -72,7 +94,7 @@ describe('AnnotationOmega', () => { ...@@ -72,7 +94,7 @@ describe('AnnotationOmega', () => {
}); });
describe('annotation body and excerpt', () => { describe('annotation body and excerpt', () => {
it('updates annotation state when text edited', () => { it('updates annotation draft when text edited', () => {
const wrapper = createComponent(); const wrapper = createComponent();
const body = wrapper.find('AnnotationBody'); const body = wrapper.find('AnnotationBody');
...@@ -80,23 +102,24 @@ describe('AnnotationOmega', () => { ...@@ -80,23 +102,24 @@ describe('AnnotationOmega', () => {
body.props().onEditText({ text: 'updated text' }); body.props().onEditText({ text: 'updated text' });
}); });
const call = fakeStore.createDraft.getCall(0);
assert.calledOnce(fakeStore.createDraft); assert.calledOnce(fakeStore.createDraft);
assert.equal(call.args[1].text, 'updated text');
}); });
}); });
describe('tags', () => { describe('tags', () => {
it('renders tag editor if `isEditing', () => { it('renders tag editor if `isEditing', () => {
// The presence of a draft will make `isEditing` `true` setEditingMode(true);
fakeStore.getDraft.returns(fixtures.defaultDraft());
const wrapper = createComponent(); const wrapper = createComponent();
assert.isOk(wrapper.find('TagEditor').exists()); assert.isTrue(wrapper.find('TagEditor').exists());
assert.notOk(wrapper.find('TagList').exists()); assert.isFalse(wrapper.find('TagList').exists());
}); });
it('updates annotation state if tags changed', () => { it('updates annotation draft if tags changed', () => {
fakeStore.getDraft.returns(fixtures.defaultDraft()); setEditingMode(true);
const wrapper = createComponent(); const wrapper = createComponent();
wrapper wrapper
...@@ -104,14 +127,183 @@ describe('AnnotationOmega', () => { ...@@ -104,14 +127,183 @@ describe('AnnotationOmega', () => {
.props() .props()
.onEditTags({ tags: ['uno', 'dos'] }); .onEditTags({ tags: ['uno', 'dos'] });
const call = fakeStore.createDraft.getCall(0);
assert.calledOnce(fakeStore.createDraft); assert.calledOnce(fakeStore.createDraft);
assert.sameMembers(call.args[1].tags, ['uno', 'dos']);
}); });
it('renders tag list if not `isEditing', () => { it('renders tag list if not `isEditing', () => {
const wrapper = createComponent(); const wrapper = createComponent();
assert.isOk(wrapper.find('TagList').exists()); assert.isTrue(wrapper.find('TagList').exists());
assert.notOk(wrapper.find('TagEditor').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() { ...@@ -156,8 +156,11 @@ describe('sidebar.frame-sync', function() {
}); });
it('sends a "publicAnnotationCountChanged" message to the frame when there are only private annotations', 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({ fakeStore.setState({
annotations: { annotations: [annotationFixtures.defaultAnnotation()] }, annotations: { annotations: [annot] },
}); });
assert.calledWithMatch( assert.calledWithMatch(
fakeBridge.call, fakeBridge.call,
......
...@@ -8,7 +8,12 @@ export function defaultAnnotation() { ...@@ -8,7 +8,12 @@ export function defaultAnnotation() {
document: { document: {
title: 'A special document', title: 'A special document',
}, },
permissions: {
read: ['group:__world__'],
},
tags: [],
target: [{ source: 'source', selector: [] }], target: [{ source: 'source', selector: [] }],
text: '',
uri: 'http://example.com', uri: 'http://example.com',
user: 'acct:bill@localhost', user: 'acct:bill@localhost',
updated: '2015-05-10T20:18:56.613388+00:00', updated: '2015-05-10T20:18:56.613388+00:00',
......
...@@ -371,7 +371,9 @@ describe('annotation-metadata', function() { ...@@ -371,7 +371,9 @@ describe('annotation-metadata', function() {
}); });
it('returns false if an annotation is missing permissions', 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