When clearing automatic tags, don't delete manual tags with same name

This commit is contained in:
Dan Stillman 2019-03-05 07:35:07 -05:00
parent fc43514ff0
commit d7dc5670d5
2 changed files with 128 additions and 97 deletions

View file

@ -281,14 +281,23 @@ Zotero.Tags = new function() {
/**
* @param {Integer} libraryID
* @param {Integer[]} tagIDs
* @param {Function} [onProgress]
* @param {Integer[]} [types]
* @return {Promise}
*/
this.removeFromLibrary = Zotero.Promise.coroutine(function* (libraryID, tagIDs, onProgress) {
this.removeFromLibrary = Zotero.Promise.coroutine(function* (libraryID, tagIDs, onProgress, types) {
var d = new Date();
tagIDs = Zotero.flattenArguments(tagIDs);
if (!Array.isArray(tagIDs)) {
tagIDs = [tagIDs];
}
if (types && !Array.isArray(types)) {
types = [types];
}
var deletedNames = [];
var colors = this.getColors(libraryID);
var done = 0;
yield Zotero.Utilities.Internal.forEachChunkAsync(
@ -296,90 +305,77 @@ Zotero.Tags = new function() {
100,
async function (chunk) {
await Zotero.DB.executeTransaction(function* () {
var oldItemIDs = [];
var notifierPairs = [];
var rowIDs = [];
var itemIDs = [];
var uniqueTags = new Set();
var notifierIDs = [];
var notifierData = {};
var a = new Date();
var sql = 'SELECT tagID, itemID FROM itemTags JOIN items USING (itemID) '
var sql = 'SELECT IT.ROWID AS rowID, tagID, itemID, type FROM itemTags IT '
+ 'JOIN items USING (itemID) '
+ 'WHERE libraryID=? AND tagID IN ('
+ Array(chunk.length).fill('?').join(', ')
+ ') ORDER BY tagID';
var chunkTagItems = yield Zotero.DB.queryAsync(sql, [libraryID, ...chunk]);
var i = 0;
chunk.sort((a, b) => a - b);
for (let tagID of chunk) {
+ ') ';
if (types) {
sql += 'AND type IN (' + types.join(', ') + ') ';
}
sql += 'ORDER BY tagID, type';
var rows = yield Zotero.DB.queryAsync(sql, [libraryID, ...chunk]);
for (let { rowID, tagID, itemID, type } of rows) {
uniqueTags.add(tagID);
let name = this.getName(tagID);
if (name === false) {
continue;
}
deletedNames.push(name);
// Since we're performing the DELETE query directly,
// get the list of items that will need their tags reloaded,
// and generate data for item-tag notifications
let itemIDs = []
while (i < chunkTagItems.length && chunkTagItems[i].tagID == tagID) {
itemIDs.push(chunkTagItems[i].itemID);
i++;
rowIDs.push(rowID);
itemIDs.push(itemID);
let ids = itemID + '-' + tagID;
notifierIDs.push(ids);
notifierData[ids] = {
libraryID: libraryID,
tag: name,
type
};
// If we're deleting the tag and not just a specific type, also clear any
// tag color
if (colors.has(name) && !types) {
yield this.setColor(libraryID, name, false);
}
for (let itemID of itemIDs) {
let pair = itemID + "-" + tagID;
notifierPairs.push(pair);
notifierData[pair] = {
libraryID: libraryID,
tag: name
};
}
oldItemIDs = oldItemIDs.concat(itemIDs);
}
if (oldItemIDs.length) {
Zotero.Notifier.queue('remove', 'item-tag', notifierPairs, notifierData);
if (itemIDs.length) {
Zotero.Notifier.queue('remove', 'item-tag', notifierIDs, notifierData);
}
var sql = "DELETE FROM itemTags WHERE tagID IN ("
+ Array(chunk.length).fill('?').join(', ') + ") AND itemID IN "
+ "(SELECT itemID FROM items WHERE libraryID=?)";
yield Zotero.DB.queryAsync(sql, chunk.concat([libraryID]));
sql = "DELETE FROM itemTags WHERE ROWID IN (" + rowIDs.join(", ") + ")";
yield Zotero.DB.queryAsync(sql);
yield this.purge(chunk);
// Update internal timestamps on all items that had these tags
yield Zotero.Utilities.Internal.forEachChunkAsync(
Zotero.Utilities.arrayUnique(oldItemIDs),
Zotero.Utilities.arrayUnique(itemIDs),
Zotero.DB.MAX_BOUND_PARAMETERS - 1,
Zotero.Promise.coroutine(function* (chunk2) {
var placeholders = Array(chunk2.length).fill('?').join(',');
async function (chunk) {
var sql = 'UPDATE items SET synced=0, clientDateModified=? '
+ 'WHERE itemID IN (' + Array(chunk.length).fill('?').join(',') + ')';
await Zotero.DB.queryAsync(sql, [Zotero.DB.transactionDateTime].concat(chunk));
sql = 'UPDATE items SET synced=0, clientDateModified=? '
+ 'WHERE itemID IN (' + placeholders + ')'
yield Zotero.DB.queryAsync(sql, [Zotero.DB.transactionDateTime].concat(chunk2));
yield Zotero.Items.reload(oldItemIDs, ['primaryData', 'tags'], true);
})
await Zotero.Items.reload(itemIDs, ['primaryData', 'tags'], true);
}
);
done += chunk.length;
if (onProgress) {
done += uniqueTags.size;
onProgress(done, tagIDs.length);
}
}.bind(this));
if (onProgress) {
onProgress(done, tagIDs.length);
}
}.bind(this)
);
// Also delete tag color setting
//
// Note that this isn't done in purge(), so the setting will not
// be removed if the tag is just removed from all items without
// being explicitly deleted.
for (let i=0; i<deletedNames.length; i++) {
yield this.setColor(libraryID, deletedNames[i], false);
}
Zotero.debug(`Removed ${tagIDs.length} ${Zotero.Utilities.pluralize(tagIDs.length, 'tag')} `
+ `in ${new Date() - d} ms`);
});
@ -429,6 +425,10 @@ Zotero.Tags = new function() {
tagIDs = Zotero.flattenArguments(tagIDs);
}
if (tagIDs && !tagIDs.length) {
return;
}
Zotero.DB.requireTransaction();
var sql;

View file

@ -39,60 +39,91 @@ describe("Zotero.Tags", function () {
});
describe("#removeFromLibrary()", function () {
it("should remove tags in given library", async function () {
it("should delete tags in given library", async function () {
var libraryID = Zotero.Libraries.userLibraryID;
var groupLibraryID = (await getGroup()).libraryID;
var tags = [];
var items = [];
await Zotero.DB.executeTransaction(async function () {
for (let i = 0; i < 10; i++) {
let tagName = Zotero.Utilities.randomString();
tags.push(tagName);
let item = createUnsavedDataObject('item', { tags: [tagName] });
await item.save();
items.push(item);
}
});
var item1 = await createDataObject('item', { tags: [{ tag: 'a' }, { tag: 'b', type: 1 }] });
var item2 = await createDataObject('item', { tags: [{ tag: 'b' }, { tag: 'c', type: 1 }] });
var item3 = await createDataObject('item', { tags: [{ tag: 'd', type: 1 }] });
var item4 = await createDataObject('item', { libraryID: groupLibraryID, tags: [{ tag: 'a' }, { tag: 'b', type: 1 }] });
var groupTagName = tags[0];
var groupItem = await createDataObject(
'item',
{
libraryID: groupLibraryID,
tags: [groupTagName]
}
);
var tagIDs = tags.map(tag => Zotero.Tags.getID(tag));
var tagIDs = ['a', 'd'].map(x => Zotero.Tags.getID(x));
await Zotero.Tags.removeFromLibrary(libraryID, tagIDs);
items.forEach(item => assert.lengthOf(item.getTags(), 0));
// Group item should still have the tag
assert.sameDeepMembers(groupItem.getTags(), [{ tag: groupTagName }]);
assert.sameDeepMembers(item1.getTags(), [{ tag: 'b', type: 1 }]);
assert.sameDeepMembers(item2.getTags(), [{ tag: 'b' }, { tag: 'c', type: 1 }]);
assert.lengthOf(item3.getTags(), 0);
assert.equal(Zotero.Tags.getID('a'), tagIDs[0]);
assert.isFalse(Zotero.Tags.getID('d'));
// Group item should still have all tags
assert.sameDeepMembers(item4.getTags(), [{ tag: 'a' }, { tag: 'b', type: 1 }]);
assert.equal(
await Zotero.DB.valueQueryAsync(
"SELECT COUNT(*) FROM itemTags WHERE itemID=?",
groupItem.id
item4.id
),
1
2
);
});
it("should reload tags of associated items", function* () {
it("should remove tags of a given type", async function () {
var libraryID = Zotero.Libraries.userLibraryID;
var groupLibraryID = (await getGroup()).libraryID;
var item1 = await createDataObject('item', { tags: [{ tag: 'a' }, { tag: 'b', type: 1 }] });
var item2 = await createDataObject('item', { tags: [{ tag: 'b' }, { tag: 'c', type: 1 }] });
var item3 = await createDataObject('item', { tags: [{ tag: 'd', type: 1 }] });
var item4 = await createDataObject('item', { libraryID: groupLibraryID, tags: [{ tag: 'a' }, { tag: 'b', type: 1 }] });
var tagIDs = ['a', 'b', 'c', 'd'].map(x => Zotero.Tags.getID(x));
var tagType = 1;
await Zotero.Tags.removeFromLibrary(libraryID, tagIDs, null, tagType);
assert.sameDeepMembers(item1.getTags(), [{ tag: 'a' }]);
assert.sameDeepMembers(item2.getTags(), [{ tag: 'b' }]);
assert.lengthOf(item3.getTags(), 0);
assert.isFalse(Zotero.Tags.getID('d'));
// Group item should still have all tags
assert.sameDeepMembers(item4.getTags(), [{ tag: 'a' }, { tag: 'b', type: 1 }]);
assert.equal(
await Zotero.DB.valueQueryAsync(
"SELECT COUNT(*) FROM itemTags WHERE itemID=?",
item4.id
),
2
);
});
it("should delete colored tag when removing tag", async function () {
var libraryID = Zotero.Libraries.userLibraryID;
var tagName = Zotero.Utilities.randomString();
var item = createUnsavedDataObject('item');
item.addTag(tagName);
yield item.saveTx();
assert.lengthOf(item.getTags(), 1);
var tag = Zotero.Utilities.randomString();
var item = await createDataObject('item', { tags: [{ tag: tag, type: 1 }] });
await Zotero.Tags.setColor(libraryID, tag, '#ABCDEF', 0);
await Zotero.Tags.removeFromLibrary(libraryID, [Zotero.Tags.getID(tag)]);
var tagID = Zotero.Tags.getID(tagName);
yield Zotero.Tags.removeFromLibrary(libraryID, tagID);
assert.lengthOf(item.getTags(), 0);
})
assert.isFalse(Zotero.Tags.getColor(libraryID, tag));
});
it("shouldn't delete colored tag when removing tag of a given type", async function () {
var libraryID = Zotero.Libraries.userLibraryID;
var tag = Zotero.Utilities.randomString();
var item = await createDataObject('item', { tags: [{ tag: tag, type: 1 }] });
await Zotero.Tags.setColor(libraryID, tag, '#ABCDEF', 0);
await Zotero.Tags.removeFromLibrary(libraryID, [Zotero.Tags.getID(tag)], null, 1);
assert.lengthOf(item.getTags(), 0);
assert.ok(Zotero.Tags.getColor(libraryID, tag));
});
})
describe("#purge()", function () {