define([
"jquery",
"underscore",
"backbone",
"models/portals/PortalModel",
"models/UserModel",
"text!templates/alert.html",
"text!templates/loading.html",
"text!templates/portals/portal.html",
"text!templates/portals/editPortals.html",
"views/portals/PortalHeaderView",
"views/portals/PortalDataView",
"views/portals/PortalSectionView",
"views/portals/PortalMetricsView",
"views/portals/PortalMembersView",
"views/portals/PortalLogosView",
"views/portals/PortalVisualizationsView",
], (
$,
_,
Backbone,
Portal,
User,
AlertTemplate,
LoadingTemplate,
PortalTemplate,
EditPortalsTemplate,
PortalHeaderView,
PortalDataView,
PortalSectionView,
PortalMetricsView,
PortalMembersView,
PortalLogosView,
PortalVisualizationsView,
) => {
"use_strict";
/**
* @class PortalView
* @classdesc The PortalView is a generic view to render
* portals, it will hold portal sections
* @classcategory Views/Portals
* @extends Backbone.View
* @constructor
*/
const PortalView = Backbone.View.extend(
/** @lends PortalView.prototype */ {
/**
* The Portal element
* @type {string}
*/
el: "#Content",
/**
* The type of View this is
* @type {string}
*/
type: "Portal",
/**
* The currently active section view
* @type {PortalSectionView}
*/
activeSection: undefined,
/**
* The currently active section label. e.g. Data, Metrics, Settings, etc.
* @type {string}
*/
activeSectionLabel: "",
/**
* The names of all sections in this portal editor
* @type {Array}
*/
sectionNames: [],
/**
* The seriesId of the portal document
* @type {string}
*/
portalId: "",
/**
* The unique short name of the portal
* @type {string}
*/
label: "",
/**
* Flag to add section name to URL. Enabled by default.
* @type {boolean}
*/
displaySectionInUrl: true,
/**
* The subviews contained within this view to be removed with onClose
* @type {Array}
*/
subviews: new Array(), // Could be a literal object {} */
/**
* A reference to the Portal Logos View that displays the logos of this portal.
* @type PortalLogosView
*/
logosView: null,
/**
* A Portal Model is associated with this view and gets created during render()
* @type {Portal}
*/
model: null,
/**
* A User Model is associated with this view for rendering node/user views
* @type {User}
*/
userModel: null,
/* Renders the compiled template into HTML */
template: _.template(PortalTemplate),
//A template to display a notification message
alertTemplate: _.template(AlertTemplate),
//A template for displaying a loading message
loadingTemplate: _.template(LoadingTemplate),
// Template for the 'edit portal' button
editPortalsTemplate: _.template(EditPortalsTemplate),
/**
* A jQuery selector for the element that a single section link will be inserted into
* @type {string}
*/
sectionLinkContainer: ".section-link-container",
/**
* A jQuery selector for the elements that are links to the individual sections
* @type {string}
*/
sectionLinks: ".portal-section-link",
/**
* A jQuery selector for the section elements
* @type {string}
*/
sectionEls: ".portal-section-view",
/**
* A jQuery selection for the element that will contain the Edit button.
* @type {string}
* @since 2.14.0
*/
editButtonContainer: ".edit-portal-link-container",
/**
* The events this view will listen to and the associated function to call.
* @type {Object}
*/
events: {
"click .portal-section-link": "handleSwitchSection",
"click .section-links-container": "toggleSectionLinks",
},
/**
* Is executed when a new PortalView is created
*/
initialize(options) {
// Set the current PortalView properties
this.portalId = options.portalId ? options.portalId : undefined;
this.model = options.model ? options.model : undefined;
this.nodeView = options.nodeView ? options.nodeView : undefined;
this.label = options.label ? options.label : undefined;
this.activeSection = options.activeSection
? options.activeSection
: undefined;
this.activeSectionLabel = options.activeSectionLabel
? options.activeSectionLabel
: undefined;
},
/**
* Initial render of the PortalView
*
* @return {PortalView} Returns itself for easy function stacking in the app
*/
render() {
const view = this;
// Make sure the subviews array is reset
this.subviews = new Array();
// Add the overall class immediately so the navbar is styled correctly right away
$("body").addClass("PortalView");
this.$el.html(
this.loadingTemplate({
msg: "Loading...",
}),
);
// Perform specific label checks
if (!MetacatUI.nodeModel.get("checked")) {
this.listenToOnce(MetacatUI.nodeModel, "change:checked", () => {
// perform node checks
if (view.isNode(view.label)) {
view.nodeView = true;
view.renderAsNode();
} else {
view.nodeView = false;
view.renderAsPortal();
}
});
this.listenToOnce(MetacatUI.nodeModel, "error", () => {
this.showError(null, "Couldn't get the DataONE Node info document");
});
} else if (MetacatUI.nodeModel.get("error")) {
this.showError(null, "Couldn't get the DataONE Node info document");
} else if (this.isNode(this.label)) {
this.nodeView = true;
this.renderAsNode();
} else if (!this.isNode(this.label)) {
this.nodeView = false;
this.renderAsPortal();
}
return this;
},
/**
* Entry point for portal rendering
*/
renderAsPortal() {
// At this point we know that the given label is not a
// repository short identifier
// Create a new Portal model
if (this.model === undefined || this.model === null) {
this.model = new Portal({
seriesId: this.portalId,
label: this.label,
});
}
// When the model has been synced, render the results
this.stopListening();
this.listenToOnce(this.model, "sync", this.renderPortal);
//If the portal isn't found, display a 404 message
this.listenTo(this.model, "notFound", this.handleNotFound);
//Listen to errors that might occur during fetch()
this.listenToOnce(this.model, "error", this.showError);
//Fetch the model
this.model.fetch({ objectOnly: true });
},
/**
* Entry point for a repository portal view
* At this point we know for sure that a given label/username is a repository user
*/
renderAsNode() {
const view = this;
// Create a UserModel with the username given
this.userModel = new User({
username: view.label,
});
this.userModel.saveAsNode();
// get the node Info
const nodeInfo = _.find(
MetacatUI.nodeModel.get("members"),
(nodeModel) =>
nodeModel.identifier.toLowerCase() ===
`urn:node:${view.label.toLowerCase()}`,
);
this.nodeInfo = nodeInfo;
this.nodeName = this.nodeInfo.name;
this.portalId = this.nodeInfo.identifier;
// create a portal model for repository
this.model = new Portal({
seriesId: this.portalId,
label: view.label,
name: this.nodeInfo.name,
description: this.nodeInfo.description,
});
// remove the members section directly from the model
this.model.removeSection("members");
this.model.createNodeAttributes(this.nodeInfo);
// Setting the repo specific statsModel
const statsSearchModel = this.userModel.get("searchModel").clone();
statsSearchModel
.set("exclude", [], { silent: true })
.set("formatType", [], { silent: true });
MetacatUI.statsModel.set("query", statsSearchModel.getQuery());
MetacatUI.statsModel.set("searchModel", statsSearchModel);
if (
_.contains(
MetacatUI.appModel.get("dataoneHostedRepos"),
this.nodeInfo.identifier,
)
) {
MetacatUI.statsModel.set("mdqImageId", this.nodeInfo.identifier);
}
// render repository view as portal view
this.renderPortal();
},
/**
* Render the Portal view
*/
renderPortal() {
// Set the document title to the portal name
MetacatUI.appModel.set("title", this.model.get("name"));
MetacatUI.appModel.set("description", this.model.get("description"));
// Getting the correct portal label and seriesID
this.label = this.model.get("label");
this.portalId = this.model.get("seriesId");
// Remove the listeners that were set during the fetch() process
this.stopListening(this.model, "notFound", this.handleNotFound);
this.stopListening(this.model, "error", this.showError);
//If this is in DataONE Plus Preview Mode, check that the portal is
// a Plus portal before rendering. Member Node portals are always displayed.
if (
MetacatUI.appModel.get("dataonePlusPreviewMode") &&
!this.nodeView
) {
const sourceMN = this.model.get("datasource");
//Check if the portal source node is from the active alt repo OR is
// configured as a Plus portal.
if (
typeof sourceMN != "string" ||
(sourceMN !=
MetacatUI.appModel.get("defaultAlternateRepositoryId") &&
!_.findWhere(
MetacatUI.appModel.get("dataonePlusPreviewPortals"),
{ datasource: sourceMN, seriesId: this.model.get("seriesId") },
))
) {
//Get the name of the source member node
const sourceMNName = "original data repository",
mnURL = "";
if (typeof sourceMN == "string") {
const sourceMNObject = MetacatUI.nodeModel.getMember(sourceMN);
if (sourceMNObject) {
sourceMNName = sourceMNObject.name;
//If there is a baseURL string
if (sourceMNObject.baseURL) {
//Parse out the origin of the baseURL string. We want to crop out the /metacat/d1/mn parts.
mnURL =
sourceMNObject.baseURL.substring(
0,
sourceMNObject.baseURL.lastIndexOf("."),
) +
sourceMNObject.baseURL.substring(
sourceMNObject.baseURL.lastIndexOf("."),
sourceMNObject.baseURL.indexOf(
"/",
sourceMNObject.baseURL.lastIndexOf("."),
),
);
}
}
}
//Show a message that the portal can be found on the repository website.
const message = $(document.createElement("h3")).addClass(
"center stripe",
);
message.text(
"The " +
this.model.get("name") +
" " +
MetacatUI.appModel.get("portalTermSingular") +
" can be viewed in the ",
);
if (mnURL) {
message.append(
$(document.createElement("a"))
.attr("href", mnURL)
.attr("target", "_blank")
.text(sourceMNName),
);
} else {
message.append(sourceMNName);
}
this.$el.html(message);
return;
}
}
// Check for theme/layout settings and add the required files
this.addTheming();
// Insert the overall portal template
this.$el.html(this.template(this.model.toJSON()));
// Render the header view
this.headerView = new PortalHeaderView({
model: this.model,
nodeView: this.nodeView,
});
this.headerView.render();
this.subviews.push(this.headerView);
// only displaying the edit button for non-repository profiles
if (!this.nodeView) {
// Add edit button if user is authorized
this.insertOwnerControls();
}
// Render the content sections
_.each(
this.model.get("sections"),
function (section) {
this.addSection(section);
},
this,
);
// Render the Data section
if (this.model.get("hideData") !== true) {
this.sectionDataView = new PortalDataView({
model: this.model,
sectionName: "Data",
id: "Data",
nodeView: this.nodeView,
});
this.subviews.push(this.sectionDataView);
this.$("#portal-sections").append(this.sectionDataView.el);
//Render the section view and add it to the page
this.sectionDataView.render();
this.addSectionLink(this.sectionDataView);
}
//Render the metrics section link
if (this.model.get("hideMetrics") !== true) {
//Create a PortalMetricsView
this.metricsView = new PortalMetricsView({
model: this.model,
id: this.model.get("metricsLabel"),
uniqueSectionName: this.model.get("metricsLabel"),
nodeView: this.nodeView,
nodeName: this.nodeName,
});
this.subviews.push(this.metricsView);
this.$("#portal-sections").append(this.metricsView.el);
this.metricsView.render();
this.addSectionLink(this.metricsView);
}
// Render the members section
if (
this.model.get("hideMembers") !== true &&
(this.model.get("associatedParties").length ||
this.model.get("acknowledgments"))
) {
this.sectionMembersView = new PortalMembersView({
model: this.model,
id: "Members",
sectionName: "Members",
});
this.subviews.push(this.sectionMembersView);
this.$("#portal-sections").append(this.sectionMembersView.el);
//Render the section view and add it to the page
this.sectionMembersView.render();
this.addSectionLink(this.sectionMembersView);
}
// Render the logos at the bottom of the portal page
const ackLogos = this.model.get("acknowledgmentsLogos") || [];
if (ackLogos.length) {
this.logosView = new PortalLogosView();
this.logosView.logos = ackLogos;
this.subviews.push(this.logosView);
this.logosView.render();
this.$(".portal-view").append(this.logosView.el);
}
// Re-order the section tabs according the the portal editor's preference,
// if one has been set
try {
const pageOrder = this.model.get("pageOrder");
if (pageOrder && pageOrder.length) {
const linksContainer = this.el.querySelector(
"#portal-section-tabs",
);
const sortableLinks = this.el.querySelectorAll(
"#portal-section-tabs .section-link-container",
);
const sortableLinksArray = Array.prototype.slice.call(
sortableLinks,
0,
);
// sort the links according the pageOrder
sortableLinksArray.sort((a, b) => {
const aName = $(a).text();
const bName = $(b).text();
const aIndex = pageOrder.indexOf(aName);
const bIndex = pageOrder.indexOf(bName);
// If the label can't be found in the list of labels, place it at the end
if (bIndex === -1) {
return +1;
}
if (aIndex === -1) {
return -1;
}
// Sort backwards, because we use preprend
return bIndex - aIndex;
});
// Rearrange the links in the DOM
for (i = 0; i < sortableLinksArray.length; ++i) {
linksContainer.prepend(sortableLinksArray[i]);
}
}
} catch (error) {
console.log(
"Error re-arranging tabs according to the pageOrder option. Error message: " +
error,
);
}
//Switch to the active section
this.switchSection();
//Scroll to an inner-page link if there is one specified
if (window.location.hash && this.$(window.location.hash).length) {
MetacatUI.appView.scrollTo(this.$(window.location.hash));
}
// Save reference to this view
const view = this;
// On mobile, hide section tabs a moment after page loads so
// users notice where they are
setTimeout(function () {
view.toggleSectionLinks();
}, 700);
// On mobile where the section-links-container is set to fixed,
// hide the portal navigation element when user scrolls down,
// show again when the user scrolls up.
MetacatUI.appView.prevScrollpos = window.pageYOffset;
$(window).on("scroll", "", undefined, this.handleScroll);
},
/**
* Checks the portal model for theme or layout options. If there are any, and if
* they are supported, then add the associated CSS.
*/
addTheming() {
try {
// Check for theme and layout settings.
const theme = this.model.get("theme");
const layout = this.model.get("layout");
// TODO: make supported themes an app model config option?
const supportedThemes = ["dark", "light"];
const supportedLayouts = ["panels"];
// We must remove theme/layout CSS when the user navigates away from the
// portal in onClose(). To do this, we need to keep track of which CSS is
// added during this step.
const view = this;
view.addedThemeCSS = [];
// Layout should be added before theme for CSS rules to work together properly
// when there is a theme + layout
if (layout && supportedLayouts.includes(layout)) {
require([
"text!" +
MetacatUI.root +
"/css/portal-layouts/" +
layout +
".css",
], function (ThemeCss) {
const cssID = "portal-layout-" + layout;
MetacatUI.appModel.addCSS(ThemeCss, cssID);
view.addedThemeCSS.push(cssID);
});
}
if (theme && supportedThemes.includes(theme)) {
require([
"text!" + MetacatUI.root + "/css/portal-themes/" + theme + ".css",
], function (ThemeCss) {
const cssID = "portal-theme-" + theme;
MetacatUI.appModel.addCSS(ThemeCss, cssID);
view.addedThemeCSS.push(cssID);
});
}
} catch (error) {
console.log(
"There was an error adding theme and/or layout styles in a PortalView" +
". Error details: " +
error,
);
}
},
/**
* toggleSectionLinks - show or hide the section links nav. Used for
* mobile/small screens only.
*/
toggleSectionLinks() {
try {
// Only toggle the section links on mobile. On mobile, the
// ".show-sections-toggle" is visible.
if (this.$(".show-sections-toggle").is(":visible")) {
this.$("#portal-section-tabs").slideToggle();
}
} catch (e) {
console.log("Failed to toggle section links, error message: " + e);
}
},
/*
* Checks the authority for the logged in user for this portal and
* inserts control elements onto the page for the user to interact
* with the portal. So far, this is just an 'edit portal' button.
*/
insertOwnerControls() {
// Insert the button into the navbar
const container = $(this.editButtonContainer);
const model = this.model;
this.listenToOnce(this.model, "change:isAuthorized", function () {
if (!model.get("isAuthorized")) {
return false;
} else {
container.html(
this.editPortalsTemplate({
editButtonText:
"Edit " + MetacatUI.appModel.get("portalTermSingular"),
pathToEdit:
MetacatUI.root +
"/edit/" +
MetacatUI.appModel.get("portalTermPlural") +
"/" +
model.get("label"),
}),
);
}
});
this.model.checkAuthority("write");
},
/**
* Update the window location path with the active section name
* @param {boolean} [showSectionLabel] - If true, the section label will be added to the path
* @param {boolean} [retainSearchQuery] Whether to keep the search query
* params during a path change. These should be kept when the page is
* loading initially.
*/
updatePath(showSectionLabel, retainSearchQuery) {
const label = this.model.get("label") || this.newPortalTempName;
const originalLabel =
this.model.get("originalLabel") || this.newPortalTempName;
const pathName = decodeURIComponent(window.location.pathname)
.substring(MetacatUI.root.length)
// remove trailing forward slash if one exists in path
.replace(/\/$/, "");
// Add or replace the label and section part of the path with updated values.
// pathRE matches "/label/section", where the "/section" part is optional
const pathRE = new RegExp(
"\\/(" + label + "|" + originalLabel + ")(\\/[^\\/]*)?$",
"i",
);
let newPathName = pathName.replace(pathRE, "") + "/" + label;
if (showSectionLabel && this.activeSection) {
newPathName += "/" + this.activeSection.uniqueSectionLabel;
}
const searchQueryString = new URL(window.location.href).search;
// Support optional parameters for loading a portal's view from URL.
if (retainSearchQuery && searchQueryString) {
newPathName += searchQueryString;
}
// Update the window location
MetacatUI.uiRouter.navigate(newPathName, { trigger: false });
this.model.reportSectionChange(this.activeSection?.model);
},
/**
* Gets a list of section names from tab elements and updates the
* sectionNames attribute on this view.
*/
updateSectionNames() {
// Get the section names from the tab elements
const sectionNames = [];
this.$(this.sectionLinks).each((i, anchorEl) => {
sectionNames[i] = $(anchorEl).attr("href").substring(1);
});
// Set the array of sectionNames on the view
this.sectionNames = sectionNames;
},
/**
* Manually switch to a section subview by making the tab and tab panel active.
* Navigation between sections is usually handled automatically by the Bootstrap
* library, but a manual switch may be necessary sometimes
* @param {PortalSectionView} [portalSectionView] - The section view to switch to. If not given, defaults to the activeSection set on the view.
*/
switchSection(portalSectionView) {
// Create a flag for whether the section label should be shown in the URL
let showSectionLabelInURL = true;
let sectionView = portalSectionView;
// If no section view is given, use the active section in the view.
if (!sectionView) {
// Use the sectionView set already
if (this.activeSection) {
sectionView = this.activeSection;
}
// Or find the section view by name, which may have been passed through the URL
else if (this.activeSectionLabel) {
sectionView = this.getSectionByLabel(this.activeSectionLabel);
}
}
// If no section view was indicated, just default to the first visible one
if (!sectionView) {
sectionView = this.$(this.sectionLinkContainer).first().data("view");
// If we are defaulting to the first section, don't show the section label in the URL
showSectionLabelInURL = false;
// If there are no section views on the page at all, exit now
if (!sectionView) {
return;
}
}
// Update the activeSection set on the view
this.activeSection = sectionView;
// Activate the section content
this.$(this.sectionEls).each((i, contentEl) => {
if ($(contentEl).data("view") == sectionView) {
$(contentEl).addClass("active");
} else {
// make sure no other sections are active
$(contentEl).removeClass("active");
}
});
// Activate the link to the content
this.$(this.sectionLinkContainer).each((i, linkEl) => {
if ($(linkEl).data("view") == sectionView) {
$(linkEl).addClass("active");
} else {
// make sure no other sections are active
$(linkEl).removeClass("active");
}
});
// If the section view has post-render functionality, execute it now
if (typeof sectionView.postRender == "function") {
sectionView.postRender();
}
// Eventually, the panels layout will allow showing multiple sections at the
// same time in different panels. For now, the visualizations sections should
// take up the full height of the viewport (minus the header elements), and the
// footer should be hidden.
if (
this.model.get("layout") === "panels" &&
sectionView instanceof PortalVisualizationsView
) {
if (this.logosView) {
this.logosView.el.style.setProperty("display", "none");
}
if (MetacatUI.footerView) {
MetacatUI.footerView.hide();
}
} else {
if (this.logosView) {
this.logosView.el.style.removeProperty("display");
}
if (MetacatUI.footerView) {
MetacatUI.footerView.show();
}
}
if (!this.nodeView) {
// Update the location path with the new section name
this.updatePath(
showSectionLabelInURL,
// portalSectionView is undefined on initial page load, so keep the
// search query parameters that the user expects.
/* retainSearchQuery= */ !portalSectionView,
);
}
},
/**
* When a section link has been clicked, switch to that section
* @param {Event} e - The click event on the section link
*/
handleSwitchSection(e) {
e.preventDefault();
const sectionView = $(e.target)
.parents(this.sectionLinkContainer)
.first()
.data("view");
if (sectionView) {
this.switchSection(sectionView);
// If the user clicks a link and is not near the top of the page
// (i.e. on mobile), scroll to the top of the section content.
// Otherwise it might look like the page hasn't changed (e.g.
// when focus is on the footer)
if (window.pageYOffset > this.$("#portal-sections").offset().top) {
MetacatUI.appView.scrollTo(this.$("#portal-sections"));
}
}
},
/**
* Returns the section view that has a label matching the one given.
* @param {string} label - The label for the section
* @return {PortalSectionView|false} - Returns false if a matching section view isn't found
*/
getSectionByLabel(label) {
//If no label is given, exit
if (!label) {
return;
}
//Find the section view whose unique label matches the given label. Case-insensitive matching.
return _.find(this.subviews, function (view) {
if (typeof view.uniqueSectionLabel == "string") {
return view.uniqueSectionLabel.toLowerCase() == label.toLowerCase();
} else {
return false;
}
});
},
/**
* Creates and returns a unique label for the given section. This label is just used in the view,
* because portal sections can have duplicate labels. But unique labels need to be used for navigation in the view.
* @param {PortEditorSection} sectionModel - The section for which to create a unique label
* @return {string} The unique label string
*/
getUniqueSectionLabel(sectionModel) {
//Get the label for this section
const sectionLabel = sectionModel
.get("label")
.replace(/[^a-zA-Z0-9 ]/g, "")
.replace(/ /g, "-"),
unalteredLabel = sectionLabel,
sectionLabels = this.sectionLabels || [],
i = 2;
//Concatenate a number to the label if this one already exists
while (sectionLabels.includes(sectionLabel)) {
sectionLabel = unalteredLabel + i;
i++;
}
return sectionLabel;
},
/**
* Creates a PortalSectionView to display the content in the given portal
* section. Also creates a navigation link to the section.
*
* @param {PortalSectionModel} sectionModel - The section to render in this view
*/
addSection(sectionModel) {
//If this is a visualization Section, render it differently with PortalVizSectionView
if (sectionModel.get("sectionType") == "visualization") {
this.addVizSection(sectionModel);
return;
}
//All other portal section types are rendered with the basic PortalSectionView
else {
//Create a new PortalSectionView
const sectionView = new PortalSectionView({
model: sectionModel,
});
//Render the section
sectionView.render();
//Add the section view to this portal view
this.$("#portal-sections").append(sectionView.el);
this.addSectionLink(sectionView);
//Create a unique label for this section and save it
const uniqueLabel = this.getUniqueSectionLabel(sectionModel);
//Set the unique section label for this view
sectionView.uniqueSectionLabel = uniqueLabel;
this.subviews.push(sectionView);
}
},
/**
* Creates a PortalSectionView to display the content in the given portal
* section. Also creates a navigation link to the section.
* @param {PortalVizSectionModel} sectionModel - The visualization section to render in this view
*
*/
addVizSection(sectionModel) {
//Create a new PortalSectionView
const sectionView = new PortalVisualizationsView({
model: sectionModel,
});
//Render the section
sectionView.render();
//Add the section view to this portal view
this.$("#portal-sections").append(sectionView.el);
this.addSectionLink(sectionView);
//Create a unique label for this section and save it
const uniqueLabel = this.getUniqueSectionLabel(sectionModel);
//Set the unique section label for this view
sectionView.uniqueSectionLabel = uniqueLabel;
this.subviews.push(sectionView);
},
/**
* Add a link to a section of this portal page
* @param {PortalSectionView} sectionView - The view to add a link to
*/
addSectionLink(sectionView) {
const label = sectionView.getName();
const hrefLabel = sectionView.getName({ linkFriendly: true });
//Create a navigation link
this.$("#portal-section-tabs").append(
$(document.createElement("li"))
.addClass("section-link-container")
.data("view", sectionView)
.append(
$(document.createElement("a"))
.text(label)
.attr("href", "#" + hrefLabel)
.attr("data-toggle", "tab")
.addClass("portal-section-link")
.data("view", sectionView),
),
);
},
/**
* Handles the case where the PortalModel is fetched and nothing is found.
*/
handleNotFound() {
const view = this;
// If the user is NOT logged in OR
// if the user is logged in, and the last fetch was done with user credentials, then this Portal is either not accessible or non-existent
if (
(MetacatUI.appUserModel.get("checked") &&
!MetacatUI.appUserModel.get("loggedIn")) ||
(MetacatUI.appUserModel.get("checked") &&
MetacatUI.appUserModel.get("loggedIn") &&
this.model.get("fetchedWithAuth"))
) {
//Check if there is an indexing queue, because this model may still be indexing
const onError = function () {
//If the request to the monitor/status API fails, then show the not-found message
view.showNotFound.call(view);
},
onSuccess = function (sizeOfQueue) {
if (sizeOfQueue > 0) {
//Show a warning message about the index queue
MetacatUI.appView.showAlert(
"<p>We couldn't find a data portal named \" <span id='portal-view-not-found-name'></span>" +
"\".</p><p><i class='icon icon-exclamation-sign'></i> If this portal was created in the last few minutes, it may still be processing, since there are currently <b>" +
sizeOfQueue +
"</b> submissions in the queue.</p>",
"alert-warning",
view.$el,
);
view.$(".loading").remove();
view
.$("#portal-view-not-found-name")
.text(view.label || view.portalId);
} else {
//If the size of the queue is 0, then show the not-found message
view.showNotFound.call(view);
}
};
//Get the size of the index queue
MetacatUI.appLookupModel.getSizeOfIndexQueue(onSuccess, onError);
}
//If the user IS logged in and we haven't fetched the model with user authentication yet
else if (
MetacatUI.appUserModel.get("checked") &&
MetacatUI.appUserModel.get("loggedIn")
) {
//Fetch again now that the user is logged in
this.model.fetch();
}
//If the user login status is unknown, because authentication is still pending
else if (!MetacatUI.appUserModel.get("checked")) {
//Wait for the authentication to be checked, and then start this function over again
this.listenToOnce(
MetacatUI.appUserModel,
"change:checked",
this.handleNotFound,
);
}
},
/**
* If the given portal doesn't exist, display a Not Found message.
*/
showNotFound() {
const notFoundMessage =
"The data portal \"<span id='portal-view-not-found-name'></span>" +
"\" doesn't exist.",
notification = this.alertTemplate({
classes: "alert-error",
msg: notFoundMessage,
includeEmail: true,
});
this.$el.html(notification);
this.$("#portal-view-not-found-name").text(this.label || this.portalId);
},
/**
* Show an error message in this view
* @param {SolrResult} model
* @param {XMLHttpRequest.response|string} reponse
*/
showError(model, response) {
try {
const errorMsg = "",
errorClass = "alert-error",
icon = "frown",
portalTerm =
MetacatUI.appModel.get("portalTermSingular") || "portal",
errorTitle =
"Something went wrong displaying this " + portalTerm + ".";
// For errors resulting from authorization errors, use a friendlier and more
// helpful error message than the default message returned from fetch
if (response && response.status == 401) {
errorTitle = "You need permission to view this " + portalTerm + ".";
errorClass = "alert-info";
icon = "lock";
// Make a suggestion of how to fix the error based on whether the user is logged in or not.
if (!MetacatUI.appUserModel.get("loggedIn")) {
// If not logged in, suggest that the user signs in
errorMsg =
'<strong><a href="' +
MetacatUI.appModel.get("signInUrlOrcid") +
window.location.href +
'">Sign in</a></strong> to see if you have already been given access to view this ' +
portalTerm +
".";
} else {
// If signed in, suggest that the user contacts that portal owner
errorMsg =
"Contact the owner of this " +
portalTerm +
" to request access.";
}
// For all other types of errors
} else {
if (response && response.responseText) {
errorMsg = "Error details: " + $(response.responseText).text();
}
if (typeof response == "string") {
errorMsg = "Error details: " + response;
}
}
if (errorMsg) {
errorMsg = "<p>" + errorMsg + "</p>";
}
//Show the error message
MetacatUI.appView.showAlert(
"<h4><i class='icon icon-" +
icon +
"'></i>" +
errorTitle +
"</h4>" +
errorMsg,
errorClass + " portal-alert-container",
this.$el,
0,
{ includeEmail: true },
);
//Remove the loading message from this view
this.$el.find(".loading").remove();
} catch (error) {
console.log(
"There was a problem trying to display the error message in the Portal View. Error details: " +
error,
);
}
},
/**
* This function is called whenever the window is scrolled.
*/
handleScroll() {
const menu = $(".section-links-container")[0],
menuHeight = $(menu).height(),
hiddenHeight = menuHeight * -1;
const currentScrollPos = window.pageYOffset;
if (MetacatUI.appView.prevScrollpos > currentScrollPos) {
//Get the height of any menu that may be displayed at the bottom of the page, too
menu.style.bottom = "0px";
} else {
menu.style.bottom = hiddenHeight + "px";
}
MetacatUI.appView.prevScrollpos = currentScrollPos;
},
/**
* This function is called when the app navigates away from this view.
* Any clean-up or housekeeping happens at this time.
*/
onClose() {
MetacatUI.appModel.resetTitle();
MetacatUI.appModel.resetDescription();
// Run subView onClose functions if they exist
for (const subView of this.subviews) {
if (typeof subView?.onClose === "function") {
subView.onClose();
}
}
//Remove each subview from the DOM and remove listeners
_.invoke(this.subviews, "remove");
this.subviews = new Array();
// Remove any CSS that was added for the theme or layout
if (this.addedThemeCSS && this.addedThemeCSS.length) {
this.addedThemeCSS.forEach(function (cssID) {
MetacatUI.appModel.removeCSS(cssID);
});
}
//Remove all listeners
this.stopListening();
//Reset the active alternate repository
//MetacatUI.appModel.set("activeAlternateRepositoryId", null);
//Delete the metrics view from this view
delete this.sectionMetricsView;
//Delete the model from this view
delete this.model;
//Remove the scroll listener
$(window).off("scroll", "", this.handleScroll);
$("body").removeClass("PortalView");
// Make sure the footer is visible (hidden for dataViz sections + panels layout)
MetacatUI.footerView.el.style.removeProperty("display");
document.body.style.removeProperty("--footer-height");
$("#editPortal").remove();
this.undelegateEvents();
},
/**
* Checks if the label is a repository
*
* @param {string} username - The portal label or the member node repository identifier
*/
isNode(username) {
if (username === undefined) {
this.showNotFound();
return;
}
const model = this;
const node = _.find(
MetacatUI.nodeModel.get("members"),
function (nodeModel) {
return (
nodeModel.shortIdentifier.toLowerCase() == username.toLowerCase()
);
},
);
return node && node !== undefined;
},
},
);
return PortalView;
});