Add Director fallback to firstCreator for Video Recording items

The firstCreator/sortCreator SQL now falls back through primary →
editor → director → contributor. This ensures Video Recording items
with only a Director creator show that creator in the items list.

Also refactors the repetitive SQL-building in _getFirstCreatorSQL()
and _getSortCreatorSQL() into helper functions.

Closes #5720
This commit is contained in:
Dan Stillman 2026-03-09 10:17:30 -04:00
parent f9a10e1874
commit 2f6825027a
2 changed files with 111 additions and 237 deletions

View file

@ -1260,6 +1260,9 @@ Zotero.Items = function () {
var validCreatorTypes = [
Zotero.CreatorTypes.getPrimaryIDForType(itemTypeID),
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')
];
@ -1395,120 +1398,54 @@ Zotero.Items = function () {
if (_firstCreatorSQL) {
return _firstCreatorSQL;
}
var editorCreatorTypeID = Zotero.CreatorTypes.getID('editor');
var contributorCreatorTypeID = Zotero.CreatorTypes.getID('contributor');
/* This whole block is to get the firstCreator */
var localizedAnd = Zotero.getString('general.andJoiner').replace(/%S/g, '%s');
var localizedEtAl = Zotero.getString('general.etAl');
var sql = "COALESCE(" +
// First try for primary creator types
"CASE (" +
"SELECT COUNT(*) FROM itemCreators IC " +
"LEFT JOIN itemTypeCreatorTypes ITCT " +
"ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " +
"WHERE itemID=O.itemID AND primaryField=1" +
") " +
"WHEN 0 THEN NULL " +
"WHEN 1 THEN (" +
"SELECT lastName FROM itemCreators IC NATURAL JOIN creators " +
"LEFT JOIN itemTypeCreatorTypes ITCT " +
"ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " +
"WHERE itemID=O.itemID AND primaryField=1" +
") " +
"WHEN 2 THEN (" +
"SELECT PRINTF(" +
`'${localizedAnd}'` +
", " +
// \u2068 FIRST STRONG ISOLATE: Isolates the directionality of characters that follow
// \u2069 POP DIRECTIONAL ISOLATE: Pops the above isolation
"(SELECT '\u2068' || lastName || '\u2069' FROM itemCreators IC NATURAL JOIN creators " +
"LEFT JOIN itemTypeCreatorTypes ITCT " +
"ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " +
"WHERE itemID=O.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1)" +
", " +
"(SELECT '\u2068' || lastName || '\u2069' FROM itemCreators IC NATURAL JOIN creators " +
"LEFT JOIN itemTypeCreatorTypes ITCT " +
"ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " +
"WHERE itemID=O.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1,1)" +
")" +
") " +
"ELSE (" +
"SELECT " +
"(SELECT lastName FROM itemCreators IC NATURAL JOIN creators " +
"LEFT JOIN itemTypeCreatorTypes ITCT " +
"ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " +
"WHERE itemID=O.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1)" +
" || ' " + localizedEtAl + "' " +
") " +
"END, " +
// Then try editors
"CASE (" +
"SELECT COUNT(*) FROM itemCreators " +
`WHERE itemID=O.itemID AND creatorTypeID=${editorCreatorTypeID}` +
") " +
"WHEN 0 THEN NULL " +
"WHEN 1 THEN (" +
"SELECT lastName FROM itemCreators NATURAL JOIN creators " +
`WHERE itemID=O.itemID AND creatorTypeID=${editorCreatorTypeID}` +
") " +
"WHEN 2 THEN (" +
"SELECT PRINTF(" +
`'${localizedAnd}'` +
", " +
"(SELECT '\u2068' || lastName || '\u2069' FROM itemCreators NATURAL JOIN creators " +
`WHERE itemID=O.itemID AND creatorTypeID=${editorCreatorTypeID} ` +
"ORDER BY orderIndex LIMIT 1)" +
", " +
"(SELECT '\u2068' || lastName || '\u2069' FROM itemCreators NATURAL JOIN creators " +
`WHERE itemID=O.itemID AND creatorTypeID=${editorCreatorTypeID} ` +
"ORDER BY orderIndex LIMIT 1,1) " +
")" +
") " +
"ELSE (" +
"SELECT " +
"(SELECT lastName FROM itemCreators NATURAL JOIN creators " +
`WHERE itemID=O.itemID AND creatorTypeID=${editorCreatorTypeID} ` +
"ORDER BY orderIndex LIMIT 1)" +
" || ' " + localizedEtAl + "' " +
") " +
"END, " +
// Then try contributors
"CASE (" +
"SELECT COUNT(*) FROM itemCreators " +
`WHERE itemID=O.itemID AND creatorTypeID=${contributorCreatorTypeID}` +
") " +
"WHEN 0 THEN NULL " +
"WHEN 1 THEN (" +
"SELECT lastName FROM itemCreators NATURAL JOIN creators " +
`WHERE itemID=O.itemID AND creatorTypeID=${contributorCreatorTypeID}` +
") " +
"WHEN 2 THEN (" +
"SELECT PRINTF(" +
`'${localizedAnd}'` +
", " +
"(SELECT '\u2068' || lastName || '\u2069' FROM itemCreators NATURAL JOIN creators " +
`WHERE itemID=O.itemID AND creatorTypeID=${contributorCreatorTypeID} ` +
"ORDER BY orderIndex LIMIT 1)" +
", " +
"(SELECT '\u2068' || lastName || '\u2069' FROM itemCreators NATURAL JOIN creators " +
`WHERE itemID=O.itemID AND creatorTypeID=${contributorCreatorTypeID} ` +
"ORDER BY orderIndex LIMIT 1,1) " +
")" +
") " +
"ELSE (" +
"SELECT " +
"(SELECT lastName FROM itemCreators NATURAL JOIN creators " +
`WHERE itemID=O.itemID AND creatorTypeID=${contributorCreatorTypeID} ` +
"ORDER BY orderIndex LIMIT 1)" +
" || ' " + localizedEtAl + "' " +
") " +
"END" +
") AS firstCreator";
// Generate a CASE block that returns the firstCreator display string
// for creators matching the given WHERE clause
function caseBlock(where) {
return "CASE ("
+ `SELECT COUNT(*) FROM itemCreators IC ${where}`
+ ") "
+ "WHEN 0 THEN NULL "
+ "WHEN 1 THEN ("
+ `SELECT lastName FROM itemCreators IC NATURAL JOIN creators ${where}`
+ ") "
+ "WHEN 2 THEN ("
+ "SELECT PRINTF("
+ `'${localizedAnd}'`
+ ", "
// \u2068 FIRST STRONG ISOLATE / \u2069 POP DIRECTIONAL ISOLATE
+ `(SELECT '\u2068' || lastName || '\u2069' FROM itemCreators IC NATURAL JOIN creators ${where} ORDER BY orderIndex LIMIT 1)`
+ ", "
+ `(SELECT '\u2068' || lastName || '\u2069' FROM itemCreators IC NATURAL JOIN creators ${where} ORDER BY orderIndex LIMIT 1,1) `
+ ")"
+ ") "
+ "ELSE ("
+ "SELECT "
+ `(SELECT lastName FROM itemCreators IC NATURAL JOIN creators ${where} ORDER BY orderIndex LIMIT 1)`
+ " || ' " + localizedEtAl + "' "
+ ") "
+ "END";
}
let primaryJoin = "LEFT JOIN itemTypeCreatorTypes ITCT "
+ "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) "
+ "WHERE itemID=O.itemID AND primaryField=1";
function creatorTypeWhere(typeName) {
return `WHERE itemID=O.itemID AND creatorTypeID=${Zotero.CreatorTypes.getID(typeName)}`;
}
let sql = "COALESCE("
+ caseBlock(primaryJoin) + ", "
+ caseBlock(creatorTypeWhere('editor')) + ", "
// Director used to be the primary type for Video Recording before
// Creator (mapped to Author) was added
+ caseBlock(creatorTypeWhere('director')) + ", "
+ caseBlock(creatorTypeWhere('contributor'))
+ ") AS firstCreator";
_firstCreatorSQL = sql;
return sql;
}
@ -1522,131 +1459,52 @@ Zotero.Items = function () {
if (_sortCreatorSQL) {
return _sortCreatorSQL;
}
var editorCreatorTypeID = Zotero.CreatorTypes.getID('editor');
var contributorCreatorTypeID = Zotero.CreatorTypes.getID('contributor');
var nameSQL = "lastName || ' ' || firstName ";
var sql = "COALESCE("
// First try for primary creator types
+ "CASE (" +
"SELECT COUNT(*) FROM itemCreators IC " +
"LEFT JOIN itemTypeCreatorTypes ITCT " +
"ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " +
"WHERE itemID=O.itemID AND primaryField=1" +
") " +
"WHEN 0 THEN NULL " +
"WHEN 1 THEN (" +
"SELECT " + nameSQL + "FROM itemCreators IC NATURAL JOIN creators " +
"LEFT JOIN itemTypeCreatorTypes ITCT " +
"ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " +
"WHERE itemID=O.itemID AND primaryField=1" +
") " +
"WHEN 2 THEN (" +
"SELECT " +
"(SELECT " + nameSQL + " FROM itemCreators IC NATURAL JOIN creators " +
"LEFT JOIN itemTypeCreatorTypes ITCT " +
"ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " +
"WHERE itemID=O.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1)" +
" || ' ' || " +
"(SELECT " + nameSQL + " FROM itemCreators IC NATURAL JOIN creators " +
"LEFT JOIN itemTypeCreatorTypes ITCT " +
"ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " +
"WHERE itemID=O.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1,1)" +
") " +
"ELSE (" +
"SELECT " +
"(SELECT " + nameSQL + " FROM itemCreators IC NATURAL JOIN creators " +
"LEFT JOIN itemTypeCreatorTypes ITCT " +
"ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " +
"WHERE itemID=O.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1)" +
" || ' ' || " +
"(SELECT " + nameSQL + " FROM itemCreators IC NATURAL JOIN creators " +
"LEFT JOIN itemTypeCreatorTypes ITCT " +
"ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " +
"WHERE itemID=O.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1,1)" +
" || ' ' || " +
"(SELECT " + nameSQL + " FROM itemCreators IC NATURAL JOIN creators " +
"LEFT JOIN itemTypeCreatorTypes ITCT " +
"ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " +
"WHERE itemID=O.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 2,1)" +
") "
+ "END, "
// Then try editors
+ "CASE ("
+ "SELECT COUNT(*) FROM itemCreators "
+ `WHERE itemID=O.itemID AND creatorTypeID=${editorCreatorTypeID}`
+ ") "
+ "WHEN 0 THEN NULL "
+ "WHEN 1 THEN ("
+ "SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators "
+ `WHERE itemID=O.itemID AND creatorTypeID=${editorCreatorTypeID}`
+ ") "
+ "WHEN 2 THEN ("
+ "SELECT "
+ "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators "
+ `WHERE itemID=O.itemID AND creatorTypeID=${editorCreatorTypeID} `
+ "ORDER BY orderIndex LIMIT 1)"
+ " || ' ' || "
+ "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators "
+ `WHERE itemID=O.itemID AND creatorTypeID=${editorCreatorTypeID} `
+ "ORDER BY orderIndex LIMIT 1,1) "
+ ") "
+ "ELSE ("
+ "SELECT "
+ "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators "
+ `WHERE itemID=O.itemID AND creatorTypeID=${editorCreatorTypeID} `
+ "ORDER BY orderIndex LIMIT 1)"
+ " || ' ' || "
+ "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators "
+ `WHERE itemID=O.itemID AND creatorTypeID=${editorCreatorTypeID} `
+ "ORDER BY orderIndex LIMIT 1,1)"
+ " || ' ' || "
+ "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators "
+ `WHERE itemID=O.itemID AND creatorTypeID=${editorCreatorTypeID} `
+ "ORDER BY orderIndex LIMIT 2,1)"
+ ") "
+ "END, "
// Then try contributors
+ "CASE ("
+ "SELECT COUNT(*) FROM itemCreators "
+ `WHERE itemID=O.itemID AND creatorTypeID=${contributorCreatorTypeID}`
+ ") "
+ "WHEN 0 THEN NULL "
+ "WHEN 1 THEN ("
+ "SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators "
+ `WHERE itemID=O.itemID AND creatorTypeID=${contributorCreatorTypeID}`
+ ") "
+ "WHEN 2 THEN ("
+ "SELECT "
+ "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators "
+ `WHERE itemID=O.itemID AND creatorTypeID=${contributorCreatorTypeID} `
+ "ORDER BY orderIndex LIMIT 1)"
+ " || ' ' || "
+ "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators "
+ `WHERE itemID=O.itemID AND creatorTypeID=${contributorCreatorTypeID} `
+ "ORDER BY orderIndex LIMIT 1,1) "
+ ") "
+ "ELSE ("
+ "SELECT "
+ "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators "
+ `WHERE itemID=O.itemID AND creatorTypeID=${contributorCreatorTypeID} `
+ "ORDER BY orderIndex LIMIT 1)"
+ " || ' ' || "
+ "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators "
+ `WHERE itemID=O.itemID AND creatorTypeID=${contributorCreatorTypeID} `
+ "ORDER BY orderIndex LIMIT 1,1)"
+ " || ' ' || "
+ "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators "
+ `WHERE itemID=O.itemID AND creatorTypeID=${contributorCreatorTypeID} `
+ "ORDER BY orderIndex LIMIT 2,1)"
+ ") "
+ "END"
+ ") AS sortCreator";
let nameSQL = "lastName || ' ' || firstName ";
// Generate a CASE block that returns sortCreator strings (full names
// space-concatenated) for creators matching the given WHERE clause
function caseBlock(where) {
return "CASE ("
+ `SELECT COUNT(*) FROM itemCreators IC ${where}`
+ ") "
+ "WHEN 0 THEN NULL "
+ "WHEN 1 THEN ("
+ `SELECT ${nameSQL}FROM itemCreators IC NATURAL JOIN creators ${where}`
+ ") "
+ "WHEN 2 THEN ("
+ "SELECT "
+ `(SELECT ${nameSQL}FROM itemCreators IC NATURAL JOIN creators ${where} ORDER BY orderIndex LIMIT 1)`
+ " || ' ' || "
+ `(SELECT ${nameSQL}FROM itemCreators IC NATURAL JOIN creators ${where} ORDER BY orderIndex LIMIT 1,1)`
+ ") "
+ "ELSE ("
+ "SELECT "
+ `(SELECT ${nameSQL}FROM itemCreators IC NATURAL JOIN creators ${where} ORDER BY orderIndex LIMIT 1)`
+ " || ' ' || "
+ `(SELECT ${nameSQL}FROM itemCreators IC NATURAL JOIN creators ${where} ORDER BY orderIndex LIMIT 1,1)`
+ " || ' ' || "
+ `(SELECT ${nameSQL}FROM itemCreators IC NATURAL JOIN creators ${where} ORDER BY orderIndex LIMIT 2,1)`
+ ") "
+ "END";
}
let primaryJoin = "LEFT JOIN itemTypeCreatorTypes ITCT "
+ "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) "
+ "WHERE itemID=O.itemID AND primaryField=1";
function creatorTypeWhere(typeName) {
return `WHERE itemID=O.itemID AND creatorTypeID=${Zotero.CreatorTypes.getID(typeName)}`;
}
let sql = "COALESCE("
+ caseBlock(primaryJoin) + ", "
+ caseBlock(creatorTypeWhere('editor')) + ", "
// Director used to be the primary type for Video Recording before
// Creator (mapped to Author) was added
+ caseBlock(creatorTypeWhere('director')) + ", "
+ caseBlock(creatorTypeWhere('contributor'))
+ ") AS sortCreator";
_sortCreatorSQL = sql;
return sql;
}

View file

@ -55,6 +55,22 @@ describe("Zotero.Item", function () {
Zotero.getString('general.andJoiner', ['\u2068B\u2069', '\u2068D\u2069'])
);
});
// https://github.com/zotero/zotero/issues/5720
it("should return director as firstCreator for a videoRecording without a primary creator", async function () {
var item = createUnsavedDataObject('item', { itemType: 'videoRecording' });
item.setCreators([
{
firstName: "A",
lastName: "B",
creatorType: "director"
}
]);
assert.equal(item.getField('firstCreator'), "B");
await item.saveTx();
assert.equal(item.getField('firstCreator'), "B");
});
it("should strip bidi isolates from firstCreator when unformatted = true", async function () {
var item = createUnsavedDataObject('item');