mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Remove legacy collection and dataset handling
This commit is contained in:
@@ -1,115 +0,0 @@
|
||||
import $ from "jquery";
|
||||
import _ from "underscore";
|
||||
import DC_LI from "mvc/collection/collection-li";
|
||||
import DATASET_LI_EDIT from "mvc/dataset/dataset-li-edit";
|
||||
|
||||
//==============================================================================
|
||||
var DCListItemView = DC_LI.DCListItemView;
|
||||
/** @class Edit view for DatasetCollection.
|
||||
*/
|
||||
var DCListItemEdit = DCListItemView.extend(
|
||||
/** @lends DCListItemEdit.prototype */ {
|
||||
/** override to add linkTarget */
|
||||
initialize: function (attributes) {
|
||||
DCListItemView.prototype.initialize.call(this, attributes);
|
||||
},
|
||||
|
||||
// ......................................................................... misc
|
||||
/** String representation */
|
||||
toString: function () {
|
||||
var modelString = this.model ? `${this.model}` : "(no model)";
|
||||
return `DCListItemEdit(${modelString})`;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
var DCEListItemView = DC_LI.DCEListItemView;
|
||||
/** @class Read only view for DatasetCollectionElement.
|
||||
*/
|
||||
var DCEListItemEdit = DCEListItemView.extend(
|
||||
/** @lends DCEListItemEdit.prototype */ {
|
||||
//TODO: this might be expendable - compacted with HDAListItemView
|
||||
|
||||
/** set up */
|
||||
initialize: function (attributes) {
|
||||
DCEListItemView.prototype.initialize.call(this, attributes);
|
||||
},
|
||||
|
||||
// ......................................................................... misc
|
||||
/** String representation */
|
||||
toString: function () {
|
||||
var modelString = this.model ? `${this.model}` : "(no model)";
|
||||
return `DCEListItemEdit(${modelString})`;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
// NOTE: this does not inherit from DatasetDCEListItemView as you would expect
|
||||
//TODO: but should - if we can find something simpler than using diamond
|
||||
/** @class Editable view for a DatasetCollectionElement that is also an DatasetAssociation
|
||||
* (a dataset contained in a dataset collection).
|
||||
*/
|
||||
var DatasetDCEListItemEdit = DATASET_LI_EDIT.DatasetListItemEdit.extend(
|
||||
/** @lends DatasetDCEListItemEdit.prototype */ {
|
||||
/** set up */
|
||||
initialize: function (attributes) {
|
||||
DATASET_LI_EDIT.DatasetListItemEdit.prototype.initialize.call(this, attributes);
|
||||
},
|
||||
|
||||
// NOTE: this does not inherit from DatasetDCEListItemView - so we duplicate this here
|
||||
//TODO: fix
|
||||
/** In this override, only get details if in the ready state.
|
||||
* Note: fetch with no 'change' event triggering to prevent automatic rendering.
|
||||
*/
|
||||
_fetchModelDetails: function () {
|
||||
var view = this;
|
||||
if (view.model.inReadyState() && !view.model.hasDetails()) {
|
||||
return view.model.fetch({ silent: true });
|
||||
}
|
||||
return $.when();
|
||||
},
|
||||
|
||||
/** Override to remove delete button */
|
||||
_renderDeleteButton: function () {
|
||||
return null;
|
||||
},
|
||||
|
||||
// ......................................................................... misc
|
||||
/** String representation */
|
||||
toString: function () {
|
||||
var modelString = this.model ? `${this.model}` : "(no model)";
|
||||
return `DatasetDCEListItemEdit(${modelString})`;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// ............................................................................ TEMPLATES
|
||||
/** underscore templates */
|
||||
DatasetDCEListItemEdit.prototype.templates = (() =>
|
||||
_.extend({}, DATASET_LI_EDIT.DatasetListItemEdit.prototype.templates, {
|
||||
titleBar: DC_LI.DatasetDCEListItemView.prototype.templates.titleBar,
|
||||
}))();
|
||||
|
||||
//==============================================================================
|
||||
/** @class Read only view for a DatasetCollectionElement that is also a DatasetCollection
|
||||
* (a nested DC).
|
||||
*/
|
||||
var NestedDCDCEListItemEdit = DC_LI.NestedDCDCEListItemView.extend(
|
||||
/** @lends NestedDCDCEListItemEdit.prototype */ {
|
||||
/** String representation */
|
||||
toString: function () {
|
||||
var modelString = this.model ? `${this.model}` : "(no model)";
|
||||
return `NestedDCDCEListItemEdit(${modelString})`;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
export default {
|
||||
DCListItemEdit: DCListItemEdit,
|
||||
DCEListItemEdit: DCEListItemEdit,
|
||||
DatasetDCEListItemEdit: DatasetDCEListItemEdit,
|
||||
NestedDCDCEListItemEdit: NestedDCDCEListItemEdit,
|
||||
};
|
||||
@@ -1,277 +0,0 @@
|
||||
import _ from "underscore";
|
||||
import $ from "jquery";
|
||||
import { ListItemView, FoldoutListItemView } from "mvc/list/list-item";
|
||||
import { DatasetListItemView } from "mvc/dataset/dataset-li";
|
||||
import BASE_MVC from "mvc/base-mvc";
|
||||
import _l from "utils/localization";
|
||||
|
||||
/** @class Read only view for DatasetCollection.
|
||||
*/
|
||||
var DCListItemView = FoldoutListItemView.extend(
|
||||
/** @lends DCListItemView.prototype */ {
|
||||
className: `${FoldoutListItemView.prototype.className} dataset-collection`,
|
||||
id: function () {
|
||||
return ["dataset_collection", this.model.get("id")].join("-");
|
||||
},
|
||||
|
||||
/** override to add linkTarget */
|
||||
initialize: function (attributes) {
|
||||
this.linkTarget = attributes.linkTarget || "_blank";
|
||||
this.hasUser = attributes.hasUser;
|
||||
FoldoutListItemView.prototype.initialize.call(this, attributes);
|
||||
},
|
||||
|
||||
/** event listeners */
|
||||
_setUpListeners: function () {
|
||||
FoldoutListItemView.prototype._setUpListeners.call(this);
|
||||
this.listenTo(this.model, "change", function (model, options) {
|
||||
// if the model has changed deletion status render it entirely
|
||||
if (_.has(model.changed, "deleted")) {
|
||||
this.render();
|
||||
|
||||
// if the model has been decorated after the fact with the element count,
|
||||
// render the subtitle where the count is displayed
|
||||
} else if (_.has(model.changed, "element_count")) {
|
||||
this.$("> .title-bar .subtitle").replaceWith(this._renderSubtitle());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// ......................................................................... rendering
|
||||
/** render a subtitle to show the user what sort of collection this is */
|
||||
_renderSubtitle: function () {
|
||||
return $(this.templates.subtitle(this.model.toJSON(), this));
|
||||
},
|
||||
|
||||
// ......................................................................... foldout
|
||||
/** override to add linktarget to sub-panel */
|
||||
_getFoldoutPanelOptions: function () {
|
||||
var options = FoldoutListItemView.prototype._getFoldoutPanelOptions.call(this);
|
||||
return _.extend(options, {
|
||||
linkTarget: this.linkTarget,
|
||||
hasUser: this.hasUser,
|
||||
});
|
||||
},
|
||||
|
||||
/** override to not catch sub-panel selectors */
|
||||
$selector: function () {
|
||||
return this.$("> .selector");
|
||||
},
|
||||
|
||||
// ......................................................................... misc
|
||||
/** String representation */
|
||||
toString: function () {
|
||||
var modelString = this.model ? `${this.model}` : "(no model)";
|
||||
return `DCListItemView(${modelString})`;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// ............................................................................ TEMPLATES
|
||||
/** underscore templates */
|
||||
DCListItemView.prototype.templates = (() => {
|
||||
var warnings = _.extend({}, FoldoutListItemView.prototype.templates.warnings, {
|
||||
error: BASE_MVC.wrapTemplate([
|
||||
// error during index fetch - show error on dataset
|
||||
"<% if( model.error ){ %>",
|
||||
'<div class="errormessagesmall">',
|
||||
_l("There was an error getting the data for this collection"),
|
||||
": <%- model.error %>",
|
||||
"</div>",
|
||||
"<% } %>",
|
||||
]),
|
||||
purged: BASE_MVC.wrapTemplate([
|
||||
"<% if( model.purged ){ %>",
|
||||
'<div class="purged-msg warningmessagesmall">',
|
||||
_l("This collection has been deleted and removed from disk"),
|
||||
"</div>",
|
||||
"<% } %>",
|
||||
]),
|
||||
deleted: BASE_MVC.wrapTemplate([
|
||||
// deleted not purged
|
||||
"<% if( model.deleted && !model.purged ){ %>",
|
||||
'<div class="deleted-msg warningmessagesmall">',
|
||||
_l("This collection has been deleted"),
|
||||
"</div>",
|
||||
"<% } %>",
|
||||
]),
|
||||
});
|
||||
|
||||
// use element identifier
|
||||
var titleBarTemplate = BASE_MVC.wrapTemplate(
|
||||
[
|
||||
'<div class="title-bar clear" tabindex="0">',
|
||||
'<div class="title">',
|
||||
'<span class="name"><%- collection.element_identifier || collection.name %></span>',
|
||||
"</div>",
|
||||
'<div class="subtitle"></div>',
|
||||
"</div>",
|
||||
],
|
||||
"collection"
|
||||
);
|
||||
|
||||
// use element identifier
|
||||
var subtitleTemplate = BASE_MVC.wrapTemplate(
|
||||
[
|
||||
'<div class="subtitle">',
|
||||
'<% var countText = collection.element_count? ( collection.element_count + " " ) : ""; %>',
|
||||
'<% if( collection.collection_type === "list" ){ %>',
|
||||
_l("a list of <%- countText %>datasets"),
|
||||
'<% } else if( collection.collection_type === "paired" ){ %>',
|
||||
_l("a pair of datasets"),
|
||||
'<% } else if( collection.collection_type === "list:paired" ){ %>',
|
||||
_l("a list of <%- countText %>dataset pairs"),
|
||||
'<% } else if( collection.collection_type === "list:list" ){ %>',
|
||||
_l("a list of <%- countText %>dataset lists"),
|
||||
"<% } %>",
|
||||
"</div>",
|
||||
],
|
||||
"collection"
|
||||
);
|
||||
|
||||
return _.extend({}, FoldoutListItemView.prototype.templates, {
|
||||
warnings: warnings,
|
||||
titleBar: titleBarTemplate,
|
||||
subtitle: subtitleTemplate,
|
||||
});
|
||||
})();
|
||||
|
||||
//==============================================================================
|
||||
/** @class Read only view for DatasetCollectionElement.
|
||||
*/
|
||||
var DCEListItemView = ListItemView.extend(
|
||||
/** @lends DCEListItemView.prototype */ {
|
||||
/** add the DCE class to the list item */
|
||||
className: `${ListItemView.prototype.className} dataset-collection-element`,
|
||||
|
||||
/** set up */
|
||||
initialize: function (attributes) {
|
||||
if (attributes.logger) {
|
||||
this.logger = this.model.logger = attributes.logger;
|
||||
}
|
||||
this.log("DCEListItemView.initialize:", attributes);
|
||||
ListItemView.prototype.initialize.call(this, attributes);
|
||||
},
|
||||
|
||||
// ......................................................................... misc
|
||||
/** String representation */
|
||||
toString: function () {
|
||||
var modelString = this.model ? `${this.model}` : "(no model)";
|
||||
return `DCEListItemView(${modelString})`;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// ............................................................................ TEMPLATES
|
||||
/** underscore templates */
|
||||
DCEListItemView.prototype.templates = (() => {
|
||||
// use the element identifier here - since that will persist and the user will need it
|
||||
var titleBarTemplate = BASE_MVC.wrapTemplate(
|
||||
[
|
||||
'<div class="title-bar clear" tabindex="0">',
|
||||
'<div class="title">',
|
||||
'<span class="name"><%- element.element_identifier %></span>',
|
||||
"</div>",
|
||||
'<div class="subtitle"></div>',
|
||||
"</div>",
|
||||
],
|
||||
"element"
|
||||
);
|
||||
|
||||
return _.extend({}, ListItemView.prototype.templates, {
|
||||
titleBar: titleBarTemplate,
|
||||
});
|
||||
})();
|
||||
|
||||
//==============================================================================
|
||||
/** @class Read only view for a DatasetCollectionElement that is also an DatasetAssociation
|
||||
* (a dataset contained in a dataset collection).
|
||||
*/
|
||||
var DatasetDCEListItemView = DatasetListItemView.extend(
|
||||
/** @lends DatasetDCEListItemView.prototype */ {
|
||||
className: `${DatasetListItemView.prototype.className} dataset-collection-element`,
|
||||
|
||||
/** set up */
|
||||
initialize: function (attributes) {
|
||||
if (attributes.logger) {
|
||||
this.logger = this.model.logger = attributes.logger;
|
||||
}
|
||||
this.log("DatasetDCEListItemView.initialize:", attributes);
|
||||
DatasetListItemView.prototype.initialize.call(this, attributes);
|
||||
},
|
||||
|
||||
/** In this override, only get details if in the ready state.
|
||||
* Note: fetch with no 'change' event triggering to prevent automatic rendering.
|
||||
*/
|
||||
_fetchModelDetails: function () {
|
||||
var view = this;
|
||||
if (view.model.inReadyState() && !view.model.hasDetails()) {
|
||||
return view.model.fetch({ silent: true });
|
||||
}
|
||||
return $.when();
|
||||
},
|
||||
|
||||
// ......................................................................... misc
|
||||
/** String representation */
|
||||
toString: function () {
|
||||
var modelString = this.model ? `${this.model}` : "(no model)";
|
||||
return `DatasetDCEListItemView(${modelString})`;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// ............................................................................ TEMPLATES
|
||||
/** underscore templates */
|
||||
DatasetDCEListItemView.prototype.templates = (() => {
|
||||
// use the element identifier here and not the dataset name
|
||||
//TODO:?? can we steal the DCE titlebar?
|
||||
var titleBarTemplate = BASE_MVC.wrapTemplate(
|
||||
[
|
||||
'<div class="title-bar clear" tabindex="0">',
|
||||
'<span class="state-icon"></span>',
|
||||
'<div class="title">',
|
||||
'<span class="name"><%- element.element_identifier %></span>',
|
||||
"</div>",
|
||||
"</div>",
|
||||
],
|
||||
"element"
|
||||
);
|
||||
|
||||
return _.extend({}, DatasetListItemView.prototype.templates, {
|
||||
titleBar: titleBarTemplate,
|
||||
});
|
||||
})();
|
||||
|
||||
//==============================================================================
|
||||
/** @class Read only view for a DatasetCollectionElement that is also a DatasetCollection
|
||||
* (a nested DC).
|
||||
*/
|
||||
var NestedDCDCEListItemView = DCListItemView.extend(
|
||||
/** @lends NestedDCDCEListItemView.prototype */ {
|
||||
className: `${DCListItemView.prototype.className} dataset-collection-element`,
|
||||
|
||||
/** In this override, add the state as a class for use with state-based CSS */
|
||||
_swapNewRender: function ($newRender) {
|
||||
DCListItemView.prototype._swapNewRender.call(this, $newRender);
|
||||
var state = this.model.get("state") || "ok";
|
||||
this.$el.addClass(`state-${state}`);
|
||||
this.$el.attr("data-state", state);
|
||||
return this.$el;
|
||||
},
|
||||
|
||||
// ......................................................................... misc
|
||||
/** String representation */
|
||||
toString: function () {
|
||||
var modelString = this.model ? `${this.model}` : "(no model)";
|
||||
return `NestedDCDCEListItemView(${modelString})`;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
export default {
|
||||
DCListItemView: DCListItemView,
|
||||
DCEListItemView: DCEListItemView,
|
||||
DatasetDCEListItemView: DatasetDCEListItemView,
|
||||
NestedDCDCEListItemView: NestedDCDCEListItemView,
|
||||
};
|
||||
@@ -1,396 +0,0 @@
|
||||
import $ from "jquery";
|
||||
import _ from "underscore";
|
||||
import Backbone from "backbone";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import DATASET_MODEL from "mvc/dataset/dataset-model";
|
||||
import BASE_MVC from "mvc/base-mvc";
|
||||
import Utils from "utils/utils";
|
||||
|
||||
//==============================================================================
|
||||
/*
|
||||
Notes:
|
||||
|
||||
Terminology:
|
||||
DatasetCollection/DC : a container of datasets or nested DatasetCollections
|
||||
Element/DatasetCollectionElement/DCE : an item contained in a DatasetCollection
|
||||
HistoryDatasetCollectionAssociation/HDCA: a DatasetCollection contained in a history
|
||||
|
||||
|
||||
This all seems too complex unfortunately:
|
||||
|
||||
- Terminology collision between DatasetCollections (DCs) and Backbone Collections.
|
||||
- In the DatasetCollections API JSON, DC Elements use a 'Has A' stucture to *contain*
|
||||
either a dataset or a nested DC. This would make the hierarchy much taller. I've
|
||||
decided to merge the contained JSON with the DC element json - making the 'has a'
|
||||
relation into an 'is a' relation. This seems simpler to me and allowed a lot of
|
||||
DRY in both models and views, but may make tracking or tracing within these models
|
||||
more difficult (since DatasetCollectionElements are now *also* DatasetAssociations
|
||||
or DatasetCollections (nested)). This also violates the rule of thumb about
|
||||
favoring aggregation over inheritance.
|
||||
- Currently, there are three DatasetCollection subclasses: List, Pair, and ListPaired.
|
||||
These each should a) be usable on their own, b) be usable in the context of
|
||||
nesting within a collection model (at least in the case of ListPaired), and
|
||||
c) be usable within the context of other container models (like History or
|
||||
LibraryFolder, etc.). I've tried to separate/extract classes in order to
|
||||
handle those three situations, but it's proven difficult to do in a simple,
|
||||
readable manner.
|
||||
- Ideally, histories and libraries would inherit from the same server models as
|
||||
dataset collections do since they are (in essence) dataset collections themselves -
|
||||
making the whole nested structure simpler. This would be a large, error-prone
|
||||
refactoring and migration.
|
||||
|
||||
Many of the classes and heirarchy are meant as extension points so, while the
|
||||
relations and flow may be difficult to understand initially, they'll allow us to
|
||||
handle the growth or flux dataset collection in the future (w/o actually implementing
|
||||
any YAGNI).
|
||||
|
||||
*/
|
||||
//_________________________________________________________________________________________________ ELEMENTS
|
||||
/** @class mixin for Dataset collection elements.
|
||||
* When collection elements are passed from the API, the underlying element is
|
||||
* in a sub-object 'object' (IOW, a DCE representing an HDA will have HDA json in element.object).
|
||||
* This mixin uses the constructor and parse methods to merge that JSON with the DCE attribtues
|
||||
* effectively changing a DCE from a container to a subclass (has a --> is a).
|
||||
*/
|
||||
var DatasetCollectionElementMixin = {
|
||||
/** default attributes used by elements in a dataset collection */
|
||||
defaults: {
|
||||
model_class: "DatasetCollectionElement",
|
||||
element_identifier: null,
|
||||
element_index: null,
|
||||
element_type: null,
|
||||
},
|
||||
|
||||
/** merge the attributes of the sub-object 'object' into this model */
|
||||
_mergeObject: function (attributes) {
|
||||
// Don't let the dataset ID replace the DCE's ID so record it as the
|
||||
// element_id and when fetching dataset details below use the element_id
|
||||
// instead of this.id.
|
||||
const object = attributes.object;
|
||||
let elementId = this.elementId;
|
||||
if (object) {
|
||||
elementId = attributes.object.id;
|
||||
delete attributes.object.id;
|
||||
}
|
||||
_.extend(attributes, attributes.object, {
|
||||
element_id: elementId,
|
||||
});
|
||||
delete attributes.object;
|
||||
return attributes;
|
||||
},
|
||||
|
||||
/** override to merge this.object into this */
|
||||
constructor: function (attributes, options) {
|
||||
// console.debug( '\t DatasetCollectionElement.constructor:', attributes, options );
|
||||
attributes = this._mergeObject(attributes);
|
||||
this.idAttribute = "element_id";
|
||||
Backbone.Model.apply(this, arguments);
|
||||
},
|
||||
|
||||
/** when the model is fetched, merge this.object into this */
|
||||
parse: function (response, options) {
|
||||
var attributes = response;
|
||||
attributes = this._mergeObject(attributes);
|
||||
return attributes;
|
||||
},
|
||||
};
|
||||
|
||||
/** @class Concrete class of Generic DatasetCollectionElement */
|
||||
var DatasetCollectionElement = Backbone.Model.extend(BASE_MVC.LoggableMixin)
|
||||
.extend(DatasetCollectionElementMixin)
|
||||
.extend({ _logNamespace: "collections" });
|
||||
|
||||
//==============================================================================
|
||||
/** @class Base/Abstract Backbone collection for Generic DCEs -
|
||||
current may be associated with a dataset (DatasetDCECollection)
|
||||
or another collection (NestedDCDCECollection).
|
||||
*/
|
||||
var DCECollection = Backbone.Collection.extend(BASE_MVC.LoggableMixin).extend(
|
||||
/** @lends DCECollection.prototype */ {
|
||||
_logNamespace: "collections",
|
||||
|
||||
model: DatasetCollectionElement,
|
||||
|
||||
/** String representation. */
|
||||
toString: function () {
|
||||
return ["DatasetCollectionElementCollection(", this.length, ")"].join("");
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
/** @class Backbone model for a dataset collection element that is a dataset (HDA).
|
||||
*/
|
||||
var DatasetDCE = DATASET_MODEL.DatasetAssociation.extend(
|
||||
BASE_MVC.mixin(
|
||||
DatasetCollectionElementMixin,
|
||||
/** @lends DatasetDCE.prototype */ {
|
||||
/** url fn */
|
||||
url: function () {
|
||||
// won't always be an hda
|
||||
if (!this.has("history_id")) {
|
||||
console.warn("no endpoint for non-hdas within a collection yet");
|
||||
// (a little silly since this api endpoint *also* points at hdas)
|
||||
return `${getAppRoot()}api/datasets`;
|
||||
}
|
||||
const datasetId = this._getDatasetId();
|
||||
return `${getAppRoot()}api/histories/${this.get("history_id")}/contents/${datasetId}`;
|
||||
},
|
||||
|
||||
_getDatasetId: function () {
|
||||
// I'm a DCE acting as dataset, this URL needs to be the dataset URL so
|
||||
// use element_id instead of id. See note above in _mergeObject and
|
||||
// discussion on #3782 for more context.
|
||||
return this.get("element_id");
|
||||
},
|
||||
|
||||
defaults: _.extend(
|
||||
{},
|
||||
DATASET_MODEL.DatasetAssociation.prototype.defaults,
|
||||
DatasetCollectionElementMixin.defaults
|
||||
),
|
||||
|
||||
_downloadQueryParameters: function () {
|
||||
// Setting the file extension to just 'data' defers that
|
||||
// decision to the serverside, setting based on the datatype.
|
||||
var fileExt = this.get("file_ext") || "data";
|
||||
var elementIdentifier = this.get("element_identifier");
|
||||
var parentHdcaId = this.get("parent_hdca_id");
|
||||
return `?to_ext=${fileExt}&hdca_id=${parentHdcaId}&element_identifier=${elementIdentifier}`;
|
||||
},
|
||||
|
||||
// because all objects have constructors (as this hashmap would even if this next line wasn't present)
|
||||
// the constructor in hcontentMixin won't be attached by BASE_MVC.mixin to this model
|
||||
// - re-apply manually for now
|
||||
/** call the mixin constructor */
|
||||
constructor: function (attributes, options) {
|
||||
this.debug("\t DatasetDCE.constructor:", attributes, options);
|
||||
//DATASET_MODEL.DatasetAssociation.prototype.constructor.call( this, attributes, options );
|
||||
DatasetCollectionElementMixin.constructor.call(this, attributes, options);
|
||||
},
|
||||
|
||||
/** Does this model already contain detailed data (as opposed to just summary level data)? */
|
||||
hasDetails: function () {
|
||||
return this.elements && this.elements.length;
|
||||
},
|
||||
|
||||
/** String representation. */
|
||||
toString: function () {
|
||||
var objStr = this.get("element_identifier");
|
||||
return `DatasetDCE(${objStr})`;
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
/** @class DCECollection of DatasetDCE's (a list of datasets, a pair of datasets).
|
||||
*/
|
||||
var DatasetDCECollection = DCECollection.extend(
|
||||
/** @lends DatasetDCECollection.prototype */ {
|
||||
model: DatasetDCE,
|
||||
|
||||
/** String representation. */
|
||||
toString: function () {
|
||||
return ["DatasetDCECollection(", this.length, ")"].join("");
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
//_________________________________________________________________________________________________ COLLECTIONS
|
||||
/** @class Backbone model for Dataset Collections.
|
||||
* The DC API returns an array of JSON objects under the attribute elements.
|
||||
* This model:
|
||||
* - removes that array/attribute ('elements') from the model,
|
||||
* - creates a bbone collection (of the class defined in the 'collectionClass' attribute),
|
||||
* - passes that json onto the bbone collection
|
||||
* - caches the bbone collection in this.elements
|
||||
*/
|
||||
var DatasetCollection = Backbone.Model.extend(BASE_MVC.LoggableMixin)
|
||||
.extend(BASE_MVC.SearchableModelMixin)
|
||||
.extend(
|
||||
/** @lends DatasetCollection.prototype */ {
|
||||
_logNamespace: "collections",
|
||||
|
||||
/** default attributes for a model */
|
||||
defaults: {
|
||||
/* 'list', 'paired', or 'list:paired' */
|
||||
collection_type: null,
|
||||
//??
|
||||
deleted: false,
|
||||
},
|
||||
|
||||
/** Which class to use for elements */
|
||||
collectionClass: function () {
|
||||
if (this.attributes.collection_type.indexOf(":") > 0) {
|
||||
return NestedDCDCECollection;
|
||||
} else {
|
||||
return DatasetDCECollection;
|
||||
}
|
||||
},
|
||||
|
||||
/** set up: create elements instance var and (on changes to elements) update them */
|
||||
initialize: function (model, options) {
|
||||
this.debug(`${this}(DatasetCollection).initialize:`, model, options, this);
|
||||
this.elements = this._createElementsModel();
|
||||
this.on("change:elements", function () {
|
||||
this.log("change:elements");
|
||||
//TODO: prob. better to update the collection instead of re-creating it
|
||||
this.elements = this._createElementsModel();
|
||||
});
|
||||
},
|
||||
|
||||
/** move elements model attribute to full collection */
|
||||
_createElementsModel: function () {
|
||||
var collectionClass = this.collectionClass();
|
||||
this.debug(`${this}._createElementsModel`, collectionClass, this.get("elements"), this.elements);
|
||||
//TODO: same patterns as DatasetCollectionElement _createObjectModel - refactor to BASE_MVC.hasSubModel?
|
||||
var elements = this.get("elements") || [];
|
||||
this.unset("elements", { silent: true });
|
||||
var parentHdcaId = this.get("parent_hdca_id") || this.get("id");
|
||||
_.each(elements, (element, index) => {
|
||||
_.extend(element, {
|
||||
parent_hdca_id: parentHdcaId,
|
||||
});
|
||||
|
||||
// Warning: MEGA-hack ahead...
|
||||
if (!element.element_type && !element.object) {
|
||||
// The DCE has to be in error state... so we simulate it
|
||||
_.extend(element, {
|
||||
element_type: "hda",
|
||||
object: { state: "error" },
|
||||
});
|
||||
}
|
||||
});
|
||||
this.elements = new collectionClass(elements);
|
||||
//this.debug( 'collectionClass:', this.collectionClass + '', this.elements );
|
||||
return this.elements;
|
||||
},
|
||||
|
||||
// ........................................................................ common queries
|
||||
/** pass the elements back within the model json when this is serialized */
|
||||
toJSON: function () {
|
||||
var json = Backbone.Model.prototype.toJSON.call(this);
|
||||
if (this.elements) {
|
||||
json.elements = this.elements.toJSON();
|
||||
}
|
||||
return json;
|
||||
},
|
||||
|
||||
/** Is this collection in a 'ready' state no processing (for the collection) is left
|
||||
* to do on the server.
|
||||
*/
|
||||
inReadyState: function () {
|
||||
var populated = this.get("populated");
|
||||
return this.isDeletedOrPurged() || populated;
|
||||
},
|
||||
|
||||
//TODO:?? the following are the same interface as DatasetAssociation - can we combine?
|
||||
/** Does the DC contain any elements yet? Is a fetch() required? */
|
||||
hasDetails: function () {
|
||||
return this.elements.length !== 0;
|
||||
},
|
||||
|
||||
/** Given the filters, what models in this.elements would be returned? */
|
||||
getVisibleContents: function (filters) {
|
||||
// filters unused for now
|
||||
return this.elements;
|
||||
},
|
||||
|
||||
// ........................................................................ ajax
|
||||
/** override to use actual Dates objects for create/update times */
|
||||
parse: function (response, options) {
|
||||
var parsed = Backbone.Model.prototype.parse.call(this, response, options);
|
||||
if (parsed.create_time) {
|
||||
parsed.create_time = new Date(parsed.create_time);
|
||||
}
|
||||
if (parsed.update_time) {
|
||||
parsed.update_time = new Date(parsed.update_time);
|
||||
}
|
||||
return parsed;
|
||||
},
|
||||
|
||||
/** save this collection, _Mark_ing it as deleted (just a flag) */
|
||||
delete: function (recursive, purge, options) {
|
||||
recursive = recursive || false;
|
||||
purge = purge || false;
|
||||
if (this.get("deleted")) {
|
||||
return $.when();
|
||||
}
|
||||
options = Utils.merge(options, { method: "delete" });
|
||||
return this.save({ deleted: true, recursive: recursive, purge: purge }, options);
|
||||
},
|
||||
/** save this collection, _Mark_ing it as undeleted */
|
||||
undelete: function (options) {
|
||||
if (!this.get("deleted")) {
|
||||
return $.when();
|
||||
}
|
||||
return this.save({ deleted: false }, options);
|
||||
},
|
||||
|
||||
/** Is this collection deleted or purged? */
|
||||
isDeletedOrPurged: function () {
|
||||
return this.get("deleted") || this.get("purged");
|
||||
},
|
||||
|
||||
// ........................................................................ searchable
|
||||
/** searchable attributes for collections */
|
||||
searchAttributes: ["name", "tags"],
|
||||
|
||||
// ........................................................................ misc
|
||||
/** String representation */
|
||||
toString: function () {
|
||||
var idAndName = [this.get("id"), this.get("name") || this.get("element_identifier")];
|
||||
return `DatasetCollection(${idAndName.join(",")})`;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
//_________________________________________________________________________________________________ NESTED COLLECTIONS
|
||||
// this is where things get weird, man. Weird.
|
||||
//TODO: it might be possible to compact all the following...I think.
|
||||
//==============================================================================
|
||||
/** @class Backbone model for a Generic DatasetCollectionElement that is also a DatasetCollection
|
||||
* (a nested collection).
|
||||
*/
|
||||
var NestedDCDCE = DatasetCollection.extend(
|
||||
BASE_MVC.mixin(
|
||||
DatasetCollectionElementMixin,
|
||||
/** @lends NestedDCDCE.prototype */ {
|
||||
// because all objects have constructors (as this hashmap would even if this next line wasn't present)
|
||||
// the constructor in hcontentMixin won't be attached by BASE_MVC.mixin to this model
|
||||
// - re-apply manually it now
|
||||
/** call the mixin constructor */
|
||||
constructor: function (attributes, options) {
|
||||
this.debug("\t NestedDCDCE.constructor:", attributes, options);
|
||||
DatasetCollectionElementMixin.constructor.call(this, attributes, options);
|
||||
},
|
||||
|
||||
/** String representation. */
|
||||
toString: function () {
|
||||
var objStr = this.object ? `${this.object}` : this.get("element_identifier");
|
||||
return ["NestedDCDCE(", objStr, ")"].join("");
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
/** @class Backbone collection containing Generic NestedDCDCE's (nested dataset collections).
|
||||
*/
|
||||
var NestedDCDCECollection = DCECollection.extend(
|
||||
/** @lends NestedDCDCECollection.prototype */ {
|
||||
/** This is a collection of nested collections */
|
||||
model: NestedDCDCE,
|
||||
|
||||
/** String representation. */
|
||||
toString: function () {
|
||||
return ["NestedDCDCECollection(", this.length, ")"].join("");
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
export default {
|
||||
DatasetCollection: DatasetCollection,
|
||||
};
|
||||
@@ -1,114 +0,0 @@
|
||||
import $ from "jquery";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import DC_VIEW from "mvc/collection/collection-view";
|
||||
import DC_EDIT from "mvc/collection/collection-li-edit";
|
||||
import { mountModelTags } from "components/Tags";
|
||||
import _l from "utils/localization";
|
||||
import "ui/editable-text";
|
||||
|
||||
/* =============================================================================
|
||||
TODO:
|
||||
|
||||
============================================================================= */
|
||||
/** @class editable View/Controller for a dataset collection.
|
||||
*/
|
||||
var _super = DC_VIEW.CollectionView;
|
||||
var CollectionViewEdit = _super.extend(
|
||||
/** @lends CollectionView.prototype */ {
|
||||
//MODEL is either a DatasetCollection (or subclass) or a DatasetCollectionElement (list of pairs)
|
||||
|
||||
/** logger used to record this.log messages, commonly set to console */
|
||||
//logger : console,
|
||||
|
||||
/** sub view class used for datasets */
|
||||
DatasetDCEViewClass: DC_EDIT.DatasetDCEListItemEdit,
|
||||
/** sub view class used for nested collections */
|
||||
NestedDCDCEViewClass: DC_EDIT.NestedDCDCEListItemEdit,
|
||||
|
||||
getNestedDCDCEViewClass: function () {
|
||||
return DC_EDIT.NestedDCDCEListItemEdit.extend({
|
||||
foldoutPanelClass: CollectionViewEdit,
|
||||
});
|
||||
},
|
||||
|
||||
// ......................................................................... SET UP
|
||||
/** Set up the view, set up storage, bind listeners to HistoryContents events
|
||||
* @param {Object} attributes optional settings for the panel
|
||||
*/
|
||||
initialize: function (attributes) {
|
||||
_super.prototype.initialize.call(this, attributes);
|
||||
},
|
||||
|
||||
/** In this override, make the collection name editable
|
||||
*/
|
||||
_setUpBehaviors: function ($where) {
|
||||
$where = $where || this.$el;
|
||||
_super.prototype._setUpBehaviors.call(this, $where);
|
||||
if (!this.model) {
|
||||
return;
|
||||
}
|
||||
|
||||
// anon users shouldn't have access to any of the following
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (!Galaxy.user || Galaxy.user.isAnonymous()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tagsEditorShown = true;
|
||||
|
||||
//TODO: extract
|
||||
var panel = this;
|
||||
|
||||
var nameSelector = "> .controls .name";
|
||||
$where
|
||||
.find(nameSelector)
|
||||
.attr("title", _l("Click to rename collection"))
|
||||
.tooltip({ placement: "bottom" })
|
||||
.make_text_editable({
|
||||
on_finish: function (newName) {
|
||||
var previousName = panel.model.get("name");
|
||||
if (newName && newName !== previousName) {
|
||||
panel.$el.find(nameSelector).text(newName);
|
||||
panel.model.save({ name: newName }).fail(() => {
|
||||
panel.$el.find(nameSelector).text(panel.model.previous("name"));
|
||||
});
|
||||
} else {
|
||||
panel.$el.find(nameSelector).text(previousName);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const el = $where.find(".tags-display")[0];
|
||||
const propsData = {
|
||||
model: this.model,
|
||||
disabled: false,
|
||||
context: "collection-view-edit",
|
||||
};
|
||||
|
||||
const vm = mountModelTags(propsData, el);
|
||||
|
||||
const toggleEditor = () => {
|
||||
$(vm.$el).toggleClass("active");
|
||||
this.tagsEditorShown = $(vm.$el).hasClass("active");
|
||||
};
|
||||
|
||||
if (this.tagsEditorShown) {
|
||||
const editorIsOpen = $(vm.$el).hasClass("active");
|
||||
if (!editorIsOpen) {
|
||||
toggleEditor();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ........................................................................ misc
|
||||
/** string rep */
|
||||
toString: function () {
|
||||
return `CollectionViewEdit(${this.model ? this.model.get("name") : ""})`;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
export default {
|
||||
CollectionViewEdit: CollectionViewEdit,
|
||||
};
|
||||
@@ -1,235 +0,0 @@
|
||||
import _ from "underscore";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import LIST_VIEW from "mvc/list/list-view";
|
||||
import DC_LI from "mvc/collection/collection-li";
|
||||
import _l from "utils/localization";
|
||||
|
||||
var logNamespace = "collections";
|
||||
/* =============================================================================
|
||||
TODO:
|
||||
|
||||
============================================================================= */
|
||||
/** @class non-editable, read-only View/Controller for a dataset collection.
|
||||
*/
|
||||
var _super = LIST_VIEW.ModelListPanel;
|
||||
var CollectionView = _super.extend(
|
||||
/** @lends CollectionView.prototype */ {
|
||||
//MODEL is either a DatasetCollection (or subclass) or a DatasetCollectionElement (list of pairs)
|
||||
_logNamespace: logNamespace,
|
||||
|
||||
className: `${_super.prototype.className} dataset-collection-panel`,
|
||||
|
||||
/** sub view class used for datasets */
|
||||
DatasetDCEViewClass: DC_LI.DatasetDCEListItemView,
|
||||
|
||||
/** key of attribute in model to assign to this.collection */
|
||||
modelCollectionKey: "elements",
|
||||
|
||||
// ......................................................................... SET UP
|
||||
/** Set up the view, set up storage, bind listeners to HistoryContents events
|
||||
* @param {Object} attributes optional settings for the panel
|
||||
*/
|
||||
initialize: function (attributes) {
|
||||
_super.prototype.initialize.call(this, attributes);
|
||||
this.linkTarget = attributes.linkTarget || "_blank";
|
||||
this.dragItems = true;
|
||||
this.hasUser = attributes.hasUser;
|
||||
/** A stack of panels that currently cover or hide this panel */
|
||||
this.panelStack = [];
|
||||
/** The text of the link to go back to the panel containing this one */
|
||||
this.parentName = attributes.parentName;
|
||||
/** foldout or drilldown */
|
||||
this.foldoutStyle = attributes.foldoutStyle || "foldout";
|
||||
this.downloadUrl = `${getAppRoot()}api/dataset_collections/${this.model.attributes.id}/download`;
|
||||
},
|
||||
|
||||
getNestedDCDCEViewClass: function () {
|
||||
return DC_LI.NestedDCDCEListItemView.extend({
|
||||
foldoutPanelClass: CollectionView,
|
||||
});
|
||||
},
|
||||
|
||||
_queueNewRender: function ($newRender, speed) {
|
||||
speed = speed === undefined ? this.fxSpeed : speed;
|
||||
var panel = this;
|
||||
this.handleWarning($newRender);
|
||||
panel.log("_queueNewRender:", $newRender, speed);
|
||||
// TODO: jquery@1.12 doesn't change display when the elem has display: flex
|
||||
// this causes display: block for those elems after the use of show/hide animations
|
||||
// animations are removed from this view for now until fixed
|
||||
panel._swapNewRender($newRender);
|
||||
panel.trigger("rendered", panel);
|
||||
},
|
||||
|
||||
handleWarning: function ($newRender) {
|
||||
var $warns = $newRender.find(".elements-warning");
|
||||
if (this.model.get("populated_state") === "failed") {
|
||||
var error = _l(`${this.model.get("populated_state_message")}`);
|
||||
$warns.html(`<div class="errormessagesmall">${error}</div>`);
|
||||
return;
|
||||
}
|
||||
var viewLength = this.views.length;
|
||||
var elementCount = this.model.get("element_count");
|
||||
if (elementCount && elementCount !== viewLength) {
|
||||
var warning = _l(`displaying only ${viewLength} of ${elementCount} items`);
|
||||
$warns.html(`<div class="warningmessagesmall">${warning}</div>`);
|
||||
}
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------------ sub-views
|
||||
/** In this override, use model.getVisibleContents */
|
||||
_filterCollection: function () {
|
||||
//TODO: should *not* be model.getVisibleContents - visibility is not model related
|
||||
return this.model.getVisibleContents();
|
||||
},
|
||||
|
||||
/** override to return proper view class based on element_type */
|
||||
_getItemViewClass: function (model) {
|
||||
//this.debug( this + '._getItemViewClass:', model );
|
||||
//TODO: subclasses use DCEViewClass - but are currently unused - decide
|
||||
switch (model.get("element_type")) {
|
||||
case "hda":
|
||||
return this.DatasetDCEViewClass;
|
||||
case "dataset_collection":
|
||||
return this.getNestedDCDCEViewClass();
|
||||
}
|
||||
throw new TypeError("Unknown element type:", model.get("element_type"));
|
||||
},
|
||||
|
||||
/** override to add link target and anon */
|
||||
_getItemViewOptions: function (model) {
|
||||
var options = _super.prototype._getItemViewOptions.call(this, model);
|
||||
return _.extend(options, {
|
||||
linkTarget: this.linkTarget,
|
||||
hasUser: this.hasUser,
|
||||
//TODO: could move to only nested: list:paired
|
||||
foldoutStyle: this.foldoutStyle,
|
||||
});
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------------ collection sub-views
|
||||
/** In this override, add/remove expanded/collapsed model ids to/from web storage */
|
||||
_setUpItemViewListeners: function (view) {
|
||||
var panel = this;
|
||||
_super.prototype._setUpItemViewListeners.call(panel, view);
|
||||
|
||||
// use pub-sub to: handle drilldown expansion and collapse
|
||||
panel.listenTo(view, {
|
||||
"expanded:drilldown": function (v, drilldown) {
|
||||
this._expandDrilldownPanel(drilldown);
|
||||
},
|
||||
"collapsed:drilldown": function (v, drilldown) {
|
||||
this._collapseDrilldownPanel(drilldown);
|
||||
},
|
||||
});
|
||||
return this;
|
||||
},
|
||||
|
||||
/** Handle drill down by hiding this panels list and controls and showing the sub-panel */
|
||||
_expandDrilldownPanel: function (drilldown) {
|
||||
this.panelStack.push(drilldown);
|
||||
// hide this panel's controls and list, set the name for back navigation, and attach to the $el
|
||||
this.$("> .controls").add(this.$list()).hide();
|
||||
drilldown.parentName = this.model.get("name");
|
||||
this.$el.append(drilldown.render().$el);
|
||||
},
|
||||
|
||||
/** Handle drilldown close by freeing the panel and re-rendering this panel */
|
||||
_collapseDrilldownPanel: function (drilldown) {
|
||||
this.panelStack.pop();
|
||||
this.render();
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------------ panel events
|
||||
/** event map */
|
||||
events: {
|
||||
"click .navigation .back": "close",
|
||||
},
|
||||
|
||||
/** close/remove this collection panel */
|
||||
close: function (event) {
|
||||
this.remove();
|
||||
this.trigger("close");
|
||||
},
|
||||
|
||||
// ........................................................................ misc
|
||||
/** string rep */
|
||||
toString: function () {
|
||||
return `CollectionView(${this.model ? this.model.get("name") : ""})`;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
//------------------------------------------------------------------------------ TEMPLATES
|
||||
CollectionView.prototype.templates = (() => {
|
||||
var controlsTemplate = (collection, view) => {
|
||||
var subtitle = collectionDescription(view.model);
|
||||
return `
|
||||
<div class="controls">
|
||||
<div class="navigation">
|
||||
<a class="back" href="javascript:void(0)">
|
||||
<span class="fa fa-icon fa-angle-left"></span>
|
||||
${_l("Back to ")}
|
||||
${_.escape(view.parentName)}
|
||||
</a>
|
||||
</div>
|
||||
<div class="title">
|
||||
<div class="name">${_.escape(collection.name) || _.escape(collection.element_identifier)}</div>
|
||||
<div class="subtitle">
|
||||
${subtitle}
|
||||
</div>
|
||||
</div>
|
||||
<div class="elements-warning">
|
||||
</div>
|
||||
<div class="tags-display"></div>
|
||||
<div class="actions">
|
||||
<a class="download-btn icon-btn" href="${view.downloadUrl}"
|
||||
title="" download="" data-original-title="Download Collection">
|
||||
<span class="fa fa-floppy-o"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
return _.extend(_.clone(_super.prototype.templates), {
|
||||
controls: controlsTemplate,
|
||||
});
|
||||
})();
|
||||
|
||||
function collectionTypeDescription(collection) {
|
||||
var collectionType = collection.get("collection_type");
|
||||
var collectionTypeDescription;
|
||||
if (collectionType == "list") {
|
||||
collectionTypeDescription = _l("list");
|
||||
} else if (collectionType == "paired") {
|
||||
collectionTypeDescription = _l("dataset pair");
|
||||
} else if (collectionType == "list:paired") {
|
||||
collectionTypeDescription = _l("list of pairs");
|
||||
} else {
|
||||
collectionTypeDescription = _l("nested list");
|
||||
}
|
||||
return collectionTypeDescription;
|
||||
}
|
||||
|
||||
function collectionDescription(collection) {
|
||||
var elementCount = collection.get("element_count");
|
||||
|
||||
var itemsDescription = `a ${collectionTypeDescription(collection)}`;
|
||||
if (elementCount) {
|
||||
var countDescription;
|
||||
if (elementCount == 1) {
|
||||
countDescription = "with 1 item";
|
||||
} else if (elementCount) {
|
||||
countDescription = `with ${elementCount} items`;
|
||||
}
|
||||
itemsDescription = `${itemsDescription} ${_l(countDescription)}`;
|
||||
}
|
||||
return itemsDescription;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
export default {
|
||||
collectionTypeDescription: collectionTypeDescription,
|
||||
collectionDescription: collectionDescription,
|
||||
CollectionView: CollectionView,
|
||||
};
|
||||
@@ -1,434 +0,0 @@
|
||||
import $ from "jquery";
|
||||
import _ from "underscore";
|
||||
import Backbone from "backbone";
|
||||
import DATASET from "mvc/dataset/dataset-model";
|
||||
import DATASET_LIST from "mvc/dataset/dataset-list";
|
||||
import MODAL from "mvc/ui/ui-modal";
|
||||
import BASE_MVC from "mvc/base-mvc";
|
||||
import _l from "utils/localization";
|
||||
|
||||
var logNamespace = "dataset";
|
||||
/* ============================================================================
|
||||
TODO:
|
||||
does this really work with mixed contents?
|
||||
Single dataset choice: allow none?
|
||||
tooltips rendered *behind* modal
|
||||
collection selector
|
||||
better handling when no results returned from filterDatasetJSON
|
||||
pass optional subtitle from choice display to modal
|
||||
onfirstclick
|
||||
drop target
|
||||
return modal with promise?
|
||||
|
||||
auto showing the modal may not be best
|
||||
|
||||
============================================================================ */
|
||||
/** Filters an array of dataset plain JSON objs.
|
||||
*/
|
||||
function _filterDatasetJSON(datasetJSON, where, datasetsOnly) {
|
||||
//TODO: replace with _.matches (underscore 1.6.0)
|
||||
function matches(obj, toMatch) {
|
||||
for (var key in toMatch) {
|
||||
if (Object.prototype.hasOwnProperty.call(toMatch, key)) {
|
||||
if (obj[key] !== toMatch[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return datasetJSON.filter((json) => {
|
||||
console.debug(json);
|
||||
return (
|
||||
!json.deleted &&
|
||||
json.visible &&
|
||||
(!datasetsOnly || json.collection_type === undefined) &&
|
||||
matches(json, where)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
/** Given an array of plain JSON objs rep. datasets, show a modal allowing a choice
|
||||
* of one or more of those datasets.
|
||||
*
|
||||
* Pass:
|
||||
* an array of plain JSON objects representing allowed dataset choices
|
||||
* a map of options (see below)
|
||||
*
|
||||
* Options:
|
||||
* datasetsOnly: T: display only datasets, F: datasets + dataset collections
|
||||
* where: a map of attributes available choices *must have* (defaults to { state: 'ok' })
|
||||
* multiselect: T: user can select more than one, F: only one
|
||||
* selected: array of dataset ids to make them selected by default
|
||||
*
|
||||
* @example:
|
||||
* var datasetJSON = // from ajax or bootstrap
|
||||
* // returns a jQuery promise (that 'fail's only if no datasets are found matching 'where' below)
|
||||
* var choice = new DatasetChoiceModal( datasetJSON, {
|
||||
* datasetsOnly : false,
|
||||
* where : { state: 'ok', file_ext: 'bed', ... },
|
||||
* multiselect : true,
|
||||
* selected : [ 'df7a1f0c02a5b08e', 'abcdef0123456789' ]
|
||||
*
|
||||
* }).done( function( json ){
|
||||
* if( json ){
|
||||
* console.debug( json );
|
||||
* // returned choice will always be an array (single or multi)
|
||||
* // [{ <selected dataset JSON 1>, <selected dataset JSON 2>, ... }]
|
||||
* // ... do stuff
|
||||
* } else {
|
||||
* // json will === null if the user cancelled selection
|
||||
* console.debug( 'cancelled' );
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
var DatasetChoiceModal = (datasetJSON, options) => {
|
||||
// option defaults
|
||||
options = _.defaults(options || {}, {
|
||||
// show datasets or datasets and collections
|
||||
datasetsOnly: true,
|
||||
// map of attributes to filter datasetJSON by
|
||||
where: { state: "ok" },
|
||||
// select more than one dataset?
|
||||
multiselect: false,
|
||||
// any dataset ids that will display as already selected
|
||||
selected: [],
|
||||
});
|
||||
// default title should depend on multiselect
|
||||
options.title = options.title || (options.multiselect ? _l("Choose datasets:") : _l("Choose a dataset:"));
|
||||
|
||||
var modal;
|
||||
var list;
|
||||
var buttons;
|
||||
var promise = $.Deferred();
|
||||
var filterFn = options.filter || _filterDatasetJSON;
|
||||
|
||||
// filter the given datasets and if none left return a rejected promise for use with fail()
|
||||
datasetJSON = filterFn(datasetJSON, options.where, options.datasetsOnly);
|
||||
if (!datasetJSON.length) {
|
||||
return promise.reject("No matches found");
|
||||
}
|
||||
|
||||
// resolve the returned promise with the json of the selected datasets
|
||||
function resolveWithSelected() {
|
||||
promise.resolve(list.getSelectedModels().map((model) => model.toJSON()));
|
||||
}
|
||||
// if multiselect - add a button for the user to complete the changes
|
||||
if (options.multiselect) {
|
||||
buttons = {};
|
||||
buttons[_l("Ok")] = resolveWithSelected;
|
||||
}
|
||||
|
||||
// create a full-height modal that's cancellable, remove unneeded elements and styles
|
||||
modal = new MODAL.View({
|
||||
height: "auto",
|
||||
buttons: buttons,
|
||||
closing_events: true,
|
||||
closing_callback: function () {
|
||||
promise.resolve(null);
|
||||
},
|
||||
body: ['<div class="list-panel"></div>'].join(""),
|
||||
});
|
||||
modal.$(".modal-header").remove();
|
||||
modal.$(".modal-footer").css("margin-top", "0px");
|
||||
|
||||
// attach a dataset list (of the filtered datasets) to that modal that's selectable
|
||||
list = new DATASET_LIST.DatasetList({
|
||||
title: options.title,
|
||||
subtitle:
|
||||
options.subtitle ||
|
||||
_l(
|
||||
[
|
||||
//TODO: as option
|
||||
"Click the checkboxes on the right to select datasets. ",
|
||||
"Click the datasets names to see their details. ",
|
||||
].join("")
|
||||
),
|
||||
el: modal.$body.find(".list-panel"),
|
||||
selecting: true,
|
||||
selected: options.selected,
|
||||
collection: new DATASET.DatasetAssociationCollection(datasetJSON),
|
||||
});
|
||||
|
||||
// when the list is rendered, show the modal (also add a specifying class for css)
|
||||
list.once("rendered:initial", () => {
|
||||
modal.show();
|
||||
modal.$el.addClass("dataset-choice-modal");
|
||||
});
|
||||
if (!options.multiselect) {
|
||||
// if single select, remove the all/none list actions from the panel
|
||||
list.on("rendered", () => {
|
||||
list.$(".list-actions").hide();
|
||||
});
|
||||
// if single select, immediately resolve on a single selection
|
||||
list.on("view:selected", (view) => {
|
||||
promise.resolve([view.model.toJSON()]);
|
||||
});
|
||||
}
|
||||
list.render(0);
|
||||
|
||||
// return the promise, and on any resolution close the modal
|
||||
return promise.always(() => {
|
||||
modal.hide();
|
||||
});
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
/** Activator for single dataset selection modal and display of the selected dataset.
|
||||
* The activator/display will show as a single div and, when a dataset is selected,
|
||||
* show the name and details of the selected dataset.
|
||||
*
|
||||
* When clicked the div will generate a DatasetChoiceModal of the available choices.
|
||||
*
|
||||
* Options:
|
||||
* datasetJSON: array of plain json objects representing allowed choices
|
||||
* datasetsOnly: T: only show datasets in the allowed choices, F: datasets + collections
|
||||
* where: map of attributes to filter datasetJSON by (e.g. { file_ext: 'bed' })
|
||||
* label: the label/prompt displayed
|
||||
* selected: array of dataset ids that will show as already selected in the control
|
||||
*
|
||||
* @example:
|
||||
* var choice1 = new DATASET_CHOICE.DatasetChoice({
|
||||
* datasetJSON : datasetJSON,
|
||||
* label : 'Input dataset',
|
||||
* selected : [ 'df7a1f0c02a5b08e' ]
|
||||
* });
|
||||
* $( 'body' ).append( choice1.render().$el )
|
||||
*
|
||||
* Listen to the DatasetChoice to react to changes in the user's choice/selection:
|
||||
* @example:
|
||||
* choice1.on( 'selected', function( chooser, selectionJSONArray ){
|
||||
* // ... do stuff with new selections
|
||||
* });
|
||||
*/
|
||||
var DatasetChoice = Backbone.View.extend(BASE_MVC.LoggableMixin).extend({
|
||||
_logNamespace: logNamespace,
|
||||
|
||||
className: "dataset-choice",
|
||||
|
||||
/** set up defaults, options, and listeners */
|
||||
initialize: function (attributes) {
|
||||
this.debug(`${this}(DatasetChoice).initialize:`, attributes);
|
||||
|
||||
this.label = attributes.label !== undefined ? _l(attributes.label) : "";
|
||||
this.where = attributes.where;
|
||||
this.datasetsOnly = attributes.datasetsOnly !== undefined ? attributes.datasetsOnly : true;
|
||||
|
||||
this.datasetJSON = attributes.datasetJSON || [];
|
||||
this.selected = attributes.selected || [];
|
||||
|
||||
this._setUpListeners();
|
||||
},
|
||||
|
||||
/** add any (bbone) listeners */
|
||||
_setUpListeners: function () {
|
||||
//this.on( 'all', function(){
|
||||
// this.log( this + '', arguments );
|
||||
//});
|
||||
},
|
||||
|
||||
/** render the view */
|
||||
render: function () {
|
||||
var json = this.toJSON();
|
||||
this.$el.html(this._template(json));
|
||||
this.$(".selected").replaceWith(this._renderSelected(json));
|
||||
return this;
|
||||
},
|
||||
|
||||
/** return plain html for the overall control */
|
||||
_template: function (json) {
|
||||
return _.template(
|
||||
["<label>", '<span class="prompt"><%- label %></span>', '<div class="selected"></div>', "</label>"].join("")
|
||||
)(json);
|
||||
},
|
||||
|
||||
/** return jQ DOM for the selected dataset (only one) */
|
||||
_renderSelected: function (json) {
|
||||
if (json.selected.length) {
|
||||
//TODO: break out?
|
||||
return $(
|
||||
_.template(
|
||||
[
|
||||
'<div class="selected">',
|
||||
'<span class="title"><%- selected.hid %>: <%- selected.name %></span>',
|
||||
'<span class="subtitle">',
|
||||
"<i><%- selected.misc_blurb %></i>",
|
||||
"<i>",
|
||||
`${_l("format")}: `,
|
||||
"<%- selected.file_ext %></i>",
|
||||
"<i><%- selected.misc_info %></i>",
|
||||
"</span>",
|
||||
"</div>",
|
||||
].join(""),
|
||||
{ variable: "selected" }
|
||||
)(json.selected[0])
|
||||
);
|
||||
}
|
||||
return $(['<span class="none-selected-msg">(', _l("click to select a dataset"), ")</span>"].join(""));
|
||||
},
|
||||
|
||||
//TODO:?? why not just pass in view?
|
||||
/** return a plain JSON object with both the view and dataset attributes */
|
||||
toJSON: function () {
|
||||
var chooser = this;
|
||||
return {
|
||||
label: chooser.label,
|
||||
datasets: chooser.datasetJSON,
|
||||
selected: _.compact(
|
||||
_.map(chooser.selected, (id) =>
|
||||
_.findWhere(chooser.datasetJSON, {
|
||||
id: id,
|
||||
})
|
||||
)
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
/** event map: when to open the modal */
|
||||
events: {
|
||||
// the whole thing functions as a button
|
||||
click: "chooseWithModal",
|
||||
},
|
||||
|
||||
//TODO:?? modal to prop of this?
|
||||
//TODO:?? should be able to handle 'none selectable' on initialize
|
||||
/** open the modal and handle the promise representing the user's choice
|
||||
* @fires 'selected' when the user selects dataset(s) - passed full json of the selected datasets
|
||||
* @fires 'cancelled' when the user clicks away/closes the modal (no selection made) - passed this
|
||||
* @fires 'error' if the modal has no selectable datasets based on this.where - passed this and other args
|
||||
*/
|
||||
chooseWithModal: function () {
|
||||
var chooser = this;
|
||||
|
||||
return this._createModal()
|
||||
.done((json) => {
|
||||
if (json) {
|
||||
chooser.selected = _.pluck(json, "id");
|
||||
chooser.trigger("selected", chooser, json);
|
||||
chooser.render();
|
||||
} else {
|
||||
chooser.trigger("cancelled", chooser);
|
||||
}
|
||||
})
|
||||
.fail(function () {
|
||||
chooser.trigger("error", chooser, arguments);
|
||||
});
|
||||
},
|
||||
|
||||
/** create and return the modal to use for choosing */
|
||||
_createModal: function () {
|
||||
return new DatasetChoiceModal(this.datasetJSON, this._getModalOptions());
|
||||
},
|
||||
|
||||
/** return a plain JSON containing the options to pass to the modal */
|
||||
_getModalOptions: function () {
|
||||
return {
|
||||
title: this.label,
|
||||
multiselect: false,
|
||||
selected: this.selected,
|
||||
where: this.where,
|
||||
datasetsOnly: this.datasetsOnly,
|
||||
};
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------------ misc
|
||||
/** string rep */
|
||||
toString: function () {
|
||||
return `DatasetChoice(${this.selected})`;
|
||||
},
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
/** Activator for multiple dataset selection modal and display of the selected datasets.
|
||||
* The activator/display will show as a table of all choices.
|
||||
*
|
||||
* See DatasetChoice (above) for example usage.
|
||||
*
|
||||
* Additional options:
|
||||
* showHeaders: T: show headers for selected dataset attributes in the display table
|
||||
* cells: map of attribute keys -> Human readable/localized column headers
|
||||
* (e.g. { file_ext: _l( 'Format' ) }) - defaults are listed below
|
||||
*/
|
||||
var MultiDatasetChoice = DatasetChoice.extend({
|
||||
className: `${DatasetChoice.prototype.className} multi`,
|
||||
|
||||
/** default (dataset attribute key -> table header text) map of what cells to display in the table */
|
||||
cells: {
|
||||
hid: _l("History #"),
|
||||
name: _l("Name"),
|
||||
misc_blurb: _l("Summary"),
|
||||
file_ext: _l("Format"),
|
||||
genome_build: _l("Genome"),
|
||||
tags: _l("Tags"),
|
||||
annotation: _l("Annotation"),
|
||||
},
|
||||
|
||||
/** in this override, add the showHeaders and cells options */
|
||||
initialize: function (attributes) {
|
||||
this.showHeaders = attributes.showHeaders !== undefined ? attributes.showHeaders : true;
|
||||
this.cells = attributes.cells || this.cells;
|
||||
DatasetChoice.prototype.initialize.call(this, attributes);
|
||||
},
|
||||
|
||||
/** in this override, display the selected datasets as a table with optional headers */
|
||||
_renderSelected: function (json) {
|
||||
if (json.selected.length) {
|
||||
return $(
|
||||
_.template(
|
||||
[
|
||||
'<table class="selected">',
|
||||
"<% if( json.showHeaders ){ %>",
|
||||
"<thead><tr>",
|
||||
"<% _.map( json.cells, function( val, key ){ %>",
|
||||
"<th><%- val %></th>",
|
||||
"<% }); %>",
|
||||
"</tr></thead>",
|
||||
"<% } %>",
|
||||
"<tbody>",
|
||||
"<% _.map( json.selected, function( selected ){ %>",
|
||||
"<tr>",
|
||||
"<% _.map( json.cells, function( val, key ){ %>",
|
||||
'<td class="cell-<%- key %>"><%- selected[ key ] %></td>',
|
||||
"<% }) %>",
|
||||
"</tr>",
|
||||
"<% }); %>",
|
||||
"</tbody>",
|
||||
"</table>",
|
||||
].join(""),
|
||||
{ variable: "json" }
|
||||
)(json)
|
||||
);
|
||||
}
|
||||
return $(['<span class="none-selected-msg">(', _l("click to select a dataset"), ")</span>"].join(""));
|
||||
},
|
||||
|
||||
/** in this override, send the showHeaders and cells options as well */
|
||||
toJSON: function () {
|
||||
return _.extend(DatasetChoice.prototype.toJSON.call(this), {
|
||||
showHeaders: this.showHeaders,
|
||||
cells: this.cells,
|
||||
});
|
||||
},
|
||||
|
||||
/** in this override, set multiselect to true */
|
||||
_getModalOptions: function () {
|
||||
return _.extend(DatasetChoice.prototype._getModalOptions.call(this), {
|
||||
multiselect: true,
|
||||
});
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------------ misc
|
||||
/** string rep */
|
||||
toString: function () {
|
||||
return `DatasetChoice(${this.selected})`;
|
||||
},
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
export default {
|
||||
DatasetChoiceModal: DatasetChoiceModal,
|
||||
DatasetChoice: DatasetChoice,
|
||||
MultiDatasetChoice: MultiDatasetChoice,
|
||||
};
|
||||
@@ -1,472 +0,0 @@
|
||||
import _ from "underscore";
|
||||
import $ from "jquery";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import STATES from "mvc/dataset/states";
|
||||
import DATASET_LI from "mvc/dataset/dataset-li";
|
||||
import { mountModelTags } from "components/Tags";
|
||||
import ANNOTATIONS from "mvc/annotation";
|
||||
import faIconButton from "ui/fa-icon-button";
|
||||
import BASE_MVC from "mvc/base-mvc";
|
||||
import _l from "utils/localization";
|
||||
|
||||
//==============================================================================
|
||||
var _super = DATASET_LI.DatasetListItemView;
|
||||
/** @class Editing view for DatasetAssociation.
|
||||
*/
|
||||
var DatasetListItemEdit = _super.extend(
|
||||
/** @lends DatasetListItemEdit.prototype */ {
|
||||
/** set up: options */
|
||||
initialize: function (attributes) {
|
||||
_super.prototype.initialize.call(this, attributes);
|
||||
this.hasUser = attributes.hasUser;
|
||||
|
||||
/** allow user purge of dataset files? */
|
||||
this.purgeAllowed = attributes.purgeAllowed || false;
|
||||
|
||||
//TODO: move to HiddenUntilActivatedViewMixin
|
||||
/** should the tags editor be shown or hidden initially? */
|
||||
this.tagsEditorShown = attributes.tagsEditorShown || false;
|
||||
/** should the tags editor be shown or hidden initially? */
|
||||
this.annotationEditorShown = attributes.annotationEditorShown || false;
|
||||
},
|
||||
|
||||
// ......................................................................... titlebar actions
|
||||
/** In this override, add the other two primary actions: edit and delete */
|
||||
_renderPrimaryActions: function () {
|
||||
var actions = _super.prototype._renderPrimaryActions.call(this);
|
||||
if (this.model.get("state") === STATES.NOT_VIEWABLE) {
|
||||
return actions;
|
||||
}
|
||||
// render the display, edit attr and delete icon-buttons
|
||||
return _super.prototype._renderPrimaryActions
|
||||
.call(this)
|
||||
.concat([this._renderEditButton(), this._renderDeleteButton()]);
|
||||
},
|
||||
|
||||
//TODO: move titleButtons into state renderers, remove state checks in the buttons
|
||||
|
||||
/** Render icon-button to edit the attributes (format, permissions, etc.) this dataset. */
|
||||
_renderEditButton: function () {
|
||||
var self = this;
|
||||
// don't show edit while uploading, in-accessible
|
||||
// DO show if in error (ala previous history panel)
|
||||
if (this.model.get("state") === STATES.DISCARDED || !this.model.get("accessible")) {
|
||||
return null;
|
||||
}
|
||||
var purged = this.model.get("purged");
|
||||
var deleted = this.model.get("deleted");
|
||||
|
||||
var editBtnData = {
|
||||
title: _l("Edit attributes"),
|
||||
href: `${getAppRoot()}datasets/edit?dataset_id=${this.model.get("element_id") || this.model.get("id")}`,
|
||||
faIcon: "fa-pencil",
|
||||
classes: "edit-btn",
|
||||
onclick: function (ev) {
|
||||
if (!deleted && !purged) {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (Galaxy.router) {
|
||||
ev.preventDefault();
|
||||
const identifier = self.model.get("element_id") || self.model.get("id");
|
||||
Galaxy.router.push("datasets/edit", {
|
||||
dataset_id: identifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// disable if purged or deleted and explain why in the tooltip
|
||||
if (deleted || purged) {
|
||||
editBtnData.disabled = true;
|
||||
if (purged) {
|
||||
editBtnData.title = _l("Cannot edit attributes of datasets removed from disk");
|
||||
} else if (deleted) {
|
||||
editBtnData.title = _l("Undelete dataset to edit attributes");
|
||||
}
|
||||
|
||||
// disable if still uploading or new
|
||||
} else if ([STATES.UPLOAD, STATES.NEW].includes(this.model.get("state"))) {
|
||||
editBtnData.disabled = true;
|
||||
editBtnData.title = _l("This dataset is not yet editable");
|
||||
}
|
||||
return faIconButton(editBtnData);
|
||||
},
|
||||
|
||||
/** Render icon-button to delete this hda. */
|
||||
_renderDeleteButton: function () {
|
||||
// don't show delete if...
|
||||
if (!this.model.get("accessible")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var deletedAlready = this.model.isDeletedOrPurged();
|
||||
return faIconButton({
|
||||
title: !deletedAlready ? _l("Delete") : _l("Dataset is already deleted"),
|
||||
disabled: deletedAlready,
|
||||
faIcon: "fa-times",
|
||||
classes: "delete-btn",
|
||||
onclick: function () {
|
||||
self.$el.find(".icon-btn.delete-btn").tooltip("dispose");
|
||||
self.model["delete"]();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// ......................................................................... details
|
||||
/** In this override, add tags and annotations controls, make the ? dbkey a link to editing page */
|
||||
_renderDetails: function () {
|
||||
//TODO: generalize to be allow different details for each state
|
||||
var $details = _super.prototype._renderDetails.call(this);
|
||||
|
||||
var state = this.model.get("state");
|
||||
|
||||
if (!this.model.isDeletedOrPurged()) {
|
||||
//Enable tagging+annotation regardless of job state (see issue #6330)
|
||||
this._renderTags($details);
|
||||
this._renderAnnotation($details);
|
||||
if ([STATES.OK, STATES.FAILED_METADATA].includes(state)) {
|
||||
this._makeDbkeyEditLink($details);
|
||||
}
|
||||
}
|
||||
|
||||
this._setUpBehaviors($details);
|
||||
return $details;
|
||||
},
|
||||
|
||||
/**************************************************************************
|
||||
* Render help button to show tool help text without rerunning the tool.
|
||||
* Issue #2100
|
||||
*/
|
||||
_renderToolHelpButton: function () {
|
||||
var datasetID = this.model.attributes.dataset_id;
|
||||
var jobID = this.model.attributes.creating_job;
|
||||
var self = this;
|
||||
|
||||
var parseToolBuild = (data) => {
|
||||
var helpString = `<div id="thdiv-${datasetID}" class="toolhelp">`;
|
||||
if (data.name && data.help) {
|
||||
helpString += `<strong>Tool help for ${data.name}</strong><hr/>`;
|
||||
helpString += data.help;
|
||||
} else {
|
||||
helpString += "<strong>Tool help is unavailable for this dataset.</strong><hr/>";
|
||||
}
|
||||
helpString += "</div>";
|
||||
self.$el.find(".details").append($.parseHTML(helpString));
|
||||
};
|
||||
var parseToolID = (data) => {
|
||||
$.ajax({
|
||||
url: `${getAppRoot()}api/tools/${data.tool_id}/build`,
|
||||
})
|
||||
.done((data) => {
|
||||
parseToolBuild(data);
|
||||
})
|
||||
.fail(() => {
|
||||
parseToolBuild({});
|
||||
});
|
||||
};
|
||||
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (Galaxy.user.id === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return faIconButton({
|
||||
title: _l("Tool Help"),
|
||||
classes: "icon-btn",
|
||||
href: "#",
|
||||
faIcon: "fa-question",
|
||||
onclick: function (ev) {
|
||||
ev.preventDefault();
|
||||
if (self.$el.find(".toolhelp").length > 0) {
|
||||
self.$el.find(".toolhelp").toggle();
|
||||
} else {
|
||||
$.ajax({
|
||||
url: `${getAppRoot()}api/jobs/${jobID}`,
|
||||
})
|
||||
.done((data) => {
|
||||
parseToolID(data);
|
||||
})
|
||||
.fail(() => {
|
||||
console.log(
|
||||
`Failed at recovering job information from the Galaxy API for job id "${jobID}".`
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
//*************************************************************************
|
||||
|
||||
/** Add less commonly used actions in the details section based on state */
|
||||
_renderSecondaryActions: function () {
|
||||
var actions = _super.prototype._renderSecondaryActions.call(this);
|
||||
switch (this.model.get("state")) {
|
||||
case STATES.UPLOAD:
|
||||
case STATES.NOT_VIEWABLE:
|
||||
return actions;
|
||||
case STATES.ERROR:
|
||||
// error button comes first
|
||||
actions.unshift(this._renderErrButton());
|
||||
return actions.concat([this._renderRerunButton(), this._renderToolHelpButton()]);
|
||||
case STATES.OK:
|
||||
case STATES.FAILED_METADATA:
|
||||
return actions.concat([
|
||||
this._renderRerunButton(),
|
||||
this._renderVisualizationsButton(),
|
||||
this._renderToolHelpButton(),
|
||||
]);
|
||||
}
|
||||
return actions.concat([this._renderRerunButton(), this._renderToolHelpButton()]);
|
||||
},
|
||||
|
||||
/** Render icon-button to report an error on this dataset to the galaxy admin. */
|
||||
_renderErrButton: function () {
|
||||
var self = this;
|
||||
return faIconButton({
|
||||
title: _l("View or report this error"),
|
||||
href: `${getAppRoot()}datasets/error?dataset_id=${
|
||||
self.model.get("element_id") || self.model.get("id")
|
||||
}`,
|
||||
classes: "report-error-btn",
|
||||
faIcon: "fa-bug",
|
||||
onclick: function (ev) {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (Galaxy.router) {
|
||||
ev.preventDefault();
|
||||
Galaxy.router.push("datasets/error", {
|
||||
dataset_id: self.model.get("element_id") || self.model.get("id"),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/** Render icon-button to re-run the job that created this dataset. */
|
||||
_renderRerunButton: function () {
|
||||
var creating_job = this.model.get("creating_job");
|
||||
if (this.model.get("rerunnable")) {
|
||||
return faIconButton({
|
||||
title: _l("Run this job again"),
|
||||
href: this.model.urls.rerun,
|
||||
classes: "rerun-btn",
|
||||
target: this.linkTarget,
|
||||
faIcon: "fa-redo",
|
||||
onclick: function (ev) {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (Galaxy.router) {
|
||||
ev.preventDefault();
|
||||
Galaxy.router.push("/", {
|
||||
job_id: creating_job,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/** Render an icon-button or popupmenu of links based on the applicable visualizations */
|
||||
_renderVisualizationsButton: function () {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
const dsid = this.model.get("element_id") || this.model.get("id");
|
||||
const url = getAppRoot() + "visualizations?dataset_id=" + dsid;
|
||||
return faIconButton({
|
||||
title: _l("Visualize this data"),
|
||||
href: url,
|
||||
classes: "visualization-link",
|
||||
faIcon: "fa-bar-chart-o",
|
||||
onclick: (ev) => {
|
||||
if (Galaxy.frame && Galaxy.frame.active) {
|
||||
ev.preventDefault();
|
||||
Galaxy.frame.add({ url: url, title: "Visualization" });
|
||||
} else if (Galaxy.router) {
|
||||
ev.preventDefault();
|
||||
Galaxy.router.push("visualizations", {
|
||||
dataset_id: dsid,
|
||||
});
|
||||
Galaxy.trigger("activate-hda", dsid);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/** Render the tags list/control */
|
||||
_renderTags: function ($where) {
|
||||
if (!this.hasUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = $where.find(".tags-display")[0];
|
||||
|
||||
const propsData = {
|
||||
model: this.model,
|
||||
disabled: false,
|
||||
context: "dataset-li-edit",
|
||||
};
|
||||
|
||||
const vm = mountModelTags(propsData, el);
|
||||
|
||||
// tag icon button open/closes
|
||||
const activator = faIconButton({
|
||||
title: _l("Edit dataset tags"),
|
||||
classes: "tag-btn",
|
||||
faIcon: "fa-tags",
|
||||
}).appendTo($where.find(".actions .right"));
|
||||
|
||||
const toggleEditor = () => {
|
||||
$(vm.$el).toggleClass("active");
|
||||
this.tagsEditorShown = $(vm.$el).hasClass("active");
|
||||
};
|
||||
|
||||
activator.on("click", toggleEditor);
|
||||
|
||||
if (this.tagsEditorShown) {
|
||||
const editorIsOpen = $(vm.$el).hasClass("active");
|
||||
if (!editorIsOpen) {
|
||||
toggleEditor();
|
||||
}
|
||||
}
|
||||
|
||||
return vm;
|
||||
},
|
||||
|
||||
/** Render the annotation display/control */
|
||||
_renderAnnotation: function ($where) {
|
||||
if (!this.hasUser) {
|
||||
return;
|
||||
}
|
||||
var view = this;
|
||||
this.annotationEditor = new ANNOTATIONS.AnnotationEditor({
|
||||
model: this.model,
|
||||
el: $where.find(".annotation-display"),
|
||||
onshowFirstTime: function () {
|
||||
this.render();
|
||||
},
|
||||
// persist state on the hda view (and not the editor) since these are currently re-created each time
|
||||
onshow: function () {
|
||||
view.annotationEditorShown = true;
|
||||
},
|
||||
onhide: function () {
|
||||
view.annotationEditorShown = false;
|
||||
},
|
||||
$activator: faIconButton({
|
||||
title: _l("Edit dataset annotation"),
|
||||
classes: "annotate-btn",
|
||||
faIcon: "fa-comment",
|
||||
}).appendTo($where.find(".actions .right")),
|
||||
});
|
||||
if (this.annotationEditorShown) {
|
||||
this.annotationEditor.toggle(true);
|
||||
}
|
||||
},
|
||||
|
||||
/** If the format/dbkey/genome_build isn't set, make the display a link to the edit page */
|
||||
_makeDbkeyEditLink: function ($details) {
|
||||
// make the dbkey a link to editing
|
||||
if (this.model.get("metadata_dbkey") === "?" && !this.model.isDeletedOrPurged()) {
|
||||
var editableDbkey = $('<a class="value">?</a>')
|
||||
.attr("href", this.model.urls.edit)
|
||||
.attr("target", "_top");
|
||||
$details.find(".dbkey .value").replaceWith(editableDbkey);
|
||||
}
|
||||
},
|
||||
|
||||
// ......................................................................... events
|
||||
/** event map */
|
||||
events: _.extend(_.clone(_super.prototype.events), {
|
||||
"click .undelete-link": "_clickUndeleteLink",
|
||||
"click .purge-link": "_clickPurgeLink",
|
||||
|
||||
"click .edit-btn": function (ev) {
|
||||
this.trigger("edit", this, ev);
|
||||
},
|
||||
"click .delete-btn": function (ev) {
|
||||
this.trigger("delete", this, ev);
|
||||
},
|
||||
"click .rerun-btn": function (ev) {
|
||||
this.trigger("rerun", this, ev);
|
||||
},
|
||||
"click .report-err-btn": function (ev) {
|
||||
this.trigger("report-err", this, ev);
|
||||
},
|
||||
"click .visualization-btn": function (ev) {
|
||||
this.trigger("visualize", this, ev);
|
||||
},
|
||||
"click .dbkey a": function (ev) {
|
||||
this.trigger("edit", this, ev);
|
||||
},
|
||||
}),
|
||||
|
||||
/** listener for item undelete (in the messages section) */
|
||||
_clickUndeleteLink: function (ev) {
|
||||
this.model.undelete();
|
||||
return false;
|
||||
},
|
||||
|
||||
/** listener for item purge (in the messages section) */
|
||||
_clickPurgeLink: function (ev) {
|
||||
if (window.confirm(_l("This will permanently remove the data in your dataset. Are you sure?"))) {
|
||||
this.model.purge();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
// ......................................................................... misc
|
||||
/** string rep */
|
||||
toString: function () {
|
||||
var modelString = this.model ? `${this.model}` : "(no model)";
|
||||
return `HDAEditView(${modelString})`;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// ............................................................................ TEMPLATES
|
||||
/** underscore templates */
|
||||
DatasetListItemEdit.prototype.templates = (() => {
|
||||
var warnings = _.extend({}, _super.prototype.templates.warnings, {
|
||||
failed_metadata: BASE_MVC.wrapTemplate(
|
||||
[
|
||||
// in this override, provide a link to the edit page
|
||||
'<% if( dataset.state === "failed_metadata" ){ %>',
|
||||
'<div class="failed_metadata-warning warningmessagesmall">',
|
||||
_l("An error occurred setting the metadata for this dataset"),
|
||||
'<br /><a href="<%- dataset.urls.edit %>" target="_top">',
|
||||
_l("Set it manually or retry auto-detection"),
|
||||
"</a>",
|
||||
"</div>",
|
||||
"<% } %>",
|
||||
],
|
||||
"dataset"
|
||||
),
|
||||
|
||||
deleted: BASE_MVC.wrapTemplate(
|
||||
[
|
||||
// in this override, provide links to undelete or purge the dataset
|
||||
"<% if( dataset.deleted && !dataset.purged ){ %>",
|
||||
// deleted not purged
|
||||
'<div class="deleted-msg warningmessagesmall">',
|
||||
_l("This dataset has been deleted"),
|
||||
'<br /><a class="undelete-link" href="javascript:void(0);">',
|
||||
_l("Undelete it"),
|
||||
"</a>",
|
||||
"<% if( view.purgeAllowed ){ %>",
|
||||
'<br /><a class="purge-link" href="javascript:void(0);">',
|
||||
_l("Permanently remove it from disk"),
|
||||
"</a>",
|
||||
"<% } %>",
|
||||
"</div>",
|
||||
"<% } %>",
|
||||
],
|
||||
"dataset"
|
||||
),
|
||||
});
|
||||
return _.extend({}, _super.prototype.templates, {
|
||||
warnings: warnings,
|
||||
});
|
||||
})();
|
||||
|
||||
//==============================================================================
|
||||
export default {
|
||||
DatasetListItemEdit: DatasetListItemEdit,
|
||||
};
|
||||
@@ -1,639 +0,0 @@
|
||||
import _ from "underscore";
|
||||
import $ from "jquery";
|
||||
import Backbone from "backbone";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import LIST_ITEM from "mvc/list/list-item";
|
||||
import STATES from "mvc/dataset/states";
|
||||
import faIconButton from "ui/fa-icon-button";
|
||||
import BASE_MVC from "mvc/base-mvc";
|
||||
import _l from "utils/localization";
|
||||
import { mountNametags } from "components/Nametags";
|
||||
import { Toast } from "ui/toast";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
|
||||
var logNamespace = "dataset";
|
||||
/*==============================================================================
|
||||
TODO:
|
||||
straighten out state rendering and templates used
|
||||
inaccessible/STATES.NOT_VIEWABLE is a special case
|
||||
simplify button rendering
|
||||
|
||||
==============================================================================*/
|
||||
var _super = LIST_ITEM.ListItemView;
|
||||
/** @class Read only list view for either LDDAs, HDAs, or HDADCEs.
|
||||
* Roughly, any DatasetInstance (and not a raw Dataset).
|
||||
*/
|
||||
export var DatasetListItemView = _super.extend(
|
||||
/** @lends DatasetListItemView.prototype */ {
|
||||
_logNamespace: logNamespace,
|
||||
|
||||
className: `${_super.prototype.className} dataset`,
|
||||
//TODO:?? doesn't exactly match an hda's type_id
|
||||
id: function () {
|
||||
return ["dataset", this.model.get("id")].join("-");
|
||||
},
|
||||
|
||||
/** Set up: instance vars, options, and event handlers */
|
||||
initialize: function (attributes) {
|
||||
if (attributes.logger) {
|
||||
this.logger = this.model.logger = attributes.logger;
|
||||
}
|
||||
this.log(`${this}.initialize:`, attributes);
|
||||
_super.prototype.initialize.call(this, attributes);
|
||||
|
||||
/** where should pages from links be displayed? (default to new tab/window) */
|
||||
this.linkTarget = attributes.linkTarget || "_blank";
|
||||
},
|
||||
|
||||
// mount new vue component for tags
|
||||
render: function () {
|
||||
const result = _super.prototype.render.apply(this, arguments);
|
||||
this._mountVueNametags();
|
||||
return result;
|
||||
},
|
||||
|
||||
_mountVueNametags: function () {
|
||||
const container = this.$(".nametags")[0];
|
||||
const { id, model_class, tags } = this.model.attributes;
|
||||
const storeKey = `${model_class}-${id}`;
|
||||
mountNametags({ storeKey, tags }, container);
|
||||
},
|
||||
|
||||
/** event listeners */
|
||||
_setUpListeners: function () {
|
||||
_super.prototype._setUpListeners.call(this);
|
||||
var self = this;
|
||||
|
||||
// re-rendering on any model changes
|
||||
return self.listenTo(self.model, {
|
||||
change: function (model) {
|
||||
// if the model moved into the ready state and is expanded without details, fetch those details now
|
||||
if (
|
||||
self.model.changedAttributes().state &&
|
||||
self.model.inReadyState() &&
|
||||
self.expanded &&
|
||||
!self.model.hasDetails()
|
||||
) {
|
||||
// normally, will render automatically (due to fetch -> change),
|
||||
// but! setting_metadata sometimes doesn't cause any other changes besides state
|
||||
// so, not rendering causes it to seem frozen in setting_metadata state
|
||||
self.model.fetch({ silent: true }).done(() => {
|
||||
self.render();
|
||||
});
|
||||
} else {
|
||||
if (_.has(self.model.changed, "tags") && _.keys(self.model.changed).length === 2) {
|
||||
// If only the tags and update time have changed,
|
||||
// rerender specifically the titlebar region.
|
||||
// Otherwise default to the full render.
|
||||
self._mountVueNametags();
|
||||
} else {
|
||||
self.render();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// ......................................................................... expandable
|
||||
/** In this override, only get details if in the ready state, get rerunnable if in other states.
|
||||
* Note: fetch with no 'change' event triggering to prevent automatic rendering.
|
||||
*/
|
||||
_fetchModelDetails: function () {
|
||||
var view = this;
|
||||
if (view.model.inReadyState() && !view.model.hasDetails()) {
|
||||
return view.model.fetch({ silent: true });
|
||||
}
|
||||
return $.when();
|
||||
},
|
||||
|
||||
// ......................................................................... removal
|
||||
/** Remove this view's html from the DOM and remove all event listeners.
|
||||
* @param {Number or String} speed jq effect speed
|
||||
* @param {Function} callback an optional function called when removal is done (scoped to this view)
|
||||
*/
|
||||
remove: function (speed, callback) {
|
||||
var view = this;
|
||||
speed = speed || this.fxSpeed;
|
||||
this.$el.fadeOut(speed, () => {
|
||||
Backbone.View.prototype.remove.call(view);
|
||||
if (callback) {
|
||||
callback.call(view);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// ......................................................................... rendering
|
||||
/* TODO:
|
||||
dataset states are the issue primarily making dataset rendering complex
|
||||
each state should have it's own way of displaying/set of details
|
||||
often with different actions that can be applied
|
||||
throw in deleted/purged/visible and things get complicated easily
|
||||
I've considered (a couple of times) - creating a view for each state
|
||||
- but recreating the view during an update...seems wrong
|
||||
*/
|
||||
/** In this override, add the dataset state as a class for use with state-based CSS */
|
||||
_swapNewRender: function ($newRender) {
|
||||
_super.prototype._swapNewRender.call(this, $newRender);
|
||||
if (this.model.has("state")) {
|
||||
this.$el.addClass(`state-${this.model.get("state")}`);
|
||||
this.$el.attr("data-state", this.model.get("state"));
|
||||
}
|
||||
return this.$el;
|
||||
},
|
||||
|
||||
// ................................................................................ titlebar
|
||||
/** In this override, add the dataset display button. */
|
||||
_renderPrimaryActions: function () {
|
||||
// render just the display for read-only
|
||||
return [this._renderDisplayButton()];
|
||||
},
|
||||
|
||||
/** Render icon-button to display dataset data */
|
||||
_renderDisplayButton: function () {
|
||||
// don't show display if not viewable or not accessible
|
||||
var state = this.model.get("state");
|
||||
if (state === STATES.NOT_VIEWABLE || state === STATES.DISCARDED || !this.model.get("accessible")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var displayBtnData = {
|
||||
target: this.linkTarget,
|
||||
classes: "display-btn",
|
||||
};
|
||||
|
||||
// show a disabled display if the data's been purged
|
||||
if (this.model.get("purged")) {
|
||||
displayBtnData.disabled = true;
|
||||
displayBtnData.title = _l("Cannot display datasets removed from disk");
|
||||
|
||||
// disable if still uploading
|
||||
} else if (state === STATES.UPLOAD) {
|
||||
displayBtnData.disabled = true;
|
||||
displayBtnData.title = _l("This dataset must finish uploading before it can be viewed");
|
||||
|
||||
// disable if still new
|
||||
} else if (state === STATES.NEW) {
|
||||
displayBtnData.disabled = true;
|
||||
displayBtnData.title = _l("This dataset is not yet viewable");
|
||||
} else {
|
||||
displayBtnData.title = _l("View data");
|
||||
|
||||
// default link for dataset
|
||||
displayBtnData.href = this.model.urls.display;
|
||||
|
||||
// add frame manager option onclick event
|
||||
var self = this;
|
||||
displayBtnData.onclick = (ev) => {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (Galaxy.frame && Galaxy.frame.active) {
|
||||
// Add dataset to frames.
|
||||
const identifier = self.model.get("element_id") || self.model.get("id");
|
||||
const url = `${getAppRoot()}datasets/${identifier}/display/?preview=True`;
|
||||
Galaxy.frame.add({ url });
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
}
|
||||
displayBtnData.faIcon = "fa-eye";
|
||||
return faIconButton(displayBtnData);
|
||||
},
|
||||
|
||||
// ......................................................................... rendering details
|
||||
/** Render the enclosing div of the hda body and, if expanded, the html in the body
|
||||
* @returns {jQuery} rendered DOM
|
||||
*/
|
||||
_renderDetails: function () {
|
||||
//TODO: generalize to be allow different details for each state
|
||||
|
||||
// no access - render nothing but a message
|
||||
if (this.model.get("state") === STATES.NOT_VIEWABLE) {
|
||||
return $(this.templates.noAccess(this.model.toJSON(), this));
|
||||
}
|
||||
|
||||
var $details = _super.prototype._renderDetails.call(this);
|
||||
$details.find(".actions .left").empty().append(this._renderSecondaryActions());
|
||||
$details.find(".summary").html(this._renderSummary()).prepend(this._renderDetailMessages());
|
||||
$details.find(".display-applications").html(this._renderDisplayApplications());
|
||||
|
||||
this._setUpBehaviors($details);
|
||||
return $details;
|
||||
},
|
||||
|
||||
/** Defer to the appropo summary rendering fn based on state */
|
||||
_renderSummary: function () {
|
||||
var json = this.model.toJSON();
|
||||
var summaryRenderFn = this.templates.summaries[json.state];
|
||||
summaryRenderFn = summaryRenderFn || this.templates.summaries.unknown;
|
||||
return summaryRenderFn(json, this);
|
||||
},
|
||||
|
||||
/** Render messages to be displayed only when the details are shown */
|
||||
_renderDetailMessages: function () {
|
||||
var view = this;
|
||||
var $warnings = $('<div class="detail-messages"></div>');
|
||||
var json = view.model.toJSON();
|
||||
//TODO:! unordered (map)
|
||||
_.each(view.templates.detailMessages, (templateFn) => {
|
||||
$warnings.append($(templateFn(json, view)));
|
||||
});
|
||||
return $warnings;
|
||||
},
|
||||
|
||||
/** Render the external display application links */
|
||||
_renderDisplayApplications: function () {
|
||||
if (this.model.isDeletedOrPurged()) {
|
||||
return "";
|
||||
}
|
||||
// render both old and new display apps using the same template
|
||||
return [
|
||||
this.templates.displayApplications(this.model.get("display_apps"), this),
|
||||
this.templates.displayApplications(this.model.get("display_types"), this),
|
||||
].join("");
|
||||
},
|
||||
|
||||
// ......................................................................... secondary/details actions
|
||||
/** A series of links/buttons for less commonly used actions: re-run, info, etc. */
|
||||
_renderSecondaryActions: function () {
|
||||
this.debug("_renderSecondaryActions");
|
||||
switch (this.model.get("state")) {
|
||||
case STATES.NOT_VIEWABLE:
|
||||
return [];
|
||||
case STATES.OK:
|
||||
case STATES.FAILED_METADATA:
|
||||
case STATES.ERROR:
|
||||
return [
|
||||
this._renderDownloadButton(),
|
||||
this._renderClipboardButton(),
|
||||
this._renderShowParamsButton(),
|
||||
];
|
||||
}
|
||||
return [this._renderShowParamsButton()];
|
||||
},
|
||||
|
||||
/** Render icon-button to show the input and output (stdout/err) for the job that created this.
|
||||
* @returns {jQuery} rendered DOM
|
||||
*/
|
||||
_renderShowParamsButton: function () {
|
||||
return faIconButton({
|
||||
title: _l("View details"),
|
||||
classes: "params-btn",
|
||||
href: this.model.urls.show_params,
|
||||
target: this.linkTarget,
|
||||
faIcon: "fa-info-circle",
|
||||
onclick: (ev) => {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
const showDetailsUrl = `/datasets/${this.model.get("element_id") || this.model.get("id")}/details`;
|
||||
if (Galaxy.frame && Galaxy.frame.active) {
|
||||
ev.preventDefault();
|
||||
Galaxy.frame.add({
|
||||
url: showDetailsUrl,
|
||||
title: `Dataset Details of ${this.model.get("name")}`,
|
||||
});
|
||||
} else if (Galaxy.router) {
|
||||
ev.preventDefault();
|
||||
Galaxy.router.push(showDetailsUrl);
|
||||
Galaxy.trigger("activate-hda", this.model.get("element_id") || this.model.get("id"));
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
isPurged: function () {
|
||||
// don't show anything if the data's been purged
|
||||
return !!(this.model.get("purged") || !this.model.hasData());
|
||||
},
|
||||
/** Render icon-button/popupmenu to download the data (and/or the associated meta files (bai, etc.)) for this.
|
||||
* @returns {jQuery} rendered DOM
|
||||
*/
|
||||
_renderClipboardButton: function () {
|
||||
const isUnsharable =
|
||||
this.model.attributes.permissions &&
|
||||
this.model.attributes.permissions.access.length === 1 &&
|
||||
this.model.attributes.permissions.access.includes(getGalaxyInstance().user.id);
|
||||
|
||||
var urls = this.model.urls;
|
||||
if (!this.isPurged() && urls.download) {
|
||||
let title = _l("Copy link");
|
||||
let style = "";
|
||||
if (isUnsharable) {
|
||||
title = _l("Preferences restrict sharing");
|
||||
style = "opacity: 0.3";
|
||||
}
|
||||
return faIconButton({
|
||||
faIcon: "fa-chain",
|
||||
style: style,
|
||||
title: title,
|
||||
onclick: function () {
|
||||
if (!isUnsharable) {
|
||||
navigator.clipboard.writeText(`${window.location.origin}${urls.download}`).then(() => {
|
||||
Toast.info("Link is copied to your clipboard");
|
||||
});
|
||||
} else {
|
||||
Toast.warning("Dataset is not sharable");
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
/** Render icon-button/popupmenu to download the data (and/or the associated meta files (bai, etc.)) for this.
|
||||
* @returns {jQuery} rendered DOM
|
||||
*/
|
||||
_renderDownloadButton: function () {
|
||||
if (this.isPurged()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// return either: a popupmenu with links to download assoc. meta files (if there are meta files)
|
||||
// or a single download icon-button (if there are no meta files)
|
||||
if (!_.isEmpty(this.model.get("meta_files"))) {
|
||||
return this._renderMetaFileDownloadButton();
|
||||
}
|
||||
|
||||
return $(`
|
||||
<a class="download-btn icon-btn" href="${this.model.urls.download}" title="${_l("Download")}">
|
||||
<span class="fa fa-floppy-o"></span>
|
||||
</a>`);
|
||||
},
|
||||
|
||||
/** Render the download button which opens a dropdown with links to download assoc. meta files (indices, etc.) */
|
||||
_renderMetaFileDownloadButton: function () {
|
||||
var urls = this.model.urls;
|
||||
return $(`
|
||||
<div class="metafile-dropdown dropdown">
|
||||
<a class="download-btn icon-btn" href="${urls.download}" data-toggle="dropdown" title="${_l(
|
||||
"Download"
|
||||
)}">
|
||||
<span class="fa fa-floppy-o"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu" role="menu">
|
||||
<a class="dropdown-item" href="${urls.download}">
|
||||
${_l("Download dataset")}
|
||||
</a>
|
||||
${_.map(
|
||||
this.model.get("meta_files"),
|
||||
(meta_file) =>
|
||||
`<a class="dropdown-item" href="${
|
||||
urls.meta_download + meta_file.file_type
|
||||
}" data-description="download ${meta_file.file_type}">
|
||||
${_l("Download")} ${meta_file.file_type}
|
||||
</a>`
|
||||
)}
|
||||
</div>
|
||||
</div>`);
|
||||
},
|
||||
|
||||
// ......................................................................... misc
|
||||
events: _.extend(_.clone(_super.prototype.events), {
|
||||
"click .display-btn": function (ev) {
|
||||
this.trigger("display", this, ev);
|
||||
},
|
||||
"click .params-btn": function (ev) {
|
||||
this.trigger("params", this, ev);
|
||||
},
|
||||
"click .download-btn": function (ev) {
|
||||
this.trigger("download", this, ev);
|
||||
},
|
||||
}),
|
||||
|
||||
// ......................................................................... misc
|
||||
/** String representation */
|
||||
toString: function () {
|
||||
var modelString = this.model ? `${this.model}` : "(no model)";
|
||||
return `DatasetListItemView(${modelString})`;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// ............................................................................ TEMPLATES
|
||||
/** underscore templates */
|
||||
DatasetListItemView.prototype.templates = (() => {
|
||||
//TODO: move to require text! plugin
|
||||
|
||||
var warnings = _.extend({}, _super.prototype.templates.warnings, {
|
||||
failed_metadata: BASE_MVC.wrapTemplate([
|
||||
// failed metadata is rendered as a warning on an otherwise ok dataset view
|
||||
'<% if( model.state === "failed_metadata" ){ %>',
|
||||
'<div class="warningmessagesmall">',
|
||||
_l("An error occurred setting the metadata for this dataset"),
|
||||
"</div>",
|
||||
"<% } %>",
|
||||
]),
|
||||
error: BASE_MVC.wrapTemplate([
|
||||
// error during index fetch - show error on dataset
|
||||
"<% if( model.error ){ %>",
|
||||
'<div class="errormessagesmall">',
|
||||
_l("There was an error getting the data for this dataset"),
|
||||
": <%- model.error %>",
|
||||
"</div>",
|
||||
"<% } %>",
|
||||
]),
|
||||
purged: BASE_MVC.wrapTemplate([
|
||||
"<% if( model.purged ){ %>",
|
||||
'<div class="purged-msg warningmessagesmall">',
|
||||
_l("This dataset has been deleted and removed from disk"),
|
||||
"</div>",
|
||||
"<% } %>",
|
||||
]),
|
||||
deleted: BASE_MVC.wrapTemplate([
|
||||
// deleted not purged
|
||||
"<% if( model.deleted && !model.purged ){ %>",
|
||||
'<div class="deleted-msg warningmessagesmall">',
|
||||
_l("This dataset has been deleted"),
|
||||
"</div>",
|
||||
"<% } %>",
|
||||
]),
|
||||
|
||||
//NOTE: hidden warning is only needed for HDAs
|
||||
});
|
||||
|
||||
var detailsTemplate = BASE_MVC.wrapTemplate(
|
||||
[
|
||||
'<div class="details">',
|
||||
'<div class="summary"></div>',
|
||||
|
||||
'<div class="actions clear">',
|
||||
'<div class="left"></div>',
|
||||
'<div class="right"></div>',
|
||||
"</div>",
|
||||
|
||||
// do not display tags, annotation, display apps, or peek when deleted
|
||||
"<% if( !dataset.deleted && !dataset.purged ){ %>",
|
||||
'<div class="tags-display"></div>',
|
||||
'<div class="annotation-display"></div>',
|
||||
|
||||
'<div class="display-applications"></div>',
|
||||
|
||||
"<% if( dataset.peek ){ %>",
|
||||
'<pre class="dataset-peek"><%= dataset.peek %></pre>',
|
||||
"<% } %>",
|
||||
"<% } %>",
|
||||
"</div>",
|
||||
],
|
||||
"dataset"
|
||||
);
|
||||
|
||||
var noAccessTemplate = BASE_MVC.wrapTemplate(
|
||||
[
|
||||
'<div class="details">',
|
||||
'<div class="summary">',
|
||||
_l("You do not have permission to view this dataset"),
|
||||
"</div>",
|
||||
"</div>",
|
||||
],
|
||||
"dataset"
|
||||
);
|
||||
|
||||
//TODO: still toooooooooooooo complex - rework
|
||||
var summaryTemplates = {};
|
||||
summaryTemplates[STATES.OK] = summaryTemplates[STATES.FAILED_METADATA] = BASE_MVC.wrapTemplate(
|
||||
[
|
||||
"<% if( dataset.misc_blurb ){ %>",
|
||||
'<div class="blurb">',
|
||||
'<span class="value"><%- dataset.misc_blurb %></span>',
|
||||
"</div>",
|
||||
"<% } %>",
|
||||
|
||||
"<% if( dataset.file_ext ){ %>",
|
||||
'<div class="datatype">',
|
||||
'<label class="prompt">',
|
||||
_l("format"),
|
||||
"</label>",
|
||||
'<span class="value"><%- dataset.file_ext %></span>',
|
||||
"</div>",
|
||||
"<% } %>",
|
||||
|
||||
"<% if( dataset.metadata_dbkey ){ %>",
|
||||
'<div class="dbkey">',
|
||||
'<label class="prompt">',
|
||||
_l("database"),
|
||||
"</label>",
|
||||
'<span class="value">',
|
||||
"<%- dataset.metadata_dbkey %>",
|
||||
"</span>",
|
||||
"</div>",
|
||||
"<% } %>",
|
||||
|
||||
"<% if( dataset.misc_info ){ %>",
|
||||
'<div class="info">',
|
||||
'<span class="value"><%- dataset.misc_info %></span>',
|
||||
"</div>",
|
||||
"<% } %>",
|
||||
],
|
||||
"dataset"
|
||||
);
|
||||
summaryTemplates[STATES.NEW] = BASE_MVC.wrapTemplate(
|
||||
["<div>", _l("This is a new dataset and not all of its data are available yet"), "</div>"],
|
||||
"dataset"
|
||||
);
|
||||
summaryTemplates[STATES.NOT_VIEWABLE] = BASE_MVC.wrapTemplate(
|
||||
["<div>", _l("You do not have permission to view this dataset"), "</div>"],
|
||||
"dataset"
|
||||
);
|
||||
summaryTemplates[STATES.DISCARDED] = BASE_MVC.wrapTemplate(
|
||||
[
|
||||
"<div>",
|
||||
_l(
|
||||
"This dataset is discarded - the job creating it may have been cancelled or it may have been imported with file data"
|
||||
),
|
||||
"</div>",
|
||||
],
|
||||
"dataset"
|
||||
);
|
||||
summaryTemplates[STATES.DEFERRED] = BASE_MVC.wrapTemplate(
|
||||
[
|
||||
"<div>",
|
||||
_l("This dataset is remote, has not be ingested by Galaxy, and full metadata may not be available"),
|
||||
"</div>",
|
||||
],
|
||||
"dataset"
|
||||
);
|
||||
summaryTemplates[STATES.QUEUED] = BASE_MVC.wrapTemplate(
|
||||
["<div>", _l("This job is waiting to run"), "</div>"],
|
||||
"dataset"
|
||||
);
|
||||
summaryTemplates[STATES.RUNNING] = BASE_MVC.wrapTemplate(
|
||||
["<div>", _l("This job is currently running"), "</div>"],
|
||||
"dataset"
|
||||
);
|
||||
summaryTemplates[STATES.UPLOAD] = BASE_MVC.wrapTemplate(
|
||||
["<div>", _l("This dataset is currently uploading"), "</div>"],
|
||||
"dataset"
|
||||
);
|
||||
summaryTemplates[STATES.SETTING_METADATA] = BASE_MVC.wrapTemplate(
|
||||
["<div>", _l("Metadata is being auto-detected"), "</div>"],
|
||||
"dataset"
|
||||
);
|
||||
summaryTemplates[STATES.PAUSED] = BASE_MVC.wrapTemplate(
|
||||
[
|
||||
"<div>",
|
||||
_l('This job is paused. Use the "Resume Paused Jobs" in the history menu to resume'),
|
||||
"</div>",
|
||||
'<div class="info"><%- dataset.misc_info %></div>',
|
||||
],
|
||||
"dataset"
|
||||
);
|
||||
summaryTemplates[STATES.ERROR] = BASE_MVC.wrapTemplate(
|
||||
[
|
||||
"<% if( !dataset.purged ){ %>",
|
||||
"<div><%- dataset.misc_blurb %></div>",
|
||||
"<% } %>",
|
||||
'<span class="help-text">',
|
||||
_l("An error occurred with this dataset"),
|
||||
":</span>",
|
||||
'<div class="job-error-text"><%- dataset.misc_info %></div>',
|
||||
],
|
||||
"dataset"
|
||||
);
|
||||
summaryTemplates[STATES.EMPTY] = BASE_MVC.wrapTemplate(
|
||||
["<div>", _l("No data"), ": <i><%- dataset.misc_blurb %></i></div>"],
|
||||
"dataset"
|
||||
);
|
||||
summaryTemplates.unknown = BASE_MVC.wrapTemplate(
|
||||
['<div>Error: unknown dataset state: "<%- dataset.state %>"</div>'],
|
||||
"dataset"
|
||||
);
|
||||
|
||||
// messages to be displayed only within the details section ('below the fold')
|
||||
var detailMessageTemplates = {
|
||||
resubmitted: BASE_MVC.wrapTemplate([
|
||||
// deleted not purged
|
||||
"<% if( model.resubmitted ){ %>",
|
||||
'<div class="resubmitted-msg alert alert-info">',
|
||||
_l("The job creating this dataset has been resubmitted"),
|
||||
"</div>",
|
||||
"<% } %>",
|
||||
]),
|
||||
};
|
||||
|
||||
// this is applied to both old and new style display apps
|
||||
var displayApplicationsTemplate = BASE_MVC.wrapTemplate(
|
||||
[
|
||||
"<% _.each( apps, function( app ){ %>",
|
||||
'<div class="display-application">',
|
||||
'<span class="display-application-location"><%- app.label %></span> ',
|
||||
'<span class="display-application-links">',
|
||||
"<% _.each( app.links, function( link ){ %>",
|
||||
'<a target="<%- link.target %>" href="<%- link.href %>">',
|
||||
"<% print( _l( link.text ) ); %>",
|
||||
"</a> ",
|
||||
"<% }); %>",
|
||||
"</span>",
|
||||
"</div>",
|
||||
"<% }); %>",
|
||||
],
|
||||
"apps"
|
||||
);
|
||||
|
||||
return _.extend({}, _super.prototype.templates, {
|
||||
warnings: warnings,
|
||||
details: detailsTemplate,
|
||||
noAccess: noAccessTemplate,
|
||||
summaries: summaryTemplates,
|
||||
detailMessages: detailMessageTemplates,
|
||||
displayApplications: displayApplicationsTemplate,
|
||||
});
|
||||
})();
|
||||
|
||||
// ============================================================================
|
||||
export default {
|
||||
DatasetListItemView: DatasetListItemView,
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import LIST_VIEW from "mvc/list/list-view";
|
||||
import DATASET_LI from "mvc/dataset/dataset-li";
|
||||
import _l from "utils/localization";
|
||||
|
||||
var logNamespace = "dataset";
|
||||
/* =============================================================================
|
||||
TODO:
|
||||
|
||||
============================================================================= */
|
||||
var _super = LIST_VIEW.ListPanel;
|
||||
/** @class non-editable, read-only View/Controller for a list of datasets.
|
||||
*/
|
||||
var DatasetList = _super.extend(
|
||||
/** @lends DatasetList.prototype */ {
|
||||
_logNamespace: logNamespace,
|
||||
|
||||
/** class to use for constructing the sub-views */
|
||||
viewClass: DATASET_LI.DatasetListItemView,
|
||||
className: `${_super.prototype.className} dataset-list`,
|
||||
|
||||
/** string to no hdas match the search terms */
|
||||
noneFoundMsg: _l("No matching datasets found"),
|
||||
|
||||
// ......................................................................... SET UP
|
||||
/** Set up the view, set up storage, bind listeners to HistoryContents events
|
||||
* @param {Object} attributes optional settings for the panel
|
||||
*/
|
||||
initialize: function (attributes) {
|
||||
_super.prototype.initialize.call(this, attributes);
|
||||
},
|
||||
|
||||
/** Return a string rep of the history */
|
||||
toString: function () {
|
||||
return `DatasetList(${this.collection})`;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
export default {
|
||||
DatasetList: DatasetList,
|
||||
};
|
||||
@@ -1,390 +0,0 @@
|
||||
import _ from "underscore";
|
||||
import jQuery from "jquery";
|
||||
import Backbone from "backbone";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import STATES from "mvc/dataset/states";
|
||||
import BASE_MVC from "mvc/base-mvc";
|
||||
import _l from "utils/localization";
|
||||
import TAB_UPDATES from "mvc/user/tab-updates";
|
||||
|
||||
var logNamespace = "dataset";
|
||||
var hiddenupdates = 1;
|
||||
//==============================================================================
|
||||
var searchableMixin = BASE_MVC.SearchableModelMixin;
|
||||
/** @class base model for any DatasetAssociation (HDAs, LDDAs, DatasetCollectionDAs).
|
||||
* No knowledge of what type (HDA/LDDA/DCDA) should be needed here.
|
||||
* The DA's are made searchable (by attribute) by mixing in SearchableModelMixin.
|
||||
*/
|
||||
var DatasetAssociation = Backbone.Model.extend(BASE_MVC.LoggableMixin).extend(
|
||||
BASE_MVC.mixin(
|
||||
searchableMixin,
|
||||
/** @lends DatasetAssociation.prototype */ {
|
||||
_logNamespace: logNamespace,
|
||||
|
||||
/** default attributes for a model */
|
||||
defaults: {
|
||||
state: STATES.NEW,
|
||||
deleted: false,
|
||||
purged: false,
|
||||
name: "(unnamed dataset)",
|
||||
accessible: true,
|
||||
// sniffed datatype (sam, tabular, bed, etc.)
|
||||
data_type: "",
|
||||
file_ext: "",
|
||||
file_size: 0,
|
||||
|
||||
// array of associated file types (eg. [ 'bam_index', ... ])
|
||||
meta_files: [],
|
||||
|
||||
misc_blurb: "",
|
||||
misc_info: "",
|
||||
|
||||
tags: [],
|
||||
// do NOT default on annotation, as this default is valid and will be passed on 'save'
|
||||
// which is incorrect behavior when the model is only partially fetched (annos are not passed in summary data)
|
||||
//annotation : ''
|
||||
},
|
||||
|
||||
/** instance vars and listeners */
|
||||
initialize: function (attributes, options) {
|
||||
this.debug(`${this}(Dataset).initialize`, attributes, options);
|
||||
|
||||
//!! this state is not in trans.app.model.Dataset.states - set it here -
|
||||
if (!this.get("accessible")) {
|
||||
this.set("state", STATES.NOT_VIEWABLE);
|
||||
}
|
||||
|
||||
/** Datasets rely/use some web controllers - have the model generate those URLs on startup */
|
||||
this.urls = this._generateUrls();
|
||||
|
||||
this._setUpListeners();
|
||||
},
|
||||
|
||||
_getDatasetId: function () {
|
||||
return this.get("id");
|
||||
},
|
||||
|
||||
/** returns misc. web urls for rendering things like re-run, display, etc. */
|
||||
_generateUrls: function () {
|
||||
const id = this._getDatasetId();
|
||||
if (!id) {
|
||||
return {};
|
||||
}
|
||||
var urls = {
|
||||
purge: `datasets/${id}/purge_async`,
|
||||
display: `datasets/${id}/display/?preview=True`,
|
||||
edit: `datasets/edit?dataset_id=${id}`,
|
||||
download: `api/datasets/${id}/display${this._downloadQueryParameters()}`,
|
||||
report_error: `datasets/error?dataset_id=${id}`,
|
||||
rerun: `tool_runner/rerun?id=${id}`,
|
||||
show_params: `datasets/${id}/details`,
|
||||
visualization: "visualization",
|
||||
meta_download: `dataset/get_metadata_file?hda_id=${id}&metadata_name=`,
|
||||
};
|
||||
_.each(urls, (value, key) => {
|
||||
urls[key] = getAppRoot() + value;
|
||||
});
|
||||
this.urls = urls;
|
||||
return urls;
|
||||
},
|
||||
|
||||
_downloadQueryParameters: function () {
|
||||
return `?to_ext=${this.get("file_ext")}`;
|
||||
},
|
||||
|
||||
/** set up any event listeners
|
||||
* event: state:ready fired when this DA moves into/is already in a ready state
|
||||
*/
|
||||
_setUpListeners: function () {
|
||||
// if the state has changed and the new state is a ready state, fire an event
|
||||
this.on("change:state", function (currModel, newState) {
|
||||
this.log(`${this} has changed state:`, currModel, newState);
|
||||
if (this.inReadyState()) {
|
||||
this.trigger("state:ready", currModel, newState, this.previous("state"));
|
||||
if (newState != "discarded") {
|
||||
if (newState === "ok") {
|
||||
// If Notifications are supported, send one.
|
||||
if (window.Notification) {
|
||||
new Notification(`Job complete: ${this.get("name")}`, {
|
||||
icon: "static/favicon.ico",
|
||||
});
|
||||
}
|
||||
if (TAB_UPDATES.is_hidden()) {
|
||||
TAB_UPDATES.hidden_count(hiddenupdates);
|
||||
hiddenupdates++;
|
||||
}
|
||||
} else if (newState == "error") {
|
||||
// If Notifications are supported, send one.
|
||||
if (window.Notification) {
|
||||
new Notification(`Job failure: ${this.get("name")}`, {
|
||||
icon: "static/erricon.ico",
|
||||
});
|
||||
if (TAB_UPDATES.is_hidden() && Notification.permission == "granted") {
|
||||
TAB_UPDATES.change_favicon("static/erricon.ico");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// the download url (currently) relies on having a correct file extension
|
||||
this.on("change:id change:file_ext", function (currModel) {
|
||||
this._generateUrls();
|
||||
});
|
||||
},
|
||||
|
||||
// ........................................................................ common queries
|
||||
/** override to add urls */
|
||||
toJSON: function () {
|
||||
var json = Backbone.Model.prototype.toJSON.call(this);
|
||||
//console.warn( 'returning json?' );
|
||||
//return json;
|
||||
return _.extend(json, {
|
||||
urls: this.urls,
|
||||
});
|
||||
},
|
||||
|
||||
/** Is this dataset deleted or purged? */
|
||||
isDeletedOrPurged: function () {
|
||||
return this.get("deleted") || this.get("purged");
|
||||
},
|
||||
|
||||
/** Is this dataset in a 'ready' state; where 'Ready' states are states where no
|
||||
* processing (for the ds) is left to do on the server.
|
||||
*/
|
||||
inReadyState: function () {
|
||||
var ready = _.contains(STATES.READY_STATES, this.get("state"));
|
||||
return this.isDeletedOrPurged() || ready;
|
||||
},
|
||||
|
||||
/** Does this model already contain detailed data (as opposed to just summary level data)? */
|
||||
hasDetails: function () {
|
||||
// if it's inaccessible assume it has everything it needs
|
||||
if (!this.get("accessible")) {
|
||||
return true;
|
||||
}
|
||||
return this.has("annotation");
|
||||
},
|
||||
|
||||
/** Convenience function to match dataset.has_data. */
|
||||
hasData: function () {
|
||||
return this.get("file_size") > 0;
|
||||
},
|
||||
|
||||
// ........................................................................ ajax
|
||||
fetch: function (options) {
|
||||
var dataset = this;
|
||||
return Backbone.Model.prototype.fetch.call(this, options).always(() => {
|
||||
dataset._generateUrls();
|
||||
});
|
||||
},
|
||||
|
||||
/** override to use actual Dates objects for create/update times */
|
||||
parse: function (response, options) {
|
||||
var parsed = Backbone.Model.prototype.parse.call(this, response, options);
|
||||
if (parsed.create_time) {
|
||||
parsed.create_time = new Date(parsed.create_time);
|
||||
}
|
||||
if (parsed.update_time) {
|
||||
parsed.update_time = new Date(parsed.update_time);
|
||||
}
|
||||
return parsed;
|
||||
},
|
||||
|
||||
/** override to wait by default */
|
||||
save: function (attrs, options) {
|
||||
options = options || {};
|
||||
options.wait = _.isUndefined(options.wait) ? true : options.wait;
|
||||
return Backbone.Model.prototype.save.call(this, attrs, options);
|
||||
},
|
||||
|
||||
//NOTE: subclasses of DA's will need to implement url and urlRoot in order to have these work properly
|
||||
/** save this dataset, _Mark_ing it as deleted (just a flag) */
|
||||
delete: function (options) {
|
||||
if (this.get("deleted")) {
|
||||
return jQuery.when();
|
||||
}
|
||||
return this.save({ deleted: true }, options);
|
||||
},
|
||||
/** save this dataset, _Mark_ing it as undeleted */
|
||||
undelete: function (options) {
|
||||
if (!this.get("deleted") || this.get("purged")) {
|
||||
return jQuery.when();
|
||||
}
|
||||
return this.save({ deleted: false }, options);
|
||||
},
|
||||
|
||||
/** remove the file behind this dataset from the filesystem (if permitted) */
|
||||
purge: function _purge(options) {
|
||||
//TODO: use, override model.destroy, HDA.delete({ purge: true })
|
||||
if (this.get("purged")) {
|
||||
return jQuery.when();
|
||||
}
|
||||
options = options || {};
|
||||
options.url = this.urls.purge;
|
||||
|
||||
//TODO: ideally this would be a DELETE call to the api
|
||||
// using purge async for now
|
||||
var hda = this;
|
||||
|
||||
var xhr = jQuery.ajax(options);
|
||||
xhr.done((message, status, responseObj) => {
|
||||
hda.set({ deleted: true, purged: true });
|
||||
});
|
||||
xhr.fail((xhr, status, message) => {
|
||||
// Exception messages are hidden within error page including: '...not allowed in this Galaxy instance.'
|
||||
// unbury and re-add to xhr
|
||||
var error = _l("Unable to purge dataset");
|
||||
var messageBuriedInUnfortunatelyFormattedError =
|
||||
"Removal of datasets by users " + "is not allowed in this Galaxy instance";
|
||||
if (xhr.responseJSON && xhr.responseJSON.error) {
|
||||
error = xhr.responseJSON.error;
|
||||
} else if (xhr.responseText.indexOf(messageBuriedInUnfortunatelyFormattedError) !== -1) {
|
||||
error = messageBuriedInUnfortunatelyFormattedError;
|
||||
}
|
||||
xhr.responseText = error;
|
||||
hda.trigger("error", hda, xhr, options, _l(error), {
|
||||
error: error,
|
||||
});
|
||||
});
|
||||
return xhr;
|
||||
},
|
||||
|
||||
// ........................................................................ searching
|
||||
/** what attributes of an HDA will be used in a text search */
|
||||
searchAttributes: ["name", "file_ext", "genome_build", "misc_blurb", "misc_info", "annotation", "tags"],
|
||||
|
||||
/** our attr keys don't often match the labels we display to the user - so, when using
|
||||
* attribute specifiers ('name="bler"') in a term, allow passing in aliases for the
|
||||
* following attr keys.
|
||||
*/
|
||||
searchAliases: {
|
||||
title: "name",
|
||||
format: "file_ext",
|
||||
database: "genome_build",
|
||||
blurb: "misc_blurb",
|
||||
description: "misc_blurb",
|
||||
info: "misc_info",
|
||||
tag: "tags",
|
||||
},
|
||||
|
||||
// ........................................................................ misc
|
||||
/** String representation */
|
||||
toString: function () {
|
||||
var nameAndId = this.get("id") || "";
|
||||
if (this.get("name")) {
|
||||
nameAndId = `"${this.get("name")}",${nameAndId}`;
|
||||
}
|
||||
return `Dataset(${nameAndId})`;
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
//==============================================================================
|
||||
/** @class Backbone collection for dataset associations.
|
||||
*/
|
||||
var DatasetAssociationCollection = Backbone.Collection.extend(BASE_MVC.LoggableMixin).extend(
|
||||
/** @lends HistoryContents.prototype */ {
|
||||
_logNamespace: logNamespace,
|
||||
|
||||
model: DatasetAssociation,
|
||||
|
||||
/** root api url */
|
||||
urlRoot: `${getAppRoot()}api/datasets`,
|
||||
|
||||
/** url fn */
|
||||
url: function () {
|
||||
return this.urlRoot;
|
||||
},
|
||||
|
||||
// ........................................................................ common queries
|
||||
/** Get the ids of every item in this collection
|
||||
* @returns array of encoded ids
|
||||
*/
|
||||
ids: function () {
|
||||
return this.map((item) => item.get("id"));
|
||||
},
|
||||
|
||||
/** Get contents that are not ready
|
||||
* @returns array of content models
|
||||
*/
|
||||
notReady: function () {
|
||||
return this.filter((content) => !content.inReadyState());
|
||||
},
|
||||
|
||||
/** return true if any datasets don't have details */
|
||||
haveDetails: function () {
|
||||
return this.all((dataset) => dataset.hasDetails());
|
||||
},
|
||||
|
||||
// ........................................................................ ajax
|
||||
/** using a queue, perform ajaxFn on each of the models in this collection */
|
||||
ajaxQueue: function (ajaxFn, options) {
|
||||
var deferred = jQuery.Deferred();
|
||||
var startingLength = this.length;
|
||||
var responses = [];
|
||||
|
||||
if (!startingLength) {
|
||||
deferred.resolve([]);
|
||||
return deferred;
|
||||
}
|
||||
|
||||
// use reverse order (stylistic choice)
|
||||
var ajaxFns = this.chain()
|
||||
.reverse()
|
||||
.map((dataset, i) => () => {
|
||||
var xhr = ajaxFn.call(dataset, options);
|
||||
// if successful, notify using the deferred to allow tracking progress
|
||||
xhr.done((response) => {
|
||||
deferred.notify({
|
||||
curr: i,
|
||||
total: startingLength,
|
||||
response: response,
|
||||
model: dataset,
|
||||
});
|
||||
});
|
||||
// (regardless of previous error or success) if not last ajax call, shift and call the next
|
||||
// if last fn, resolve deferred
|
||||
xhr.always((response) => {
|
||||
responses.push(response);
|
||||
if (ajaxFns.length) {
|
||||
ajaxFns.shift()();
|
||||
} else {
|
||||
deferred.resolve(responses);
|
||||
}
|
||||
});
|
||||
})
|
||||
.value();
|
||||
// start the queue
|
||||
ajaxFns.shift()();
|
||||
|
||||
return deferred;
|
||||
},
|
||||
|
||||
// ........................................................................ sorting/filtering
|
||||
/** return a new collection of datasets whose attributes contain the substring matchesWhat */
|
||||
matches: function (matchesWhat) {
|
||||
return this.filter((dataset) => dataset.matches(matchesWhat));
|
||||
},
|
||||
|
||||
/** String representation. */
|
||||
toString: function () {
|
||||
return ["DatasetAssociationCollection(", this.length, ")"].join("");
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
window.addEventListener("focus", function () {
|
||||
const testing = document.getElementById("tabicon");
|
||||
if (testing && testing.href.search("erricon") != -1) {
|
||||
TAB_UPDATES.change_favicon("static/favicon.ico");
|
||||
}
|
||||
document.title = "Galaxy";
|
||||
hiddenupdates = 1;
|
||||
});
|
||||
//==============================================================================
|
||||
export default {
|
||||
DatasetAssociation: DatasetAssociation,
|
||||
DatasetAssociationCollection: DatasetAssociationCollection,
|
||||
};
|
||||
Reference in New Issue
Block a user