From 83f20914fab28bddd37e92176a8d8bd675523caa Mon Sep 17 00:00:00 2001 From: Abe Jellinek <1770299+AbeJellinek@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:17:13 -0400 Subject: [PATCH] Add composer and wordsBy to firstCreator And show alongside performer (the primary creator type for audioRecording). --- chrome/content/zotero/xpcom/data/items.js | 25 ++-- test/tests/itemTest.js | 139 ++++++++++++++++++++++ 2 files changed, 156 insertions(+), 8 deletions(-) diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js index dd35b89a53..2b45cd314d 100644 --- a/chrome/content/zotero/xpcom/data/items.js +++ b/chrome/content/zotero/xpcom/data/items.js @@ -1409,17 +1409,22 @@ Zotero.Items = function () { return ""; } + // Groups of creator types that are shown together, in order of precedence var validCreatorTypes = [ - Zotero.CreatorTypes.getPrimaryIDForType(itemTypeID), - Zotero.CreatorTypes.getID('editor'), + [ + Zotero.CreatorTypes.getPrimaryIDForType(itemTypeID), + Zotero.CreatorTypes.getID('composer'), + Zotero.CreatorTypes.getID('wordsBy') + ], + [Zotero.CreatorTypes.getID('editor')], // Director used to be the primary type for Video Recording before // Creator (mapped to Author) was added - Zotero.CreatorTypes.getID('director'), - Zotero.CreatorTypes.getID('contributor') + [Zotero.CreatorTypes.getID('director')], + [Zotero.CreatorTypes.getID('contributor')], ]; - for (let creatorTypeID of validCreatorTypes) { - let matches = creatorsData.filter(data => data.creatorTypeID == creatorTypeID) + for (let creatorTypeIDs of validCreatorTypes) { + let matches = creatorsData.filter(data => creatorTypeIDs.includes(data.creatorTypeID)); if (!matches.length) { continue; } @@ -1584,7 +1589,9 @@ Zotero.Items = function () { let primaryJoin = "LEFT JOIN itemTypeCreatorTypes ITCT " + "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " - + "WHERE itemID=O.itemID AND primaryField=1"; + // composer and wordsBy are shown alongside primary creators (e.g., performers) + + "WHERE itemID=O.itemID AND (primaryField=1 " + + `OR IC.creatorTypeID IN (${Zotero.CreatorTypes.getID('composer')}, ${Zotero.CreatorTypes.getID('wordsBy')}))`; function creatorTypeWhere(typeName) { return `WHERE itemID=O.itemID AND creatorTypeID=${Zotero.CreatorTypes.getID(typeName)}`; } @@ -1643,7 +1650,9 @@ Zotero.Items = function () { let primaryJoin = "LEFT JOIN itemTypeCreatorTypes ITCT " + "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " - + "WHERE itemID=O.itemID AND primaryField=1"; + // composer and wordsBy are shown alongside primary creators (e.g., performers) + + "WHERE itemID=O.itemID AND (primaryField=1 " + + `OR IC.creatorTypeID IN (${Zotero.CreatorTypes.getID('composer')}, ${Zotero.CreatorTypes.getID('wordsBy')}))`; function creatorTypeWhere(typeName) { return `WHERE itemID=O.itemID AND creatorTypeID=${Zotero.CreatorTypes.getID(typeName)}`; } diff --git a/test/tests/itemTest.js b/test/tests/itemTest.js index 7be0af6367..a660857d8a 100644 --- a/test/tests/itemTest.js +++ b/test/tests/itemTest.js @@ -72,6 +72,145 @@ describe("Zotero.Item", function () { assert.equal(item.getField('firstCreator'), "B"); }); + it("should return composer as firstCreator for an audioRecording without a primary creator", async function () { + var item = createUnsavedDataObject('item', { itemType: 'audioRecording' }); + item.setCreators([ + { + firstName: "A", + lastName: "B", + creatorType: "translator" + }, + { + firstName: "C", + lastName: "D", + creatorType: "composer" + } + ]); + assert.equal(item.getField('firstCreator'), "D"); + + await item.saveTx(); + assert.equal(item.getField('firstCreator'), "D"); + assert.equal(item.sortCreator, "D C"); + }); + + it("should return wordsBy as firstCreator for an audioRecording without a primary creator", async function () { + var item = createUnsavedDataObject('item', { itemType: 'audioRecording' }); + item.setCreators([ + { + firstName: "A", + lastName: "B", + creatorType: "translator" + }, + { + firstName: "C", + lastName: "D", + creatorType: "wordsBy" + }, + { + firstName: "E", + lastName: "F", + creatorType: "wordsBy" + } + ]); + var expected = Zotero.getString('general.andJoiner', ['⁨D⁩', '⁨F⁩']); + assert.equal(item.getField('firstCreator'), expected); + + await item.saveTx(); + assert.equal(item.getField('firstCreator'), expected); + assert.equal(item.sortCreator, "D C F E"); + }); + + it("should show composer and wordsBy alongside performer for an audioRecording", async function () { + var item = createUnsavedDataObject('item', { itemType: 'audioRecording' }); + item.setCreators([ + { + firstName: "A", + lastName: "B", + creatorType: "performer" + }, + { + firstName: "C", + lastName: "D", + creatorType: "translator" + }, + { + firstName: "E", + lastName: "F", + creatorType: "composer" + } + ]); + var expected = Zotero.getString('general.andJoiner', ['⁨B⁩', '⁨F⁩']); + assert.equal(item.getField('firstCreator'), expected); + + await item.saveTx(); + assert.equal(item.getField('firstCreator'), expected); + assert.equal(item.sortCreator, "B A F E"); + + item.setCreators([ + ...item.getCreators(), + { + firstName: "G", + lastName: "H", + creatorType: "wordsBy" + } + ]); + expected = "B " + Zotero.getString('general.etAl'); + assert.equal(Zotero.Items.getFirstCreatorFromData(item.itemTypeID, item.getCreators()), expected); + + await item.saveTx(); + assert.equal(item.getField('firstCreator'), expected); + assert.equal(item.sortCreator, "B A F E H G"); + }); + + it("should show wordsBy alongside performer for an audioRecording", async function () { + var item = createUnsavedDataObject('item', { itemType: 'audioRecording' }); + item.setCreators([ + { + firstName: "A", + lastName: "B", + creatorType: "wordsBy" + }, + { + firstName: "C", + lastName: "D", + creatorType: "performer" + } + ]); + var expected = Zotero.getString('general.andJoiner', ['⁨B⁩', '⁨D⁩']); + assert.equal(item.getField('firstCreator'), expected); + + await item.saveTx(); + assert.equal(item.getField('firstCreator'), expected); + assert.equal(item.sortCreator, "B A D C"); + }); + + it("should prefer composer and wordsBy over contributor", async function () { + var item = createUnsavedDataObject('item', { itemType: 'audioRecording' }); + item.setCreators([ + { + firstName: "A", + lastName: "B", + creatorType: "wordsBy" + }, + { + firstName: "C", + lastName: "D", + creatorType: "contributor" + }, + { + firstName: "E", + lastName: "F", + creatorType: "composer" + } + ]); + var expected = Zotero.getString('general.andJoiner', ['⁨B⁩', '⁨F⁩']); + assert.equal(item.getField('firstCreator'), expected); + + await item.saveTx(); + assert.equal(item.getField('firstCreator'), expected); + assert.equal(item.sortCreator, "B A F E"); + }); + it("should strip bidi isolates from firstCreator when unformatted = true", async function () { var item = createUnsavedDataObject('item'); item.setCreators([