diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/BridgeCalls.cs b/src/activities/Elsa.Activities.Telnyx/Activities/BridgeCalls.cs index 640f0bb15..fe77f0df3 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/BridgeCalls.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/BridgeCalls.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Elsa.Activities.Telnyx.Client.Models; using Elsa.Activities.Telnyx.Client.Services; using Elsa.Activities.Telnyx.Extensions; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Call; using Elsa.ActivityResults; using Elsa.Attributes; using Elsa.Builders; @@ -18,7 +21,7 @@ namespace Elsa.Activities.Telnyx.Activities [Action( Category = Constants.Category, Description = "Bridge two call control calls.", - Outcomes = new[] { OutcomeNames.Done, TelnyxOutcomeNames.CallIsNoLongerActive }, + Outcomes = new[] {TelnyxOutcomeNames.Bridging, TelnyxOutcomeNames.Bridged, TelnyxOutcomeNames.LegABridged, TelnyxOutcomeNames.LegBBridged, OutcomeNames.Done, TelnyxOutcomeNames.CallIsNoLongerActive}, DisplayName = "Bridge Calls" )] public class BridgeCalls : Activity @@ -30,40 +33,52 @@ namespace Elsa.Activities.Telnyx.Activities _telnyxClient = telnyxClient; } - [ActivityInput(Label = "Call Control ID A", Hint = "Unique identifier and token for controlling the call.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] - public string? CallControlIdA { get; set; } = default!; + [ActivityInput(Label = "Call Control ID A", Hint = "Unique identifier and token for controlling the call.", SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] + public string? CallControlIdA { get; set; } - [ActivityInput(Label = "Call Control ID B", Hint = "The Call Control ID of the call you want to bridge with.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] - public string CallControlIdB { get; set; } = default!; + [ActivityInput(Label = "Call Control ID B", Hint = "The Call Control ID of the call you want to bridge with.", SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] + public string? CallControlIdB { get; set; } [ActivityInput( Label = "Command ID", Hint = "Use this field to avoid duplicate commands. Telnyx will ignore commands with the same Command ID.", Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public string? CommandId { get; set; } [ActivityInput( - Hint = "Use this field to add state to every subsequent webhook. It must be a valid Base-64 encoded string.", + Hint = "Use this field to add state to every subsequent webhook. It must be a valid Base-64 encoded string.", Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public string? ClientState { get; set; } [ActivityInput( - Label = "Park After Unbridged", - Hint = "HTTP request type used for Webhook URL", - UIHint = ActivityInputUIHints.Dropdown, - Options = new[] { "", "self" }, - Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid })] + Label = "Park After Unbridged", + Hint = "HTTP request type used for Webhook URL", + UIHint = ActivityInputUIHints.Dropdown, + Options = new[] {"", "self"}, + Category = PropertyCategories.Advanced, + SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid})] public string? ParkAfterUnbridged { get; set; } + [ActivityOutput] public CallBridgedPayload? CallBridgedPayloadA { get; set; } + [ActivityOutput] public CallBridgedPayload? CallBridgedPayloadB { get; set; } + protected override async ValueTask OnExecuteAsync(ActivityExecutionContext context) { - var callControlIdA = context.GetCallControlId(CallControlIdA); + CallBridgedPayloadA = null; + CallBridgedPayloadB = null; + var callControlIdA = CallControlIdA = context.GetCallControlId(CallControlIdA); + var callControlIdB = CallControlIdB = GetCallControlB(context); + + if (callControlIdB == null) + throw new WorkflowException("Cannot bridge calls because the second leg's call control ID was not specified and no incoming activities provided this value"); + + CallControlIdA = callControlIdA; + var request = new BridgeCallsRequest( - CallControlIdB, + callControlIdB, ClientState, CommandId, ParkAfterUnbridged @@ -72,7 +87,7 @@ namespace Elsa.Activities.Telnyx.Activities try { await _telnyxClient.Calls.BridgeCallsAsync(callControlIdA, request, context.CancellationToken); - return Done(); + return Combine(Outcome(TelnyxOutcomeNames.Bridging), Suspend()); } catch (ApiException e) { @@ -82,6 +97,50 @@ namespace Elsa.Activities.Telnyx.Activities throw new WorkflowException(e.Content ?? e.Message, e); } } + + protected override IActivityExecutionResult OnResume(ActivityExecutionContext context) + { + var payload = context.GetInput()!; + var results = new List(); + + if (payload.CallControlId == CallControlIdA) + { + CallBridgedPayloadA = payload; + results.Add(Outcome(TelnyxOutcomeNames.LegABridged, payload)); + } + + if (payload.CallControlId == CallControlIdB) + { + CallBridgedPayloadB = payload; + results.Add(Outcome(TelnyxOutcomeNames.LegBBridged, payload)); + } + + if (CallBridgedPayloadA != null && CallBridgedPayloadB != null) + { + results.Add(Outcome(TelnyxOutcomeNames.Bridged)); + } + else + { + results.Add(Suspend()); + } + + return Combine(results); + } + + private string? GetCallControlB(ActivityExecutionContext context) + { + if (!string.IsNullOrWhiteSpace(CallControlIdB)) + return CallControlIdB; + + var input = context.GetInput(); + + if (input != null) + return input.CallControlId; + + var inboundCallActivityId = context.WorkflowExecutionContext.GetInboundConnections(Id).Where(x => x.Source.Activity.Type == nameof(Dial)).Select(x => x.Source.Activity.Id).FirstOrDefault(); + var inboundCallActivityResponse = inboundCallActivityId != null ? context.WorkflowExecutionContext.GetActivityProperty(inboundCallActivityId, x => x.DialResponse) : default; + return inboundCallActivityResponse != null ? inboundCallActivityResponse.CallControlId : null; + } } public static class BridgeCallsExtensions diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/CallRingGroup.cs b/src/activities/Elsa.Activities.Telnyx/Activities/CallRingGroup.cs index 71722395c..7ddd47ff4 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/CallRingGroup.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/CallRingGroup.cs @@ -26,7 +26,7 @@ namespace Elsa.Activities.Telnyx.Activities [Action( Category = Constants.Category, Description = "Call a ring group.", - Outcomes = new[] { "Connected", "No Response" }, + Outcomes = new[] {TelnyxOutcomeNames.Connected, TelnyxOutcomeNames.NoResponse}, DisplayName = "Call Ring Group" )] public class CallRingGroup : CompositeActivity, IActivityPropertyDefaultValueProvider @@ -38,14 +38,14 @@ namespace Elsa.Activities.Telnyx.Activities _logger = logger; } - [ActivityInput(UIHint = ActivityInputUIHints.MultiText, DefaultSyntax = SyntaxNames.Json, SupportedSyntaxes = new[] { SyntaxNames.Json, SyntaxNames.JavaScript, SyntaxNames.Liquid })] + [ActivityInput(UIHint = ActivityInputUIHints.MultiText, DefaultSyntax = SyntaxNames.Json, SupportedSyntaxes = new[] {SyntaxNames.Json, SyntaxNames.JavaScript, SyntaxNames.Liquid})] public IList Extensions { get => GetState>(() => new List()); set => SetState(value); } - [ActivityInput(Label = "Call Control ID", Hint = "Unique identifier and token for controlling the call.", Category = PropertyCategories.Advanced, SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + [ActivityInput(Label = "Call Control ID", Hint = "Unique identifier and token for controlling the call.", Category = PropertyCategories.Advanced, SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public string CallControlId { get => GetState()!; @@ -56,7 +56,7 @@ namespace Elsa.Activities.Telnyx.Activities Label = "Call Control App ID", Hint = "The ID of the Call Control App (formerly ID of the connection) to be used when dialing the destination.", Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? CallControlAppId { @@ -64,7 +64,7 @@ namespace Elsa.Activities.Telnyx.Activities set => SetState(value); } - [ActivityInput(SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid })] + [ActivityInput(SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid})] public RingGroupStrategy Strategy { get => GetState(); @@ -73,7 +73,7 @@ namespace Elsa.Activities.Telnyx.Activities [ActivityInput( Hint = "The 'from' number to be used as the caller id presented to the destination ('To' number). The number should be in +E164 format. This attribute will default to the 'From' number of the original call if omitted.", - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? From { @@ -84,7 +84,7 @@ namespace Elsa.Activities.Telnyx.Activities [ActivityInput( Hint = "The string to be used as the caller id name (SIP From Display Name) presented to the destination ('To' number). The string should have a maximum of 128 characters, containing only letters, numbers, spaces, and -_~!.+ special characters. If omitted, the display name will be the same as the number in the 'From' field.", - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? FromDisplayName { @@ -92,18 +92,21 @@ namespace Elsa.Activities.Telnyx.Activities set => SetState(value); } - [ActivityInput(DefaultValueProvider = typeof(CallRingGroup), SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + [ActivityInput(DefaultValueProvider = typeof(CallRingGroup), SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public Duration RingTime { get => GetState(() => Duration.FromSeconds(20)); set => SetState(value); } - - private string? DialedControlId - { - get => GetState(); - set => SetState(value); - } + + [ActivityInput( + Hint = "Enables Answering Machine Detection.", + UIHint = ActivityInputUIHints.Dropdown, + Options = new[] {"disabled", "detect", "detect_beep", "detect_words", "greeting_end"}, + DefaultValue = "disabled", + Category = PropertyCategories.Advanced, + SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid})] + public string? AnsweringMachineDetection { get; set; } = "disabled"; private CallAnsweredPayload? CallAnsweredPayload { @@ -148,64 +151,53 @@ namespace Elsa.Activities.Telnyx.Activities private void BuildPrioritizedHuntFlow(IOutcomeBuilder builder) => builder .ForEach(() => Extensions, iterate => iterate - .Then(a => a - .WithConnectionId(() => CallControlAppId) - .WithTo(async context => await ResolveExtensionAsync(context, context.GetInput()!)) - .WithTimeoutSecs(() => (int) RingTime.TotalSeconds) - .WithFrom(() => From) - .WithFromDisplayName(() => FromDisplayName) - .WithClientState(context => new ClientStatePayload(context.CorrelationId!).ToBase64()) + .Then(dial => dial + .WithConnectionId(() => CallControlAppId) + .WithTo(ResolveExtensionAsync) + .WithTimeoutSecs(() => (int) RingTime.TotalSeconds) + .WithFrom(() => From) + .WithFromDisplayName(() => FromDisplayName), + dial => + { + dial + .When(TelnyxOutcomeNames.Answered) + .Then(bridgeCalls => + bridgeCalls.When(TelnyxOutcomeNames.Bridged) + .Finish(TelnyxOutcomeNames.Connected)); + } ) - .Then(context => DialedControlId = context.GetInput()!.CallControlId) - .Then(fork => fork.WithBranches("Connected", "No Response"), fork => - { - fork - .When("Connected") - .ThenTypeNamed(CallAnsweredPayload.ActivityTypeName) - .Then(context => CallAnsweredPayload = (CallAnsweredPayload) context.GetInput()!.Data.Payload) - .Then(bridge => bridge - .WithCallControlIdA(() => CallControlId) - .WithCallControlIdB(() => DialedControlId)) - .ThenTypeNamed(CallBridgedPayload.ActivityTypeName) - .ThenTypeNamed(CallBridgedPayload.ActivityTypeName) - .Then(finish => finish.WithOutcome("Connected").WithOutput(() => CallAnsweredPayload)); - - fork - .When("No Response") - .ThenTypeNamed(CallHangupPayload.ActivityTypeName); - }) ) - .Finish("No Response"); + .Finish(TelnyxOutcomeNames.NoResponse); private void BuildRingAllFlow(IOutcomeBuilder builder) => builder .Then(fork => fork.WithBranches("Connected", "Timeout", "Dial Everyone"), fork => { fork - .When("Connected") + .When(TelnyxOutcomeNames.Connected) .ThenTypeNamed(CallAnsweredPayload.ActivityTypeName) .Then(context => CallAnsweredPayload = (CallAnsweredPayload) context.GetInput()!.Data.Payload) .Then(bridge => bridge .WithCallControlIdA(() => CallControlId) - .WithCallControlIdB(() => CallAnsweredPayload!.CallControlId)) - .ThenTypeNamed(CallBridgedPayload.ActivityTypeName) - .Then(finish => finish.WithOutcome("Connected").WithOutput(() => CallAnsweredPayload)); + .WithCallControlIdB(() => CallAnsweredPayload!.CallControlId), bridge => bridge + .When(TelnyxOutcomeNames.Bridged) + .Finish(TelnyxOutcomeNames.Connected)); fork .When("Timeout") .StartIn(() => RingTime) - .Finish("No Response"); + .Finish(TelnyxOutcomeNames.NoResponse); fork .When("Dial Everyone") .ParallelForEach(() => Extensions, iterate => iterate .Then(a => a + .WithSuspendWorkflow(false) .WithConnectionId(() => CallControlAppId) - .WithTo(async context => await ResolveExtensionAsync(context, context.GetInput()!)) + .WithTo(ResolveExtensionAsync) .WithTimeoutSecs(() => (int) RingTime.TotalSeconds) .WithFrom(() => From) .WithFromDisplayName(() => FromDisplayName) - .WithClientState(context => new ClientStatePayload(context.CorrelationId!).ToBase64()) ) .Then(CollectCallControlIds)); }); @@ -217,9 +209,13 @@ namespace Elsa.Activities.Telnyx.Activities collection.Add(dialResponse); CollectedDialResponses = collection; } - - private static async Task ResolveExtensionAsync(ActivityExecutionContext context, string extension) + + private static async ValueTask ResolveExtensionAsync(ActivityExecutionContext context) { + if (context.Resuming) + return context.GetActivityProperty(x => x.To)!; + + var extension = context.GetInput()!; var extensionProvider = context.GetService(); var resolvedExtension = await extensionProvider.GetAsync(extension, context.CancellationToken); return resolvedExtension?.Number ?? extension; diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/Dial.cs b/src/activities/Elsa.Activities.Telnyx/Activities/Dial.cs index 337d792f8..e432ce0b9 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/Dial.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/Dial.cs @@ -5,7 +5,9 @@ using Elsa.Activities.Telnyx.Client.Models; using Elsa.Activities.Telnyx.Client.Services; using Elsa.Activities.Telnyx.Exceptions; using Elsa.Activities.Telnyx.Extensions; +using Elsa.Activities.Telnyx.Models; using Elsa.Activities.Telnyx.Options; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Call; using Elsa.ActivityResults; using Elsa.Attributes; using Elsa.Builders; @@ -21,7 +23,7 @@ namespace Elsa.Activities.Telnyx.Activities [Action( Category = Constants.Category, Description = "Dial a number or SIP URI from a given connection.", - Outcomes = new[] { OutcomeNames.Done }, + Outcomes = new[] { TelnyxOutcomeNames.Dialing, TelnyxOutcomeNames.CallInitiated, TelnyxOutcomeNames.Answered, TelnyxOutcomeNames.Hangup, OutcomeNames.Done }, DisplayName = "Dial" )] public class Dial : Activity @@ -58,9 +60,10 @@ namespace Elsa.Activities.Telnyx.Activities [ActivityInput( Hint = "Enables Answering Machine Detection.", UIHint = ActivityInputUIHints.Dropdown, - Options = new[] { "disabled", "detect", "detect_beep", "detect_words", "greeting_end" }, - SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid })] - public string? AnsweringMachineDetection { get; set; } + Options = new[] {"disabled", "detect", "detect_beep", "detect_words", "greeting_end"}, + DefaultValue = "disabled", + SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid})] + public string? AnsweringMachineDetection { get; set; } = "disabled"; [ActivityInput( Label = "Answering Machine Detection Configuration", @@ -76,12 +79,6 @@ namespace Elsa.Activities.Telnyx.Activities SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] public string? CommandId { get; set; } - [ActivityInput( - Hint = "Use this field to add state to every subsequent webhook. It must be a valid Base-64 encoded string.", - Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] - public string? ClientState { get; set; } - [ActivityInput(Label = "Custom Headers", Hint = "Custom headers to be added to the SIP INVITE.", Category = PropertyCategories.Advanced, UIHint = ActivityInputUIHints.Json)] public IList
? CustomHeaders { get; set; } @@ -116,10 +113,36 @@ namespace Elsa.Activities.Telnyx.Activities SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid })] public string? WebhookUrlMethod { get; set; } + [ActivityInput( + Hint = "A flag indicating whether this activity should complete immediately or suspend the workflow.", + Category = PropertyCategories.Advanced, + DefaultValue = true, + SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid})] + public bool SuspendWorkflow { get; set; } = true; + + [ActivityOutput] public DialResponse DialResponse { get; set; } + protected override async ValueTask OnExecuteAsync(ActivityExecutionContext context) { var response = await DialAsync(context); - return Done(response); + DialResponse = response; + + return !SuspendWorkflow + ? Done(response) + : Combine(Outcome(TelnyxOutcomeNames.Dialing, response), Suspend()); + } + + protected override IActivityExecutionResult OnResume(ActivityExecutionContext context) + { + var payload = context.GetInput(); + + return payload switch + { + CallAnsweredPayload callAnsweredPayload => Outcome(TelnyxOutcomeNames.Answered, callAnsweredPayload), + CallHangupPayload callHangupPayload => Outcome(TelnyxOutcomeNames.Hangup, callHangupPayload), + CallInitiatedPayload callInitiatedPayload => Combine(Outcome(TelnyxOutcomeNames.CallInitiated, callInitiatedPayload), Suspend()), + _ => throw new ArgumentOutOfRangeException(nameof(payload)) + }; } private async Task DialAsync(ActivityExecutionContext context) @@ -131,6 +154,8 @@ namespace Elsa.Activities.Telnyx.Activities var fromNumber = context.GetFromNumber(From); + var clientState = new ClientStatePayload(context.CorrelationId!).ToBase64(); + var request = new DialRequest( connectionId, To, @@ -138,7 +163,7 @@ namespace Elsa.Activities.Telnyx.Activities FromDisplayName, AnsweringMachineDetection, AnsweringMachineDetectionConfig, - ClientState, + clientState, CommandId, CustomHeaders, SipAuthUsername, @@ -207,12 +232,6 @@ namespace Elsa.Activities.Telnyx.Activities public static ISetupActivity WithCommandId(this ISetupActivity setup, Func value) => setup.Set(x => x.CommandId, value); public static ISetupActivity WithCommandId(this ISetupActivity setup, string? value) => setup.Set(x => x.CommandId, value); - public static ISetupActivity WithClientState(this ISetupActivity setup, Func> value) => setup.Set(x => x.ClientState, value); - public static ISetupActivity WithClientState(this ISetupActivity setup, Func value) => setup.Set(x => x.ClientState, value); - public static ISetupActivity WithClientState(this ISetupActivity setup, Func> value) => setup.Set(x => x.ClientState, value); - public static ISetupActivity WithClientState(this ISetupActivity setup, Func value) => setup.Set(x => x.ClientState, value); - public static ISetupActivity WithClientState(this ISetupActivity setup, string? value) => setup.Set(x => x.ClientState, value); - public static ISetupActivity WithCustomHeaders(this ISetupActivity setup, Func?>> value) => setup.Set(x => x.CustomHeaders, value); public static ISetupActivity WithCustomHeaders(this ISetupActivity setup, Func?> value) => setup.Set(x => x.CustomHeaders, value); public static ISetupActivity WithCustomHeaders(this ISetupActivity setup, Func?>> value) => setup.Set(x => x.CustomHeaders, value); @@ -254,5 +273,11 @@ namespace Elsa.Activities.Telnyx.Activities public static ISetupActivity WithWebhookUrlMethod(this ISetupActivity setup, Func> value) => setup.Set(x => x.WebhookUrlMethod, value); public static ISetupActivity WithWebhookUrlMethod(this ISetupActivity setup, Func value) => setup.Set(x => x.WebhookUrlMethod, value); public static ISetupActivity WithWebhookUrlMethod(this ISetupActivity setup, string? value) => setup.Set(x => x.WebhookUrlMethod, value); + + public static ISetupActivity WithSuspendWorkflow(this ISetupActivity setup, Func> value) => setup.Set(x => x.SuspendWorkflow, value); + public static ISetupActivity WithSuspendWorkflow(this ISetupActivity setup, Func value) => setup.Set(x => x.SuspendWorkflow, value); + public static ISetupActivity WithSuspendWorkflow(this ISetupActivity setup, Func> value) => setup.Set(x => x.SuspendWorkflow, value); + public static ISetupActivity WithSuspendWorkflow(this ISetupActivity setup, Func value) => setup.Set(x => x.SuspendWorkflow, value); + public static ISetupActivity WithSuspendWorkflow(this ISetupActivity setup, bool value) => setup.Set(x => x.SuspendWorkflow, value); } } \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingAudio.cs b/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingAudio.cs index 04b5f119e..a0168ca9e 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingAudio.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingAudio.cs @@ -18,7 +18,7 @@ namespace Elsa.Activities.Telnyx.Activities [Job( Category = Constants.Category, Description = "Play an audio file on the call until the required DTMF signals are gathered to build interactive menus.", - Outcomes = new[] { TelnyxOutcomeNames.Pending, TelnyxOutcomeNames.GatherCompleted, TelnyxOutcomeNames.CallIsNoLongerActive }, + Outcomes = new[] { TelnyxOutcomeNames.GatheringInput, TelnyxOutcomeNames.GatherCompleted, TelnyxOutcomeNames.CallIsNoLongerActive }, DisplayName = "Gather Using Audio" )] public class GatherUsingAudio : Activity @@ -121,7 +121,7 @@ namespace Elsa.Activities.Telnyx.Activities try { await _telnyxClient.Calls.GatherUsingAudioAsync(callControlId, request, context.CancellationToken); - return Combine(Outcome(TelnyxOutcomeNames.Pending), Suspend()); + return Combine(Outcome(TelnyxOutcomeNames.GatheringInput), Suspend()); } catch (ApiException e) { diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingSpeak.cs b/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingSpeak.cs index f5b6f2f6f..132011faa 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingSpeak.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/GatherUsingSpeak.cs @@ -21,7 +21,7 @@ namespace Elsa.Activities.Telnyx.Activities [Action( Category = Constants.Category, Description = "Convert text to speech and play it on the call until the required DTMF signals are gathered to build interactive menus.", - Outcomes = new[] { TelnyxOutcomeNames.Pending, TelnyxOutcomeNames.GatherCompleted, TelnyxOutcomeNames.CallIsNoLongerActive }, + Outcomes = new[] { TelnyxOutcomeNames.GatheringInput, TelnyxOutcomeNames.GatherCompleted, TelnyxOutcomeNames.CallIsNoLongerActive }, DisplayName = "Gather Using Speak" )] public class GatherUsingSpeak : Activity @@ -151,7 +151,7 @@ namespace Elsa.Activities.Telnyx.Activities try { await _telnyxClient.Calls.GatherUsingSpeakAsync(callControlId, request, context.CancellationToken); - return Combine(Outcome(TelnyxOutcomeNames.Pending), Suspend()); + return Combine(Outcome(TelnyxOutcomeNames.GatheringInput), Suspend()); } catch (ApiException e) { diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/SpeakText.cs b/src/activities/Elsa.Activities.Telnyx/Activities/SpeakText.cs index ea8939b7f..4485ceacb 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/SpeakText.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/SpeakText.cs @@ -109,7 +109,7 @@ namespace Elsa.Activities.Telnyx.Activities try { await _telnyxClient.Calls.SpeakTextAsync(callControlId, request, context.CancellationToken); - return Combine(Outcome(TelnyxOutcomeNames.Pending), Suspend()); + return Combine(Outcome(TelnyxOutcomeNames.Speaking), Suspend()); } catch (ApiException e) { diff --git a/src/activities/Elsa.Activities.Telnyx/Activities/TransferCall.cs b/src/activities/Elsa.Activities.Telnyx/Activities/TransferCall.cs index d3694b1f5..6caed1a78 100644 --- a/src/activities/Elsa.Activities.Telnyx/Activities/TransferCall.cs +++ b/src/activities/Elsa.Activities.Telnyx/Activities/TransferCall.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Elsa.Activities.Telnyx.Client.Models; using Elsa.Activities.Telnyx.Client.Services; using Elsa.Activities.Telnyx.Extensions; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Call; using Elsa.ActivityResults; using Elsa.Attributes; using Elsa.Builders; @@ -19,7 +20,14 @@ namespace Elsa.Activities.Telnyx.Activities [Action( Category = Constants.Category, Description = "Transfer a call to a new destination", - Outcomes = new[] { OutcomeNames.Done }, + Outcomes = new[] + { + TelnyxOutcomeNames.Transferring, + TelnyxOutcomeNames.CallInitiated, + TelnyxOutcomeNames.Bridged, + TelnyxOutcomeNames.Answered, + TelnyxOutcomeNames.Hangup + }, DisplayName = "Transfer Call" )] public class TransferCall : Activity @@ -35,23 +43,23 @@ namespace Elsa.Activities.Telnyx.Activities Label = "Call Control ID", Hint = "Unique identifier and token for controlling the call.", Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? CallControlId { get; set; } = default!; - [ActivityInput(Label = "To", Hint = "The DID or SIP URI to dial out and bridge to the given call.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + [ActivityInput(Label = "To", Hint = "The DID or SIP URI to dial out and bridge to the given call.", SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public string To { get; set; } = default!; [ActivityInput( Hint = "The 'from' number to be used as the caller id presented to the destination ('To' number). The number should be in +E164 format. This attribute will default to the 'From' number of the original call if omitted.", - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? From { get; set; } [ActivityInput( Hint = "The string to be used as the caller id name (SIP From Display Name) presented to the destination ('To' number). The string should have a maximum of 128 characters, containing only letters, numbers, spaces, and -_~!.+ special characters. If omitted, the display name will be the same as the number in the 'From' field.", - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? FromDisplayName { get; set; } @@ -59,8 +67,8 @@ namespace Elsa.Activities.Telnyx.Activities Label = "Answering Machine Detection", Hint = "Enables Answering Machine Detection.", UIHint = ActivityInputUIHints.Dropdown, - Options = new[] { "disabled", "detect", "detect_beep", "detect_words", "greeting_end" }, - SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid } + Options = new[] {"disabled", "detect", "detect_beep", "detect_words", "greeting_end"}, + SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? AnsweringMachineDetection { get; set; } @@ -76,46 +84,46 @@ namespace Elsa.Activities.Telnyx.Activities Label = "Command ID", Hint = "Use this field to avoid duplicate commands. Telnyx will ignore commands with the same Command ID.", Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? CommandId { get; set; } [ActivityInput( Label = "Audio URL", Hint = "Audio URL to be played back when the transfer destination answers before bridging the call. The URL can point to either a WAV or MP3 file.", - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public Uri? AudioUrl { get; set; } [ActivityInput( Hint = "Use this field to add state to every subsequent webhook. It must be a valid Base-64 encoded string.", Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public string? ClientState { get; set; } [ActivityInput( Hint = "Use this field to add state to every subsequent webhook for the new leg. It must be a valid Base-64 encoded string.", Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? TargetLegClientState { get; set; } [ActivityInput(Hint = "Custom headers to be added to the SIP INVITE.", Category = PropertyCategories.Advanced, UIHint = ActivityInputUIHints.Json)] public IList
? CustomHeaders { get; set; } - [ActivityInput(Label = "SIP Authentication Username", Hint = "SIP Authentication username used for SIP challenges.", Category = "SIP Authentication", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + [ActivityInput(Label = "SIP Authentication Username", Hint = "SIP Authentication username used for SIP challenges.", Category = "SIP Authentication", SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public string? SipAuthUsername { get; set; } - [ActivityInput(Label = "SIP Authentication Password", Hint = "SIP Authentication password used for SIP challenges.", Category = "SIP Authentication", SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + [ActivityInput(Label = "SIP Authentication Password", Hint = "SIP Authentication password used for SIP challenges.", Category = "SIP Authentication", SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public string? SipAuthPassword { get; set; } - [ActivityInput(Label = "Time Limit", Hint = "Sets the maximum duration of a Call Control Leg in seconds.", Category = PropertyCategories.Advanced, SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] + [ActivityInput(Label = "Time Limit", Hint = "Sets the maximum duration of a Call Control Leg in seconds.", Category = PropertyCategories.Advanced, SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid})] public int? TimeLimitSecs { get; set; } [ActivityInput( Label = "Timeout", Hint = "The number of seconds that Telnyx will wait for the call to be answered by the destination to which it is being transferred.", Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public int? TimeoutSecs { get; set; } @@ -123,7 +131,7 @@ namespace Elsa.Activities.Telnyx.Activities Label = "Webhook URL", Hint = "Use this field to override the URL for which Telnyx will send subsequent webhooks to for this call.", Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? WebhookUrl { get; set; } @@ -131,22 +139,36 @@ namespace Elsa.Activities.Telnyx.Activities Label = "Webhook URL Method", Hint = "HTTP request type used for Webhook URL", UIHint = ActivityInputUIHints.Dropdown, - Options = new[] { "GET", "POST" }, + Options = new[] {"GET", "POST"}, Category = PropertyCategories.Advanced, - SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid } + SupportedSyntaxes = new[] {SyntaxNames.Literal, SyntaxNames.JavaScript, SyntaxNames.Liquid} )] public string? WebhookUrlMethod { get; set; } protected override async ValueTask OnExecuteAsync(ActivityExecutionContext context) { await TransferCallAsync(context); - return Done(); + return Combine(Outcome(TelnyxOutcomeNames.Transferring), Suspend()); + } + + protected override IActivityExecutionResult OnResume(ActivityExecutionContext context) + { + var payload = context.GetInput(); + + return payload switch + { + CallAnsweredPayload callAnsweredPayload => Outcome(TelnyxOutcomeNames.Answered, callAnsweredPayload), + CallBridgedPayload callBridgedPayload => Outcome(TelnyxOutcomeNames.Bridged, callBridgedPayload), + CallHangupPayload callHangupPayload => Outcome(TelnyxOutcomeNames.Hangup, callHangupPayload), + CallInitiatedPayload callInitiatedPayload => Outcome(TelnyxOutcomeNames.CallInitiated, callInitiatedPayload), + _ => throw new ArgumentOutOfRangeException(nameof(payload)) + }; } private async ValueTask TransferCallAsync(ActivityExecutionContext context) { var fromNumber = context.GetFromNumber(From); - + var request = new TransferCallRequest( To, fromNumber, diff --git a/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeBridgeCalls.cs b/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeBridgeCalls.cs new file mode 100644 index 000000000..80c93d28e --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeBridgeCalls.cs @@ -0,0 +1,13 @@ +using Elsa.Activities.Telnyx.Activities; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Call; +using Elsa.Services; + +namespace Elsa.Activities.Telnyx.Handlers +{ + public class ResumeBridgeCalls : ResumeWebhookDrivenActivity + { + public ResumeBridgeCalls(IWorkflowLaunchpad workflowLaunchpad) : base(workflowLaunchpad) + { + } + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeDial.cs b/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeDial.cs new file mode 100644 index 000000000..75271f63d --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeDial.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using Elsa.Activities.Telnyx.Activities; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Call; +using Elsa.Services; + +namespace Elsa.Activities.Telnyx.Handlers +{ + public class ResumeDial : ResumeWebhookDrivenActivity + { + public ResumeDial(IWorkflowLaunchpad workflowLaunchpad) : base(workflowLaunchpad) + { + } + + protected override IEnumerable GetSupportedPayloadTypes() => new[] {typeof(CallInitiatedPayload), typeof(CallAnsweredPayload), typeof(CallHangupPayload)}; + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeTransferCall.cs b/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeTransferCall.cs new file mode 100644 index 000000000..56535c94e --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeTransferCall.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using Elsa.Activities.Telnyx.Activities; +using Elsa.Activities.Telnyx.Webhooks.Payloads.Call; +using Elsa.Services; + +namespace Elsa.Activities.Telnyx.Handlers +{ + public class ResumeTransferCall : ResumeWebhookDrivenActivity + { + public ResumeTransferCall(IWorkflowLaunchpad workflowLaunchpad) : base(workflowLaunchpad) + { + } + + protected override IEnumerable GetSupportedPayloadTypes() => new[] {typeof(CallAnsweredPayload), typeof(CallInitiatedPayload), typeof(CallBridgedPayload), typeof(CallHangupPayload)}; + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeWebhookDrivenActivity.cs b/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeWebhookDrivenActivity.cs index 4b6e875d5..ef360e41c 100644 --- a/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeWebhookDrivenActivity.cs +++ b/src/activities/Elsa.Activities.Telnyx/Handlers/ResumeWebhookDrivenActivity.cs @@ -1,6 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; -using Elsa.Activities.Telnyx.Activities; using Elsa.Activities.Telnyx.Models; using Elsa.Activities.Telnyx.Providers.Bookmarks; using Elsa.Activities.Telnyx.Webhooks.Events; @@ -11,27 +13,41 @@ using MediatR; namespace Elsa.Activities.Telnyx.Handlers { - public abstract class ResumeWebhookDrivenActivity : INotificationHandler where TPayload: CallPayload + public abstract class ResumeWebhookDrivenActivity : ResumeWebhookDrivenActivity where TPayload : CallPayload + { + protected ResumeWebhookDrivenActivity(IWorkflowLaunchpad workflowLaunchpad) : base(workflowLaunchpad) + { + } + + protected override IEnumerable GetSupportedPayloadTypes() => new[] {typeof(TPayload)}; + } + + public abstract class ResumeWebhookDrivenActivity : INotificationHandler { private readonly IWorkflowLaunchpad _workflowLaunchpad; protected ResumeWebhookDrivenActivity(IWorkflowLaunchpad workflowLaunchpad) => _workflowLaunchpad = workflowLaunchpad; protected virtual string ActivityTypeName => typeof(TActivity).Name; + protected abstract IEnumerable GetSupportedPayloadTypes(); public async Task Handle(TelnyxWebhookReceived notification, CancellationToken cancellationToken) { - if (notification.Webhook.Data.Payload is not TPayload payload) + var supportedPayloadTypes = GetSupportedPayloadTypes().ToHashSet(); + var receivedPayload = (CallPayload) notification.Webhook.Data.Payload; + var receivedPayloadType = receivedPayload.GetType(); + + if (!supportedPayloadTypes.Contains(receivedPayloadType)) return; - - var correlationId = GetCorrelationId(payload); + + var correlationId = GetCorrelationId(receivedPayload); var trigger = CreateBookmark(); var bookmark = CreateBookmark(); var context = new CollectWorkflowsContext(ActivityTypeName, bookmark, trigger, correlationId); - await _workflowLaunchpad.CollectAndDispatchWorkflowsAsync(context, payload, cancellationToken); + await _workflowLaunchpad.CollectAndDispatchWorkflowsAsync(context, receivedPayload, cancellationToken); } - + protected virtual IBookmark CreateBookmark() => new GatherUsingSpeakBookmark(); - - private string GetCorrelationId(TPayload payload) + + private string GetCorrelationId(CallPayload payload) { if (!string.IsNullOrWhiteSpace(payload.ClientState)) { diff --git a/src/activities/Elsa.Activities.Telnyx/OutcomeNames.cs b/src/activities/Elsa.Activities.Telnyx/OutcomeNames.cs index 21bad4cdb..d9fb78766 100644 --- a/src/activities/Elsa.Activities.Telnyx/OutcomeNames.cs +++ b/src/activities/Elsa.Activities.Telnyx/OutcomeNames.cs @@ -4,9 +4,21 @@ { public const string CallIsNoLongerActive = "Call Is No Longer Active"; public const string Pending = "Pending"; + public const string Connected = "Connected"; + public const string NoResponse = "No Response"; + public const string Dialing = "Dialing"; + public const string CallInitiated = "CallInitiated"; public const string Answered = "Answered"; + public const string Hangup = "Hangup"; + public const string GatheringInput = "Gathering Input"; public const string GatherCompleted = "Gather Completed"; - public const string InputReceived = "Input Received"; + public const string Transferring = "Transferring"; + public const string Bridging = "Bridging"; + public const string Bridged = "Bridged"; + public const string LegABridged = "Call A Answered"; + public const string LegBBridged = "Call B Answered"; + public const string MachineDetectionEnded = "Machine Detection Ended"; + public const string MachineGreetingEnded = "Machine Greeting Ended"; public const string FinishedSpeaking = "Finished Speaking"; public const string Speaking = "Speaking"; public const string Recording = "Recording"; diff --git a/src/activities/Elsa.Activities.Telnyx/Providers/Bookmarks/BridgeCallsBookmark.cs b/src/activities/Elsa.Activities.Telnyx/Providers/Bookmarks/BridgeCallsBookmark.cs new file mode 100644 index 000000000..65fbadf6a --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Providers/Bookmarks/BridgeCallsBookmark.cs @@ -0,0 +1,13 @@ +using Elsa.Activities.Telnyx.Activities; +using Elsa.Bookmarks; + +namespace Elsa.Activities.Telnyx.Providers.Bookmarks +{ + public class BridgeCallsBookmark : IBookmark + { + } + + public class BridgeCallsBookmarkProvider : DefaultBookmarkProvider + { + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Providers/Bookmarks/DialBookmark.cs b/src/activities/Elsa.Activities.Telnyx/Providers/Bookmarks/DialBookmark.cs new file mode 100644 index 000000000..8e483be28 --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Providers/Bookmarks/DialBookmark.cs @@ -0,0 +1,13 @@ +using Elsa.Activities.Telnyx.Activities; +using Elsa.Bookmarks; + +namespace Elsa.Activities.Telnyx.Providers.Bookmarks +{ + public class DialBookmark : IBookmark + { + } + + public class DialBookmarkProvider : DefaultBookmarkProvider + { + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.Telnyx/Providers/Bookmarks/TransferCallBookmark.cs b/src/activities/Elsa.Activities.Telnyx/Providers/Bookmarks/TransferCallBookmark.cs new file mode 100644 index 000000000..2ac7c8e2f --- /dev/null +++ b/src/activities/Elsa.Activities.Telnyx/Providers/Bookmarks/TransferCallBookmark.cs @@ -0,0 +1,13 @@ +using Elsa.Activities.Telnyx.Activities; +using Elsa.Bookmarks; + +namespace Elsa.Activities.Telnyx.Providers.Bookmarks +{ + public class TransferCallBookmark : IBookmark + { + } + + public class TransferCallBookmarkProvider : DefaultBookmarkProvider + { + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/ActivityResults/OutputResult.cs b/src/core/Elsa.Abstractions/ActivityResults/OutputResult.cs index 1250f4a68..c69fe7f6d 100644 --- a/src/core/Elsa.Abstractions/ActivityResults/OutputResult.cs +++ b/src/core/Elsa.Abstractions/ActivityResults/OutputResult.cs @@ -3,7 +3,6 @@ using Elsa.Services.Models; namespace Elsa.ActivityResults { - [Obsolete("Use activity output properties to return output.")] public class OutputResult : ActivityExecutionResult { public OutputResult(object? output) => Output = output; diff --git a/src/core/Elsa.Abstractions/Extensions/JObjectExtensions.cs b/src/core/Elsa.Abstractions/Extensions/JObjectExtensions.cs index 0faed24d0..4721e83fd 100644 --- a/src/core/Elsa.Abstractions/Extensions/JObjectExtensions.cs +++ b/src/core/Elsa.Abstractions/Extensions/JObjectExtensions.cs @@ -1,4 +1,5 @@ using System; +using Elsa.Serialization.Converters; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NodaTime; @@ -19,6 +20,16 @@ namespace Elsa return item.ToObject(CreateSerializer())!; } + + public static object? GetState(this JObject? state, string key, Type targetType) + { + var item = state?.GetValue(key, StringComparison.OrdinalIgnoreCase); + + if (item == null || item.Type == JTokenType.Null) + return null; + + return item.ToObject(targetType, CreateSerializer())!; + } public static T? GetState(this JObject? state, Type type, string key) => state.GetState(type, key, () => default!); @@ -32,16 +43,19 @@ namespace Elsa public static bool HasKey(this JObject state, string key) => state.ContainsKey(key); - private static JsonSerializer CreateSerializer() + public static JObject CreateState(object value) => JObject.FromObject(value, CreateSerializer()); + + public static JsonSerializer CreateSerializer() { var serializer = new JsonSerializer(); - + serializer.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); serializer.NullValueHandling = NullValueHandling.Ignore; serializer.ReferenceLoopHandling = ReferenceLoopHandling.Serialize; serializer.PreserveReferencesHandling = PreserveReferencesHandling.Objects; serializer.TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple; serializer.TypeNameHandling = TypeNameHandling.Auto; + serializer.Converters.Add(new TypeJsonConverter()); return serializer; } diff --git a/src/core/Elsa.Abstractions/Services/Activity.cs b/src/core/Elsa.Abstractions/Services/Activity.cs index f35cc0969..d9dbe49c9 100644 --- a/src/core/Elsa.Abstractions/Services/Activity.cs +++ b/src/core/Elsa.Abstractions/Services/Activity.cs @@ -34,18 +34,12 @@ namespace Elsa.Services protected virtual IActivityExecutionResult OnResume() => Done(); protected virtual NoopResult Noop() => new(); protected virtual OutcomeResult Done() => Outcome(OutcomeNames.Done); - - [Obsolete("Do not use methods that accept an output. Use Done() instead and activity output properties to return output.")] protected virtual CombinedResult Done(object? output) => Combine(Output(output), Done()); protected virtual OutcomeResult Outcomes(IEnumerable outcomes) => new(outcomes); protected virtual OutcomeResult Outcomes(params string[] outcomes) => Outcomes((IEnumerable) outcomes); protected virtual OutcomeResult Outcome(string outcome) => Outcomes(outcome); - - [Obsolete("Do not use methods that accept an output. Use Outcome(string outcome) instead and activity output properties to return output.")] protected virtual CombinedResult Outcome(string outcome, object? output) => Combine(Output(output), Outcome(outcome)); - - [Obsolete("Use activity output properties to return output.")] protected virtual OutputResult Output(object? output) => new(output); protected virtual SuspendResult Suspend() => new(); diff --git a/src/core/Elsa.Abstractions/Services/ActivityPropertyProviders.cs b/src/core/Elsa.Abstractions/Services/ActivityPropertyProviders.cs index 81984d4bc..71cbc726e 100644 --- a/src/core/Elsa.Abstractions/Services/ActivityPropertyProviders.cs +++ b/src/core/Elsa.Abstractions/Services/ActivityPropertyProviders.cs @@ -50,7 +50,7 @@ namespace Elsa.Services public async ValueTask SetActivityPropertiesAsync(IActivity activity, ActivityExecutionContext activityExecutionContext, CancellationToken cancellationToken = default) { - var properties = activity.GetType().GetProperties().Where(IsActivityProperty).ToList(); + var properties = activity.GetType().GetProperties().Where(IsActivityInputProperty).ToList(); var providers = GetProviders(activity.Id); foreach (var property in properties) @@ -85,7 +85,7 @@ namespace Elsa.Services } } - private bool IsActivityProperty(PropertyInfo property) => property.GetCustomAttribute() != null; + private bool IsActivityInputProperty(PropertyInfo property) => property.GetCustomAttribute() != null; public IEnumerator>> GetEnumerator() => _providers.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs b/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs index d64f77903..5daa58d75 100644 --- a/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs +++ b/src/core/Elsa.Abstractions/Services/Models/ActivityExecutionContext.cs @@ -40,17 +40,18 @@ namespace Elsa.Services.Models get => WorkflowExecutionContext.ContextId; set => WorkflowExecutionContext.ContextId = value; } + public IReadOnlyCollection Outcomes { get; set; } public object? Input { get; } public bool Resuming { get; } public bool IsFirstPass => WorkflowExecutionContext.IsFirstPass; - + public string? CorrelationId { get => WorkflowExecutionContext.CorrelationId; set => WorkflowExecutionContext.CorrelationId = value; } - + public CancellationToken CancellationToken { get; } public JObject GetData() => WorkflowInstance.ActivityData.GetItem(ActivityBlueprint.Id, () => new JObject()); @@ -60,19 +61,25 @@ namespace Elsa.Services.Models var data = GetData(); data.SetState(propertyName, value); } - + public T? GetState(string propertyName) { var data = GetData(); return data.GetState(propertyName); } - + public T GetState(string propertyName, Func defaultValue) { var data = GetData(); return data.GetState(propertyName, defaultValue); } - + + public object? GetState(string propertyName, Type targetType) + { + var data = GetData(); + return data.GetState(propertyName, targetType); + } + public T? GetState(Expression> propertyExpression) where TActivity : IActivity { var expression = (MemberExpression) propertyExpression.Body; @@ -81,27 +88,27 @@ namespace Elsa.Services.Models } public T? GetContainerState() => GetContainerState(typeof(T).Name); - + public T? GetContainerState(string key) { var parentActivityId = ActivityBlueprint.Parent?.Id; if (parentActivityId == null) return default; - + var parentData = WorkflowExecutionContext.WorkflowInstance.ActivityData.GetItem(parentActivityId); return parentData.GetState(key); } - public void SetContainerState(object? value) => SetContainerState(typeof(T).Name, value); - + public void SetContainerState(object? value) => SetContainerState(typeof(T).Name, value); + public void SetContainerState(string key, object? value) { var parentActivityId = ActivityBlueprint.Parent?.Id; if (parentActivityId == null) return; - + var parentData = WorkflowExecutionContext.WorkflowInstance.ActivityData.GetItem(parentActivityId); parentData?.SetState(key, value); } @@ -119,7 +126,7 @@ namespace Elsa.Services.Models public ActivityScope GetNamedScope(string activityName) => WorkflowExecutionContext.GetNamedScope(activityName); public void SetVariable(string name, object? value) => WorkflowExecutionContext.SetVariable(name, value); - + public T? SetVariable(string name, Func updater) { var value = GetVariable(name); @@ -151,7 +158,7 @@ namespace Elsa.Services.Models var activityTypeService = ServiceProvider.GetRequiredService(); return await activityTypeService.ActivateActivityAsync(ActivityBlueprint, cancellationToken); } - + public T? GetInput() => Input.ConvertTo(); public T? GetInput(Func defaultValue) => Input != null ? Input.ConvertTo() : defaultValue(); public T? GetInput(T? defaultValue) => Input != null ? Input.ConvertTo() : defaultValue; @@ -167,6 +174,7 @@ namespace Elsa.Services.Models public T GetWorkflowContext() => WorkflowExecutionContext.GetWorkflowContext(); public JObject GetActivityData() => GetActivityData(ActivityId); public JObject GetActivityData(string activityId) => WorkflowExecutionContext.GetActivityData(activityId); + public T? GetActivityProperty(Expression> propertyExpression) where TActivity : IActivity => WorkflowExecutionContext.GetActivityProperty(ActivityId, propertyExpression); public void Fault(Exception exception) => WorkflowExecutionContext.Fault(exception, ActivityId, Input, Resuming); } } \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs b/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs index e585d8bec..6c03667dd 100644 --- a/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs +++ b/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using Elsa.Events; @@ -203,6 +204,20 @@ namespace Elsa.Services.Models } public T GetOutputFrom(string activityName) => (T) GetOutputFrom(activityName)!; + + public T? GetActivityProperty(string activityId, Expression> propertyExpression) where TActivity : IActivity + { + var expression = (MemberExpression) propertyExpression.Body; + string propertyName = expression.Member.Name; + return GetActivityProperty(activityId, propertyName); + } + + public T? GetActivityProperty(string activityId, string propertyName) + { + var data = GetActivityData(activityId); + return data.GetState(propertyName); + } + public void SetWorkflowContext(object? value) => WorkflowContext = value; public object? GetWorkflowContext() => WorkflowContext; public T GetWorkflowContext() => (T) WorkflowContext!; diff --git a/src/core/Elsa.Core/Handlers/WriteWorkflowExecutionLog.cs b/src/core/Elsa.Core/Handlers/WriteWorkflowExecutionLog.cs index f1297d92e..008784d03 100644 --- a/src/core/Elsa.Core/Handlers/WriteWorkflowExecutionLog.cs +++ b/src/core/Elsa.Core/Handlers/WriteWorkflowExecutionLog.cs @@ -41,7 +41,6 @@ namespace Elsa.Handlers { Output = activityExecutionContext.Output, Outcomes = activityExecutionContext.Outcomes, - State = activityExecutionContext.GetActivityData() }; var resuming = activityExecutionContext.Resuming; diff --git a/src/core/Elsa.Core/Services/ActivityActivator.cs b/src/core/Elsa.Core/Services/ActivityActivator.cs index 0acb73560..6c09de004 100644 --- a/src/core/Elsa.Core/Services/ActivityActivator.cs +++ b/src/core/Elsa.Core/Services/ActivityActivator.cs @@ -1,7 +1,10 @@ using System; +using System.Linq; +using System.Reflection; using System.Threading; using System.Threading.Tasks; using Elsa.Activities.ControlFlow; +using Elsa.Attributes; using Elsa.Services.Models; namespace Elsa.Services @@ -14,15 +17,17 @@ namespace Elsa.Services { _elsaOptions = options; } - + public async Task ActivateActivityAsync(ActivityExecutionContext context, Type type, CancellationToken cancellationToken = default) { var activity = _elsaOptions.ActivityFactory.CreateService(type, context.ServiceProvider); activity.Data = context.GetData(); activity.Id = context.ActivityId; + ApplyStoredValues(context, activity); + // TODO: Make extensible / apply open/closed. - if(ShouldSetProperties(activity)) + if (ShouldSetProperties(context, activity)) { // TODO: Figure out how to deal with dynamically defined properties and what it means to set values to these. // ActivityTypes can have dynamic properties, so they need to be able to "intercept" when values are being applied. @@ -30,11 +35,24 @@ namespace Elsa.Services //var activityType = await _activityTypeService.GetActivityTypeAsync(activity.Type, cancellationToken); await context.WorkflowExecutionContext.WorkflowBlueprint.ActivityPropertyProviders.SetActivityPropertiesAsync(activity, context, context.CancellationToken); } - + return activity; } - private bool ShouldSetProperties(IActivity activity) + private void ApplyStoredValues(ActivityExecutionContext context, IActivity activity) + { + var properties = activity.GetType().GetProperties().Where(IsActivityProperty).ToList(); + + foreach (var property in properties) + { + var value = context.GetState(property.Name, property.PropertyType); + + if (value != null) + property.SetValue(activity, value); + } + } + + private bool ShouldSetProperties(ActivityExecutionContext context, IActivity activity) { if (IsReturningComposite(activity)) return false; @@ -44,7 +62,7 @@ namespace Elsa.Services activity.Data.SetState("Unwinding", false); return false; } - + if (IsReturningSwitch(activity)) { activity.Data.SetState("Unwinding", false); @@ -53,9 +71,10 @@ namespace Elsa.Services return true; } - + private bool IsReturningComposite(IActivity activity) => activity is CompositeActivity && activity.Data.GetState(nameof(CompositeActivity.IsScheduled)); private bool IsReturningIf(IActivity activity) => activity is If && activity.Data.GetState("Unwinding"); private bool IsReturningSwitch(IActivity activity) => activity is Switch && activity.Data.GetState("Unwinding"); + private bool IsActivityProperty(PropertyInfo property) => property.GetCustomAttribute() != null || property.GetCustomAttribute() != null; } } \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/WorkflowLaunchpad.cs b/src/core/Elsa.Core/Services/WorkflowLaunchpad.cs index e99d1e559..bd810b9f7 100644 --- a/src/core/Elsa.Core/Services/WorkflowLaunchpad.cs +++ b/src/core/Elsa.Core/Services/WorkflowLaunchpad.cs @@ -247,20 +247,23 @@ namespace Elsa.Services var correlationId = context.CorrelationId!; var lockKey = correlationId; - await using var handle = await AcquireLockAsync(lockKey, cancellationToken); - - var correlatedWorkflowInstanceCount = !string.IsNullOrWhiteSpace(correlationId) - ? await _workflowInstanceStore.CountAsync(new CorrelationIdSpecification(correlationId).WithStatus(WorkflowStatus.Suspended), cancellationToken) - : 0; - - _logger.LogDebug("Found {CorrelatedWorkflowCount} workflows with correlation ID {CorrelationId}", correlatedWorkflowInstanceCount, correlationId); - - if (correlatedWorkflowInstanceCount > 0) + await using (var handle = await AcquireLockAsync(lockKey, cancellationToken)) { - var bookmarkResults = context.Bookmark != null ? await _bookmarkFinder.FindBookmarksAsync(context.ActivityType, context.Bookmark, correlationId, context.TenantId, cancellationToken).ToList() : new List(); - _logger.LogDebug("Found {BookmarkCount} bookmarks for activity type {ActivityType}", bookmarkResults.Count, context.ActivityType); - return bookmarkResults.Select(x => new PendingWorkflow(x.WorkflowInstanceId, x.ActivityId)).ToList(); - } + var correlatedWorkflowInstanceCount = !string.IsNullOrWhiteSpace(correlationId) + ? await _workflowInstanceStore.CountAsync(new CorrelationIdSpecification(correlationId).WithStatus(WorkflowStatus.Suspended), cancellationToken) + : 0; + + _logger.LogDebug("Found {CorrelatedWorkflowCount} workflows with correlation ID {CorrelationId}", correlatedWorkflowInstanceCount, correlationId); + + if (correlatedWorkflowInstanceCount > 0) + { + var bookmarkResults = context.Bookmark != null + ? await _bookmarkFinder.FindBookmarksAsync(context.ActivityType, context.Bookmark, correlationId, context.TenantId, cancellationToken).ToList() + : new List(); + _logger.LogDebug("Found {BookmarkCount} bookmarks for activity type {ActivityType}", bookmarkResults.Count, context.ActivityType); + return bookmarkResults.Select(x => new PendingWorkflow(x.WorkflowInstanceId, x.ActivityId)).ToList(); + } + } // This ensures the lock handle is released before calling the next line (CollectStartableWorkflowsAsync), which also acquires a lock on the correlation ID. var startableWorkflows = await CollectStartableWorkflowsAsync(context, cancellationToken); return startableWorkflows.Select(x => new PendingWorkflow(x.WorkflowInstance.Id, x.ActivityId)).ToList(); diff --git a/src/designer/bindings/aspnet/Elsa.Designer.Components.Web/WorkflowStudio.razor b/src/designer/bindings/aspnet/Elsa.Designer.Components.Web/WorkflowStudio.razor index b0843423b..ae0315437 100644 --- a/src/designer/bindings/aspnet/Elsa.Designer.Components.Web/WorkflowStudio.razor +++ b/src/designer/bindings/aspnet/Elsa.Designer.Components.Web/WorkflowStudio.razor @@ -1,4 +1,4 @@ - + @code{ diff --git a/src/designer/elsa-workflows-studio/src/components/controls/elsa-monaco/elsa-monaco-utils.ts b/src/designer/elsa-workflows-studio/src/components/controls/elsa-monaco/elsa-monaco-utils.ts index a02c421a6..117d62cd8 100644 --- a/src/designer/elsa-workflows-studio/src/components/controls/elsa-monaco/elsa-monaco-utils.ts +++ b/src/designer/elsa-workflows-studio/src/components/controls/elsa-monaco/elsa-monaco-utils.ts @@ -12,7 +12,7 @@ export function initializeMonacoWorker(libPath?: string) { return; const origin = document.location.origin; - const baseUrl = `${origin}/${libPath}`; + const baseUrl = libPath.startsWith('http') ? libPath : `${origin}/${libPath}`; require.config({paths: {'vs': `${baseUrl}/vs`}}); win.MonacoEnvironment = {getWorkerUrl: () => proxy}; diff --git a/src/designer/elsa-workflows-studio/src/index.html b/src/designer/elsa-workflows-studio/src/index.html index 2a7843ce6..6dffced22 100644 --- a/src/designer/elsa-workflows-studio/src/index.html +++ b/src/designer/elsa-workflows-studio/src/index.html @@ -12,7 +12,7 @@ - + diff --git a/src/samples/dashboard/aspnetcore/ElsaDashboard.Samples.AspNetCore.Monolith/Program.cs b/src/samples/dashboard/aspnetcore/ElsaDashboard.Samples.AspNetCore.Monolith/Program.cs index 9588c73f6..12a97ad38 100644 --- a/src/samples/dashboard/aspnetcore/ElsaDashboard.Samples.AspNetCore.Monolith/Program.cs +++ b/src/samples/dashboard/aspnetcore/ElsaDashboard.Samples.AspNetCore.Monolith/Program.cs @@ -12,6 +12,8 @@ namespace ElsaDashboard.Samples.AspNetCore.Monolith public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + .ConfigureWebHostDefaults(webBuilder => { webBuilder + .UseStaticWebAssets() + .UseStartup(); }); } } \ No newline at end of file diff --git a/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorServer/ElsaDashboard.Samples.BlazorServer.csproj b/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorServer/ElsaDashboard.Samples.BlazorServer.csproj index b17878180..918065b68 100644 --- a/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorServer/ElsaDashboard.Samples.BlazorServer.csproj +++ b/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorServer/ElsaDashboard.Samples.BlazorServer.csproj @@ -1,22 +1,11 @@ - net6.0 + net5.0 - <_ContentIncludedByDefault Remove="wwwroot\css\bootstrap\bootstrap.min.css" /> - <_ContentIncludedByDefault Remove="wwwroot\css\bootstrap\bootstrap.min.css.map" /> - <_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\FONT-LICENSE" /> - <_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\css\open-iconic-bootstrap.min.css" /> - <_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.eot" /> - <_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.otf" /> - <_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.svg" /> - <_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.ttf" /> - <_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.woff" /> - <_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\ICON-LICENSE" /> - <_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\README.md" /> - <_ContentIncludedByDefault Remove="wwwroot\css\site.css" /> + diff --git a/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorServer/Pages/Index.razor b/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorServer/Pages/Index.razor index cab6db75a..d332f4481 100644 --- a/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorServer/Pages/Index.razor +++ b/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorServer/Pages/Index.razor @@ -1,3 +1,3 @@ @page "/" - \ No newline at end of file + \ No newline at end of file diff --git a/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorServer/Pages/_Host.cshtml b/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorServer/Pages/_Host.cshtml index 5c79c29cf..a32fa7ce1 100644 --- a/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorServer/Pages/_Host.cshtml +++ b/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorServer/Pages/_Host.cshtml @@ -12,12 +12,12 @@ ElsaDashboard.Samples.BlazorServer - - - - - - + + + + + + diff --git a/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorServer/_Imports.razor b/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorServer/_Imports.razor index 4e350137b..860d97a17 100644 --- a/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorServer/_Imports.razor +++ b/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorServer/_Imports.razor @@ -6,5 +6,6 @@ @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop +@using Elsa.Designer.Components.Web @using ElsaDashboard.Samples.BlazorServer @using ElsaDashboard.Samples.BlazorServer.Shared \ No newline at end of file diff --git a/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorWasm/ElsaDashboard.Samples.BlazorWasm.csproj b/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorWasm/ElsaDashboard.Samples.BlazorWasm.csproj index 265655a38..d6c6d6b93 100644 --- a/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorWasm/ElsaDashboard.Samples.BlazorWasm.csproj +++ b/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorWasm/ElsaDashboard.Samples.BlazorWasm.csproj @@ -1,29 +1,14 @@ - net6.0 + net5.0 - - + + - - - <_ContentIncludedByDefault Remove="wwwroot\css\app.css" /> - <_ContentIncludedByDefault Remove="wwwroot\css\bootstrap\bootstrap.min.css" /> - <_ContentIncludedByDefault Remove="wwwroot\css\bootstrap\bootstrap.min.css.map" /> - <_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\FONT-LICENSE" /> - <_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\css\open-iconic-bootstrap.min.css" /> - <_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.eot" /> - <_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.otf" /> - <_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.svg" /> - <_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.ttf" /> - <_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\font\fonts\open-iconic.woff" /> - <_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\ICON-LICENSE" /> - <_ContentIncludedByDefault Remove="wwwroot\css\open-iconic\README.md" /> - - + diff --git a/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorWasm/Pages/Index.razor b/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorWasm/Pages/Index.razor index cab6db75a..d332f4481 100644 --- a/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorWasm/Pages/Index.razor +++ b/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorWasm/Pages/Index.razor @@ -1,3 +1,3 @@ @page "/" - \ No newline at end of file + \ No newline at end of file diff --git a/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorWasm/_Imports.razor b/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorWasm/_Imports.razor index 8df963f24..d6ea2e52d 100644 --- a/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorWasm/_Imports.razor +++ b/src/samples/dashboard/blazor/ElsaDashboard.Samples.BlazorWasm/_Imports.razor @@ -6,5 +6,6 @@ @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.AspNetCore.Components.WebAssembly.Http @using Microsoft.JSInterop +@using Elsa.Designer.Components.Web @using ElsaDashboard.Samples.BlazorWasm @using ElsaDashboard.Samples.BlazorWasm.Shared \ No newline at end of file diff --git a/src/scripting/Elsa.Scripting.JavaScript/Extensions/JObjectExtensions.cs b/src/scripting/Elsa.Scripting.JavaScript/Extensions/JObjectExtensions.cs index 613e62739..d2ea86184 100644 --- a/src/scripting/Elsa.Scripting.JavaScript/Extensions/JObjectExtensions.cs +++ b/src/scripting/Elsa.Scripting.JavaScript/Extensions/JObjectExtensions.cs @@ -22,7 +22,10 @@ namespace Elsa.Scripting.JavaScript.Extensions where value != null && value.GetType() == typeof(JArray) select key).ToList(); - jArrayKeys.ForEach(key => result[key] = ((JArray) result[key]).Values().Select(x => ((JValue) x).Value).ToArray()); + jArrayKeys.ForEach(key => result[key] = ((JArray) result[key]).Values().Select(x => + { + return x is JValue jValue ? jValue.Value : x is JProperty jProperty ? ((JValue) jProperty.Value).Value : x; + }).ToArray()); jObjectKeys.ForEach(key => result[key] = ToDictionary((JObject) result[key])); return result; diff --git a/src/scripting/Elsa.Scripting.JavaScript/Handlers/ConfigureJavaScriptEngine.cs b/src/scripting/Elsa.Scripting.JavaScript/Handlers/ConfigureJavaScriptEngine.cs index 03bcf1193..8f7a1b543 100644 --- a/src/scripting/Elsa.Scripting.JavaScript/Handlers/ConfigureJavaScriptEngine.cs +++ b/src/scripting/Elsa.Scripting.JavaScript/Handlers/ConfigureJavaScriptEngine.cs @@ -82,19 +82,18 @@ namespace Elsa.Scripting.JavaScript.Handlers foreach (var variable in variables.Data) engine.SetValue(variable.Key, variable.Value); - // TODO: Deprecated. Remove with next breaking version. - // Activity outputs. - foreach (var activity in workflowBlueprint.Activities.Where(x => x.Name is not null and not "" && workflowInstance.ActivityOutput.ContainsKey(x.Id))) - { - var output = new { Output = workflowInstance.ActivityOutput[activity.Id!] }; - engine.SetValue(activity.Name, output); - } - // Named activities. foreach (var activity in workflowBlueprint.Activities.Where(x => !string.IsNullOrWhiteSpace(x.Name))) { var state = activityExecutionContext.GetActivityData(activity.Id); var dictionary = state.ToDictionary(); + + // Output. + if (workflowInstance.ActivityOutput.ContainsKey(activity.Id)) + { + var output = workflowInstance.ActivityOutput[activity.Id]; + dictionary["Output"] = output; + } engine.SetValue(activity.Name, dictionary); } diff --git a/src/scripting/Elsa.Scripting.Liquid/Handlers/ConfigureLiquidEngine.cs b/src/scripting/Elsa.Scripting.Liquid/Handlers/ConfigureLiquidEngine.cs index 42450e2b2..3fa91ff08 100644 --- a/src/scripting/Elsa.Scripting.Liquid/Handlers/ConfigureLiquidEngine.cs +++ b/src/scripting/Elsa.Scripting.Liquid/Handlers/ConfigureLiquidEngine.cs @@ -76,11 +76,10 @@ namespace Elsa.Scripting.Liquid.Handlers { var activityExecutionContext = activityModel.ActivityExecutionContext; - // Deprecated. if (name == "Output") { var output = activityExecutionContext.GetOutputFrom(activityModel.ActivityName!); - return output != null ? new JObject(output) : default; + return output != null ? JObjectExtensions.CreateState(output) : default; } var workflowExecutionContext = activityExecutionContext.WorkflowExecutionContext; diff --git a/src/server/Elsa.Server.Api/Endpoints/Signals/Models.cs b/src/server/Elsa.Server.Api/Endpoints/Signals/Models.cs index 4c4c4ad45..9b1374841 100644 --- a/src/server/Elsa.Server.Api/Endpoints/Signals/Models.cs +++ b/src/server/Elsa.Server.Api/Endpoints/Signals/Models.cs @@ -1,13 +1,74 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Elsa.Services.Models; namespace Elsa.Server.Api.Endpoints.Signals { - public record DispatchSignalRequest(string? WorkflowInstanceId, string? CorrelationId, object? Input); + public record DispatchSignalRequest + { + // Need this for Swagger. + public DispatchSignalRequest() + { + } + + public DispatchSignalRequest(string? workflowInstanceId, string? correlationId, object? input) + { + WorkflowInstanceId = workflowInstanceId; + CorrelationId = correlationId; + Input = input; + } - public record DispatchSignalResponse(ICollection StartedWorkflows); + public string? WorkflowInstanceId { get; init; } + public string? CorrelationId { get; init; } + public object? Input { get; init; } + } - public record ExecuteSignalRequest(string? WorkflowInstanceId, string? CorrelationId, object? Input); + public record DispatchSignalResponse + { + // Need this for Swagger. + public DispatchSignalResponse() + { + } + + public DispatchSignalResponse(ICollection startedWorkflows) + { + StartedWorkflows = startedWorkflows; + } - public record ExecuteSignalResponse(ICollection StartedWorkflows); + public ICollection StartedWorkflows { get; init; } = default!; + } + + public record ExecuteSignalRequest + { + // Need this for Swagger. + public ExecuteSignalRequest() + { + } + + public ExecuteSignalRequest(string? workflowInstanceId, string? correlationId, object? input) + { + WorkflowInstanceId = workflowInstanceId; + CorrelationId = correlationId; + Input = input; + } + + public string? WorkflowInstanceId { get; init; } + public string? CorrelationId { get; init; } + public object? Input { get; init; } + } + + public record ExecuteSignalResponse + { + // Need this for Swagger. + public ExecuteSignalResponse() + { + } + + public ExecuteSignalResponse(ICollection startedWorkflows) + { + StartedWorkflows = startedWorkflows; + } + + public ICollection StartedWorkflows { get; init; } = new List(); + } } \ No newline at end of file