define([
"jquery",
"underscore",
"backbone",
"clipboard",
"collections/UserGroup",
"models/UserModel",
"models/Stats",
"views/StatsView",
"views/DataCatalogView",
"views/UserGroupView",
"text!templates/userProfile.html",
"text!templates/alert.html",
"text!templates/loading.html",
"text!templates/userProfileMenu.html",
"text!templates/userSettings.html",
"text!templates/noResults.html",
], (
$,
_,
Backbone,
Clipboard,
UserGroup,
UserModel,
Stats,
StatsView,
DataCatalogView,
UserGroupView,
userProfileTemplate,
AlertTemplate,
LoadingTemplate,
ProfileMenuTemplate,
SettingsTemplate,
NoResultsTemplate,
) => {
"use strict";
const TEXT_TO_HTML_EL = (txt) => {
const tmp = document.createElement("div");
tmp.innerHTML = txt;
return tmp.firstChild;
};
/**
* @class UserView
* @classdesc A major view that displays a public profile for the user and a settings page for the logged-in user
* to manage their account info, groups, identities, and API tokens.
* @classcategory Views
* @screenshot views/UserView.png
* @augments Backbone.View
*/
const UserView = Backbone.View.extend(
/** @lends UserView.prototype */ {
el: "#Content",
// Templates
profileTemplate: _.template(userProfileTemplate),
alertTemplate: _.template(AlertTemplate),
loadingTemplate: _.template(LoadingTemplate),
settingsTemplate: _.template(SettingsTemplate),
menuTemplate: _.template(ProfileMenuTemplate),
noResultsTemplate: _.template(NoResultsTemplate),
/**
* A jQuery selector for the element that the PortalListView should be inserted into
* @type {string}
*/
portalListContainer: ".my-portals-container",
events: {
"click .section-link": "switchToSection",
"click .subsection-link": "switchToSubSection",
"click .token-generator": "getToken",
"click #mod-save-btn": "saveUser",
"click [highlight-subsection]": "highlightSubSection",
"keypress #add-group-name": "preventSubmit",
"click .token-tab": "switchTabs",
},
initialize() {
this.subviews = [];
},
// ------------------------------------------ Rendering the main parts of the view ------------------------------------------------//
render(options) {
// Don't render anything if the user profiles are turned off
if (MetacatUI.appModel.get("enableUserProfiles") === false) {
return this;
}
this.stopListening();
if (this.model) this.model.stopListening();
// Create a Stats model
this.statsModel = new Stats();
this.activeSection =
options && options.section ? options.section : "profile";
this.activeSubSection =
options && options.subsection ? options.subsection : "";
this.username =
options && options.username ? options.username : undefined;
// Add the container element for our profile sections
this.sectionHolder = $(document.createElement("section")).addClass(
"user-view-section",
);
this.$el.html(this.sectionHolder);
// Show the loading sign first
$(this.sectionHolder).html(this.loadingTemplate());
this.$el.show();
// set the header type
MetacatUI.appModel.set("headerType", "default");
// Render the user profile only after the app user's info has been checked
// This prevents the app from rendering the profile before the login process has completed - which would
// cause this profile to render twice (first before the user is logged in then again after they log in)
if (MetacatUI.appUserModel.get("checked")) this.renderUser();
else MetacatUI.appUserModel.on("change:checked", this.renderUser, this);
return this;
},
/**
* Update the window location path to route to /portals path
* @param {string} username - Short identifier for the member node
*/
forwardToPortals(username) {
const pathName = decodeURIComponent(window.location.pathname)
.substring(MetacatUI.root.length)
// remove trailing forward slash if one exists in path
.replace(/\/$/, "");
// Routes the /profile/{node-id} to /portals/{node-id}
const pathRE = /\/profile(\/[^/]*)?$/i;
const newPathName = `${pathName.replace(
pathRE,
"",
)}/${MetacatUI.appModel.get("portalTermPlural")}/${username}`;
// Update the window location
MetacatUI.uiRouter.navigate(newPathName, {
trigger: true,
replace: true,
});
},
renderUser() {
const view = this;
this.model = MetacatUI.appUserModel;
const username =
MetacatUI.appModel.get("profileUsername") || view.username;
const currentUser = MetacatUI.appUserModel.get("username") || "";
if (username.toUpperCase() === currentUser.toUpperCase()) {
// Case-insensitive matching of usernames
this.model = MetacatUI.appUserModel;
this.model.set("type", "user");
// If the user is logged in, display the settings options
if (this.model.get("loggedIn")) {
this.insertMenu();
this.renderProfile();
this.renderSettings();
this.resetSections();
}
}
// If this isn't the currently-logged in user, then let's find out more info about this account
else {
// Create a UserModel with the username given
this.model = new UserModel({
username,
});
// Is this a member node?
if (MetacatUI.nodeModel.get("checked") && this.model.isNode()) {
this.model.saveAsNode();
this.model.set(
"nodeInfo",
_.find(
MetacatUI.nodeModel.get("members"),
(nodeModel) =>
nodeModel.identifier.toLowerCase() ===
`urn:node:${username.toLowerCase()}`,
),
);
this.forwardToPortals(username);
return;
}
// If the node model hasn't been checked yet
if (!MetacatUI.nodeModel.get("checked")) {
const user = this.model;
this.listenTo(MetacatUI.nodeModel, "change:checked", () => {
if (user.isNode()) view.render();
});
}
// When we get the infomration about this account, then crender the profile
this.model.once("change:checked", this.renderProfile, this);
this.model.once("change:checked", this.resetSections, this);
// Get the info
this.model.getInfo();
}
// When the model is reset, refresh the page
this.listenTo(this.model, "reset", this.render);
},
renderProfile() {
// Insert the template first
const profileEl = $.parseHTML(
this.profileTemplate({
type: this.model.get("type"),
logo: this.model.get("logo") || "",
description: this.model.get("description") || "",
user: this.model.toJSON(),
}).trim(),
);
// If the profile is being redrawn, then replace it
if (this.$profile && this.$profile.length) {
// If the profile section is currently hidden, make sure we hide our new profile rendering too
if (!this.$profile.is(":visible")) $(profileEl).hide();
this.$profile.replaceWith(profileEl);
}
// If this is a fresh rendering, then append it to the page and save it
else this.sectionHolder.append(profileEl);
this.$profile = $(profileEl);
// If this user hasn't uploaded anything yet, display so
this.listenTo(this.statsModel, "change:totalCount", () => {
if (!this.statsModel.get("totalCount")) this.noActivity();
});
// Insert the user data statistics
this.insertStats();
// Insert the user's basic information
this.listenTo(this.model, "change:fullName", this.insertUserInfo);
this.insertUserInfo();
// Listen to changes in the user's search terms
this.listenTo(this.model, "change:searchModel", this.renderProfile);
// Insert this user's data content
this.insertContent();
// create the UserGroupView to generate the membership list
// this is the first call to UserGroupView so we instantiate it here
const groupView = new UserGroupView({ model: this.model });
this.subviews.push(groupView);
this.renderMembershipList();
},
renderMembershipList() {
// List the groups this user is in by creating usergroupview subview
// List the groups this user is in by creating usergroupview subview
const groupView = _.where(this.subviews, { type: "UserGroupView" }).at(
0,
);
if (this.model.get("type") === "group") {
// Create the User Group collection
const options = {
name: this.model.get("fullName"),
groupId: this.model.get("username"),
rawData: this.model.get("rawData") || null,
};
const userGroup = new UserGroup([], options);
// Create the group list and add it to the page
const viewOptions = { collapsable: false, showGroupName: false };
const groupList = groupView.createGroupList(userGroup, viewOptions);
this.$("#user-membership-container").html(groupList);
} else {
const groups = _.sortBy(this.model.get("isMemberOf"), "name");
if (!groups.length) {
this.$("#user-membership-header").hide();
return;
}
this.sectionHolder.append(
groupView
.insertMembership(groups, this.$("#user-membership-container"))
.html(),
);
}
},
renderGroupsSection() {
const groupView = _.where(this.subviews, { type: "UserGroupView" }).at(
0,
);
const container = this.$("#groups-container");
container.append(groupView.render().el);
},
renderSettings() {
// Don't render anything if the user profile settings are turned off
if (MetacatUI.appModel.get("enableUserProfileSettings") === false) {
return;
}
if (this.settingsEl) {
this.settingsEl.remove();
this.settingsEl = null;
}
const settingsText = this.settingsTemplate({
...this.model.toJSON(),
emailContact: MetacatUI.appModel.get("emailContact") || "",
});
this.settingsEl = TEXT_TO_HTML_EL(settingsText.trim());
// Insert the template first
this.sectionHolder.append(this.settingsEl);
this.$settings = this.$("[data-section='settings']");
// Draw the group list
this.renderGroupsSection();
// Listen for the identity list
this.listenTo(this.model, "change:identities", this.insertIdentityList);
this.insertIdentityList();
// Listen for the pending list
// Render the portals subsection
this.renderMyPortals();
// Listen for updates to person details
this.listenTo(
this.model,
"change:lastName change:firstName change:email change:registered",
this.updateModForm,
);
this.updateModForm();
// init autocomplete fields
this.setUpAutocomplete();
// Get the token right away
this.getToken();
},
/*
* Displays a menu for the user to switch between different views of the user profile
*/
insertMenu() {
if (this.menu) {
this.menu.remove();
this.menu = null;
}
// If the user is not logged in, then remove the menu
if (!MetacatUI.appUserModel.get("loggedIn")) {
this.$(".nav").remove();
return;
}
// Otherwise, insert the menu
const menuText = this.menuTemplate({
username: this.model.get("username"),
});
this.menu = TEXT_TO_HTML_EL(menuText.trim());
this.el.prepend(this.menu);
},
// ------------------------------------------ Navigating sections of view ------------------------------------------------//
switchToSection(e, sectionName) {
if (e) e.preventDefault();
const label = sectionName || $(e.target).attr("data-section");
// Hide all the sections first
$(this.sectionHolder).children().slideUp().removeClass(".active");
// Display the specified section
let activeSection = this.$(`.section[data-section='${label}']`);
if (!activeSection.length)
activeSection = this.$(".section[data-section='profile']");
$(activeSection).addClass("active").slideDown();
// Change the navigation tabs
this.$(".nav-tab").removeClass("active");
$(`.nav-tab[data-section='${label}']`).addClass("active");
// Find all the subsections, if there are any
if ($(activeSection).find(".subsection").length > 0) {
// Find any item classified as "active"
const activeItem = $(activeSection).find(".active");
if (activeItem.length > 0) {
// Find the data section this active item is referring to
if ($(activeItem).children("[data-subsection]").length > 0) {
// Get the section name
const subsectionName = $(activeItem)
.find("[data-subsection]")
.first()
.attr("data-subsection");
// If we found a section name, find the subsection element and display it
if (subsectionName) this.switchToSubSection(null, subsectionName);
} else
this.switchToSubSection(
null,
$(activeSection)
.children("[data-section]")
.first()
.attr("data-section"),
);
}
}
},
/**
* Activate (show) a sub-section and hide all others
* @param {Event} e - The click event that triggered this function
* @param {string} subsectionName - The name of the sub-section to show,
* if an event did not trigger this function
*/
switchToSubSection(e, subsectionName) {
let label = subsectionName;
if (e) {
e.preventDefault();
label = $(e.target).attr("data-section");
if (!label) {
label = $(e.target)
.parents("[data-section]")
.first()
.attr("data-section");
}
}
this.hideActiveSubSections();
this.showSubSection(label);
},
/** Hide contents of all sub-sections that are active */
hideActiveSubSections() {
// Unactivate all the subsection links
const activeLinks = document.querySelectorAll(
".subsection-link.active",
);
activeLinks.forEach((link) => link.classList.remove("active"));
// Hide all the subsections
const activeSubsections = document.querySelectorAll(
".section.active .subsection",
);
activeSubsections.forEach((subsection) => {
const el = subsection;
el.style.display = "none";
});
},
/**
* Activate (show) a sub-section
* @param {string} label - The data-section label of the sub-section to show
*/
showSubSection(label) {
const sectionLink = document.querySelector(
`.subsection-link[data-section='${label}']`,
);
if (sectionLink) sectionLink.classList.add("active");
const subsection = document.querySelector(
`.subsection[data-section='${label}']`,
);
if (subsection) subsection.style.display = "block";
},
resetSections() {
// Hide all the sections first, then display the section specified in the URL (or the default)
this.$(".subsection, .section").hide();
this.switchToSection(null, this.activeSection);
// Show the subsection
if (this.activeSubSection)
this.switchToSubSection(null, this.activeSubSection);
},
highlightSubSection(e, subsectionName) {
let label = subsectionName;
if (e) e.preventDefault();
if (!label && e) {
// Get the subsection name
label = $(e.target).attr("highlight-subsection");
if (!label) return;
} else if (!label && !e) return;
// Find the subsection
let subsection = this.$(`.subsection[data-section='${label}']`);
if (!subsection.length)
subsection = this.$("[data-subsection='add-account']");
if (!subsection.length) return;
// Visually highlight the subsection
subsection.addClass("highlight");
MetacatUI.appView.scrollTo(subsection);
// Wait about a second and then remove the highlight style
window.setTimeout(() => {
subsection.removeClass("highlight");
}, 1500);
},
// ------------------------------------------ Inserting public profile UI elements ------------------------------------------------//
insertStats() {
if (this.model.noActivity && this.statsView) {
this.statsView.$el.addClass("no-activity");
this.$("#total-download-wrapper, section.downloads").hide();
return;
}
const view = this;
// Insert a couple stats into the profile
this.listenToOnce(
this.statsModel,
"change:firstUpload",
this.insertFirstUpload,
);
this.listenToOnce(this.statsModel, "change:totalCount", () => {
view
.$("#total-upload-container")
.text(
MetacatUI.appView.commaSeparateNumber(
view.statsModel.get("totalCount"),
),
);
});
// Create a base query for the statistics
const statsSearchModel = this.model.get("searchModel").clone();
statsSearchModel
.set("exclude", [], { silent: true })
.set("formatType", [], { silent: true });
this.statsModel.set("query", statsSearchModel.getQuery());
this.statsModel.set("isSystemMetadataQuery", true);
this.statsModel.set("searchModel", statsSearchModel);
// Create the description for this profile
let description;
switch (this.model.get("type")) {
case "node":
description = `A summary of all datasets from the ${this.model.get(
"fullName",
)} repository`;
break;
case "group":
description = `A summary of all datasets from the ${this.model.get(
"fullName",
)} group`;
break;
case "user":
description = `A summary of all datasets from ${this.model.get("fullName")}`;
break;
default:
description = "";
break;
}
// Render the Stats View for this person
this.statsView = new StatsView({
title: "Statistics and Figures",
description,
userType: "user",
el: this.$("#user-stats"),
model: this.statsModel,
});
this.subviews.push(this.statsView);
this.statsView.render();
if (this.model.noActivity) this.statsView.$el.addClass("no-activity");
},
/*
* Insert the name of the user
*/
insertUserInfo() {
// Don't try to insert anything if we haven't gotten all the user info yet
if (!this.model.get("fullName")) return;
// Insert the name into this page
const usernameLink = $(document.createElement("a"))
.attr(
"href",
`${MetacatUI.root}/profile/${this.model.get("username")}`,
)
.text(this.model.get("fullName"));
this.$(".insert-fullname").append(usernameLink);
// Insert the username
if (this.model.get("type") !== "node") {
if (!this.model.get("usernameReadable"))
this.model.createReadableUsername();
this.$(".insert-username").text(this.model.get("usernameReadable"));
} else {
$("#username-wrapper").hide();
}
// Show or hide ORCID logo
if (this.model.isOrcid()) this.$(".show-orcid").show();
else this.$(".show-orcid").hide();
// Show the email
if (this.model.get("email")) {
this.$(".email-wrapper").show();
const parts = this.model.get("email").split("@");
this.$(".email-container").attr("data-user", parts[0]);
this.$(".email-container").attr("data-domain", parts[1]);
} else this.$(".email-wrapper").hide();
},
// Creates an HTML element to display in front of the user identity/subject.
// Only used for the ORCID logo right now
createIdPrefix() {
if (this.model.isOrcid())
return $(document.createElement("img"))
.attr("src", `${MetacatUI.root}/img/orcid_64x64.png`)
.addClass("orcid-logo");
return "";
},
/*
* Insert the first year of contribution for this user
*/
insertFirstUpload() {
if (this.model.noActivity || !this.statsModel.get("firstUpload")) {
this.$(
"#first-upload-container, #first-upload-year-container",
).hide();
return;
}
// Get the first upload or first operational date
let firstUpload;
if (this.model.get("type") === "node") {
// Get the member node object
const node = _.findWhere(MetacatUI.nodeModel.get("members"), {
identifier: `urn:node:${this.model.get("username")}`,
});
// If there is no memberSince date, then hide this statistic and exit
if (!node.memberSince) {
this.$(
"#first-upload-container, #first-upload-year-container",
).hide();
return;
}
firstUpload = node.memberSince
? new Date(
node.memberSince.substring(0, node.memberSince.indexOf("T")),
)
: new Date();
} else {
firstUpload = new Date(this.statsModel.get("firstUpload"));
}
// Construct the first upload date sentence
const monthNames = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const m = monthNames[firstUpload.getUTCMonth()];
const y = firstUpload.getUTCFullYear();
const d = firstUpload.getUTCDate();
// For Member Nodes, start all dates at July 2012, the beginning of DataONE
if (this.model.get("type") === "node") {
this.$("#first-upload-container").text(
`DataONE Member Node since ${y}`,
);
} else
this.$("#first-upload-container").text(
`Contributor since ${m} ${d}, ${y}`,
);
// Construct the time-elapsed sentence
const now = new Date();
const msElapsed = now - firstUpload;
const years = msElapsed / 31556952000;
const months = msElapsed / 2629746000;
const weeks = msElapsed / 604800000;
const days = msElapsed / 86400000;
let time = "";
// If one week or less, express in days
if (weeks <= 1) {
time = `${Math.round(days) || 1} day`;
if (days > 1.5) time += "s";
}
// If one month or less, express in weeks
else if (months < 1) {
time = `${Math.round(weeks) || 1} week`;
if (weeks > 1.5) time += "s";
}
// If less than 12 months, express in months
else if (months <= 11.5) {
time = `${Math.round(months) || 1} month`;
if (months > 1.5) time += "s";
}
// If one year or more, express in years and months
else {
let yearsOnly = Math.floor(years) || 1;
let monthsOnly = Math.round((years % 1) * 12);
if (monthsOnly === 12) {
yearsOnly += 1;
monthsOnly = 0;
}
time = `${yearsOnly} year`;
if (yearsOnly > 1) time += "s";
if (monthsOnly) time += `, ${monthsOnly} month`;
if (monthsOnly > 1) time += "s";
}
this.$("#first-upload-year-container").text(time);
},
/*
* Insert a list of this user's content
*/
insertContent() {
if (this.model.noActivity) {
this.$("#data-list").html(
this.noResultsTemplate({
fullName: this.model.get("fullName"),
username:
this.model === MetacatUI.appUserModel &&
MetacatUI.appUserModel.get("loggedIn")
? this.model.get("username")
: null,
}),
);
return;
}
const view = new DataCatalogView({
el: this.$("#data-list")[0],
searchModel: this.model.get("searchModel"),
searchResults: this.model.get("searchResults"),
mode: "list",
isSubView: true,
filters: false,
});
this.subviews.push(view);
view.render();
view.$el.addClass("list-only");
view.$(".auto-height").removeClass("auto-height").css("height", "auto");
$("#metacatui-app").removeClass("DataCatalog mapMode");
},
/*
* When this user has not uploaded any content, render the profile differently
*/
noActivity() {
this.model.noActivity = true;
this.insertContent();
this.insertFirstUpload();
this.insertStats();
},
// ------------------------------------------------ Identities/Accounts -------------------------------------------------------//
/**
* @deprecated since 2.36.0. Users should contact support to link
* accounts.
*/
sendMapRequest() {},
/**
* @deprecated since 2.36.0. Users should contact support to unlink
* accounts.
*/
removeMap() {},
/** @deprecated since 2.36.0. */
confirmMapRequest() {},
/** @deprecated since 2.36.0. */
rejectMapRequest() {},
insertIdentityList() {
const identities = this.model.get("identities");
// Remove the equivalentIdentities list if it was drawn already so we don't do it twice
this.$("#identity-list-container").empty();
if (!identities) return;
// Create the list element
let identityList;
if (identities.length < 1) {
identityList = $(document.createElement("p")).text(
"You have no linked accounts.",
);
identityList.addClass("well");
} else
identityList = $(document.createElement("ul"))
.addClass("list-identity")
.attr("id", "identity-list");
const view = this;
// Create a list item for each identity
_.each(identities, (identity) => {
const listItem = view.createUserListItem(identity, {
confirmed: true,
});
// When/if the info from the equivalent identities is retrieved, update the item
view.listenToOnce(identity, "change:fullName", (id) => {
const newListItem = view.createUserListItem(id, {
confirmed: true,
});
listItem.replaceWith(newListItem);
});
$(identityList).append(listItem);
});
// Add to the page
this.$("#identity-list-container").append(identityList);
},
/** @deprecated since 2.36.0 */
insertPendingList() {},
createUserListItem(user) {
const username = user.get("username");
const fullName = user.get("fullName") || username;
const listItem = $(document.createElement("li")).addClass(
"list-group-item identity",
);
const link = $(document.createElement("a"))
.attr("href", `${MetacatUI.root}/profile/${username}`)
.attr("data-identity", username)
.text(fullName);
const details = $(document.createElement("span"))
.addClass("subtle details")
.text(username);
listItem.append(link, details);
if (user.isOrcid()) {
details.prepend(this.createIdPrefix(), " ORCID: ");
} else details.prepend(" Username: ");
return listItem;
},
updateModForm() {
this.$("#mod-givenName").val(this.model.get("firstName"));
this.$("#mod-familyName").val(this.model.get("lastName"));
this.$("#mod-email").val(this.model.get("email"));
if (!this.model.get("email")) {
this.$("#mod-email").parent(".form-group").addClass("has-warning");
this.$("#mod-email")
.parent(".form-group")
.find(".help-block")
.text("Please provide an email address.");
} else {
this.$("#mod-email").parent(".form-group").removeClass("has-warning");
this.$("#mod-email")
.parent(".form-group")
.find(".help-block")
.text("");
}
if (this.model.get("registered")) {
this.$("#registered-user-container").show();
} else {
this.$("#registered-user-container").hide();
}
},
/*
* Gets the user account settings, updates the UserModel and saves this new info to the server
*/
saveUser(e) {
e.preventDefault();
const view = this;
const container =
this.$('[data-subsection="edit-account"] .content') ||
$(e.target).parent();
const success = () => {
$(container).find(".loading").detach();
$(container).children().show();
view.showAlert(
"Success! Your profile has been updated.",
"alert-success",
container,
);
};
const error = (data) => {
$(container).find(".loading").detach();
$(container).children().show();
const msg =
data && data.responseText
? data.responseText
: "Sorry, updating your profile failed. Please try again.";
if (!data.responseText) view.showAlert(msg, "alert-error", container);
};
// Get info entered into form
const givenName = this.$("#mod-givenName").val();
const familyName = this.$("#mod-familyName").val();
const email = this.$("#mod-email").val();
// Update the model
this.model.set("firstName", givenName);
this.model.set("lastName", familyName);
this.model.set("email", email);
// Loading icon
$(container).children().hide();
$(container).prepend(this.loadingTemplate());
// Send the update
this.model.update(success, error);
},
// ---------------------------------- Token -----------------------------------------//
getToken() {
const { model } = this;
// Show loading sign
this.$("#token-generator-container").html(this.loadingTemplate());
// When the token is retrieved, then show it
this.listenToOnce(this.model, "change:token", this.showToken);
// Get the token from the CN
this.model.getToken((data, _textStatus, _xhr) => {
model.getTokenExpiration();
model.set("token", data);
model.trigger("change:token");
});
},
showToken() {
const token = this.model.get("token");
if (!token || !this.model.get("loggedIn")) return;
const expires = this.model.get("expires");
const rTokenName =
MetacatUI.appModel.get("d1CNBaseUrl").indexOf("cn.dataone.org") > -1
? "dataone_token"
: "dataone_test_token";
const rToken = `options(${rTokenName} = "${token}")`;
const matlabToken = `import org.dataone.client.run.RunManager; mgr = RunManager.getInstance(); mgr.configuration.authentication_token = '${token}';`;
const tokenInput = $(document.createElement("textarea"))
.attr("type", "text")
.attr("rows", "5")
.addClass("token copy")
.text(token);
const copyButton = $(document.createElement("a"))
.addClass("btn btn-primary copy")
.text("Copy")
.attr("data-clipboard-text", token);
const copyRButton = $(document.createElement("a"))
.addClass("btn btn-primary copy")
.text("Copy")
.attr("data-clipboard-text", rToken);
const copyMatlabButton = $(document.createElement("a"))
.addClass("btn btn-primary copy")
.text("Copy")
.attr("data-clipboard-text", matlabToken);
const successIcon = $(document.createElement("i")).addClass(
"icon icon-ok",
);
const copySuccess = $(document.createElement("div"))
.addClass("notification success copy-success hidden")
.append(successIcon, " Copied!");
const expirationMsg = expires
? `<strong>Note:</strong> Your authentication token expires on ${expires.toLocaleDateString()} at ${expires.toLocaleTimeString()}`
: "";
let usernameMsg = "<div class='footnote'>Your user identity: ";
const usernamePrefix = this.createIdPrefix();
const tabs = $(document.createElement("ul"))
.addClass("nav nav-tabs")
.append(
$(document.createElement("li"))
.addClass("active")
.append(
$(document.createElement("a"))
.attr("href", "#token-code-panel")
.addClass("token-tab")
.text("Token"),
),
)
.append(
$(document.createElement("li")).append(
$(document.createElement("a"))
.attr("href", "#r-token-code-panel")
.addClass("token-tab")
.text("Token for DataONE R"),
),
)
.append(
$(document.createElement("li")).append(
$(document.createElement("a"))
.attr("href", "#matlab-token-code-panel")
.addClass("token-tab")
.text("Token for Matlab DataONE Toolbox"),
),
);
const tokenRInput = $(document.createElement("textarea"))
.attr("type", "text")
.attr("rows", "5")
.addClass("token copy")
.text(rToken);
const tokenRText = $(document.createElement("p")).text(
"Copy this code snippet to use your token with the DataONE R package.",
);
const tokenMatlabInput = $(document.createElement("textarea"))
.attr("type", "text")
.attr("rows", "5")
.addClass("token copy")
.text(matlabToken);
const tokenMatlabText = $(document.createElement("p")).text(
"Copy this code snippet to use your token with the Matlab DataONE toolbox.",
);
const tokenInputContain = $(document.createElement("div"))
.attr("id", "token-code-panel")
.addClass("tab-panel active")
.append(tokenInput, copyButton, copySuccess);
const rTokenInputContain = $(document.createElement("div"))
.attr("id", "r-token-code-panel")
.addClass("tab-panel")
.append(tokenRText, tokenRInput, copyRButton, copySuccess.clone())
.addClass("hidden");
const matlabTokenInputContain = $(document.createElement("div"))
.attr("id", "matlab-token-code-panel")
.addClass("tab-panel")
.append(
tokenMatlabText,
tokenMatlabInput,
copyMatlabButton,
copySuccess.clone(),
)
.addClass("hidden");
if (typeof usernamePrefix === "object")
usernameMsg += usernamePrefix[0].outerHTML;
else if (typeof usernamePrefix === "string")
usernameMsg += usernamePrefix;
usernameMsg += `${this.model.get("username")}</div>`;
const successMessage = $.parseHTML(
this.alertTemplate({
msg: `Copy your authentication token: <br/> ${expirationMsg}${usernameMsg}`,
classes: "alert-success",
containerClasses: "well",
}),
);
$(successMessage).append(
tabs,
tokenInputContain,
rTokenInputContain,
matlabTokenInputContain,
);
this.$("#token-generator-container").html(successMessage);
$(".token-tab").tab();
// Create clickable "Copy" buttons to copy text (e.g. token) to the user's clipboard
const clipboard = new Clipboard(".copy");
clipboard.on("success", () => {
$(".copy-success").show().delay(3000).fadeOut();
});
clipboard.on("error", (e) => {
const textarea = $(e.trigger).parent().children("textarea.token");
textarea.trigger("focus");
textarea.tooltip({
title: "Press Ctrl+c to copy",
placement: "bottom",
});
textarea.tooltip("show");
});
},
setUpAutocomplete() {
const input = this.$(".account-autocomplete");
if (!input || !input.length) return;
// look up registered identities
$(input).hoverAutocomplete({
source(request, response) {
const term = $.ui.autocomplete.escapeRegex(request.term);
const list = [];
// Ids/Usernames that we want to ignore in the autocompelte
const ignoreEquivIds =
$(this.element).attr("id") === "map-request-field";
const ignoreIds = ignoreEquivIds
? MetacatUI.appUserModel.get("identitiesUsernames")
: [];
ignoreIds.push(
MetacatUI.appUserModel.get("username").toLowerCase(),
);
const url = `${MetacatUI.appModel.get(
"accountsUrl",
)}?query=${encodeURIComponent(term)}`;
const requestSettings = {
url,
success(data, _textStatus, _xhr) {
_.each($(data).find("person"), (person) => {
const item = {};
item.value = $(person).find("subject").text();
// Don't display yourself in the autocomplete dropdown (prevents users from adding themselves as an equivalent identity or group member)
// Also don't display your equivalent identities in the autocomplete
if (_.contains(ignoreIds, item.value.toLowerCase())) return;
item.label =
$(person).find("fullName").text() ||
`${$(person).find("givenName").text()} ${$(person)
.find("familyName")
.text()}`;
list.push(item);
});
response(list);
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
// Send an ORCID search when the search string gets long enough
if (request.term.length > 3)
MetacatUI.appLookupModel.orcidSearch(
request,
response,
false,
ignoreIds,
);
},
select(e, ui) {
e.preventDefault();
// set the text field
$(e.target).val(ui.item.value);
$(e.target)
.parents("form")
.find("input[name='fullName']")
.val(ui.item.label);
},
position: {
my: "left top",
at: "left bottom",
collision: "none",
},
});
},
/**
* Renders a list of portals that this user is an owner of.
*/
renderMyPortals() {
// If my portals has been disabled, don't render the list
if (MetacatUI.appModel.get("showMyPortals") === false) {
return;
}
const view = this;
// If Bookkeeper services are enabled, render the Portals via a PortalUsagesView,
// which queries Bookkeeper for portal Usages
if (MetacatUI.appModel.get("enableBookkeeperServices")) {
// eslint-disable-next-line import/no-dynamic-require
require(["views/portals/PortalUsagesView"], (PortalUsagesView) => {
const portalListView = new PortalUsagesView();
// Render the Portal list view and insert it in the page
portalListView.render();
view.$(view.portalListContainer).html(portalListView.el);
});
}
// If Bookkeeper services are disabled, render the Portals via a PortalListView,
// which queries Solr for portal docs
else {
// eslint-disable-next-line import/no-dynamic-require
require(["views/portals/PortalListView"], (PortalListView) => {
// Create a PortalListView
const portalListView = new PortalListView();
// Render the Portal list view and insert it in the page
portalListView.render();
view.$(view.portalListContainer).html(portalListView.el);
});
}
},
// ---------------------------------- Misc. and Utilities -----------------------------------------//
showAlert(msg, classes, container) {
const classesFinal = classes || "alert-success";
const containerFinal =
container && $(container).length ? container : this.$el;
// Remove any alerts that are already in this container
if ($(containerFinal).children(".alert-container").length > 0)
$(containerFinal).children(".alert-container").remove();
$(containerFinal).prepend(
this.alertTemplate({
msg,
classesFinal,
}),
);
},
switchTabs(e) {
e.preventDefault();
$(e.target).tab("show");
this.$(".tab-panel").hide();
this.$(`.tab-panel${$(e.target).attr("href")}`).show();
this.$("#token-generator-container .copy-button").attr(
"data-clipboard-text",
);
},
preventSubmit(e) {
if (e.keyCode !== 13) return;
e.preventDefault();
},
onClose() {
// Clear the template
this.$el.html("");
// Reset the active section and subsection
this.activeSection = "profile";
this.activeSubSection = "";
// Reset the model
if (this.model) {
this.model.noActivity = null;
this.stopListening(this.model);
}
// Remove saved elements
this.$profile = null;
// Stop listening to changes in models
this.stopListening(this.statsModel);
this.stopListening(MetacatUI.appUserModel);
// Close the subviews
_.each(this.subviews, (view) => {
view.onClose();
});
this.subviews = [];
},
},
);
return UserView;
});