Incremental work on activity + connection rendering

This commit is contained in:
Sipke Schoorstra 2020-11-15 15:52:24 +01:00
parent 80912563d8
commit 7c913601c0
19 changed files with 267 additions and 208 deletions

View file

@ -41491,13 +41491,17 @@ div.tree {
}
.tree ul {
padding-top: 60px;
padding-top: 1.5rem;
position: relative;
transition: all 0.5s;
-webkit-transition: all 0.5s;
-moz-transition: all 0.5s;
}
.tree ul.root {
padding-top: 0;
}
.tree li {
display: inline-block;
vertical-align: top;

View file

@ -289,13 +289,13 @@
<div class="canvas select-none">
<div class="tree">
<ul>
<ul class="root">
<li>
<div class="inline-flex flex flex-col items-center">
<button id="start-button" type="button" class="px-6 py-3 border border-transparent text-base leading-6 font-medium rounded-md text-white bg-green-600 hover:bg-green-500 focus:outline-none focus:border-green-700 focus:shadow-outline-green active:bg-green-700 transition ease-in-out duration-150">
Start
</button>
<div id="start-button-plus" class="mt-6">
<div id="start-button-plus" class="my-6">
<svg class="h-8 w-8 text-gray-400 hover:text-blue-500 cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>

View file

@ -24,13 +24,17 @@ div.tree {
}
.tree ul {
padding-top: 60px;
@apply pt-6;
position: relative;
transition: all 0.5s;
-webkit-transition: all 0.5s;
-moz-transition: all 0.5s;
}
.tree ul.root {
@apply pt-0;
}
.tree li {
display: inline-block;
vertical-align: top;

View file

@ -25,14 +25,14 @@
<div class="canvas select-none">
<div class="tree">
<ul>
<ul class="root">
<li>
<div class="inline-flex flex flex-col items-center">
<button id="start-button" type="button"
class="px-6 py-3 border border-transparent text-base leading-6 font-medium rounded-md text-white bg-green-600 hover:bg-green-500 focus:outline-none focus:border-green-700 focus:shadow-outline-green active:bg-green-700 transition ease-in-out duration-150">
Start
</button>
<div id="start-button-plus" class="mt-6">
<div id="start-button-plus" class="my-6">
<svg class="h-8 w-8 text-gray-400 hover:text-blue-500 cursor-pointer" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"

View file

@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.Linq;
using ElsaDashboard.Application.Models;
namespace ElsaDashboard.Application.Extensions
{
public static class WorkflowModelExtensions
{
public static IEnumerable<ActivityModel> GetChildActivities(this WorkflowModel workflowModel, string? parentId)
{
if (parentId == null)
{
var targetIds = workflowModel.Connections.Select(x => x.TargetId).Distinct().ToLookup(x => x);
var children = workflowModel.Activities.Where(x => !targetIds.Contains(x.ActivityId)).ToList();
return children;
}
else
{
var targetIds = workflowModel.Connections.Where(x => x.SourceId == parentId).Select(x => x.TargetId).Distinct().ToLookup(x => x);
var children = workflowModel.Activities.Where(x => targetIds.Contains(x.ActivityId)).ToList();
return children;
}
}
}
}

View file

@ -0,0 +1,13 @@
namespace ElsaDashboard.Application.Models
{
public record ActivityModel
{
public ActivityModel()
{
}
public ActivityModel(string activityId, string type) => (ActivityId, Type) = (activityId, type);
public string ActivityId { get; init; }
public string Type { get; init; }
}
}

View file

@ -0,0 +1,4 @@
namespace ElsaDashboard.Application.Models
{
public sealed record ConnectionModel(string SourceId, string? TargetId, string Outcome);
}

View file

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Elsa.Client.Models;
namespace ElsaDashboard.Application.Models
{
public sealed record WorkflowModel
{
public WorkflowModel()
{
}
public WorkflowModel(string? name, IEnumerable<ActivityModel> activities, IEnumerable<ConnectionModel> connections) => (Name, Activities, Connections) = (name, activities.ToImmutableList(), connections.ToImmutableList());
public WorkflowModel(string? name) => Name = (name);
public string? Name { get; init; }
public IImmutableList<ActivityModel> Activities { get; init; } = Array.Empty<ActivityModel>().ToImmutableList();
public IImmutableList<ConnectionModel> Connections { get; init; } = Array.Empty<ConnectionModel>().ToImmutableList();
public static WorkflowModel Blank() => new("New Workflow");
public static WorkflowModel Demo() => new WorkflowModel
{
Activities = new []
{
new ActivityModel("activity-1", "WriteLine"),
new ActivityModel("activity-2", "WriteLine"),
new ActivityModel("activity-3", "WriteLine"),
new ActivityModel("activity-4", "WriteLine"),
new ActivityModel("activity-5", "WriteLine"),
}.ToImmutableList(),
Connections = new[]
{
new ConnectionModel("activity-1", "activity-2", "Done"),
new ConnectionModel("activity-2", "activity-3", "Done"),
new ConnectionModel("activity-2", "activity-4", "Done"),
new ConnectionModel("activity-4", "activity-5", "Done")
}.ToImmutableList()
};
public WorkflowModel AddActivity(ActivityModel activity) => this with { Activities = Activities.Add(activity)};
public WorkflowModel RemoveActivity(ActivityModel activity) => this with { Activities = Activities.Where(x => x != activity).ToImmutableList()};
}
}

View file

@ -0,0 +1,16 @@
@page "/designer"
<div class="flex-0 border-b border-gray-200 px-4 py-4 sm:flex sm:items-center sm:justify-between sm:px-6 lg:px-8">
<div class="flex-1 min-w-0">
<h1 class="text-lg font-medium leading-6 text-gray-900 sm:truncate">
Workflow Editor
</h1>
</div>
<div class="mt-4 flex sm:mt-0 sm:ml-4">
<span class="order-0 sm:order-1 sm:ml-3 shadow-sm rounded-md">
<button type="button" class="inline-flex items-center px-4 py-2 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-blue-500 hover:bg-blue-400 focus:outline-none focus:shadow-outline-blue focus:border-blue-700 active:bg-blue-700 transition duration-150 ease-in-out">
Publish
</button>
</span>
</div>
</div>
<WorkflowDesigner />

View file

@ -0,0 +1,6 @@
namespace ElsaDashboard.Application.Pages
{
partial class Designer
{
}
}

View file

@ -1,197 +0,0 @@
@implements IAsyncDisposable
@page "/designer"
<div class="flex-0 border-b border-gray-200 px-4 py-4 sm:flex sm:items-center sm:justify-between sm:px-6 lg:px-8">
<div class="flex-1 min-w-0">
<h1 class="text-lg font-medium leading-6 text-gray-900 sm:truncate">
Workflow Editor
</h1>
</div>
<div class="mt-4 flex sm:mt-0 sm:ml-4">
<span class="order-0 sm:order-1 sm:ml-3 shadow-sm rounded-md">
<button type="button" class="inline-flex items-center px-4 py-2 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-blue-500 hover:bg-blue-400 focus:outline-none focus:shadow-outline-blue focus:border-blue-700 active:bg-blue-700 transition duration-150 ease-in-out">
Publish
</button>
</span>
</div>
</div>
<div class="relative">
<div id="workflow-canvas" class="flex-1 flex">
<div class="flex-1 text-gray-200">
<div class="p-10">
<div class="canvas select-none">
<div class="tree">
<ul>
<li>
<div class="inline-flex flex flex-col items-center">
<button id="start-button" type="button"
class="px-6 py-3 border border-transparent text-base leading-6 font-medium rounded-md text-white bg-green-600 hover:bg-green-500 focus:outline-none focus:border-green-700 focus:shadow-outline-green active:bg-green-700 transition ease-in-out duration-150"
@onclick="@(async e => await OnStartClick())">
Start
</button>
<div id="start-button-plus" class="mt-6">
<svg class="h-8 w-8 text-gray-400 hover:text-blue-500 cursor-pointer" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
</div>
<ul>
<li>
<Activity ActivityId="activity-2" OnClick="@(async e => await OnActivityClick(e))"/>
<ul>
<li>
<Activity ActivityId="activity-3"/>
<ul>
<li>
<div class="blockcondition blockyes ">
<span style="padding: 6px;">Yes</span>
</div>
<ul>
<li>
<Activity ActivityId="activity-4a"/>
<ul>
<li>
<Activity ActivityId="activity-5"/>
<ul>
<li>
<div class="blockcondition blockno ">
<span style="padding: 6px;">No</span>
</div>
<ul>
<li>
<Activity ActivityId="activity-6"/>
</li>
</ul>
</li>
<li>
<div class="blockcondition blockyes ">
<span style="padding: 6px;">Yes</span>
</div>
<ul>
<li>
<Activity ActivityId="activity-7"/>
<ul>
<li>
<div class="blockcondition blockyes ">
<span style="padding: 6px;">Yes</span>
</div>
<ul>
<li>
<Activity ActivityId="activity-8"/>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<div class="blockcondition blockno ">
<span style="padding: 6px;">No</span>
</div>
<ul>
<li>
<Activity ActivityId="activity-4b"/>
<ul>
<li>
<module href="./src/partials/activity.html"></module>
<ul>
<li>
<div class="blockcondition blockno ">
<span style="padding: 6px;">No</span>
</div>
<ul>
<li>
<Activity ActivityId="activity-9"/>
<ul>
<li>
<div class="blockcondition blockyes ">
<span style="padding: 6px;">Yes</span>
</div>
<ul>
<li>
<Activity ActivityId="activity-10"/>
<ul>
<li>
<Activity ActivityId="activity-11"/>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<div class="blockcondition blockyes ">
<span style="padding: 6px;">Yes</span>
</div>
<ul>
<li>
<Activity ActivityId="activity-12"/>
<ul>
<li>
<div class="blockcondition blockyes ">
<span style="padding: 6px;">Yes</span>
</div>
<ul>
<li>
<Activity ActivityId="activity-13"/>
<ul>
<li>
<div class="blockcondition blockyes ">
<span style="padding: 6px;">+</span>
</div>
<ul>
<li>
<Activity ActivityId="activity-14"/>
<ul>
<li>
<Activity ActivityId="activity-15"/>
<ul>
<li>
<Activity ActivityId="activity-16"/>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<FlyoutPanel/>
</div>

View file

@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
namespace ElsaDashboard.Application.Pages.Designer
namespace ElsaDashboard.Application.Shared
{
partial class Activity
{

View file

@ -1,7 +1,7 @@
using Elsa.Client.Models;
using Microsoft.AspNetCore.Components;
namespace ElsaDashboard.Application.Pages.Designer
namespace ElsaDashboard.Application.Shared
{
partial class ActivityEditor
{

View file

@ -7,7 +7,7 @@ using ElsaDashboard.Application.Models;
using ElsaDashboard.Shared.Rpc;
using Microsoft.AspNetCore.Components;
namespace ElsaDashboard.Application.Pages.Designer
namespace ElsaDashboard.Application.Shared
{
partial class ActivityPicker
{

View file

@ -0,0 +1,83 @@
@using ElsaDashboard.Application.Extensions
<div class="relative">
<div id="workflow-canvas" class="flex-1 flex">
<div class="flex-1 text-gray-200">
<div class="p-10">
<div class="canvas select-none">
<div class="tree">
<ul>
<li>
<div class="inline-flex flex flex-col items-center">
<button id="start-button"
type="button"
class="px-6 py-3 border border-transparent text-base leading-6 font-medium rounded-md text-white bg-green-600 hover:bg-green-500 focus:outline-none focus:border-green-700 focus:shadow-outline-green active:bg-green-700 transition ease-in-out duration-150"
@onclick="@(async e => await OnStartClick())">
Start
</button>
</div>
@RenderTree(GetRootActivities(), true)
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<FlyoutPanel/>
</div>
@code {
private RenderFragment RenderTree(IEnumerable<ActivityModel> activities, bool isRoot)
{
var list = activities.ToList();
var cssClass = isRoot ? "root" : default;
return
@<text>
@if (list.Any())
{
<ul class="@cssClass">
@foreach (var activity in list)
{
var activityId = activity.ActivityId;
var plusId = $"{activityId}-button-plus";
var children = Model.GetChildActivities(activity.ActivityId).ToList();
<li @key=@activity.ActivityId>
<div class="inline-flex flex flex-col items-center">
@if (isRoot)
{
var beforePlusId = $"start-button-plus-{activity.ActivityId}";
<div id="@beforePlusId" class="my-6">
<svg class="h-8 w-8 text-gray-400 hover:text-blue-500 cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
}
<Activity ActivityId="@activity.ActivityId"/>
@if (children.Any())
{
<div id="@plusId" class="mt-6">
<svg class="h-8 w-8 text-gray-400 hover:text-blue-500 cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3m0 0v3m0-3h3m-3 0H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
}
</div>
@if (children.Any())
{
@RenderTree(children, false)
}
</li>
}
</ul>
}
</text>;
}
}

View file

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Elsa.Client.Models;
using ElsaDashboard.Application.Extensions;
@ -8,10 +10,11 @@ using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
namespace ElsaDashboard.Application.Pages.Designer
namespace ElsaDashboard.Application.Shared
{
partial class Designer
partial class WorkflowDesigner : IAsyncDisposable
{
[Parameter] public WorkflowModel Model { private get; set; } = WorkflowModel.Demo();
[Inject] private IJSRuntime JS { get; set; } = default!;
[Inject] private IFlyoutPanelService FlyoutPanelService { get; set; } = default!;
private IJSObjectReference _designerModule = default!;
@ -21,7 +24,10 @@ namespace ElsaDashboard.Application.Pages.Designer
if (firstRender)
{
_designerModule = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/ElsaDashboard.Application/workflowDesigner.js");
}
await RepaintConnections();
}
public async ValueTask DisposeAsync()
@ -30,6 +36,50 @@ namespace ElsaDashboard.Application.Pages.Designer
await _designerModule.DisposeAsync();
}
private IEnumerable<ActivityModel> GetRootActivities() => Model.GetChildActivities(null);
private async ValueTask RepaintConnections()
{
var rootActivities = Model.GetChildActivities(null).ToList();
var rootConnections = rootActivities.SelectMany(x =>
{
return new[]
{
new
{
sourceId = "start-button",
targetId = $"start-button-plus-{x.ActivityId}"
},
new
{
sourceId = $"start-button-plus-{x.ActivityId}",
targetId = x.ActivityId
},
};
}).ToArray();
var connections = Model.Connections.SelectMany(x =>
{
return new[]
{
new
{
sourceId = x.SourceId,
targetId = $"{x.SourceId}-button-plus"
},
new
{
sourceId = $"{x.SourceId}-button-plus",
targetId = x.TargetId
},
};
}).ToArray();
var allConnections = rootConnections.Concat(connections);
await _designerModule.InvokeVoidAsync("updateConnections", (object) allConnections);
}
private async ValueTask ClosePanelAsync() => await FlyoutPanelService.HideAsync();
private async ValueTask ShowActivityPickerAsync()
@ -42,7 +92,10 @@ namespace ElsaDashboard.Application.Pages.Designer
private async ValueTask AddActivityAsync(ActivityDescriptor activityDescriptor)
{
var activity = new ActivityModel(Guid.NewGuid().ToString("N"), activityDescriptor.Type);
Model = Model.AddActivity(activity);
await FlyoutPanelService.HideAsync();
await RepaintConnections();
}
private async ValueTask OnStartClick() => await ShowActivityPickerAsync();