Implement runtime list items provider

This commit is contained in:
Sipke Schoorstra 2021-04-29 22:43:41 +02:00
parent 1277003ca5
commit 2f6ce896c6
12 changed files with 215 additions and 14 deletions

View file

@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Elsa.Design
{
public interface IRuntimeSelectListItemsProvider
{
ValueTask<IEnumerable<SelectListItem>> GetItemsAsync(object? context = default, CancellationToken cancellationToken = default);
}
}

View file

@ -0,0 +1,29 @@
using System;
using Elsa.Serialization.Converters;
using Newtonsoft.Json;
namespace Elsa.Design
{
/// <summary>
/// Represents settings to be used by the designer to invoke an Elsa's API endpoint to retrieve list options at runtime.
/// </summary>
public class RuntimeSelectListItemsProviderSettings
{
public RuntimeSelectListItemsProviderSettings(Type providerType, object? context = default)
{
RuntimeSelectListItemsProviderType = providerType;
Context = context;
}
/// <summary>
/// The type of the list items provider.
/// </summary>
[JsonConverter(typeof(FullTypeJsonConverter))]
public Type RuntimeSelectListItemsProviderType { get; }
/// <summary>
/// Optionally provide an object containing useful information for the list select items provider to determine what items to provide.
/// </summary>
public object? Context { get; }
}
}

View file

@ -0,0 +1,23 @@
using System;
using Newtonsoft.Json;
namespace Elsa.Serialization.Converters
{
public class FullTypeJsonConverter : JsonConverter<Type>
{
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<string>(reader)!;
return typeName is not null and not "" ? Type.GetType(typeName)! : default!;
}
}
}

View file

@ -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<T>(this IServiceCollection services) where T : class, IActivityPropertyOptionsProvider => services.AddSingleton<IActivityPropertyOptionsProvider, T>();
public static IServiceCollection AddRuntimeSelectItemsProvider<T>(this IServiceCollection services) where T : class, IRuntimeSelectListItemsProvider => services.AddScoped<IRuntimeSelectListItemsProvider, T>();
public static IServiceCollection AddActivityTypeProvider<T>(this IServiceCollection services) where T : class, IActivityTypeProvider => services.AddSingleton<IActivityTypeProvider, T>();
private static ElsaOptionsBuilder AddWorkflowsCore(this ElsaOptionsBuilder options)

View file

@ -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;

View file

@ -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<any> || [];
}
this.items = items;
}
async fetchRuntimeItems(options: RuntimeSelectListItemsProviderSettings): Promise<Array<SelectListItem>>{
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<any> || [];
const currentValue = this.currentValue;
const items = this.items;
return (
<elsa-property-editor propertyDescriptor={propertyDescriptor}
@ -43,10 +67,10 @@ export class ElsaDropdownProperty {
editor-height="2.75em"
single-line={true}>
<select id={fieldId} name={fieldName} onChange={e => this.onChange(e)} class="mt-1 block focus:ring-blue-500 focus:border-blue-500 w-full shadow-sm sm:max-w-xs sm:text-sm border-gray-300 rounded-md">
{options.map(option => {
const optionIsObject = typeof (option) == 'object';
const value = optionIsObject ? option.value : option.toString();
const text = optionIsObject ? option.text : option.toString();
{items.map(item => {
const optionIsObject = typeof (item) == 'object';
const value = optionIsObject ? item.value : item.toString();
const text = optionIsObject ? item.text : item.toString();
return <option value={value} selected={value === currentValue}>{text}</option>;
})}
</select>
@ -54,3 +78,5 @@ export class ElsaDropdownProperty {
);
}
}
Tunnel.injectProps(ElsaDropdownProperty, ['serverUrl']);

View file

@ -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';

View file

@ -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';

View file

@ -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<string>(`v1/scripting/javascript/type-definitions/${workflowDefinitionId}?t=${new Date().getTime()}&context=${context}`);
return response.data;
}
},
designerApi:{
runtimeSelectItemsApi:{
get: async (providerTypeName: string, context?: any): Promise<Array<SelectListItem>> => {
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<string>
}
export interface DesignerApi {
runtimeSelectItemsApi: RuntimeSelectItemsApi;
}
export interface RuntimeSelectItemsApi {
get(providerTypeName: string, context?: any): Promise<Array<SelectListItem>>
}
export interface SaveWorkflowDefinitionRequest {
workflowDefinitionId?: string;
name?: string;

View file

@ -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<IEnumerable<SelectListItem>> GetItemsAsync(object? context, CancellationToken cancellationToken = default)
{
var brands = new[] { "BMW", "Peugot", "Tesla" };
var items = brands.Select(x => new SelectListItem(x)).ToList();
return new ValueTask<IEnumerable<SelectListItem>>(items);
}
protected override IActivityExecutionResult OnExecute() => Done(Brand);
}
}

View file

@ -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<VehicleActivity>()
.AddRuntimeSelectItemsProvider<VehicleActivity>()
.AddElsa(elsa => elsa
//.UseEntityFrameworkPersistence(ef => ef.UseSqlite())
.UseYesSqlPersistence()
@ -62,9 +65,11 @@ namespace Elsa.Samples.Server.Host
.AddJavaScriptActivities()
.AddUserTaskActivities()
.AddTelnyx()
.AddActivitiesFrom<VehicleActivity>()
.AddWorkflowsFrom<Startup>()
);
// Elsa API endpoints.
services
.AddElsaApiEndpoints()
.AddElsaSwagger();

View file

@ -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<IActionResult> 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);
}
}
}