From 2f6ce896c68fa29988c6262040c867f14b495c2e Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 29 Apr 2021 22:43:41 +0200 Subject: [PATCH] Implement runtime list items provider --- .../Design/IRuntimeSelectListItemsProvider.cs | 11 +++++ .../RuntimeSelectListItemsProviderSettings.cs | 29 +++++++++++++ .../Converters/FullTypeJsonConverter.cs | 23 ++++++++++ .../ElsaServiceCollectionExtensions.cs | 2 + .../elsa-workflows-studio/src/components.d.ts | 2 + .../elsa-dropdown-property.tsx | 38 +++++++++++++--- .../elsa-workflow-blueprint-viewer-screen.tsx | 10 ++--- .../src/models/domain.ts | 10 +++++ .../src/services/elsa-client.ts | 19 +++++++- .../Activities/VehicleActivity.cs | 37 ++++++++++++++++ .../Elsa.Samples.Server.Host/Startup.cs | 5 +++ .../Designer/RuntimeSelectListItems/Get.cs | 43 +++++++++++++++++++ 12 files changed, 215 insertions(+), 14 deletions(-) create mode 100644 src/core/Elsa.Abstractions/Design/IRuntimeSelectListItemsProvider.cs create mode 100644 src/core/Elsa.Abstractions/Design/RuntimeSelectListItemsProviderSettings.cs create mode 100644 src/core/Elsa.Abstractions/Serialization/Converters/FullTypeJsonConverter.cs create mode 100644 src/samples/server/Elsa.Samples.Server.Host/Activities/VehicleActivity.cs create mode 100644 src/server/Elsa.Server.Api/Endpoints/Designer/RuntimeSelectListItems/Get.cs diff --git a/src/core/Elsa.Abstractions/Design/IRuntimeSelectListItemsProvider.cs b/src/core/Elsa.Abstractions/Design/IRuntimeSelectListItemsProvider.cs new file mode 100644 index 000000000..20722e405 --- /dev/null +++ b/src/core/Elsa.Abstractions/Design/IRuntimeSelectListItemsProvider.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Elsa.Design +{ + public interface IRuntimeSelectListItemsProvider + { + ValueTask> GetItemsAsync(object? context = default, CancellationToken cancellationToken = default); + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Design/RuntimeSelectListItemsProviderSettings.cs b/src/core/Elsa.Abstractions/Design/RuntimeSelectListItemsProviderSettings.cs new file mode 100644 index 000000000..e35fb3d02 --- /dev/null +++ b/src/core/Elsa.Abstractions/Design/RuntimeSelectListItemsProviderSettings.cs @@ -0,0 +1,29 @@ +using System; +using Elsa.Serialization.Converters; +using Newtonsoft.Json; + +namespace Elsa.Design +{ + /// + /// Represents settings to be used by the designer to invoke an Elsa's API endpoint to retrieve list options at runtime. + /// + public class RuntimeSelectListItemsProviderSettings + { + public RuntimeSelectListItemsProviderSettings(Type providerType, object? context = default) + { + RuntimeSelectListItemsProviderType = providerType; + Context = context; + } + + /// + /// The type of the list items provider. + /// + [JsonConverter(typeof(FullTypeJsonConverter))] + public Type RuntimeSelectListItemsProviderType { get; } + + /// + /// Optionally provide an object containing useful information for the list select items provider to determine what items to provide. + /// + public object? Context { get; } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Serialization/Converters/FullTypeJsonConverter.cs b/src/core/Elsa.Abstractions/Serialization/Converters/FullTypeJsonConverter.cs new file mode 100644 index 000000000..8247a71eb --- /dev/null +++ b/src/core/Elsa.Abstractions/Serialization/Converters/FullTypeJsonConverter.cs @@ -0,0 +1,23 @@ +using System; +using Newtonsoft.Json; + +namespace Elsa.Serialization.Converters +{ + public class FullTypeJsonConverter : JsonConverter + { + public override bool CanRead => true; + public override bool CanWrite => true; + + public override void WriteJson(JsonWriter writer, Type? value, JsonSerializer serializer) + { + var typeName = value!.AssemblyQualifiedName; + serializer.Serialize(writer, typeName); + } + + public override Type ReadJson(JsonReader reader, Type objectType, Type? existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var typeName = serializer.Deserialize(reader)!; + return typeName is not null and not "" ? Type.GetType(typeName)! : default!; + } + } +} \ No newline at end of file diff --git a/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs b/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs index d22362682..9294698b5 100644 --- a/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs +++ b/src/core/Elsa.Core/Extensions/ElsaServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ using Elsa.Builders; using Elsa.Consumers; using Elsa.Converters; using Elsa.Decorators; +using Elsa.Design; using Elsa.Dispatch; using Elsa.Dispatch.Consumers; using Elsa.Events; @@ -102,6 +103,7 @@ namespace Microsoft.Extensions.DependencyInjection } public static IServiceCollection AddActivityPropertyOptionsProvider(this IServiceCollection services) where T : class, IActivityPropertyOptionsProvider => services.AddSingleton(); + public static IServiceCollection AddRuntimeSelectItemsProvider(this IServiceCollection services) where T : class, IRuntimeSelectListItemsProvider => services.AddScoped(); public static IServiceCollection AddActivityTypeProvider(this IServiceCollection services) where T : class, IActivityTypeProvider => services.AddSingleton(); private static ElsaOptionsBuilder AddWorkflowsCore(this ElsaOptionsBuilder options) diff --git a/src/designer/elsa-workflows-studio/src/components.d.ts b/src/designer/elsa-workflows-studio/src/components.d.ts index ec5cf6d43..3ac60a9c7 100644 --- a/src/designer/elsa-workflows-studio/src/components.d.ts +++ b/src/designer/elsa-workflows-studio/src/components.d.ts @@ -48,6 +48,7 @@ export namespace Components { interface ElsaDropdownProperty { "propertyDescriptor": ActivityPropertyDescriptor; "propertyModel": ActivityDefinitionProperty; + "serverUrl": string; } interface ElsaExpressionEditor { "context"?: string; @@ -543,6 +544,7 @@ declare namespace LocalJSX { interface ElsaDropdownProperty { "propertyDescriptor"?: ActivityPropertyDescriptor; "propertyModel"?: ActivityDefinitionProperty; + "serverUrl"?: string; } interface ElsaExpressionEditor { "context"?: string; diff --git a/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-dropdown-property/elsa-dropdown-property.tsx b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-dropdown-property/elsa-dropdown-property.tsx index 7c1a25615..dc5ffcd12 100644 --- a/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-dropdown-property/elsa-dropdown-property.tsx +++ b/src/designer/elsa-workflows-studio/src/components/editors/properties/elsa-dropdown-property/elsa-dropdown-property.tsx @@ -1,5 +1,7 @@ import {Component, h, Prop, State} from '@stencil/core'; -import {ActivityDefinitionProperty, ActivityPropertyDescriptor, SyntaxNames} from "../../../../models"; +import {ActivityDefinitionProperty, ActivityPropertyDescriptor, RuntimeSelectListItemsProviderSettings, SelectListItem, SyntaxNames} from "../../../../models"; +import Tunnel from "../../../../data/workflow-editor"; +import {createElsaClient} from "../../../../services/elsa-client"; @Component({ tag: 'elsa-dropdown-property', @@ -10,7 +12,10 @@ export class ElsaDropdownProperty { @Prop() propertyDescriptor: ActivityPropertyDescriptor; @Prop() propertyModel: ActivityDefinitionProperty; + @Prop({mutable: true}) serverUrl: string; @State() currentValue?: string; + + items: any[]; async componentWillLoad() { const defaultSyntax = this.propertyDescriptor.defaultSyntax || SyntaxNames.Literal; @@ -26,6 +31,25 @@ export class ElsaDropdownProperty { onDefaultSyntaxValueChanged(e: CustomEvent) { this.currentValue = e.detail; } + + async componentWillRender(){ + const propertyDescriptor = this.propertyDescriptor; + const options = propertyDescriptor.options; + let items = []; + + if (!!options.runtimeSelectListItemsProviderType) { + items = await this.fetchRuntimeItems(options); + } else { + items = options as Array || []; + } + + this.items = items; + } + + async fetchRuntimeItems(options: RuntimeSelectListItemsProviderSettings): Promise>{ + const elsaClient = createElsaClient(this.serverUrl); + return await elsaClient.designerApi.runtimeSelectItemsApi.get(options.runtimeSelectListItemsProviderType, options.context || {}); + } render() { const propertyDescriptor = this.propertyDescriptor; @@ -33,8 +57,8 @@ export class ElsaDropdownProperty { const propertyName = propertyDescriptor.name; const fieldId = propertyName; const fieldName = propertyName; - const options = propertyDescriptor.options as Array || []; const currentValue = this.currentValue; + const items = this.items; return ( @@ -54,3 +78,5 @@ export class ElsaDropdownProperty { ); } } + +Tunnel.injectProps(ElsaDropdownProperty, ['serverUrl']); \ No newline at end of file diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-blueprint-viewer/elsa-workflow-blueprint-viewer-screen/elsa-workflow-blueprint-viewer-screen.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-blueprint-viewer/elsa-workflow-blueprint-viewer-screen/elsa-workflow-blueprint-viewer-screen.tsx index 5c60b8104..524ab92bc 100644 --- a/src/designer/elsa-workflows-studio/src/components/screens/workflow-blueprint-viewer/elsa-workflow-blueprint-viewer-screen/elsa-workflow-blueprint-viewer-screen.tsx +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-blueprint-viewer/elsa-workflow-blueprint-viewer-screen/elsa-workflow-blueprint-viewer-screen.tsx @@ -1,17 +1,13 @@ import {Component, h, Host, Method, Prop, State, Watch} from '@stencil/core'; -import {eventBus} from '../../../../services/event-bus'; import * as collection from 'lodash/collection'; import { ActivityBlueprint, ActivityDefinitionProperty, ActivityDescriptor, ActivityModel, Connection, ConnectionModel, - EventTypes, getVersionOptionsString, SyntaxNames, - WorkflowBlueprint, WorkflowExecutionLogRecord, - WorkflowInstance, - WorkflowModel, - WorkflowPersistenceBehavior, - WorkflowStatus + SyntaxNames, + WorkflowBlueprint, WorkflowModel, + WorkflowPersistenceBehavior } from "../../../../models"; import {createElsaClient} from "../../../../services/elsa-client"; import {pluginManager} from '../../../../services/plugin-manager'; diff --git a/src/designer/elsa-workflows-studio/src/models/domain.ts b/src/designer/elsa-workflows-studio/src/models/domain.ts index a83741811..a0d36f9eb 100644 --- a/src/designer/elsa-workflows-studio/src/models/domain.ts +++ b/src/designer/elsa-workflows-studio/src/models/domain.ts @@ -291,6 +291,16 @@ export enum ActivityTraits { Job = 4 } +export interface SelectListItem { + text: string; + value: string; +} + +export interface RuntimeSelectListItemsProviderSettings { + runtimeSelectListItemsProviderType: string; + context?: any; +} + export class SyntaxNames { static readonly Literal = 'Literal'; static readonly JavaScript = 'JavaScript'; diff --git a/src/designer/elsa-workflows-studio/src/services/elsa-client.ts b/src/designer/elsa-workflows-studio/src/services/elsa-client.ts index 1c13ac2cc..5aef65bab 100644 --- a/src/designer/elsa-workflows-studio/src/services/elsa-client.ts +++ b/src/designer/elsa-workflows-studio/src/services/elsa-client.ts @@ -5,7 +5,7 @@ import { ActivityDescriptor, ConnectionDefinition, getVersionOptionsString, OrderBy, - PagedList, + PagedList, SelectListItem, Variables, VersionOptions, WorkflowBlueprint, WorkflowBlueprintSummary, WorkflowContextOptions, @@ -152,6 +152,14 @@ export const createElsaClient = function (serverUrl: string): ElsaClient { const response = await httpClient.get(`v1/scripting/javascript/type-definitions/${workflowDefinitionId}?t=${new Date().getTime()}&context=${context}`); return response.data; } + }, + designerApi:{ + runtimeSelectItemsApi:{ + get: async (providerTypeName: string, context?: any): Promise> => { + const response = await httpClient.post(`v1/designer/runtime-select-list-items/${providerTypeName}`, context); + return response.data; + } + } } } } @@ -163,6 +171,7 @@ export interface ElsaClient { workflowInstancesApi: WorkflowInstancesApi; workflowExecutionLogApi: WorkflowExecutionLogApi; scriptingApi: ScriptingApi; + designerApi: DesignerApi; } export interface ActivitiesApi { @@ -220,6 +229,14 @@ export interface ScriptingApi { getJavaScriptTypeDefinitions(workflowDefinitionId: string, context?: string): Promise } +export interface DesignerApi { + runtimeSelectItemsApi: RuntimeSelectItemsApi; +} + +export interface RuntimeSelectItemsApi { + get(providerTypeName: string, context?: any): Promise> +} + export interface SaveWorkflowDefinitionRequest { workflowDefinitionId?: string; name?: string; diff --git a/src/samples/server/Elsa.Samples.Server.Host/Activities/VehicleActivity.cs b/src/samples/server/Elsa.Samples.Server.Host/Activities/VehicleActivity.cs new file mode 100644 index 000000000..a49d845cb --- /dev/null +++ b/src/samples/server/Elsa.Samples.Server.Host/Activities/VehicleActivity.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Elsa.ActivityResults; +using Elsa.Attributes; +using Elsa.Design; +using Elsa.Expressions; +using Elsa.Metadata; +using Elsa.Services; + +namespace Elsa.Samples.Server.Host.Activities +{ + [Action] + public class VehicleActivity : Activity, IActivityPropertyOptionsProvider, IRuntimeSelectListItemsProvider + { + [ActivityProperty( + UIHint = ActivityPropertyUIHints.Dropdown, + OptionsProvider = typeof(VehicleActivity), + DefaultSyntax = SyntaxNames.Literal, + SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.Json, SyntaxNames.JavaScript, SyntaxNames.Liquid } + )] + public string? Brand { get; set; } + + public object GetOptions(PropertyInfo property) => new RuntimeSelectListItemsProviderSettings(GetType()); + + public ValueTask> GetItemsAsync(object? context, CancellationToken cancellationToken = default) + { + var brands = new[] { "BMW", "Peugot", "Tesla" }; + var items = brands.Select(x => new SelectListItem(x)).ToList(); + return new ValueTask>(items); + } + + protected override IActivityExecutionResult OnExecute() => Done(Brand); + } +} \ No newline at end of file diff --git a/src/samples/server/Elsa.Samples.Server.Host/Startup.cs b/src/samples/server/Elsa.Samples.Server.Host/Startup.cs index b2ae8d5fb..4d6e3043a 100644 --- a/src/samples/server/Elsa.Samples.Server.Host/Startup.cs +++ b/src/samples/server/Elsa.Samples.Server.Host/Startup.cs @@ -3,6 +3,7 @@ using Elsa.Activities.UserTask.Extensions; using Elsa.Persistence.EntityFramework.Core.Extensions; using Elsa.Persistence.EntityFramework.Sqlite; using Elsa.Persistence.YesSql; +using Elsa.Samples.Server.Host.Activities; using Elsa.Server.Hangfire.Extensions; using Hangfire; using Microsoft.AspNetCore.Builder; @@ -48,6 +49,8 @@ namespace Elsa.Samples.Server.Host }); services + .AddActivityPropertyOptionsProvider() + .AddRuntimeSelectItemsProvider() .AddElsa(elsa => elsa //.UseEntityFrameworkPersistence(ef => ef.UseSqlite()) .UseYesSqlPersistence() @@ -62,9 +65,11 @@ namespace Elsa.Samples.Server.Host .AddJavaScriptActivities() .AddUserTaskActivities() .AddTelnyx() + .AddActivitiesFrom() .AddWorkflowsFrom() ); + // Elsa API endpoints. services .AddElsaApiEndpoints() .AddElsaSwagger(); diff --git a/src/server/Elsa.Server.Api/Endpoints/Designer/RuntimeSelectListItems/Get.cs b/src/server/Elsa.Server.Api/Endpoints/Designer/RuntimeSelectListItems/Get.cs new file mode 100644 index 000000000..9839b5dcb --- /dev/null +++ b/src/server/Elsa.Server.Api/Endpoints/Designer/RuntimeSelectListItems/Get.cs @@ -0,0 +1,43 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Design; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Swashbuckle.AspNetCore.Annotations; + +namespace Elsa.Server.Api.Endpoints.Designer.RuntimeSelectListItems +{ + [ApiController] + [ApiVersion("1")] + [Route("v{apiVersion:apiVersion}/designer/runtime-select-list-items/{providerTypeName}")] + [Produces("application/json")] + public class Get : Controller + { + private readonly IServiceProvider _serviceProvider; + + public Get(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(OkObjectResult))] + [SwaggerOperation( + Summary = "Returns a list of items to be used by the list control requesting the items.", + Description = "Returns a list of items to be used by the list control requesting the items.", + OperationId = "Designer.RuntimeSelectListItems.Get", + Tags = new[] { "Designer.RuntimeSelectListItems" }) + ] + public async Task Handle(string providerTypeName, object? context = default, CancellationToken cancellationToken = default) + { + var type = Type.GetType(providerTypeName)!; + var provider = (IRuntimeSelectListItemsProvider)ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, type); + var items = (await provider.GetItemsAsync(context, cancellationToken)).ToList(); + + return Ok(items); + } + } +} \ No newline at end of file