From 7da1c86af9b7a633fb6cc2cda8ca3580f62166ab Mon Sep 17 00:00:00 2001 From: guerler Date: Wed, 21 Sep 2022 23:08:35 -0400 Subject: [PATCH] Remove legacy collection and dataset handling --- .../src/mvc/collection/collection-li-edit.js | 115 ---- client/src/mvc/collection/collection-li.js | 277 -------- client/src/mvc/collection/collection-model.js | 396 ----------- .../mvc/collection/collection-view-edit.js | 114 ---- client/src/mvc/collection/collection-view.js | 235 ------- client/src/mvc/dataset/dataset-choice.js | 434 ------------ client/src/mvc/dataset/dataset-li-edit.js | 472 ------------- client/src/mvc/dataset/dataset-li.js | 639 ------------------ client/src/mvc/dataset/dataset-list.js | 42 -- client/src/mvc/dataset/dataset-model.js | 390 ----------- 10 files changed, 3114 deletions(-) delete mode 100644 client/src/mvc/collection/collection-li-edit.js delete mode 100644 client/src/mvc/collection/collection-li.js delete mode 100644 client/src/mvc/collection/collection-model.js delete mode 100644 client/src/mvc/collection/collection-view-edit.js delete mode 100644 client/src/mvc/collection/collection-view.js delete mode 100644 client/src/mvc/dataset/dataset-choice.js delete mode 100644 client/src/mvc/dataset/dataset-li-edit.js delete mode 100644 client/src/mvc/dataset/dataset-li.js delete mode 100644 client/src/mvc/dataset/dataset-list.js delete mode 100644 client/src/mvc/dataset/dataset-model.js diff --git a/client/src/mvc/collection/collection-li-edit.js b/client/src/mvc/collection/collection-li-edit.js deleted file mode 100644 index 2d6e286efe5..00000000000 --- a/client/src/mvc/collection/collection-li-edit.js +++ /dev/null @@ -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, -}; diff --git a/client/src/mvc/collection/collection-li.js b/client/src/mvc/collection/collection-li.js deleted file mode 100644 index 148588bfa19..00000000000 --- a/client/src/mvc/collection/collection-li.js +++ /dev/null @@ -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 ){ %>", - '
', - _l("There was an error getting the data for this collection"), - ": <%- model.error %>", - "
", - "<% } %>", - ]), - purged: BASE_MVC.wrapTemplate([ - "<% if( model.purged ){ %>", - '
', - _l("This collection has been deleted and removed from disk"), - "
", - "<% } %>", - ]), - deleted: BASE_MVC.wrapTemplate([ - // deleted not purged - "<% if( model.deleted && !model.purged ){ %>", - '
', - _l("This collection has been deleted"), - "
", - "<% } %>", - ]), - }); - - // use element identifier - var titleBarTemplate = BASE_MVC.wrapTemplate( - [ - '
', - '
', - '<%- collection.element_identifier || collection.name %>', - "
", - '
', - "
", - ], - "collection" - ); - - // use element identifier - var subtitleTemplate = BASE_MVC.wrapTemplate( - [ - '
', - '<% 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"), - "<% } %>", - "
", - ], - "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( - [ - '
', - '
', - '<%- element.element_identifier %>', - "
", - '
', - "
", - ], - "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( - [ - '
', - '', - '
', - '<%- element.element_identifier %>', - "
", - "
", - ], - "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, -}; diff --git a/client/src/mvc/collection/collection-model.js b/client/src/mvc/collection/collection-model.js deleted file mode 100644 index 8c70bf2ce79..00000000000 --- a/client/src/mvc/collection/collection-model.js +++ /dev/null @@ -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, -}; diff --git a/client/src/mvc/collection/collection-view-edit.js b/client/src/mvc/collection/collection-view-edit.js deleted file mode 100644 index b07e77ec503..00000000000 --- a/client/src/mvc/collection/collection-view-edit.js +++ /dev/null @@ -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, -}; diff --git a/client/src/mvc/collection/collection-view.js b/client/src/mvc/collection/collection-view.js deleted file mode 100644 index 13b10d5e0c1..00000000000 --- a/client/src/mvc/collection/collection-view.js +++ /dev/null @@ -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(`
${error}
`); - 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(`
${warning}
`); - } - }, - - // ------------------------------------------------------------------------ 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 ` -
- -
-
${_.escape(collection.name) || _.escape(collection.element_identifier)}
-
- ${subtitle} -
-
-
-
-
-
- - - -
-
`; - }; - - 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, -}; diff --git a/client/src/mvc/dataset/dataset-choice.js b/client/src/mvc/dataset/dataset-choice.js deleted file mode 100644 index 387e4ff0f78..00000000000 --- a/client/src/mvc/dataset/dataset-choice.js +++ /dev/null @@ -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) - * // [{ , , ... }] - * // ... 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: ['
'].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( - [""].join("") - )(json); - }, - - /** return jQ DOM for the selected dataset (only one) */ - _renderSelected: function (json) { - if (json.selected.length) { - //TODO: break out? - return $( - _.template( - [ - '
', - '<%- selected.hid %>: <%- selected.name %>', - '', - "<%- selected.misc_blurb %>", - "", - `${_l("format")}: `, - "<%- selected.file_ext %>", - "<%- selected.misc_info %>", - "", - "
", - ].join(""), - { variable: "selected" } - )(json.selected[0]) - ); - } - return $(['(', _l("click to select a dataset"), ")"].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( - [ - '', - "<% if( json.showHeaders ){ %>", - "", - "<% _.map( json.cells, function( val, key ){ %>", - "", - "<% }); %>", - "", - "<% } %>", - "", - "<% _.map( json.selected, function( selected ){ %>", - "", - "<% _.map( json.cells, function( val, key ){ %>", - '', - "<% }) %>", - "", - "<% }); %>", - "", - "
<%- val %>
<%- selected[ key ] %>
", - ].join(""), - { variable: "json" } - )(json) - ); - } - return $(['(', _l("click to select a dataset"), ")"].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, -}; diff --git a/client/src/mvc/dataset/dataset-li-edit.js b/client/src/mvc/dataset/dataset-li-edit.js deleted file mode 100644 index 7931bddc5da..00000000000 --- a/client/src/mvc/dataset/dataset-li-edit.js +++ /dev/null @@ -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 = `
`; - if (data.name && data.help) { - helpString += `Tool help for ${data.name}
`; - helpString += data.help; - } else { - helpString += "Tool help is unavailable for this dataset.
"; - } - helpString += "
"; - 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 = $('?') - .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" ){ %>', - '", - "<% } %>", - ], - "dataset" - ), - - deleted: BASE_MVC.wrapTemplate( - [ - // in this override, provide links to undelete or purge the dataset - "<% if( dataset.deleted && !dataset.purged ){ %>", - // deleted not purged - '
', - _l("This dataset has been deleted"), - '
', - _l("Undelete it"), - "", - "<% if( view.purgeAllowed ){ %>", - '
', - _l("Permanently remove it from disk"), - "", - "<% } %>", - "
", - "<% } %>", - ], - "dataset" - ), - }); - return _.extend({}, _super.prototype.templates, { - warnings: warnings, - }); -})(); - -//============================================================================== -export default { - DatasetListItemEdit: DatasetListItemEdit, -}; diff --git a/client/src/mvc/dataset/dataset-li.js b/client/src/mvc/dataset/dataset-li.js deleted file mode 100644 index 012fcf2441d..00000000000 --- a/client/src/mvc/dataset/dataset-li.js +++ /dev/null @@ -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 = $('
'); - 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 $(` - - - `); - }, - - /** 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 $(` - `); - }, - - // ......................................................................... 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" ){ %>', - '
', - _l("An error occurred setting the metadata for this dataset"), - "
", - "<% } %>", - ]), - error: BASE_MVC.wrapTemplate([ - // error during index fetch - show error on dataset - "<% if( model.error ){ %>", - '
', - _l("There was an error getting the data for this dataset"), - ": <%- model.error %>", - "
", - "<% } %>", - ]), - purged: BASE_MVC.wrapTemplate([ - "<% if( model.purged ){ %>", - '
', - _l("This dataset has been deleted and removed from disk"), - "
", - "<% } %>", - ]), - deleted: BASE_MVC.wrapTemplate([ - // deleted not purged - "<% if( model.deleted && !model.purged ){ %>", - '
', - _l("This dataset has been deleted"), - "
", - "<% } %>", - ]), - - //NOTE: hidden warning is only needed for HDAs - }); - - var detailsTemplate = BASE_MVC.wrapTemplate( - [ - '
', - '
', - - '
', - '
', - '
', - "
", - - // do not display tags, annotation, display apps, or peek when deleted - "<% if( !dataset.deleted && !dataset.purged ){ %>", - '
', - '
', - - '
', - - "<% if( dataset.peek ){ %>", - '
<%= dataset.peek %>
', - "<% } %>", - "<% } %>", - "
", - ], - "dataset" - ); - - var noAccessTemplate = BASE_MVC.wrapTemplate( - [ - '
', - '
', - _l("You do not have permission to view this dataset"), - "
", - "
", - ], - "dataset" - ); - - //TODO: still toooooooooooooo complex - rework - var summaryTemplates = {}; - summaryTemplates[STATES.OK] = summaryTemplates[STATES.FAILED_METADATA] = BASE_MVC.wrapTemplate( - [ - "<% if( dataset.misc_blurb ){ %>", - '
', - '<%- dataset.misc_blurb %>', - "
", - "<% } %>", - - "<% if( dataset.file_ext ){ %>", - '
', - '", - '<%- dataset.file_ext %>', - "
", - "<% } %>", - - "<% if( dataset.metadata_dbkey ){ %>", - '
', - '", - '', - "<%- dataset.metadata_dbkey %>", - "", - "
", - "<% } %>", - - "<% if( dataset.misc_info ){ %>", - '
', - '<%- dataset.misc_info %>', - "
", - "<% } %>", - ], - "dataset" - ); - summaryTemplates[STATES.NEW] = BASE_MVC.wrapTemplate( - ["
", _l("This is a new dataset and not all of its data are available yet"), "
"], - "dataset" - ); - summaryTemplates[STATES.NOT_VIEWABLE] = BASE_MVC.wrapTemplate( - ["
", _l("You do not have permission to view this dataset"), "
"], - "dataset" - ); - summaryTemplates[STATES.DISCARDED] = BASE_MVC.wrapTemplate( - [ - "
", - _l( - "This dataset is discarded - the job creating it may have been cancelled or it may have been imported with file data" - ), - "
", - ], - "dataset" - ); - summaryTemplates[STATES.DEFERRED] = BASE_MVC.wrapTemplate( - [ - "
", - _l("This dataset is remote, has not be ingested by Galaxy, and full metadata may not be available"), - "
", - ], - "dataset" - ); - summaryTemplates[STATES.QUEUED] = BASE_MVC.wrapTemplate( - ["
", _l("This job is waiting to run"), "
"], - "dataset" - ); - summaryTemplates[STATES.RUNNING] = BASE_MVC.wrapTemplate( - ["
", _l("This job is currently running"), "
"], - "dataset" - ); - summaryTemplates[STATES.UPLOAD] = BASE_MVC.wrapTemplate( - ["
", _l("This dataset is currently uploading"), "
"], - "dataset" - ); - summaryTemplates[STATES.SETTING_METADATA] = BASE_MVC.wrapTemplate( - ["
", _l("Metadata is being auto-detected"), "
"], - "dataset" - ); - summaryTemplates[STATES.PAUSED] = BASE_MVC.wrapTemplate( - [ - "
", - _l('This job is paused. Use the "Resume Paused Jobs" in the history menu to resume'), - "
", - '
<%- dataset.misc_info %>
', - ], - "dataset" - ); - summaryTemplates[STATES.ERROR] = BASE_MVC.wrapTemplate( - [ - "<% if( !dataset.purged ){ %>", - "
<%- dataset.misc_blurb %>
", - "<% } %>", - '', - _l("An error occurred with this dataset"), - ":", - '
<%- dataset.misc_info %>
', - ], - "dataset" - ); - summaryTemplates[STATES.EMPTY] = BASE_MVC.wrapTemplate( - ["
", _l("No data"), ": <%- dataset.misc_blurb %>
"], - "dataset" - ); - summaryTemplates.unknown = BASE_MVC.wrapTemplate( - ['
Error: unknown dataset state: "<%- dataset.state %>"
'], - "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 ){ %>", - '
', - _l("The job creating this dataset has been resubmitted"), - "
", - "<% } %>", - ]), - }; - - // this is applied to both old and new style display apps - var displayApplicationsTemplate = BASE_MVC.wrapTemplate( - [ - "<% _.each( apps, function( app ){ %>", - '
', - '<%- app.label %> ', - '', - "<% _.each( app.links, function( link ){ %>", - '', - "<% print( _l( link.text ) ); %>", - " ", - "<% }); %>", - "", - "
", - "<% }); %>", - ], - "apps" - ); - - return _.extend({}, _super.prototype.templates, { - warnings: warnings, - details: detailsTemplate, - noAccess: noAccessTemplate, - summaries: summaryTemplates, - detailMessages: detailMessageTemplates, - displayApplications: displayApplicationsTemplate, - }); -})(); - -// ============================================================================ -export default { - DatasetListItemView: DatasetListItemView, -}; diff --git a/client/src/mvc/dataset/dataset-list.js b/client/src/mvc/dataset/dataset-list.js deleted file mode 100644 index 23a77cfcfde..00000000000 --- a/client/src/mvc/dataset/dataset-list.js +++ /dev/null @@ -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, -}; diff --git a/client/src/mvc/dataset/dataset-model.js b/client/src/mvc/dataset/dataset-model.js deleted file mode 100644 index e7f7a9d132d..00000000000 --- a/client/src/mvc/dataset/dataset-model.js +++ /dev/null @@ -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, -};