Source: src/js/views/ProvStatementView.js

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