Source: src/js/views/portals/PortalView.js

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;
});