Commit eb5c9ab2 authored by Bas Lijnse's avatar Bas Lijnse

Merge branch 'master' into eliminate-sass

parents 524e3075 386ba915
Pipeline #29512 passed with stage
in 4 minutes and 42 seconds
......@@ -2,6 +2,7 @@ implementation module iTasks.Engine
import Data.Func
import Data.Functor
import Data.List
import Data.Queue
import Internet.HTTP
import StdEnv
......@@ -53,16 +54,24 @@ doTasksWithOptions initFun startable world
# (Right iworld) = mbIWorld
# (symbolsResult, iworld) = initSymbolsShare options.distributed options.appName iworld
| symbolsResult =: (Error _) = show ["Error reading symbols while required: " +++ fromError symbolsResult] (setReturnCode 1 (destroyIWorld iworld))
# iworld = if (hasDup requestPaths)
(iShow ["Warning: duplicate paths in the web tasks: " +++ join ", " ["'" +++ p +++ "'"\\p<-requestPaths]] iworld)
iworld
# iworld = serve (startupTasks options) (tcpTasks options.serverPort options.keepaliveTime) (timeout options.timeout) iworld
= destroyIWorld iworld
where
webTasks = [t \\ WebTask t <- toStartable startable]
requestPaths = [path\\{path}<-webTasks]
webTasks = [t \\ WebTask t <- toStartable startable]
startupTasks {distributed, sdsPort}
= if webTasks=:[]
//if there are no webtasks: stop when stable
[systemTask (startTask stopOnStable)]
//if there are: show instructions andcleanup old sessions
[startTask viewWebServerInstructions
,systemTask (startTask removeOutdatedSessions)]
//If distributed, start sds service task
= (if distributed [systemTask (startTask (sdsServiceTask sdsPort))] [])
++ (if distributed [systemTask (startTask (sdsServiceTask sdsPort))] [])
++ [systemTask (startTask flushWritesWhenIdle)
//If there no webtasks, stop when stable, otherwise cleanup old sessions
,systemTask (startTask if (webTasks =: []) stopOnStable removeOutdatedSessions)
//Start all startup tasks
:[t \\ StartupTask t <- toStartable startable]]
......@@ -161,17 +170,11 @@ where
instance Startable (Task a) | iTask a //Default as web task
where
toStartable task =
[onStartup viewWebServerInstructions
,onRequest "/" task
]
toStartable task = [onRequest "/" task]
instance Startable (HTTPRequest -> Task a) | iTask a //As web task
where
toStartable task =
[onStartup viewWebServerInstructions
,onRequestFromRequest "/" task
]
toStartable task = [onRequestFromRequest "/" task]
instance Startable StartableTask
where
......
......@@ -48,8 +48,6 @@ usersWithRole :: !Role -> SDSLens () [User] ()
* @param Password: The password
*
* @return A single user who matches the given credentials, or nothing of none or more than one exists.
* @gin-icon key
*/
authenticateUser :: !Username !Password -> Task (Maybe User)
......@@ -57,8 +55,6 @@ authenticateUser :: !Username !Password -> Task (Maybe User)
* Wraps a task with an authentication task
*
* @param the task to wrap
*
* @gin-icon key
*/
doAuthenticated :: (Task a) -> Task a | iTask a
......@@ -71,8 +67,6 @@ doAuthenticatedWith :: !(Credentials -> Task (Maybe User)) (Task a) -> Task a |
* @param User details: The user-information which needs to be stored
*
* @return The stored user
*
* @gin-icon user_add
*/
createUser :: !UserAccount -> Task StoredUserAccount
/**
......@@ -81,8 +75,6 @@ createUser :: !UserAccount -> Task StoredUserAccount
* @param User: The user who needs to be deleted
*
* @return The deleted user
*
* @gin-icon user_delete
*/
deleteUser :: !UserId -> Task ()
/**
......
......@@ -105,8 +105,6 @@ manageWorkOfCurrentUser :: !(Maybe HtmlTag) -> Task ()
*
* @param Workflow: The workflow to add
* @return The description of the added workflow
*
* @gin False
*/
addWorkflows :: ![Workflow] -> Task [Workflow]
......
......@@ -11,8 +11,6 @@ from System.FilePath import :: FilePath
*
* @return The imported content
* @throws FileException
*
* @gin-icon page_white_csv
*/
importCSVFile :: !FilePath -> Task [[String]]
importCSVDocument :: !Document -> Task [[String]]
......@@ -27,8 +25,6 @@ importCSVDocument :: !Document -> Task [[String]]
*
* @return The imported content
* @throws FileException
*
* @gin False
*/
importCSVFileWith :: !Char !Char !Char !FilePath -> Task [[String]]
importCSVDocumentWith :: !Char !Char !Char !Document -> Task [[String]]
......@@ -40,8 +36,6 @@ importCSVDocumentWith :: !Char !Char !Char !Document -> Task [[String]]
* @param Cells: The content to export as a list of rows of lists of fields
*
* @return The exported content as a document
*
* @gin-icon page_white_csv
*/
createCSVFile :: !String ![[String]] -> Task Document
/**
......@@ -52,8 +46,6 @@ createCSVFile :: !String ![[String]] -> Task Document
*
* @return The exported content
* @throws FileException
*
* @gin-icon page_white_csv
*/
exportCSVFile :: !FilePath ![[String]] -> Task [[String]]
/**
......@@ -68,7 +60,5 @@ exportCSVFile :: !FilePath ![[String]] -> Task [[String]]
*
* @return The exported content
* @throws FileException
*
* @gin False
*/
exportCSVFileWith :: !Char !Char !Char !FilePath ![[String]] -> Task [[String]]
......@@ -36,8 +36,6 @@ derive class iTask FileException
*
* @return The imported document
* @throws FileException
*
* @gin-icon page_white
*/
importDocument :: !FilePath -> Task Document
......@@ -49,8 +47,6 @@ importDocument :: !FilePath -> Task Document
*
* @return The exported document
* @throws FileException
*
* @gin-icon page_white
*/
exportDocument :: !FilePath !Document -> Task Document
......@@ -14,8 +14,6 @@ instance toString JSONParseException
*
* @return The imported content
* @throws FileException
*
* @gin-icon page_white_json
*/
importJSONFile :: !FilePath -> Task a | iTask a
importJSONDocument :: !Document -> Task a | iTask a
......@@ -28,8 +26,6 @@ importJSONDocument :: !Document -> Task a | iTask a
*
* @return The imported content
* @throws FileException
*
* @gin False
*/
importJSONFileWith :: !(JSONNode -> Maybe a) !FilePath -> Task a | iTask a
/**
......@@ -48,8 +44,6 @@ createJSONFile :: !String a -> Task Document | iTask a
* @param Value: The content to encode as JSON using the generic JSON encoder
*
* @return The exported content
*
* @gin-icon page_white_json
*/
exportJSONFile :: !FilePath a -> Task a | iTask a
/**
......@@ -61,7 +55,5 @@ exportJSONFile :: !FilePath a -> Task a | iTask a
*
* @return The exported content
* @throws FileException
*
* @gin False
*/
exportJSONFileWith :: !(a -> JSONNode) !FilePath a -> Task a | iTask a
......@@ -35,9 +35,6 @@ instance toString CallException
* @param Run with pseudo terminal options
* @return return-code of the process
* @throws CallException
*
* @gin-title Start executable
* @gin-icon executable
*/
callProcess :: ![ViewOption ProcessInformation] !FilePath ![String] !(Maybe FilePath) (Maybe ProcessPtyOptions) -> Task ProcessInformation
......
......@@ -11,8 +11,6 @@ from iTasks.Extensions.Document import :: Document
*
* @return The imported content
* @throws FileException
*
* @gin-icon page_white_text
*/
importTextFile :: !FilePath -> Task String
......@@ -34,7 +32,5 @@ importTextDocument :: !Document -> Task String
*
* @return The exported content
* @throws FileException
*
* @gin-icon page_white_text
*/
exportTextFile :: !FilePath !String -> Task String
......@@ -98,8 +98,6 @@ workAs :: !User !(Task a) -> Task a | iTask a
* @param Task: The task that is to be delegated
*
* @return The combined task
*
* @gin False
*/
assign :: !TaskAttributes !(Task a) -> Task a | iTask a
......@@ -110,10 +108,6 @@ assign :: !TaskAttributes !(Task a) -> Task a | iTask a
* @param Task: The task that is to be delegated.
*
* @return The combined task
*
* @gin-title Assign to user
* @gin-icon user
* @gin-shape assign
*/
(@:) infix 3 :: !worker !(Task a) -> Task a | iTask a & toUserConstraint worker
......
definition module iTasks.Extensions.Web
import iTasks
from Internet.HTTP import :: HTTPMethod, :: HTTPRequest, :: HTTPResponse
from Text.URI import :: URI
from Text.HTML import class html
/**
* This module provides support for building web applications.
*/
* This module provides support for building web applications.
*/
//* Uniform resource locators
:: URL = URL !String
......@@ -19,24 +21,32 @@ derive JSONDecode URL
derive gDefault URL
derive gEq URL
//Simple web server task
//* Simple web server task
serveWebService :: Int (HTTPRequest -> Task HTTPResponse) -> Task ()
//Task for serving a static file
//* Task for serving a static file
serveFile :: [FilePath] HTTPRequest -> Task HTTPResponse
/**
* Calls an external HTTP webservice.
*
* @param HTTP Method: the HTTP method (GET or POST) to use
* @param URL: The URL of the webservice
* @param Parameters: A list of name/value pairs
* @param Response handler: A parse function that parses the response
*
* @return The parsedd value
*
* @gin-title Call web service
* @gin-icon webservice
*/
* Calls an external HTTP webservice.
*
* @param HTTP Method: the HTTP method (GET or POST) to use
* @param URL: The URL of the webservice
* @param Data: The body of the request
* @param Response handler: A parse function that parses the response
*
* @return The parsed value
*/
callHTTP :: !HTTPMethod !URI !String !(HTTPResponse -> (MaybeErrorString a)) -> Task a | iTask a
/**
* Calls an external HTTP webservice.
*
* @param HTTP Method: the HTTP method (GET or POST) to use
* @param URL: The URL of the webservice
* @param Parameters: A list of name/value pairs
* @param Response handler: A parse function that parses the response
*
* @return The parsed value
*/
callRPCHTTP :: !HTTPMethod !URI ![(String,String)] !(HTTPResponse -> a) -> Task a | iTask a
implementation module iTasks.Extensions.Web
import iTasks
import iTasks.UI.Editor.Controls, iTasks.UI.Editor.Modifiers
import Internet.HTTP, Text, Text.HTML, Text.URI, Text.Encodings.MIME, Text.Encodings.UrlEncoding, StdArray, Data.Either
......@@ -14,7 +15,6 @@ import qualified Data.Map as DM
import Data.Map.GenJSON
import qualified Data.List as DL
//* URL
gText{|URL|} _ val = [maybe "" toString val]
gEditor{|URL|} = selectByMode
......@@ -215,5 +215,3 @@ callHTTP _ url _ _
callRPCHTTP :: !HTTPMethod !URI ![(String,String)] !(HTTPResponse -> a) -> Task a | iTask a
callRPCHTTP method url params transformResult
= callHTTP method url (urlEncodePairs params) (Ok o transformResult)
......@@ -55,12 +55,6 @@ derive gDefault TIMeta
= TIValue !(TaskValue DeferredJSON)
| TIException !Dynamic !String
// UI State
:: TIUIState
= UIDisabled //The UI is disabled (e.g. when nobody is viewing the task)
| UIEnabled !Int !UIChange //The UI is enabled, a version number and the previous task rep are stored for comparision //FIXME
| UIException !String //An unhandled exception occurred and the UI should only show the error message
:: AsyncAction = Read | Write | Modify
:: DeferredJSON
......
......@@ -34,10 +34,10 @@ from Control.Applicative import class Alternative(<|>)
import Data.GenEq
//Derives required for storage of UI definitions
derive JSONEncode TaskOutputMessage, TaskResult, TaskEvalInfo, TIValue, ParallelTaskState, ParallelTaskChange, TIUIState
derive JSONEncode TaskOutputMessage, TaskResult, TaskEvalInfo, TIValue, ParallelTaskState, ParallelTaskChange
derive JSONEncode Queue, Event
derive JSONDecode TaskOutputMessage, TaskResult, TaskEvalInfo, TIValue, ParallelTaskState, ParallelTaskChange, TIUIState
derive JSONDecode TaskOutputMessage, TaskResult, TaskEvalInfo, TIValue, ParallelTaskState, ParallelTaskChange
derive JSONDecode Queue, Event
derive gDefault InstanceFilter
......
......@@ -7,7 +7,6 @@ from Internet.HTTP import :: HTTPRequest, :: HTTPResponse
from iTasks.Engine import :: WebTask
from iTasks.Internal.IWorld import :: IWorld
from iTasks.Internal.Task import :: Task, :: ConnectionTask
from iTasks.Internal.TaskState import :: TIUIState
from iTasks.Internal.TaskStore import :: TaskOutput, :: TaskOutputMessage
import iTasks.SDS.Definition
from iTasks.UI.Definition import :: UIChange
......
......@@ -97,6 +97,7 @@ derive class iTask UIChange, UIAttributeChange, UIChildChange
| UIChoiceList // - A mutually exclusive set of radio buttons
| UIGrid // - Grid (selecting an item in a table)
| UITree // - Tree (selecting a node in a tree structure)
| UITabBar // - A tab bar (to make a selection with)
// Data elements (implemented in itasks-core.js)
| UIData
......
......@@ -231,6 +231,7 @@ where
toString UIChoiceList = "ChoiceList"
toString UIGrid = "Grid"
toString UITree = "Tree"
toString UITabBar = "TabBar"
toString UIContainer = "Container"
toString UIPanel = "Panel"
......
......@@ -194,7 +194,7 @@
.itasks-tabset > * {
align-self: stretch;
}
.itasks-tabset .itasks-tabbar {
.itasks-tabbar {
text-align: left;
list-style: none;
margin: 0;
......@@ -206,7 +206,7 @@
background: linear-gradient(var(--panel-header-base-color-lighter2), var(--panel-header-base-color));
overflow: hidden;
}
.itasks-tabset .itasks-tabbar li {
.itasks-tabbar li {
margin: 0;
padding: 0 10px;
border: 1px solid var(--tab-border-color);
......@@ -218,12 +218,12 @@
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}
.itasks-tabset .itasks-tabbar li a {
.itasks-tabbar li a {
color: var(--tab-text-color);
text-decoration: none;
}
.itasks-tabset .itasks-tabbar li:before,
.itasks-tabset .itasks-tabbar li:after {
.itasks-tabbar li:before,
.itasks-tabbar li:after {
position: absolute;
bottom: -1px;
width: 5px;
......@@ -231,31 +231,31 @@
content: " ";
border: 1px solid var(--tab-border-color);
}
.itasks-tabset .itasks-tabbar li:before {
.itasks-tabbar li:before {
left: -6px;
border-bottom-right-radius: 5px;
border-width: 0 1px 1px 0;
box-shadow: 2px 2px 0 var(--tab-base-color-darker);
}
.itasks-tabset .itasks-tabbar li:after {
.itasks-tabbar li:after {
right: -6px;
border-bottom-left-radius: 5px;
border-width: 0 0 1px 1px;
box-shadow: -2px 2px 0 var(--tab-base-color-darker);
}
.itasks-tabset .itasks-tabbar li.itasks-selected {
.itasks-tabbar li.itasks-selected {
background: var(--tab-base-color);
color: var(--tab-text-color);
z-index: 2;
border-bottom-color: var(--tab-base-color);
}
.itasks-tabset .itasks-tabbar li.itasks-selected:before {
.itasks-tabbar li.itasks-selected:before {
box-shadow: 2px 2px 0 var(--tab-base-color);
}
.itasks-tabset .itasks-tabbar li.itasks-selected:after {
.itasks-tabbar li.itasks-selected:after {
box-shadow: -2px 2px 0 var(--tab-base-color);
}
.itasks-tabset .itasks-tabbar li a.itasks-tabclose {
.itasks-tabbar li a.itasks-tabclose {
color: #aaa;
display: inline-block;
position: relative;
......@@ -270,7 +270,7 @@
padding: 0 2px;
}
.itasks-tabset .itasks-tabbar:after {
.itasks-tabbar:after {
position: absolute;
content: "";
width: 100%;
......@@ -279,11 +279,11 @@
border-bottom: 1px solid var(--tab-border-color);
z-index: 1;
}
.itasks-tabset .itasks-tabbar:before {
.itasks-tabbar:before {
z-index: 1;
}
.itasks-tabset .itasks-tabicon {
.itasks-tabicon {
width: 16px;
height: 16px;
display: inline-block;
......
......@@ -226,6 +226,34 @@
color: var(--select-text-color);
}
.itasks-progress {
height: 1.5em;
width: 100%;
background-color: #c9c9c9;
position: relative;
}
.itasks-progress:before {
content: attr(title);
font-size: 0.8em;
position: absolute;
text-align: center;
height: 80%;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.itasks-progress .value {
background-color: #7cc4ff;
display: inline-block;
height: 100%;
width: 100%;
}
*.itasks-viewport [data-tooltip] {
position: relative;
display: inline-block;
......
......@@ -118,6 +118,12 @@ grid :: Editor (ChoiceGrid, [Int])
* Supported attributes:
*/
tree :: Editor ([ChoiceNode], [Int])
/**
* A horizontal bar with tabs to make a selection with
*/
tabBar :: Editor ([ChoiceText], [Int])
/**
* Modifies the above editors for making choices such that they use a constant set of choices.
*/
......
......@@ -77,6 +77,9 @@ checkGroup = choiceComponent (const 'DM'.newMap) id toOptionText checkBoundsText
choiceList :: Editor ([ChoiceText], [Int])
choiceList = choiceComponent (const 'DM'.newMap) id toOptionText checkBoundsText UIChoiceList
tabBar :: Editor ([ChoiceText], [Int])
tabBar = choiceComponent (const 'DM'.newMap) id toOptionText checkBoundsText UITabBar
toOptionText {ChoiceText|id,text}= JSONObject [("id",JSONInt id),("text",JSONString text)]
checkBoundsText options idx = or [id == idx \\ {ChoiceText|id} <- options]
......@@ -206,19 +209,23 @@ where
onRefresh dp (newVal, newSel) (mbOldVal, oldSel, multiple) vst
//Check options
# oldOpts = mbValToOptions mbOldVal
# newOpts = mbValToOptions $ Just newVal
# cOptions = if (newOpts =!= oldOpts)
(ChangeUI [SetAttribute "options" (JSONArray newOpts)] [])
# oldOptsJson = mbValToOptions mbOldVal
# newOpts = getOptions newVal
# newOptsJson = toOption <$> newOpts
# cOptions = if (newOptsJson =!= oldOptsJson)
(ChangeUI [SetAttribute "options" (JSONArray newOptsJson)] [])
NoChange
//Check selection
//Check selection, if the selection is out of bounds assume the empty selection
# newSel = if (all (checkBounds newOpts) newSel) newSel []
# cSel = if (newSel =!= oldSel) (ChangeUI [SetAttribute "value" (toJSON newSel)] []) NoChange
= (Ok (mergeUIChanges cOptions cSel, (Just newVal, newSel, multiple)),vst)
valueFromState (Just val, sel, multiple)
//The selection is only allowed to be empty when multiselect is enabled
| not multiple && isEmpty sel = Nothing
| otherwise = Just (val, sel)
| not multiple && lengthSel <> 0 && lengthSel <> 1 = Nothing
| otherwise = Just (val, sel)
where
lengthSel = length sel
valueFromState _ = Nothing
mbValToOptions mbVal = toOption <$> maybe [] getOptions mbVal
......@@ -191,6 +191,11 @@ jsDocument :== jsGlobal "document"
*/
jsWrapFun :: !({!JSVal} *JSWorld -> *JSWorld) !JSVal !*JSWorld -> *(!JSFun, !*JSWorld)
/**
* Like {{`jsWrapFun`}}, but the Clean function can return a result.
*/
jsWrapFunWithResult :: !({!JSVal} *JSWorld -> *(JSVal, *JSWorld)) !JSVal !*JSWorld -> *(!JSFun, !*JSWorld)
/**
* Wrap a function receiving a reference to a JavaScript iTasks component to
* one matching the calling convention for the JavaScript interface (i.e.,
......@@ -225,7 +230,16 @@ addJSFromUrl :: !String !(Maybe JSFun) !*JSWorld -> *JSWorld
/**
* A simple wrapper around JavaScript's `console.log`.
* Use {{`jsTraceVal`}} to trace JavaScript values.
* @param The value to log.
* @param The value to return.
*/
jsTrace :: !a .b -> .b | toString a
/**
* A simple wrapper around JavaScript's `console.log`.
* Use {{`jsTrace`}} to trace Clean values.
* @param The value to log.
* @param The value to return.
*/
jsTraceVal :: !JSVal .a -> .a
......@@ -532,6 +532,13 @@ jsGlobal s = JSVar s
jsWrapFun :: !({!JSVal} *JSWorld -> *JSWorld) !JSVal !*JSWorld -> *(!JSFun, !*JSWorld)
jsWrapFun f attach_to world = (share attach_to \(JSArray args) w -> f args w, world)
jsWrapFunWithResult :: !({!JSVal} *JSWorld -> *(JSVal, *JSWorld)) !JSVal !*JSWorld -> *(!JSFun, !*JSWorld)
jsWrapFunWithResult f attach_to world = (share attach_to fun, world)
where
fun (JSArray args) w
# (r,w) = f args w
= hyperstrict (js_val_to_string r,w)
wrapInitUIFunction :: !(JSVal *JSWorld -> *JSWorld) -> {!JSVal} -> *JSWorld -> *JSWorld
wrapInitUIFunction f = init
where
......@@ -608,9 +615,12 @@ where
}
jsTrace :: !a .b -> .b | toString a
jsTrace s x = case eval_js (js_val_to_string (JSCall (JSVar "console.log") {JSString (toString s)})) of
jsTrace s x = jsTraceVal (JSString (toString s)) x
jsTraceVal :: !JSVal .a -> .a
jsTraceVal v x = case eval_js (js_val_to_string (JSCall (JSVar "console.log") {v})) of
True -> x
False -> abort_with_node s // just in case it is a JSVal
False -> abort_with_node v
set_js :: !*String !*String -> Bool
set_js var val = code {
......
......@@ -38,7 +38,7 @@ instance tune UIAttributes Editor
where
tune extra editor=:{Editor|genUI=editorGenUI} = {Editor|editor & genUI = genUI}
where
genUI attr dp mode vst = editorGenUI ('DM'.union attr extra) dp (mapEditMode id mode) vst
genUI attr dp mode vst = editorGenUI ('DM'.union extra attr) dp (mapEditMode id mode) vst
instance tune UIAttribute Task
where
......
......@@ -125,4 +125,31 @@ const ABC_loading_promise=ABCInterpreter.instantiate({
}).then(function(instance){
ABC=instance;
ABC.initialized=false;
// Overwrite ap to return a result (in the case of jsWrapFunWithResult)
ABC.ap=function(index){
var f=function () {
var args=[];
for (var i=0; i<arguments.length; i++)
args[i]=arguments[i];
ABC.interpret(new SharedCleanValue(index), args);
var result=undefined;
const new_asp=ABC.interpreter.instance.exports.get_asp();
const hp_ptr=ABC.memory_array[new_asp/4];
if (ABC.memory_array[hp_ptr/4]!=25*8+2) { // INT, i.e. JSWorld
// Assume we have received a tuple with the first element as the result
const str_ptr=ABC.memory_array[hp_ptr/4+2];
const string=ABC.get_clean_string(ABC.memory_array[str_ptr/4+2], false);
if (ABC_DEBUG)
console.log('result:',string);
result=eval('('+string+')');
}
if (typeof result!='undefined')
return result;
};
f.shared_clean_value_index=index;
return f;
};
});
......@@ -140,12 +140,6 @@ itasks.TabSet = {
},
replacing: false,
initComponent: function() {
var me = this;
me.children.forEach(function(child,i) {
child.selected = (i == me.activeTab);
});
},
initDOMEl: function() {
var me = this,
el = me.domEl;
......@@ -155,7 +149,7 @@ itasks.TabSet = {
//Create tabs for the initial children
me.children.forEach(function(child,i) {
me.tabBar.appendChild(me.createTabEl(child));
me.tabBar.appendChild(me.createTabEl(child, i == me.activeTab));
});
me.domEl.appendChild(me.tabBar);
......@@ -164,7 +158,7 @@ itasks.TabSet = {
me.containerEl.classList.add(me.cssPrefix + 'tabitems');
me.domEl.appendChild(me.containerEl);
},
createTabEl: function (cmp) {
createTabEl: function (cmp, selected) {
var me = this, tab, label, icon;