diff --git a/src/js/views/AnnotationView.js b/src/js/views/AnnotationView.js index e9cbbe25b..eac56a15c 100644 --- a/src/js/views/AnnotationView.js +++ b/src/js/views/AnnotationView.js @@ -16,6 +16,11 @@ define([ */ var AnnotationView = Backbone.View.extend( /** @lends AnnotationView.prototype */ { + /** + * The type of View this is + * @type {string} + */ + type: "AnnotationView", className: "annotation-view", annotationPopoverTemplate: _.template(AnnotationPopoverTemplate), diff --git a/src/js/views/CanonicalDatasetHandlerView.js b/src/js/views/CanonicalDatasetHandlerView.js new file mode 100644 index 000000000..eb8a35d0a --- /dev/null +++ b/src/js/views/CanonicalDatasetHandlerView.js @@ -0,0 +1,234 @@ +define(["backbone"], (Backbone) => { + // The "Type" property of the annotation view + const ANNO_VIEW_TYPE = "AnnotationView"; + // The URI for the schema.org:sameAs annotation + const SCHEMA_ORG_SAME_AS = "http://www.w3.org/2002/07/owl#sameAs"; + // The URI for the prov:wasDerivedFrom annotation + const PROV_WAS_DERIVED_FROM = "http://www.w3.org/ns/prov#wasDerivedFrom"; + + // What to call the field that links to the original dataset + const CANONICAL_LABEL = "Canonical Dataset"; + // The text to display in the info tooltip to explain what the canonical + // dataset field means + const CANONICAL_TOOLTIP_TEXT = + "The original dataset this version was derived from. This dataset is essentially a duplicate of the original."; + + // The following properties are used to identify parts of the MetadataView. + // If the MetadataView changes, these properties may need to be updated. + + // The name of the property on the MetadataView that contains subviews + const SUBVIEW_PROP = "subviews"; + // Class names used in the MetadataView that we also need to use in this view + const METADATA_VIEW_CLASS_NAMES = { + fieldItem: "control-group", + fieldLabel: "control-label", + fieldValue: ["controls", "controls-well"], + }; + + /** + * @class CanonicalDatasetHandlerView + * @classdesc A scoped subview responsible for inspecting the rendered DOM + * within the MetadataView to identify and highlight the canonical (original) + * dataset based on schema.org:sameAs and prov:derivedFrom annotations. This + * view modifies specific parts of the MetadataView when a canonical dataset + * is detected, providing a clearer distinction between original and derived + * datasets. + * @classcategory Views + * @augments Backbone.View + * @class + * @since 0.0.0 + * @screenshot views/CanonicalDatasetHandlerViewView.png TODO + */ + const CanonicalDatasetHandlerView = Backbone.View.extend( + /** @lends CanonicalDatasetHandlerView.prototype */ + { + /** @inheritdoc */ + type: "CanonicalDatasetHandlerView", + + /** + * The MetadataView instance this view is scoped to. + * @type {MetadataView} + */ + metdataView: null, + + /** + * The value of the label to insert the canonical dataset field before. + * @type {string} + */ + insertFieldBefore: "Identifier", + + /** + * Creates a field item for the MetadataView. + * @param {string} label - The label for the field. + * @param {string} value - The value for the field. + * @param {string} tooltipText - The text to display in the info tooltip. + * @param {object} classes - The classes to apply to the field item. + * @returns {string} The HTML for the field item. + */ + fieldItemTemplate(label, value, tooltipText, classes) { + return `
+ +
${value}
+
`; + }, + + /** + * Initialize the CanonicalDatasetHandlerView. + * @param {object} options - A set of options to initialize the view with. + * @param {MetadataView} options.metadataView - The MetadataView instance + * this view is scoped to. Required. + */ + initialize(options) { + this.metadataView = options?.metadataView; + if (!this.metadataView) { + throw new Error( + "The CanonicalDatasetHandlerView requires a MetadataView instance.", + ); + } + }, + + /** @inheritdoc */ + render() { + if (this.detectCanonicalDataset()) { + this.getCitationInfo(); + this.addFieldItem(); + this.modifyCitationModal(); + this.addInfoIcon(); + this.removeAnnotations(); + } + return this; + }, + + /** + * Inspects the MetadataView DOM to determine if a canonical dataset is + * present based on schema.org:sameAs and prov:wasDerivedFrom annotations. + * If a canonical dataset is detected, this method sets the appropriate + * properties on the view instance. + * @returns {boolean} True if a canonical dataset is detected, false + * otherwise. + */ + detectCanonicalDataset() { + const subViews = this.metadataView[SUBVIEW_PROP]; + if (!subViews || !subViews.length) { + // TODO: Handle no subviews. Wait for subviews to be added? + return false; + } + + let sameAs = null; + let sameAsView = null; + let derivedFrom = null; + let derivedFromView = null; + + let originalUri = null; + let hasCanonical = false; + + // The annotation views provide the URI and value of annotations on the + // metadata. We consider the dataset to be canonical if the sameAs and + // derivedFrom annotations both point to the same URI. + subViews.forEach((view) => { + if (view.type !== ANNO_VIEW_TYPE) { + return; + } + const { uri } = view.property; + const { value } = view.value; + // TODO: handle multiple sameAs/derivedFrom annotations. Unlikely but + // technically possible? + if (uri === SCHEMA_ORG_SAME_AS) { + sameAs = value; + sameAsView = view; + } else if (uri === PROV_WAS_DERIVED_FROM) { + derivedFrom = value; + derivedFromView = view; + } + }); + + if (sameAs === derivedFrom && sameAs) { + originalUri = sameAs; + hasCanonical = true; + } + this.derivedFromAnnotationView = derivedFromView; + this.sameAsAnnotationView = sameAsView; + this.canonicalUri = originalUri; + this.hasCanonical = hasCanonical; + return hasCanonical; + }, + + /** + * Given the canonical dataset URI, fetches citation information for the + * canonical dataset, like the title, authors, publication date, etc. Saves + * this information in a CitationModel instance. + */ + getCitationInfo() { + // TODO: Get citation info from the canonical dataset + // what API can we use to get this info? + // this.citationModel = new CitationModel({}); + }, + + /** + * Removes the sameAs and derivedFrom annotations from the MetadataView. + * This is done to prevent redundancy in the metadata display. + */ + removeAnnotations() { + this.sameAsAnnotationView?.remove(); + this.derivedFromAnnotationView?.remove(); + this.sameAsAnnotationView = null; + this.derivedFromAnnotationView = null; + }, + + /** + * Adds a "row" in the MetadataView to display the canonical dataset URI. + */ + addFieldItem() { + const { canonicalUri, fieldItemTemplate } = this; + const itemHTML = fieldItemTemplate( + CANONICAL_LABEL, + `${canonicalUri}`, + CANONICAL_TOOLTIP_TEXT, + METADATA_VIEW_CLASS_NAMES, + ); + + // Find the parent item that contains the field name the view should + // be inserted before + const labels = Array.from( + this.metadataView.el.querySelectorAll("label"), + ); + const insertBeforeLabel = labels.find( + (label) => label.textContent.trim() === this.insertFieldBefore, + ); + // Insert the new field item before the label + insertBeforeLabel.parentElement.insertAdjacentHTML( + "beforebegin", + itemHTML, + ); + }, + + /** + * Modifies the CitationModalView to add the citation information for the + * canonical dataset in addition to the citation information for the + * current dataset. + */ + modifyCitationModal() { + // TODO + }, + + /** + * Adds a badge to the header of the MetadataView to indicate that the + * dataset being displayed is essentially a duplicate of another dataset. + */ + addInfoIcon() { + // TODO + }, + + // TODO: Do we need to remove the view from the DOM when the MetadataView + // is removed? Do we need methods to undo the changes made by this view? + remove() {}, + }, + ); + + return CanonicalDatasetHandlerView; +}); diff --git a/src/js/views/MetadataView.js b/src/js/views/MetadataView.js index 1bcbed1b4..d0dbc62a3 100644 --- a/src/js/views/MetadataView.js +++ b/src/js/views/MetadataView.js @@ -22,6 +22,7 @@ define([ "views/AnnotationView", "views/MarkdownView", "views/ViewObjectButtonView", + "views/CanonicalDatasetHandlerView", "text!templates/metadata/metadata.html", "text!templates/dataSource.html", "text!templates/publishDOI.html", @@ -59,6 +60,7 @@ define([ AnnotationView, MarkdownView, ViewObjectButtonView, + CanonicalDatasetHandlerView, MetadataTemplate, DataSourceTemplate, PublishDoiTemplate, @@ -189,6 +191,15 @@ define([ this.once("metadataLoaded", () => { this.createAnnotationViews(); this.insertMarkdownViews(); + // Modifies the view to indicate that this is a dataset is essentially + // a duplicate of another dataset, if applicable + if (!this.canonicalDatasetHandler) { + // The view should only be created once, but "metadataLoaded" can be + // triggered multiple times + this.canonicalDatasetHandler = new CanonicalDatasetHandlerView({ + metadataView: this, + }).render(); + } }); // Listen to when the package table has been rendered