diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java index 2fa6b5707a..5a3be7dd26 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryImportResource.java @@ -51,6 +51,7 @@ import org.slf4j.LoggerFactory; import sonia.scm.NotSupportedFeatuerException; import sonia.scm.Type; +import sonia.scm.api.rest.RestActionUploadResult; import sonia.scm.repository.AdvancedImportHandler; import sonia.scm.repository.ImportHandler; import sonia.scm.repository.ImportResult; @@ -167,54 +168,7 @@ public class RepositoryImportResource @PathParam("type") String type, @FormDataParam("name") String name, @FormDataParam("bundle") InputStream inputStream) { - SecurityUtils.getSubject().checkRole(Role.ADMIN); - - checkArgument(!Strings.isNullOrEmpty(name), - "request does not contain name of the repository"); - checkNotNull(inputStream, "bundle inputStream is required"); - - Repository repository; - - try - { - Type t = type(type); - - checkSupport(t, Command.UNBUNDLE, "bundle"); - - repository = create(type, name); - - RepositoryService service = null; - - File file = File.createTempFile("scm-import-", ".bundle"); - - try - { - long length = Files.asByteSink(file).writeFrom(inputStream); - - logger.info("copied {} bytes to temp, start bundle import", length); - service = serviceFactory.create(repository); - service.getUnbundleCommand().unbundle(file); - } - catch (RepositoryException ex) - { - handleImportFailure(ex, repository); - } - catch (IOException ex) - { - handleImportFailure(ex, repository); - } - finally - { - IOUtil.close(service); - IOUtil.delete(file); - } - } - catch (IOException ex) - { - logger.warn("could not create temporary file", ex); - - throw new WebApplicationException(ex); - } + Repository repository = doImportFromBundle(type, name, inputStream); return buildResponse(uriInfo, repository); } @@ -236,7 +190,6 @@ public class RepositoryImportResource * * * - * @param uriInfo uri info * @param type repository type * @param name name of the repository * @param inputStream input bundle @@ -249,11 +202,25 @@ public class RepositoryImportResource @Path("{type}/bundle.html") @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.TEXT_HTML) - public Response importFromBundleUI(@Context UriInfo uriInfo, - @PathParam("type") String type, @FormDataParam("name") String name, + public Response importFromBundleUI(@PathParam("type") String type, + @FormDataParam("name") String name, @FormDataParam("bundle") InputStream inputStream) { - return importFromBundle(uriInfo, type, name, inputStream); + Response response; + + try + { + doImportFromBundle(type, name, inputStream); + response = Response.ok(new RestActionUploadResult(true)).build(); + } + catch (WebApplicationException ex) + { + logger.warn("error durring bundle import", ex); + response = Response.fromResponse(ex.getResponse()).entity( + new RestActionUploadResult(false)).build(); + } + + return response; } /** @@ -595,6 +562,71 @@ public class RepositoryImportResource return repository; } + /** + * Start bundle import. + * + * + * @param type repository type + * @param name name of the repository + * @param inputStream bundle stream + * + * @return imported repository + */ + private Repository doImportFromBundle(String type, String name, + InputStream inputStream) + { + SecurityUtils.getSubject().checkRole(Role.ADMIN); + + checkArgument(!Strings.isNullOrEmpty(name), + "request does not contain name of the repository"); + checkNotNull(inputStream, "bundle inputStream is required"); + + Repository repository; + + try + { + Type t = type(type); + + checkSupport(t, Command.UNBUNDLE, "bundle"); + + repository = create(type, name); + + RepositoryService service = null; + + File file = File.createTempFile("scm-import-", ".bundle"); + + try + { + long length = Files.asByteSink(file).writeFrom(inputStream); + + logger.info("copied {} bytes to temp, start bundle import", length); + service = serviceFactory.create(repository); + service.getUnbundleCommand().unbundle(file); + } + catch (RepositoryException ex) + { + handleImportFailure(ex, repository); + } + catch (IOException ex) + { + handleImportFailure(ex, repository); + } + finally + { + IOUtil.close(service); + IOUtil.delete(file); + } + } + catch (IOException ex) + { + logger.warn("could not create temporary file", ex); + + throw new WebApplicationException(ex); + } + + return repository; + } + /** * Method description * diff --git a/scm-webapp/src/main/webapp/index.mustache b/scm-webapp/src/main/webapp/index.mustache index e29c2cd556..4c2c3c913c 100644 --- a/scm-webapp/src/main/webapp/index.mustache +++ b/scm-webapp/src/main/webapp/index.mustache @@ -77,6 +77,7 @@ + diff --git a/scm-webapp/src/main/webapp/resources/css/style.css b/scm-webapp/src/main/webapp/resources/css/style.css index 2ce002295c..355e8afcbe 100644 --- a/scm-webapp/src/main/webapp/resources/css/style.css +++ b/scm-webapp/src/main/webapp/resources/css/style.css @@ -105,6 +105,11 @@ a.scm-link:hover { margin-left: 2px; } +.scm-form-fileupload-help-button { + position: absolute; + right: -19px; +} + .scm-nav-item { cursor: pointer; } @@ -238,3 +243,12 @@ div.noscript-container h1 { .unhealthy { color: red; } + +/** import **/ +.import-fu { + margin-right: 24px; +} + +.import-fu input { + width: 215px; +} diff --git a/scm-webapp/src/main/webapp/resources/js/override/ext.form.field.js b/scm-webapp/src/main/webapp/resources/js/override/ext.form.field.js index 548599e3a0..61abb54b88 100644 --- a/scm-webapp/src/main/webapp/resources/js/override/ext.form.field.js +++ b/scm-webapp/src/main/webapp/resources/js/override/ext.form.field.js @@ -77,6 +77,9 @@ Ext.override(Ext.form.Field, { cls = 'scm-form-combo-help-button'; } break; + case 'fileuploadfield': + cls = 'scm-form-fileupload-help-button'; + break; case 'textarea': cls = 'scm-form-textarea-help-button'; break; diff --git a/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.importwindow.js b/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.importwindow.js index 53ea0dcf19..aa097626ab 100644 --- a/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.importwindow.js +++ b/scm-webapp/src/main/webapp/resources/js/repository/sonia.repository.importwindow.js @@ -31,125 +31,406 @@ Sonia.repository.ImportWindow = Ext.extend(Ext.Window,{ - titleText: 'Import Repositories', - okText: 'Ok', - closeText: 'Close', + title: 'Repository Import Wizard', - // cache - importForm: null, + initComponent: function(){ + + this.addEvents('finish'); + + var config = { + title: this.title, + layout: 'fit', + width: 420, + height: 190, + closable: true, + resizable: true, + plain: true, + border: false, + modal: true, + bodyCssClass: 'x-panel-mc', + items: [{ + id: 'scmRepositoryImportWizard', + xtype: 'scmRepositoryImportWizard', + listeners: { + finish: { + fn: this.onFinish, + scope: this + } + } + }] + }; + + Ext.apply(this, Ext.apply(this.initialConfig, config)); + Sonia.repository.ImportWindow.superclass.initComponent.apply(this, arguments); + }, + + onFinish: function(config){ + this.fireEvent('finish', config); + this.close(); + } + +}); + +Sonia.repository.ImportPanel = Ext.extend(Ext.Panel, { + + // text + backText: 'Back', + nextText: 'Next', + finishText: 'Finish', imported: [], importJobsFinished: 0, importJobs: 0, + // help text + importTypeDirectoryHelpText: 'Imports all repositories that are located at the repository folder of SCM-Manager.', + importTypeURLHelpText: 'Imports a repository from a remote url.', + importTypeFileHelpText: 'Imports a repository from a file (e.g. SVN dump).', + + importUrlNameHelpText: 'The name of the repository in SCM-Manager.', + importUrlHelpText: 'The source url of the repository.', + + importFileNameHelpText: 'The name of the repository in SCM-Manager.', + importFileHelpText: 'Choose the dump file you want to import to SCM-Manager.', + + // tips + tipRepositoryType: 'Choose your repository type for the import.', + tipImportType: 'Select the type of import. Note: Not all repository types support all options.', + + // cache + nextButton: null, + prevButton: null, + + // settings + repositoryType: null, + + // active card + activeForm: null, + initComponent: function(){ + this.addEvents('finish'); + + // fix initialization bug this.imported = []; this.importJobsFinished = 0; this.importJobs = 0; + this.activeForm = null; + + var importedStore = new Ext.data.JsonStore({ + fields: ['type', 'name'] + }); + // store.loadData(this.imported); + + var importedColModel = new Ext.grid.ColumnModel({ + defaults: { + sortable: true, + scope: this + }, + columns: [ + {id: 'name', header: 'Name', dataIndex: 'name'}, + {id: 'type', header: 'Type', dataIndex: 'type'} + ] + }); + + var typeItems = []; + + Ext.each(state.repositoryTypes, function(repositoryType){ + typeItems.push({ + boxLabel: repositoryType.displayName, + name: 'repositoryType', + inputValue: repositoryType.name, + checked: false + }); + }); + + typeItems = typeItems.sort(function(a, b){ + return a.boxLabel > b.boxLabel; + }); + + typeItems.push({ + xtype: 'scmTip', + content: this.tipRepositoryType, + width: '100%' + }); var config = { - layout:'fit', - width:300, - height:170, - closable: true, - resizable: false, - plain: true, - border: false, - modal: true, - title: this.titleText, - items: [{ - id: 'importRepositoryForm', - frame: true, - xtype: 'form', - defaultType: 'checkbox' - }], - buttons: [{ - id: 'startRepositoryImportButton', - text: this.okText, - formBind: true, - scope: this, - handler: this.importRepositories + layout: 'card', + activeItem: 0, + bodyStyle: 'padding: 5px', + defaults: { + bodyCssClass: 'x-panel-mc', + border: false, + labelWidth: 120, + width: 250 + }, + bbar: ['->',{ + id: 'move-prev', + text: this.backText, + handler: this.navHandler.createDelegate(this, [-1]), + disabled: true, + scope: this },{ - text: this.closeText, - scope: this, - handler: this.close + id: 'move-next', + text: this.nextText, + handler: this.navHandler.createDelegate(this, [1]), + disabled: true, + scope: this + },{ + id: 'finish', + text: this.finishText, + handler: this.applyChanges, + disabled: true, + scope: this }], - listeners: { - afterrender: { - fn: this.readImportableTypes, - scope: this + items: [{ + id: 'repositoryTypeLayout', + items: [{ + id: 'chooseRepositoryType', + xtype: 'radiogroup', + name: 'chooseRepositoryType', + columns: 1, + items: [typeItems], + listeners: { + change: function(){ + Ext.getCmp('move-next').setDisabled(false); + } + } + }] + },{ + id: 'importTypeLayout', + items: [{ + id: 'chooseImportType', + xtype: 'radiogroup', + name: 'chooseImportType', + columns: 1, + items: [{ + id: 'importTypeDirectory', + boxLabel: 'Import from directory', + name: 'importType', + inputValue: 'directory', + disabled: false, + helpText: this.importTypeDirectoryHelpText + },{ + id: 'importTypeURL', + boxLabel: 'Import from url', + name: 'importType', + inputValue: 'url', + checked: false, + disabled: true, + helpText: this.importTypeURLHelpText + },{ + id: 'importTypeFile', + boxLabel: 'Import from file', + name: 'importType', + inputValue: 'file', + checked: false, + disabled: true, + helpText: this.importTypeFileHelpText + },{ + xtype: 'scmTip', + content: this.tipImportType, + width: '100%' + }], + listeners: { + change: function(){ + Ext.getCmp('move-next').setDisabled(false); + } + } + }] + },{ + id: 'importUrlLayout', + xtype: 'form', + monitorValid: true, + defaults: { + width: 250 + }, + listeners: { + clientvalidation: { + fn: this.urlFormValidityMonitor, + scope: this + } + }, + items: [{ + id: 'importUrlName', + xtype: 'textfield', + fieldLabel: 'Repository name', + name: 'name', + allowBlank: false, + vtype: 'repositoryName', + helpText: this.importUrlNameHelpText + },{ + id: 'importUrl', + xtype: 'textfield', + fieldLabel: 'Import URL', + name: 'url', + allowBlank: false, + vtype: 'url', + helpText: this.importUrlHelpText + },{ + xtype: 'scmTip', + content: 'Please insert name and remote url of the repository.', + width: '100%' + }] + },{ + id: 'importFileLayout', + xtype: 'form', + fileUpload: true, + monitorValid: true, + listeners: { + clientvalidation: { + fn: this.fileFormValidityMonitor, + scope: this + } + }, + items: [{ + id: 'importFileName', + xtype: 'textfield', + fieldLabel: 'Repository name', + name: 'name', + type: 'textfield', + width: 250, + allowBlank: false, + vtype: 'repositoryName', + helpText: this.importFileNameHelpText + },{ + id: 'importFile', + xtype: 'fileuploadfield', + fieldLabel: 'Import File', + ctCls: 'import-fu', + name: 'bundle', + allowBlank: false, + helpText: this.importFileHelpText, + cls: 'import-fu', + buttonCfg: { + iconCls: 'upload-icon' + } + },{ + xtype: 'scmTip', + content: 'Please insert name and upload the repository file.', + width: '100%' + }] + },{ + id: 'importFinishedLayout', + layout: 'form', + defaults: { + width: 250 + }, + items: [{ + id: 'importedGrid', + xtype: 'grid', + autoExpandColumn: 'name', + store: importedStore, + colModel: importedColModel, + height: 100 + }] + }] + }; + + Ext.apply(this, Ext.apply(this.initialConfig, config)); + Sonia.repository.ImportPanel.superclass.initComponent.apply(this, arguments); + }, + + navHandler: function(direction){ + this.activeForm = null; + + var layout = this.getLayout(); + var id = layout.activeItem.id; + + var next = -1; + + if ( id === 'repositoryTypeLayout' && direction === 1 ){ + this.repositoryType = Ext.getCmp('chooseRepositoryType').getValue().getRawValue(); + console.log('rt: ' + this.repositoryType); + this.enableAvailableImportTypes(); + next = 1; + } + else if ( id === 'importTypeLayout' && direction === -1 ){ + next = 0; + Ext.getCmp('move-prev').setDisabled(true); + Ext.getCmp('move-next').setDisabled(false); + } + else if ( id === 'importTypeLayout' && direction === 1 ){ + Ext.getCmp('move-next').setDisabled(false); + var v = Ext.getCmp('chooseImportType').getValue(); + if ( v ){ + switch (v.getRawValue()){ + case 'directory': + this.importFromDirectory(layout); + break; + case 'url': + next = 2; + this.activeForm = 'url'; + break; + case 'file': + next = 3; + this.activeForm = 'file'; + break; } } - }; - Ext.apply(this, Ext.apply(this.initialConfig, config)); - Sonia.repository.ImportWindow.superclass.initComponent.apply(this, arguments); - }, - - readImportableTypes: function(){ - if (debug){ - console.debug('read importable types'); + } + else if ( (id === 'importUrlLayout' || id === 'importFileLayout') && direction === -1 ) + { + next = 1; + } + else if ( id === 'importUrlLayout' && direction === 1 ) + { + this.importFromUrl(layout, Ext.getCmp('importUrlLayout').getForm().getValues()); + } + else if ( id === 'importFileLayout' && direction === 1 ) + { + this.importFromFile(layout, Ext.getCmp('importFileLayout').getForm()); } - Ext.Ajax.request({ - url: restUrl + 'import/repositories.json', - method: 'GET', - scope: this, - success: function(response){ - var obj = Ext.decode(response.responseText); - this.renderTypeCheckboxes(obj); - this.doLayout(); - }, - failure: function(result){ - main.handleRestFailure( - result, - this.errorTitleText, - this.errorMsgText - ); - } - }); - - }, - - renderTypeCheckboxes: function(types){ - Ext.each(types, function(type){ - this.renderCheckbox(type); - }, this); - }, - - getImportForm: function(){ - if (!this.importForm){ - this.importForm = Ext.getCmp('importRepositoryForm'); + if ( next >= 0 ){ + layout.setActiveItem(next); } - return this.importForm; + }, + + getNextButton: function(){ + if (!this.nextButton){ + this.nextButton = Ext.getCmp('move-next'); + } + return this.nextButton; + }, + + getPrevButton: function(){ + if (!this.prevButton){ + this.prevButton = Ext.getCmp('move-prev'); + } + return this.prevButton; }, - renderCheckbox: function(type){ - this.getImportForm().add({ - xtype: 'checkbox', - name: 'type', - fieldLabel: type.displayName, - inputValue: type.name + showLoadingBox: function(){ + return Ext.MessageBox.show({ + title: 'Loading', + msg: 'Import repository', + width: 300, + wait: true, + animate: true, + progress: true, + closable: false }); }, - importRepositories: function(){ - if (debug){ - console.debug('start import of repositories'); + urlFormValidityMonitor: function(form, valid){ + if (this.activeForm === 'url'){ + this.formValidityMonitor(form, valid); } - var form = this.getImportForm().getForm(); - var values = form.getValues().type; - if ( values ){ - if ( Ext.isArray(values) ){ - this.importJobs = values.length; - } else { - this.importJobs = 1; - } - } else { - this.importJobs = 0; + }, + + fileFormValidityMonitor: function(form, valid){ + if (this.activeForm === 'file'){ + this.formValidityMonitor(form, valid); + } + }, + + formValidityMonitor: function(form, valid){ + var nbt = this.getNextButton(); + if (valid && nbt.disabled){ + nbt.setDisabled(false); + } else if (!valid && !nbt.disabled){ + nbt.setDisabled(true); } - Ext.each(values, function(value){ - this.importRepositoriesOfType(value); - }, this); }, appendImported: function(repositories){ @@ -161,62 +442,77 @@ Sonia.repository.ImportWindow = Ext.extend(Ext.Window,{ if (debug){ console.debug( 'import of ' + this.importJobsFinished + ' jobs finished' ); } - this.printImported(); + Ext.getCmp('importedGrid').getStore().loadData(this.imported); + Ext.getCmp('move-next').setDisabled(true); + Ext.getCmp('move-prev').setDisabled(true); + Ext.getCmp('finish').setDisabled(false); } }, - printImported: function(){ - var store = new Ext.data.JsonStore({ - fields: ['type', 'name'] - }); - store.loadData(this.imported); - - var colModel = new Ext.grid.ColumnModel({ - defaults: { - sortable: true, - scope: this + importFromFile: function(layout, form){ + var lbox = this.showLoadingBox(); + form.submit({ + url: restUrl + 'import/repositories/' + this.repositoryType + '/bundle.html', + scope: this, + success: function(form, action){ + this.appendImported([{ + name: form.getValues().name, + type: this.repositoryType + }]); + lbox.hide(); + layout.setActiveItem(4); }, - columns: [ - {id: 'name', header: 'Name', dataIndex: 'name'}, - {id: 'type', header: 'Type', dataIndex: 'type'} - ] + failure: function(form, action){ + lbox.hide(); + main.handleRestFailure( + action.response, + this.errorTitleText, + this.errorMsgText + ); + } }); - - this.getImportForm().add({ - xtype: 'grid', - autoExpandColumn: 'name', - store: store, - colModel: colModel, - height: 100 - }); - var h = this.getHeight(); - this.setHeight( h + 100 ); - this.doLayout(); - - // reload repositories panel - var panel = Ext.getCmp('repositories'); - if (panel){ - panel.getGrid().reload(); - } }, - importRepositoriesOfType: function(type){ - if (debug){ - console.debug('start import of ' + type + ' repositories'); - } - var b = Ext.getCmp('startRepositoryImportButton'); - if ( b ){ - b.setDisabled(true); - } + importFromUrl: function(layout, repository){ + var lbox = this.showLoadingBox(); Ext.Ajax.request({ - url: restUrl + 'import/repositories/' + type + '.json', + url: restUrl + 'import/repositories/' + this.repositoryType + '/url.json', + method: 'POST', + scope: this, + jsonData: repository, + success: function(){ + this.appendImported([{ + name: repository.name, + type: this.repositoryType + }]); + lbox.hide(); + layout.setActiveItem(4); + }, + failure: function(result){ + lbox.hide(); + main.handleRestFailure( + result, + this.errorTitleText, + this.errorMsgText + ); + } + }); + }, + + importFromDirectory: function(layout){ + var lbox = this.showLoadingBox(); + Ext.Ajax.request({ + url: restUrl + 'import/repositories/' + this.repositoryType + '.json', method: 'POST', scope: this, success: function(response){ var obj = Ext.decode(response.responseText); this.appendImported(obj); + lbox.hide(); + layout.setActiveItem(4); }, failure: function(result){ + lbox.hide(); main.handleRestFailure( result, this.errorTitleText, @@ -224,6 +520,34 @@ Sonia.repository.ImportWindow = Ext.extend(Ext.Window,{ ); } }); + }, + + enableAvailableImportTypes: function(){ + var type = null; + Ext.each(state.repositoryTypes, function(repositoryType){ + if (repositoryType.name === this.repositoryType){ + type = repositoryType; + } + }, this); + + if ( type !== null ){ + Ext.getCmp('chooseImportType').setValue(null); + Ext.getCmp('move-next').setDisabled(true); + Ext.getCmp('move-prev').setDisabled(false); + Ext.getCmp('importTypeURL').setDisabled(type.supportedCommands.indexOf('PULL') < 0); + Ext.getCmp('importTypeFile').setDisabled(type.supportedCommands.indexOf('UNBUNDLE') < 0); + } + }, + + applyChanges: function(){ + var panel = Ext.getCmp('repositories'); + if (panel){ + panel.getGrid().reload(); + } + this.fireEvent('finish'); } -}); \ No newline at end of file +}); + +// register xtype +Ext.reg('scmRepositoryImportWizard', Sonia.repository.ImportPanel); \ No newline at end of file diff --git a/scm-webapp/src/main/webapp/resources/js/util/sonia.util.tip.js b/scm-webapp/src/main/webapp/resources/js/util/sonia.util.tip.js new file mode 100644 index 0000000000..18cadb9884 --- /dev/null +++ b/scm-webapp/src/main/webapp/resources/js/util/sonia.util.tip.js @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +Sonia.util.Tip = Ext.extend(Ext.BoxComponent, { + + tpl: new Ext.XTemplate('\ +
\ +
\ +
\ +
\ +
\ + \ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ + {content}\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
'), + + constructor: function(config) { + config = config || {}; + var cl = 'scm-tip'; + if (config['class']){ + cl += ' ' + config['class']; + } + config.xtype = 'box'; + this.html = this.tpl.apply({content: config.content}); + Sonia.util.Tip.superclass.constructor.apply(this, arguments); + } + +}); + +// register xtype +Ext.reg('scmTip', Sonia.util.Tip); \ No newline at end of file