define([
"jquery",
"underscore",
"backbone",
"collections/Filters",
"collections/SolrResults",
"views/PagerView",
"text!templates/portals/portalList.html",
], function ($, _, Backbone, Filters, SearchResults, PagerView, Template) {
/**
* @class PortalListView
* @classdesc A view that shows a list of Portals
* @classcategory Views/Portals
* @extends Backbone.View
* @screenshot views/portals/PortalListView.png
* @constructor
*/
return Backbone.View.extend(
/** @lends PortalListView.prototype */ {
/**
* An array of Filter models or Filter model JSON to use in the query.
* If not provided, a default query will be used.
* @type {Filter[]}
*/
filters: null,
/**
* A SolrResults collection that contains the results of the search for the portals
* @type {SolrResults}
*/
searchResults: new SearchResults(),
/**
* A comma-separated list of Solr index fields to retrieve when searching for portals
* @type {string}
* @default "id,seriesId,title,formatId,label,logo"
*/
searchFields:
"id,seriesId,title,formatId,label,logo,datasource,writePermission,changePermission,rightsHolder,abstract",
/**
* The number of portals to dispaly per page
* @default 10
* @type {number}
*/
numPortalsPerPage: 10,
/**
* The number of portals to retrieve and render in this view
* @default 100
* @type {number}
*/
numPortals: 100,
/**
* An array of additional SolrResult models for portals that will be displayed
* in this view in addition to the SolrResults found as a result of the search.
* These could be portals that wouldn't otherwise be found by a search but should be displayed anyway.
* @type {SolrResult[]}
*/
additionalPortalsToDisplay: [],
/**
* The message to display when there are no portals in this list
* @type {string}
*/
noResultsMessage:
"You haven't created or have access to any " +
MetacatUI.appModel.get("portalTermPlural") +
" yet.",
/**
* A jQuery selector for the element that the list should be inserted into
* @type {string}
*/
listContainer: ".portal-list-container",
/**
* A jQuery selector for the element that the Create Portal should be inserted into
* @type {string}
*/
createBtnContainer: ".create-btn-container",
/**
* References to templates for this view. HTML files are converted to Underscore.js templates
*/
template: _.template(Template),
/**
* Initializes a new view
*/
initialize: function () {
//Create a new SearchResults collection
this.searchResults = new SearchResults();
},
/**
* Renders the list of portals
*/
render: function () {
try {
//If the "my portals" feature is disabled, exit now
if (MetacatUI.appModel.get("showMyPortals") === false) {
return;
}
//Insert the template
this.$el.html(this.template());
//If there are no given filters, create default ones
if (!this.filters) {
//Create search filters for finding the portals
var filters = new Filters();
//Filter datasets that the user has ownership of
filters.addWritePermissionFilter();
this.filters = filters;
}
//If the filters set on this view is an array of JSON, add it to a Filters collection
else if (
this.filters.length &&
!Filters.prototype.isPrototypeOf(this.filters)
) {
//Create search filters for finding the portals
var filters = new Filters();
filters.add(this.filters);
this.filters = filters;
}
//If there is an empty array, create a new Filters collection
else if (!this.filters.length) {
this.filters = new Filters();
}
//Get the search results and render them
this.getSearchResults();
//Display any additional portals in the list that have been passed to
// the view directly.
_.each(
this.additionalPortalsToDisplay,
function (searchResult) {
//Get the list container element
var listContainer = this.$(this.listContainer);
//Remove any 'loading' elements before adding items to the list
listContainer.find(".loading").remove();
//Create a list item element and add the search result element
// to the list container
listContainer.append(this.createListItem(searchResult));
},
this,
);
if (this.additionalPortalsToDisplay.length) {
//While the search is being sent for the other portals in this list,
// show a loading sign underneath the additional portals we just displayed.
var loadingListItem = this.createListItem();
loadingListItem.html(
"<td class='loading subtle' colspan='4'>Loading more " +
MetacatUI.appModel.get("portalTermPlural") +
"...</td>",
);
this.$(this.listContainer).append(loadingListItem);
}
} catch (e) {
console.error(e);
}
},
/**
* Queries for the portal objects using the SearchResults collection
*/
getSearchResults: function () {
try {
//Filter by the portal format ID
this.filters.add({
fields: ["formatId"],
values: ["dataone.org/portals"],
matchSubstring: true,
exclude: false,
});
//Filter datasets by their ownership
this.filters.add({
fields: ["obsoletedBy"],
values: ["*"],
matchSubstring: false,
exclude: true,
});
//Get 100 rows
this.searchResults.rows = this.numPortals;
//The fields to return
this.searchResults.fields = this.searchFields;
//Set the query service URL
try {
if (MetacatUI.appModel.get("defaultAlternateRepositoryId")) {
var mnToQuery = _.findWhere(
MetacatUI.appModel.get("alternateRepositories"),
{
identifier: MetacatUI.appModel.get(
"defaultAlternateRepositoryId",
),
},
);
if (mnToQuery) {
this.searchResults.queryServiceUrl = mnToQuery.queryServiceUrl;
}
}
} catch (e) {
console.error("Could not get active alt repo. ", e);
}
//Set the query on the SearchResults
this.searchResults.setQuery(this.filters.getQuery());
//Listen to the search results collection and render the results when the search is complete
this.listenToOnce(this.searchResults, "reset", this.renderList);
//Listen to the search results collection for errors
this.listenToOnce(this.searchResults, "error", this.showError);
//Get the first page of results
this.searchResults.toPage(0);
} catch (e) {
this.showError();
console.error(
"Failed to fetch the SearchResults for the PortalsList: ",
e,
);
}
},
/**
* Renders each search result from the SolrResults collection
*/
renderList: function () {
try {
//Get the list container element
var listContainer = this.$(this.listContainer);
//If no search results were found, display a message
if (
(!this.searchResults || !this.searchResults.length) &&
!this.additionalPortalsToDisplay.length
) {
var row = this.createListItem();
row.html(
"<div class='no-results'>" + this.noResultsMessage + "</div>",
);
listContainer.html(row);
//Add a "Create" button to create a new portal
this.renderCreateButton();
return;
}
//Remove any 'loading' elements before adding items to the list
listContainer.find(".loading").remove();
//Iterate over each search result and render it
this.searchResults.each(function (searchResult) {
//Create a list item element and add the search result element
// to the list container
listContainer.append(this.createListItem(searchResult));
}, this);
//Add a "Create" button to create a new portal
this.renderCreateButton();
// Create a pager for this list if there are many portals
if (this.$(".portals-list-entry").length > this.numPortalsPerPage) {
var pager = new PagerView({
pages: this.$(".portals-list-entry"),
itemsPerPage: this.numPortalsPerPage,
});
this.$el.append(pager.render().el);
}
} catch (e) {
console.error(e);
this.showError();
}
},
/**
* Creates a table row for the given portal SolrResult model
* @param {SolrResult} - The SolrResult model that represent the portal
* @return {Element}
*/
createListItem: function (searchResult) {
try {
var listItem = $(document.createElement("div")).addClass(
"portals-list-entry",
);
if (searchResult && typeof searchResult.get == "function") {
//Don't render a list item for a portal that is already there
if (
this.$("tr[data-seriesId='" + searchResult.get("seriesId") + "']")
.length
) {
return listItem;
}
//Add an id to the list element
listItem.attr("data-seriesId", searchResult.get("seriesId"));
//Create a logo image
var logoImg = "";
var logoDiv = "";
// Add link to the portal to the list item
var link = $(document.createElement("a")).attr(
"href",
MetacatUI.root +
"/" +
MetacatUI.appModel.get("portalTermPlural") +
"/" +
encodeURIComponent(
searchResult.get("label") ||
searchResult.get("seriesId") ||
searchResult.get("id"),
),
);
if (searchResult.get("logo")) {
if (!searchResult.get("logo").startsWith("http")) {
var urlBase = "";
//If there are alt repos configured, use the datasource obbject service URL
if (
MetacatUI.appModel.get("alternateRepositories").length &&
searchResult.get("datasource")
) {
var sourceMN = _.findWhere(
MetacatUI.appModel.get("alternateRepositories"),
{ identifier: searchResult.get("datasource") },
);
if (sourceMN) {
urlBase = sourceMN.objectServiceUrl;
}
}
if (!urlBase) {
// use the resolve service if there is no object service url
// (e.g. in DataONE theme)
urlBase =
MetacatUI.appModel.get("objectServiceUrl") ||
MetacatUI.appModel.get("resolveServiceUrl");
}
searchResult.set("logo", urlBase + searchResult.get("logo"));
}
var logoImg = $(document.createElement("img"))
.attr("src", searchResult.get("logo"))
.attr("alt", searchResult.get("title") + " logo");
var logoLink = link.clone().append(logoImg);
logoDiv = $(document.createElement("div"))
.addClass("portal-logo")
.append(logoLink);
} else {
// Create an empty <div>, as no portal image is available.
logoDiv = $(document.createElement("div")).addClass(
"portal-logo",
);
}
var portalTitle = $(document.createElement("h5"))
.addClass("portal-title")
.text(searchResult.get("title"));
var titleLink = link.clone().append(portalTitle);
var descriptionText = searchResult.get("abstract") || "",
maxLength = window.innerWidth < 800 ? 150 : 300;
if (descriptionText.length > maxLength) {
descriptionText = descriptionText.substr(0, maxLength);
descriptionText = descriptionText.substr(
0,
Math.min(
descriptionText.length,
descriptionText.lastIndexOf(" "),
),
);
descriptionText += "...";
}
var description = $(document.createElement("div"))
.addClass("portal-description")
.append($(document.createElement("p")).text(descriptionText));
var portalInfo = $(document.createElement("div"))
.addClass("portal-info")
.append(titleLink, description);
var editDiv = $(document.createElement("div"))
.addClass("portal-edit-link")
.addClass("controls");
//Add all the elements to the row
listItem.append(logoDiv, portalInfo, editDiv);
//Construct an array of ownership subjects
var wPermission = searchResult.get("writePermission"),
cPermission = searchResult.get("changePermission"),
rightsHolder = searchResult.get("rightsHolder");
var owners = [];
[wPermission, cPermission, rightsHolder].forEach((subjects) => {
if (typeof subjects == "string") {
owners.push(subjects);
} else if (Array.isArray(subjects)) {
owners = owners.concat(subjects);
}
});
//Render an Edit button
if (MetacatUI.appUserModel.hasIdentityOverlap(owners)) {
//Create an Edit buttton
var editButton = $(document.createElement("a"))
.attr(
"href",
MetacatUI.root +
"/edit/" +
MetacatUI.appModel.get("portalTermPlural") +
"/" +
encodeURIComponent(
searchResult.get("label") ||
searchResult.get("seriesId") ||
searchResult.get("id"),
),
)
.text("Edit")
.addClass("btn edit");
editDiv.append(editButton);
}
}
//Return the list item
return listItem;
} catch (e) {
console.error(e);
return "";
}
},
/**
* Renders a "Create" button for the user to create a new portal
*/
renderCreateButton: function () {
try {
//If the authorization hasn't been checked yet
if (
MetacatUI.appUserModel.get("isAuthorizedCreatePortal") !== true &&
MetacatUI.appUserModel.get("isAuthorizedCreatePortal") !== false
) {
//Check is this user is authorized to create a new portal
this.listenToOnce(
MetacatUI.appUserModel,
"change:isAuthorizedCreatePortal",
this.renderCreateButton,
);
MetacatUI.appUserModel.isAuthorizedCreatePortal();
} else {
//Create a New portal buttton
var createButton = $(document.createElement("a"))
.addClass("btn btn-primary")
.append(
$(document.createElement("i")).addClass(
"icon icon-plus icon-on-left",
),
"New " + MetacatUI.appModel.get("portalTermSingular"),
);
var isNotAuthorizedNoBookkeeper =
!MetacatUI.appModel.get("enableBookkeeperServices") &&
MetacatUI.appUserModel.get("isAuthorizedCreatePortal") ===
false,
reachedLimitWithBookkeeper =
MetacatUI.appModel.get("enableBookkeeperServices") &&
MetacatUI.appUserModel.get("isAuthorizedCreatePortal") ===
false,
reachedLimitWithoutBookkeeper =
!MetacatUI.appModel.get("enableBookkeeperServices") &&
MetacatUI.appModel.get("portalLimit") <=
this.searchResults.length;
//If creating portals is disabled in the entire app, or is only limited to certain groups,
// then don't show the Create button.
if (isNotAuthorizedNoBookkeeper) {
return;
}
//If creating portals is enabled, but this person is unauthorized because of Bookkeeper info,
// then show the Create button as disabled.
else if (
reachedLimitWithBookkeeper ||
reachedLimitWithoutBookkeeper
) {
//Disable the button
createButton.addClass("disabled");
//Add the create button to the view
this.$(this.createBtnContainer).html(createButton);
var message =
"You've already reached the " +
MetacatUI.appModel.get("portalTermSingular") +
" limit for your ";
if (MetacatUI.appModel.get("enableBookkeeperServices")) {
message += MetacatUI.appModel.get("dataonePlusName");
if (MetacatUI.appModel.get("dataonePlusPreviewMode")) {
message += " free preview. ";
} else {
message += " subscription. ";
}
var portalQuotas = MetacatUI.appUserModel.getQuotas("portal");
if (portalQuotas.length) {
message +=
"(" +
portalQuotas[0].get("softLimit") +
" " +
(portalQuotas[0].get("softLimit") > 1
? MetacatUI.appModel.get("portalTermPlural")
: MetacatUI.appModel.get("portalTermSingular")) +
")";
}
message += " Contact us to upgrade your subscription.";
} else {
message += " account. ";
var portalLimit = MetacatUI.appModel.get("portalLimit");
if (portalLimit > 0) {
message +=
"(" +
portalLimit +
" " +
(portalLimit > 1
? MetacatUI.appModel.get("portalTermPlural")
: MetacatUI.appModel.get("portalTermSingular")) +
")";
}
}
//Add the tooltip to the button
createButton.tooltip({
placement: "top",
trigger: "hover click focus",
delay: {
show: 500,
},
title: message,
});
} else {
//Add the link URL to the button
createButton.attr(
"href",
MetacatUI.root +
"/edit/" +
MetacatUI.appModel.get("portalTermPlural"),
);
//Add the create button to the view
this.$(this.createBtnContainer).html(createButton);
}
//Reset the isAuthorizedCreatePortal attribute
MetacatUI.appUserModel.set("isAuthorizedCreatePortal", null);
}
} catch (e) {
console.error(e);
}
},
/**
* Displays an error message when rendering this view has failed.
*/
showError: function () {
//Remove the loading elements
this.$(this.listContainer).find(".loading").remove();
if (this.$(this.listContainer).children("tr").length == 0) {
//Show an error message
MetacatUI.appView.showAlert(
"Something went wrong while getting this list of portals.",
"alert-error",
this.$(this.listContainer),
);
}
},
},
);
});