From 89141be519948cdd36112d97b9bebe90c3943094 Mon Sep 17 00:00:00 2001 From: Sergi Blanco-Cuaresma Date: Tue, 6 Nov 2018 11:14:08 -0500 Subject: [PATCH] Revert "Changes for new ORCID microservice table, part 2" --- src/js/components/api_targets.js | 1 - src/js/mixins/papers_utils.js | 2 +- src/js/modules/orcid/bio.js | 44 ----- src/js/modules/orcid/extension.js | 37 +++- src/js/modules/orcid/orcid_api.js | 162 ++++++++++++++---- src/js/modules/orcid/profile.js | 27 ++- src/js/modules/orcid/widget/widget.js | 23 ++- src/js/modules/orcid/work.js | 75 ++++++-- src/js/widgets/library_list/widget.js | 2 +- src/js/widgets/list_of_things/widget.js | 4 +- src/js/widgets/navbar/widget.js | 21 +-- src/js/widgets/preferences/widget.js | 2 +- src/js/widgets/results/widget.js | 2 +- .../widgets/sort/components/sort-app.jsx.js | 5 + test/mocha/js/modules/orcid/helpers.js | 5 +- test/mocha/js/modules/orcid/orcid_api.spec.js | 114 +++++++++++- .../js/modules/orcid/orcid_widget.spec.js | 15 +- test/mocha/js/widgets/navbar_widget.spec.js | 2 +- .../js/widgets/preferences_widget.spec.js | 4 +- 19 files changed, 393 insertions(+), 154 deletions(-) delete mode 100644 src/js/modules/orcid/bio.js diff --git a/src/js/components/api_targets.js b/src/js/components/api_targets.js index ab6a4657a..56e704332 100644 --- a/src/js/components/api_targets.js +++ b/src/js/components/api_targets.js @@ -66,7 +66,6 @@ function ( // store ADS information connected with ORCID here ORCID_PREFERENCES: 'orcid/preferences', - ORCID_NAME: 'orcid/orcid-name', // library endpoints // can get info about all libraries, or list of bibcodes associated w/specific lib (libraries/id) diff --git a/src/js/mixins/papers_utils.js b/src/js/mixins/papers_utils.js index 6e90af517..cb3c7e3d6 100644 --- a/src/js/mixins/papers_utils.js +++ b/src/js/mixins/papers_utils.js @@ -132,7 +132,7 @@ function ( data.shortAbstract = data.abstract ? this.shortenAbstract(data.abstract) : undefined; data.details = data.details || { 'shortAbstract': data.shortAbstract, 'pub': data.pub, 'abstract': data.abstract }; data.num_citations = data['[citations]'] ? data['[citations]'].num_citations : undefined; - data.identifier = data.bibcode ? data.bibcode : data.identifier; + data.identifier = data.bibcode; // make sure undefined doesn't become "undefined" data.encodedIdentifier = _.isUndefined(data.identifier) diff --git a/src/js/modules/orcid/bio.js b/src/js/modules/orcid/bio.js deleted file mode 100644 index d686c4b24..000000000 --- a/src/js/modules/orcid/bio.js +++ /dev/null @@ -1,44 +0,0 @@ - -define([ - 'underscore', - 'jsonpath', - 'js/modules/orcid/work' -], function (_, jp, Work) { - var PATHS = { - firstName: '$.name["given-names"].value', - lastName: '$.name["family-name"].value', - orcid: '$.name.path' - }; - - var Bio = function Bio(bio) { - this._root = bio || {}; - - this.get = function (path) { - var val = jp.query(this._root, path); - return val[0]; - }; - - this.toADSFormat = function () { - return { - responseHeader: { - params: { - orcid: this.getOrcid(), - firstName: this.getFirstName(), - lastName: this.getLastName() - } - } - }; - }; - - // generate getters for each path on PATHS - _.reduce(PATHS, function (obj, p, k) { - if (_.isString(k) && k.slice) { - var prop = k[0].toUpperCase() + k.slice(1); - obj['get' + prop] = _.partial(obj.get, p); - } - return obj; - }, this); - - }; - return Bio; -}); \ No newline at end of file diff --git a/src/js/modules/orcid/extension.js b/src/js/modules/orcid/extension.js index 7edc16a52..dcdc72474 100644 --- a/src/js/modules/orcid/extension.js +++ b/src/js/modules/orcid/extension.js @@ -201,23 +201,41 @@ function ( // make sure the doc has any information we gained if (_.isUndefined(work.identifier)) { - work.identifier = work._work.getIdentifier(); + if (_.isString(info.bibcode)) { + work.identifier = info.bibcode; + } else if (_.isArray(info.doi)) { + work.identifier = info.doi[0]; + } else if (_.isPlainObject(work._work)) { + var type = work._work.getExternalIdType(); + type = _.isArray(type) ? type[0] : type; + if (_.isString(type)) { + work.identifier = work._work.getExternalIds()[type]; + } + } } var model = _.find(self.hiddenCollection.models, function (m) { // do our best to find the match return (_.isPlainObject(work._work) && work._work === m.get('_work')) + || (_.isString(work.bibcode) && work.bibcode === m.get('bibcode')) + || (_.isArray(work.doi) && work.doi === m.get('doi')) || (!_.isUndefined(work.identifier) && work.identifier === m.get('identifier')); }); // found the model, update it if (model) { var sources; + var orcidPath; // grab the array of sources, if it exists if (_.isPlainObject(work._work)) { sources = work._work.getSources(); + var host = work._work.getSourceOrcidIdHost(); + var path = work._work.getPath(); + if (_.isString(host) && _.isString(path)) { + orcidPath = '//' + host + '/' + path; + } } if (_.isUndefined(model.get('identifier')) && self.orcidWidget) { @@ -226,7 +244,8 @@ function ( model.set({ orcid: actions, - source_name: _.isArray(sources) ? sources.join('; ') : model.get('source_name') + source_name: _.isArray(sources) ? sources.join('; ') : model.get('source_name'), + orcidWorkPath: orcidPath }); } else if (count < 60) { _.delay(_.bind(onSuccess, self, [work], count + 1), 500); @@ -312,7 +331,7 @@ function ( */ WidgetClass.prototype._updateModelsWithOrcid = function (models, tries) { var modelsToUpdate = _.filter(models || this.hiddenCollection.models, function (m) { - return !m.has('_work') && (m.has('bibcode') || m.has('doi') || m.has('identifier')); + return !m.has('_work') && (m.has('bibcode') || m.has('doi')); }); if (_.isEmpty(modelsToUpdate)) { @@ -329,7 +348,7 @@ function ( _.forEach(modelsToUpdate, function (m) { var exIds = _.flatten(_.values(_.pick(m.attributes, ['bibcode', 'doi', 'identifier']))); _.forEach(works, function (w) { - var wIds = _.flatten(_.values(w.getIdentifier())); + var wIds = _.flatten(_.values(w.getExternalIds())); var idMatch = _.intersection(exIds, wIds).length > 0; if (idMatch) { @@ -362,7 +381,7 @@ function ( if (pagination.numFound !== result.length) { _.extend(pagination, this.getPaginationInfo(apiResponse, docs)); } - _.delay(_.bind(this._updateModelsWithOrcid, this, 0), 1000); + _.delay(_.bind(this._updateModelsWithOrcid, this), 1000); return result; } return docs; @@ -425,7 +444,11 @@ function ( * @param {Work} fullOrcidWork - the full orcid work record */ var onRecieveFullOrcidWork = function (fullOrcidWork) { - var identifier = model.get('identifier'); + var identifier = model.get('identifier') + || fullOrcidWork.pickIdentifier(['bibcode', 'doi']); + if (!identifier) { + throw Error('Unable to determine suitable identifier'); + } var q = new ApiQuery({ q: 'identifier:' + queryUpdater.quoteIfNecessary(identifier), @@ -485,7 +508,7 @@ function ( var success = function (profile) { var works = profile.getWorks(); var matchedWork = _.find(works, function (w) { - var wIds = w.getIdentifier(); + var wIds = w.getExternalIds(); var doi = _.any(exIds.doi, wIds.doi); return exIds.bibcode === wIds.bibcode || doi; diff --git a/src/js/modules/orcid/orcid_api.js b/src/js/modules/orcid/orcid_api.js index c4483480d..65942d9c9 100644 --- a/src/js/modules/orcid/orcid_api.js +++ b/src/js/modules/orcid/orcid_api.js @@ -54,8 +54,7 @@ define([ 'js/components/api_query_updater', 'js/components/api_feedback', 'js/modules/orcid/work', - 'js/modules/orcid/profile', - 'js/modules/orcid/bio' + 'js/modules/orcid/profile' ], function ( _, @@ -73,8 +72,7 @@ function ( ApiQueryUpdater, ApiFeedback, Work, - Profile, - Bio + Profile ) { var OrcidApi = GenericModule.extend({ @@ -205,23 +203,6 @@ function ( return request; }, - getUserBio: function () { - var dd = new $.Deferred(); - var url = this.getBeeHive().getService('Api').url - + ApiTargets.ORCID_NAME + '/' + this.authData.orcid; - var request = this.createRequest(url); - request.fail(function () { - var msg = 'ADS name could not be retrieved'; - console.error.apply(console, [msg].concat(arguments)); - dd.reject(); - }); - request.done(function (bio) { - var orcidBio = new Bio(bio); - dd.resolve(orcidBio); - }); - return dd.promise(); - }, - /** * Forgets the OAuth access_token */ @@ -338,8 +319,7 @@ function ( */ getUrl: function (name, putCodes) { var targets = { - profile_bib: '/orcid-profile/simple', - profile_full: '/orcid-profile/full?update=True', + profile: '/orcid-profile', works: '/orcid-works', work: '/orcid-work' }; @@ -384,15 +364,14 @@ function ( */ _getUserProfile: function () { var self = this; - var request = this.createRequest(this.getUrl('profile_full')); + var request = this.createRequest(this.getUrl('profile')); // get everything so far in the cache var cache = self.getUserProfileCache.splice(0); request.done(function (profile) { _.forEach(cache, function (promise) { - orcidProfile = new Profile(profile); - promise.resolve(orcidProfile.setWorks(_.map(profile, function (profile, idx) {return new Work(profile)}))); + promise.resolve(self._reconcileProfileWorks(profile)); }); }); @@ -404,6 +383,86 @@ function ( }); }, + /** + * Reconcile the works contained in the incoming profile. + * Since it's possible for an ORCiD record to contain multiple sources, + * we have to figure out the best one to pick. + * + * The user can selected a "preferred" source, but since we can only match + * on items that have enough information (bibcode, doi, etc), we have to search + * through them all to find the best one. + * + * @param {object} rawProfile - the incoming profile + * @returns {Profile} - the new profile (with reconciled works) + */ + _reconcileProfileWorks: function (rawProfile) { + /* + 1. Source is ADS + 2. Has Bibcode + 3. Has DOI + 4. Other + */ + var self = this; + var profile = new Profile(rawProfile); + var works = _.map(profile.getWorksDeep(), function (work, idx) { + var w; + + // only operate on arrays > 1 + if (work.length > 1) { + var workWithBibcode; + var workWithDoi; + _.forEach(work, function (item) { + // check if the source is ADS + var isADS = self.isSourcedByADS(item); + + // grab an array of external ids ['bibcode', 'doi', '...'] + var exIds = item.getExternalIdType(); + var hasBibcode = exIds.indexOf('bibcode') > -1; + var hasDoi = exIds.indexOf('doi') > -1; + + // if it's sourced by ADS, use that one and break out of loop + if (isADS) { + w = item; + return false; + } + + // grab the first one that has a bibcode + if (hasBibcode && !workWithBibcode) { + workWithBibcode = item; + } + + // grab the first one that has a doi + if (hasDoi && !workWithDoi) { + workWithDoi = item; + } + return true; + }); + + // w will be defined if we found an ADS-sourced work + // otherwise, set the work accordingly below + if (!w && workWithBibcode) { + w = workWithBibcode; + } else if (!w && workWithDoi) { + w = workWithDoi; + } else if (!w) { + w = work[0]; + } + + // set the work's list of sources based on the full list from orcid + w.setSources(_.map(work, function (_w) { + return _w.getSourceName(); + })); + } + + // take the first work if we haven't found an array to process + return w || work[0]; + }); + + // set the new works + profile.setWorks(works); + return profile; + }, + /** * Retrieves user profile * Must have scope: /orcid-profile/read-limited @@ -893,7 +952,7 @@ function ( * @param {Work} work */ isSourcedByADS: function isSourcedByADS(work) { - return work.getSourceName().indexOf('NASA Astrophysics Data System') > -1; + return this.config.clientId === work.getSourceClientIdPath(); }, /** @@ -971,9 +1030,18 @@ function ( var db = {}; _.forEach(works, function addIdsToDatabase(w, i) { var key = 'identifier:'; - var ids = w.getIdentifier(); - - key += ids; + var ids = w.getExternalIds(); + + if (_.has(ids, 'bibcode')) { + key += ids.bibcode; + } else if (_.has(ids, 'doi')) { + key += ids.doi; + } else if (_.has(ids, 'null')) { + key += 'NONE'; + } else if (!_.isEmpty(ids)) { + // grab the first value + key += _.values(ids)[0]; + } if (key) { query.push(key); @@ -1035,6 +1103,7 @@ function ( } }); + self._combineDatabaseWorks(db); finishUpdate(db); }; @@ -1060,6 +1129,38 @@ function ( return self.dbUpdatePromise.promise(); }, + /** + * Looks at the identifier of the work and attempts to + * detect if a bibcode has a child within the other entries + * of the database. + * + * @param {object} db - the database object + * @returns {object} db - the update database object + */ + _combineDatabaseWorks: function (db) { + // loop through each entry of the database + _.forEach(db, function (data, identifier) { + // we can only do this for entries with data and bibcodes + if (_.isUndefined(data) || _.isUndefined(data.bibcode)) { + return true; + } + + // remove 'identifier:' from front of key + var key = identifier.split(':')[1]; + + // add an children property to the current (parent entry) + _.forEach(db, function (entry, subKey) { + // excluding our parent, see if the key matches the bibcode + if (entry.bibcode === key && subKey !== identifier) { + data.children = data.children || []; + data.children.push(entry.putcode); + } + }); + }); + + return db; + }, + /** * Creates a metadata object based on the work that is passed in that * helps with understanding the record's relationship with ADS. Figures @@ -1163,7 +1264,6 @@ function ( hardenedInterface: { hasAccess: 'boolean indicating access to ORCID Api', getUserProfile: 'get user profile', - getUserBio: 'get user bio', signIn: 'login', signOut: 'logout', getADSUserData: '', diff --git a/src/js/modules/orcid/profile.js b/src/js/modules/orcid/profile.js index 50578d684..534176bd2 100644 --- a/src/js/modules/orcid/profile.js +++ b/src/js/modules/orcid/profile.js @@ -5,7 +5,10 @@ define([ 'js/modules/orcid/work' ], function (_, jp, Work) { var PATHS = { - workSummaries: '$' + firstName: '$.person.name["given-names"].value', + lastName: '$.person.name["family-name"].value', + orcid: '$["orcid-identifier"].path', + workSummaries: '$["activities-summary"].works.group' }; /** @@ -54,6 +57,20 @@ define([ return this; }; + /** + * Pull all arrays of works from the profile + * grabs all works, not just the first + * + * @returns {[]Work[]} - the array of arrays of Work summaries + */ + this.getWorksDeep = function () { + return _.map(this.getWorkSummaries(), function (w) { + return _.map(w['work-summary'], function (subWork) { + return new Work(subWork); + }); + }); + }; + /** * Convenience method for generating an ADS response object * this can then be used to update the pagination of lists of orcid works @@ -84,8 +101,12 @@ define([ return { responseHeader: { - params: {} - }, + params: { + orcid: this.getOrcid(), + firstName: this.getFirstName(), + lastName: this.getLastName() + } + }, response: { numFound: docs.length, start: 0, diff --git a/src/js/modules/orcid/widget/widget.js b/src/js/modules/orcid/widget/widget.js index 7d4bd361f..848a38e5c 100644 --- a/src/js/modules/orcid/widget/widget.js +++ b/src/js/modules/orcid/widget/widget.js @@ -17,8 +17,7 @@ define([ 'js/components/api_feedback', 'js/components/json_response', 'hbs!js/modules/orcid/widget/templates/empty-template', - 'js/modules/orcid/extension', - 'js/modules/orcid/bio' + 'js/modules/orcid/extension' ], function ( _, ListOfThingsWidget, @@ -32,8 +31,7 @@ define([ ApiFeedback, JsonResponse, EmptyViewTemplate, - OrcidExtension, - OrcidBio + OrcidExtension ) { var ResultsWidget = ListOfThingsWidget.extend({ @@ -239,19 +237,18 @@ define([ } self.model.set('loading', true); - var orcidBio = oApi.getUserBio(); - var orcidProfile = oApi.getUserProfile(); - $.when(orcidBio, orcidProfile).done(function (bio, profile) { + var profile = oApi.getUserProfile(); + + profile.done(function gotProfile(profile) { var response = new JsonResponse(profile.toADSFormat()); - var bioResponse = new JsonResponse(bio.toADSFormat()); - var params = bioResponse.get('responseHeader.params'); + var params = response.get('responseHeader.params'); - var firstName = bio.getFirstName(); - var lastName = bio.getLastName(); + var firstName = params.firstName; + var lastName = params.lastName; self.model.set({ - orcidID: bio.getOrcid(), + orcidID: params.orcid, orcidUserName: firstName + ' ' + lastName, orcidFirstName: firstName, orcidLastName: lastName, @@ -263,7 +260,7 @@ define([ self.processResponse(response); }); - orcidProfile.fail(function () { + profile.fail(function () { self.model.set({ loading: false }); diff --git a/src/js/modules/orcid/work.js b/src/js/modules/orcid/work.js index e7d4c1787..d6fdfd559 100644 --- a/src/js/modules/orcid/work.js +++ b/src/js/modules/orcid/work.js @@ -3,16 +3,6 @@ define([ 'underscore', 'jsonpath' ], function (_, jp) { - var ADSPATHS = { - status: '$.status', - title: '$.title', - publicationDateMonth: '$.pubmonth', - publicationDateYear: '$.pubyear', - lastModifiedDate: '$.updated', - sourceName: '$.source', - putCode: '$.putcode', - identifier: '$.identifier' - }; var PATHS = { createdDate: '$["created-date"].value', lastModifiedDate: '$["last-modified-date"].value', @@ -65,7 +55,7 @@ define([ work = work || {}; // find the inner summary as the root - this._root = work; + this._root = (work['work-summary']) ? work['work-summary'][0] : work; this.sources = []; @@ -114,7 +104,7 @@ define([ /** * Returns the generated ORCiD work from the current _root object. - * The object will be based on the paths in ADSPATHS + * The object will be based on the paths in PATHS * * @returns {*} - ORCiD formatted object */ @@ -140,13 +130,17 @@ define([ * @returns {*} - ADS formatted object */ this.toADSFormat = function () { - var ids = this.getIdentifier(); - + var ids = this.getExternalIds(); + if (ids.doi) { + ids.doi = [ids.doi]; + } return _.extend({}, ids, { + 'author': this.getContributorName(), 'title': [this.getTitle()], 'formattedDate': this.getFormattedPubDate(), + 'abstract': this.getShortDescription(), 'source_name': this.getSources().join('; '), - 'identifier': this.getIdentifier(), + 'pub': this.getJournalTitle(), '_work': this }); }; @@ -162,8 +156,57 @@ define([ return year + '/' + month; }; + /** + * Creates an object containing all external ids + * @example + * { bibcode: ["2018CNSNS..56..270Q"], doi: [...] } + * + * @returns {Object} - object containing external ids + */ + this.getExternalIds = function () { + var types = this.getExternalIdType(); + var values = this.getExternalIdValue(); + types = _.isArray(types) ? types : [types]; + values = _.isArray(values) ? values : [values]; + if (types.length !== values.length) { + return {}; + } + + return _.reduce(types, function (res, t, i) { + res[t] = values[i]; + return res; + }, {}); + }; + + /** + * Convenience method for distinguishing a particular identifier by priority. + * Given a set of external ids, this will return the value of the identifier. + * + * @example + * pickIdentifier(['bibcode', 'doi']); + * // returns: "2018CNSNS..56..270Q" + * + * @example + * pickIdentifier(['doi', 'bibcode']); + * // returns: "10.1016/j.cnsns.2017.08.014" + * + * @param {String[]} props - priority of the chosen ids + * @returns {String} - value of the chosen identifier + */ + this.pickIdentifier = function (props) { + var ids = this.getExternalIds(); + var out = {}; + _.eachRight(props, function (p) { + if (_.isString(ids[p])) { + out = ids[p]; + return false; + } + }); + return out; + }; + // create getters for each of the PATHS - _.reduce(ADSPATHS, function (obj, p, k) { + _.reduce(PATHS, function (obj, p, k) { if (_.isString(k) && k.slice) { var prop = k[0].toUpperCase() + k.slice(1); obj['get' + prop] = _.partial(obj.get, p); diff --git a/src/js/widgets/library_list/widget.js b/src/js/widgets/library_list/widget.js index 08e38463b..275311f94 100644 --- a/src/js/widgets/library_list/widget.js +++ b/src/js/widgets/library_list/widget.js @@ -302,7 +302,7 @@ define([ docs = PaginationMixin.addPaginationToDocs(docs, start); _.each(docs, function (d, i) { - d.identifier = d.bibcode ? d.bibcode : d.identifier; + d.identifier = d.bibcode; d.noCheckbox = true; var maxAuthorNames = 3; diff --git a/src/js/widgets/list_of_things/widget.js b/src/js/widgets/list_of_things/widget.js index a9b427491..6e5ff094f 100644 --- a/src/js/widgets/list_of_things/widget.js +++ b/src/js/widgets/list_of_things/widget.js @@ -217,9 +217,7 @@ define([ extractDocs: function (apiResponse) { var docs = apiResponse.get('response.docs'); docs = _.map(docs, function (d) { - if (d.bibcode) { - d.identifier = d.bibcode ? d.bibcode : d.identifier; - } + d.identifier = d.bibcode; return d; }); return docs; diff --git a/src/js/widgets/navbar/widget.js b/src/js/widgets/navbar/widget.js index 2ac30a2c5..65d770341 100644 --- a/src/js/widgets/navbar/widget.js +++ b/src/js/widgets/navbar/widget.js @@ -273,16 +273,17 @@ define([ if (this.model.get('orcidLoggedIn')) { // set the orcid username into the model var that = this; - orcidApi.getUserBio().done(function(bio) { - var firstName = bio.getFirstName(); - var lastName = bio.getLastName(); - that.model.set('orcidFirstName', firstName); - that.model.set('orcidLastName', lastName); - that.model.set('orcidQueryName', lastName + ', ' + firstName); - - // this will always be available - that.model.set('orcidURI', bio.getOrcid()); - }); + orcidApi.getUserProfile() + .done(function (profile) { + var firstName = profile.getFirstName(); + var lastName = profile.getLastName(); + that.model.set('orcidFirstName', firstName); + that.model.set('orcidLastName', lastName); + that.model.set('orcidQueryName', lastName + ', ' + firstName); + + // this will always be available + that.model.set('orcidURI', profile.getOrcid()); + }); } // also set in the "hourly" flag diff --git a/src/js/widgets/preferences/widget.js b/src/js/widgets/preferences/widget.js index d31edc6ee..6030a05f2 100644 --- a/src/js/widgets/preferences/widget.js +++ b/src/js/widgets/preferences/widget.js @@ -138,7 +138,7 @@ define([ this.model.set('loading', true); // get main orcid name - var orcidProfile = this.getBeeHive().getService('OrcidApi').getUserBio(); + var orcidProfile = this.getBeeHive().getService('OrcidApi').getUserProfile(); var adsOrcidUserInfo = this.getBeeHive().getService('OrcidApi').getADSUserData(); // doing it at once so there's no flicker of rapid rendering as different vals change diff --git a/src/js/widgets/results/widget.js b/src/js/widgets/results/widget.js index 06974a3ef..d5ba8fa4a 100644 --- a/src/js/widgets/results/widget.js +++ b/src/js/widgets/results/widget.js @@ -261,7 +261,7 @@ function ( docs = _.map(docs, function (d) { // used by link generator mixin d.link_server = link_server; - d.identifier = d.bibcode ? d.bibcode : d.identifier; + d.identifier = d.bibcode; // make sure undefined doesn't become "undefined" d.encodedIdentifier = _.isUndefined(d.identifier) diff --git a/src/js/widgets/sort/components/sort-app.jsx.js b/src/js/widgets/sort/components/sort-app.jsx.js index 78c59024c..87453d571 100644 --- a/src/js/widgets/sort/components/sort-app.jsx.js +++ b/src/js/widgets/sort/components/sort-app.jsx.js @@ -7,6 +7,7 @@ define([ const options = app.get('options'); const sort = app.get('sort'); const direction = app.get('direction'); + const locked = app.get('locked'); /** * Call the handler after a selection is made from the dropdown @@ -22,6 +23,8 @@ define([ return (