diff --git a/src/main/resources/META-INF/xnat/spawner/site-admin-elements.yaml b/src/main/resources/META-INF/xnat/spawner/site-admin-elements.yaml index 8117452b170ec0ee00577716d23f8e2d9ea15ea1..fc5cd386dcd443c41d75cde56a2706a39cdd0549 100644 --- a/src/main/resources/META-INF/xnat/spawner/site-admin-elements.yaml +++ b/src/main/resources/META-INF/xnat/spawner/site-admin-elements.yaml @@ -1,7 +1,7 @@ tabGroups: # dashboard: Dashboard - xnatSetup: XNAT Setup + xnatSetup: Site Settings manageAccess: Manage Access manageData: Manage Data processing: Processing @@ -14,7 +14,7 @@ siteId: kind: panel.input.text name: siteId label: Site ID - validation: required id onblur + validation: required id-strict max-length:24 description: > The id used to refer to this site (also used to generate database ids). The Site ID must start with a letter and contain only letters, numbers @@ -43,6 +43,7 @@ passwordExpirationInterval: # id: passwordExpirationInterval name: passwordExpirationInterval label: Password Expiration (Interval) + validation: interval after: > <p class="description">Interval of time after which unchanged passwords expire and users have to change them. Uses <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a>.</p> @@ -52,6 +53,7 @@ passwordExpirationDate: # id: passwordExpirationDate name: passwordExpirationDate label: Password Expiration (Date) + validation: date-us after: datePicker: tag: span#datePicker @@ -106,6 +108,7 @@ siteLoginLanding: kind: panel.input.text name: siteLoginLanding label: Site Login Landing + validation: required uri description: "The page users will land on immediately after logging in." siteLandingLayout: @@ -117,6 +120,7 @@ siteHome: kind: panel.input.text name: siteHome label: Site Home + validation: required uri description: "The page users will land on by clicking the XNAT logo in the menu bar." siteHomeLayout: @@ -139,6 +143,7 @@ adminEmail: kind: panel.input.email name: adminEmail label: Site Admin Email + validation: required email description: > The administrative email account to receive system emails. This address will receive frequent emails on system events, such as errors, processing completion, new user registration and so on. These emails @@ -194,7 +199,7 @@ archivePath: kind: panel.input.text name: archivePath label: Archive Path - validation: required path + validation: required uri description: "" element: disabled: true @@ -202,31 +207,31 @@ cachePath: kind: panel.input.text name: cachePath label: Cache Path - validation: required path + validation: required uri description: "" prearchivePath: kind: panel.input.text name: prearchivePath label: Prearchive Path - validation: required path + validation: required uri description: "" ftpPath: kind: panel.input.text name: ftpPath label: FTP Path - validation: required path + validation: required uri description: "" buildPath: kind: panel.input.text name: buildPath label: Build Path - validation: required id onblur + validation: required id description: "" pipelinePath: kind: panel.input.text name: pipelinePath label: Pipeline Path - validation: required id onblur + validation: required id description: "" zipExtensions: kind: panel.input.text @@ -344,6 +349,7 @@ userLoginsSessionControls: kind: panel.input.text name: sessionTimeout label: Session Timeout + validation: required interval description: > Interval for timing out user sessions. Uses <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a>. @@ -351,6 +357,7 @@ userLoginsSessionControls: kind: panel.input.text name: aliasTokenTimeout label: Alias Token Timeout + validation: required interval description: > Interval for timing out alias tokens. Uses <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a>. @@ -358,6 +365,7 @@ userLoginsSessionControls: kind: panel.input.text name: aliasTokenTimeoutSchedule label: Alias Token Timeout Schedule + validation: required cron description: > How often to check alias tokens for timeout (0 0 * * * * means it runs every hour). Uses basic <a target="_blank" href="http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/scheduling/support/CronSequenceGenerator.html">Cron notation</a> (lists and ranges aren't supported). @@ -377,6 +385,7 @@ userLoginsSessionControls: kind: panel.input.number name: ":sessions.concurrent_max" label: Maximum Concurrent Sessions + validation: required integer gte:1 lte:9999 description: The maximum number of permitted sessions a user can have open simultaneously. You must restart Tomcat for changes to this to take effect. loginFailureMessage: kind: panel.textarea @@ -389,11 +398,13 @@ userLoginsSessionControls: kind: panel.input.number name: maxFailedLogins label: Maximum Failed Logins + validation: required integer gte:1 lte:9999 description: Number of failed login attempts before accounts are temporarily locked. (-1 disables feature) failedLoginLockoutDuration: kind: panel.input.text name: maxFailedLoginsLockoutDuration label: Failed Logins Lockout Duration + validation: required interval description: > Interval of time to lock user accounts that have exceeded the max_failed_logins count. Uses <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a>. @@ -401,6 +412,7 @@ userLoginsSessionControls: kind: panel.input.text name: resetFailedLoginsSchedule label: Reset Failed Logins Schedule + validation: required cron description: > How often to check if the Failed Logins Lockout Duration time has expired so locked out users can be allowed to log in again (0 0 * * * * means it runs every hour). Uses basic <a target="_blank" href="http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/scheduling/support/CronSequenceGenerator.html">Cron notation</a> (lists and ranges aren't supported). @@ -408,6 +420,7 @@ userLoginsSessionControls: kind: panel.input.text name: inactivityBeforeLockout label: User Inactivity Lockout + validation: required interval description: > Interval of inactivity before a user account is disabled. Uses <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a>. @@ -415,6 +428,7 @@ userLoginsSessionControls: kind: panel.input.text name: inactivityBeforeLockoutSchedule label: Inactivity Lockout Schedule + validation: required cron description: > How often to check user accounts for inactivity (0 0 1 * * * means it runs at 1AM every day). Uses basic <a target="_blank" href="http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/scheduling/support/CronSequenceGenerator.html">Cron notation</a> (lists and ranges aren't supported). @@ -778,17 +792,30 @@ pluginTable: id: xnat-plugin-list name: pluginList load: /xapi/plugins + # specify which properties to show in the table items: - id: ~ # '~' creates hidden column + #id: ~! # '~!' creates hidden column + id: ~data # '~data' adds this item as a [data-] attribute to each <tr> + beanName: ~data.bean name: Plugin Name - pluginClass: Plugin Class - dataModelBeans: + pluginClass: + label: Plugin Class + className: center + viewPluginInfo: label: Contents - cells: - apply: formatJSON # function that accepts value as sole argument - html: > - <a href="#!" class="view-plugin-info link">View Plugin Info</a> - <div class="hidden plugin-json-string">__VALUE__</div> + value: "-" # this gets overwritten with 'html' below + className: center + # function that accepts current item as sole argument and parent object as 'this' + # returned value will be new value - if nothing's returned, value is passed through + call: (function(){ return formatJSON(this) }) + html: > + <a href="#!" class="view-plugin-info link">View Plugin Info</a> + <div data-code-language="json" class="hidden plugin-json-string">__VALUE__</div> + td: + title: "Plugin JSON" + #$: addClass('plugin-info') # this can be a string or object + $: + addClass: plugin-info messages: noData: > There are no plugins installed in this XNAT. Try searching for plugins on <a href="http://marketplace.xnat.org/plugins/" title="XNAT Marketplace" target="_blank">XNAT Marketplace</a>. @@ -796,29 +823,7 @@ pluginTable: An error occurred retrieving information for installed plugins. pluginTableScript: - tag: script - content: > - var $body = $('body'); - $body.on('click', '.view-plugin-info', function(){ - var $this = $(this); - var $tr = $this.closest('tr'); - var _id = $tr.find('td.id').text().trim(); - var _url = XNAT.url.restUrl('/xapi/plugins/' + _id); - XNAT.xhr.getJSON(_url).done(function(data){ - var _source = spawn('textarea', JSON.stringify(data, null, 4)); - XNAT.app.codeEditor.init(_source, { - language: 'json' - }).openEditor({ - title: 'Plugin Info', - classes: 'plugin-json', - footerContent: '(read-only)', - buttons: { close: { label: 'Close' } } - }); - }); - }); - $body.on('focus', '.plugin-json textarea.ace_text-input', function(){ - this.disabled = true; - }); + tag: script|src=/scripts/xnat/admin/pluginManager.js pluginList: kind: panel @@ -1016,7 +1021,7 @@ sessionBuilder: name: sessionXmlRebuilderRepeat label: Session Idle Check Interval placeholder: Interval in milliseconds - afterElement: milliseconds + afterElement: <span class="after">milliseconds</span> description: > This controls how often the system checks to see if any incoming DICOM sessions in the prearchive have been idle for longer than the configured session idle time. This value should be specified in @@ -1026,7 +1031,7 @@ sessionBuilder: name: sessionXmlRebuilderInterval label: Session Idle Time placeholder: Time in minutes - afterElement: minutes + afterElement: <span class="after">minutes</span> description: > This tells the system how long a DICOM session should sit idle—that is, with no new data added to the session—before attempting to build a session document from the DICOM data. This value is specified in @@ -1259,6 +1264,55 @@ automationSettings: offText: Disabled description: "When enabled, administrators can create and edit scripts that run internally to the XNAT process. This can be a powerful feature, but also can pose security hazards that may be unacceptable for certain deployments. For these situations, configurable internal scripting can be disabled (XNAT itself may still use some scripting for feature implementation, but these scripts can not be modified or updated by users)." +investigators: + kind: panel + name: investigators + label: List of Investigators + contents: + investigatorsTable: + kind: table.dataTable + load: /xapi/investigators + sortable: firstname, lastname, email, institution + before: + investigatorsjs: + tag: script|src=/scripts/xnat/app/investigators.js + script: + tag: script + content: > + XNAT.admin = getObject(XNAT.admin); + XNAT.admin.investigatorTable = XNAT.app.investigators.init().dataTable(); + XNAT.admin.investigatorFieldValue = function(val){ return val || '-' }; + XNAT.admin.investigatorArrayList = function(val){ return val.join(', ') }; + items: + xnatInvestigatordataId: + label: <div class="hidden"></div> + className: center + content: <a href="#!" class="view-investigator link" data-id="__VALUE__">View</a> +# title: +# label: Title +# call: function(val){ return val || '-' } + firstname: + label: First Name + call: XNAT.admin.investigatorFieldValue + lastname: + label: Last Name + call: XNAT.admin.investigatorFieldValue + email: + label: Email + call: XNAT.admin.investigatorFieldValue + institution: + label: Institution + call: XNAT.admin.investigatorFieldValue + primaryProjects: + label: Project PI + call: XNAT.admin.investigatorArrayList + investigatorProjects: + label: ~! + call: XNAT.admin.investigatorArrayList + newInvestigatorButton: + tag: button#new-investigator.btn1|style=margin-top:15px + content: Create New Investigator + misc: kind: panel.form name: misc @@ -1431,6 +1485,13 @@ adminPage: group: advanced contents: ${automationSettings} + manageInvestigators: + kind: tab + name: manageInvestigators + label: Manage Investigators + group: other + contents: + ${investigators} misc: kind: tab name: misc diff --git a/src/main/webapp/scripts/polyfills.js b/src/main/webapp/scripts/polyfills.js index 20b0a733d9e7dd47166597591be62d15f3d6d4ce..5104deb203e95da47a745c62524a161759fc82d6 100644 --- a/src/main/webapp/scripts/polyfills.js +++ b/src/main/webapp/scripts/polyfills.js @@ -256,6 +256,101 @@ if (!Array.prototype.filter) { }; } +// trim spaces from all string items in an array +// optionally removing empty strings from the array +// and optionally purging non-string items +if (!Array.prototype.trim) { + Array.prototype.trim = function(strip, purge) { + 'use strict'; + if (this == null) { + throw new TypeError('Array.prototype.trim called on null or undefined'); + } + var i = -1; + var len = this.length; + var outputArray = []; + var item; + while (++i < len) { + item = this[i]; + // return non-string items as-is + if (typeof item !== 'string') { + // if not purging them + if (purge !== true) { + outputArray.push(item); + } + continue; + } + // trim whitespace from string + item = item.trim(); + // only push empty item if not stripping them + if (item === '') { + if (!strip) { + outputArray.push(item); + } + } + else { + // always push non-empty strings + outputArray.push(item); + } + } + return outputArray; + } +} + +// force 'stringable' array elements to string, +// optionally including non-stringable items +if (!Array.prototype.strings) { + Array.prototype.strings = function(other){ + 'use strict'; + if (this == null) { + throw new TypeError('Array.prototype.strings called on null or undefined'); + } + var stringArray = []; + this.forEach(function(item){ + var _item = null; + if (/string|number|boolean/.test(typeof item)) { + _item = item+''; + } + else if (other) { + if (Array.isArray(item)){ + _item = '[' + item.strings(other) + ']'; + } + else if (typeof item === 'function') { + _item = item.toString().replace(/\s+/, ' '); + } + else if (typeof item === 'object') { + try { + _item = JSON.stringify(item) + } + catch(e1) { + try { + _item = item.toString() + } + catch(e2) { + try { + _item = item+'' + } + catch(e3) { + console.error('Could not stringify. ' + [e1, e2, e3].join(' ')); + } + } + } + } + } + if (_item) { + stringArray.push(_item); + } + }); + return stringArray; + } +} + +// simple check for a plain object +if (typeof Object.isObject != 'function') { + Object.isObject = function(obj) { + return Object.prototype.toString.call(obj) === '[object Object]'; + }; +} + if (typeof Object.assign != 'function') { Object.assign = function (target) { var undefined; diff --git a/src/main/webapp/scripts/utils.js b/src/main/webapp/scripts/utils.js index f394ccea730194c039987d910122b71a88386840..581df582764f0c61505c682a08369d3dd0166b42 100755 --- a/src/main/webapp/scripts/utils.js +++ b/src/main/webapp/scripts/utils.js @@ -183,6 +183,75 @@ jQuery.loadScript = function (url, arg1, arg2) { }; +// return defined attributes for selection +// optionally passing a list of attributes to get +// (as an array or space- or comma-separated list) +(function($attr){ + + function notEmpty(item){ + return item > '' + } + + function mapAttrs(attrs){ + var i = -1, + attr, + obj = {}; + while (++i < attrs.length){ + attr = attrs[i]; + if (attr.specified) { + obj[attr.name] = + obj[toCamelCase(attr.name)] = + attr; + } + } + return obj; + } + + $.fn.getAttr = + $.fn.getAttrs = + $.fn.getAttributes = function(attrs){ + var attributes = this[0].attributes, + attrMap = mapAttrs(attributes), + names = [], + obj = {}; + if (!this.length) { + return null; + } + // normalize to an array of attribute names + if (attrs) { + names = [].concat(attrs||[]).join(' ').split(/[,\s+]/).filter(notEmpty); + } + else { + names = toArray(attributes).map(function(item){ return item.name }); + } + if (!names.length) { + return attrMap; + } + names.forEach(function(name){ + obj[name] = + obj[toCamelCase(name)] = + attrMap[name].value; + }); + return obj; + } + + // given: <div id="foo" title="Foo" class="bar">Foo</div> + // $('#foo').attr(); + // --> { id: 'foo', title: 'Foo', "class": 'bar' } + $.fn.attr = + $.fn.attrs = function() { + if (!arguments.length) { + if (!this.length) { + return null; + } + return mapAttrs(this[0].attributes); + } + return $attr.apply(this, arguments); + }; + +})($.fn.attr); + + // set the value of a form element, then fire the // 'onchange' event ONLY if the value actually changed // (works on hidden inputs too) @@ -340,12 +409,14 @@ jQuery.fn.tableSort = function(){ // return this.innerHTML.trim() > ''; // }).addClass('sort'); $table.find('th.sort') - .append('<i> </i>') // wrapInner('<a href="#" class="nolink" title="click to sort on this column"/>'). .each(function(){ + var $this = $(this); + $this.find('i').remove(); + $this.append('<i> </i>'); // don't overwrite existing title this.title += ' (click to sort) '; - $(this).on('click.sort', function(){ + $this.on('click.sort', function(){ var $th = $(this), thIndex = $th.index(), sorted = $th.hasAnyClass('asc desc'), @@ -513,6 +584,7 @@ function menuInit(select, opts, width){ } function menuUpdate(select){ + if (!select) return false; return $$(select||'select.xnat-menu').trigger('chosen:updated'); } diff --git a/src/main/webapp/scripts/xnat/admin/passwords.js b/src/main/webapp/scripts/xnat/admin/passwords.js index 9fa3adfa66a707bbf5f4949cdb3c955013e9978d..30b020b3d0cb4b81b261a1a29c32f40ff4c0286d 100644 --- a/src/main/webapp/scripts/xnat/admin/passwords.js +++ b/src/main/webapp/scripts/xnat/admin/passwords.js @@ -1,5 +1,5 @@ // interactions with 'Security Passwords' section of admin ui -console.log('passwordExpirationType.js'); +console.log('passwords.js'); (function(){ diff --git a/src/main/webapp/scripts/xnat/admin/pluginManager.js b/src/main/webapp/scripts/xnat/admin/pluginManager.js new file mode 100644 index 0000000000000000000000000000000000000000..1b9d91eecd51aa78e6e1006d29fdd2ce45a47f60 --- /dev/null +++ b/src/main/webapp/scripts/xnat/admin/pluginManager.js @@ -0,0 +1,66 @@ +/*! + * Display list of installed plugins and view plugin info + */ + +$(function(){ + + console.log('pluginManager.js'); + + var $body = $('body'); + + // open dialog to view JSON + $body.on('click', 'a.view-plugin-info', function(){ + + var _id = $(this).closest('tr').dataAttr('id'); + var _url = XNAT.url.restUrl('/xapi/plugins/' + _id); + + XNAT.xhr.getJSON(_url).done(function(data){ + + var _source = spawn('textarea', JSON.stringify(data, null, 4)); + + var _editor = XNAT.app.codeEditor.init(_source, { + language: 'json' + }); + + _editor.openEditor({ + title: data.name, + classes: 'plugin-json', + footerContent: '(read-only)', + buttons: { + // json: { + // label: 'View JSON', + // link: true, + // action: function(){ + // xmodal.iframe({ + // title: data.id, + // url: _url, + // width: 720, height: 480, + // buttons: { close: { label: 'Close' } } + // }) + // } + // }, + close: { label: 'Close' } + }, + afterShow: function(dialog, obj){ + obj.aceEditor.setReadOnly(true); + } + }); + }); + }); + + // // alternate function that gets JSON from a hidden <div> + // $body.on('click', 'a.view-plugin-info', function(){ + // var _source = $(this).next('div.plugin-json-string'); + // XNAT.app.codeEditor.init(_source).openEditor({ + // title: 'Plugin Info', + // classes: 'plugin-json', + // footerContent: '(read-only)', + // buttons: { close: { label: 'Close' } }, + // afterShow: function(dialog, obj){ + // obj.aceEditor.setReadOnly(true); + // } + // }); + // }); + +}); + diff --git a/src/main/webapp/scripts/xnat/app/codeEditor.js b/src/main/webapp/scripts/xnat/app/codeEditor.js index ee021e8a585ce78ffeb846e1a1437196d359dd6f..4b53d0194315e1882e9e74ab4abe87006e904b4e 100644 --- a/src/main/webapp/scripts/xnat/app/codeEditor.js +++ b/src/main/webapp/scripts/xnat/app/codeEditor.js @@ -10,6 +10,8 @@ var XNAT = getObject(XNAT || {}); var codeEditor, xhr = XNAT.xhr; + console.log('codeEditor.js'); + var csrfParam = { XNAT_CSRF: csrfToken }; @@ -132,12 +134,13 @@ var XNAT = getObject(XNAT || {}); _this.aceEditor.setTheme('ace/theme/eclipse'); _this.aceEditor.getSession().setMode('ace/mode/' + stringLower(_this.language||'')); _this.aceEditor.session.setValue(_this.code); + // _this.aceEditor.setReadOnly(_this.readonly); } }); // put the new editor div in the wrapper _this.$editor.empty().append(editor); - + return this; }; @@ -154,7 +157,8 @@ var XNAT = getObject(XNAT || {}); // open code in a dialog for editing Editor.fn.openEditor = function(opts){ - var _this = this; + var _this = this, + fn = {}; var modal = {}; modal.width = 880; @@ -190,13 +194,31 @@ var XNAT = getObject(XNAT || {}); 'Changes will be submitted on save.' : 'Changes are not submitted automatically.<br>The containing form will need to be submitted to save.') + '</span>'; - modal.beforeShow = function(obj){ - _this.$editor = obj.$modal.find('div.code-editor'); + + // the 'beforeShow' and 'afterShow' methods + // get an extra argument - the Editor instance + + var _beforeShow = opts.beforeShow; + + fn.beforeShow = function(dialog){ + _this.$editor = this.$modal.find('div.code-editor'); _this.load(); + if (isFunction(_beforeShow)) { + // '_this' is the Editor instance + _beforeShow.call(this, dialog, _this) + } }; - modal.afterShow = function(){ - _this.$editor.focus(); + + var _afterShow = opts.afterShow; + + fn.afterShow = function(dialog){ + if (isFunction(_afterShow)) { + // '_this' is the Editor instance + _afterShow.call(this, dialog, _this) + } + _this.aceEditor.focus(); }; + modal.buttons = { save: { label: _this.isUrl ? 'Submit Changes' : 'Apply Changes', @@ -212,9 +234,7 @@ var XNAT = getObject(XNAT || {}); }; // override modal options with {opts} - extend(modal, opts); - - this.dialog = xmodal.open(modal); + this.dialog = xmodal.open(extend({}, modal, opts, fn)); return this; diff --git a/src/main/webapp/scripts/xnat/app/investigators.js b/src/main/webapp/scripts/xnat/app/investigators.js index b35ff3291f2c915ddbd503d8bb55a5c5f71483d6..b4f00892751ab6e659b34f7487b27434fc3c233a 100644 --- a/src/main/webapp/scripts/xnat/app/investigators.js +++ b/src/main/webapp/scripts/xnat/app/investigators.js @@ -25,6 +25,8 @@ var XNAT = getObject(XNAT); XNAT.app = getObject(XNAT.app||{}); XNAT.xapi = getObject(XNAT.xapi||{}); + console.log('investigators.js'); + function setupUrl(part){ part = part ? '/' + part : ''; return xurl.rootUrl(BASE_URL + part); @@ -74,6 +76,7 @@ var XNAT = getObject(XNAT); function Investigators(opts){ extend(true, this, opts); + this.menu = null; // this will be updated when a menu is created } Investigators.fn = Investigators.prototype; @@ -238,7 +241,7 @@ var XNAT = getObject(XNAT); } } - var isPrimary = self.menu.value == investigators.primary; + var isPrimary = self.menu ? self.menu.value == investigators.primary : false; function investigatorForm(){ return { @@ -258,7 +261,7 @@ var XNAT = getObject(XNAT); department: createInput('Department', 'department'), email: createInput('Email', 'email', 'email'), phone: createInput('Phone', 'phone', 'numeric-dash'), - primary: { + primary: self.menu ? { kind: 'panel.element', label: false, contents: @@ -266,6 +269,9 @@ var XNAT = getObject(XNAT); '<input type="checkbox" class="set-primary">' + ' Set as Primary' + '</label>' + } : { + tag: 'i.hidden', + content: '(no menu, no checkbox)' } // ID: createInput('ID', 'ID'), // invId: { @@ -288,13 +294,15 @@ var XNAT = getObject(XNAT); invForm.render(obj.$modal.find('div.add-edit-investigator')); }, afterShow: function(obj){ - obj.$modal.find('input.set-primary').prop('checked', isPrimary); + if (self.menu) { + obj.$modal.find('input.set-primary').prop('checked', isPrimary); + } }, okLabel: 'Submit', okClose: false, okAction: function(obj){ var _form = obj.$modal.find('form[name="editInvestigator"]'), - setPrimary = _form.find('input.set-primary')[0].checked; + setPrimary = self.menu ? _form.find('input.set-primary')[0].checked : false; $(_form).submitJSON({ delim: '!', validate: function(){ @@ -315,20 +323,22 @@ var XNAT = getObject(XNAT); return errors === 0; }, - success: function(data){ + success: function(data, status, xhrObj){ var selected = data.xnatInvestigatordataId; ui.banner.top(2000, 'Investigator data saved.', 'success'); // update other menus, if specified - if (menus) { - [].concat(menus).forEach(function(menu){ - menu.updateMenu(setPrimary ? selected : ''); - }) - } - // update the menu associated with the dialog - self.updateMenu([].concat(self.getSelected(), (!setPrimary ? selected : []))); - // set the PI if editing/creating PI - if (setPrimary) { - investigators.primary = selected; + if (self.menu) { + if (menus) { + [].concat(menus).forEach(function(menu){ + menu.updateMenu(setPrimary ? selected : ''); + }) + } + // update the menu associated with the dialog + self.updateMenu([].concat(self.getSelected(), (!setPrimary ? selected : []))); + // set the PI if editing/creating PI + if (setPrimary) { + investigators.primary = selected; + } } dialog.close(); } @@ -346,6 +356,75 @@ var XNAT = getObject(XNAT); }; + Investigators.fn.dataTable = function(container){ + + var self = this; + + var tableConfig = { + "investigatorsTable": { + kind: "table.dataTable", + load: "/xapi/investigators", + "items": { + "xnatInvestigatordataId": { + label: '<div class="hidden"></div>', + className: "center", + content: '<a href="#!" class="view-investigator" data-id="__VALUE__">View</a>', + call: function(id){} + }, + // "title": { + // label: "Title", + // call: function(val){ return val || '-' } + // }, + "firstname": { + label: "First Name", + call: function(val){ return val || '-' } + }, + "lastname": { + label: "Last Name", + call: function(val){ return val || '-' } + }, + "email": { + label: "Email", + call: function(val){ return val || '-' } + }, + "institution": { + label: "Institution", + call: function(val){ return val || '-' } + }, + "primaryProjects": { + label: "PI", + call: function(val){ return val.join(', ') } + }, + "investigatorProjects": { + label: "~!", + call: function(val){ return val.join(', ') } + } + } + } + }; + + this.spawnedTable = XNAT.spawner.spawn(tableConfig); + this.table = this.spawnedTable.get(); + + if (container) { + this.spawnedTable.render(container); + } + + var $body = $('body'); + + $body.on('click', 'a.view-investigator', function(){ + self.dialog($(this).data('id')); + }); + + $body.on('click', 'button#new-investigator', function(){ + self.dialog(); + }); + + return this; + + }; + + // init function for XNAT.misc.blank investigators.init = function(opts){ return new Investigators(opts); diff --git a/src/main/webapp/scripts/xnat/ui/panel.js b/src/main/webapp/scripts/xnat/ui/panel.js index bcc141ad457941df46514cab57712cb5471f120c..88a4a85d7321d7c6dd1db139b75188f0734a5372 100644 --- a/src/main/webapp/scripts/xnat/ui/panel.js +++ b/src/main/webapp/scripts/xnat/ui/panel.js @@ -387,6 +387,7 @@ var XNAT = getObject(XNAT || {}); // custom event for reloading data (refresh) $formPanel.on('reload-data', function(){ var _load = opts.refresh || opts.load || opts.url; + $(this).find('.valid, .invalid').removeClass('valid invalid'); loadData(this, { load: _load ? ('$?' + _load.replace(/^\$\?/, '')) : '' }); diff --git a/src/main/webapp/scripts/xnat/ui/table.js b/src/main/webapp/scripts/xnat/ui/table.js index 97aa14c640f6d4455fc142dfa0b4cbdee6033e62..ea7b5283e1bb31500655faeb296ac2e4e9dd91c6 100755 --- a/src/main/webapp/scripts/xnat/ui/table.js +++ b/src/main/webapp/scripts/xnat/ui/table.js @@ -397,7 +397,19 @@ var XNAT = getObject(XNAT); // create a div to hold the table // or message (if no data or error) - var tableContainer = spawn('div.data-table-container', [newTable.table]); + var $tableContainer = $.spawn('div.data-table-container', [newTable.table]); + var tableContainer = $tableContainer[0]; + + // if (opts.before) { + // $tableContainer.prepend(opts.before); + // } + + // add the table + // $tableContainer.append(newTable.table); + + // if (opts.after) { + // $tableContainer.append(opts.after); + // } function createTable(rows){ var props = [], objRows = []; @@ -408,6 +420,7 @@ var XNAT = getObject(XNAT); }); rows = objRows; // now it's an array } + // create header row if (!allItems && (opts.items || opts.properties)) { newTable.tr(); forOwn(opts.items||opts.properties, function(name, val){ @@ -415,16 +428,24 @@ var XNAT = getObject(XNAT); // if it's an object, get the 'label' property //var label = stringable(val) ? val+'' : val.label; props.push(name); + + // don't create <th> for items labeled as '~data' + if (/^~data/.test(val)) { + return; + } + newTable.th(val.label || val); - if (val === '~') { + + if (/^~!/.test(val.label || val)) { $(newTable.last.th).html(name) .addClass('hidden') .dataAttr('prop', name); return; } - if (!opts.sortable) return; - if (opts.sortable === true || opts.sortable.indexOf(name) !== -1) { + //if (!opts.sortable) return; + if (val.sort || opts.sortable === true || (opts.sortable||[]).indexOf(name) !== -1) { addClassName(newTable.last.th, 'sort'); + newTable.last.th.appendChild(spawn('i', ' ')) } }); } @@ -435,7 +456,7 @@ var XNAT = getObject(XNAT); forOwn(rows[0], function(name, val){ if (allItems) { newTable.th(name); - if (val === '~') { + if (/^~!/.test(val)) { addClassName(newTable.last.th, 'hidden'); } } @@ -444,23 +465,77 @@ var XNAT = getObject(XNAT); } rows.forEach(function(item){ newTable.tr(); + // iterate properties for each row props.forEach(function(name){ - var cellObj = { className: name }; + + var hidden = false; var itemVal = item[name]; - if (opts.items && opts.items[name].cells) { - extend(true, cellObj, opts.items[name].cells); - } - else { - cellObj.html = itemVal; + var cellObj = {}; + var tdElement = { + className: name, + html: itemVal + }; + + if (opts.items) { + cellObj = opts.items[name]; + if (typeof cellObj === 'string') { + // set item label to '~data' to add as a + // [data-*] attribute to the <tr> + if (/^~data/.test(cellObj)) { + var dataName = cellObj.split('.')[1] || name; + newTable.last$('tr').dataAttr(dataName, itemVal); + return; + } + hidden = /^~!/.test(cellObj); + } + else { + if (cellObj.td || cellObj.element) { + extend(true, tdElement, cellObj.td || cellObj.element); + } + if (cellObj.value) { + // explicitly override value + itemVal = cellObj.value; + } + if (cellObj.className) { + addClassName(tdElement, cellObj.className); + } + // if (cellObj.apply) { + // itemVal = eval(cellObj.apply).apply(item, [itemVal]); + // } + if (cellObj['call']) { + if (isFunction(cellObj['call'])) { + itemVal = cellObj['call'].call(item, itemVal) || itemVal; + } + else { + itemVal = eval('('+cellObj['call'].trim()+')').call(item, itemVal) || itemVal; + } + } + // special __VALUE__ string gets replaced + if (cellObj.html || cellObj.content) { + tdElement.html = (cellObj.html || cellObj.content).replace(/__VALUE__/g, itemVal); + } + hidden = /^~!/.test(cellObj.label); + } } - if (cellObj.apply) { - itemVal = eval(cellObj.apply).apply(item, itemVal); + + newTable.td(tdElement); + + var $td = $(newTable.last.td); + + // evaluate jQuery methods + if (cellObj.$) { + if (typeof cellObj.$ === 'string') { + eval('$(newTable.last.td).'+(cellObj.$).trim()); + } + else { + forOwn(cellObj.$, function(method, args){ + $td[method].apply($td, [].concat(args)) + }); + } } - // special __VALUE__ string gets replaced - cellObj.html = cellObj.html.replace(/__VALUE__/, itemVal); - newTable.td(cellObj); - if (opts.items[name] === '~') { - addClassName(newTable.last.td, 'hidden'); + + if (hidden) { + $td.addClass('hidden'); } }); diff --git a/src/main/webapp/scripts/xnat/validate.js b/src/main/webapp/scripts/xnat/validate.js index b5dab661e86717e709ade2f2f6ccbc5ea325d154..210a75004a869fffe18619ab5d4555f67a17def2 100644 --- a/src/main/webapp/scripts/xnat/validate.js +++ b/src/main/webapp/scripts/xnat/validate.js @@ -17,7 +17,7 @@ var XNAT = getObject(XNAT); } }(function(){ - var undefined, validate; + var undefined, undef, validate; XNAT.validation = getObject(XNAT.validation || {}); @@ -158,7 +158,7 @@ var XNAT = getObject(XNAT); var units = /\s+(sec|second|min|minute|hour|day|week|month|year)(s)?\s*/; var num = true; var valid = true; - var i = 1; // start i at 1 since parts[0] will be an empty string + var i = parts[0] === '' ? 1 : 0; // start i at 1 if parts[0] is an empty string var part; while (parts[i] && valid === true) { part = (parts[i] + ''); @@ -549,7 +549,7 @@ var XNAT = getObject(XNAT); var elValidate = new Validator(this); elValidate.is(type, args); //valid = regex[type].test(this.value); - if (elValidate.isValid(false)) { + if (!elValidate.isValid(true)) { invalid++ } }); @@ -629,9 +629,20 @@ var XNAT = getObject(XNAT); // function (must return true or false), // or custom regex Validator.fn.check = function(type){ + var self = this, + types = []; if (type) { this.is(type); } + else if (type !== false) { + if (this.element$.dataAttr('validate')) { + types = this.element$.dataAttr('validate').split(/\s+/); + types.forEach(function(_type){ + var parts = _type.split(/[:=]/); + self.is(parts[0], parts[1]); + }) + } + } return this.isValid(true); }; diff --git a/src/main/webapp/scripts/xnat/xhr.js b/src/main/webapp/scripts/xnat/xhr.js index ff51eae79c990831bacb28221ef6c6e012e154a0..83e57cda11eab13bd99d87e953c7593d9ca5a65f 100755 --- a/src/main/webapp/scripts/xnat/xhr.js +++ b/src/main/webapp/scripts/xnat/xhr.js @@ -480,6 +480,7 @@ var XNAT = getObject(XNAT||{}), if (this.value === '') { this.value = val; } + changeValue($this, val); } else if (/radio/i.test(this.type)) { this.checked = isEqual(this.value, val);