Skip to content

Commit 50bba8b

Browse files
authored
Merge pull request #584 from doganc/ProjenBTEApps
add RunRepeatedly/DecisionOptionName to WorkflowEventEntity
2 parents 686b7b2 + 056c20a commit 50bba8b

File tree

9 files changed

+100
-13
lines changed

9 files changed

+100
-13
lines changed

Signum.Engine.Extensions/Workflow/CaseActivityLogic.cs

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,14 @@ public static IQueryable<CaseTagEntity> Tags(this CaseEntity e) =>
8383
public static IQueryable<CaseNotificationEntity> Notifications(this CaseActivityEntity e) =>
8484
As.Expression(() => Database.Query<CaseNotificationEntity>().Where(a => a.CaseActivity.Is(e)));
8585

86-
8786
[AutoExpressionField]
8887
public static IQueryable<CaseActivityExecutedTimerEntity> ExecutedTimers(this CaseActivityEntity e) =>
8988
As.Expression(() => Database.Query<CaseActivityExecutedTimerEntity>().Where(a => a.CaseActivity.Is(e)));
9089

90+
[AutoExpressionField]
91+
public static CaseActivityExecutedTimerEntity? LastExecutedTimer(this CaseActivityEntity ca, Lite<WorkflowEventEntity> we) =>
92+
As.Expression(() => ca.ExecutedTimers().Where(a => a.BoundaryEvent.Is(we)).OrderByDescending(a => a.CreationDate).FirstOrDefault());
93+
9194
public static void Start(SchemaBuilder sb)
9295
{
9396
if (sb.NotDefined(MethodInfo.GetCurrentMethod()))
@@ -291,11 +294,10 @@ IQueryable<CaseActivityEntity> CancelledCases(CaseEntity c)
291294
where ca.State == CaseActivityState.Pending
292295
from we in ((WorkflowActivityEntity)ca.WorkflowActivity).BoundaryTimers
293296
where we.Type == WorkflowEventType.BoundaryInterruptingTimer ? true :
294-
we.Type == WorkflowEventType.BoundaryForkTimer ? !ca.ExecutedTimers().Any(t => t.BoundaryEvent.Is(we)) :
297+
we.Type == WorkflowEventType.BoundaryForkTimer ? (we.RunRepeatedly || !ca.ExecutedTimers().Any(t => t.BoundaryEvent.Is(we))) :
295298
false
296299
select new ActivityEvent(ca, we)).ToList();
297300

298-
299301
var intermediateCandidates =
300302
(from ca in Database.Query<CaseActivityEntity>()
301303
where !ca.Workflow().HasExpired()
@@ -306,12 +308,20 @@ from we in ((WorkflowActivityEntity)ca.WorkflowActivity).BoundaryTimers
306308

307309
var candidates = boundaryCandidates.Concat(intermediateCandidates).Distinct().ToList();
308310
var conditions = candidates.Select(a => a.Event.Timer!.Condition).Distinct().ToList();
311+
var lastExecutions = boundaryCandidates
312+
.Where(a => a.Event.Type == WorkflowEventType.BoundaryForkTimer && a.Event.RunRepeatedly)
313+
.Select(a => new { ActivityEvent = a, LastExecution = a.Activity.LastExecutedTimer(a.Event.ToLite()) })
314+
.ToList();
309315

310316
var now = Clock.Now;
311317
var activities = conditions.SelectMany(cond =>
312318
{
313319
if (cond == null)
314-
return candidates.Where(a => a.Event.Timer!.Duration != null && a.Event.Timer!.Duration!.Add(a.Activity.StartDate) < now).Select(a => a.Activity.ToLite()).ToList();
320+
return candidates.Where(a =>
321+
{
322+
var startDate = lastExecutions.SingleOrDefaultEx(le => le.ActivityEvent.Is(a))?.LastExecution?.CreationDate ?? a.Activity.StartDate;
323+
return a.Event.Timer!.Duration != null && a.Event.Timer!.Duration!.Add(startDate) < now;
324+
}).Select(a => a.Activity.ToLite()).ToList();
315325

316326
return candidates.Where(a => a.Event.Timer!.Condition.Is(cond) && cond.Evaluate(a.Activity, now)).Select(a => a.Activity.ToLite()).ToList();
317327
}).Distinct().ToList();
@@ -426,6 +436,11 @@ public ActivityEvent(CaseActivityEntity activity, WorkflowEventEntity @event)
426436
Event = @event;
427437
}
428438

439+
public bool Is(ActivityEvent other)
440+
{
441+
return this.Activity.Is(other.Activity) && this.Event.Is(other.Event);
442+
}
443+
429444
public CaseActivityEntity Activity { get; set; }
430445
public WorkflowEventEntity Event { get; set; }
431446
}
@@ -835,15 +850,22 @@ public static void Register()
835850
{
836851
var now = Clock.Now;
837852

838-
var alreadyExecuted = ca.ExecutedTimers().Select(a => a.BoundaryEvent).ToHashSet();
839-
840853
var candidateEvents = ca.WorkflowActivity is WorkflowEventEntity @event ? new WorkflowEventEntity[] { @event } :
841854
((WorkflowActivityEntity)ca.WorkflowActivity).BoundaryTimers.ToArray();
842855

843-
var timer = candidateEvents.Where(e => e.Type == WorkflowEventType.BoundaryInterruptingTimer || !alreadyExecuted.Contains(e.ToLite())).FirstOrDefault(t =>
856+
var lastExecutions = (
857+
from et in ca.ExecutedTimers()
858+
group et by et.BoundaryEvent into g
859+
select new { Event = g.Key, LastExecution = g.OrderByDescending(a => a.CreationDate).FirstOrDefault() }
860+
).ToDictionary(a => a.Event, a => a.LastExecution);
861+
862+
var timer = candidateEvents.Where(e => e.Type == WorkflowEventType.BoundaryInterruptingTimer || e.RunRepeatedly || !lastExecutions.ContainsKey(e.ToLite())).FirstOrDefault(t =>
844863
{
845864
if (t.Timer!.Duration != null)
846-
return t.Timer!.Duration!.Add(ca.StartDate) < now;
865+
{
866+
var startDate = lastExecutions.GetValueOrDefault(t.ToLite())?.CreationDate ?? ca.StartDate;
867+
return t.Timer!.Duration!.Add(startDate) < now;
868+
}
847869

848870
return t.Timer!.Condition!.Evaluate(ca, now);
849871
});
@@ -1098,7 +1120,7 @@ private static void ExecuteBoundaryTimer(CaseActivityEntity ca, WorkflowEventEnt
10981120
}.Save();
10991121
break;
11001122
case WorkflowEventType.BoundaryInterruptingTimer:
1101-
ca.MakeDone(DoneType.Timeout, null);
1123+
ca.MakeDone(DoneType.Timeout, boundaryEvent.DecisionOptionName);
11021124
break;
11031125
default:
11041126
throw new InvalidOperationException("Unexpected Boundary Timer Type " + boundaryEvent.Type);

Signum.Engine.Extensions/Workflow/CaseFlowLogic.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,14 @@ public static CaseFlow GetCaseFlow(CaseEntity @case)
4343
if (from is WorkflowActivityEntity wa)
4444
{
4545
var conns = wa.BoundaryTimers.Where(a => a.Type == WorkflowEventType.BoundaryInterruptingTimer)
46-
.SelectMany(e => gr.GetAllConnections(e, to, path => path.All(a => a.Type == ConnectionType.Normal)));
46+
.SelectMany(e => gr.GetAllConnections(e, to, path =>
47+
{
48+
if (prev.DoneDecision != null)
49+
return IsValidPath(DoneType.Timeout, prev.DoneDecision, path);
50+
51+
return path.All(a => a.Type == ConnectionType.Normal);
52+
}));
53+
4754
if (conns.Any())
4855
return conns.Select(c => new CaseConnectionStats().WithConnection(c).WithDone(prev));
4956
}
@@ -192,10 +199,10 @@ private static bool IsValidPath(DoneType doneType, string? doneDecision, Stack<W
192199
case DoneType.Next:
193200
case DoneType.ScriptSuccess:
194201
case DoneType.Recompose:
202+
case DoneType.Timeout:
195203
return path.All(a => a.Type == ConnectionType.Normal || doneDecision != null && (a.DoneDecision() == doneDecision));
196204
case DoneType.Jump: return path.All(a => a.Is(path.FirstEx()) ? a.Type == ConnectionType.Jump : a.Type == ConnectionType.Normal);
197205
case DoneType.ScriptFailure: return path.All(a => a.Is(path.FirstEx()) ? a.Type == ConnectionType.ScriptException : a.Type == ConnectionType.Normal);
198-
case DoneType.Timeout:
199206
default:
200207
throw new InvalidOperationException();
201208
}

Signum.Engine.Extensions/Workflow/WorkflowLogic.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,8 @@ from e in Database.Query<WorkflowEntity>()
297297
e.BpmnElementId,
298298
e.Lane,
299299
e.Lane.Pool.Workflow,
300+
e.RunRepeatedly,
301+
e.DecisionOptionName,
300302
});
301303

302304

Signum.Engine.Extensions/Workflow/WorkflowNodeGraph.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Signum.Utilities.DataStructures;
33
using Signum.Entities.Authorization;
44
using Signum.Engine.Authorization;
5+
using Signum.Entities.Reflection;
56

67
namespace Signum.Engine.Workflow;
78

@@ -224,7 +225,6 @@ public void Validate(List<WorkflowIssue> issuesContainer, Action<WorkflowGateway
224225
if (e.Type.IsTimer())
225226
{
226227
var boundaryOutput = NextConnections(e).Only();
227-
228228
if (boundaryOutput == null || boundaryOutput.Type != ConnectionType.Normal)
229229
{
230230
if (e.Type == WorkflowEventType.IntermediateTimer)
@@ -238,6 +238,19 @@ public void Validate(List<WorkflowIssue> issuesContainer, Action<WorkflowGateway
238238

239239
if (e.Type == WorkflowEventType.IntermediateTimer && !e.Name.HasText())
240240
issues.AddError(e, WorkflowValidationMessage.IntermediateTimer0ShouldHaveName.NiceToString(e));
241+
242+
if (e.Type == WorkflowEventType.BoundaryInterruptingTimer)
243+
{
244+
var parentActivity = Activities.Values.Where(a => a.BoundaryTimers.Contains(e)).SingleEx();
245+
if (string.IsNullOrWhiteSpace(e.DecisionOptionName) && parentActivity.Type == WorkflowActivityType.Decision)
246+
issues.AddError(e, WorkflowValidationMessage.BoundaryTimer0OfActivity1ShouldHave2BecauseActivityIs3.NiceToString(e, parentActivity, e.NicePropertyName(a => a.DecisionOptionName), WorkflowActivityType.Decision.NiceToString()));
247+
248+
if (!string.IsNullOrWhiteSpace(e.DecisionOptionName) && parentActivity.Type != WorkflowActivityType.Decision)
249+
issues.AddError(e, WorkflowValidationMessage.BoundaryTimer0OfActivity1CanNotHave2BecauseActivityIsNot3.NiceToString(e, parentActivity, e.NicePropertyName(a => a.DecisionOptionName), WorkflowActivityType.Decision.NiceToString()));
250+
251+
if (!string.IsNullOrWhiteSpace(e.DecisionOptionName) && !parentActivity.DecisionOptions.Any(a => a.Name == e.DecisionOptionName))
252+
issues.AddError(e, WorkflowValidationMessage.BoundaryTimer0OfActivity1HasInvalid23.NiceToString(e, parentActivity, e.NicePropertyName(a => a.DecisionOptionName), e.DecisionOptionName));
253+
}
241254
}
242255
});
243256

Signum.Entities.Extensions/Workflow/Workflow.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,12 @@ public enum WorkflowValidationMessage
280280
DecisionOption0IsDeclaredButNeverUsedInAConnection,
281281
[Description("Decision option name '{0}' is not declared in any activity")]
282282
DecisionOptionName0IsNotDeclaredInAnyActivity,
283+
[Description("Boundary timer '{0}' of activity '{1}' can not have {2} because activity is not {3}")]
284+
BoundaryTimer0OfActivity1CanNotHave2BecauseActivityIsNot3,
285+
[Description("Boundary timer '{0}' of activity '{1}' should have {2} because activity is {3}")]
286+
BoundaryTimer0OfActivity1ShouldHave2BecauseActivityIs3,
287+
[Description("Boundary timer '{0}' of activity '{1}' has invalid {2}: '{3}'")]
288+
BoundaryTimer0OfActivity1HasInvalid23,
283289
}
284290

285291
public enum WorkflowActivityMonitorMessage

Signum.Entities.Extensions/Workflow/WorkflowActivity.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ public ModelEntity GetModel()
125125
Name = we.Name,
126126
MainEntityType = we.Lane.Pool.Workflow.MainEntityType,
127127
Type = we.Type,
128+
RunRepeatedly = we.RunRepeatedly,
129+
DecisionOptionName = we.DecisionOptionName,
128130
Timer = we.Timer,
129131
BpmnElementId = we.BpmnElementId
130132
}).ToMList());

Signum.Entities.Extensions/Workflow/WorkflowEvent.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ public class WorkflowEventEntity : Entity, IWorkflowNodeEntity, IWithModel
1717

1818
public WorkflowEventType Type { get; set; }
1919

20-
public WorkflowTimerEmbedded? Timer { get; set; }
20+
public bool RunRepeatedly { get; set; }
21+
22+
[StringLengthValidator(Min = 3, Max = 100)]
23+
public string? DecisionOptionName { get; set; }
24+
25+
26+
public WorkflowTimerEmbedded? Timer { get; set; }
2127

2228
public Lite<WorkflowActivityEntity>? BoundaryOf { get; set; }
2329

@@ -34,6 +40,8 @@ public ModelEntity GetModel()
3440
MainEntityType = this.Lane.Pool.Workflow.MainEntityType,
3541
Name = this.Name,
3642
Type = this.Type,
43+
RunRepeatedly = this.RunRepeatedly,
44+
DecisionOptionName = this.DecisionOptionName,
3745
Task = WorkflowEventTaskModel.GetModel(this),
3846
Timer = this.Timer,
3947
BpmnElementId = this.BpmnElementId,
@@ -47,11 +55,24 @@ public void SetModel(ModelEntity model)
4755
var wModel = (WorkflowEventModel)model;
4856
this.Name = wModel.Name;
4957
this.Type = wModel.Type;
58+
this.RunRepeatedly = wModel.RunRepeatedly;
59+
this.DecisionOptionName = wModel.DecisionOptionName;
5060
this.Timer = wModel.Timer;
5161
this.BpmnElementId = wModel.BpmnElementId;
5262
this.CopyMixinsFrom(wModel);
5363
//WorkflowEventTaskModel.ApplyModel(this, wModel.Task);
5464
}
65+
66+
protected override void PreSaving(PreSavingContext ctx)
67+
{
68+
if (Type != WorkflowEventType.BoundaryForkTimer && RunRepeatedly)
69+
RunRepeatedly = false;
70+
71+
if (Type != WorkflowEventType.BoundaryInterruptingTimer && !string.IsNullOrWhiteSpace(DecisionOptionName))
72+
DecisionOptionName = null;
73+
74+
base.PreSaving(ctx);
75+
}
5576
}
5677

5778
public class WorkflowTimerEmbedded : EmbeddedEntity
@@ -132,6 +153,11 @@ public class WorkflowEventModel : ModelEntity
132153

133154
public WorkflowEventType Type { get; set; }
134155

156+
public bool RunRepeatedly { get; set; }
157+
158+
[StringLengthValidator(Min = 3, Max = 100)]
159+
public string? DecisionOptionName { get; set; }
160+
135161
public WorkflowEventTaskModel? Task { get; set; }
136162

137163
public WorkflowTimerEmbedded? Timer { get; set; }

Signum.React.Extensions/Workflow/Signum.Entities.Workflow.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,8 @@ export interface WorkflowEventEntity extends Entities.Entity, IWorkflowNodeEntit
502502
bpmnElementId: string;
503503
lane: WorkflowLaneEntity;
504504
type: WorkflowEventType;
505+
runRepeatedly: boolean;
506+
decisionOptionName: string | null;
505507
timer: WorkflowTimerEmbedded | null;
506508
boundaryOf: Entities.Lite<WorkflowActivityEntity> | null;
507509
xml: WorkflowXmlEmbedded;
@@ -513,6 +515,8 @@ export interface WorkflowEventModel extends Entities.ModelEntity {
513515
mainEntityType: Basics.TypeEntity;
514516
name: string | null;
515517
type: WorkflowEventType;
518+
runRepeatedly: boolean;
519+
decisionOptionName: string | null;
516520
task: WorkflowEventTaskModel | null;
517521
timer: WorkflowTimerEmbedded | null;
518522
bpmnElementId: string;
@@ -846,6 +850,9 @@ export module WorkflowValidationMessage {
846850
export const Join0OfType1DoesNotMatchWithItsPairTheSplit2OfType3 = new MessageKey("WorkflowValidationMessage", "Join0OfType1DoesNotMatchWithItsPairTheSplit2OfType3");
847851
export const DecisionOption0IsDeclaredButNeverUsedInAConnection = new MessageKey("WorkflowValidationMessage", "DecisionOption0IsDeclaredButNeverUsedInAConnection");
848852
export const DecisionOptionName0IsNotDeclaredInAnyActivity = new MessageKey("WorkflowValidationMessage", "DecisionOptionName0IsNotDeclaredInAnyActivity");
853+
export const BoundaryTimer0OfActivity1CanNotHave2BecauseActivityIsNot3 = new MessageKey("WorkflowValidationMessage", "BoundaryTimer0OfActivity1CanNotHave2BecauseActivityIsNot3");
854+
export const BoundaryTimer0OfActivity1ShouldHave2BecauseActivityIs3 = new MessageKey("WorkflowValidationMessage", "BoundaryTimer0OfActivity1ShouldHave2BecauseActivityIs3");
855+
export const BoundaryTimer0OfActivity1HasInvalid23 = new MessageKey("WorkflowValidationMessage", "BoundaryTimer0OfActivity1HasInvalid23");
849856
}
850857

851858
export const WorkflowXmlEmbedded = new Type<WorkflowXmlEmbedded>("WorkflowXmlEmbedded");

Signum.React.Extensions/Workflow/Workflow/WorkflowEventModel.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export default function WorkflowEventModelComponent(p: WorkflowEventModelCompone
6161
<div>
6262
<ValueLine ctx={ctx.subCtx(we => we.name)} />
6363
<ValueLine ctx={ctx.subCtx(we => we.type)} readOnly={isTimer(ctx.value.type!)} optionItems={getTypeComboItems()} onChange={loadTask} />
64+
{ctx.value.type == "BoundaryForkTimer" && <ValueLine ctx={ctx.subCtx(a => a.runRepeatedly)} />}
65+
{ctx.value.type == "BoundaryInterruptingTimer" && <ValueLine ctx={ctx.subCtx(a => a.decisionOptionName)} />}
6466
{ctx.value.task && <WorkflowEventTask ctx={ctx.subCtx(a => a.task!)} mainEntityType={ctx.value.mainEntityType} isConditional={isConditional()} />}
6567
{ctx.value.timer && <WorkflowTimer ctx={ctx.subCtx(a => a.timer!)} mainEntityType={ctx.value.mainEntityType}/>}
6668
</div>

0 commit comments

Comments
 (0)