From c89590c7b74fc062cbd4843adadb36fa1603cf60 Mon Sep 17 00:00:00 2001
From: windingwind <33902321+windingwind@users.noreply.github.com>
Date: Tue, 25 Jul 2023 12:47:12 +0800
Subject: [PATCH] Add ItemTree column API (#3186)
---
chrome/content/zotero/advancedSearch.js | 6 +-
.../zotero/components/virtualized-table.jsx | 24 +-
chrome/content/zotero/elements/relatedBox.js | 2 +-
.../zotero/integration/addCitationDialog.js | 4 +
.../integration/editBibliographyDialog.js | 4 +
chrome/content/zotero/itemTree.jsx | 57 ++-
chrome/content/zotero/itemTreeColumns.jsx | 240 +++++++-----
chrome/content/zotero/rtfScan.js | 2 +-
chrome/content/zotero/selectItemsDialog.js | 2 +-
chrome/content/zotero/xpcom/integration.js | 1 +
.../content/zotero/xpcom/itemTreeManager.js | 361 ++++++++++++++++++
chrome/content/zotero/xpcom/notifier.js | 3 +-
components/zotero-service.js | 1 +
13 files changed, 586 insertions(+), 121 deletions(-)
create mode 100644 chrome/content/zotero/xpcom/itemTreeManager.js
diff --git a/chrome/content/zotero/advancedSearch.js b/chrome/content/zotero/advancedSearch.js
index aa9c6c83f8..a7357f0824 100644
--- a/chrome/content/zotero/advancedSearch.js
+++ b/chrome/content/zotero/advancedSearch.js
@@ -26,7 +26,7 @@
Components.utils.import("resource://gre/modules/Services.jsm");
import ItemTree from 'zotero/itemTree';
-import { getDefaultColumnsByDataKeys } from 'zotero/itemTreeColumns';
+import { getColumnDefinitionsByDataKey } from 'zotero/itemTreeColumns'
var ZoteroAdvancedSearch = new function() {
@@ -60,13 +60,14 @@ var ZoteroAdvancedSearch = new function() {
id: "advanced-search",
dragAndDrop: true,
onActivate: this.onItemActivate.bind(this),
- columns: getDefaultColumnsByDataKeys(['title', 'firstCreator']),
+ columns: getColumnDefinitionsByDataKey(["title", "firstCreator"]),
});
// A minimal implementation of Zotero.CollectionTreeRow
var collectionTreeRow = {
view: {},
ref: _searchBox.search,
+ visibilityGroup: 'default',
isSearchMode: () => true,
getItems: async () => [],
isLibrary: () => false,
@@ -96,6 +97,7 @@ var ZoteroAdvancedSearch = new function() {
var collectionTreeRow = {
view: {},
ref: _searchBox.search,
+ visibilityGroup: 'default',
isSearchMode: () => true,
getItems: async function () {
await Zotero.Libraries.get(_libraryID).waitForDataLoad('item');
diff --git a/chrome/content/zotero/components/virtualized-table.jsx b/chrome/content/zotero/components/virtualized-table.jsx
index 57eb0be005..076369896a 100644
--- a/chrome/content/zotero/components/virtualized-table.jsx
+++ b/chrome/content/zotero/components/virtualized-table.jsx
@@ -1090,9 +1090,23 @@ class VirtualizedTable extends React.Component {
return this._getVisibleColumns().map((column, index) => {
let columnName = formatColumnName(column);
let label = columnName;
+ // Allow custom icons to be used in column headers
+ if (column.iconPath) {
+ column.iconLabel =
+ ;
+ }
if (column.iconLabel) {
label = column.iconLabel;
}
+ if (column.htmlLabel) {
+ if (React.isValidElement(column.htmlLabel)) {
+ label = column.htmlLabel;
+ } else if (typeof column.htmlLabel === "string") {
+ label = ;
+ }
+ }
let resizer = (= this._jsWindow.getFirstVisibleRow()
&& row <= this._jsWindow.getLastVisibleRow();
}
+
+ async _resetColumns() {
+ this.invalidate();
+ this._columns = new Columns(this);
+ await new Promise((resolve) => {this.forceUpdate(resolve)});
+ }
}
/**
@@ -1452,7 +1472,7 @@ var Columns = class {
}
_getColumnPrefsToPersist(column) {
- let persistKeys = column.zoteroPersist;
+ let persistKeys = new Set(column.zoteroPersist);
if (!persistKeys) persistKeys = new Set();
// Always persist
['ordinal', 'hidden', 'sortDirection'].forEach(k => persistKeys.add(k));
@@ -1585,7 +1605,7 @@ var Columns = class {
column.sortDirection *= -1;
}
else {
- column.sortDirection = column.defaultSort || 1;
+ column.sortDirection = column.sortReverse ? -1 : 1;
}
}
});
diff --git a/chrome/content/zotero/elements/relatedBox.js b/chrome/content/zotero/elements/relatedBox.js
index 7477b13925..d27bba0dc4 100644
--- a/chrome/content/zotero/elements/relatedBox.js
+++ b/chrome/content/zotero/elements/relatedBox.js
@@ -173,7 +173,7 @@
}
add = async () => {
- let io = { dataIn: null, dataOut: null, deferred: Zotero.Promise.defer() };
+ let io = { dataIn: null, dataOut: null, deferred: Zotero.Promise.defer(), itemTreeID: 'related-box-select-item-dialog' };
window.openDialog('chrome://zotero/content/selectItemsDialog.xhtml', '',
'chrome,dialog=no,centerscreen,resizable=yes', io);
diff --git a/chrome/content/zotero/integration/addCitationDialog.js b/chrome/content/zotero/integration/addCitationDialog.js
index 7681d673a8..e3ba76b693 100644
--- a/chrome/content/zotero/integration/addCitationDialog.js
+++ b/chrome/content/zotero/integration/addCitationDialog.js
@@ -138,6 +138,10 @@ var Zotero_Citation_Dialog = new function () {
i++;
}
menu.selectedIndex = pageLocatorIndex;
+
+ if (!io.itemTreeID) {
+ io.itemTreeID = "add-citation-select-item-dialog";
+ }
// load (from selectItemsDialog.js)
yield doLoad();
diff --git a/chrome/content/zotero/integration/editBibliographyDialog.js b/chrome/content/zotero/integration/editBibliographyDialog.js
index 08583d6593..8406889709 100644
--- a/chrome/content/zotero/integration/editBibliographyDialog.js
+++ b/chrome/content/zotero/integration/editBibliographyDialog.js
@@ -57,6 +57,10 @@ var Zotero_Bibliography_Dialog = new function () {
window.addEventListener('dialogcancel', () => Zotero_Bibliography_Dialog.close());
_editor = document.querySelector('#editor').contentWindow.editor;
+
+ if (!io.itemTreeID) {
+ io.itemTreeID = "edit-bib-select-item-dialog";
+ }
// load (from selectItemsDialog.js)
await doLoad();
diff --git a/chrome/content/zotero/itemTree.jsx b/chrome/content/zotero/itemTree.jsx
index c1c68a7e53..69dced5680 100644
--- a/chrome/content/zotero/itemTree.jsx
+++ b/chrome/content/zotero/itemTree.jsx
@@ -32,10 +32,14 @@ const VirtualizedTable = require('components/virtualized-table');
const { renderCell, formatColumnName } = VirtualizedTable;
const Icons = require('components/icons');
const { getDOMElement } = Icons;
-const { COLUMNS } = require('./itemTreeColumns');
+const { COLUMNS } = require("zotero/itemTreeColumns");
const { Cc, Ci, Cu } = require('chrome');
Cu.import("resource://gre/modules/osfile.jsm");
+/**
+ * @typedef {import("./itemTreeColumns.jsx").ItemTreeColumnOptions} ItemTreeColumnOptions
+ */
+
const CHILD_INDENT = 12;
const COLORED_TAGS_RE = new RegExp("^(?:Numpad|Digit)([0-" + Zotero.Tags.MAX_COLORED_TAGS + "]{1})$");
const COLUMN_PREFS_FILEPATH = OS.Path.join(Zotero.Profile.dir, "treePrefs.json");
@@ -99,7 +103,7 @@ var ItemTree = class ItemTree extends LibraryTree {
this._unregisterID = Zotero.Notifier.registerObserver(
this,
- ['item', 'collection-item', 'item-tag', 'share-items', 'bucket', 'feedItem', 'search'],
+ ['item', 'collection-item', 'item-tag', 'share-items', 'bucket', 'feedItem', 'search', 'itemtree'],
'itemTreeView',
50
);
@@ -107,8 +111,7 @@ var ItemTree = class ItemTree extends LibraryTree {
this._itemsPaneMessage = null;
this._columnsId = null;
- this.columns = null;
-
+
if (this.collectionTreeRow) {
this.collectionTreeRow.view.itemTreeView = this;
}
@@ -142,12 +145,20 @@ var ItemTree = class ItemTree extends LibraryTree {
/**
- * Extension developers: use this to monkey-patch additional columns. See
- * itemTreeColumns.js for available column fields.
- * @returns {Array}
+ * Get global columns from ItemTreeColumns and local columns from this.columns
+ * @returns {ItemTreeColumnOptions[]}
*/
getColumns() {
- return Array.from(this.props.columns);
+ const extraColumns = Zotero.ItemTreeManager.getCustomColumns(this.props.id);
+
+ /** @type {ItemTreeColumnOptions[]} */
+ const currentColumns = this.props.columns.map(col => Object.assign({}, col));
+ extraColumns.forEach((column) => {
+ if (!currentColumns.find(c => c.dataKey === column.dataKey)) {
+ currentColumns.push(column);
+ }
+ });
+ return currentColumns;
}
/**
@@ -349,6 +360,12 @@ var ItemTree = class ItemTree extends LibraryTree {
return;
}
+ // Reset columns on custom column change
+ if(type === "itemtree" && action === "refresh") {
+ await this._resetColumns();
+ return;
+ }
+
if (type == 'search' && action == 'modify') {
// TODO: Only refresh on condition change (not currently available in extraData)
await this.refresh();
@@ -1300,7 +1317,8 @@ var ItemTree = class ItemTree extends LibraryTree {
return (row.ref.isFeedItem && Zotero.Feeds.get(row.ref.libraryID).name) || "";
default:
- return row.ref.getField(field, false, true);
+ // Get from row.getField() to allow for custom fields
+ return row.getField(field, false, true);
}
}
@@ -3165,6 +3183,7 @@ var ItemTree = class ItemTree extends LibraryTree {
let columnsSettings = this._getColumnPrefs();
+ // Refresh columns from itemTreeColumns
const columns = this.getColumns();
let hasDefaultIn = columns.some(column => 'defaultIn' in column);
for (let column of columns) {
@@ -3193,7 +3212,7 @@ var ItemTree = class ItemTree extends LibraryTree {
// Initial hidden value
if (!("hidden" in column)) {
if (hasDefaultIn) {
- column.hidden = !(column.defaultIn && column.defaultIn.has(visibilityGroup));
+ column.hidden = !(column.defaultIn && column.defaultIn.includes(visibilityGroup));
}
else {
column.hidden = false;
@@ -3573,7 +3592,7 @@ var ItemTree = class ItemTree extends LibraryTree {
let columnMenuitemElements = {};
for (let i = 0; i < columns.length; i++) {
const column = columns[i];
- if (column.ignoreInColumnPicker === true) continue;
+ if (column.showInColumnPicker === false) continue;
let label = formatColumnName(column);
let menuitem = doc.createXULElement('menuitem');
menuitem.setAttribute('type', 'checkbox');
@@ -3604,7 +3623,7 @@ var ItemTree = class ItemTree extends LibraryTree {
let moreItems = [];
for (let i = 0; i < columns.length; i++) {
const column = columns[i];
- if (column.submenu) {
+ if (column.columnPickerSubMenu) {
moreItems.push(columnMenuitemElements[column.dataKey]);
}
}
@@ -3815,6 +3834,15 @@ var ItemTree = class ItemTree extends LibraryTree {
}
return span;
}
+
+ async _resetColumns(){
+ this._columnsId = null;
+ return new Promise((resolve) => this.forceUpdate(async () => {
+ await this.tree._resetColumns();
+ await this.refreshAndMaintainSelection();
+ resolve();
+ }));
+ }
};
var ItemTreeRow = function(ref, level, isOpen)
@@ -3827,7 +3855,10 @@ var ItemTreeRow = function(ref, level, isOpen)
ItemTreeRow.prototype.getField = function(field, unformatted)
{
- return this.ref.getField(field, unformatted, true);
+ if (!Zotero.ItemTreeManager.isCustomColumn(field)) {
+ return this.ref.getField(field, unformatted, true);
+ }
+ return Zotero.ItemTreeManager.getCustomCellData(this.ref, field);
}
ItemTreeRow.prototype.numNotes = function() {
diff --git a/chrome/content/zotero/itemTreeColumns.jsx b/chrome/content/zotero/itemTreeColumns.jsx
index 2b0928d53e..08fcd2be55 100644
--- a/chrome/content/zotero/itemTreeColumns.jsx
+++ b/chrome/content/zotero/itemTreeColumns.jsx
@@ -2,7 +2,7 @@
***** BEGIN LICENSE BLOCK *****
Copyright © 2020 Corporation for Digital Scholarship
- Vienna, Virginia, USA
+ Vienna, Virginia, USA
http://zotero.org
This file is part of Zotero.
@@ -23,318 +23,358 @@
***** END LICENSE BLOCK *****
*/
-(function() {
const React = require('react');
const Icons = require('components/icons');
/**
- * @type Column {
- * dataKey: string, // Required, see use in ItemTree#_getRowData()
- *
- * defaultIn: Set, // Types of trees the column is default in. Can be [default, feed];
- * disabledIn: Set, // Types of trees where the column is not available
- * defaultSort: number // Default: 1. -1 for descending sort
- *
- * flex: number, // Default: 1. When the column is added to the tree how much space it should occupy as a flex ratio
- * width: string, // A column width instead of flex ratio. See above.
- * fixedWidth: boolean // Default: false. Set to true to disable column resizing
- * staticWidth: boolean // Default: false. Set to true to prevent columns from changing width when
- * // the width of the tree increases or decreases
- * minWidth: number, // Override the default [20px] column min-width for resizing
- *
- * label: string, // The column label. Either a string or the id to an i18n string.
- * iconLabel: React.Component, // Set an Icon label instead of a text-based one
- *
- * ignoreInColumnPicker: boolean // Default: false. Set to true to not display in column picker.
- * submenu: boolean, // Default: false. Set to true to display the column in "More Columns" submenu of column picker.
- *
- * primary: boolean, // Should only be one column at the time. Title is the primary column
- * zoteroPersist: Set, // Which column properties should be persisted between zotero close
- * }
+ * @typedef ItemTreeColumnOptions
+ * @type {object}
+ * @property {string} dataKey - Required, see use in ItemTree#_getRowData()
+ * @property {string} label - The column label. Either a string or the id to an i18n string.
+ * @property {string} [pluginID] - Set plugin ID to auto remove column when plugin is removed.
+ * @property {string[]} [enabledTreeIDs=[]] - Which tree ids the column should be enabled in. If undefined, enabled in main tree. If ["*"], enabled in all trees.
+ * @property {string[]} [defaultIn] - Will be deprecated. Types of trees the column is default in. Can be [default, feed];
+ * @property {string[]} [disabledIn] - Will be deprecated. Types of trees where the column is not available
+ * @property {boolean} [sortReverse=false] - Default: false. Set to true to reverse the sort order
+ * @property {number} [flex=1] - Default: 1. When the column is added to the tree how much space it should occupy as a flex ratio
+ * @property {string} [width] - A column width instead of flex ratio. See above.
+ * @property {boolean} [fixedWidth] - Default: false. Set to true to disable column resizing
+ * @property {boolean} [staticWidth] - Default: false. Set to true to prevent columns from changing width when the width of the tree increases or decreases
+ * @property {number} [minWidth] - Override the default [20px] column min-width for resizing
+ * @property {React.Component} [iconLabel] - Set an Icon label instead of a text-based one
+ * @property {string} [iconPath] - Set an Icon path, overrides {iconLabel}
+ * @property {string | React.Component} [htmlLabel] - Set an HTML label, overrides {iconLabel} and {label}. Can be a HTML string or a React component.
+ * @property {boolean} [showInColumnPicker=true] - Default: true. Set to true to show in column picker.
+ * @property {boolean} [columnPickerSubMenu=false] - Default: false. Set to true to display the column in "More Columns" submenu of column picker.
+ * @property {boolean} [primary] - Should only be one column at the time. Title is the primary column
+ * @property {boolean} [custom] - Set automatically to true when the column is added by the user
+ * @property {(item: Zotero.Item, dataKey: string) => string} [dataProvider] - Custom data provider that is called when rendering cells
+ * @property {string[]} [zoteroPersist] - Which column properties should be persisted between zotero close
+ */
+
+/**
+ * @type {ItemTreeColumnOptions[]}
+ * @constant
*/
const COLUMNS = [
{
dataKey: "title",
primary: true,
- defaultIn: new Set(["default", "feeds", "feed"]),
+ defaultIn: ["default", "feeds", "feed"],
label: "itemFields.title",
- ignoreInColumnPicker: true,
+ showInColumnPicker: false,
flex: 4,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "firstCreator",
- defaultIn: new Set(["default", "feeds", "feed"]),
+ defaultIn: ["default", "feeds", "feed"],
label: "zotero.items.creator_column",
+ showInColumnPicker: true,
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "itemType",
label: "zotero.items.itemType",
+ showInColumnPicker: true,
width: "40",
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "date",
- defaultIn: new Set(["feeds", "feed"]),
- defaultSort: -1,
+ defaultIn: ["feeds", "feed"],
+ sortReverse: true,
label: "itemFields.date",
+ showInColumnPicker: true,
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "year",
disabledIn: ["feeds", "feed"],
- defaultSort: -1,
+ sortReverse: true,
label: "zotero.items.year_column",
+ showInColumnPicker: true,
flex: 1,
staticWidth: true,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "publisher",
label: "itemFields.publisher",
+ showInColumnPicker: true,
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "publicationTitle",
label: "itemFields.publicationTitle",
+ showInColumnPicker: true,
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "journalAbbreviation",
disabledIn: ["feeds", "feed"],
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.journalAbbreviation",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "language",
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.language",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "accessDate",
disabledIn: ["feeds", "feed"],
- defaultSort: -1,
- submenu: true,
+ sortReverse: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.accessDate",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "libraryCatalog",
disabledIn: ["feeds", "feed"],
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.libraryCatalog",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "callNumber",
disabledIn: ["feeds", "feed"],
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.callNumber",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "rights",
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.rights",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "dateAdded",
- defaultSort: -1,
+ sortReverse: true,
disabledIn: ["feeds", "feed"],
+ showInColumnPicker: true,
label: "itemFields.dateAdded",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "dateModified",
- defaultSort: -1,
+ sortReverse: true,
disabledIn: ["feeds", "feed"],
+ showInColumnPicker: true,
label: "zotero.items.dateModified_column",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "archive",
disabledIn: ["feeds", "feed"],
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.archive",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "archiveLocation",
disabledIn: ["feeds", "feed"],
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.archiveLocation",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "place",
disabledIn: ["feeds", "feed"],
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.place",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "volume",
disabledIn: ["feeds", "feed"],
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.volume",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "edition",
disabledIn: ["feeds", "feed"],
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.edition",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "number",
disabledIn: ["feeds", "feed"],
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.number",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "pages",
disabledIn: ["feeds", "feed"],
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.pages",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "issue",
disabledIn: ["feeds", "feed"],
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.issue",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "series",
disabledIn: ["feeds", "feed"],
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.series",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "seriesTitle",
disabledIn: ["feeds", "feed"],
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.seriesTitle",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "court",
disabledIn: ["feeds", "feed"],
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.court",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "medium",
disabledIn: ["feeds", "feed"],
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.medium",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "genre",
disabledIn: ["feeds", "feed"],
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.genre",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "system",
disabledIn: ["feeds", "feed"],
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.system",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "shortTitle",
disabledIn: ["feeds", "feed"],
- submenu: true,
+ showInColumnPicker: true,
+ columnPickerSubMenu: true,
label: "itemFields.shortTitle",
flex: 2,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "extra",
disabledIn: ["feeds", "feed"],
+ showInColumnPicker: true,
label: "itemFields.extra",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "hasAttachment",
- defaultIn: new Set(["default"]),
+ defaultIn: ["default"],
disabledIn: ["feeds", "feed"],
+ showInColumnPicker: true,
label: "zotero.tabs.attachments.label",
iconLabel: ,
fixedWidth: true,
width: "16",
- zoteroPersist: new Set(["hidden", "sortDirection"])
+ zoteroPersist: ["hidden", "sortDirection"]
},
{
dataKey: "numNotes",
disabledIn: ["feeds", "feed"],
+ showInColumnPicker: true,
label: "zotero.tabs.notes.label",
iconLabel: ,
width: "14",
minWidth: 14,
staticWidth: true,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
},
{
dataKey: "feed",
disabledIn: ["default", "feed"],
+ showInColumnPicker: true,
label: "itemFields.feed",
flex: 1,
- zoteroPersist: new Set(["width", "hidden", "sortDirection"])
+ zoteroPersist: ["width", "hidden", "sortDirection"]
}
];
-function getDefaultColumnByDataKey(dataKey) {
- return Object.assign({}, COLUMNS.find(col => col.dataKey == dataKey), {hidden: false});
-}
-
-function getDefaultColumnsByDataKeys(dataKeys) {
- return COLUMNS.filter(column => dataKeys.includes(column.dataKey)).map(column => Object.assign({}, column, {hidden: false}));
+/**
+ * Returns the columns that match the given data keys from the COLUMNS constant.
+ * @param {string | string[]} dataKeys - The data key(s) to match.
+ * @returns {ItemTreeColumnOptions | ItemTreeColumnOptions[]} - The matching columns.
+ */
+function getColumnDefinitionsByDataKey(dataKeys) {
+ const isSingle = !Array.isArray(dataKeys);
+ if (isSingle) {
+ dataKeys = [dataKeys];
+ }
+ const matches = COLUMNS.filter(column => dataKeys.includes(column.dataKey)).map(column => Object.assign({}, column, { hidden: false }));
+ return isSingle ? matches[0] : matches;
}
module.exports = {
COLUMNS,
- getDefaultColumnByDataKey,
- getDefaultColumnsByDataKeys,
+ getColumnDefinitionsByDataKey,
};
-
-})();
diff --git a/chrome/content/zotero/rtfScan.js b/chrome/content/zotero/rtfScan.js
index 793b8bcb0c..f265bb5a8a 100644
--- a/chrome/content/zotero/rtfScan.js
+++ b/chrome/content/zotero/rtfScan.js
@@ -341,7 +341,7 @@ const Zotero_RTFScan = { // eslint-disable-line no-unused-vars, camelcase
}
else { // mapped or unmapped citation, or ambiguous citation parent
var citation = row.rtf;
- var io = { singleSelection: true };
+ var io = { singleSelection: true, itemTreeID: 'rtf-scan-select-item-dialog' };
if (this.citationItemIDs[citation] && this.citationItemIDs[citation].length == 1) { // mapped citation
// specify that item should be selected in window
io.select = this.citationItemIDs[citation][0];
diff --git a/chrome/content/zotero/selectItemsDialog.js b/chrome/content/zotero/selectItemsDialog.js
index 1ecc85a9c8..f244da2574 100644
--- a/chrome/content/zotero/selectItemsDialog.js
+++ b/chrome/content/zotero/selectItemsDialog.js
@@ -60,7 +60,7 @@ var doLoad = async function () {
onItemSelected();
}
},
- id: "select-items-dialog",
+ id: io.itemTreeID || "select-items-dialog",
dragAndDrop: false,
persistColumns: true,
columnPicker: true,
diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js
index 6ba2affcd6..dddb797b3c 100644
--- a/chrome/content/zotero/xpcom/integration.js
+++ b/chrome/content/zotero/xpcom/integration.js
@@ -3181,6 +3181,7 @@ Zotero.Integration.Citation = class {
io.addBorder = Zotero.isWin;
io.singleSelection = true;
+ io.itemTreeID = "handle-missing-item-select-item-dialog";
await Zotero.Integration.displayDialog('chrome://zotero/content/selectItemsDialog.xhtml', 'resizable', io);
diff --git a/chrome/content/zotero/xpcom/itemTreeManager.js b/chrome/content/zotero/xpcom/itemTreeManager.js
new file mode 100644
index 0000000000..ea34a773a3
--- /dev/null
+++ b/chrome/content/zotero/xpcom/itemTreeManager.js
@@ -0,0 +1,361 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2019 Corporation for Digital Scholarship
+ Vienna, Virginia, USA
+ https://digitalscholar.org
+
+ This file is part of Zotero.
+
+ Zotero is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Zotero is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Zotero. If not, see .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+const { COLUMNS: ITEMTREE_COLUMNS } = require("zotero/itemTreeColumns");
+
+/**
+ * @typedef {import("../itemTreeColumns.jsx").ItemTreeColumnOptions} ItemTreeColumnOptions
+ * @typedef {"dataKey" | "label" | "pluginID"} RequiredCustomColumnOptionKeys
+ * @typedef {Required>} RequiredCustomColumnOptionsPartial
+ * @typedef {Omit} CustomColumnOptionsPartial
+ * @typedef {RequiredCustomColumnOptionsPartial & CustomColumnOptionsPartial} ItemTreeCustomColumnOptions
+ * @typedef {Partial>} ItemTreeCustomColumnFilters
+ */
+
+class ItemTreeManager {
+ _observerAdded = false;
+
+ /** @type {Record {
+ * return item.getField('title').split('').reverse().join('');
+ * },
+ * });
+ * ```
+ * @example
+ * A custom column using all available options.
+ * Note that the column will only be shown in the main item tree.
+ * ```js
+ * const registeredDataKey = await Zotero.ItemTreeManager.registerColumns(
+ * {
+ * dataKey: 'rtitle',
+ * label: 'Reversed Title',
+ * enabledTreeIDs: ['main'], // only show in the main item tree
+ * sortReverse: true, // sort by increasing order
+ * flex: 0, // don't take up all available space
+ * width: 100, // assign fixed width in pixels
+ * fixedWidth: true, // don't allow user to resize
+ * staticWidth: true, // don't allow column to be resized when the tree is resized
+ * minWidth: 50, // minimum width in pixels
+ * iconPath: 'chrome://zotero/skin/tick.png', // icon to show in the column header
+ * htmlLabel: 'reversed title', // use HTML in the label. This will override the label and iconPath property
+ * showInColumnPicker: true, // show in the column picker
+ * columnPickerSubMenu: true, // show in the column picker submenu
+ * pluginID: 'make-it-red@zotero.org', // plugin ID, which will be used to unregister the column when the plugin is unloaded
+ * dataProvider: (item, dataKey) => {
+ * // item: the current item in the row
+ * // dataKey: the dataKey of the column
+ * // return: the data to display in the column
+ * return item.getField('title').split('').reverse().join('');
+ * },
+ * zoteroPersist: ['width', 'hidden', 'sortDirection'], // persist the column properties
+ * });
+ * ```
+ * @example
+ * Register multiple custom columns:
+ * ```js
+ * const registeredDataKeys = await Zotero.ItemTreeManager.registerColumns(
+ * [
+ * {
+ * dataKey: 'rtitle',
+ * iconPath: 'chrome://zotero/skin/tick.png',
+ * label: 'Reversed Title',
+ * pluginID: 'make-it-red@zotero.org', // Replace with your plugin ID
+ * dataProvider: (item, dataKey) => {
+ * return item.getField('title').split('').reverse().join('');
+ * },
+ * },
+ * {
+ * dataKey: 'utitle',
+ * label: 'Uppercase Title',
+ * pluginID: 'make-it-red@zotero.org', // Replace with your plugin ID
+ * dataProvider: (item, dataKey) => {
+ * return item.getField('title').toUpperCase();
+ * },
+ * },
+ * ]);
+ * ```
+ */
+ async registerColumns(options) {
+ const registeredDataKeys = this._addColumns(options);
+ if (!registeredDataKeys) {
+ return false;
+ }
+ this._addPluginShutdownObserver();
+ await this._notifyItemTrees();
+ return registeredDataKeys;
+ }
+
+ /**
+ * Unregister a custom column.
+ * Although it's async, resolving does not promise the item trees are updated.
+ * @param {string | string[]} dataKeys - The dataKey of the column to unregister
+ * @returns {boolean} true if the column(s) are unregistered
+ * @example
+ * ```js
+ * Zotero.ItemTreeManager.unregisterColumns('rtitle');
+ * ```
+ */
+ async unregisterColumns(dataKeys) {
+ const success = this._removeColumns(dataKeys);
+ if (!success) {
+ return false;
+ }
+ await this._notifyItemTrees();
+ return true;
+ }
+
+ // TODO: add cell renderer registration
+
+ /**
+ * Get column(s) that matches the properties of option
+ * @param {string | string[]} [filterTreeIDs] - The tree IDs to match
+ * @param {ItemTreeCustomColumnFilters} [options] - An option or array of options to match
+ * @returns {ItemTreeCustomColumnOptions[]}
+ */
+ getCustomColumns(filterTreeIDs, options) {
+ const allColumns = Object.values(this._customColumns).map(opt => Object.assign({}, opt));
+ if (!filterTreeIDs && !options) {
+ return allColumns;
+ }
+ let filteredColumns = allColumns;
+ if (typeof filterTreeIDs === "string") {
+ filterTreeIDs = [filterTreeIDs];
+ }
+ if (filterTreeIDs && !filterTreeIDs.includes("*")) {
+ const filterTreeIDsSet = new Set(filterTreeIDs);
+ filteredColumns = filteredColumns.filter((column) => {
+ if (column.enabledTreeIDs[0] == "*") return true;
+
+ for (const treeID of column.enabledTreeIDs) {
+ if (filterTreeIDsSet.has(treeID)) return true;
+ }
+ return false;
+ });
+ }
+ if (options) {
+ filteredColumns = filteredColumns.filter((col) => {
+ return Object.keys(options).every((key) => {
+ // Ignore undefined properties
+ if (options[key] === undefined) {
+ return true;
+ }
+ return options[key] === col[key];
+ });
+ });
+ }
+ return filteredColumns;
+ }
+
+ /**
+ * Check if a column is registered as a custom column
+ * @param {string} dataKey - The dataKey of the column
+ * @returns {boolean} true if the column is registered as a custom column
+ */
+ isCustomColumn(dataKey) {
+ return !!this._customColumns[dataKey];
+ }
+
+ /**
+ * A centralized data source for custom columns. This is used by the ItemTreeRow to get data.
+ * @param {Zotero.Item} item - The item to get data from
+ * @param {string} dataKey - The dataKey of the column
+ * @returns {string}
+ */
+ getCustomCellData(item, dataKey) {
+ const options = this._customColumns[dataKey];
+ if (options && options.dataProvider) {
+ return options.dataProvider(item, dataKey);
+ }
+ return "";
+ }
+
+ /**
+ * Check if column options is valid.
+ * All its children must be valid. Otherwise, the validation fails.
+ * @param {ItemTreeCustomColumnOptions[]} options - An array of options to validate
+ * @returns {boolean} true if the options are valid
+ */
+ _validateColumnOption(options) {
+ // Check if the input option has duplicate dataKeys
+ const noInputDuplicates = !options.find((opt, i, arr) => arr.findIndex(o => o.dataKey === opt.dataKey) !== i);
+ if (!noInputDuplicates) {
+ Zotero.warn(`ItemTree Column options have duplicate dataKey.`);
+ }
+ const noDeniedProperties = options.every((option) => {
+ const valid = !option.primary;
+ return valid;
+ });
+ const requiredProperties = options.every((option) => {
+ const valid = option.dataKey && option.label && option.pluginID;
+ if (!valid) {
+ Zotero.warn(`ItemTree Column option ${JSON.stringify(option)} must have dataKey, label, and pluginID.`);
+ }
+ return valid;
+ });
+ const noRegisteredDuplicates = options.every((option) => {
+ const valid = !this._customColumns[option.dataKey] && !ITEMTREE_COLUMNS.find(col => col.dataKey === option.dataKey);
+ if (!valid) {
+ Zotero.warn(`ItemTree Column option ${JSON.stringify(option)} with dataKey ${option.dataKey} already exists.`);
+ }
+ return valid;
+ });
+ return noInputDuplicates && noDeniedProperties && requiredProperties && noRegisteredDuplicates;
+ }
+
+
+ /**
+ * Add a new column or new columns.
+ * If the options is an array, all its children must be valid.
+ * Otherwise, no columns are added.
+ * @param {ItemTreeCustomColumnOptions | ItemTreeCItemTreeCustomColumnOptionsolumnOptions[]} options - An option or array of options to add
+ * @returns {string | string[] | false} - The dataKey(s) of the added column(s) or false if no columns were added
+ */
+ _addColumns(options) {
+ const isSingle = !Array.isArray(options);
+ if (isSingle) {
+ options = [options];
+ }
+ options.forEach((o) => {
+ o.dataKey = this._namespacedDataKey(o);
+ o.enabledTreeIDs = o.enabledTreeIDs || ["main"];
+ if (o.enabledTreeIDs.includes("*")) {
+ o.enabledTreeIDs = ["*"];
+ }
+ o.showInColumnPicker = o.showInColumnPicker === undefined ? true : o.showInColumnPicker;
+ });
+ // If any check fails, return check results
+ if (!this._validateColumnOption(options)) {
+ return false;
+ }
+ for (const opt of options) {
+ this._customColumns[opt.dataKey] = Object.assign({}, opt, { custom: true });
+ }
+ return isSingle ? options[0].dataKey : options.map(opt => opt.dataKey);
+ }
+
+ /**
+ * Remove a column option
+ * @param {string | string[]} dataKeys - The dataKey of the column to remove
+ * @returns {boolean} - True if column(s) were removed, false if not
+ */
+ _removeColumns(dataKeys) {
+ if (!Array.isArray(dataKeys)) {
+ dataKeys = [dataKeys];
+ }
+ // If any check fails, return check results and do not remove any columns
+ for (const key of dataKeys) {
+ if (!this._customColumns[key]) {
+ Zotero.warn(`ItemTree Column option with dataKey ${key} does not exist.`);
+ return false;
+ }
+ }
+ for (const key of dataKeys) {
+ delete this._customColumns[key];
+ }
+ return true;
+ }
+
+ /**
+ * Make sure the dataKey is namespaced with the plugin ID
+ * @param {ItemTreeCustomColumnOptions} options
+ * @returns {string}
+ */
+ _namespacedDataKey(options) {
+ if (options.pluginID && options.dataKey) {
+ // Make sure the return value is valid as class name or element id
+ return `${options.pluginID}-${options.dataKey}`.replace(/[^a-zA-Z0-9-_]/g, "-");
+ }
+ return options.dataKey;
+ }
+
+
+ /**
+ * Reset the item trees to update the columns
+ */
+ async _notifyItemTrees() {
+ await Zotero.DB.executeTransaction(async function () {
+ Zotero.Notifier.queue(
+ 'refresh',
+ 'itemtree',
+ [],
+ {},
+ );
+ });
+ }
+
+ /**
+ * Unregister all columns registered by a plugin
+ * @param {string} pluginID - Plugin ID
+ */
+ async _unregisterColumnByPluginID(pluginID) {
+ const columns = this.getCustomColumns(undefined, { pluginID });
+ if (columns.length === 0) {
+ return;
+ }
+ // Remove the columns one by one
+ // This is to ensure that the columns are removed and not interrupted by any non-existing columns
+ columns.forEach(column => this._removeColumns(column.dataKey));
+ Zotero.debug(`ItemTree columns registered by plugin ${pluginID} unregistered due to shutdown`);
+ await this._notifyItemTrees();
+ }
+
+ /**
+ * Ensure that the shutdown observer is added
+ * @returns {void}
+ */
+ _addPluginShutdownObserver() {
+ if (this._observerAdded) {
+ return;
+ }
+
+ Zotero.Plugins.addObserver({
+ shutdown: ({ id: pluginID }) => {
+ this._unregisterColumnByPluginID(pluginID);
+ }
+ });
+ this._observerAdded = true;
+ }
+}
+
+Zotero.ItemTreeManager = new ItemTreeManager();
diff --git a/chrome/content/zotero/xpcom/notifier.js b/chrome/content/zotero/xpcom/notifier.js
index 98621c4077..b118056f8c 100644
--- a/chrome/content/zotero/xpcom/notifier.js
+++ b/chrome/content/zotero/xpcom/notifier.js
@@ -33,7 +33,8 @@ Zotero.Notifier = new function(){
var _types = [
'collection', 'search', 'share', 'share-items', 'item', 'file',
'collection-item', 'item-tag', 'tag', 'setting', 'group', 'trash',
- 'bucket', 'relation', 'feed', 'feedItem', 'sync', 'api-key', 'tab'
+ 'bucket', 'relation', 'feed', 'feedItem', 'sync', 'api-key', 'tab',
+ 'itemtree'
];
var _transactionID = false;
var _queue = {};
diff --git a/components/zotero-service.js b/components/zotero-service.js
index ef825641b1..b906efcfe5 100644
--- a/components/zotero-service.js
+++ b/components/zotero-service.js
@@ -154,6 +154,7 @@ const xpcomFilesLocal = [
'connector/httpIntegrationClient',
'connector/server_connector',
'connector/server_connectorIntegration',
+ 'itemTreeManager',
];
Components.utils.import("resource://gre/modules/ComponentUtils.jsm");