define(["underscore", "jquery", "backbone"], (_, $, Backbone) => {
/**
* @class AccessRuleView
* @classdesc Renders a single access rule from an object's access policy
* @classcategory Views
* @screenshot views/AccessRuleView.png
* @augments Backbone.View
*/
const AccessRuleView = Backbone.View.extend(
/** @lends AccessRuleView.prototype */ {
/**
* The type of View this is
* @type {string}
*/
type: "AccessRule",
/**
* The HTML tag name for this view's element
* @type {string}
*/
tagName: "tr",
/**
* The HTML classes to use for this view's element
* @type {string}
*/
className: "access-rule",
/**
* The AccessRule model that is displayed in this view
* @type {AccessRule}
*/
model: undefined,
/**
* If true, this view represents a new AccessRule that hasn't been added to the AccessPolicy yet
* @type {boolean}
*/
isNew: false,
/**
* If true, the user can change the AccessRule via this view.
* If false, the AccessRule will just be displayed.
* @type {boolean}
*/
allowChanges: true,
/**
* The events this view will listen to and the associated function to call.
* @type {object}
*/
events: {
"keypress .search input": "listenForEnter",
"click .add.icon": "updateModel",
"change .access select": "updateModel",
},
/**
* Renders a single Access Rule
*/
render() {
try {
this.$el.empty();
// If there's no model, exit now since there's nothing to render
if (!this.model) {
return;
}
// Get the subjects that should be hidden
const hiddenSubjects = MetacatUI.appModel.get(
"hiddenSubjectsInAccessPolicy",
);
// If this AccessRule is for a subject that should be hidden,
if (
Array.isArray(hiddenSubjects) &&
_.contains(hiddenSubjects, this.model.get("subject"))
) {
const userSubjects = _.union(
[MetacatUI.appUserModel.get("username")],
_.pluck(MetacatUI.appUserModel.get("isMemberOf"), "groupId"),
MetacatUI.appUserModel.get("allIdentitiesAndGroups") || [],
);
// If the current user is not part of this hidden group or is not the hidden user
if (!_.contains(userSubjects, this.model.get("subject"))) {
// Remove this view
this.remove();
// Exit
return;
}
}
if (this.isNew) {
// If we aren't allowing changes to this AccessRule, then don't display
// anything for new AcccessRule rows
if (!this.allowChanges) {
return;
}
this.$el.addClass("new");
// Create a text input for adding a subject or name
const label = $(document.createElement("label"))
.attr("for", "search")
.text("Search by name, ORCID, or group name")
.addClass("subtle");
const input = $(document.createElement("input"))
.attr("type", "text")
.attr("name", "search")
.attr("placeholder", "e.g. Lauren Walker");
const hiddenInput = $(document.createElement("input"))
.attr("type", "hidden")
.attr("name", "subject")
.addClass("hidden");
const searchCell = $(document.createElement("td"))
.addClass("search")
.attr("colspan", "2")
.append(label, input, hiddenInput);
const view = this;
// Setup the autocomplete widget for the input so users can search for people and groups
input.autocomplete({
source(request, response) {
return MetacatUI.appLookupModel.getAccountsAutocomplete(
request,
response,
);
},
select(e, ui) {
e.preventDefault();
const { value } = ui.item;
hiddenInput.val(value);
input.val(value);
view.updateSubject();
},
position: {
my: "left top",
at: "left bottom",
of: input,
collision: "flip",
},
appendTo: searchCell,
minLength: 2,
});
this.$el.append(searchCell);
} else {
const subject = this.model.get("subject");
try {
if (this.$el.is(".new")) {
this.$el.removeClass("new");
}
// Create elements for the 'Name' column of this table row
let icon;
// If the subject is public, don't display an icon
if (subject === "public") {
icon = "";
}
// If this is a group subject, display the group icon
else if (this.model.isGroup()) {
icon = $(document.createElement("i")).addClass(
"icon icon-on-left icon-group",
);
}
// If this is a username, display the user icon
else {
icon = $(document.createElement("i")).addClass(
"icon icon-on-left icon-user",
);
}
// Get the user or group's name - or use the subject, as a backup
let name = this.model.get("name") || subject;
// Display "You" next to the user's own name, for extra helpfulness
if (subject === MetacatUI.appUserModel.get("username")) {
name += " (You)";
}
// Create an element for the name
const nameEl = $(document.createElement("span")).text(name);
this.$el.append(
$(document.createElement("td"))
.addClass("name")
.append(icon, nameEl),
);
} catch (e) {
console.error(
"Couldn't render the name column of the AccessRuleView: ",
e,
);
}
try {
// If this subject is an ORCID, display the ORCID and ORCID icon
if (subject.indexOf("orcid") >= 0) {
// Create the "subject/orcid" column
const orcidImg = $(document.createElement("img"))
.attr("src", `${MetacatUI.root}/img/orcid_64x64.png`)
.addClass("orcid icon icon-on-left");
const orcid = $(document.createElement("span")).text(
this.model.get("subject"),
);
this.$el.append(
$(document.createElement("td"))
.addClass("subject")
.append(orcidImg, orcid),
);
} else {
// For other subject types, don't show an ORCID icon
this.$el.append(
$(document.createElement("td"))
.addClass("subject")
.text(this.model.get("subject")),
);
}
} catch (e) {
console.error(
"Couldn't render the subject column of the AccessRuleView: ",
e,
);
}
}
try {
let accessOptions;
if (this.allowChanges) {
// Create the access/permission options select dropdown
accessOptions = $(document.createElement("select"));
// Create option elements for each access rule type that is enabled in the app
_.mapObject(
MetacatUI.appModel.get("accessRuleOptions"),
(isEnabled, optionType) => {
if (isEnabled) {
const option = $(document.createElement("option"))
.attr("value", optionType)
.text(
MetacatUI.appModel.get("accessRuleOptionNames")[
optionType
],
);
// If this is the access type enabled in this AccessRule, then select this option
if (this.model.get(optionType)) {
option.prop("selected", "selected");
}
accessOptions.append(option);
}
},
);
} else {
// Create an element to display the access type
accessOptions = $(document.createElement("span"));
// Create option elements for each access rule type that is enabled in the app
_.mapObject(
MetacatUI.appModel.get("accessRuleOptions"),
(isEnabled, optionType) => {
// If this is the access type enabled in this AccessRule, then select this option
if (this.model.get(optionType)) {
accessOptions
.text(
MetacatUI.appModel.get("accessRuleOptionNames")[
optionType
],
)
.attr("title", "This cannot be changed.");
}
},
);
}
// Create the table cell and add the access options element
this.$el.append(
$(document.createElement("td"))
.addClass("access")
.append(accessOptions),
);
} catch (e) {
console.error(
"Couldn't render the access column of the AccessRuleView: ",
e,
);
}
// Render the Remove column of the table
try {
if (this.isNew) {
const addIcon = $(document.createElement("i"))
.addClass("add icon icon-plus")
.attr("title", "Add this access");
// Create an empty table cell for "new" blank rows
this.$el.append(
$(document.createElement("td"))
.addClass("add-rule")
.append(addIcon),
);
} else if (this.allowChanges) {
// Create a remove icon
const userType = this.model.isGroup() ? "group" : "person";
const removeIcon = $(document.createElement("i"))
.addClass("remove icon icon-remove")
.attr("title", `Remove access for this ${userType}`);
// Create a table cell and append the remove icon
this.$el.append(
$(document.createElement("td"))
.addClass("remove-rule")
.append(removeIcon),
);
} else {
// Add an empty table cell so the other rows don't look weird, if they have remove icons
this.$el.append($(document.createElement("td")));
}
} catch (e) {
console.error(
"Couldn't render a remove button for an access rule: ",
e,
);
}
// If there is no name set on this model, listen to when it may be set, and update the view
if (!this.model.get("name")) {
this.listenToOnce(
this.model,
"change:name",
this.updateNameDisplay,
);
}
// Listen to changes on the access options and update the view if they are changed
this.listenTo(
this.model,
"change:read change:write change:changePermission",
this.updateAccessDisplay,
);
// When the model is removed from the collection, remove this view
this.listenTo(this.model, "remove", this.onRemove);
// Attach the AccessRule model to the view element
this.$el.data("model", this.model);
this.$el.data("view", this);
} catch (e) {
console.error(e);
// Don't display a message to the user since this view is pretty small. Just remove it from the page.
this.$el.remove();
}
},
/**
* Update the name in this view with the name from the model
*/
updateNameDisplay() {
// If there is no name set on the model, exit now, so that we don't show an empty string or falsey value
if (!this.model.get("name")) {
return;
}
let name = this.model.get("name");
// Display "You" next to the user's own name, for extra helpfulness
if (
this.model.get("subject") === MetacatUI.appUserModel.get("username")
) {
name += " (You)";
}
// Find the name element and update the text content
this.$(".name span").text(name);
},
/**
* Update the AccessRule model with the selected access option
*/
updateAccess() {
try {
// Get the value of the dropdown
const selection = this.$(".access select").val();
// If nothing was selected for some reason, exit now
if (!selection) {
return;
}
if (selection === "read") {
this.model.set("read", true);
this.model.set("write", null);
this.model.set("changePermission", null);
} else if (selection === "write") {
this.model.set("read", true);
this.model.set("write", true);
this.model.set("changePermission", null);
} else if (selection === "changePermission") {
this.model.set("read", true);
this.model.set("write", true);
this.model.set("changePermission", true);
}
} catch (e) {
console.error(e);
}
},
/**
* Update the access in this view with the access from the model
*/
updateAccessDisplay() {
// Get the select dropdown menu from this view
const select = this.$(".access select");
// Update the select dropdown menu with the value from the model
if (this.model.get("changePermission")) {
select.val("changePermission");
} else if (this.model.get("write")) {
select.val("write");
} else {
select.val("read");
}
},
/**
* Update the subject of the AccessRule
*/
updateSubject() {
// Get the subject from the hidden text input, which is populated from the
// jQueryUI autocomplete widget
let subject = this.$(".search input.hidden").val();
// If the hidden input doesn't have a value, get the value from the visible input
if (!subject) {
subject = this.$(".search input:not(.hidden)").val();
}
// If there is no subject typed in, exit
if (!subject) {
return;
}
// Set the subject on the model
this.model.set("subject", subject);
this.isNew = false;
this.render();
},
/**
* Updates the model associated with this view
*/
updateModel() {
// Update the access and the subject
this.updateAccess();
this.updateSubject();
},
/**
* Remove this AccessRule from the AccessPolicy
*/
onRemove() {
// If it is the rightsHolder of the object, don't remove the view
const rightsHolder = this.accessPolicyView.getRightsHolder();
if (rightsHolder && rightsHolder === this.model.get("subject")) {
return;
}
// Remove this view from the page
this.remove();
},
/**
* Handles when the user has typed at least one character in the name search input
* @param {Event} e - The keypress event
*/
listenForEnter(e) {
try {
if (!e) {
return;
}
// If Enter was pressed,
if (e.keyCode === 13) {
// Update the subject on this model
this.updateSubject();
}
} catch (error) {
MetacatUI.appView.showAlert(
"This group or person could not be added.",
"alert-error",
this.$el,
3000,
);
console.error(
"Error while listening to the Enter key in AccessRuleView: ",
error,
);
}
},
},
);
return AccessRuleView;
});