Closes #1868, Dragging collections into group libraries

This commit is contained in:
Dan Stillman 2011-08-05 18:17:06 +00:00
parent 7aa474e584
commit 067df94822
6 changed files with 398 additions and 195 deletions

View File

@ -235,24 +235,8 @@ Zotero.CollectionTreeView.prototype.refresh = function()
*/
Zotero.CollectionTreeView.prototype.reload = function()
{
var openCollections = [];
for (var i=0; i<this.rowCount; i++) {
if (this.isContainer(i) && this.isContainerOpen(i)) {
openCollections.push(this._getItemAtRow(i).ref.id);
}
}
this._treebox.beginUpdateBatch();
this.refresh();
for(var i = 0; i < openCollections.length; i++)
{
var row = this._collectionRowMap[openCollections[i]];
if (typeof row != 'undefined') {
this.toggleOpenState(row);
}
}
this._treebox.invalidate();
this._treebox.endUpdateBatch();
}
@ -360,9 +344,9 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids)
{
case 'collection':
var collection = Zotero.Collections.get(ids);
var collectionID = collection.id;
// Open container if creating subcollection
var parentID = collection.getParent();
var parentID = collection.parent;
if (parentID) {
if (!this.isContainerOpen(this._collectionRowMap[parentID])){
this.toggleOpenState(this._collectionRowMap[parentID]);
@ -374,7 +358,7 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids)
this.rememberSelection(savedSelection);
break;
}
this.selection.select(this._collectionRowMap[collectionID]);
this.selection.select(this._collectionRowMap[collection.id]);
break;
case 'search':
@ -1259,27 +1243,42 @@ Zotero.CollectionTreeView.prototype.canDrop = function(row, orient, dragData)
return true;
}
else if (dataType == 'zotero/collection') {
// Collections cannot be dropped on themselves
if (data[0] == itemGroup.ref.id) {
return false;
}
// Nor in their children
if (Zotero.Collections.get(data[0]).hasDescendent('collection', itemGroup.ref.id)) {
return false;
}
var col = Zotero.Collections.get(data[0]);
// Nor, at least for now, on another group
if (itemGroup.isWithinGroup()) {
if (itemGroup.ref.libraryID != col.libraryID) {
if (itemGroup.ref.libraryID == col.libraryID) {
// Collections cannot be dropped on themselves
if (data[0] == itemGroup.ref.id) {
return false;
}
// Nor in their children
if (Zotero.Collections.get(data[0]).hasDescendent('collection', itemGroup.ref.id)) {
return false;
}
}
// Nor from a group library to the local library
else if (col.libraryID) {
return false;
// Dragging a collection to a different library
else {
// Allow cross-library drag only to root library and collections
if (!itemGroup.isLibrary(true) && !itemGroup.isCollection()) {
return false;
}
// Disallow if linked collection already exists
if (col.getLinkedCollection(itemGroup.ref.libraryID)) {
return false;
}
var descendents = col.getDescendents(false, 'collection');
for each(var descendent in descendents) {
descendent = Zotero.Collections.get(descendent.id);
// Disallow if linked collection already exists for any subcollections
//
// If this is allowed in the future for the root collection,
// need to allow drag only to root
if (descendent.getLinkedCollection(itemGroup.ref.libraryID)) {
return false;
}
}
}
return true;
@ -1303,15 +1302,202 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
var data = dragData.data;
var itemGroup = this._getItemAtRow(row);
if(dataType == 'zotero/collection')
{
var targetCollectionID;
if (itemGroup.isCollection()) {
targetCollectionID = itemGroup.ref.id;
function copyItem (item, targetLibraryID) {
// Check if there's already a copy of this item in the library
var linkedItem = item.getLinkedItem(targetLibraryID);
if (linkedItem) {
return linkedItem.id;
/*
// TODO: support tags, related, attachments, etc.
// Overlay source item fields on unsaved clone of linked item
var newItem = item.clone(false, linkedItem.clone(true));
newItem.setField('dateAdded', item.dateAdded);
newItem.setField('dateModified', item.dateModified);
var diff = newItem.diff(linkedItem, false, ["dateAdded", "dateModified"]);
if (!diff) {
// Check if creators changed
var creatorsChanged = false;
var creators = item.getCreators();
var linkedCreators = linkedItem.getCreators();
if (creators.length != linkedCreators.length) {
Zotero.debug('Creators have changed');
creatorsChanged = true;
}
else {
for (var i=0; i<creators.length; i++) {
if (!creators[i].ref.equals(linkedCreators[i].ref)) {
Zotero.debug('changed');
creatorsChanged = true;
break;
}
}
}
if (!creatorsChanged) {
Zotero.debug("Linked item hasn't changed -- skipping conflict resolution");
continue;
}
}
toReconcile.push([newItem, linkedItem]);
continue;
*/
}
// Standalone attachment
if (item.isAttachment()) {
return Zotero.Attachments.copyAttachmentToLibrary(item, targetLibraryID);
}
// Create new unsaved clone item in target library
var newItem = new Zotero.Item(item.itemTypeID);
newItem.libraryID = targetLibraryID;
// DEBUG: save here because clone() doesn't currently work on unsaved tagged items
var id = newItem.save();
newItem = Zotero.Items.get(id);
item.clone(false, newItem);
newItem.save();
//var id = newItem.save();
//var newItem = Zotero.Items.get(id);
// Record link
item.addLinkedItem(newItem);
var newID = id;
if (item.isNote()) {
return newID;
}
// For regular items, add child items if prefs and permissions allow
// Child notes
if (Zotero.Prefs.get('groups.copyChildNotes')) {
var noteIDs = item.getNotes();
var notes = Zotero.Items.get(noteIDs);
for each(var note in notes) {
var newNote = new Zotero.Item('note');
newNote.libraryID = targetLibraryID;
// DEBUG: save here because clone() doesn't currently work on unsaved tagged items
var id = newNote.save();
newNote = Zotero.Items.get(id);
note.clone(false, newNote);
newNote.setSource(newItem.id);
newNote.save();
note.addLinkedItem(newNote);
}
}
// Child attachments
var copyChildLinks = Zotero.Prefs.get('groups.copyChildLinks');
var copyChildFileAttachments = Zotero.Prefs.get('groups.copyChildFileAttachments');
if (copyChildLinks || copyChildFileAttachments) {
var attachmentIDs = item.getAttachments();
var attachments = Zotero.Items.get(attachmentIDs);
for each(var attachment in attachments) {
var linkMode = attachment.attachmentLinkMode;
// Skip linked files
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
continue;
}
// Skip imported files if we don't have pref and permissions
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
if (!copyChildLinks) {
Zotero.debug("Skipping child link attachment on drag");
continue;
}
}
else {
if (!copyChildFileAttachments || !itemGroup.filesEditable) {
Zotero.debug("Skipping child file attachment on drag");
continue;
}
}
Zotero.Attachments.copyAttachmentToLibrary(attachment, targetLibraryID, newItem.id);
}
}
return newID;
}
var targetLibraryID = itemGroup.isWithinGroup() ? itemGroup.ref.libraryID : null;
var targetCollectionID = itemGroup.isCollection() ? itemGroup.ref.id : false;
if (dataType == 'zotero/collection') {
var droppedCollection = Zotero.Collections.get(data[0]);
droppedCollection.parent = targetCollectionID;
droppedCollection.save();
// Collection drag between libraries
if (targetLibraryID != droppedCollection.libraryID) {
Zotero.DB.beginTransaction();
function copyCollections(descendents, parent, addItems) {
for each(var desc in descendents) {
// Collections
if (desc.type == 'collection') {
var c = Zotero.Collections.get(desc.id);
var newCollection = new Zotero.Collection;
newCollection.libraryID = targetLibraryID;
c.clone(false, newCollection);
if (parent) {
newCollection.parent = parent;
}
var collectionID = newCollection.save();
// Record link
c.addLinkedCollection(newCollection);
// Recursively copy subcollections
if (desc.children.length) {
copyCollections(desc.children, collectionID, addItems);
}
}
// Items
else {
var item = Zotero.Items.get(desc.id);
var id = copyItem(item, targetLibraryID);
// Mark copied item for adding to collection
if (parent) {
if (!addItems[parent]) {
addItems[parent] = [];
}
addItems[parent].push(id);
}
}
}
return collectionID;
}
var collections = [{
id: droppedCollection.id,
children: droppedCollection.getDescendents(true),
type: 'collection'
}];
var addItems = {};
copyCollections(collections, targetCollectionID, addItems);
for (var collectionID in addItems) {
var collection = Zotero.Collections.get(collectionID);
collection.addItems(addItems[collectionID]);
}
// TODO: add subcollections and subitems, if they don't already exist,
// and display a warning if any of the subcollections already exist
Zotero.DB.commitTransaction();
}
// Collection drag within a library
else {
droppedCollection.parent = targetCollectionID;
droppedCollection.save();
}
}
else if (dataType == 'zotero/item') {
var ids = data;
@ -1319,13 +1505,6 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
return;
}
if (itemGroup.isWithinGroup()) {
var targetLibraryID = itemGroup.ref.libraryID;
}
else {
var targetLibraryID = null;
}
if(itemGroup.isBucket()) {
itemGroup.ref.uploadItems(ids);
return;
@ -1335,6 +1514,7 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
var items = Zotero.Items.get(ids);
if (!items) {
Zotero.DB.commitTransaction();
return;
}
@ -1364,131 +1544,9 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
if (!sameLibrary) {
var toReconcile = [];
var newIDs = [];
for each(var item in newItems) {
// Check if there's already a copy of this item in the library
var linkedItem = item.getLinkedItem(targetLibraryID);
if (linkedItem) {
// Add linked item to target collection rather than copying
if (itemGroup.isCollection()) {
itemGroup.ref.addItem(linkedItem.id);
continue;
}
/*
// TODO: support tags, related, attachments, etc.
// Overlay source item fields on unsaved clone of linked item
var newItem = item.clone(false, linkedItem.clone(true));
newItem.setField('dateAdded', item.dateAdded);
newItem.setField('dateModified', item.dateModified);
var diff = newItem.diff(linkedItem, false, ["dateAdded", "dateModified"]);
if (!diff) {
// Check if creators changed
var creatorsChanged = false;
var creators = item.getCreators();
var linkedCreators = linkedItem.getCreators();
if (creators.length != linkedCreators.length) {
Zotero.debug('Creators have changed');
creatorsChanged = true;
}
else {
for (var i=0; i<creators.length; i++) {
if (!creators[i].ref.equals(linkedCreators[i].ref)) {
Zotero.debug('changed');
creatorsChanged = true;
break;
}
}
}
if (!creatorsChanged) {
Zotero.debug("Linked item hasn't changed -- skipping conflict resolution");
continue;
}
}
toReconcile.push([newItem, linkedItem]);
continue;
*/
}
// Standalone attachment
if (item.isAttachment()) {
var id = Zotero.Attachments.copyAttachmentToLibrary(item, targetLibraryID);
newIDs.push(id);
continue;
}
// Create new unsaved clone item in target library
var newItem = new Zotero.Item(item.itemTypeID);
newItem.libraryID = targetLibraryID;
// DEBUG: save here because clone() doesn't currently work on unsaved tagged items
var id = newItem.save();
newItem = Zotero.Items.get(id);
item.clone(false, newItem);
newItem.save();
//var id = newItem.save();
//var newItem = Zotero.Items.get(id);
// Record link
item.addLinkedItem(newItem);
newIDs.push(id);
if (item.isNote()) {
continue;
}
// For regular items, add child items if prefs and permissions allow
// Child notes
if (Zotero.Prefs.get('groups.copyChildNotes')) {
var noteIDs = item.getNotes();
var notes = Zotero.Items.get(noteIDs);
for each(var note in notes) {
var newNote = new Zotero.Item('note');
newNote.libraryID = targetLibraryID;
// DEBUG: save here because clone() doesn't currently work on unsaved tagged items
var id = newNote.save();
newNote = Zotero.Items.get(id);
note.clone(false, newNote);
newNote.setSource(newItem.id);
newNote.save();
note.addLinkedItem(newNote);
}
}
// Child attachments
var copyChildLinks = Zotero.Prefs.get('groups.copyChildLinks');
var copyChildFileAttachments = Zotero.Prefs.get('groups.copyChildFileAttachments');
if (copyChildLinks || copyChildFileAttachments) {
var attachmentIDs = item.getAttachments();
var attachments = Zotero.Items.get(attachmentIDs);
for each(var attachment in attachments) {
var linkMode = attachment.attachmentLinkMode;
// Skip linked files
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
continue;
}
// Skip imported files if we don't have pref and permissions
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
if (!copyChildLinks) {
Zotero.debug("Skipping child link attachment on drag");
continue;
}
}
else {
if (!copyChildFileAttachments || !itemGroup.filesEditable) {
Zotero.debug("Skipping child file attachment on drag");
continue;
}
}
var id = Zotero.Attachments.copyAttachmentToLibrary(attachment, targetLibraryID, newItem.id);
}
}
newIDs.push(copyItem(item, libraryID));
}
if (toReconcile.length) {
@ -1529,8 +1587,10 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
}
}
if (newIDs.length && itemGroup.isCollection()) {
itemGroup.ref.addItems(newIDs);
// Add items to target collection
if (targetCollectionID) {
var collection = Zotero.Collections.get(targetCollectionID);
collection.addItems(newIDs);
}
Zotero.DB.commitTransaction();

View File

@ -896,6 +896,40 @@ Zotero.Collection.prototype.diff = function (collection, includeMatches, ignoreO
}
/**
* Returns an unsaved copy of the collection
*
* Does not copy parent collection or child items
*
* @param {Boolean} [includePrimary=false]
* @param {Zotero.Collection} [newCollection=null]
*/
Zotero.Collection.prototype.clone = function (includePrimary, newCollection) {
Zotero.debug('Cloning collection ' + this.id);
if (newCollection) {
var sameLibrary = newCollection.libraryID == this.libraryID;
}
else {
var newCollection = new Zotero.Collection;
var sameLibrary = true;
if (includePrimary) {
newCollection.id = this.id;
newCollection.libraryID = this.libraryID;
newCollection.key = this.key;
// TODO: This isn't used, but if it were, it should probably include
// parent collection and child items
}
}
newCollection.name = this.name;
return newCollection;
}
/**
* Deletes collection and all descendent collections (and optionally items)
**/
@ -930,6 +964,10 @@ Zotero.Collection.prototype.erase = function(deleteItems) {
Zotero.Items.erase(del);
}
// Remove relations
var uri = Zotero.URI.getCollectionURI(this);
Zotero.Relations.eraseByURIPrefix(uri);
var placeholders = collections.map(function () '?').join();
// Remove item associations for all descendent collections
@ -1095,6 +1133,60 @@ Zotero.Collection.prototype.getDescendents = function(nested, type, includeDelet
}
/**
* Return a collection in the specified library equivalent to this collection
*/
Zotero.Collection.prototype.getLinkedCollection = function (libraryID) {
if (libraryID == this.libraryID) {
throw ("Collection is already in library " + libraryID + " in Zotero.Collection.getLinkedCollection()");
}
var predicate = Zotero.Relations.linkedObjectPredicate;
var collectionURI = Zotero.URI.getCollectionURI(this);
var links = Zotero.Relations.getObject(collectionURI, predicate, false).concat(
Zotero.Relations.getSubject(false, predicate, collectionURI)
);
if (!links.length) {
return false;
}
if (libraryID) {
var libraryCollectionPrefix = Zotero.URI.getLibraryURI(libraryID) + "/collections/";
}
else {
var libraryCollectionPrefix = Zotero.URI.getCurrentUserURI() + "/collections/";
}
for each(var link in links) {
if (link.indexOf(libraryCollectionPrefix) == 0) {
var collection = Zotero.URI.getURICollection(link);
if (!collection) {
Zotero.debug("Referenced linked collection '" + link + "' not found in Zotero.Collection.getLinkedCollection()", 2);
continue;
}
return collection;
}
}
return false;
}
Zotero.Collection.prototype.addLinkedCollection = function (collection) {
var url1 = Zotero.URI.getCollectionURI(this);
var url2 = Zotero.URI.getCollectionURI(collection);
var predicate = Zotero.Relations.linkedObjectPredicate;
if (Zotero.Relations.getByURIs(url1, predicate, url2).length
|| Zotero.Relations.getByURIs(url2, predicate, url1).length) {
Zotero.debug("Collections " + this.key + " and " + collection.key + " are already linked");
return false;
}
Zotero.Relations.add(null, url1, predicate, url2);
}
//
// Private methods
//
Zotero.Collection.prototype._prepFieldChange = function (field) {
if (!this._changed) {
this._changed = {};

View File

@ -3548,12 +3548,12 @@ Zotero.Item.prototype.getLinkedItem = function (libraryID) {
throw ("Item is already in library " + libraryID + " in Zotero.Item.getLinkedItem()");
}
var predicate = Zotero.Items.linkedItemPredicate;
var predicate = Zotero.Relations.linkedObjectPredicate;
var itemURI = Zotero.URI.getItemURI(this);
var links = Zotero.Relations.getObject(itemURI, predicate, false).concat(
Zotero.Relations.getSubject(false, predicate, itemURI)
);
Zotero.debug(links);
if (!links.length) {
return false;
}
@ -3581,7 +3581,7 @@ Zotero.Item.prototype.getLinkedItem = function (libraryID) {
Zotero.Item.prototype.addLinkedItem = function (item) {
var url1 = Zotero.URI.getItemURI(this);
var url2 = Zotero.URI.getItemURI(item);
var predicate = Zotero.Items.linkedItemPredicate;
var predicate = Zotero.Relations.linkedObjectPredicate;
if (Zotero.Relations.getByURIs(url1, predicate, url2).length
|| Zotero.Relations.getByURIs(url2, predicate, url1).length) {
Zotero.debug("Items " + this.key + " and " + item.key + " are already linked");

View File

@ -52,7 +52,6 @@ Zotero.Items = new function() {
return _primaryFields;
});
this.__defineGetter__('linkedItemPredicate', function () "owl:sameAs");
// Private members
var _cachedFields = [];

View File

@ -27,6 +27,7 @@ Zotero.Relations = new function () {
Zotero.DataObjects.apply(this, ['relation']);
this.constructor.prototype = new Zotero.DataObjects();
this.__defineGetter__('linkedObjectPredicate', function () "owl:sameAs");
this.__defineGetter__('deletedItemPredicate', function () 'dc:isReplacedBy');
var _namespaces = {
@ -226,7 +227,10 @@ Zotero.Relations = new function () {
if (uri.indexOf(prefix) == -1) {
continue;
}
if (!Zotero.URI.getURIItem(uri)) {
if (uri.indexOf(/\/items\//) != -1 && !Zotero.URI.getURIItem(uri)) {
this.eraseByURI(uri);
}
if (uri.indexOf(/\/collections\//) != -1 && !Zotero.URI.getURICollection(uri)) {
this.eraseByURI(uri);
}
}

View File

@ -131,6 +131,28 @@ Zotero.URI = new function () {
}
/**
* Return URI of collection, which might be a local URI if user hasn't synced
*/
this.getCollectionURI = function (collection) {
if (collection.libraryID) {
var baseURI = this.getLibraryURI(collection.libraryID);
}
else {
var baseURI = this.getCurrentUserURI();
}
return baseURI + "/collections/" + collection.key;
}
/**
* Get path portion of collection URI (e.g., users/6/collections/ABCD1234 or groups/1/collections/ABCD1234)
*/
this.getCollectionPath = function (collection) {
return this.getLibraryPath(collection.libraryID) + "/collections/" + collection.key;
}
this.getGroupsURL = function () {
return ZOTERO_CONFIG.WWW_BASE_URL + "groups";
}
@ -156,17 +178,42 @@ Zotero.URI = new function () {
* @param {Zotero.Item|FALSE}
*/
this.getURIItem = function (itemURI) {
return this._getURIObject(itemURI, 'item');
}
/**
* Convert a collection URI into a collection
*
* @param {String} collectionURI
* @param {Zotero.Collection|FALSE}
*/
this.getURICollection = function (collectionURI) {
return this._getURIObject(collectionURI, 'collection');
}
/**
* Convert an object URI into an object (item, collection, etc.)
*
* @param {String} objectURI
* @param {Zotero.Item|Zotero.Collection|FALSE}
*/
this._getURIObject = function (objectURI, type) {
var Types = type[0].toUpperCase() + type.substr(1) + 's';
var types = Types.toLowerCase();
var libraryType = null;
// If this is a local URI, compare to the local user key
if (itemURI.match(/\/users\/local\//)) {
if (objectURI.match(/\/users\/local\//)) {
// For now, at least, don't check local id
/*
var localUserURI = this.getLocalUserURI();
if (localUserURI) {
localUserURI += "/";
if (itemURI.indexOf(localUserURI) == 0) {
itemURI = itemURI.substr(localUserURI.length);
if (objectURI.indexOf(localUserURI) == 0) {
objectURI = objectURI.substr(localUserURI.length);
var libraryType = 'user';
var id = null;
}
@ -178,29 +225,30 @@ Zotero.URI = new function () {
// If not found, try global URI
if (!libraryType) {
if (itemURI.indexOf(_baseURI) != 0) {
throw ("Invalid base URI '" + itemURI + "' in Zotero.URI.getURIItem()");
if (objectURI.indexOf(_baseURI) != 0) {
throw ("Invalid base URI '" + objectURI + "' in Zotero.URI._getURIObject()");
}
itemURI = itemURI.substr(_baseURI.length);
objectURI = objectURI.substr(_baseURI.length);
var typeRE = /^(users|groups)\/([0-9]+)\//;
var matches = itemURI.match(typeRE);
var matches = objectURI.match(typeRE);
if (!matches) {
throw ("Invalid library URI '" + itemURI + "' in Zotero.URI.getURIItem()");
throw ("Invalid library URI '" + objectURI + "' in Zotero.URI._getURIObject()");
}
var libraryType = matches[1].substr(0, matches[1].length-1);
var id = matches[2];
itemURI = itemURI.replace(typeRE, '');
objectURI = objectURI.replace(typeRE, '');
}
// TODO: itemID-based URI?
var matches = itemURI.match(/items\/([A-Z0-9]{8})/);
// TODO: objectID-based URI?
var re = new RegExp(types + "\/([A-Z0-9]{8})");
var matches = objectURI.match(re);
if (!matches) {
throw ("Invalid item URI '" + itemURI + "' in Zotero.URI.getURIItem()");
throw ("Invalid object URI '" + objectURI + "' in Zotero.URI._getURIObject()");
}
var itemKey = matches[1];
var objectKey = matches[1];
if (libraryType == 'user') {
return Zotero.Items.getByLibraryAndKey(null, itemKey);
return Zotero[Types].getByLibraryAndKey(null, objectKey);
}
if (libraryType == 'group') {
@ -208,7 +256,7 @@ Zotero.URI = new function () {
return false;
}
var libraryID = Zotero.Groups.getLibraryIDFromGroupID(id);
return Zotero.Items.getByLibraryAndKey(libraryID, itemKey);
return Zotero[Types].getByLibraryAndKey(libraryID, objectKey);
}
}
}