define([
"jquery",
"underscore",
"backbone",
"views/ExpandCollapseListView",
"text!templates/provStatement.html",
], ($, _, Backbone, ExpandCollapseList, ProvTemplate) => {
"use strict";
const PREDICATE_LABELS = {
generatedByProgram: {
subject: "was generated by ",
object: "generated ",
},
usedByProgram: {
subject: "was used by ",
object: "used ",
},
wasDerivedFrom: {
subject: "was derived from ",
object: "was used as an input to create ",
},
wasInformedByProgram: {
subject: "was informed by ",
object: "informed ",
},
};
/**
* @class ProvStatementView
* @classdesc Renders provenance relationships as human readable statements
* @classcategory Views
* @augments Backbone.View
* @screenshot views/ProvStatementView.png
*/
const ProvStatementView = Backbone.View.extend({
/**
* Initialize a provenance statement for one chart record.
* @param {object} [options] View options
* @param {object} [options.model] Context chart record
* @param {object[]} [options.relationships] Projected relationships
* @param {object} [options.currentlyViewing] Currently displayed record
* @returns {void}
*/
initialize(options = {}) {
this.model = options.model || null;
this.relationships = options.relationships || [];
this.currentlyViewing = options.currentlyViewing || null;
this.subviews = [];
},
/** @inheritdoc */
template: _.template(ProvTemplate),
/** @inheritdoc */
tagName: "p",
/** @inheritdoc */
className: "provenance-statement-container",
/**
* Render relationships involving the context record.
* @returns {ProvStatementView|boolean} Rendered view, or false without a
* model
*/
render() {
if (!this.model) return false;
this.$el.html(this.template({ statement: "" }));
const groups = new Map();
this.relationships.forEach((relationship) => {
const isSubject = relationship.subject.pid === this.model.pid;
const isObject = relationship.object.pid === this.model.pid;
if (!isSubject && !isObject) return;
const direction = isSubject ? "subject" : "object";
const label = PREDICATE_LABELS[relationship.predicate]?.[direction];
if (!label) return;
const related = isSubject ? relationship.object : relationship.subject;
const key = `${relationship.predicate}:${direction}`;
if (!groups.has(key)) {
groups.set(key, { label, records: new Map() });
}
groups.get(key).records.set(related.pid, related);
});
const noun =
this.model.type === "resource" ? "resource" : this.model.type;
const statements = $(document.createElement("div"));
groups.forEach(({ label, records }) => {
const list = Array.from(records.values()).map((record) =>
this.createRelatedElement(record),
);
const collapseList = new ExpandCollapseList({
list,
prependText: `This ${noun || "data"} ${label}`,
appendText: ". ",
});
this.subviews.push(collapseList);
$(collapseList.render().el)
.addClass("list-group-item statement")
.appendTo(statements);
});
this.$(".provenance-statement").append(statements);
return this;
},
/**
* Build a linked label for a related provenance record.
* @param {object} record Related chart record
* @returns {JQuery} Linked or currently viewed record label
* @since 2.39.0
*/
createRelatedElement(record) {
const icon = $(document.createElement("i")).addClass(
`icon ${this.getIconType(record.type)} icon-on-left`,
);
const label = $(document.createElement("span"))
.text(record.fileName || record.pid)
.prepend(icon);
if (this.currentlyViewing?.pid === record.pid) {
label
.addClass("currently-viewing pointer highlight-node node-link")
.attr("data-id", record.pid);
return $(document.createElement("span")).append(
$(document.createElement("span")).text(
`the ${this.currentlyViewing.type || "data"} you are currently viewing, `,
),
label,
);
}
return $(document.createElement("a"))
.attr(
"href",
`${MetacatUI.root}/view/${encodeURIComponent(record.pid)}`,
)
.attr("data-id", record.pid)
.addClass("highlight-node preview node-link")
.append(label);
},
/**
* Return the icon class for a chart record type.
* @param {string} type Chart record type
* @returns {string} Icon CSS class
*/
getIconType(type) {
if (type === "program") return "icon-code";
if (type === "metadata") return "icon-file-text";
if (type === "image") return "icon-picture";
if (String(type).toLowerCase() === "pdf") return "icon-file pdf";
if (type === "resource") return "icon-folder-open";
return "icon-table";
},
/** Remove subviews and the view itself from the DOM. */
onClose() {
this.subviews.forEach((view) => view.onClose?.());
this.remove();
},
});
return ProvStatementView;
});