mirror of
https://github.com/zotero/zotero.git
synced 2026-10-03 02:21:49 +00:00
Citation Explorer: show suggested items when relinking an unlinked item
This commit is contained in:
parent
06eb70abda
commit
af5ab8427b
7 changed files with 336 additions and 20 deletions
|
|
@ -47,6 +47,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
}
|
||||
|
||||
static defaultProps = {
|
||||
customRows: [],
|
||||
dragAndDrop: false,
|
||||
filterLibraryIDs: false,
|
||||
hideSources: [],
|
||||
|
|
@ -57,6 +58,7 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
static propTypes = {
|
||||
onSelectionChange: PropTypes.func.isRequired,
|
||||
|
||||
customRows: PropTypes.array,
|
||||
dragAndDrop: PropTypes.bool,
|
||||
filterLibraryIDs: PropTypes.array,
|
||||
hideSources: PropTypes.array,
|
||||
|
|
@ -598,6 +600,26 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
);
|
||||
added += await this._expandRow(newRows, added - 1);
|
||||
}
|
||||
|
||||
// Custom rows, e.g. Suggested Items in select dialog
|
||||
let customRows = this.props.customRows.map((customRow) => {
|
||||
let row = new Zotero.CollectionTreeRow(
|
||||
this,
|
||||
customRow.type,
|
||||
customRow.ref,
|
||||
customRow.level,
|
||||
customRow.isOpen
|
||||
);
|
||||
row._id = customRow.id;
|
||||
return Object.assign(row, customRow.properties);
|
||||
});
|
||||
let addedCustomRows = customRows.length;
|
||||
if (addedCustomRows) {
|
||||
customRows.push(new Zotero.CollectionTreeRow(this, 'separator', false, 0));
|
||||
addedCustomRows++;
|
||||
newRows.unshift(...customRows);
|
||||
added += addedCustomRows;
|
||||
}
|
||||
|
||||
this.selection.selectEventsSuppressed = true;
|
||||
// If the focused row does not match the filter, create a hidden dummy row at the bottom
|
||||
|
|
@ -1499,6 +1521,10 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
|
||||
getIconName(index) {
|
||||
const treeRow = this.getRow(index);
|
||||
// Custom row icons
|
||||
if (treeRow.iconName) {
|
||||
return treeRow.iconName;
|
||||
}
|
||||
let collectionType = treeRow.type;
|
||||
let icon = collectionType;
|
||||
|
||||
|
|
|
|||
|
|
@ -729,7 +729,18 @@ window.ZoteroCitationExplorer = {
|
|||
let treeRow = itemList.getRow(itemList.selection.focused);
|
||||
let oldItemID = treeRow.id;
|
||||
|
||||
let io = { dataIn: null, dataOut: null, multiSelect: false, deferred: Zotero.Promise.defer() };
|
||||
let libraryIDs = Zotero.Libraries.getAll()
|
||||
.filter(library => library.libraryType != 'feed')
|
||||
.map(library => library.libraryID);
|
||||
let itemIDs = await new Zotero.Duplicates(libraryIDs).findDuplicatesOf(treeRow.ref);
|
||||
let io = {
|
||||
dataIn: null,
|
||||
dataOut: null,
|
||||
itemIDs: itemIDs.length ? itemIDs : undefined,
|
||||
multiSelect: false,
|
||||
onlyRegularItems: true,
|
||||
deferred: Zotero.Promise.defer()
|
||||
};
|
||||
window.openDialog('chrome://zotero/content/selectItemsDialog.xhtml', '',
|
||||
'chrome,dialog=no,centerscreen,resizable=yes', io);
|
||||
|
||||
|
|
@ -748,6 +759,7 @@ window.ZoteroCitationExplorer = {
|
|||
await this._initMappings();
|
||||
await this.refreshCitationList();
|
||||
await this.refreshItemList();
|
||||
await itemList.selectItem(items[0].id);
|
||||
},
|
||||
|
||||
async addToLibraryAndLink() {
|
||||
|
|
|
|||
|
|
@ -25,11 +25,14 @@
|
|||
|
||||
import CollectionTree from 'zotero/collectionTree';
|
||||
import CollectionViewItemTree from 'zotero/collectionViewItemTree';
|
||||
const { getCSSIcon } = require('components/icons');
|
||||
const { COLUMNS } = require('zotero/itemTreeColumns');
|
||||
|
||||
var itemsView;
|
||||
var collectionsView;
|
||||
var loaded;
|
||||
var io;
|
||||
var suggestedItemsTempTable;
|
||||
const isSelectItemsDialog = !!document.querySelector('#zotero-select-items-dialog');
|
||||
const isEditBibliographyDialog = !!document.querySelector('#zotero-edit-bibliography-dialog');
|
||||
const isAddEditItemsDialog = !!document.querySelector('#zotero-add-citation-dialog');
|
||||
|
|
@ -62,9 +65,82 @@ var doLoad = async function () {
|
|||
if(io.wrappedJSObject) io = io.wrappedJSObject;
|
||||
if(io.addBorder) document.getElementsByTagName("dialog")[0].style.border = "1px solid black";
|
||||
if(io.singleSelection) document.getElementById("zotero-items-tree").setAttribute("seltype", "single");
|
||||
|
||||
let columns = COLUMNS;
|
||||
let customRows = [];
|
||||
if (io.itemIDs) {
|
||||
let items = await Zotero.Items.getAsync(io.itemIDs);
|
||||
let itemIDs = items.map(item => item.id);
|
||||
let libraryIDs = new Set(items.map(item => item.libraryID));
|
||||
suggestedItemsTempTable = await Zotero.Search.idsToTempTable(itemIDs, { idColumn: 'id' });
|
||||
let search = new Zotero.Search();
|
||||
search.name = Zotero.getString('select-items-suggested-items');
|
||||
// Called by CollectionViewItemTree when the custom row is loaded or filtered
|
||||
let getSuggestedItems = async function () {
|
||||
let ids = [];
|
||||
for (let libraryID of libraryIDs) {
|
||||
let filteredSearch = new Zotero.Search();
|
||||
filteredSearch.libraryID = libraryID;
|
||||
filteredSearch.addCondition('tempTable', 'is', suggestedItemsTempTable);
|
||||
if (this.searchText) {
|
||||
let condition = 'quicksearch-'
|
||||
+ (this.searchMode || Zotero.Prefs.get('search.quicksearch-mode'));
|
||||
filteredSearch.addCondition(condition, 'contains', this.searchText);
|
||||
}
|
||||
for (let tag of this.tags) {
|
||||
filteredSearch.addCondition('tag', 'is', tag);
|
||||
}
|
||||
ids.push(...await filteredSearch.search());
|
||||
}
|
||||
return Zotero.Items.getAsync(ids);
|
||||
};
|
||||
customRows.push({
|
||||
id: 'suggested-items',
|
||||
type: 'suggestedItems',
|
||||
ref: search,
|
||||
properties: {
|
||||
iconName: 'duplicates',
|
||||
// This cross-library row has no libraryID
|
||||
isWithinGroup: () => false,
|
||||
getItems: getSuggestedItems
|
||||
}
|
||||
});
|
||||
if ([...libraryIDs].some(id => id != Zotero.Libraries.userLibraryID)) {
|
||||
let libraryColumn = {
|
||||
dataKey: 'library',
|
||||
label: 'select-items-library-column',
|
||||
showInColumnPicker: true,
|
||||
defaultIn: ['suggestedItems'],
|
||||
enabledIn: ['suggestedItems'],
|
||||
width: '180',
|
||||
minWidth: 120,
|
||||
zoteroPersist: ['width', 'hidden', 'sortDirection'],
|
||||
renderCell(index, libraryName, column, _isFirstColumn, doc) {
|
||||
let item = this.getRow(index).ref;
|
||||
let library = Zotero.Libraries.get(item.libraryID);
|
||||
let cell = doc.createElement('span');
|
||||
cell.className = `cell ${column.className}`;
|
||||
let icon = getCSSIcon(library.libraryType === 'group' ? 'library-group' : 'library');
|
||||
icon.classList.add('cell-icon', 'item-icon', 'icon-item-type');
|
||||
let text = doc.createElement('span');
|
||||
text.className = 'cell-text';
|
||||
text.textContent = libraryName;
|
||||
cell.append(icon, text);
|
||||
return cell;
|
||||
}
|
||||
};
|
||||
let attachmentColumnIndex = COLUMNS.findIndex(column => column.dataKey === 'hasAttachment');
|
||||
columns = [
|
||||
...COLUMNS.slice(0, attachmentColumnIndex),
|
||||
libraryColumn,
|
||||
...COLUMNS.slice(attachmentColumnIndex)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
itemsView = await CollectionViewItemTree.init(document.getElementById('zotero-items-tree'), {
|
||||
onSelectionChange: () => {
|
||||
updateShowInZoteroButton();
|
||||
if (isEditBibliographyDialog) {
|
||||
Zotero_Bibliography_Dialog.treeItemSelected();
|
||||
}
|
||||
|
|
@ -78,8 +154,15 @@ var doLoad = async function () {
|
|||
id: io.itemTreeID || "select-items-dialog",
|
||||
dragAndDrop: false,
|
||||
regularOnly: io.onlyRegularItems,
|
||||
columns,
|
||||
columnPicker: true,
|
||||
multiSelect: io.multiSelect,
|
||||
getExtraField: (item, field) => {
|
||||
if (field === 'library' && item.libraryID) {
|
||||
return Zotero.Libraries.get(item.libraryID).name;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
emptyMessage: Zotero.getString('pane.items.loading')
|
||||
});
|
||||
itemsView.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
|
||||
|
|
@ -88,19 +171,34 @@ var doLoad = async function () {
|
|||
const hideSources = io.hideCollections || ['duplicates', 'trash', 'feeds'];
|
||||
collectionsView = await CollectionTree.init(document.getElementById('zotero-collections-tree'), {
|
||||
onSelectionChange: () => onCollectionSelected(),
|
||||
customRows,
|
||||
filterLibraryIDs,
|
||||
hideSources
|
||||
});
|
||||
|
||||
await collectionsView.makeVisible();
|
||||
|
||||
if (io.select) {
|
||||
if (io.itemIDs) {
|
||||
await collectionsView.selectWait(collectionsView.getRowIndexByID('suggested-items'));
|
||||
}
|
||||
else if (io.select) {
|
||||
await collectionsView.selectItem(io.select);
|
||||
}
|
||||
|
||||
Zotero.updateQuickSearchBox(document);
|
||||
|
||||
document.addEventListener('dialogaccept', doAccept);
|
||||
if (io.itemIDs) {
|
||||
let showInZoteroButton = document.querySelector("dialog button[dlgtype='extra1']");
|
||||
document.l10n.setAttributes(showInZoteroButton, 'select-items-show-in-zotero');
|
||||
showInZoteroButton.addEventListener('click', async () => {
|
||||
await Zotero.Utilities.Internal.showInLibrary(itemsView.getSelectedItems());
|
||||
});
|
||||
updateShowInZoteroButton();
|
||||
}
|
||||
else if (!io.extraButtons?.some(button => button.type === 'extra1')) {
|
||||
document.querySelector("dialog button[dlgtype='extra1']")?.setAttribute('hidden', true);
|
||||
}
|
||||
|
||||
if (isSelectItemsDialog) {
|
||||
// Set proper tab order. It is only needed in selectItemsDialog -- other dialogs' focus order is correct
|
||||
|
|
@ -126,13 +224,25 @@ var doLoad = async function () {
|
|||
loaded = true;
|
||||
};
|
||||
|
||||
function doUnload()
|
||||
{
|
||||
collectionsView.unregister();
|
||||
if(itemsView)
|
||||
itemsView.unregister();
|
||||
|
||||
io.deferred && io.deferred.resolve();
|
||||
function doUnload() {
|
||||
collectionsView?.unregister();
|
||||
itemsView?.unregister();
|
||||
if (suggestedItemsTempTable) {
|
||||
Zotero.DB.queryAsync(
|
||||
`DROP TABLE IF EXISTS ${suggestedItemsTempTable}`,
|
||||
false,
|
||||
{ noCache: true }
|
||||
).catch(e => Zotero.logError(e));
|
||||
}
|
||||
io?.deferred?.resolve();
|
||||
}
|
||||
|
||||
function updateShowInZoteroButton() {
|
||||
let button = document.querySelector("dialog button[dlgtype='extra1']");
|
||||
if (!button || !io?.itemIDs || !collectionsView) return;
|
||||
let row = collectionsView.getRow(collectionsView.selection.focused);
|
||||
button.hidden = row?.type !== 'suggestedItems';
|
||||
button.disabled = button.hidden || !itemsView?.getSelectedItems().length;
|
||||
}
|
||||
|
||||
var onCollectionSelected = async function () {
|
||||
|
|
@ -142,21 +252,31 @@ var onCollectionSelected = async function () {
|
|||
if (itemsView && itemsView.collectionTreeRows[0]?.id == collectionTreeRow.id) {
|
||||
return;
|
||||
}
|
||||
document.getElementById('zotero-tb-search').onCollectionSelected();
|
||||
collectionTreeRow.setSearch('');
|
||||
Zotero.Prefs.set('lastViewedFolder', collectionTreeRow.id);
|
||||
if (collectionTreeRow.type !== 'suggestedItems') {
|
||||
Zotero.Prefs.set('lastViewedFolder', collectionTreeRow.id);
|
||||
}
|
||||
|
||||
itemsView.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
|
||||
|
||||
// Load library data if necessary
|
||||
var library = Zotero.Libraries.get(collectionTreeRow.ref.libraryID);
|
||||
if (!library.getDataLoaded('item')) {
|
||||
Zotero.debug("Waiting for items to load for library " + library.libraryID);
|
||||
await library.waitForDataLoad('item');
|
||||
if (collectionTreeRow.type !== 'suggestedItems') {
|
||||
var library = Zotero.Libraries.get(collectionTreeRow.ref.libraryID);
|
||||
if (!library.getDataLoaded('item')) {
|
||||
Zotero.debug("Waiting for items to load for library " + library.libraryID);
|
||||
await library.waitForDataLoad('item');
|
||||
}
|
||||
}
|
||||
// Prevent a race-condition if rapidly clicking on different libraries without loaded
|
||||
// item data
|
||||
if (collectionsView.getRow(collectionsView.selection.focused)?.id !== collectionTreeRow.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
await itemsView.changeCollectionTreeRows([collectionTreeRow]);
|
||||
|
||||
itemsView.clearItemsPaneMessage();
|
||||
updateShowInZoteroButton();
|
||||
};
|
||||
|
||||
function onSearch()
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
<dialog
|
||||
id="select-items-dialog"
|
||||
orient="vertical"
|
||||
buttons="cancel,accept"
|
||||
buttons="extra1,cancel,accept"
|
||||
data-l10n-id="select-items-dialog"
|
||||
data-l10n-attrs="buttonlabelaccept"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1105,6 +1105,10 @@ select-items-window =
|
|||
.title = Select Items
|
||||
select-items-dialog =
|
||||
.buttonlabelaccept = Select
|
||||
select-items-suggested-items = Suggested Items
|
||||
select-items-library-column = Library
|
||||
select-items-show-in-zotero =
|
||||
.label = { general-show-in-library }
|
||||
select-items-convertToStandalone =
|
||||
.label = Convert to Standalone
|
||||
select-items-convertToStandaloneAttachment =
|
||||
|
|
|
|||
|
|
@ -406,10 +406,12 @@ $-attachmentIcons: (
|
|||
attachment-video
|
||||
);
|
||||
|
||||
// Non-item objects that can appear in the trash
|
||||
$-trashableObjectIcons: (
|
||||
// Non-item objects that can appear in the item tree
|
||||
$-nonItemIcons: (
|
||||
"collection",
|
||||
"search"
|
||||
"search",
|
||||
"library",
|
||||
"library-group"
|
||||
);
|
||||
|
||||
// Few icons that appear in color in the item tree
|
||||
|
|
@ -475,7 +477,7 @@ $-coloredIcons: (
|
|||
}
|
||||
}
|
||||
|
||||
@each $icon in $-trashableObjectIcons {
|
||||
@each $icon in $-nonItemIcons {
|
||||
.icon-css.icon-#{$icon} {
|
||||
@include svgicon($icon, $color, "16", "collection-tree");
|
||||
}
|
||||
|
|
|
|||
152
test/tests/selectItemsDialogTest.js
Normal file
152
test/tests/selectItemsDialogTest.js
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
"use strict";
|
||||
|
||||
describe("Select Items Dialog", function () {
|
||||
var candidate1;
|
||||
var candidate2;
|
||||
var childNote;
|
||||
var excluded;
|
||||
var collection;
|
||||
var group;
|
||||
|
||||
async function openDialog(itemIDs) {
|
||||
let io = {
|
||||
dataOut: null,
|
||||
itemIDs,
|
||||
multiSelect: false,
|
||||
onlyRegularItems: true,
|
||||
deferred: Zotero.Promise.defer()
|
||||
};
|
||||
let dialogPromise = waitForWindow('chrome://zotero/content/selectItemsDialog.xhtml');
|
||||
Services.ww.openWindow(null, 'chrome://zotero/content/selectItemsDialog.xhtml', '',
|
||||
'chrome,dialog=no,centerscreen,resizable=yes', io);
|
||||
let dialog = await dialogPromise;
|
||||
await waitForCallback(() => dialog.loaded);
|
||||
return { dialog, io };
|
||||
}
|
||||
|
||||
async function selectSuggestedItems(dialog) {
|
||||
let row = dialog.collectionsView.getRowIndexByID('suggested-items');
|
||||
assert.isNumber(row);
|
||||
await dialog.collectionsView.selectWait(row);
|
||||
}
|
||||
|
||||
function assertLibraryColumnPosition(dialog) {
|
||||
let columns = dialog.itemsView._getColumns();
|
||||
let libraryColumnIndex = columns.findIndex(column => column.dataKey === 'library');
|
||||
let attachmentColumnIndex = columns.findIndex(column => column.dataKey === 'hasAttachment');
|
||||
assert.equal(libraryColumnIndex, attachmentColumnIndex - 1);
|
||||
assert.isAtLeast(parseInt(columns[libraryColumnIndex].width), 120);
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
candidate1 = await createDataObject('item', { title: 'Candidate One' });
|
||||
group = await createGroup({ name: 'Candidate Library' });
|
||||
candidate2 = await createDataObject('item', {
|
||||
title: 'Candidate Two',
|
||||
libraryID: group.libraryID
|
||||
});
|
||||
childNote = await createDataObject('item', { itemType: 'note', parentID: candidate1.id });
|
||||
excluded = await createDataObject('item', { title: 'Excluded Item' });
|
||||
collection = await createDataObject('collection', { name: 'Browsable Collection' });
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await group.eraseTx();
|
||||
});
|
||||
|
||||
describe("with suggestions from multiple libraries", function () {
|
||||
var dialog;
|
||||
var showInLibrary;
|
||||
|
||||
before(async function () {
|
||||
({ dialog } = await openDialog([candidate1.id, candidate2.id]));
|
||||
showInLibrary = sinon.stub(Zotero.Utilities.Internal, 'showInLibrary').resolves();
|
||||
await waitForCallback(() => dialog.itemsView.getRowIndexByID(candidate2.id) !== false);
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
await selectSuggestedItems(dialog);
|
||||
await dialog.itemsView.setFilter('search', '');
|
||||
dialog.document.getElementById('zotero-tb-search').searchTextbox.value = '';
|
||||
dialog.itemsView.selection.clearSelection();
|
||||
showInLibrary.resetHistory();
|
||||
});
|
||||
|
||||
after(function () {
|
||||
showInLibrary.restore();
|
||||
if (!dialog.closed) {
|
||||
dialog.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("should show Suggested Items above the normal collection tree", function () {
|
||||
assert.equal(dialog.collectionsView.getRow(0).id, 'suggested-items');
|
||||
assert.equal(dialog.collectionsView.getRow(0).type, 'suggestedItems');
|
||||
assert.isTrue(dialog.collectionsView.getRow(1).isSeparator());
|
||||
assert.isNumber(
|
||||
dialog.collectionsView.getRowIndexByID(`L${Zotero.Libraries.userLibraryID}`)
|
||||
);
|
||||
assert.isNumber(dialog.collectionsView.getRowIndexByID(`C${collection.id}`));
|
||||
assert.isFalse(dialog.document.getElementById('zotero-collections-tree-container').hidden);
|
||||
assert.isFalse(dialog.document.getElementById('search-toolbar').hidden);
|
||||
});
|
||||
|
||||
it("should show only regular candidate items", function () {
|
||||
assert.isNumber(dialog.itemsView.getRowIndexByID(candidate1.id));
|
||||
assert.isNumber(dialog.itemsView.getRowIndexByID(candidate2.id));
|
||||
assert.strictEqual(dialog.itemsView.getRowIndexByID(childNote.id), false);
|
||||
assert.strictEqual(dialog.itemsView.getRowIndexByID(excluded.id), false);
|
||||
});
|
||||
|
||||
it("should show the Library column before Attachments", async function () {
|
||||
let libraryColumn = dialog.itemsView._getColumns()
|
||||
.find(column => column.dataKey === 'library');
|
||||
assert.isOk(libraryColumn);
|
||||
assert.isFalse(libraryColumn.hidden);
|
||||
assertLibraryColumnPosition(dialog);
|
||||
|
||||
await dialog.itemsView.selectItem(candidate2.id);
|
||||
let rowIndex = dialog.itemsView.getRowIndexByID(candidate2.id);
|
||||
let row = dialog.document.getElementById(`${dialog.itemsView.id}-row-${rowIndex}`);
|
||||
let icon = row.querySelector('.library .icon-library-group');
|
||||
assert.isOk(icon);
|
||||
let iconStyle = dialog.getComputedStyle(icon);
|
||||
assert.notEqual(iconStyle.backgroundImage, 'none');
|
||||
assert.equal(iconStyle.width, '16px');
|
||||
assert.equal(iconStyle.height, '16px');
|
||||
});
|
||||
|
||||
it("should enable Show in Library for a selected suggestion", async function () {
|
||||
let button = dialog.document.querySelector("dialog button[dlgtype='extra1']");
|
||||
assert.isFalse(button.hidden);
|
||||
assert.isTrue(button.disabled);
|
||||
|
||||
await dialog.itemsView.selectItem(candidate2.id);
|
||||
assert.isFalse(button.disabled);
|
||||
button.click();
|
||||
await waitForCallback(() => showInLibrary.called);
|
||||
sinon.assert.calledOnceWithExactly(showInLibrary, [candidate2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with suggestions only from My Library", function () {
|
||||
var dialog;
|
||||
|
||||
before(async function () {
|
||||
({ dialog } = await openDialog([candidate1.id]));
|
||||
await waitForCallback(() => dialog.itemsView.getRowIndexByID(candidate1.id) !== false);
|
||||
});
|
||||
|
||||
after(function () {
|
||||
if (!dialog.closed) {
|
||||
dialog.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("should not add the Library column", function () {
|
||||
let libraryColumn = dialog.itemsView._getColumns()
|
||||
.find(column => column.dataKey === 'library');
|
||||
assert.isUndefined(libraryColumn);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue