Commit 063c7a9f authored by Bas Lijnse's avatar Bas Lijnse

Merge branch...

Merge branch '292-open-action-in-basicapiexamples-opens-new-tab-when-task-is-already-open' into 'master'

Resolve ""open" action in BasicAPIExamples opens new tab when task is already open"

Closes #292

See merge request !303
parents 81322d4c 550f0e10
Pipeline #29101 passed with stage
in 4 minutes and 41 seconds
......@@ -205,7 +205,7 @@ where
hasName name attributes = maybe False ((==) (JSONString name)) ('DM'.get "name" attributes)
startTask _ = appendTask (NamedDetached identity defaultValue True) (removeWhenStable (task @! ())) topLevelTasks @! ()
startTask _ = appendTask (Detached ('DM'.singleton "name" (JSONString identity)) True) (removeWhenStable (task @! ())) topLevelTasks @! ()
stopTask (Just (taskId,_)) = removeTask taskId topLevelTasks @! ()
removeWhenStable t l = t >>* [OnValue (ifStable (\_ -> get (taskListSelfId l) >>- \id -> removeTask id l @? const NoValue))]
......
......@@ -13,10 +13,8 @@ addOnceToWorkspace :: String (Task a) Workspace -> Task () | iTask a
addOnceToWorkspace identity task workspace
= get (taskListMeta workspace)
>>- \items -> case find identity items of
Nothing = appendTask (NamedEmbedded identity) (removeWhenStable task) workspace
>>- \taskId ->
focusTask taskId workspace @! ()
(Just taskId) = focusTask taskId workspace @! ()
Nothing = appendTask Embedded (\l -> (removeWhenStable task l <<@ ("name", JSONString identity))) workspace @! ()
_ = return ()
where
find identity [] = Nothing
find identity [p=:{TaskListItem|taskId,attributes}:ps]
......
......@@ -13,10 +13,8 @@ addOnceToWorkspace :: String (Task a) Workspace -> Task () | iTask a
addOnceToWorkspace identity task workspace
= get (taskListMeta workspace)
>>- \items -> case find identity items of
Nothing = appendTask (NamedEmbedded identity) (removeWhenStable task) workspace
>>- \taskId ->
focusTask taskId workspace @! ()
(Just taskId) = focusTask taskId workspace @! ()
Nothing = appendTask Embedded (\l -> (removeWhenStable task l <<@ ("name", JSONString identity))) workspace @! ()
_ = return ()
find identity [] = Nothing
find identity [p=:{TaskListItem|taskId,attributes}:ps]
......
......@@ -328,16 +328,21 @@ where
appendOnce :: TaskId (Task a) (SharedTaskList a) -> Task () | iTask a
appendOnce identity task slist
= get (taskListMeta slist)
>>- \items -> if (checkItems name items)
(return ())
(appendTask (NamedEmbedded name) (removeWhenStable task) slist @! ())
= get (taskListMeta slist)
>>- \items -> if (checkItems name items)
(upd (bringToFront name) (taskListMeta slist) @! ())
(appendTask Embedded (removeWhenStable (task <<@ ("name", JSONString name) <<@ ("order", JSONInt (maxOrder items + 1)))) slist @! ())
where
name = toString identity
maxOrder items = foldr max 0 [maybe 0 (\(JSONInt i) -> i) ('DM'.get "order" attributes) \\ {TaskListItem|attributes} <- items]
hasName name {TaskListItem|attributes} = maybe False ((==) (JSONString name)) ('DM'.get "name" attributes)
checkItems name [] = False
checkItems name [{TaskListItem|attributes}:is]
| maybe False ((==) (JSONString name)) ('DM'.get "name" attributes) = True //Item with name exists!
= checkItems name is
checkItems name [i:is] = hasName name i || checkItems name is
bringToFront name items =
[(taskId, if (hasName name i) ('DM'.singleton "order" (JSONInt (maxOrder items + 1))) 'DM'.newMap)
\\ i=:{TaskListItem|taskId} <- items]
removeWhenStable :: (Task a) (SharedTaskList a) -> Task a | iTask a
removeWhenStable task slist
......
......@@ -75,20 +75,19 @@ derive gEq DeferredJSON
derive gText DeferredJSON
:: ParallelTaskState =
{ taskId :: !TaskId //Identification
, index :: !Int //Explicit index (when shares filter the list, you want to keep access to the index in the full list)
, detached :: !Bool
, attributes :: !TaskAttributes
, value :: !TaskValue DeferredJSON //Value (only for embedded tasks)
, createdAt :: !TaskTime //Time the entry was added to the set (used by layouts to highlight new items)
, lastFocus :: !Maybe TaskTime //Time the entry was last explicitly focused
, lastEvent :: !TaskTime //Last modified time
, change :: !Maybe ParallelTaskChange //Changes like removing or replacing a parallel task are only done when the
//parallel is evaluated. This field is used to schedule such changes.
{ taskId :: !TaskId //Identification
, index :: !Int //Explicit index (when shares filter the list, you want to keep access to the index in the full list)
, detached :: !Bool
, implicitAttributes :: !TaskAttributes //Attributes that reflect the latest attributes from the task UI
, explicitAttributes :: !Map String (!JSONNode,!Bool) //Attributes that are explicitly written to the list and shadow the implicit attributes
, value :: !TaskValue DeferredJSON //Value (only for embedded tasks)
, createdAt :: !TaskTime //Time the entry was added to the set (used by layouts to highlight new items)
, lastEvent :: !TaskTime //Last modified time
, change :: !Maybe ParallelTaskChange //Changes like removing or replacing a parallel task are only done when the
//parallel is evaluated. This field is used to schedule such changes.
, initialized :: !Bool
}
:: ParallelTaskChange
= RemoveParallelTask //Mark for removal from the set on the next evaluation
| ReplaceParallelTask !Dynamic //Replace the task on the next evaluation
......@@ -24,6 +24,7 @@ import iTasks.WF.Combinators.Common
import iTasks.WF.Derives
import iTasks.Extensions.Document
from Data.Map import instance Functor (Map k)
import qualified Data.Map as DM
import Data.Map.GenJSON
import qualified Data.Set as DS
......@@ -442,7 +443,7 @@ where
//In this SDS the include value and include attributes flags are used to indicate what is written for notification
//During a read the whole ParallelTaskState record is used
param (listId,taskId,includeValue)
= (listId,{TaskListFilter|onlyIndex=Nothing,onlyTaskId=Just [taskId],onlySelf=False,includeValue=includeValue,includeAttributes=False,includeProgress=False})
= (listId,{TaskListFilter|onlyIndex=Nothing,onlyTaskId=Just [taskId],onlySelf=False,includeValue=includeValue,includeAttributes=True,includeProgress=False})
read p=:(listId,taskId,_) [] = Error (exception ("Could not find parallel task " <+++ taskId <+++ " in list " <+++ listId))
read p=:(_,taskId,_) [x:xs] = if (x.ParallelTaskState.taskId == taskId) (Ok x) (read p xs)
write (_,taskId,_) list pts = Ok (Just [if (x.ParallelTaskState.taskId == taskId) pts x \\ x <- list])
......@@ -478,22 +479,22 @@ where
where
items = [{TaskListItem|taskId = taskId, listId = listId
, detached = detached, self = taskId == selfId
, value = decode value, progress = Nothing, attributes = attributes
} \\ {ParallelTaskState|taskId,detached,attributes,value,change} <- states | change =!= Just RemoveParallelTask]
, value = decode value, progress = Nothing, attributes = 'DM'.union (fmap fst explicitAttributes) implicitAttributes
} \\ {ParallelTaskState|taskId,detached,implicitAttributes,explicitAttributes,value,change} <- states | change =!= Just RemoveParallelTask]
decode NoValue = NoValue
decode (Value json stable) = maybe NoValue (\v -> Value v stable) (fromDeferredJSON json)
write (listId,selfId,listFilter) _ [] = Ok Nothing
write (listId,selfId,{TaskListFilter|includeAttributes=False}) _ _ = Ok Nothing
write (listId,selfId,listFilter) states [] = Ok (Just states)
write (listId,selfId,listFilter) states [(t,a):updates]
# states = [if (taskId == t) {ParallelTaskState|pts & attributes = a} pts \\ pts=:{ParallelTaskState|taskId} <- states]
= write (listId,selfId,listFilter) states updates
# states = [if (taskId == t) {ParallelTaskState|pts & explicitAttributes = fmap (\x -> (x,True)) a} pts \\ pts=:{ParallelTaskState|taskId} <- states]
= (write (listId,selfId,listFilter) states updates)
notify (listId,_,_) states ts (regListId,_,_) = regListId == listId //Only check list id, the listFilter is checked one level up
lensReducer (listId, selfId, listFilter) ws
= (Ok ([(taskId, attributes) \\ {ParallelTaskState|taskId,detached,attributes,value,change} <- ws | change =!= Just RemoveParallelTask]))
= (Ok ([(taskId, fmap fst explicitAttributes) \\ {ParallelTaskState|taskId,detached,explicitAttributes,value,change} <- ws | change =!= Just RemoveParallelTask]))
param2 _ (listId,items) = {InstanceFilter|onlyInstanceNo=Just [instanceNo \\ {TaskListItem|taskId=(TaskId instanceNo _),detached} <- items | detached],notInstanceNo=Nothing
,includeSessions=True,includeDetached=True,includeStartup=True,matchAttribute=Nothing, includeConstants = False, includeAttributes = True,includeProgress = True}
......
......@@ -242,6 +242,7 @@ itasks.TabSet = {
}
me.activeTab = idx || 0;
//Select new tab
if(me.children[me.activeTab]) {
me.children[me.activeTab].domEl.classList.add(me.cssPrefix + 'selected');
......@@ -249,6 +250,20 @@ itasks.TabSet = {
me.children[me.activeTab].onShow();
}
},
setActiveTabBasedOnOrder: function() {
var me = this,
maxOrder = 0,
maxIndex = 0;
me.children.forEach(function(child,n) {
var order = child.attributes["order"] || 0;
if(order > maxOrder) {
maxOrder = order;
maxIndex = n;
}
});
me.setActiveTab(maxIndex);
},
beforeChildInsert: function(idx,spec) {
var me = this;
......@@ -273,19 +288,29 @@ itasks.TabSet = {
} else {
me.tabBar.insertBefore(tabEl,me.tabBar.children[idx]);
}
me.setActiveTabBasedOnOrder();
}
},
afterChildRemove: function(idx) {
var me = this;
if(me.initialized && !me.replacing) {
if(me.replacing || me.children.length == 1) { //Automatically select the first tab
me.setActiveTab(idx);
me.tabBar.removeChild(me.tabBar.children[idx]);
//If we removed the currently active tab, we need to select another one
if(idx == me.activeTab) {
me.setActiveTabBasedOnOrder();
} else if(idx < me.activeTab) {
//When a tab before the active tab is removed, it implies that the active index
//is now one less
me.activeTab--;
}
}
},
beforeChildRemove: function(idx) {
afterChildChange: function(idx,change) {
var me = this;
if(me.initialized) {
if(!me.replacing && (idx == me.activeTab) && (me.children.length > 1)) { //Unless we remove the last tab, select another tab
me.setActiveTab( (idx == 0) ? 1 : (idx - 1));
}
me.tabBar.removeChild(me.tabBar.children[idx]);
if(change["attributes"]) {
me.setActiveTabBasedOnOrder();
}
},
replaceChild: function(idx,spec) {
......
......@@ -234,6 +234,7 @@ itasks.Component = {
me.containerEl.removeChild(me.containerEl.childNodes[idx]);
}
me.children.splice(idx,1);
me.afterChildRemove(idx);
},
replaceChild: function(idx,spec) {
var me = this;
......@@ -257,6 +258,7 @@ itasks.Component = {
me.children.splice(didx, 0, child);
},
beforeChildRemove: function(idx,child) {},
afterChildRemove: function(idx) {},
/* beforeRemove can be overwritten to add a handler for 'destroy' events.
* _beforeRemove is internal and should not be overwritten.
*/
......@@ -281,6 +283,7 @@ itasks.Component = {
me.onAttributeChange(name,value);
},
onAttributeChange: function(name,value) {},
onUIChange: function(change) {
var me = this;
me.world=me.world.then (function(){
......@@ -317,7 +320,11 @@ itasks.Component = {
switch(change[1]) {
case 'change':
if(idx >= 0 && idx < me.children.length) {
return me.children[idx].onUIChange(change[2]);
me.children[idx].onUIChange(change[2]);
me.world = me.world.then(function () {
me.afterChildChange(idx,change[2]);
});
return;
} else {
console.log("UNKNOWN CHILD",idx,me.children.length,change);
}
......@@ -334,6 +341,7 @@ itasks.Component = {
}), Promise.resolve());
}
},
afterChildChange: function(idx,change) {},
onShow: function() {
this.children.forEach(function(child) { if(child.onShow) {child.onShow();}});
},
......
......@@ -33,10 +33,8 @@ ActionRefresh :== Action "Refresh"
ActionClose :== Action "Close"
:: ParallelTaskType
= Embedded //Simplest embedded
| NamedEmbedded !String //Embedded with name
= Embedded
| Detached !TaskAttributes !Bool //Management meta and flag whether the task should be started at once
| NamedDetached !String !TaskAttributes !Bool //Detached with name
:: ParallelTask a :== (SharedTaskList a) -> Task a
......@@ -161,10 +159,6 @@ removeTask :: !TaskId !(SharedTaskList a) -> Task () | TC a
* All meta-data is kept
*/
replaceTask :: !TaskId !(ParallelTask a) !(SharedTaskList a) -> Task () | iTask a
/**
* Focuses a task in a task list
*/
focusTask :: !TaskId !(SharedTaskList a) -> Task () | iTask a
/**
* Attaches a a detached task.
......
......@@ -32,6 +32,7 @@ import qualified Data.Queue as DQ
import Data.Maybe, Data.Either, Data.Error, Data.Func
import Text.GenJSON
from Data.Functor import <$>, class Functor(fmap)
from Data.Map import qualified instance Functor (Map k)
derive gEq ParallelTaskChange
......@@ -39,9 +40,7 @@ derive gEq ParallelTaskChange
:: ParallelTaskType
= Embedded //Simplest embedded
| NamedEmbedded !String //Embedded with name
| Detached !TaskAttributes !Bool //Management meta and flag whether the task should be started at once
| NamedDetached !String !TaskAttributes !Bool //Detached with name
:: ParallelTask a :== (SharedTaskList a) -> Task a
......@@ -331,9 +330,7 @@ initParallelTask ::
initParallelTask evalOpts listId index parType parTask iworld=:{current={taskTime}}
# (mbTaskStuff,iworld) = case parType of
Embedded = mkEmbedded 'DM'.newMap iworld
NamedEmbedded name = mkEmbedded ('DM'.singleton "name" (JSONString name)) iworld
Detached attributes evalDirect = mkDetached attributes evalDirect iworld
NamedDetached name attributes evalDirect = mkDetached ('DM'.put "name" (JSONString name) attributes) evalDirect iworld
= case mbTaskStuff of
Ok (taskId,attributes,mbTask)
# state =
......@@ -341,10 +338,10 @@ initParallelTask evalOpts listId index parType parTask iworld=:{current={taskTim
| taskId = taskId
, index = index
, detached = isNothing mbTask
, attributes = attributes
, implicitAttributes = 'DM'.newMap
, explicitAttributes = fmap (\x -> (x,True)) attributes
, value = NoValue
, createdAt = taskTime
, lastFocus = Nothing
, lastEvent = taskTime
, change = Nothing
, initialized = False
......@@ -383,11 +380,14 @@ evalParallelTasks event evalOpts=:{TaskEvalOpts|taskId=listId} conts completed [
[] = case searchContValue (genParallelValue (reverse (map snd completed))) (matchAction listId event) conts of
Nothing //We have evaluated all branches and nothing is added
//Remove all entries that are marked as removed from the list, they have been cleaned up by now
# taskListFilter = {TaskListFilter|onlyIndex=Nothing,onlyTaskId=Nothing,onlySelf=False,includeValue=False,includeAttributes=False,includeProgress=False}
# (mbError,iworld) = modify (\l -> [x \\ x <- l | not (isRemoved x)])
(sdsFocus (listId,taskListFilter) taskInstanceParallelTaskList) EmptyContext iworld
# taskListFilter = {TaskListFilter|onlyIndex=Nothing,onlyTaskId=Nothing,onlySelf=False,includeValue=False,includeAttributes=True,includeProgress=False}
# (mbError,iworld) = modify (\l -> [clearExplicitAttributeChange x \\ x <- l | not (isRemoved x)])
(sdsFocus (listId,taskListFilter) taskInstanceParallelTaskList) EmptyContext iworld
| mbError =:(Error _) = (Error (fromError mbError),iworld)
= (Ok (map snd completed),iworld)
//Bit of a hack... find updated attributes
# completed = reverse [(t,addAttributeChanges explicitAttributes c) \\ (t,c) <- reverse completed & {ParallelTaskState|explicitAttributes} <- directResult (fromOk mbList) ]
= (Ok (map snd completed),iworld)
Just (_,(type,task),_) //Add extension
# (mbStateMbTask, iworld) = initParallelTask evalOpts listId 0 type task iworld
......@@ -405,9 +405,27 @@ evalParallelTasks event evalOpts=:{TaskEvalOpts|taskId=listId} conts completed [
err = (liftError err, iworld)
//There is more work to do:
todo = evalParallelTasks event evalOpts conts completed todo iworld
where
isRemoved {ParallelTaskState|change=Just RemoveParallelTask} = True
isRemoved _ = False
where
isRemoved {ParallelTaskState|change=Just RemoveParallelTask} = True
isRemoved _ = False
addAttributeChanges explicitAttributes (ValueResult val evalInfor rep tree)
//Add the explicit attributes
# rep = case rep of
ReplaceUI (UI type attr items)
# expAtt = 'DM'.fromList [(k,v) \\ (k,(v,True)) <- 'DM'.toList explicitAttributes]
= (ReplaceUI (UI type ('DM'.union expAtt attr) items))
ChangeUI attrChanges itemChanges
# expChanges = [SetAttribute k v \\ (k,(v,True)) <- 'DM'.toList explicitAttributes]
= (ChangeUI (attrChanges ++ expChanges) itemChanges)
NoChange = case [SetAttribute k v \\ (k,(v,True)) <- 'DM'.toList explicitAttributes] of
[] = NoChange
attrChanges = (ChangeUI attrChanges [])
= (ValueResult val evalInfor rep tree)
addAttributeChanges explicitAttributes c = c
clearExplicitAttributeChange pts=:{ParallelTaskState|explicitAttributes} = {pts & explicitAttributes = fmap (\(v,_) -> (v,False)) explicitAttributes}
//Evaluate an embedded parallel task
evalParallelTasks event evalOpts=:{TaskEvalOpts|taskId=listId} conts completed [t=:{ParallelTaskState|taskId=taskId=:(TaskId _ taskNo)}:todo] iworld
= case evalParallelTask listId event evalOpts t iworld of
......@@ -430,7 +448,7 @@ where
= evalEmbeddedParallelTask listId event evalOpts taskState iworld
evalEmbeddedParallelTask listId event evalOpts
{ParallelTaskState|taskId,createdAt,lastFocus,value,change,initialized} iworld=:{current={taskTime}}
{ParallelTaskState|taskId,createdAt,value,change,initialized} iworld=:{current={taskTime}}
//Lookup task evaluation function and task evaluation state
# (mbTask,iworld) = read (sdsFocus taskId taskInstanceEmbeddedTask) EmptyContext iworld
| mbTask =:(Error _) = (Error (fromError mbTask),iworld)
......@@ -449,32 +467,28 @@ where
//If the exception can not be handled, don't continue evaluating just stop
= (Ok (ExceptionResult e),iworld)
(ValueResult val evalInfo=:{TaskEvalInfo|lastEvent,removedTasks} rep task, iworld)
//Check for a focus event targeted at this branc
# mbNewFocus= case event of
(FocusEvent focusId) = if (focusId == taskId) (Just taskTime) Nothing
_ = Nothing
# lastFocus = maybe lastFocus Just mbNewFocus
# attributeUpdate = case rep of
# result = ValueResult val evalInfo rep task
# implicitAttributeUpdate = case rep of
ReplaceUI (UI _ attributes _) = const attributes
ChangeUI changes _ = \a -> foldl (flip applyUIAttributeChange) a changes
_ = id
# result = ValueResult val evalInfo rep task
//Check if the value changed
# valueChanged = val =!= decode value
//Write the new reduct
# (mbError, iworld) = write task (sdsFocus taskId taskInstanceEmbeddedTask) EmptyContext iworld
| mbError =:(Error _) = (Error (fromError mbError), iworld)
//Write updated value, and optionally the new lastFocus time to the tasklist
# (mbError,iworld) = if valueChanged
(modify
(\pts -> {ParallelTaskState|pts & value = encode val, lastFocus = maybe pts.ParallelTaskState.lastFocus Just mbNewFocus, attributes = attributeUpdate pts.ParallelTaskState.attributes, initialized = True})
(sdsFocus (listId,taskId,True) taskInstanceParallelTaskListItem)
//Write updated value
# (mbError,iworld) = if valueChanged
(modify
(\pts -> {ParallelTaskState|pts & value = encode val,
implicitAttributes = implicitAttributeUpdate pts.ParallelTaskState.implicitAttributes,initialized = True})
(sdsFocus (listId,taskId,True) taskInstanceParallelTaskListItem)
EmptyContext iworld)
(modify
(\pts -> {ParallelTaskState|pts &
implicitAttributes = implicitAttributeUpdate pts.ParallelTaskState.implicitAttributes, initialized = True})
(sdsFocus (listId,taskId,False) taskInstanceParallelTaskListItem)
EmptyContext iworld)
(modify
(\pts -> {ParallelTaskState|pts & lastFocus = maybe pts.ParallelTaskState.lastFocus Just mbNewFocus, attributes = attributeUpdate pts.ParallelTaskState.attributes, initialized = True})
(sdsFocus (listId,taskId,False) taskInstanceParallelTaskListItem)
EmptyContext
iworld)
| mbError =:(Error _) = (Error (fromError mbError),iworld)
= (Ok result,iworld)
where
......@@ -637,7 +651,7 @@ where
| mbListId =:(Error _) = (mbListId,iworld)
# listId = fromOk mbListId
//Check if someone is trying to add an embedded task to the topLevel list
| listId == TaskId 0 0 && (parType =:(Embedded) || parType =:(NamedEmbedded _))
| listId == TaskId 0 0 && parType =:(Embedded)
= (Error (exception "Embedded tasks can not be added to the top-level task list"),iworld)
# (mbStateMbTask, iworld) = initParallelTask mkEvalOpts listId 0 parType parTask iworld
= case mbStateMbTask of
......@@ -734,19 +748,6 @@ where
| taskId == replaceId = [{ParallelTaskState|s & change = Just (ReplaceParallelTask (dynamic task :: Task a^))}:ss]
| otherwise = [s:scheduleReplacement replaceId task ss]
focusTask :: !TaskId !(SharedTaskList a) -> Task () | iTask a
focusTask focusId slist = mkInstantTask eval
where
eval taskId iworld=:{IWorld|current={taskTime}}
# (mbListId,iworld) = readListId slist iworld
| mbListId =:(Error _) = (liftError mbListId, iworld)
# listId = fromOk mbListId
| listId == TaskId 0 0
= (Ok (), iworld)
# (mbError,iworld) = modify (\pts -> {ParallelTaskState|pts & lastFocus = Just taskTime}) (sdsFocus (listId,focusId,False) taskInstanceParallelTaskListItem) EmptyContext iworld
| mbError =:(Error _) = (liftError mbError, iworld)
= (Ok (), iworld)
attach :: !InstanceNo !Bool -> Task AttachmentStatus
attach instanceNo steal = Task evalinit
where
......
......@@ -73,7 +73,6 @@ where
= case ok of
Ok _ = eval tmpDir taskIda (taskfun tmpDir) event eo iworld
Error e = (ExceptionResult (exception ("Could not create temporary directory: " +++ tmpDir +++ " (" +++ toString e +++ ")")) , iworld)
//Actual task execution
//First destroy the inner task, then delete the tmp dir
eval tmpDir innerTaskId (Task inner) DestroyEvent evalOpts iworld
......
......@@ -27,7 +27,6 @@ from StdOverloaded import class ==
:: Event = EditEvent !TaskId !String !JSONNode //Update something in an interaction: Task id, edit name, value
| ActionEvent !TaskId !String //Progress in a step combinator: Task id, action id
| FocusEvent !TaskId //Update last event time without changing anything: Task id
| RefreshEvent !(Set TaskId) !String //Recalcalutate the tasks with given IDs,
//using the current SDS values (the string is the reason for the refresh)
| ResetEvent //Nop event, recalculate the entire task and reset output stream
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment