Skip to content

Commit

Permalink
Generic cascading drop down (elsa-workflows#3063)
Browse files Browse the repository at this point in the history
* Modify Sample vehicleActivity and dropdown-property UI to manage Generic CascadingDropDown

* add dynamic vehicule activity

* change record

* add property DependsOnEvents and DependsOnValues in ActivityInputAttribute

* Update elsa-dropdown-property.tsx

* Update DynamicVehicleActivity.cs

Co-authored-by: Jérémie DEVILLARD <[email protected]>
Co-authored-by: Sipke Schoorstra <[email protected]>
  • Loading branch information
3 people authored Jun 23, 2022
1 parent c4bade7 commit 2583e8d
Show file tree
Hide file tree
Showing 3 changed files with 273 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,14 @@ public class ActivityInputAttribute : ActivityPropertyAttributeBase
/// A value indicating whether this property values should be used as outcomes in the workflow designer.
/// </summary>
public bool ConsiderValuesAsOutcomes { get; set; }

/// <summary>
/// A list of dependents property of the activity that can trigger event
/// </summary>
public string[]? DependsOnEvents { get; set; }
/// <summary>
/// A list of dependents property value of the activity that should be embedded with the event triggered
/// </summary>
public string[]? DependsOnValues { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, h, Prop, State } from '@stencil/core';
import {Component, h, Prop, State} from '@stencil/core';
import {
ActivityDefinitionProperty, ActivityModel,
ActivityPropertyDescriptor,
Expand All @@ -8,7 +8,7 @@ import {
SyntaxNames
} from "../../../../models";
import Tunnel from "../../../../data/workflow-editor";
import { getSelectListItems } from "../../../../utils/select-list-items";
import {getSelectListItems} from "../../../../utils/select-list-items";

@Component({
tag: 'elsa-dropdown-property',
Expand All @@ -19,26 +19,124 @@ export class ElsaDropdownProperty {
@Prop() activityModel: ActivityModel;
@Prop() propertyDescriptor: ActivityPropertyDescriptor;
@Prop() propertyModel: ActivityDefinitionProperty;
@Prop({ mutable: true }) serverUrl: string;
@Prop({mutable: true}) serverUrl: string;
@State() currentValue?: string;

selectList: SelectList = { items: [], isFlagsEnum: false };
selectList: SelectList = {items: [], isFlagsEnum: false};

async componentWillLoad() {
const defaultSyntax = this.propertyDescriptor.defaultSyntax || SyntaxNames.Literal;
this.currentValue = this.propertyModel.expressions[defaultSyntax] || undefined;
this.selectList = await getSelectListItems(this.serverUrl, this.propertyDescriptor);

if (this.currentValue == undefined) {
const firstOption : any = this.selectList.items[0];
if (this.propertyDescriptor.options?.context.dependsOnEvent) {
const initialDepsValue = {};
let index = 0;

if (firstOption) {
const optionIsObject = typeof (firstOption) == 'object';
this.currentValue = optionIsObject ? firstOption.value : firstOption.toString();
for (const dependsOnEvent of this.propertyDescriptor.options.context.dependsOnEvent) {

const element = dependsOnEvent.element;
const array = dependsOnEvent.array;

this.activityModel.properties.forEach((value, index, array) => {
initialDepsValue[value.name] = value.expressions["Literal"];
});

// Listen for change events on the dropdown list.
const dependentInputElement = await this.awaitElement('#' + element);

// Setup a change handler for when the user changes the selected dropdown list item.
dependentInputElement.addEventListener('change', async e => await this.ReloadSelectListFromDeps(e));

index++;
}

let options: RuntimeSelectListProviderSettings = {
context: {
...this.propertyDescriptor.options.context,
depValues: initialDepsValue
},
runtimeSelectListProviderType: (this.propertyDescriptor.options as RuntimeSelectListProviderSettings).runtimeSelectListProviderType

}
this.selectList = await getSelectListItems(this.serverUrl, {options: options} as ActivityPropertyDescriptor);
if (this.currentValue == undefined) {
const firstOption: any = this.selectList.items[0];

if (firstOption) {
const optionIsObject = typeof (firstOption) == 'object';
this.currentValue = optionIsObject ? firstOption.value : firstOption.toString();
}
}

} else {
this.selectList = await getSelectListItems(this.serverUrl, this.propertyDescriptor);


if (this.currentValue == undefined) {
const firstOption: any = this.selectList.items[0];

if (firstOption) {
const optionIsObject = typeof (firstOption) == 'object';
this.currentValue = optionIsObject ? firstOption.value : firstOption.toString();
}
}
}
}

async ReloadSelectListFromDeps(e) {
let depValues = {};

for (const dependsOnValue of this.propertyDescriptor.options.context.dependsOnValue) {
const element = dependsOnValue.element;

let value = this.activityModel.properties.find((prop) => {
return prop.name == element;
})
depValues[element] = value.expressions["Literal"];
}

// Need to get the latest value of the component that just changed.
// For this we need to get the value from the event.
depValues[e.currentTarget.id] = e.currentTarget.value;

let options: RuntimeSelectListProviderSettings = {
context: {
...this.propertyDescriptor.options.context,
depValues: depValues
},
runtimeSelectListProviderType: (this.propertyDescriptor.options as RuntimeSelectListProviderSettings).runtimeSelectListProviderType
}

this.selectList = await getSelectListItems(this.serverUrl, {options: options} as ActivityPropertyDescriptor);

const firstOption: any = this.selectList.items[0];
let currentSelectList = await this.awaitElement('#' + this.propertyDescriptor.name);

if (firstOption) {
const optionIsObject = typeof (firstOption) == 'object';
this.currentValue = optionIsObject ? firstOption.value : firstOption.toString();

// Rebuild the dropdown list to avoid issue between dispatchevent vs render() for the content of the HTMLElement.
currentSelectList.innerHTML = "";
for (const prop of this.selectList.items) {
let item: any = prop;
const optionIsObject = typeof (item) == 'object';
const selected = (optionIsObject ? item.value : item.toString()) === this.currentValue;
const option = new Option(optionIsObject ? item.text : item.toString(), optionIsObject ? item.value : item.toString(), selected, selected);
currentSelectList.options.add(option);
}
}
// Dispatch event to the next dependant input.
currentSelectList.dispatchEvent(new Event("change"));
}

awaitElement = async selector => {
while (document.querySelector(selector) === null) {
await new Promise(resolve => requestAnimationFrame(resolve))
}
return document.querySelector(selector);
};

onChange(e: Event) {
const select = (e.target as HTMLSelectElement);
const defaultSyntax = this.propertyDescriptor.defaultSyntax || SyntaxNames.Literal;
Expand All @@ -50,14 +148,13 @@ export class ElsaDropdownProperty {
}

render() {

const propertyDescriptor = this.propertyDescriptor;
const propertyModel = this.propertyModel;
const propertyName = propertyDescriptor.name;
const fieldId = propertyName;
const fieldName = propertyName;
let currentValue = this.currentValue;
const { items } = this.selectList;
const {items} = this.selectList;

if (currentValue == undefined) {
const defaultValue = this.propertyDescriptor.defaultValue;
Expand All @@ -71,8 +168,10 @@ export class ElsaDropdownProperty {
propertyModel={propertyModel}
onDefaultSyntaxValueChanged={e => this.onDefaultSyntaxValueChanged(e)}
single-line={true}>
<select id={fieldId} name={fieldName} onChange={e => this.onChange(e)}
class="elsa-mt-1 elsa-block focus:elsa-ring-blue-500 focus:elsa-border-blue-500 elsa-w-full elsa-shadow-sm sm:elsa-max-w-xs sm:elsa-text-sm elsa-border-gray-300 elsa-rounded-md">
<select id={fieldId}
name={fieldName}
onChange={e => this.onChange(e)}
class="elsa-mt-1 elsa-block focus:elsa-ring-blue-500 focus:elsa-border-blue-500 elsa-w-full elsa-shadow-sm sm:elsa-max-w-xs sm:elsa-text-sm elsa-border-gray-300 elsa-rounded-md">
{items.map(item => {
const optionIsObject = typeof (item) == 'object';
const value = optionIsObject ? item.value : item.toString();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
using System;
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;
using Newtonsoft.Json.Linq;

namespace Elsa.Samples.Server.Host.Activities
{
[Action]
public class DynamicVehicleActivity : Activity, IActivityPropertyOptionsProvider, IRuntimeSelectListProvider
{
private readonly Random _random;

public DynamicVehicleActivity()
{
_random = new Random();
}

[ActivityInput(
UIHint = ActivityInputUIHints.Dropdown,
OptionsProvider = typeof(DynamicVehicleActivity),
DefaultSyntax = SyntaxNames.Literal,
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.Json, SyntaxNames.JavaScript, SyntaxNames.Liquid }
)]
public string? Brand { get; set; }

[ActivityInput(
UIHint = ActivityInputUIHints.Dropdown,
OptionsProvider = typeof(DynamicVehicleActivity),
DefaultSyntax = SyntaxNames.Literal,
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.Json, SyntaxNames.JavaScript, SyntaxNames.Liquid },
DependsOnEvents = new[] { "Brand" },
DependsOnValues = new[] { "Brand" }
)]
public string? Model { get; set; }

[ActivityInput(
UIHint = ActivityInputUIHints.Dropdown,
OptionsProvider = typeof(DynamicVehicleActivity),
DefaultSyntax = SyntaxNames.Literal,
SupportedSyntaxes = new[] { SyntaxNames.Literal, SyntaxNames.Json, SyntaxNames.JavaScript, SyntaxNames.Liquid },
DependsOnEvents = new[] { "Model" },
DependsOnValues = new[] { "Model", "Brand" }
)]
public string? Color { get; set; }

[ActivityOutput] public string? Output { get; set; }

/// <summary>
/// Return options to be used by the designer. The designer will pass back whatever context is provided here.
/// </summary>
public object GetOptions(PropertyInfo property) => new RuntimeSelectListProviderSettings(GetType(),
new CascadingDropDownContext(property.Name,
property.GetCustomAttribute<ActivityInputAttribute>()!.DependsOnEvents!,
property.GetCustomAttribute<ActivityInputAttribute>()!.DependsOnValues!
, new Dictionary<string, string>(), new DynamicVehicleContext(_random.Next(100))));

/// <summary>
/// Invoked from an API endpoint that is invoked by the designer every time an activity editor for this activity is opened.
/// </summary>
/// <param name="context">The context from GetOptions</param>
public ValueTask<SelectList> GetSelectListAsync(object? context = default, CancellationToken cancellationToken = default)
{
var cascadingDropDownContext = (CascadingDropDownContext)context!;
var vehicleContext = ((JObject)cascadingDropDownContext.Context!).ToObject<DynamicVehicleContext>()!;

if (cascadingDropDownContext.Name == "Brand")
{
var brands = new[] { "BMW", "Peugeot", "Tesla", vehicleContext.RandomNumber.ToString() };
var items = brands.Select(x => new SelectListItem(x)).ToList();
return new ValueTask<SelectList>(new SelectList(items));
}

if (cascadingDropDownContext.Name == "Model")
{
if (cascadingDropDownContext.DepValues != null! &&
cascadingDropDownContext.DepValues.TryGetValue("Brand", out var brandValue))
{
if (brandValue == "BMW")
return new ValueTask<SelectList>(
new SelectList((new[] { "1 Series", "2 Series", "i3", "i4", "i5" }).Select(x => new SelectListItem(x)).ToList())
);
if (brandValue == "Tesla")
return new ValueTask<SelectList>(
new SelectList((new[] { "Roadster", "Model S", "Model 3", "Model X", "Model Y__", "Cybertruck" }).Select(x => new SelectListItem(x)).ToList())
);
if (brandValue == "Peugeot")
return new ValueTask<SelectList>(
new SelectList((new[] { "208", "301", "508", "2008" }).Select(x => new SelectListItem(x)).ToList())
);
}
}

if (cascadingDropDownContext.Name == "Color")
{
if (cascadingDropDownContext.DepValues != null)
{
cascadingDropDownContext.DepValues.TryGetValue("Brand", out var brandValue);
cascadingDropDownContext.DepValues.TryGetValue("Model", out var modelValue);

if (brandValue == "Tesla")
{
if (modelValue == "Model S")
return new ValueTask<SelectList>(
new SelectList((new[] { "White", "Black" }).Select(x => new SelectListItem(x)).ToList())
);
if (modelValue == "Model X")
return new ValueTask<SelectList>(
new SelectList((new[] { "Blue", "Red" }).Select(x => new SelectListItem(x)).ToList())
);
else
return new ValueTask<SelectList>(
new SelectList((new[] { "Purple", "Brown" }).Select(x => new SelectListItem(x)).ToList())
);
}

if (brandValue == "BMW")
{
return new ValueTask<SelectList>(
new SelectList((new[] { "Purple Silk metallic", "Java Green metallic", "Macao Blue" }).Select(x => new SelectListItem(x)).ToList())
);
}

return new ValueTask<SelectList>(
new SelectList((new[] { "default color" }).Select(x => new SelectListItem(x)).ToList())
);
}
}

return new ValueTask<SelectList>();
}

protected override IActivityExecutionResult OnExecute()
{
Output = Brand;
return Done();
}
}

public record DynamicVehicleContext(int RandomNumber);

public record CascadingDropDownContext(string Name, string[] DependsOnEvent, string[] DependsOnValue, IDictionary<string, string> DepValues, object? Context);
}

0 comments on commit 2583e8d

Please sign in to comment.