Commit b85e07a6 authored by Steffen Michels's avatar Steffen Michels

add (un)dockable tasks, still with some layout problems

git-svn-id: https://svn.cs.ru.nl/repos/iTask-system/trunk@941 63da3aa8-80fd-4f01-9db8-e6ea747a3da2
parent 161b1a0f
......@@ -8,16 +8,12 @@ itasks.ttc.FormContainer = Ext.extend(Ext.Panel, {
this.tbar = this.content.tbar;
delete this.content;
delete this.description;
if(this.hideDescription)
this.panel.cls = 'FormPanelWindow';
Ext.apply(this,
{ layout: 'anchor'
, taskUpdates : {}
, url: itasks.config.serverUrl + '/work/tab'
, items: this.hideDescription ? [this.panel] : [this.descpanel,this.panel]
, items: [this.descpanel,this.panel]
, unstyled: true
, autoScroll: true
, cls: 'FormContainer'
......@@ -171,8 +167,7 @@ itasks.ttc.FormContainer = Ext.extend(Ext.Panel, {
this.removeAll();
this.buildComponents(data);
if(this.hideDescription) this.panel.cls = 'FormPanelWindow';
if(!this.hideDescription) this.add(this.descpanel);
this.add(this.descpanel);
this.add(this.panel);
this.doLayout();
......@@ -198,7 +193,6 @@ itasks.ttc.form.FormDescription = Ext.extend(Ext.Panel,{
Ext.apply(this,{
cls: 'task-description FormDescription',
unstyled: true,
width:700
});
itasks.ttc.form.FormDescription.superclass.initComponent.apply(this,arguments);
......@@ -208,9 +202,8 @@ itasks.ttc.form.FormDescription = Ext.extend(Ext.Panel,{
itasks.ttc.form.FormPanel = Ext.extend(Ext.Panel, {
initComponent : function(){
Ext.applyIf(this,
Ext.apply(this,
{ layout: 'fit'
, width: 700
, unstyled: true
, cls: 'FormPanel'
});
......
......@@ -3,17 +3,12 @@ Ext.ns('itasks.ttc')
itasks.ttc.GroupContainer = Ext.extend(Ext.Panel,{
initComponent: function(){
this.containerMenus = new Ext.util.MixedCollection();
this.containerButtons = new Ext.util.MixedCollection();
Ext.apply(this,
{ layout:'auto'
, autoScroll: true
, cls: 'GroupContainer'
, unstyled: true
, tbar: []
, bbar: []
, taskUpdates: {}
});
itasks.ttc.GroupContainer.superclass.initComponent.apply(this,arguments);
......@@ -21,69 +16,151 @@ itasks.ttc.GroupContainer = Ext.extend(Ext.Panel,{
this.content = this.filterContent(this.content);
for(var i=0; i < this.content.length; i++) {
this.addContainer(this.content[i].panel,this.content[i].behaviour,this.content[i].index);
this.addContainer(this.content[i].panel,this.content[i].behaviour,this.content[i].index,false);
}
},
afterRender: function() {
itasks.ttc.GroupContainer.superclass.afterRender.call(this,arguments);
this.focusFirstContainer();
onLayout: function() {
itasks.ttc.GroupContainer.superclass.onLayout.call(this,arguments);
if(!Ext.isDefined(this.focusedContainer))
this.focusFirstContainer();
},
addContainer: function(cont,behaviour,idx,pos) {
addContainer: function(cont,behaviour,id,focus,pos) {
var group = this;
var id = this.mkElementId(idx);
if(Ext.isNumber(id))
id = this.mkElementId(id);
// add undock button to toolbar for (un)dockable tasks
if((behaviour == 'GBFixed' || behaviour == 'GBFloating') && cont.content) {
var undockButton = {
iconCls: 'icon-unpin',
cls: 'GroupToolbarUndockControls',
handler: function() {
var panel = group.focusedContainer;
var pos = group.items.indexOf(panel);
delete group.focusedContainer;
var cont = panel.get(0);
panel.removeAll(false);
panel.destroy();
// copy toolbar back from shared one
group.copyTbar(group.getTopToolbar(), cont.getTopToolbar());
group.addContainer(cont, 'GBFloating', panel.id, false, pos);
group.doLayout();
group.focusFirstContainer();
}
};
var tbar = cont.content.tbar;
tbar[tbar.length] = {xtype: 'tbseparator', cls: 'GroupToolbarUndockControls'};
tbar[tbar.length] = undockButton;
}
switch(behaviour) {
case 'AlwaysFixed':
this.storeButtonsAndMenus(cont);
case 'GBFixed':
case 'GBAlwaysFixed':
var panel = {
xtype: 'panel',
cls: 'GroupFixed GroupFixedNoFocus',
id: id,
items: [cont],
unstyled: true,
cls: 'ttc-no-focus',
focused: false,
listeners: {
afterrender: function(p) {
p.el.on('mousedown', function() {
group.focusContainer(p);
});
if(focus)
group.focusContainer(p);
}
},
focusFixed: function() {this.focused = true; this.removeClass('ttc-no-focus');},
unfocusFixed: function() {this.focused = false; this.removeClass('ttc-no-focus');this.addClass('ttc-no-focus');}
focusFixed: function() {
this.removeClass('GroupFixedNoFocus');
this.doLayout();
},
unfocusFixed: function() {
this.removeClass('GroupFixedNoFocus');
this.addClass('GroupFixedNoFocus');
this.doLayout();
}
};
break;
case 'AlwaysFloating':
this.configWindowContent(cont);
case 'GBFloating':
var tools = [{
id: 'pin',
handler: function(ev,el,window) {
var pos = group.items.indexOf(window);
var cont = window.get(0);
window.removeAll(false);
window.destroy();
group.addContainer(cont, 'GBFixed', window.id, true, pos);
group.doLayout();
cont.getTopToolbar().doLayout();
}
}];
case 'GBAlwaysFloating':
var panel = {
xtype: 'window',
cls: 'GroupFloating',
id: id,
initHidden: false,
closable: false,
shadow: false,
shadow: false,
items: [cont],
title: cont.description || "No Description"
title: cont.description || "No Description",
tools: tools
};
break;
}
if (pos)
if (Ext.isDefined(pos))
this.insert(pos,panel);
else
this.add(panel);
},
focusContainer: function(cont) {
if(Ext.isDefined(this.focusedContainer))
this.focusedContainer.unfocusFixed();
if(cont == this.focusedContainer)
return;
var groupTbar = this.getTopToolbar();
if(Ext.isDefined(this.focusedContainer)) {
// copy top toolbar back to container
var prevTbar = this.focusedContainer.get(0).getTopToolbar();
this.copyTbar(groupTbar, prevTbar);
this.focusedContainer.unfocusFixed();
} else {
groupTbar.removeAll();
}
// copy top toolbar to shared group toolbar
var taskTbar = cont.get(0).getTopToolbar();
if(taskTbar)
this.copyTbar(taskTbar, groupTbar);
groupTbar.setVisible(groupTbar.items.length > 0);
//apply the taskId to the new TB, so that attachTaskHandlers nows on which subtask the actions should
//be applied
groupTbar.cascade(function(){
Ext.apply(this,{
taskId : cont.get(0).taskId
});
});
itasks.ttc.common.attachTaskHandlers(groupTbar,cont.get(0).taskId);
// focus container
cont.focusFixed();
this.focusedContainer = cont;
this.refreshToolbar(this.getTopToolbar(), this.containerMenus, cont);
this.refreshToolbar(this.getBottomToolbar(), this.containerButtons, cont);
},
copyTbar: function(src,dst) {
var items = src.items.getRange();
src.removeAll(false);
dst.removeAll();
dst.add(items);
src.doLayout();
dst.doLayout();
},
focusFirstContainer: function() {
......@@ -97,51 +174,19 @@ itasks.ttc.GroupContainer = Ext.extend(Ext.Panel,{
}
if(!fixedExisting) {
delete this.focusedContainer;
this.getTopToolbar().hide();
this.getBottomToolbar().hide();
}
},
refreshToolbar: function(tb, contentCollection, focusedCont) {
tb.removeAll();
var newTb = contentCollection.get(focusedCont.get(0).taskId);
if(newTb)
tb.add(newTb);
tb.setVisible(tb.items.length > 0);
tb.doLayout();
//itasks.ttc.FormContainer.prototype.attachTaskHandlers(tb);
//apply the taskId to the new TB, so that attachTaskHandlers nows on which subtask the actions should
//be applied
tb.cascade(function(){
Ext.apply(this,{
taskId : focusedCont.get(0).taskId
});
});
itasks.ttc.common.attachTaskHandlers(tb,focusedCont.get(0).taskId);
},
removeContainer: function(i) {
var cont = this.get(i);
if(cont.focused && cont.getXType() != 'Window') {
if(cont == this.focusedContainer && cont.getXType() != 'Window') {
delete this.focusedContainer;
}
this.containerButtons.remove(cont.taskId);
this.containerMenus.remove(cont.taskId);
this.remove(i,true);
},
storeButtonsAndMenus: function(cont) {
if(cont.content) {
// store buttons/menus and remove them from container
this.containerButtons.add(cont.taskId, cont.content.buttons);
delete cont.content.buttons;
this.containerMenus.add(cont.taskId, cont.content.tbar);
cont.content.tbar = [];
}
},
filterContent: function(content) {
return content.filter(function (val) {
if(val.panel == "done" || val.panel == "redundant") return false;
......@@ -165,54 +210,16 @@ itasks.ttc.GroupContainer = Ext.extend(Ext.Panel,{
if(i < this.items.length){
var cont = this.get(i).get(0);
var isWindow = this.get(i).getXType() == "window";
if(isWindow) this.configWindowContent(data);
if(cont.getXType() == data.xtype){
if (!isWindow) this.storeButtonsAndMenus(data);
if(cont.getXType() == data.xtype && cont.taskId == data.taskId) {
cont.update(data);
if (data.updates) {
// update disabled-flag in stored buttons/menus
function updateToolbar(tb) {
if(!tb)
return;
if(Ext.isArray(tb)) {
for(var i=0; i < tb.length; i++){
updateToolbar(tb[i]);
}
} else {
if(Ext.isDefined(tb.id) && Ext.isDefined(tb.disabled)) {
function findSetEnabledUpdate(id, def) {
var num = data.updates.length;
for (i = 0; i < num; i++) {
var update = data.updates[i];
if(update[0] == "TUISetEnabled" && update[1] == id)
return !update[2];
}
return def;
}
tb.disabled = findSetEnabledUpdate(tb.id, tb.disabled);
}
if(tb.menu)
updateToolbar(tb.menu);
if(tb.items)
updateToolbar(tb.items);
}
}
updateToolbar(this.containerButtons.get(cont.taskId));
updateToolbar(this.containerMenus.get(cont.taskId));
}
}else{
//if not same xtype - completely replace container contents
} else {
//if not same xtype or taskId - completely replace container contents
this.get(i).removeAll();
if (!isWindow) this.storeButtonsAndMenus(data);
this.get(i).add(data);
}
} else {
this.addContainer(data,content[i].behaviour,content[i].index,j);
this.addContainer(data,content[i].behaviour,content[i].index,false,j);
}
}
......@@ -221,10 +228,6 @@ itasks.ttc.GroupContainer = Ext.extend(Ext.Panel,{
this.removeContainer(this.items.length-1);
}
// focus first fixed container if focused one is deleted
if(!Ext.isDefined(this.focusedContainer))
this.focusFirstContainer();
this.doLayout();
},
......@@ -236,22 +239,6 @@ itasks.ttc.GroupContainer = Ext.extend(Ext.Panel,{
this.focusedContainer.get(0).sendUpdates(delay);
},
configWindowContent: function(cont) {
if(cont.content) {
var noTbar = true;
for(var i=0; i < cont.content.tbar.length; i++){
if(!cont.content.tbar[i].disabled) {
noTbar = false;
break;
}
}
if (noTbar)
cont.content.tbar = [];
}
cont.hideDescription = true;
},
mkElementId: function(idx) {
return 'groupEl-' + this.taskId + '-' + idx;
}
......
......@@ -120,6 +120,10 @@ button.icon-expand-tree, .icon-expand-tree {
.icon-overview{
background-image: url('img/icons/application_view_list.png') !important;
}
.icon-unpin{
background-image: url('img/icons/unpin.gif') !important;
}
#userdisplay {
color: #3a81ad;
font-weight: bold;
......
......@@ -18,11 +18,7 @@
border-bottom: 1px solid #99BBE8;
border-right: 1px solid #99BBE8;
margin: 0px 0px 0px 10px;
}
.FormPanelWindow {
background-image : url('img/ttc-icons/TTCBackground80.png') !important;
padding: 4px;
width: 700px;
}
.FormDescription {
......@@ -30,6 +26,7 @@
background-repeat: repeat-x;
border: 1px solid #99BBE8;
margin: 10px 0px 0px 10px;
width: 700px;
}
/* Group Container */
......@@ -37,22 +34,51 @@
background-color: #eee;
}
.ttc-no-focus .FormPanel {
.GroupFixedNoFocus .x-panel-fbar {
display: none;
}
.GroupFixed .x-plain-tbar {
display: none;
}
.GroupFixedNoFocus .FormPanel {
background-image : url('img/ttc-icons/TTCBackgroundNoFocus.png') !important;
border-bottom:1px solid #ddd;
border-left:1px solid #ddd;
border-right:1px solid #ddd;
}
.ttc-no-focus .FormDescription {
.GroupFixedNoFocus .FormDescription {
background-image: url("img/ttc-icons/TTCDescriptionBackgroundNoFocus.png") !important;
border:1px solid #ddd;
}
.ttc-no-focus div.task-description {
.GroupFixedNoFocus div.task-description {
color:#888899;
}
.GroupFloating .FormPanel {
margin: 0px;
border: none;
}
.GroupFloating .FormDescription {
display: none;
}
/*.GroupFloating .FormPanel {
width: auto;
}*/
.GroupFloating .GroupToolbarUndockControls {
display: none;
}
.GroupFloating .GroupToolbarNoEnabledItems {
display: none;
}
/* Finished Container */
.FinishedContainer{
background-color: #eee;
......
......@@ -48,7 +48,7 @@ derive gMerge AppState, TextFile
derive gMakeSharedCopy AppState, TextFile
derive gMakeLocalCopy AppState, TextFile
subtaskBehaviour = AlwaysFloating
subtaskBehaviour = GBFloating
openFile :: (DBRef TextFile) (DBid AppState) -> Task Void
openFile id sid =
......@@ -143,7 +143,7 @@ ActionReplace :== ActionLabel "replace"
ActionStats :== ActionLabel "stats"
textEditorMain :: (DBid AppState) -> Task AppAction
textEditorMain sid = AlwaysFixed @>> (
textEditorMain sid = GBFixed @>> (
updateShared "Text Editor" [MenuParamAction ("openFile", Always):(map MenuAction actions)] sid [titleListener,mainEditor]
>>= \(action, _). case action of
ActionNew = writeDB sid initState >>| return (AppAction (Extend [textEditorMain sid]))
......
......@@ -61,7 +61,7 @@ initTaskInfo
, taskLabel = ""
, traceValue = ""
, worker = UserName "" ""
, groupedBehaviour = AlwaysFixed
, groupedBehaviour = GBFixed
, taskDescription = ""
}
......@@ -229,7 +229,7 @@ where
resetTSt :: !ProcessId !TaskProperties !(Maybe TaskParallelType) !*TSt -> *TSt
resetTSt processId properties inptype tst
# taskNr = taskNrFromString processId
# info = {TaskInfo|taskId = toString processId, taskLabel = properties.managerProps.subject, traceValue = "", worker=properties.managerProps.TaskManagerProperties.worker, groupedBehaviour = AlwaysFixed, taskDescription = ""}
# info = {TaskInfo|taskId = toString processId, taskLabel = properties.managerProps.subject, traceValue = "", worker=properties.managerProps.TaskManagerProperties.worker, groupedBehaviour = GBFixed, taskDescription = ""}
# tree = TTMainTask info properties menus inptype (TTFinishedTask info [])
= {TSt| tst & taskNr = taskNr, tree = tree, staticInfo = {tst.staticInfo & currentProcessId = processId}, mainTask = processId}
......@@ -397,7 +397,7 @@ calculateTaskTree processId tst
# (mbProcess,tst) = getProcess processId tst
= case mbProcess of
Nothing
= (TTFinishedTask {TaskInfo|taskId = toString processId, taskLabel = "Deleted Process", traceValue="Deleted", worker = UserName "" "", groupedBehaviour = AlwaysFixed, taskDescription="Task Result"} [], tst)
= (TTFinishedTask {TaskInfo|taskId = toString processId, taskLabel = "Deleted Process", traceValue="Deleted", worker = UserName "" "", groupedBehaviour = GBFixed, taskDescription="Task Result"} [], tst)
Just process=:{Process|status,properties}
= case status of
Active
......@@ -406,7 +406,7 @@ calculateTaskTree processId tst
= (tree,tst)
_
//retrieve process result from store and show it??
= (TTFinishedTask {TaskInfo|taskId = toString processId, taskLabel = properties.managerProps.subject, traceValue = "Finished", worker = properties.managerProps.TaskManagerProperties.worker, groupedBehaviour = AlwaysFixed, taskDescription="Task Result"} [], tst)
= (TTFinishedTask {TaskInfo|taskId = toString processId, taskLabel = properties.managerProps.subject, traceValue = "Finished", worker = properties.managerProps.TaskManagerProperties.worker, groupedBehaviour = GBFixed, taskDescription="Task Result"} [], tst)
calculateTaskForest :: !*TSt -> (![TaskTree], !*TSt)
calculateTaskForest tst
......@@ -464,31 +464,31 @@ mkTaskFunction :: (*TSt -> (!a,!*TSt)) -> (*TSt -> (!TaskResult a,!*TSt))
mkTaskFunction f = \tst -> let (a,tst`) = f tst in (TaskFinished a,tst`)
mkInteractiveTask :: !String !(*TSt -> *(!TaskResult a,!*TSt)) -> Task a
mkInteractiveTask taskname taskfun = Task {TaskDescription| title = taskname, description = Note "", groupedBehaviour = AlwaysFixed} Nothing mkInteractiveTask`
mkInteractiveTask taskname taskfun = Task {TaskDescription| title = taskname, description = Note "", groupedBehaviour = GBFixed} Nothing mkInteractiveTask`
where
mkInteractiveTask` tst=:{TSt|taskNr,taskInfo}
= taskfun {tst & tree = TTInteractiveTask taskInfo (abort "No interface definition given")}
mkInstantTask :: !String !(*TSt -> *(!TaskResult a,!*TSt)) -> Task a
mkInstantTask taskname taskfun = Task {TaskDescription| title = taskname, description = Note "", groupedBehaviour = AlwaysFixed} Nothing mkInstantTask`
mkInstantTask taskname taskfun = Task {TaskDescription| title = taskname, description = Note "", groupedBehaviour = GBFixed} Nothing mkInstantTask`
where
mkInstantTask` tst=:{TSt|taskNr,taskInfo}
= taskfun {tst & tree = TTFinishedTask taskInfo []} //We use a FinishedTask node because the task is finished after one evaluation
mkMonitorTask :: !String !(*TSt -> *(!TaskResult a,!*TSt)) -> Task a
mkMonitorTask taskname taskfun = Task {TaskDescription| title = taskname, description = Note "", groupedBehaviour = AlwaysFixed} Nothing mkMonitorTask`
mkMonitorTask taskname taskfun = Task {TaskDescription| title = taskname, description = Note "", groupedBehaviour = GBFixed} Nothing mkMonitorTask`
where
mkMonitorTask` tst=:{TSt|taskNr,taskInfo}
= taskfun {tst & tree = TTMonitorTask taskInfo []}
mkInstructionTask :: !String !(*TSt -> *(!TaskResult Void,!*TSt)) -> Task Void
mkInstructionTask taskname taskfun = Task {TaskDescription | title = taskname, description = Note "", groupedBehaviour = AlwaysFixed} Nothing mkInstructionTask`
mkInstructionTask taskname taskfun = Task {TaskDescription | title = taskname, description = Note "", groupedBehaviour = GBFixed} Nothing mkInstructionTask`
where
mkInstructionTask` tst =:{TSt | taskInfo}
= taskfun {tst & tree = TTInstructionTask taskInfo [] Nothing}
mkRpcTask :: !String !RPCExecute !(String -> a) -> Task a | gUpdate{|*|} a
mkRpcTask taskname rpce parsefun = Task {TaskDescription| title = taskname, description = Note "", groupedBehaviour = AlwaysFixed} Nothing mkRpcTask`
mkRpcTask taskname rpce parsefun = Task {TaskDescription| title = taskname, description = Note "", groupedBehaviour = GBFixed} Nothing mkRpcTask`
where
mkRpcTask` tst=:{TSt | taskNr, taskInfo}
# rpce = {RPCExecute | rpce & taskId = taskNrToString taskNr}
......@@ -543,27 +543,27 @@ where
setStatus status tst = setTaskStore "status" status tst
mkSequenceTask :: !String !(*TSt -> *(!TaskResult a,!*TSt)) -> Task a
mkSequenceTask taskname taskfun = Task {TaskDescription| title = taskname, description = Note "", groupedBehaviour = AlwaysFixed} Nothing mkSequenceTask`
mkSequenceTask taskname taskfun = Task {TaskDescription| title = taskname, description = Note "", groupedBehaviour = GBFixed} Nothing mkSequenceTask`
where
mkSequenceTask` tst=:{TSt|taskNr,taskInfo}
= taskfun {tst & tree = TTSequenceTask taskInfo [], taskNr = [0:taskNr]}
mkParallelTask :: !String !TaskParallelInfo !(*TSt -> *(!TaskResult a,!*TSt)) -> Task a
mkParallelTask taskname tpi taskfun = Task {TaskDescription| title = taskname, description = Note "", groupedBehaviour = AlwaysFixed} Nothing mkParallelTask`
mkParallelTask taskname tpi taskfun = Task {TaskDescription| title = taskname, description = Note "", groupedBehaviour = GBFixed} Nothing mkParallelTask`
where
mkParallelTask` tst=:{TSt|taskNr,taskInfo}
# tst = {tst & tree = TTParallelTask taskInfo tpi [], taskNr = [0:taskNr]}
= taskfun tst
mkGroupedTask :: !String !(*TSt -> *(!TaskResult a,!*TSt)) -> Task a
mkGroupedTask taskname taskfun = Task {TaskDescription| title = taskname, description = Note "", groupedBehaviour = AlwaysFixed} Nothing mkGroupedTask`
mkGroupedTask taskname taskfun = Task {TaskDescription| title = taskname, description = Note "", groupedBehaviour = GBFixed} Nothing mkGroupedTask`
where
mkGroupedTask` tst=:{TSt|taskNr,taskInfo}
# tst = {tst & tree = TTGroupedTask taskInfo [], taskNr = [0:taskNr]}
= taskfun tst
mkMainTask :: !String !(*TSt -> *(!TaskResult a,!*TSt)) -> Task a
mkMainTask taskname taskfun = Task {TaskDescription| title = taskname, description = Note "", groupedBehaviour = AlwaysFixed} Nothing mkMainTask`
mkMainTask taskname taskfun = Task {TaskDescription| title = taskname, description = Note "", groupedBehaviour = GBFixed} Nothing mkMainTask`
where
mkMainTask` tst=:{taskNr,taskInfo}
= taskfun {tst & tree = TTMainTask taskInfo initTaskProperties Nothing Nothing (TTFinishedTask taskInfo [])}
......
......@@ -76,7 +76,7 @@ from TUIDefinition import :: TUIDef, :: TUIUpdate
:: TaskParallelType = Open //Everybody to whom a subtask is assigned can see the full status of this parallel, including the results of others
| Closed //Only the manager can see the overview. For assigned users, it just looks like an ordinary task.
:: GroupedBehaviour = //Fixed //The editor is fixed in the window, user can undock editor (making it floating)
//| Floating //The editor is shown in a floating window, user can dock editor (making it fixed)
AlwaysFixed //Same as Fixed, but user cannot undock
| AlwaysFloating //Same as Floating, but user cannot dock
\ No newline at end of file
:: GroupedBehaviour = GBFixed //The editor is fixed in the window, user can undock editor (making it floating)
| GBFloating //The editor is shown in a floating window, user can dock editor (making it fixed)
| GBAlwaysFixed //Same as Fixed, but user cannot undock
| GBAlwaysFloating //Same as Floating, but user cannot dock
\ No newline at end of file
......@@ -80,7 +80,7 @@ gVisualize{|Task|} fx _ _ vst = ([],0,vst)
gUpdate{|Task|} fx _ ust=:{mode=UDCreate}
# (a,ust) = fx (abort "Task create with undef") ust
= (Task {TaskDescription|title = "return", description = Note "", groupedBehaviour=AlwaysFixed} Nothing (\tst -> (TaskFinished a,tst)), ust)
= (Task {TaskDescription|title = "return", description = Note "", groupedBehaviour=GBFixed} Nothing (\tst -> (TaskFinished a,tst)), ust)
gUpdate{|Task|} fx x ust = (x,ust)
derive gUpdate User
......
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