diff --git a/chrome/content/zotero/elements/itemBox.js b/chrome/content/zotero/elements/itemBox.js index 41a99925a7..b87f7c392e 100644 --- a/chrome/content/zotero/elements/itemBox.js +++ b/chrome/content/zotero/elements/itemBox.js @@ -1999,8 +1999,7 @@ let tooltipText; if (fieldID && !this._extraItems.length) { // Display the SQL date as a tooltip for date fields - // TEMP - filingDate - if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date') || fieldName == 'filingDate') { + if (Zotero.ItemFields.isDate(fieldID)) { tooltipText = Zotero.Date.multipartToSQL(this.item.getField(fieldName, true)); } } @@ -2549,9 +2548,9 @@ break; default: - // TODO: generalize to all date rows/fields - if (Zotero.ItemFields.isFieldOfBase(fieldName, 'date')) { - // Parse 'yesterday'/'today'/'tomorrow' + // Parse 'yesterday'/'today'/'tomorrow' for date fields -- not applied to Original Date + if (Zotero.ItemFields.isDate(fieldName) + && !Zotero.ItemFields.isFieldOfBase(fieldName, 'originalDate')) { value = Zotero.Date.parseDescriptiveString(value); } } diff --git a/chrome/content/zotero/xpcom/data/itemFields.js b/chrome/content/zotero/xpcom/data/itemFields.js index d1951793af..a9e16e1675 100644 --- a/chrome/content/zotero/xpcom/data/itemFields.js +++ b/chrome/content/zotero/xpcom/data/itemFields.js @@ -190,11 +190,16 @@ Zotero.ItemFields = new function () { this.isDate = function (field) { var fieldID = this.getID(field); var fieldName = this.getName(field); - if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date')) { - return true; - } if (Zotero.Schema.globalSchemaMeta.fields[fieldName]) { - return Zotero.Schema.globalSchemaMeta.fields[fieldName].type == 'date' + return Zotero.Schema.globalSchemaMeta.fields[fieldName].type == 'date'; + } + // A type-specific field has the type of the base field it maps to + for (let baseFieldID in _typeFieldIDsByBase) { + if (_typeFieldIDsByBase[baseFieldID].includes(fieldID)) { + let baseName = this.getName(parseInt(baseFieldID)); + let baseMeta = Zotero.Schema.globalSchemaMeta.fields[baseName]; + return !!baseMeta && baseMeta.type == 'date'; + } } return false; }; diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js index edce44e695..523225dd06 100644 --- a/chrome/content/zotero/xpcom/schema.js +++ b/chrome/content/zotero/xpcom/schema.js @@ -587,7 +587,40 @@ Zotero.Schema = new function () { + baseFieldMappingsValueSets.join(", ")); await Zotero.DB.queryAsync("INSERT INTO itemTypeCreatorTypes VALUES " + itemTypeCreatorTypesValueSets.join(", ")); - + + // Convert stored values of date-type fields to multipart dates, for + // values saved while a field was a text field + var dateFieldIDs = new Set(); + for (let [fieldName, meta] of Object.entries(data.meta?.fields || {})) { + if (meta.type != 'date') { + continue; + } + let fieldID = postFieldIDsByName.get(fieldName); + if (!fieldID) { + continue; + } + dateFieldIDs.add(fieldID); + // Type-specific fields mapped to a date-type base field + let mappedIDs = await Zotero.DB.columnQueryAsync( + "SELECT DISTINCT fieldID FROM baseFieldMappings WHERE baseFieldID=?", + fieldID + ); + for (let id of mappedIDs) { + dateFieldIDs.add(id); + } + } + if (dateFieldIDs.size) { + // strToMultipart() needs the month strings, which aren't loaded + // yet this early in startup + Zotero.Date.init(); + await _updateItemDataValues( + [...dateFieldIDs], + value => (Zotero.Date.isMultipart(value) + ? false + : Zotero.Date.strToMultipart(value)) + ); + } + // Store data in DB as compressed binary string. This lets us use a schema that matches the // DB tables even if the user downgrades to a version with an earlier bundled schema file. var pako = require('pako'); @@ -630,6 +663,51 @@ Zotero.Schema = new function () { await Zotero.DB.queryAsync("PRAGMA foreign_keys=ON"); } }; + + + /** + * Rewrite stored itemData values through a transform, reusing or creating + * rows in itemDataValues + * + * @param {Number[]} fieldIDs - Fields whose values to rewrite + * @param {Function} transform - Given a value, returns the new value, or + * false to leave the value as it is + */ + async function _updateItemDataValues(fieldIDs, transform) { + if (!fieldIDs.length) { + return; + } + var rows = await Zotero.DB.queryAsync( + "SELECT itemID, fieldID, value FROM itemData " + + "JOIN itemDataValues USING (valueID) " + + "WHERE fieldID IN (" + fieldIDs.join(", ") + ")" + ); + for (let row of rows) { + let value = transform(row.value); + if (value === false || value === row.value) { + continue; + } + let valueID = await Zotero.DB.valueQueryAsync( + "SELECT valueID FROM itemDataValues WHERE value=?", value + ); + if (!valueID) { + valueID = Zotero.ID.get('itemDataValues'); + await Zotero.DB.queryAsync( + "INSERT INTO itemDataValues (valueID, value, valueNormalized) " + + "VALUES (?, ?, ?)", + [ + valueID, + value, + Zotero.Utilities.Internal.normalizeForSearchStorage(value) + ] + ); + } + await Zotero.DB.queryAsync( + "UPDATE itemData SET valueID=? WHERE itemID=? AND fieldID=?", + [valueID, row.itemID, row.fieldID] + ); + } + } diff --git a/resource/schema/global b/resource/schema/global index 70c3aa9862..b86c79b564 160000 --- a/resource/schema/global +++ b/resource/schema/global @@ -1 +1 @@ -Subproject commit 70c3aa98627413d6a30dca955886eafbde085ce9 +Subproject commit b86c79b56479cadac3288e1f122cf34ef04e8809 diff --git a/test/tests/itemFieldsTest.js b/test/tests/itemFieldsTest.js index 143e6f9efc..5ea3e09f09 100644 --- a/test/tests/itemFieldsTest.js +++ b/test/tests/itemFieldsTest.js @@ -45,6 +45,16 @@ describe("Zotero.ItemFields", function () { }); }); + describe("#isDate()", function () { + it("should treat base and mapped date-type fields as dates", function () { + assert.isTrue(Zotero.ItemFields.isDate('date')); + assert.isTrue(Zotero.ItemFields.isDate('dateDecided')); + assert.isTrue(Zotero.ItemFields.isDate('originalDate')); + assert.isTrue(Zotero.ItemFields.isDate('priorityDate')); + assert.isFalse(Zotero.ItemFields.isDate('title')); + }); + }); + describe("#getDirection()", function () { it("should follow app locale for primary field", function () { assert.equal(Zotero.ItemFields.getDirection('book', 'dateAdded', ''), Zotero.dir) diff --git a/test/tests/schemaTest.js b/test/tests/schemaTest.js index c4a59392bc..d9083018e9 100644 --- a/test/tests/schemaTest.js +++ b/test/tests/schemaTest.js @@ -94,6 +94,42 @@ describe("Zotero.Schema", function () { }); }); + describe("date-type field conversion", function () { + it("should convert stored text values of date-type fields to multipart dates", async function () { + var item = await createDataObject('item', { itemType: 'patent' }); + // A value stored while the field was a text field + var fieldID = Zotero.ItemFields.getID('priorityDate'); + var valueID = Zotero.ID.get('itemDataValues'); + await Zotero.DB.queryAsync( + "INSERT INTO itemDataValues (valueID, value, valueNormalized) VALUES (?, ?, ?)", + [valueID, '1969-04-20', null] + ); + await Zotero.DB.queryAsync( + "INSERT INTO itemData VALUES (?, ?, ?)", + [item.id, fieldID, valueID] + ); + + schema.version++; + await Zotero.Schema._updateGlobalSchemaForTest(schema); + await item.reload(['itemData'], true); + + var stored = await Zotero.DB.valueQueryAsync( + "SELECT value FROM itemData JOIN itemDataValues USING (valueID) " + + "WHERE itemID=? AND fieldID=?", + [item.id, fieldID] + ); + assert.isTrue(Zotero.Date.isMultipart(stored)); + assert.equal(item.getField('priorityDate'), '1969-04-20'); + + // The converted value matches a date search on the base field + var s = new Zotero.Search(); + s.libraryID = item.libraryID; + s.addCondition('originalDate', 'isBefore', '1970'); + var matches = await s.search(); + assert.include(matches, item.id); + }); + }); + describe("#migrateExtraFields()", function () { async function migrate(options) { schema.version++;