Commit 06678e64 authored by Robert Knight's avatar Robert Knight

Preserve private/shared status of annotations on import

Fixes https://github.com/hypothesis/client/issues/5757
parent c2d543aa
import type { Annotation, APIAnnotationData } from '../../types/api'; import type { Annotation, APIAnnotationData } from '../../types/api';
import { quote } from '../helpers/annotation-metadata'; import { quote } from '../helpers/annotation-metadata';
import {
isShared,
privatePermissions,
sharedPermissions,
} from '../helpers/permissions';
import type { SidebarStore } from '../store'; import type { SidebarStore } from '../store';
import type { Frame } from '../store/modules/frames'; import type { Frame } from '../store/modules/frames';
import type { AnnotationsService } from './annotations'; import type { AnnotationsService } from './annotations';
...@@ -154,6 +159,15 @@ export class ImportAnnotationsService { ...@@ -154,6 +159,15 @@ export class ImportAnnotationsService {
async import(anns: APIAnnotationData[]): Promise<ImportResult[]> { async import(anns: APIAnnotationData[]): Promise<ImportResult[]> {
this._store.beginImport(anns.length); this._store.beginImport(anns.length);
const currentUser = this._store.profile().userid;
if (!currentUser) {
throw new Error('Cannot import when logged out');
}
const currentGroup = this._store.focusedGroupId();
if (!currentGroup) {
throw new Error('Cannot import when no group is selected');
}
const existingAnns = this._store.allAnnotations(); const existingAnns = this._store.allAnnotations();
const currentFrame = this._store.mainFrame(); const currentFrame = this._store.mainFrame();
...@@ -173,6 +187,13 @@ export class ImportAnnotationsService { ...@@ -173,6 +187,13 @@ export class ImportAnnotationsService {
const saveData = const saveData =
this._annotationsService.annotationFromData(importData); this._annotationsService.annotationFromData(importData);
// Preserve shared / private status from existing annotation. We map
// the existing permissions to a shared/not-shared boolean and
// regenerate permissions, since the current user/group may differ.
saveData.permissions = isShared(ann.permissions)
? sharedPermissions(currentUser, currentGroup)
: privatePermissions(currentUser);
// Persist the annotation. // Persist the annotation.
const saved = await this._annotationsService.save(saveData); const saved = await this._annotationsService.save(saveData);
......
import {
privatePermissions,
sharedPermissions,
} from '../../helpers/permissions';
import { ImportAnnotationsService } from '../import-annotations'; import { ImportAnnotationsService } from '../import-annotations';
describe('ImportAnnotationsService', () => { describe('ImportAnnotationsService', () => {
...@@ -13,7 +17,11 @@ describe('ImportAnnotationsService', () => { ...@@ -13,7 +17,11 @@ describe('ImportAnnotationsService', () => {
allAnnotations: sinon.stub().returns([]), allAnnotations: sinon.stub().returns([]),
beginImport: sinon.stub(), beginImport: sinon.stub(),
completeImport: sinon.stub(), completeImport: sinon.stub(),
focusedGroupId: sinon.stub().returns('group-a'),
mainFrame: sinon.stub().returns(null), mainFrame: sinon.stub().returns(null),
profile: sinon.stub().returns({
userid: 'acct:foo@example.org',
}),
}; };
fakeToastMessenger = { fakeToastMessenger = {
...@@ -54,6 +62,14 @@ describe('ImportAnnotationsService', () => { ...@@ -54,6 +62,14 @@ describe('ImportAnnotationsService', () => {
text: `Annotation ${counter}`, text: `Annotation ${counter}`,
tags: ['foo'], tags: ['foo'],
document: { title: 'Example' }, document: { title: 'Example' },
permissions: sharedPermissions(
fakeStore.profile().userid,
// nb. We intentionally use a different group ID here than the current
// "focused" group in the store. The shared-ness will be preserved, but
// not the group ID.
'some-other-group',
),
...fields, ...fields,
}; };
} }
...@@ -70,6 +86,10 @@ describe('ImportAnnotationsService', () => { ...@@ -70,6 +86,10 @@ describe('ImportAnnotationsService', () => {
source: 'import', source: 'import',
original_id: ann.id, original_id: ann.id,
}, },
permissions: sharedPermissions(
fakeStore.profile().userid,
fakeStore.focusedGroupId(),
),
}; };
} }
...@@ -101,6 +121,21 @@ describe('ImportAnnotationsService', () => { ...@@ -101,6 +121,21 @@ describe('ImportAnnotationsService', () => {
}); });
}); });
it('preserves the private status of private annotations', async () => {
const originalUser = 'acct:original@example.com';
const svc = createService();
const ann = generateAnnotation();
ann.permissions = privatePermissions(originalUser);
await svc.import([ann]);
assert.calledWith(fakeAnnotationsService.save, {
$tag: 'dummy',
...importedAnnotation(ann),
permissions: privatePermissions(fakeStore.profile().userid),
});
});
it('sets annotation URI and document metadata to match current document', async () => { it('sets annotation URI and document metadata to match current document', async () => {
const newUri = 'new_document_uri'; const newUri = 'new_document_uri';
const newTitle = 'new_document_title'; const newTitle = 'new_document_title';
...@@ -190,5 +225,35 @@ describe('ImportAnnotationsService', () => { ...@@ -190,5 +225,35 @@ describe('ImportAnnotationsService', () => {
assert.calledWith(fakeToastMessenger.error, '2 imports failed'); assert.calledWith(fakeToastMessenger.error, '2 imports failed');
}); });
it('throws an error if called when user is logged out', async () => {
const svc = createService();
fakeStore.profile.returns({ userid: null });
let error;
try {
await svc.import([generateAnnotation()]);
} catch (err) {
error = err;
}
assert.instanceOf(error, Error);
assert.equal(error.message, 'Cannot import when logged out');
});
it('throws an error if called when no group is selected', async () => {
const svc = createService();
fakeStore.focusedGroupId.returns(null);
let error;
try {
await svc.import([generateAnnotation()]);
} catch (err) {
error = err;
}
assert.instanceOf(error, Error);
assert.equal(error.message, 'Cannot import when no group is selected');
});
}); });
}); });
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