1 /** 2 * @fileOverview 3 * @author <a href="https://www.labkey.org">LabKey</a> (<a href="mailto:info@labkey.com">info@labkey.com</a>) 4 * @license Copyright (c) 2014-2019 LabKey Corporation 5 * <p/> 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * <p/> 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * <p/> 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 * <p/> 18 */ 19 (function($) { 20 21 /** 22 * @description Portal class to allow programmatic administration of portal pages. 23 * @class Portal class to allow programmatic administration of portal pages. 24 * <p>Additional Documentation: 25 * <ul> 26 * <li><a href= "https://www.labkey.org/Documentation/wiki-page.view?name=projects">Project and Folder Administration</a></li> 27 * <li><a href= "https://www.labkey.org/Documentation/wiki-page.view?name=addModule">Add Web Parts</a></li> 28 * <li><a href= "https://www.labkey.org/Documentation/wiki-page.view?name=manageWebParts">Manage Web Parts</a></li> 29 * </ul> 30 * </p> 31 */ 32 LABKEY.Portal = new function() 33 { 34 // private methods: 35 var MOVE_ACTION = 'move'; 36 var REMOVE_ACTION = 'remove'; 37 var TOGGLE_FRAME_ACTION = 'toggle_frame'; 38 var MOVE_UP = 0; 39 var MOVE_DOWN = 1; 40 var MOVE_LEFT = 0; 41 var MOVE_RIGHT = 1; 42 43 function wrapSuccessCallback(userSuccessCallback, action, webPartId, direction) 44 { 45 return function(webparts, responseObj, options) 46 { 47 updateDOM(webparts, action, webPartId, direction); 48 // after update, call the user's success function: 49 if (userSuccessCallback) 50 userSuccessCallback(webparts, responseObj, options); 51 } 52 } 53 54 function updateDOM(webparts, action, webPartId, direction) 55 { 56 var targetTable = document.getElementById('webpart_' + webPartId); 57 58 if (targetTable) 59 { 60 if (action === MOVE_ACTION) 61 { 62 var swapTable; 63 swapTable = direction === MOVE_UP ? 64 getAdjacentWebparts(targetTable).above : 65 getAdjacentWebparts(targetTable).below; 66 67 if (swapTable) 68 { 69 var parentEl = targetTable.parentNode; 70 var insertPoint = swapTable.nextSibling; 71 var swapPoint = targetTable.nextSibling; 72 73 // Need to make sure the element is actually a child before trying to remove 74 for (var node = 0; node < parentEl.childNodes.length; node++) { 75 if (parentEl.childNodes[node] === swapTable) { 76 parentEl.removeChild(targetTable); 77 parentEl.removeChild(swapTable); 78 parentEl.insertBefore(targetTable, insertPoint); 79 parentEl.insertBefore(swapTable, swapPoint); 80 break; 81 } 82 } 83 84 var targetUpButtonClass = getUpDownButtons(targetTable).upButton.className; 85 var targetDownButtonClass= getUpDownButtons(targetTable).downButton.className; 86 updateUpDownButtons(targetTable, getUpDownButtons(swapTable).upButton.className, getUpDownButtons(swapTable).downButton.className); 87 updateUpDownButtons(swapTable, targetUpButtonClass, targetDownButtonClass); 88 } 89 } 90 else if (action === REMOVE_ACTION) 91 { 92 var adjacentWebparts = getAdjacentWebparts(targetTable); 93 94 if (adjacentWebparts.below) 95 { 96 updateUpDownButtons(adjacentWebparts.below, getUpDownButtons(targetTable).upButton.className, undefined); 97 } 98 99 if (adjacentWebparts.above) 100 { 101 updateUpDownButtons(adjacentWebparts.above, undefined, getUpDownButtons(targetTable).downButton.className); 102 } 103 104 targetTable.parentNode.removeChild(targetTable); 105 } 106 } 107 } 108 109 function getAdjacentWebparts(webpart) 110 { 111 var above = webpart.previousElementSibling; 112 var below = webpart.nextElementSibling; 113 114 return { 115 above: above && above.getAttribute("name") === "webpart" ? above : undefined, 116 below: below && below.getAttribute("name") === "webpart" ? below : undefined 117 } 118 } 119 120 function getUpDownButtons(webpart) 121 { 122 var moveUpImage = 'fa fa-caret-square-o-up labkey-fa-portal-nav'; 123 var moveUpDisabledImage = 'fa fa-caret-square-o-up labkey-btn-default-toolbar-small-disabled labkey-fa-portal-nav'; 124 var moveDownImage = 'fa fa-caret-square-o-down labkey-fa-portal-nav'; 125 var moveDownDisabledImage = 'fa fa-caret-square-o-down labkey-btn-default-toolbar-small-disabled labkey-fa-portal-nav'; 126 127 var getImageEl = function(webpart, imgClass) 128 { 129 if (webpart){ 130 var imgChildren = webpart.getElementsByClassName('labkey-fa-portal-nav'); 131 for (var imageIndex = 0; imageIndex < imgChildren.length; imageIndex++) 132 { 133 if (imgChildren[imageIndex].className.indexOf(imgClass) >= 0) 134 { 135 return imgChildren[imageIndex]; 136 } 137 } 138 } 139 }; 140 141 return { 142 upButton: getImageEl(webpart, moveUpImage) || getImageEl(webpart, moveUpDisabledImage), 143 downButton: getImageEl(webpart, moveDownImage) || getImageEl(webpart, moveDownDisabledImage) 144 } 145 } 146 147 148 function updateUpDownButtons(webpart, upButtonClass, downButtonClass) 149 { 150 if (upButtonClass) 151 getUpDownButtons(webpart).upButton.className = upButtonClass; 152 if (downButtonClass) 153 getUpDownButtons(webpart).downButton.className = downButtonClass; 154 } 155 156 function wrapErrorCallback(userErrorCallback) 157 { 158 return function(exceptionObj, responseObj, options) 159 { 160 // after update, call the user's success function: 161 return userErrorCallback(exceptionObj, responseObj, options); 162 } 163 } 164 165 function defaultErrorHandler(exceptionObj, responseObj, options) 166 { 167 LABKEY.Utils.displayAjaxErrorResponse(responseObj, exceptionObj); 168 } 169 170 function mapIndexConfigParameters(config, action, direction) 171 { 172 var params = {}; 173 174 LABKEY.Utils.applyTranslated(params, config, { 175 success: false, 176 failure: false, 177 scope: false 178 }); 179 180 if (direction == MOVE_UP || direction == MOVE_DOWN) 181 params.direction = direction; 182 183 // These layered callbacks are confusing. The outermost (second wrapper, below) de-JSONs the response, passing 184 // native javascript objects to the success wrapper function defined by wrapErrorCallback (wrapSuccessCallback 185 // below). The wrapErrorCallback/wrapSuccessCallback function is responsible for updating the DOM, if necessary, 186 // closing the wait dialog, and then calling the API developer's success callback function, if one exists. If 187 // no DOM update is requested, we skip the middle callback layer. 188 var errorCallback = LABKEY.Utils.getOnFailure(config) || defaultErrorHandler; 189 190 if (config.updateDOM) 191 errorCallback = wrapErrorCallback(errorCallback); 192 errorCallback = LABKEY.Utils.getCallbackWrapper(errorCallback, config.scope, true); 193 194 // do the same double-wrap with the success callback as with the error callback: 195 var successCallback = config.success; 196 if (config.updateDOM) 197 successCallback = wrapSuccessCallback(LABKEY.Utils.getOnSuccess(config), action, config.webPartId, direction); 198 successCallback = LABKEY.Utils.getCallbackWrapper(successCallback, config.scope); 199 200 return { 201 params: params, 202 success: successCallback, 203 error: errorCallback 204 }; 205 } 206 207 // TODO: This should be considered 'Native UI' and be migrated away from ExtJS 208 var showEditTabWindow = function(title, handler, name) 209 { 210 LABKEY.requiresExt4Sandbox(function() { 211 Ext4.onReady(function() { 212 var nameTextField = Ext4.create('Ext.form.field.Text', { 213 xtype: 'textfield', 214 fieldLabel: 'Name', 215 labelWidth: 50, 216 width: 250, 217 name: 'tabName', 218 value: name ? name : '', 219 maxLength: 64, 220 enforceMaxLength: true, 221 enableKeyEvents: true, 222 labelSeparator: '', 223 selectOnFocus: true, 224 listeners: { 225 scope: this, 226 keypress: function(field, event){ 227 if (event.getKey() == event.ENTER) { 228 handler(nameTextField.getValue(), editTabWindow); 229 } 230 } 231 } 232 }); 233 234 var editTabWindow = Ext4.create('Ext.window.Window', { 235 title: title, 236 closeAction: 'destroy', 237 modal: true, 238 border: false, 239 items: [{ 240 xtype: 'panel', 241 border: false, 242 frame: false, 243 bodyPadding: 5, 244 items: [nameTextField] 245 }], 246 buttons: [{ 247 text: 'Ok', 248 scope: this, 249 handler: function(){handler(nameTextField.getValue(), editTabWindow);} 250 },{ 251 text: 'Cancel', 252 scope: this, 253 handler: function(){ 254 editTabWindow.close(); 255 } 256 }] 257 }); 258 259 // TODO: Until async CSS load blocking is complete give style a moment to load 260 setTimeout(function() { 261 editTabWindow.show(false, function(){nameTextField.focus();}, this); 262 }, 100); 263 }); 264 }); 265 }; 266 267 var showPermissions = function(webpartID, permission, containerPath) { 268 269 var display = function() { 270 Ext4.onReady(function() { 271 Ext4.create('LABKEY.Portal.WebPartPermissionsPanel', { 272 webPartId: webpartID, 273 permission: permission, 274 containerPath: containerPath, 275 autoShow: true 276 }); 277 }); 278 }; 279 280 var loader = function() { 281 LABKEY.requiresExt4Sandbox(function() { 282 LABKEY.requiresScript('WebPartPermissionsPanel.js', display, this); 283 }, this); 284 }; 285 286 // Require a webpartID for any action 287 if (webpartID) { 288 if (LABKEY.Portal.WebPartPermissionsPanel) { 289 display(); 290 } 291 else { 292 loader(); 293 } 294 } 295 }; 296 297 // public methods: 298 /** @scope LABKEY.Portal.prototype */ 299 return { 300 301 /** 302 * Move an existing web part up within its portal page, identifying the web part by its unique web part ID. 303 * @param config An object which contains the following configuration properties. 304 * @param {String} [config.pageId] Reserved for a time when multiple portal pages are allowed per container. 305 * If not provided, main portal page for the container will be queried. 306 * @param {String} [config.containerPath] Specifies the container in which the web part query should be performed. 307 * If not provided, the method will operate on the current container. 308 * @param {Function} config.success 309 Function called when the this function completes successfully. 310 This function will be called with the following arguments: 311 <ul> 312 <li>webparts: an object with one property for each page region, generally 'body' and 'right'. The value 313 of each property is an ordered array of objects indicating the current web part configuration 314 on the page. Each object has the following properties: 315 <ul> 316 <li>name: the name of the web part</li> 317 <li>index: the index of the web part</li> 318 <li>webPartId: the unique integer ID of this web part.</li> 319 </ul> 320 </li> 321 <li>responseObj: the XMLHttpResponseObject instance used to make the AJAX request</li> 322 <li>options: the options used for the AJAX request</li> 323 </ul> 324 * @param {Function} [config.failure] Function called when execution fails. 325 * This function will be called with the following arguments: 326 <ul> 327 <li>exceptionObj: A JavaScript Error object caught by the calling code.</li> 328 <li>responseObj: The XMLHttpRequest object containing the response data.</li> 329 <li>options: the options used for the AJAX request</li> 330 </ul> 331 */ 332 getWebParts : function(config) 333 { 334 LABKEY.Ajax.request({ 335 url: LABKEY.ActionURL.buildURL('project', 'getWebParts.api', config.containerPath), 336 method : 'GET', 337 success: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnSuccess(config), config.scope), 338 failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(config), config.scope, true), 339 params: config 340 }); 341 }, 342 343 /** 344 * Move an existing web part up within its portal page, identifying the web part by index. 345 * @param config An object which contains the following configuration properties. 346 * @param {String} [config.pageId] Reserved for a time when multiple portal pages are allowed per container. 347 * If not provided, main portal page for the container will be modified. 348 * @param {String} [config.containerPath] Specifies the container in which the web part modification should be performed. 349 * If not provided, the method will operate on the current container. 350 * @param {String} config.webPartId The unique integer ID of the web part to be moved. 351 * @param {Boolean} [config.updateDOM] Indicates whether the current page's DOM should be updated to reflect changes to web part layout. 352 * Defaults to false. 353 * @param {Function} config.success 354 Function called when the this function completes successfully. 355 This function will be called with the following arguments: 356 <ul> 357 <li>webparts: an object with one property for each page region, generally 'body' and 'right'. The value 358 of each property is an ordered array of objects indicating the current web part configuration 359 on the page. Each object has the following properties: 360 <ul> 361 <li>name: the name of the web part</li> 362 <li>index: the index of the web part</li> 363 <li>webPartId: the unique integer ID of this web part.</li> 364 </ul> 365 </li> 366 <li>responseObj: the XMLHttpResponseObject instance used to make the AJAX request</li> 367 <li>options: the options used for the AJAX request</li> 368 </ul> 369 * @param {Function} [config.failure] Function called when execution fails. 370 * This function will be called with the following arguments: 371 <ul> 372 <li>exceptionObj: A JavaScript Error object caught by the calling code.</li> 373 <li>responseObj: The XMLHttpRequest object containing the response data.</li> 374 <li>options: the options used for the AJAX request</li> 375 </ul> 376 */ 377 moveWebPartUp : function(config) 378 { 379 var callConfig = mapIndexConfigParameters(config, MOVE_ACTION, MOVE_UP); 380 LABKEY.Ajax.request({ 381 url: LABKEY.ActionURL.buildURL('project', 'moveWebPartAsync.api', config.containerPath), 382 method : 'POST', 383 success: LABKEY.Utils.getOnSuccess(callConfig), 384 failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(callConfig), callConfig.scope, true), 385 params: callConfig.params 386 }); 387 }, 388 389 390 /** 391 * Move an existing web part down within its portal page, identifying the web part by the unique ID of the containing span. 392 * This span will have name 'webpart'. 393 * @param config An object which contains the following configuration properties. 394 * @param {String} [config.pageId] Reserved for a time when multiple portal pages are allowed per container. 395 * If not provided, main portal page for the container will be modified. 396 * @param {String} [config.containerPath] Specifies the container in which the web part modification should be performed. 397 * If not provided, the method will operate on the current container. 398 * @param {String} config.webPartId The unique integer ID of the web part to be moved. 399 * @param {Boolean} [config.updateDOM] Indicates whether the current page's DOM should be updated to reflect changes to web part layout. 400 * Defaults to false. 401 * @param {Function} config.success 402 Function called when the this function completes successfully. 403 This function will be called with the following arguments: 404 <ul> 405 <li>webparts: an object with one property for each page region, generally 'body' and 'right'. The value 406 of each property is an ordered array of objects indicating the current web part configuration 407 on the page. Each object has the following properties: 408 <ul> 409 <li>name: the name of the web part</li> 410 <li>index: the index of the web part</li> 411 <li>webPartId: the unique integer ID of this web part.</li> 412 </ul> 413 </li> 414 <li>responseObj: the XMLHttpResponseObject instance used to make the AJAX request</li> 415 <li>options: the options used for the AJAX request</li> 416 </ul> 417 * @param {Function} [config.failure] Function called when execution fails. 418 * This function will be called with the following arguments: 419 <ul> 420 <li>exceptionObj: A JavaScript Error object caught by the calling code.</li> 421 <li>responseObj: The XMLHttpRequest object containing the response data.</li> 422 <li>options: the options used for the AJAX request</li> 423 </ul> 424 */ 425 moveWebPartDown : function(config) 426 { 427 var callConfig = mapIndexConfigParameters(config, MOVE_ACTION, MOVE_DOWN); 428 LABKEY.Ajax.request({ 429 url: LABKEY.ActionURL.buildURL('project', 'moveWebPartAsync.api', config.containerPath), 430 method : 'POST', 431 success: LABKEY.Utils.getOnSuccess(callConfig), 432 failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(callConfig), callConfig.scope, true), 433 params: callConfig.params 434 }); 435 }, 436 /** 437 * Remove an existing web part within its portal page. 438 * @param config An object which contains the following configuration properties. 439 * @param {String} [config.pageId] Reserved for a time when multiple portal pages are allowed per container. 440 * If not provided, main portal page for the container will be modified. 441 * @param {String} [config.containerPath] Specifies the container in which the web part modification should be performed. 442 * If not provided, the method will operate on the current container. 443 * @param {String} config.webPartId The unique integer ID of the web part to be moved. 444 * @param {Boolean} [config.updateDOM] Indicates whether the current page's DOM should be updated to reflect changes to web part layout. 445 * Defaults to false. 446 * @param {Function} config.success 447 Function called when the this function completes successfully. 448 This function will be called with the following arguments: 449 <ul> 450 <li>webparts: an object with one property for each page region, generally 'body' and 'right'. The value 451 of each property is an ordered array of objects indicating the current web part configuration 452 on the page. Each object has the following properties: 453 <ul> 454 <li>name: the name of the web part</li> 455 <li>index: the index of the web part</li> 456 <li>webPartId: the unique integer ID of this web part.</li> 457 </ul> 458 </li> 459 <li>responseObj: the XMLHttpResponseObject instance used to make the AJAX request</li> 460 <li>options: the options used for the AJAX request</li> 461 </ul> 462 * @param {Function} [config.failure] Function called when execution fails. 463 * This function will be called with the following arguments: 464 <ul> 465 <li>exceptionObj: A JavaScript Error object caught by the calling code.</li> 466 <li>responseObj: The XMLHttpRequest object containing the response data.</li> 467 <li>options: the options used for the AJAX request</li> 468 </ul> 469 */ 470 removeWebPart : function(config) 471 { 472 var callConfig = mapIndexConfigParameters(config, REMOVE_ACTION, undefined); 473 LABKEY.Ajax.request({ 474 url: LABKEY.ActionURL.buildURL('project', 'deleteWebPartAsync.api', config.containerPath), 475 method : 'POST', 476 success: LABKEY.Utils.getOnSuccess(callConfig), 477 failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(callConfig), callConfig.scope, true), 478 params: callConfig.params 479 }); 480 }, 481 482 toggleWebPartFrame : function(config) 483 { 484 var callConfig = mapIndexConfigParameters(config, TOGGLE_FRAME_ACTION, undefined); 485 LABKEY.Ajax.request({ 486 url: LABKEY.ActionURL.buildURL('project', 'toggleWebPartFrameAsync.api', config.containerPath), 487 method : 'POST', 488 success: LABKEY.Utils.getOnSuccess(callConfig), 489 failure: LABKEY.Utils.getCallbackWrapper(LABKEY.Utils.getOnFailure(callConfig), callConfig.scope, true), 490 params: callConfig.params 491 }); 492 }, 493 494 /** 495 * Move a folder tab to the left. 496 * @param pageId the pageId of the tab. 497 * @param domId the id of the anchor tag of the tab. 498 */ 499 moveTabLeft : function(pageId, domId) 500 { 501 LABKEY.Ajax.request({ 502 url: LABKEY.ActionURL.buildURL('admin', 'moveTab.api', LABKEY.container.path), 503 method: 'POST', 504 params: { 505 pageId: pageId, 506 direction: MOVE_LEFT 507 }, 508 success: LABKEY.Utils.getCallbackWrapper(function(response, options) { 509 if(domId && response.pageIdToSwap && response.pageIdToSwap !== response.pageId) { 510 var tabAnchor = $('#' + domId)[0]; 511 if (tabAnchor) { 512 $(tabAnchor.parentElement).insertBefore(tabAnchor.parentNode.previousElementSibling); 513 } 514 } 515 }, this, false), 516 failure: function(response){ 517 // Currently no-op when failure occurs. 518 } 519 }); 520 }, 521 522 /** 523 * Move a folder tab to the right. 524 * @param pageId the pageId of the tab. 525 * @param domId the id of the anchor tag of the tab. 526 */ 527 moveTabRight : function(pageId, domId) 528 { 529 LABKEY.Ajax.request({ 530 url: LABKEY.ActionURL.buildURL('admin', 'moveTab.api', LABKEY.container.path), 531 method: 'POST', 532 params: { 533 pageId: pageId, 534 direction: MOVE_RIGHT 535 }, 536 success: LABKEY.Utils.getCallbackWrapper(function(response, options) { 537 if(domId && response.pageIdToSwap && response.pageIdToSwap !== response.pageId) { 538 var tabAnchor = $('#' + domId)[0]; 539 if (tabAnchor) { 540 $(tabAnchor.parentElement).insertAfter(tabAnchor.parentNode.nextElementSibling); 541 } 542 } 543 }, this, false), 544 failure: function(response, options){ 545 // Currently no-op when failure occurs. 546 } 547 }); 548 }, 549 550 /** 551 * Allows an administrator to add a new portal page tab. 552 */ 553 addTab : function() 554 { 555 var addTabHandler = function(name, editWindow) 556 { 557 LABKEY.Ajax.request({ 558 url: LABKEY.ActionURL.buildURL('admin', 'addTab.api'), 559 method: 'POST', 560 jsonData: {tabName: name}, 561 success: function(response) 562 { 563 var jsonResp = LABKEY.Utils.decode(response.responseText); 564 if (jsonResp && jsonResp.success) 565 { 566 if (jsonResp.url) 567 window.location = jsonResp.url; 568 } 569 }, 570 failure: function(response) 571 { 572 var jsonResp = LABKEY.Utils.decode(response.responseText); 573 var errorMsg; 574 if (jsonResp && jsonResp.errors) 575 errorMsg = jsonResp.errors[0].message; 576 else 577 errorMsg = 'An unknown error occurred. Please contact your administrator.'; 578 Ext4.Msg.alert(errorMsg); 579 } 580 }); 581 }; 582 583 if (LABKEY.pageAdminMode) { 584 showEditTabWindow("Add Tab", addTabHandler, null); 585 } 586 }, 587 588 /** 589 * Shows a hidden tab. 590 * @param pageId the pageId of the tab. 591 */ 592 showTab : function(pageId) 593 { 594 LABKEY.Ajax.request({ 595 url: LABKEY.ActionURL.buildURL('admin', 'showTab.api'), 596 method: 'POST', 597 jsonData: {tabPageId: pageId}, 598 success: function(response) 599 { 600 var jsonResp = LABKEY.Utils.decode(response.responseText); 601 if (jsonResp && jsonResp.success) 602 { 603 if (jsonResp.url) 604 window.location = jsonResp.url; 605 } 606 }, 607 failure: function(response) 608 { 609 var jsonResp = LABKEY.Utils.decode(response.responseText); 610 if (jsonResp && jsonResp.errors) 611 { 612 alert(jsonResp.errors[0].message); 613 } 614 } 615 }); 616 }, 617 618 /** 619 * Allows an administrator to rename a tab. 620 * @param pageId the pageId of the tab. 621 * @param domId the id of the anchor tag of the tab. 622 * @param currentLabel the current label of the tab. 623 */ 624 renameTab : function(pageId, domId, currentLabel) 625 { 626 var tabLinkEl = document.getElementById(domId); 627 628 if (tabLinkEl) 629 { 630 var renameHandler = function(name, editWindow) 631 { 632 LABKEY.Ajax.request({ 633 url: LABKEY.ActionURL.buildURL('admin', 'renameTab.api'), 634 method: 'POST', 635 jsonData: { 636 tabPageId: pageId, 637 tabName: name 638 }, 639 success: function(response) 640 { 641 var jsonResp = LABKEY.Utils.decode(response.responseText); 642 if (jsonResp.success) 643 tabLinkEl.textContent = name; 644 editWindow.close(); 645 }, 646 failure: function(response) 647 { 648 var jsonResp = LABKEY.Utils.decode(response.responseText); 649 var errorMsg; 650 if (jsonResp.errors) 651 errorMsg = jsonResp.errors[0].message; 652 else 653 errorMsg = 'An unknown error occurred. Please contact your administrator.'; 654 Ext4.Msg.alert('Oops', errorMsg); 655 } 656 }); 657 }; 658 659 if (LABKEY.pageAdminMode) { 660 showEditTabWindow("Rename Tab", renameHandler, currentLabel); 661 } 662 } 663 }, 664 665 _showPermissions : showPermissions 666 }; 667 }; 668 669 })(jQuery); 670 671